From 22a6e8891ef7b50f0c6a4849ea9f6768c394f7ee Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 16 Aug 2013 00:03:58 -0500 Subject: [PATCH 0001/3494] Add Identity v2 user tests * implement Identity command tests for v2 user Also re-work the user create and set commands for exclusive options (--enable|--disable) to actually behave properly. Yay tests! Change-Id: Ie1ec2569b3d85a9d556ee70f2e8f69fd2a3c03c8 --- openstackclient/identity/v2_0/user.py | 70 ++- .../tests/identity/v2_0/test_users.py | 496 ++++++++++++++++++ 2 files changed, 538 insertions(+), 28 deletions(-) create mode 100644 openstackclient/tests/identity/v2_0/test_users.py diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index 78f2164637..7174d4ce74 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -17,7 +17,6 @@ import logging import six -import sys from cliff import command from cliff import lister @@ -53,15 +52,14 @@ def get_parser(self, prog_name): enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', - dest='enabled', action='store_true', - default=True, - help='Enable user') + help='Enable user (default)', + ) enable_group.add_argument( '--disable', - dest='enabled', - action='store_false', - help='Disable user') + action='store_true', + help='Disable user', + ) return parser def take_action(self, parsed_args): @@ -74,13 +72,19 @@ def take_action(self, parsed_args): ).id else: project_id = None + enabled = True + if parsed_args.disable: + enabled = False user = identity_client.users.create( parsed_args.name, parsed_args.password, parsed_args.email, tenant_id=project_id, - enabled=parsed_args.enabled, + enabled=enabled, ) + # NOTE(dtroyer): The users.create() method wants 'tenant_id' but + # the returned resource has 'tenantId'. Sigh. + # We're using project_id now inside OSC so there. user._info.update( {'project_id': user._info.pop('tenantId')} ) @@ -217,40 +221,50 @@ def get_parser(self, prog_name): enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', - dest='enabled', action='store_true', - default=True, - help='Enable user (default)') + help='Enable user (default)', + ) enable_group.add_argument( '--disable', - dest='enabled', - action='store_false', - help='Disable user') + action='store_true', + help='Disable user', + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity user = utils.find_resource(identity_client.users, parsed_args.user) - kwargs = {} - if parsed_args.name: - kwargs['name'] = parsed_args.name - if parsed_args.email: - kwargs['email'] = parsed_args.email + + if parsed_args.password: + identity_client.users.update_password( + user.id, + parsed_args.password, + ) + if parsed_args.project: project = utils.find_resource( identity_client.tenants, parsed_args.project, ) - kwargs['tenantId'] = project.id - if 'enabled' in parsed_args: - kwargs['enabled'] = parsed_args.enabled - - if not len(kwargs): - sys.stdout.write("User not updated, no arguments present") - return - identity_client.users.update(user.id, **kwargs) - return + identity_client.users.update_tenant( + user.id, + project.id, + ) + + kwargs = {} + if parsed_args.name: + kwargs['name'] = parsed_args.name + if parsed_args.email: + kwargs['email'] = parsed_args.email + if parsed_args.enable: + kwargs['enabled'] = True + if parsed_args.disable: + kwargs['enabled'] = False + + if len(kwargs): + identity_client.users.update(user.id, **kwargs) class ShowUser(show.ShowOne): diff --git a/openstackclient/tests/identity/v2_0/test_users.py b/openstackclient/tests/identity/v2_0/test_users.py new file mode 100644 index 0000000000..49a5262b56 --- /dev/null +++ b/openstackclient/tests/identity/v2_0/test_users.py @@ -0,0 +1,496 @@ +# Copyright 2013 Nebula Inc. +# +# 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 copy + +from openstackclient.identity.v2_0 import user +from openstackclient.tests import fakes +from openstackclient.tests.identity import fakes as identity_fakes +from openstackclient.tests import utils + + +IDENTITY_API_VERSION = "2.0" + +user_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' +user_name = 'paul' +user_description = 'Sir Paul' +user_email = 'paul@applecorps.com' + +project_id = '8-9-64' +project_name = 'beatles' +project_description = 'Fab Four' + +USER = { + 'id': user_id, + 'name': user_name, + 'tenantId': project_id, + 'email': user_email, + 'enabled': True, +} + +PROJECT = { + 'id': project_id, + 'name': project_name, + 'description': project_description, + 'enabled': True, +} + +PROJECT_2 = { + 'id': project_id + '-2222', + 'name': project_name + ' reprise', + 'description': project_description + 'plus four more', + 'enabled': True, +} + + +class TestUser(utils.TestCommand): + + def setUp(self): + super(TestUser, self).setUp() + self.app.client_manager.identity = \ + identity_fakes.FakeIdentityv2Client() + + # Get a shortcut to the TenantManager Mock + self.projects_mock = self.app.client_manager.identity.tenants + # Get a shortcut to the UserManager Mock + self.users_mock = self.app.client_manager.identity.users + + +class TestUserCreate(TestUser): + + def setUp(self): + super(TestUserCreate, self).setUp() + + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(PROJECT), + loaded=True, + ) + self.users_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(USER), + loaded=True, + ) + + # Get the command object to test + self.cmd = user.CreateUser(self.app, None) + + def test_user_create_no_options(self): + arglist = [user_name] + verifylist = [ + ('name', user_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': True, + 'tenant_id': None, + } + # users.create(name, password, email, tenant_id=None, enabled=True) + self.users_mock.create.assert_called_with( + user_name, + None, + None, + **kwargs + ) + + collist = ('email', 'enabled', 'id', 'name', 'project_id') + self.assertEqual(columns, collist) + datalist = (user_email, True, user_id, user_name, project_id) + self.assertEqual(data, datalist) + + def test_user_create_password(self): + arglist = ['--password', 'secret', user_name] + verifylist = [('password', 'secret')] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': True, + 'tenant_id': None, + } + # users.create(name, password, email, tenant_id=None, enabled=True) + self.users_mock.create.assert_called_with( + user_name, + 'secret', + None, + **kwargs + ) + + collist = ('email', 'enabled', 'id', 'name', 'project_id') + self.assertEqual(columns, collist) + datalist = (user_email, True, user_id, user_name, project_id) + self.assertEqual(data, datalist) + + def test_user_create_email(self): + arglist = ['--email', 'barney@example.com', user_name] + verifylist = [('email', 'barney@example.com')] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': True, + 'tenant_id': None, + } + # users.create(name, password, email, tenant_id=None, enabled=True) + self.users_mock.create.assert_called_with( + user_name, + None, + 'barney@example.com', + **kwargs + ) + + collist = ('email', 'enabled', 'id', 'name', 'project_id') + self.assertEqual(columns, collist) + datalist = (user_email, True, user_id, user_name, project_id) + self.assertEqual(data, datalist) + + def test_user_create_project(self): + # Return the new project + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(PROJECT_2), + loaded=True, + ) + # Set up to return an updated user + USER_2 = copy.deepcopy(USER) + USER_2['tenantId'] = PROJECT_2['id'] + self.users_mock.create.return_value = fakes.FakeResource( + None, + USER_2, + loaded=True, + ) + + arglist = ['--project', PROJECT_2['name'], user_name] + verifylist = [('project', PROJECT_2['name'])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': True, + 'tenant_id': PROJECT_2['id'], + } + # users.create(name, password, email, tenant_id=None, enabled=True) + self.users_mock.create.assert_called_with( + user_name, + None, + None, + **kwargs + ) + + collist = ('email', 'enabled', 'id', 'name', 'project_id') + self.assertEqual(columns, collist) + datalist = (user_email, True, user_id, user_name, PROJECT_2['id']) + self.assertEqual(data, datalist) + + def test_user_create_enable(self): + arglist = ['--enable', project_name] + verifylist = [('enable', True), ('disable', False)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': True, + 'tenant_id': None, + } + # users.create(name, password, email, tenant_id=None, enabled=True) + self.users_mock.create.assert_called_with( + project_name, + None, + None, + **kwargs + ) + + collist = ('email', 'enabled', 'id', 'name', 'project_id') + self.assertEqual(columns, collist) + datalist = (user_email, True, user_id, user_name, project_id) + self.assertEqual(data, datalist) + + def test_user_create_disable(self): + arglist = ['--disable', project_name] + verifylist = [('enable', False), ('disable', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': False, + 'tenant_id': None, + } + # users.create(name, password, email, tenant_id=None, enabled=True) + self.users_mock.create.assert_called_with( + project_name, + None, + None, + **kwargs + ) + + collist = ('email', 'enabled', 'id', 'name', 'project_id') + self.assertEqual(columns, collist) + datalist = (user_email, True, user_id, user_name, project_id) + self.assertEqual(data, datalist) + + +class TestUserDelete(TestUser): + + def setUp(self): + super(TestUserDelete, self).setUp() + + # This is the return value for utils.find_resource() + self.users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(USER), + loaded=True, + ) + self.users_mock.delete.return_value = None + + # Get the command object to test + self.cmd = user.DeleteUser(self.app, None) + + def test_user_delete_no_options(self): + arglist = [user_id] + verifylist = [ + ('user', user_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + self.users_mock.delete.assert_called_with(user_id) + + +class TestUserList(TestUser): + + def setUp(self): + super(TestUserList, self).setUp() + + self.users_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(USER), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = user.ListUser(self.app, None) + + def test_user_list_no_options(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.users_mock.list.assert_called_with() + + collist = ('ID', 'Name') + self.assertEqual(columns, collist) + datalist = ((user_id, user_name), ) + self.assertEqual(tuple(data), datalist) + + def test_user_list_project(self): + arglist = ['--project', project_id] + verifylist = [('project', project_id)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.users_mock.list.assert_called_with() + + collist = ('ID', 'Name') + self.assertEqual(columns, collist) + datalist = ((user_id, user_name), ) + self.assertEqual(tuple(data), datalist) + + def test_user_list_long(self): + arglist = ['--long'] + verifylist = [('long', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.users_mock.list.assert_called_with() + + collist = ('ID', 'Name', 'Project', 'Email', 'Enabled') + self.assertEqual(columns, collist) + datalist = ((user_id, user_name, project_id, user_email, True), ) + self.assertEqual(tuple(data), datalist) + + +class TestUserSet(TestUser): + + def setUp(self): + super(TestUserSet, self).setUp() + + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(PROJECT), + loaded=True, + ) + self.users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(USER), + loaded=True, + ) + + # Get the command object to test + self.cmd = user.SetUser(self.app, None) + + def test_user_set_no_options(self): + arglist = [user_name] + verifylist = [ + ('user', user_name), + ('enable', False), + ('disable', False), + ('project', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + self.assertFalse(self.users_mock.update.called) + + def test_user_set_name(self): + arglist = ['--name', 'qwerty', user_name] + verifylist = [('name', 'qwerty')] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': 'qwerty', + } + self.users_mock.update.assert_called_with(user_id, **kwargs) + + def test_user_set_password(self): + arglist = ['--password', 'secret', user_name] + verifylist = [('password', 'secret')] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + self.users_mock.update_password.assert_called_with(user_id, 'secret') + + def test_user_set_email(self): + arglist = ['--email', 'barney@example.com', user_name] + verifylist = [('email', 'barney@example.com')] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'email': 'barney@example.com', + } + self.users_mock.update.assert_called_with(user_id, **kwargs) + + def test_user_set_project(self): + arglist = ['--project', project_id, user_name] + verifylist = [('project', project_id)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + self.users_mock.update_tenant.assert_called_with( + user_id, + project_id, + ) + + def test_user_set_enable(self): + arglist = ['--enable', user_name] + verifylist = [('enable', True), ('disable', False)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': True, + } + self.users_mock.update.assert_called_with(user_id, **kwargs) + + def test_user_set_disable(self): + arglist = ['--disable', user_name] + verifylist = [('enable', False), ('disable', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': False, + } + self.users_mock.update.assert_called_with(user_id, **kwargs) + + +class TestUserShow(TestUser): + + def setUp(self): + super(TestUserShow, self).setUp() + + self.users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(USER), + loaded=True, + ) + + # Get the command object to test + self.cmd = user.ShowUser(self.app, None) + + def test_user_show(self): + arglist = [user_id] + verifylist = [('user', user_id)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.users_mock.get.assert_called_with(user_id) + + collist = ('email', 'enabled', 'id', 'name', 'project_id') + self.assertEqual(columns, collist) + datalist = (user_email, True, user_id, user_name, project_id) + self.assertEqual(data, datalist) From 880323e91d0a71fb599677304fe1a014399ec559 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Wed, 21 Aug 2013 13:13:45 -0500 Subject: [PATCH 0002/3494] Re-order oauth commands and sync with keystoneclient 1) split out token and consumer 2) sync parameters with keystoneclient Change-Id: I2d529f0f9087f9939101e963af3d801497fc1171 --- openstackclient/identity/v3/consumer.py | 157 ++++++++++++++ .../identity/v3/{oauth.py => token.py} | 197 ++++-------------- setup.cfg | 25 +-- 3 files changed, 206 insertions(+), 173 deletions(-) create mode 100644 openstackclient/identity/v3/consumer.py rename openstackclient/identity/v3/{oauth.py => token.py} (53%) diff --git a/openstackclient/identity/v3/consumer.py b/openstackclient/identity/v3/consumer.py new file mode 100644 index 0000000000..ddeae6189a --- /dev/null +++ b/openstackclient/identity/v3/consumer.py @@ -0,0 +1,157 @@ +# Copyright 2012-2013 OpenStack Foundation +# +# 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. +# + +"""Identity v3 Consumer action implementations""" + +import logging +import six +import sys + +from cliff import command +from cliff import lister +from cliff import show + +from openstackclient.common import utils + + +class CreateConsumer(show.ShowOne): + """Create consumer command""" + + log = logging.getLogger(__name__ + '.CreateConsumer') + + def get_parser(self, prog_name): + parser = super(CreateConsumer, self).get_parser(prog_name) + parser.add_argument( + '--description', + metavar='', + help='New consumer description', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity + consumer = identity_client.consumers.create_consumer( + parsed_args.description + ) + info = {} + info.update(consumer._info) + return zip(*sorted(six.iteritems(info))) + + +class DeleteConsumer(command.Command): + """Delete consumer command""" + + log = logging.getLogger(__name__ + '.DeleteConsumer') + + def get_parser(self, prog_name): + parser = super(DeleteConsumer, self).get_parser(prog_name) + parser.add_argument( + 'consumer', + metavar='', + help='ID of consumer to delete', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity + consumer = utils.find_resource( + identity_client.consumers, parsed_args.consumer) + identity_client.consumers.delete_consumer(consumer.id) + return + + +class ListConsumer(lister.Lister): + """List consumer command""" + + log = logging.getLogger(__name__ + '.ListConsumer') + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + columns = ('ID', 'Description') + data = self.app.client_manager.identity.consumers.list_consumers() + return (columns, + (utils.get_item_properties( + s, columns, + formatters={}, + ) for s in data)) + + +class SetConsumer(command.Command): + """Set consumer command""" + + log = logging.getLogger(__name__ + '.SetConsumer') + + def get_parser(self, prog_name): + parser = super(SetConsumer, self).get_parser(prog_name) + parser.add_argument( + 'consumer', + metavar='', + help='ID of consumer to change', + ) + parser.add_argument( + '--description', + metavar='', + help='New consumer description', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity + consumer = utils.find_resource( + identity_client.consumers, parsed_args.consumer) + kwargs = {} + if parsed_args.description: + kwargs['description'] = parsed_args.description + + if not len(kwargs): + sys.stdout.write("Consumer not updated, no arguments present") + return + + consumer = identity_client.consumers.update_consumer( + consumer.id, + **kwargs + ) + + info = {} + info.update(consumer._info) + return zip(*sorted(six.iteritems(info))) + + +class ShowConsumer(show.ShowOne): + """Show consumer command""" + + log = logging.getLogger(__name__ + '.ShowConsumer') + + def get_parser(self, prog_name): + parser = super(ShowConsumer, self).get_parser(prog_name) + parser.add_argument( + 'consumer', + metavar='', + help='ID of consumer to display', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity + consumer = utils.find_resource( + identity_client.consumers, parsed_args.consumer) + + info = {} + info.update(consumer._info) + return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/identity/v3/oauth.py b/openstackclient/identity/v3/token.py similarity index 53% rename from openstackclient/identity/v3/oauth.py rename to openstackclient/identity/v3/token.py index a97f86c714..ba667be3b3 100644 --- a/openstackclient/identity/v3/oauth.py +++ b/openstackclient/identity/v3/token.py @@ -13,11 +13,10 @@ # under the License. # -"""Identity v3 OAuth action implementations""" +"""Identity v3 Token action implementations""" import logging import six -import sys from cliff import command from cliff import lister @@ -62,8 +61,8 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) - oauth_client = self.app.client_manager.identity.oauth - keystone_token = oauth_client.authenticate( + token_client = self.app.client_manager.identity.tokens + keystone_token = token_client.authenticate_access_token( parsed_args.consumer_key, parsed_args.consumer_secret, parsed_args.access_key, parsed_args.access_secret) return zip(*sorted(six.iteritems(keystone_token))) @@ -82,20 +81,14 @@ def get_parser(self, prog_name): help='Consumer key', required=True ) - parser.add_argument( - '--roles', - metavar='', - help='Role to authorize', - required=True - ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) - oauth_client = self.app.client_manager.identity.oauth + token_client = self.app.client_manager.identity.tokens - verifier_pin = oauth_client.authorize_request_token( - parsed_args.request_key, parsed_args.roles) + verifier_pin = token_client.authorize_request_token( + parsed_args.request_key) info = {} info.update(verifier_pin._info) return zip(*sorted(six.iteritems(info))) @@ -142,39 +135,14 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) - oauth_client = self.app.client_manager.identity.oauth - access_token = oauth_client.create_access_token( + token_client = self.app.client_manager.identity.tokens + access_token = token_client.create_access_token( parsed_args.consumer_key, parsed_args.consumer_secret, parsed_args.request_key, parsed_args.request_secret, parsed_args.verifier) return zip(*sorted(six.iteritems(access_token))) -class CreateConsumer(show.ShowOne): - """Create consumer command""" - - log = logging.getLogger(__name__ + '.CreateConsumer') - - def get_parser(self, prog_name): - parser = super(CreateConsumer, self).get_parser(prog_name) - parser.add_argument( - 'name', - metavar='', - help='New consumer name', - ) - return parser - - def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) - identity_client = self.app.client_manager.identity - consumer = identity_client.oauth.create_consumer( - parsed_args.name - ) - info = {} - info.update(consumer._info) - return zip(*sorted(six.iteritems(info))) - - class CreateRequestToken(show.ShowOne): """Create request token command""" @@ -195,61 +163,44 @@ def get_parser(self, prog_name): required=True ) parser.add_argument( - '--roles', - metavar='', - help='Role requested', + '--role-ids', + metavar='', + help='Requested role IDs', + ) + parser.add_argument( + '--project-id', + metavar='', + help='Requested project ID', ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) - oauth_client = self.app.client_manager.identity.oauth - request_token = oauth_client.create_request_token( + token_client = self.app.client_manager.identity.tokens + request_token = token_client.create_request_token( parsed_args.consumer_key, parsed_args.consumer_secret, - parsed_args.roles) + parsed_args.role_ids, + parsed_args.project_id) return zip(*sorted(six.iteritems(request_token))) -class DeleteConsumer(command.Command): - """Delete consumer command""" +class DeleteAccessToken(command.Command): + """Delete access token command""" - log = logging.getLogger(__name__ + '.DeleteConsumer') + log = logging.getLogger(__name__ + '.DeleteAccessToken') def get_parser(self, prog_name): - parser = super(DeleteConsumer, self).get_parser(prog_name) - parser.add_argument( - 'consumer', - metavar='', - help='Name or ID of consumer to delete', - ) - return parser - - def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) - identity_client = self.app.client_manager.identity - consumer = utils.find_resource( - identity_client.oauth, parsed_args.consumer) - identity_client.oauth.delete_consumer(consumer.id) - return - - -class DeleteUserAuthorization(command.Command): - """Delete user authorization command""" - - log = logging.getLogger(__name__ + '.DeleteUserAuthorization') - - def get_parser(self, prog_name): - parser = super(DeleteUserAuthorization, self).get_parser(prog_name) + parser = super(DeleteAccessToken, self).get_parser(prog_name) parser.add_argument( 'user', metavar='', help='Name or Id of user', ) parser.add_argument( - 'access_id', - metavar='', - help='Access Id to be deleted', + 'access_key', + metavar='', + help='Access Token to be deleted', ) return parser @@ -259,34 +210,18 @@ def take_action(self, parsed_args): identity_client = self.app.client_manager.identity user = utils.find_resource( identity_client.users, parsed_args.user).id - identity_client.oauth.delete_authorization(user, - parsed_args.access_id) + identity_client.tokens.delete_access_token(user, + parsed_args.access_key) return -class ListConsumer(lister.Lister): - """List consumer command""" +class ListAccessToken(lister.Lister): + """List access tokens command""" - log = logging.getLogger(__name__ + '.ListConsumer') - - def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) - columns = ('ID', 'Name', 'Consumer Key', 'Consumer Secret') - data = self.app.client_manager.identity.oauth.list_consumers() - return (columns, - (utils.get_item_properties( - s, columns, - formatters={}, - ) for s in data)) - - -class ListUserAuthorizations(lister.Lister): - """List user authorizations command""" - - log = logging.getLogger(__name__ + '.ListUserAuthorizations') + log = logging.getLogger(__name__ + '.ListAccessToken') def get_parser(self, prog_name): - parser = super(ListUserAuthorizations, self).get_parser(prog_name) + parser = super(ListAccessToken, self).get_parser(prog_name) parser.add_argument( 'user', metavar='', @@ -301,71 +236,11 @@ def take_action(self, parsed_args): user = utils.find_resource( identity_client.users, parsed_args.user).id - columns = ('Access Key', 'Consumer Key', 'Issued At', - 'Project Id', 'User Id', 'Requested Roles') - data = identity_client.oauth.list_authorizations(user) + columns = ('ID', 'Consumer ID', 'Expires At', + 'Project Id', 'Authorizing User Id') + data = identity_client.tokens.list_access_tokens(user) return (columns, (utils.get_item_properties( s, columns, formatters={}, ) for s in data)) - - -class SetConsumer(command.Command): - """Set consumer command""" - - log = logging.getLogger(__name__ + '.SetConsumer') - - def get_parser(self, prog_name): - parser = super(SetConsumer, self).get_parser(prog_name) - parser.add_argument( - 'consumer', - metavar='', - help='Name or ID of consumer to change', - ) - parser.add_argument( - '--name', - metavar='', - help='New consumer name', - ) - return parser - - def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) - identity_client = self.app.client_manager.identity - consumer = utils.find_resource( - identity_client.oauth, parsed_args.consumer) - kwargs = {} - if parsed_args.name: - kwargs['name'] = parsed_args.name - - if not len(kwargs): - sys.stdout.write("Consumer not updated, no arguments present") - return - identity_client.oauth.update_consumer(consumer.id, **kwargs) - return - - -class ShowConsumer(show.ShowOne): - """Show consumer command""" - - log = logging.getLogger(__name__ + '.ShowConsumer') - - def get_parser(self, prog_name): - parser = super(ShowConsumer, self).get_parser(prog_name) - parser.add_argument( - 'consumer', - metavar='', - help='Name or ID of consumer to display', - ) - return parser - - def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) - identity_client = self.app.client_manager.identity - consumer = utils.find_resource( - identity_client.oauth, parsed_args.consumer) - - info = {} - info.update(consumer._info) - return zip(*sorted(six.iteritems(info))) diff --git a/setup.cfg b/setup.cfg index 786914eb33..ebfcb9fa87 100644 --- a/setup.cfg +++ b/setup.cfg @@ -70,11 +70,16 @@ openstack.identity.v2_0 = user_show = openstackclient.identity.v2_0.user:ShowUser openstack.identity.v3 = - consumer_create = openstackclient.identity.v3.oauth:CreateConsumer - consumer_delete = openstackclient.identity.v3.oauth:DeleteConsumer - consumer_list = openstackclient.identity.v3.oauth:ListConsumer - consumer_set = openstackclient.identity.v3.oauth:SetConsumer - consumer_show = openstackclient.identity.v3.oauth:ShowConsumer + access_token_authenticate = openstackclient.identity.v3.token:AuthenticateAccessToken + access_token_create = openstackclient.identity.v3.token:CreateAccessToken + access_token_delete = openstackclient.identity.v3.token:DeleteAccessToken + access_token_list = openstackclient.identity.v3.token:ListAccessToken + + consumer_create = openstackclient.identity.v3.consumer:CreateConsumer + consumer_delete = openstackclient.identity.v3.consumer:DeleteConsumer + consumer_list = openstackclient.identity.v3.consumer:ListConsumer + consumer_set = openstackclient.identity.v3.consumer:SetConsumer + consumer_show = openstackclient.identity.v3.consumer:ShowConsumer credential_create = openstackclient.identity.v3.credential:CreateCredential credential_delete = openstackclient.identity.v3.credential:DeleteCredential @@ -103,13 +108,6 @@ openstack.identity.v3 = group_set = openstackclient.identity.v3.group:SetGroup group_show = openstackclient.identity.v3.group:ShowGroup - oauth_access_token_authenticate = openstackclient.identity.v3.oauth:AuthenticateAccessToken - oauth_access_token_create = openstackclient.identity.v3.oauth:CreateAccessToken - oauth_request_token_authorize = openstackclient.identity.v3.oauth:AuthorizeRequestToken - oauth_request_token_create = openstackclient.identity.v3.oauth:CreateRequestToken - oauth_authorization_delete = openstackclient.identity.v3.oauth:DeleteUserAuthorization - oauth_authorization_list = openstackclient.identity.v3.oauth:ListUserAuthorizations - policy_create = openstackclient.identity.v3.policy:CreatePolicy policy_delete = openstackclient.identity.v3.policy:DeletePolicy policy_list = openstackclient.identity.v3.policy:ListPolicy @@ -122,6 +120,9 @@ openstack.identity.v3 = project_set = openstackclient.identity.v3.project:SetProject project_show = openstackclient.identity.v3.project:ShowProject + request_token_authorize = openstackclient.identity.v3.token:AuthorizeRequestToken + request_token_create = openstackclient.identity.v3.token:CreateRequestToken + role_add = openstackclient.identity.v3.role:AddRole role_create = openstackclient.identity.v3.role:CreateRole role_delete = openstackclient.identity.v3.role:DeleteRole From 17f13f7bf4cea80e8e1380fbc8295318de5be383 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 13 Aug 2013 17:14:42 -0500 Subject: [PATCH 0003/3494] Create a new base REST API interface * restapi module provides basic REST API support * uses dicts rather than Resource classes * JSON serialization/deserialization * log requests in 'curl' format * basic API boilerplate for create/delete/list/set/show verbs * ignore H302 due to urllib import Change-Id: I3cb91e44e631ee19e9f5dea19b6bac5d599d19ce --- openstackclient/common/restapi.py | 188 +++++++++++ openstackclient/common/utils.py | 24 ++ openstackclient/shell.py | 5 + openstackclient/tests/common/__init__.py | 14 + openstackclient/tests/common/test_restapi.py | 320 +++++++++++++++++++ tox.ini | 2 +- 6 files changed, 552 insertions(+), 1 deletion(-) create mode 100644 openstackclient/common/restapi.py create mode 100644 openstackclient/tests/common/__init__.py create mode 100644 openstackclient/tests/common/test_restapi.py diff --git a/openstackclient/common/restapi.py b/openstackclient/common/restapi.py new file mode 100644 index 0000000000..4cea5a06cc --- /dev/null +++ b/openstackclient/common/restapi.py @@ -0,0 +1,188 @@ +# Copyright 2013 Nebula Inc. +# +# 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. +# + +"""REST API bits""" + +import json +import logging +import requests + +try: + from urllib.parse import urlencode +except ImportError: + from urllib import urlencode + + +_logger = logging.getLogger(__name__) + + +class RESTApi(object): + """A REST api client that handles the interface from us to the server + + RESTApi is an extension of a requests.Session that knows + how to do: + * JSON serialization/deserialization + * log requests in 'curl' format + * basic API boilerplate for create/delete/list/set/show verbs + + * authentication is handled elsewhere and a token is passed in + + The expectation that there will be a RESTApi object per authentication + token in use, i.e. project/username/auth_endpoint + + On the other hand, a Client knows details about the specific REST Api that + it communicates with, such as the available endpoints, API versions, etc. + """ + + USER_AGENT = 'RAPI' + + def __init__( + self, + os_auth=None, + user_agent=USER_AGENT, + debug=None, + **kwargs + ): + self.set_auth(os_auth) + self.debug = debug + self.session = requests.Session(**kwargs) + + self.set_header('User-Agent', user_agent) + self.set_header('Content-Type', 'application/json') + + def set_auth(self, os_auth): + """Sets the current auth blob""" + self.os_auth = os_auth + + def set_header(self, header, content): + """Sets passed in headers into the session headers + + Replaces existing headers!! + """ + if content is None: + del self.session.headers[header] + else: + self.session.headers[header] = content + + def request(self, method, url, **kwargs): + if self.os_auth: + self.session.headers.setdefault('X-Auth-Token', self.os_auth) + if 'data' in kwargs and isinstance(kwargs['data'], type({})): + kwargs['data'] = json.dumps(kwargs['data']) + log_request(method, url, headers=self.session.headers, **kwargs) + response = self.session.request(method, url, **kwargs) + log_response(response) + return self._error_handler(response) + + def create(self, url, data=None, response_key=None, **kwargs): + response = self.request('POST', url, data=data, **kwargs) + if response_key: + return response.json()[response_key] + else: + return response.json() + + #with self.completion_cache('human_id', self.resource_class, mode="a"): + # with self.completion_cache('uuid', self.resource_class, mode="a"): + # return self.resource_class(self, body[response_key]) + + def delete(self, url): + self.request('DELETE', url) + + def list(self, url, data=None, response_key=None, **kwargs): + if data: + response = self.request('POST', url, data=data, **kwargs) + else: + kwargs.setdefault('allow_redirects', True) + response = self.request('GET', url, **kwargs) + + return response.json()[response_key] + + ###hack this for keystone!!! + #data = body[response_key] + # NOTE(ja): keystone returns values as list as {'values': [ ... ]} + # unlike other services which just return the list... + #if isinstance(data, dict): + # try: + # data = data['values'] + # except KeyError: + # pass + + #with self.completion_cache('human_id', obj_class, mode="w"): + # with self.completion_cache('uuid', obj_class, mode="w"): + # return [obj_class(self, res, loaded=True) + # for res in data if res] + + def set(self, url, data=None, response_key=None, **kwargs): + response = self.request('PUT', url, data=data) + if data: + if response_key: + return response.json()[response_key] + else: + return response.json() + else: + return None + + def show(self, url, response_key=None, **kwargs): + response = self.request('GET', url, **kwargs) + if response_key: + return response.json()[response_key] + else: + return response.json() + + def _error_handler(self, response): + if response.status_code < 200 or response.status_code >= 300: + _logger.debug( + "ERROR: %s", + response.text, + ) + response.raise_for_status() + return response + + +def log_request(method, url, **kwargs): + # put in an early exit if debugging is not enabled? + if 'params' in kwargs and kwargs['params'] != {}: + url += '?' + urlencode(kwargs['params']) + + string_parts = [ + "curl -i", + "-X '%s'" % method, + "'%s'" % url, + ] + + for element in kwargs['headers']: + header = " -H '%s: %s'" % (element, kwargs['headers'][element]) + string_parts.append(header) + + _logger.debug("REQ: %s" % " ".join(string_parts)) + if 'data' in kwargs: + _logger.debug("REQ BODY: %s\n" % (kwargs['data'])) + + +def log_response(response): + _logger.debug( + "RESP: [%s] %s\n", + response.status_code, + response.headers, + ) + if response._content_consumed: + _logger.debug( + "RESP BODY: %s\n", + response.text, + ) + _logger.debug( + "encoding: %s", + response.encoding, + ) diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py index f72bb505a5..91a20895b2 100644 --- a/openstackclient/common/utils.py +++ b/openstackclient/common/utils.py @@ -115,6 +115,30 @@ def get_item_properties(item, fields, mixed_case_fields=[], formatters={}): return tuple(row) +def get_dict_properties(item, fields, mixed_case_fields=[], formatters={}): + """Return a tuple containing the item properties. + + :param item: a single dict resource + :param fields: tuple of strings with the desired field names + :param mixed_case_fields: tuple of field names to preserve case + :param formatters: dictionary mapping field names to callables + to format the values + """ + row = [] + + for field in fields: + if field in mixed_case_fields: + field_name = field.replace(' ', '_') + else: + field_name = field.lower().replace(' ', '_') + data = item[field_name] if field_name in item else '' + if field in formatters: + row.append(formatters[field](data)) + else: + row.append(data) + return tuple(row) + + def string_to_bool(arg): return arg.strip().lower() in ('t', 'true', 'yes', '1') diff --git a/openstackclient/shell.py b/openstackclient/shell.py index b66611b163..91b02a2b0a 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -29,6 +29,7 @@ from openstackclient.common import commandmanager from openstackclient.common import exceptions as exc from openstackclient.common import openstackkeyring +from openstackclient.common import restapi from openstackclient.common import utils @@ -368,6 +369,9 @@ def initialize_app(self, argv): if self.options.deferred_help: self.DeferredHelpAction(self.parser, self.parser, None, None) + # Set up common client session + self.restapi = restapi.RESTApi() + # If the user is not asking for help, make sure they # have given us auth. cmd_name = None @@ -376,6 +380,7 @@ def initialize_app(self, argv): cmd_factory, cmd_name, sub_argv = cmd_info if self.interactive_mode or cmd_name != 'help': self.authenticate_user() + self.restapi.set_auth(self.client_manager.identity.auth_token) def prepare_to_run_command(self, cmd): """Set up auth and API versions""" diff --git a/openstackclient/tests/common/__init__.py b/openstackclient/tests/common/__init__.py new file mode 100644 index 0000000000..c534c012e8 --- /dev/null +++ b/openstackclient/tests/common/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2013 OpenStack Foundation +# +# 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. +# diff --git a/openstackclient/tests/common/test_restapi.py b/openstackclient/tests/common/test_restapi.py new file mode 100644 index 0000000000..4b83ffa460 --- /dev/null +++ b/openstackclient/tests/common/test_restapi.py @@ -0,0 +1,320 @@ +# Copyright 2013 Nebula Inc. +# +# 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. +# + +"""Test rest module""" + +import json +import mock + +import requests + +from openstackclient.common import restapi +from openstackclient.tests import utils + +fake_auth = '11223344556677889900' +fake_url = 'http://gopher.com' +fake_key = 'gopher' +fake_keys = 'gophers' +fake_gopher_mac = { + 'id': 'g1', + 'name': 'mac', + 'actor': 'Mel Blanc', +} +fake_gopher_tosh = { + 'id': 'g2', + 'name': 'tosh', + 'actor': 'Stan Freeberg', +} +fake_gopher_single = { + fake_key: fake_gopher_mac, +} +fake_gopher_list = { + fake_keys: + [ + fake_gopher_mac, + fake_gopher_tosh, + ] +} + + +class FakeResponse(requests.Response): + def __init__(self, headers={}, status_code=None, data=None, encoding=None): + super(FakeResponse, self).__init__() + + self.status_code = status_code + + self.headers.update(headers) + self._content = json.dumps(data) + + +@mock.patch('openstackclient.common.restapi.requests.Session') +class TestRESTApi(utils.TestCase): + + def test_request_get(self, session_mock): + resp = FakeResponse(status_code=200, data=fake_gopher_single) + session_mock.return_value = mock.MagicMock( + request=mock.MagicMock(return_value=resp), + ) + + api = restapi.RESTApi() + gopher = api.request('GET', fake_url) + session_mock.return_value.request.assert_called_with( + 'GET', + fake_url, + ) + self.assertEqual(gopher.status_code, 200) + self.assertEqual(gopher.json(), fake_gopher_single) + + def test_request_get_return_300(self, session_mock): + resp = FakeResponse(status_code=300, data=fake_gopher_single) + session_mock.return_value = mock.MagicMock( + request=mock.MagicMock(return_value=resp), + ) + + api = restapi.RESTApi() + gopher = api.request('GET', fake_url) + session_mock.return_value.request.assert_called_with( + 'GET', + fake_url, + ) + self.assertEqual(gopher.status_code, 300) + self.assertEqual(gopher.json(), fake_gopher_single) + + def test_request_get_fail_404(self, session_mock): + resp = FakeResponse(status_code=404, data=fake_gopher_single) + session_mock.return_value = mock.MagicMock( + request=mock.MagicMock(return_value=resp), + ) + + api = restapi.RESTApi() + self.assertRaises(requests.HTTPError, api.request, 'GET', fake_url) + session_mock.return_value.request.assert_called_with( + 'GET', + fake_url, + ) + + def test_request_get_auth(self, session_mock): + resp = FakeResponse(data=fake_gopher_single) + session_mock.return_value = mock.MagicMock( + request=mock.MagicMock(return_value=resp), + headers=mock.MagicMock(return_value={}), + ) + + api = restapi.RESTApi(os_auth=fake_auth) + gopher = api.request('GET', fake_url) + session_mock.return_value.headers.setdefault.assert_called_with( + 'X-Auth-Token', + fake_auth, + ) + session_mock.return_value.request.assert_called_with( + 'GET', + fake_url, + ) + self.assertEqual(gopher.json(), fake_gopher_single) + + def test_request_get_header(self, session_mock): + resp = FakeResponse(data=fake_gopher_single) + session_mock.return_value = mock.MagicMock( + request=mock.MagicMock(return_value=resp), + headers=mock.MagicMock(return_value={}), + ) + + api = restapi.RESTApi(user_agent='fake_agent') + api.set_header('X-Fake-Header', 'wb') + gopher = api.request('GET', fake_url) + session_mock.return_value.headers.__setitem__.assert_any_call( + 'Content-Type', + 'application/json', + ) + session_mock.return_value.headers.__setitem__.assert_any_call( + 'User-Agent', + 'fake_agent', + ) + session_mock.return_value.headers.__setitem__.assert_any_call( + 'X-Fake-Header', + 'wb', + ) + session_mock.return_value.request.assert_called_with( + 'GET', + fake_url, + ) + self.assertEqual(gopher.json(), fake_gopher_single) + + api.set_header('X-Fake-Header', None) + session_mock.return_value.headers.__delitem__.assert_any_call( + 'X-Fake-Header', + ) + + def test_request_post(self, session_mock): + resp = FakeResponse(data=fake_gopher_single) + session_mock.return_value = mock.MagicMock( + request=mock.MagicMock(return_value=resp), + ) + + api = restapi.RESTApi() + data = fake_gopher_tosh + gopher = api.request('POST', fake_url, data=data) + session_mock.return_value.request.assert_called_with( + 'POST', + fake_url, + data=json.dumps(data), + ) + self.assertEqual(gopher.json(), fake_gopher_single) + + def test_create(self, session_mock): + resp = FakeResponse(data=fake_gopher_single) + session_mock.return_value = mock.MagicMock( + request=mock.MagicMock(return_value=resp), + ) + + api = restapi.RESTApi() + data = fake_gopher_mac + + # Test no key + gopher = api.create(fake_url, data=data) + session_mock.return_value.request.assert_called_with( + 'POST', + fake_url, + data=json.dumps(data), + ) + self.assertEqual(gopher, fake_gopher_single) + + # Test with key + gopher = api.create(fake_url, data=data, response_key=fake_key) + session_mock.return_value.request.assert_called_with( + 'POST', + fake_url, + data=json.dumps(data), + ) + self.assertEqual(gopher, fake_gopher_mac) + + def test_delete(self, session_mock): + resp = FakeResponse(data=None) + session_mock.return_value = mock.MagicMock( + request=mock.MagicMock(return_value=resp), + ) + + api = restapi.RESTApi() + gopher = api.delete(fake_url) + session_mock.return_value.request.assert_called_with( + 'DELETE', + fake_url, + ) + self.assertEqual(gopher, None) + + def test_list(self, session_mock): + resp = FakeResponse(data=fake_gopher_list) + session_mock.return_value = mock.MagicMock( + request=mock.MagicMock(return_value=resp), + ) + + # test base + api = restapi.RESTApi() + gopher = api.list(fake_url, response_key=fake_keys) + session_mock.return_value.request.assert_called_with( + 'GET', + fake_url, + allow_redirects=True, + ) + self.assertEqual(gopher, [fake_gopher_mac, fake_gopher_tosh]) + + # test body + api = restapi.RESTApi() + data = {'qwerty': 1} + gopher = api.list(fake_url, response_key=fake_keys, data=data) + session_mock.return_value.request.assert_called_with( + 'POST', + fake_url, + data=json.dumps(data), + ) + self.assertEqual(gopher, [fake_gopher_mac, fake_gopher_tosh]) + + # test query params + api = restapi.RESTApi() + params = {'qaz': '123'} + gophers = api.list(fake_url, response_key=fake_keys, params=params) + session_mock.return_value.request.assert_called_with( + 'GET', + fake_url, + allow_redirects=True, + params=params, + ) + self.assertEqual(gophers, [fake_gopher_mac, fake_gopher_tosh]) + + def test_set(self, session_mock): + new_gopher = fake_gopher_single + new_gopher[fake_key]['name'] = 'Chip' + resp = FakeResponse(data=fake_gopher_single) + session_mock.return_value = mock.MagicMock( + request=mock.MagicMock(return_value=resp), + ) + + api = restapi.RESTApi() + data = fake_gopher_mac + data['name'] = 'Chip' + + # Test no data, no key + gopher = api.set(fake_url) + session_mock.return_value.request.assert_called_with( + 'PUT', + fake_url, + data=None, + ) + self.assertEqual(gopher, None) + + # Test data, no key + gopher = api.set(fake_url, data=data) + session_mock.return_value.request.assert_called_with( + 'PUT', + fake_url, + data=json.dumps(data), + ) + self.assertEqual(gopher, fake_gopher_single) + + # NOTE:(dtroyer): Key and no data is not tested as without data + # the response_key is moot + + # Test data and key + gopher = api.set(fake_url, data=data, response_key=fake_key) + session_mock.return_value.request.assert_called_with( + 'PUT', + fake_url, + data=json.dumps(data), + ) + self.assertEqual(gopher, fake_gopher_mac) + + def test_show(self, session_mock): + resp = FakeResponse(data=fake_gopher_single) + session_mock.return_value = mock.MagicMock( + request=mock.MagicMock(return_value=resp), + ) + + api = restapi.RESTApi() + + # Test no key + gopher = api.show(fake_url) + session_mock.return_value.request.assert_called_with( + 'GET', + fake_url, + ) + self.assertEqual(gopher, fake_gopher_single) + + # Test with key + gopher = api.show(fake_url, response_key=fake_key) + session_mock.return_value.request.assert_called_with( + 'GET', + fake_url, + ) + self.assertEqual(gopher, fake_gopher_mac) diff --git a/tox.ini b/tox.ini index 61bdc9dec4..e2c61af489 100644 --- a/tox.ini +++ b/tox.ini @@ -23,6 +23,6 @@ commands = python setup.py testr --coverage --testr-args='{posargs}' downloadcache = ~/cache/pip [flake8] -ignore = E126,E202,W602,H402 +ignore = E126,E202,W602,H302,H402 show-source = True exclude = .venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,tools From 725e2543efef8913ec9e69769eb45d5bc3d56aad Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 20 Aug 2013 15:13:41 -0500 Subject: [PATCH 0004/3494] Object API commands using our REST API layer * Add object-store API to ClientManager * Add object-store client * Add Object API library in openstackclient.object.v1.lib * Add Object API {container,object} list commands * Add library tests * Add command tests This should complete the Object v1 container and object list commands Change-Id: Ib1770d45efa8871959826b85faafa1e0bcef0a03 --- openstackclient/common/clientmanager.py | 2 + openstackclient/object/__init__.py | 12 + openstackclient/object/client.py | 58 +++ openstackclient/object/v1/__init__.py | 12 + openstackclient/object/v1/container.py | 100 +++++ openstackclient/object/v1/lib/__init__.py | 12 + openstackclient/object/v1/lib/container.py | 77 ++++ openstackclient/object/v1/lib/object.py | 97 +++++ openstackclient/object/v1/object.py | 118 ++++++ openstackclient/shell.py | 15 +- openstackclient/tests/fakes.py | 5 + openstackclient/tests/object/__init__.py | 12 + openstackclient/tests/object/fakes.py | 67 ++++ .../tests/object/test_container.py | 315 +++++++++++++++ openstackclient/tests/object/test_object.py | 362 ++++++++++++++++++ openstackclient/tests/object/v1/__init__.py | 12 + .../tests/object/v1/lib/__init__.py | 12 + .../tests/object/v1/lib/test_container.py | 159 ++++++++ .../tests/object/v1/lib/test_object.py | 203 ++++++++++ openstackclient/tests/utils.py | 1 + setup.cfg | 4 + 21 files changed, 1653 insertions(+), 2 deletions(-) create mode 100644 openstackclient/object/__init__.py create mode 100644 openstackclient/object/client.py create mode 100644 openstackclient/object/v1/__init__.py create mode 100644 openstackclient/object/v1/container.py create mode 100644 openstackclient/object/v1/lib/__init__.py create mode 100644 openstackclient/object/v1/lib/container.py create mode 100644 openstackclient/object/v1/lib/object.py create mode 100644 openstackclient/object/v1/object.py create mode 100644 openstackclient/tests/object/__init__.py create mode 100644 openstackclient/tests/object/fakes.py create mode 100644 openstackclient/tests/object/test_container.py create mode 100644 openstackclient/tests/object/test_object.py create mode 100644 openstackclient/tests/object/v1/__init__.py create mode 100644 openstackclient/tests/object/v1/lib/__init__.py create mode 100644 openstackclient/tests/object/v1/lib/test_container.py create mode 100644 openstackclient/tests/object/v1/lib/test_object.py diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index fdeca139ec..690cabba7e 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -20,6 +20,7 @@ from openstackclient.compute import client as compute_client from openstackclient.identity import client as identity_client from openstackclient.image import client as image_client +from openstackclient.object import client as object_client from openstackclient.volume import client as volume_client @@ -44,6 +45,7 @@ class ClientManager(object): compute = ClientCache(compute_client.make_client) identity = ClientCache(identity_client.make_client) image = ClientCache(image_client.make_client) + object = ClientCache(object_client.make_client) volume = ClientCache(volume_client.make_client) def __init__(self, token=None, url=None, auth_url=None, project_name=None, diff --git a/openstackclient/object/__init__.py b/openstackclient/object/__init__.py new file mode 100644 index 0000000000..02be10cd89 --- /dev/null +++ b/openstackclient/object/__init__.py @@ -0,0 +1,12 @@ +# 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. +# diff --git a/openstackclient/object/client.py b/openstackclient/object/client.py new file mode 100644 index 0000000000..a83a5c0a3d --- /dev/null +++ b/openstackclient/object/client.py @@ -0,0 +1,58 @@ +# Copyright 2013 Nebula Inc. +# +# 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. +# + +"""Object client""" + +import logging + +from openstackclient.common import utils + +LOG = logging.getLogger(__name__) + +API_NAME = 'object-store' +API_VERSIONS = { + '1': 'openstackclient.object.client.ObjectClientv1', +} + + +def make_client(instance): + """Returns an object service client.""" + object_client = utils.get_client_class( + API_NAME, + instance._api_version[API_NAME], + API_VERSIONS) + if instance._url: + endpoint = instance._url + else: + endpoint = instance.get_endpoint_for_service_type(API_NAME) + LOG.debug('instantiating object client') + client = object_client( + endpoint=endpoint, + token=instance._token, + ) + return client + + +class ObjectClientv1(object): + + def __init__( + self, + endpoint_type='publicURL', + endpoint=None, + token=None, + ): + self.endpoint_type = endpoint_type + self.endpoint = endpoint + self.token = token diff --git a/openstackclient/object/v1/__init__.py b/openstackclient/object/v1/__init__.py new file mode 100644 index 0000000000..02be10cd89 --- /dev/null +++ b/openstackclient/object/v1/__init__.py @@ -0,0 +1,12 @@ +# 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. +# diff --git a/openstackclient/object/v1/container.py b/openstackclient/object/v1/container.py new file mode 100644 index 0000000000..8c4db66ae4 --- /dev/null +++ b/openstackclient/object/v1/container.py @@ -0,0 +1,100 @@ +# Copyright 2013 Nebula Inc. +# +# 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. +# + +"""Container v1 action implementations""" + + +import logging + +from cliff import lister + +from openstackclient.common import utils +from openstackclient.object.v1.lib import container as lib_container + + +class ListContainer(lister.Lister): + """List containers""" + + log = logging.getLogger(__name__ + '.ListContainer') + + def get_parser(self, prog_name): + parser = super(ListContainer, self).get_parser(prog_name) + parser.add_argument( + "--prefix", + metavar="", + help="Filter list using ", + ) + parser.add_argument( + "--marker", + metavar="", + help="Anchor for paging", + ) + parser.add_argument( + "--end-marker", + metavar="", + help="End anchor for paging", + ) + parser.add_argument( + "--limit", + metavar="", + type=int, + help="Limit the number of containers returned", + ) + parser.add_argument( + '--long', + action='store_true', + default=False, + help='List additional fields in output', + ) + parser.add_argument( + '--all', + action='store_true', + default=False, + help='List all containers (default is 10000)', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + + if parsed_args.long: + columns = ('Name', 'Bytes', 'Count') + else: + columns = ('Name',) + + kwargs = {} + if parsed_args.prefix: + kwargs['prefix'] = parsed_args.prefix + if parsed_args.marker: + kwargs['marker'] = parsed_args.marker + if parsed_args.end_marker: + kwargs['end_marker'] = parsed_args.end_marker + if parsed_args.limit: + kwargs['limit'] = parsed_args.limit + if parsed_args.all: + kwargs['full_listing'] = True + + data = lib_container.list_containers( + self.app.restapi, + self.app.client_manager.object.endpoint, + **kwargs + ) + #print "data: %s" % data + + return (columns, + (utils.get_dict_properties( + s, columns, + formatters={}, + ) for s in data)) diff --git a/openstackclient/object/v1/lib/__init__.py b/openstackclient/object/v1/lib/__init__.py new file mode 100644 index 0000000000..02be10cd89 --- /dev/null +++ b/openstackclient/object/v1/lib/__init__.py @@ -0,0 +1,12 @@ +# 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. +# diff --git a/openstackclient/object/v1/lib/container.py b/openstackclient/object/v1/lib/container.py new file mode 100644 index 0000000000..f30533c804 --- /dev/null +++ b/openstackclient/object/v1/lib/container.py @@ -0,0 +1,77 @@ +# Copyright 2010-2012 OpenStack Foundation +# Copyright 2013 Nebula Inc. +# +# 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. +# + +"""Object v1 API library""" + + +def list_containers( + api, + url, + marker=None, + limit=None, + end_marker=None, + prefix=None, + full_listing=False, +): + """Get containers in an account + + :param api: a restapi object + :param url: endpoint + :param marker: marker query + :param limit: limit query + :param end_marker: end_marker query + :param prefix: prefix query + :param full_listing: if True, return a full listing, else returns a max + of 10000 listings + :returns: list of containers + """ + + if full_listing: + data = listing = list_containers( + api, + url, + marker, + limit, + end_marker, + prefix, + ) + while listing: + marker = listing[-1]['name'] + listing = list_containers( + api, + url, + marker, + limit, + end_marker, + prefix, + ) + if listing: + data.extend(listing) + return data + + object_url = url + query = "format=json" + if marker: + query += '&marker=%s' % marker + if limit: + query += '&limit=%d' % limit + if end_marker: + query += '&end_marker=%s' % end_marker + if prefix: + query += '&prefix=%s' % prefix + url = "%s?%s" % (object_url, query) + response = api.request('GET', url) + return response.json() diff --git a/openstackclient/object/v1/lib/object.py b/openstackclient/object/v1/lib/object.py new file mode 100644 index 0000000000..d7c4a1ce37 --- /dev/null +++ b/openstackclient/object/v1/lib/object.py @@ -0,0 +1,97 @@ +# Copyright 2010-2012 OpenStack Foundation +# Copyright 2013 Nebula Inc. +# +# 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. +# + +"""Object v1 API library""" + + +def list_objects( + api, + url, + container, + marker=None, + limit=None, + end_marker=None, + delimiter=None, + prefix=None, + path=None, + full_listing=False, +): + """Get objects in a container + + :param api: a restapi object + :param url: endpoint + :param container: container name to get a listing for + :param marker: marker query + :param limit: limit query + :param end_marker: marker query + :param delimiter: string to delimit the queries on + :param prefix: prefix query + :param path: path query (equivalent: "delimiter=/" and "prefix=path/") + :param full_listing: if True, return a full listing, else returns a max + of 10000 listings + :returns: a tuple of (response headers, a list of objects) The response + headers will be a dict and all header names will be lowercase. + """ + + if full_listing: + data = listing = list_objects( + api, + url, + container, + marker, + limit, + end_marker, + delimiter, + prefix, + path, + ) + while listing: + if delimiter: + marker = listing[-1].get('name', listing[-1].get('subdir')) + else: + marker = listing[-1]['name'] + listing = list_objects( + api, + url, + container, + marker, + limit, + end_marker, + delimiter, + prefix, + path, + ) + if listing: + data.extend(listing) + return data + + object_url = url + query = "format=json" + if marker: + query += '&marker=%s' % marker + if limit: + query += '&limit=%d' % limit + if end_marker: + query += '&end_marker=%s' % end_marker + if delimiter: + query += '&delimiter=%s' % delimiter + if prefix: + query += '&prefix=%s' % prefix + if path: + query += '&path=%s' % path + url = "%s/%s?%s" % (object_url, container, query) + response = api.request('GET', url) + return response.json() diff --git a/openstackclient/object/v1/object.py b/openstackclient/object/v1/object.py new file mode 100644 index 0000000000..c6bd755b32 --- /dev/null +++ b/openstackclient/object/v1/object.py @@ -0,0 +1,118 @@ +# Copyright 2013 Nebula Inc. +# +# 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. +# + +"""Object v1 action implementations""" + + +import logging + +from cliff import lister + +from openstackclient.common import utils +from openstackclient.object.v1.lib import object as lib_object + + +class ListObject(lister.Lister): + """List objects""" + + log = logging.getLogger(__name__ + '.ListObject') + + def get_parser(self, prog_name): + parser = super(ListObject, self).get_parser(prog_name) + parser.add_argument( + "container", + metavar="", + help="List contents of container-name", + ) + parser.add_argument( + "--prefix", + metavar="", + help="Filter list using ", + ) + parser.add_argument( + "--delimiter", + metavar="", + help="Roll up items with ", + ) + parser.add_argument( + "--marker", + metavar="", + help="Anchor for paging", + ) + parser.add_argument( + "--end-marker", + metavar="", + help="End anchor for paging", + ) + parser.add_argument( + "--limit", + metavar="", + type=int, + help="Limit the number of objects returned", + ) + parser.add_argument( + '--long', + action='store_true', + default=False, + help='List additional fields in output', + ) + parser.add_argument( + '--all', + action='store_true', + default=False, + help='List all objects in container (default is 10000)', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + + if parsed_args.long: + columns = ( + 'Name', + 'Bytes', + 'Hash', + 'Content Type', + 'Last Modified', + ) + else: + columns = ('Name',) + + kwargs = {} + if parsed_args.prefix: + kwargs['prefix'] = parsed_args.prefix + if parsed_args.delimiter: + kwargs['delimiter'] = parsed_args.delimiter + if parsed_args.marker: + kwargs['marker'] = parsed_args.marker + if parsed_args.end_marker: + kwargs['end_marker'] = parsed_args.end_marker + if parsed_args.limit: + kwargs['limit'] = parsed_args.limit + if parsed_args.all: + kwargs['full_listing'] = True + + data = lib_object.list_objects( + self.app.restapi, + self.app.client_manager.object.endpoint, + parsed_args.container, + **kwargs + ) + + return (columns, + (utils.get_dict_properties( + s, columns, + formatters={}, + ) for s in data)) diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 91b02a2b0a..6cb7c1ee2c 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -38,6 +38,7 @@ DEFAULT_COMPUTE_API_VERSION = '2' DEFAULT_IDENTITY_API_VERSION = '2.0' DEFAULT_IMAGE_API_VERSION = '1' +DEFAULT_OBJECT_API_VERSION = '1' DEFAULT_VOLUME_API_VERSION = '1' DEFAULT_DOMAIN = 'default' @@ -187,6 +188,15 @@ def build_option_parser(self, description, version): help='Image API version, default=' + DEFAULT_IMAGE_API_VERSION + ' (Env: OS_IMAGE_API_VERSION)') + parser.add_argument( + '--os-object-api-version', + metavar='', + default=env( + 'OS_OBJECT_API_VERSION', + default=DEFAULT_OBJECT_API_VERSION), + help='Object API version, default=' + + DEFAULT_OBJECT_API_VERSION + + ' (Env: OS_OBJECT_API_VERSION)') parser.add_argument( '--os-volume-api-version', metavar='', @@ -339,14 +349,15 @@ def initialize_app(self, argv): 'compute': self.options.os_compute_api_version, 'identity': self.options.os_identity_api_version, 'image': self.options.os_image_api_version, + 'object-store': self.options.os_object_api_version, 'volume': self.options.os_volume_api_version, } # Add the API version-specific commands for api in self.api_version.keys(): version = '.v' + self.api_version[api].replace('.', '_') - self.command_manager.add_command_group( - 'openstack.' + api + version) + cmd_group = 'openstack.' + api.replace('-', '_') + version + self.command_manager.add_command_group(cmd_group) # Commands that span multiple APIs self.command_manager.add_command_group( diff --git a/openstackclient/tests/fakes.py b/openstackclient/tests/fakes.py index d0cd4a9f81..e0122022a8 100644 --- a/openstackclient/tests/fakes.py +++ b/openstackclient/tests/fakes.py @@ -44,6 +44,11 @@ def __init__(self): pass +class FakeRESTApi(object): + def __init__(self): + pass + + class FakeResource(object): def __init__(self, manager, info, loaded=False): self.manager = manager diff --git a/openstackclient/tests/object/__init__.py b/openstackclient/tests/object/__init__.py new file mode 100644 index 0000000000..02be10cd89 --- /dev/null +++ b/openstackclient/tests/object/__init__.py @@ -0,0 +1,12 @@ +# 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. +# diff --git a/openstackclient/tests/object/fakes.py b/openstackclient/tests/object/fakes.py new file mode 100644 index 0000000000..fbc784aae5 --- /dev/null +++ b/openstackclient/tests/object/fakes.py @@ -0,0 +1,67 @@ +# Copyright 2013 Nebula Inc. +# +# 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. +# + +container_name = 'bit-bucket' +container_bytes = 1024 +container_count = 1 + +container_name_2 = 'archive' +container_name_3 = 'bit-blit' + +CONTAINER = { + 'name': container_name, + 'bytes': container_bytes, + 'count': container_count, +} + +CONTAINER_2 = { + 'name': container_name_2, + 'bytes': container_bytes * 2, + 'count': container_count * 2, +} + +CONTAINER_3 = { + 'name': container_name_3, + 'bytes': container_bytes * 3, + 'count': container_count * 3, +} + +object_name_1 = 'punch-card' +object_bytes_1 = 80 +object_hash_1 = '1234567890' +object_content_type_1 = 'text' +object_modified_1 = 'today' + +object_name_2 = 'floppy-disk' +object_bytes_2 = 1440000 +object_hash_2 = '0987654321' +object_content_type_2 = 'text' +object_modified_2 = 'today' + +OBJECT = { + 'name': object_name_1, + 'bytes': object_bytes_1, + 'hash': object_hash_1, + 'content_type': object_content_type_1, + 'last_modified': object_modified_1, +} + +OBJECT_2 = { + 'name': object_name_2, + 'bytes': object_bytes_2, + 'hash': object_hash_2, + 'content_type': object_content_type_2, + 'last_modified': object_modified_2, +} diff --git a/openstackclient/tests/object/test_container.py b/openstackclient/tests/object/test_container.py new file mode 100644 index 0000000000..9b53e36037 --- /dev/null +++ b/openstackclient/tests/object/test_container.py @@ -0,0 +1,315 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 copy +import mock + +from openstackclient.common import clientmanager +from openstackclient.object.v1 import container +from openstackclient.tests.object import fakes as object_fakes +from openstackclient.tests import utils + + +AUTH_TOKEN = "foobar" +AUTH_URL = "http://0.0.0.0" + + +class FakeClient(object): + def __init__(self, endpoint=None, **kwargs): + self.endpoint = AUTH_URL + self.token = AUTH_TOKEN + + +class TestObject(utils.TestCommand): + def setUp(self): + super(TestObject, self).setUp() + + api_version = {"object-store": "1"} + self.app.client_manager = clientmanager.ClientManager( + token=AUTH_TOKEN, + url=AUTH_URL, + auth_url=AUTH_URL, + api_version=api_version, + ) + + +class TestObjectClient(TestObject): + + def test_make_client(self): + self.assertEqual(self.app.client_manager.object.endpoint, AUTH_URL) + self.assertEqual(self.app.client_manager.object.token, AUTH_TOKEN) + + +@mock.patch( + 'openstackclient.object.v1.container.lib_container.list_containers' +) +class TestContainerList(TestObject): + + def setUp(self): + super(TestContainerList, self).setUp() + + # Get the command object to test + self.cmd = container.ListContainer(self.app, None) + + def test_object_list_containers_no_options(self, c_mock): + c_mock.return_value = [ + copy.deepcopy(object_fakes.CONTAINER), + copy.deepcopy(object_fakes.CONTAINER_3), + copy.deepcopy(object_fakes.CONTAINER_2), + ] + + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + } + c_mock.assert_called_with( + self.app.restapi, + AUTH_URL, + **kwargs + ) + + collist = ('Name',) + self.assertEqual(columns, collist) + datalist = ( + (object_fakes.container_name, ), + (object_fakes.container_name_3, ), + (object_fakes.container_name_2, ), + ) + self.assertEqual(tuple(data), datalist) + + def test_object_list_containers_prefix(self, c_mock): + c_mock.return_value = [ + copy.deepcopy(object_fakes.CONTAINER), + copy.deepcopy(object_fakes.CONTAINER_3), + ] + + arglist = [ + '--prefix', 'bit', + ] + verifylist = [ + ('prefix', 'bit'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'prefix': 'bit', + } + c_mock.assert_called_with( + self.app.restapi, + AUTH_URL, + **kwargs + ) + + collist = ('Name',) + self.assertEqual(columns, collist) + datalist = ( + (object_fakes.container_name, ), + (object_fakes.container_name_3, ), + ) + self.assertEqual(tuple(data), datalist) + + def test_object_list_containers_marker(self, c_mock): + c_mock.return_value = [ + copy.deepcopy(object_fakes.CONTAINER), + copy.deepcopy(object_fakes.CONTAINER_3), + ] + + arglist = [ + '--marker', object_fakes.container_name, + ] + verifylist = [ + ('marker', object_fakes.container_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'marker': object_fakes.container_name, + } + c_mock.assert_called_with( + self.app.restapi, + AUTH_URL, + **kwargs + ) + + collist = ('Name',) + self.assertEqual(columns, collist) + datalist = ( + (object_fakes.container_name, ), + (object_fakes.container_name_3, ), + ) + self.assertEqual(tuple(data), datalist) + + def test_object_list_containers_end_marker(self, c_mock): + c_mock.return_value = [ + copy.deepcopy(object_fakes.CONTAINER), + copy.deepcopy(object_fakes.CONTAINER_3), + ] + + arglist = [ + '--end-marker', object_fakes.container_name_3, + ] + verifylist = [ + ('end_marker', object_fakes.container_name_3), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'end_marker': object_fakes.container_name_3, + } + c_mock.assert_called_with( + self.app.restapi, + AUTH_URL, + **kwargs + ) + + collist = ('Name',) + self.assertEqual(columns, collist) + datalist = ( + (object_fakes.container_name, ), + (object_fakes.container_name_3, ), + ) + self.assertEqual(tuple(data), datalist) + + def test_object_list_containers_limit(self, c_mock): + c_mock.return_value = [ + copy.deepcopy(object_fakes.CONTAINER), + copy.deepcopy(object_fakes.CONTAINER_3), + ] + + arglist = [ + '--limit', '2', + ] + verifylist = [ + ('limit', 2), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'limit': 2, + } + c_mock.assert_called_with( + self.app.restapi, + AUTH_URL, + **kwargs + ) + + collist = ('Name',) + self.assertEqual(columns, collist) + datalist = ( + (object_fakes.container_name, ), + (object_fakes.container_name_3, ), + ) + self.assertEqual(tuple(data), datalist) + + def test_object_list_containers_long(self, c_mock): + c_mock.return_value = [ + copy.deepcopy(object_fakes.CONTAINER), + copy.deepcopy(object_fakes.CONTAINER_3), + ] + + arglist = [ + '--long', + ] + verifylist = [ + ('long', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + } + c_mock.assert_called_with( + self.app.restapi, + AUTH_URL, + **kwargs + ) + + collist = ('Name', 'Bytes', 'Count') + self.assertEqual(columns, collist) + datalist = ( + ( + object_fakes.container_name, + object_fakes.container_bytes, + object_fakes.container_count, + ), + ( + object_fakes.container_name_3, + object_fakes.container_bytes * 3, + object_fakes.container_count * 3, + ), + ) + self.assertEqual(tuple(data), datalist) + + def test_object_list_containers_all(self, c_mock): + c_mock.return_value = [ + copy.deepcopy(object_fakes.CONTAINER), + copy.deepcopy(object_fakes.CONTAINER_2), + copy.deepcopy(object_fakes.CONTAINER_3), + ] + + arglist = [ + '--all', + ] + verifylist = [ + ('all', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'full_listing': True, + } + c_mock.assert_called_with( + self.app.restapi, + AUTH_URL, + **kwargs + ) + + collist = ('Name',) + self.assertEqual(columns, collist) + datalist = ( + (object_fakes.container_name, ), + (object_fakes.container_name_2, ), + (object_fakes.container_name_3, ), + ) + self.assertEqual(tuple(data), datalist) diff --git a/openstackclient/tests/object/test_object.py b/openstackclient/tests/object/test_object.py new file mode 100644 index 0000000000..ddd5b592a9 --- /dev/null +++ b/openstackclient/tests/object/test_object.py @@ -0,0 +1,362 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 copy +import mock + +from openstackclient.common import clientmanager +from openstackclient.object.v1 import object as obj +from openstackclient.tests.object import fakes as object_fakes +from openstackclient.tests import utils + + +AUTH_TOKEN = "foobar" +AUTH_URL = "http://0.0.0.0" + + +class FakeClient(object): + def __init__(self, endpoint=None, **kwargs): + self.endpoint = AUTH_URL + self.token = AUTH_TOKEN + + +class TestObject(utils.TestCommand): + def setUp(self): + super(TestObject, self).setUp() + + api_version = {"object-store": "1"} + self.app.client_manager = clientmanager.ClientManager( + token=AUTH_TOKEN, + url=AUTH_URL, + auth_url=AUTH_URL, + api_version=api_version, + ) + + +class TestObjectClient(TestObject): + + def test_make_client(self): + self.assertEqual(self.app.client_manager.object.endpoint, AUTH_URL) + self.assertEqual(self.app.client_manager.object.token, AUTH_TOKEN) + + +@mock.patch( + 'openstackclient.object.v1.object.lib_object.list_objects' +) +class TestObjectList(TestObject): + + def setUp(self): + super(TestObjectList, self).setUp() + + # Get the command object to test + self.cmd = obj.ListObject(self.app, None) + + def test_object_list_objects_no_options(self, o_mock): + o_mock.return_value = [ + copy.deepcopy(object_fakes.OBJECT), + copy.deepcopy(object_fakes.OBJECT_2), + ] + + arglist = [ + object_fakes.container_name, + ] + verifylist = [ + ('container', object_fakes.container_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + o_mock.assert_called_with( + self.app.restapi, + AUTH_URL, + object_fakes.container_name, + ) + + collist = ('Name',) + self.assertEqual(columns, collist) + datalist = ( + (object_fakes.object_name_1, ), + (object_fakes.object_name_2, ), + ) + self.assertEqual(tuple(data), datalist) + + def test_object_list_objects_prefix(self, o_mock): + o_mock.return_value = [ + copy.deepcopy(object_fakes.OBJECT_2), + ] + + arglist = [ + '--prefix', 'floppy', + object_fakes.container_name_2, + ] + verifylist = [ + ('prefix', 'floppy'), + ('container', object_fakes.container_name_2), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'prefix': 'floppy', + } + o_mock.assert_called_with( + self.app.restapi, + AUTH_URL, + object_fakes.container_name_2, + **kwargs + ) + + collist = ('Name',) + self.assertEqual(columns, collist) + datalist = ( + (object_fakes.object_name_2, ), + ) + self.assertEqual(tuple(data), datalist) + + def test_object_list_objects_delimiter(self, o_mock): + o_mock.return_value = [ + copy.deepcopy(object_fakes.OBJECT_2), + ] + + arglist = [ + '--delimiter', '=', + object_fakes.container_name_2, + ] + verifylist = [ + ('delimiter', '='), + ('container', object_fakes.container_name_2), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'delimiter': '=', + } + o_mock.assert_called_with( + self.app.restapi, + AUTH_URL, + object_fakes.container_name_2, + **kwargs + ) + + collist = ('Name',) + self.assertEqual(columns, collist) + datalist = ( + (object_fakes.object_name_2, ), + ) + self.assertEqual(tuple(data), datalist) + + def test_object_list_objects_marker(self, o_mock): + o_mock.return_value = [ + copy.deepcopy(object_fakes.OBJECT_2), + ] + + arglist = [ + '--marker', object_fakes.object_name_2, + object_fakes.container_name_2, + ] + verifylist = [ + ('marker', object_fakes.object_name_2), + ('container', object_fakes.container_name_2), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'marker': object_fakes.object_name_2, + } + o_mock.assert_called_with( + self.app.restapi, + AUTH_URL, + object_fakes.container_name_2, + **kwargs + ) + + collist = ('Name',) + self.assertEqual(columns, collist) + datalist = ( + (object_fakes.object_name_2, ), + ) + self.assertEqual(tuple(data), datalist) + + def test_object_list_objects_end_marker(self, o_mock): + o_mock.return_value = [ + copy.deepcopy(object_fakes.OBJECT_2), + ] + + arglist = [ + '--end-marker', object_fakes.object_name_2, + object_fakes.container_name_2, + ] + verifylist = [ + ('end_marker', object_fakes.object_name_2), + ('container', object_fakes.container_name_2), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'end_marker': object_fakes.object_name_2, + } + o_mock.assert_called_with( + self.app.restapi, + AUTH_URL, + object_fakes.container_name_2, + **kwargs + ) + + collist = ('Name',) + self.assertEqual(columns, collist) + datalist = ( + (object_fakes.object_name_2, ), + ) + self.assertEqual(tuple(data), datalist) + + def test_object_list_objects_limit(self, o_mock): + o_mock.return_value = [ + copy.deepcopy(object_fakes.OBJECT_2), + ] + + arglist = [ + '--limit', '2', + object_fakes.container_name_2, + ] + verifylist = [ + ('limit', 2), + ('container', object_fakes.container_name_2), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'limit': 2, + } + o_mock.assert_called_with( + self.app.restapi, + AUTH_URL, + object_fakes.container_name_2, + **kwargs + ) + + collist = ('Name',) + self.assertEqual(columns, collist) + datalist = ( + (object_fakes.object_name_2, ), + ) + self.assertEqual(tuple(data), datalist) + + def test_object_list_objects_long(self, o_mock): + o_mock.return_value = [ + copy.deepcopy(object_fakes.OBJECT), + copy.deepcopy(object_fakes.OBJECT_2), + ] + + arglist = [ + '--long', + object_fakes.container_name, + ] + verifylist = [ + ('long', True), + ('container', object_fakes.container_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + } + o_mock.assert_called_with( + self.app.restapi, + AUTH_URL, + object_fakes.container_name, + **kwargs + ) + + collist = ('Name', 'Bytes', 'Hash', 'Content Type', 'Last Modified') + self.assertEqual(columns, collist) + datalist = ( + ( + object_fakes.object_name_1, + object_fakes.object_bytes_1, + object_fakes.object_hash_1, + object_fakes.object_content_type_1, + object_fakes.object_modified_1, + ), + ( + object_fakes.object_name_2, + object_fakes.object_bytes_2, + object_fakes.object_hash_2, + object_fakes.object_content_type_2, + object_fakes.object_modified_2, + ), + ) + self.assertEqual(tuple(data), datalist) + + def test_object_list_objects_all(self, o_mock): + o_mock.return_value = [ + copy.deepcopy(object_fakes.OBJECT), + copy.deepcopy(object_fakes.OBJECT_2), + ] + + arglist = [ + '--all', + object_fakes.container_name, + ] + verifylist = [ + ('all', True), + ('container', object_fakes.container_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'full_listing': True, + } + o_mock.assert_called_with( + self.app.restapi, + AUTH_URL, + object_fakes.container_name, + **kwargs + ) + + collist = ('Name',) + self.assertEqual(columns, collist) + datalist = ( + (object_fakes.object_name_1, ), + (object_fakes.object_name_2, ), + ) + self.assertEqual(tuple(data), datalist) diff --git a/openstackclient/tests/object/v1/__init__.py b/openstackclient/tests/object/v1/__init__.py new file mode 100644 index 0000000000..02be10cd89 --- /dev/null +++ b/openstackclient/tests/object/v1/__init__.py @@ -0,0 +1,12 @@ +# 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. +# diff --git a/openstackclient/tests/object/v1/lib/__init__.py b/openstackclient/tests/object/v1/lib/__init__.py new file mode 100644 index 0000000000..02be10cd89 --- /dev/null +++ b/openstackclient/tests/object/v1/lib/__init__.py @@ -0,0 +1,12 @@ +# 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. +# diff --git a/openstackclient/tests/object/v1/lib/test_container.py b/openstackclient/tests/object/v1/lib/test_container.py new file mode 100644 index 0000000000..a241cc021f --- /dev/null +++ b/openstackclient/tests/object/v1/lib/test_container.py @@ -0,0 +1,159 @@ +# Copyright 2013 Nebula Inc. +# +# 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. +# + +"""Test Object API library module""" + +from __future__ import unicode_literals + +import mock + + +from openstackclient.object.v1.lib import container as lib_container +from openstackclient.tests.common import test_restapi as restapi +from openstackclient.tests import utils + + +fake_auth = '11223344556677889900' +fake_url = 'http://gopher.com' + +fake_container = 'rainbarrel' + + +class FakeClient(object): + def __init__(self, endpoint=None, **kwargs): + self.endpoint = fake_url + self.token = fake_auth + + +class TestObject(utils.TestCommand): + + def setUp(self): + super(TestObject, self).setUp() + self.app.client_manager.object = FakeClient() + self.app.restapi = mock.MagicMock() + + +class TestObjectListContainers(TestObject): + + def test_list_containers_no_options(self): + resp = [{'name': 'is-name'}] + self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) + + data = lib_container.list_containers( + self.app.restapi, + self.app.client_manager.object.endpoint, + ) + + # Check expected values + self.app.restapi.request.assert_called_with( + 'GET', + fake_url + '?format=json', + ) + self.assertEqual(data, resp) + + def test_list_containers_marker(self): + resp = [{'name': 'is-name'}] + self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) + + data = lib_container.list_containers( + self.app.restapi, + self.app.client_manager.object.endpoint, + marker='next', + ) + + # Check expected values + self.app.restapi.request.assert_called_with( + 'GET', + fake_url + '?format=json&marker=next', + ) + self.assertEqual(data, resp) + + def test_list_containers_limit(self): + resp = [{'name': 'is-name'}] + self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) + + data = lib_container.list_containers( + self.app.restapi, + self.app.client_manager.object.endpoint, + limit=5, + ) + + # Check expected values + self.app.restapi.request.assert_called_with( + 'GET', + fake_url + '?format=json&limit=5', + ) + self.assertEqual(data, resp) + + def test_list_containers_end_marker(self): + resp = [{'name': 'is-name'}] + self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) + + data = lib_container.list_containers( + self.app.restapi, + self.app.client_manager.object.endpoint, + end_marker='last', + ) + + # Check expected values + self.app.restapi.request.assert_called_with( + 'GET', + fake_url + '?format=json&end_marker=last', + ) + self.assertEqual(data, resp) + + def test_list_containers_prefix(self): + resp = [{'name': 'is-name'}] + self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) + + data = lib_container.list_containers( + self.app.restapi, + self.app.client_manager.object.endpoint, + prefix='foo/', + ) + + # Check expected values + self.app.restapi.request.assert_called_with( + 'GET', + fake_url + '?format=json&prefix=foo/', + ) + self.assertEqual(data, resp) + + def test_list_containers_full_listing(self): + + def side_effect(*args, **kwargs): + rv = self.app.restapi.request.return_value + self.app.restapi.request.return_value = restapi.FakeResponse( + data=[], + ) + self.app.restapi.request.side_effect = None + return rv + + resp = [{'name': 'is-name'}] + self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) + self.app.restapi.request.side_effect = side_effect + + data = lib_container.list_containers( + self.app.restapi, + self.app.client_manager.object.endpoint, + full_listing=True, + ) + + # Check expected values + self.app.restapi.request.assert_called_with( + 'GET', + fake_url + '?format=json&marker=is-name', + ) + self.assertEqual(data, resp) diff --git a/openstackclient/tests/object/v1/lib/test_object.py b/openstackclient/tests/object/v1/lib/test_object.py new file mode 100644 index 0000000000..ef8ae18dc5 --- /dev/null +++ b/openstackclient/tests/object/v1/lib/test_object.py @@ -0,0 +1,203 @@ +# Copyright 2013 Nebula Inc. +# +# 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. +# + +"""Test Object API library module""" + +from __future__ import unicode_literals + +import mock + +from openstackclient.object.v1.lib import object as lib_object +from openstackclient.tests.common import test_restapi as restapi +from openstackclient.tests import utils + + +fake_auth = '11223344556677889900' +fake_url = 'http://gopher.com' + +fake_container = 'rainbarrel' + + +class FakeClient(object): + def __init__(self, endpoint=None, **kwargs): + self.endpoint = fake_url + self.token = fake_auth + + +class TestObject(utils.TestCommand): + + def setUp(self): + super(TestObject, self).setUp() + self.app.client_manager.object = FakeClient() + self.app.restapi = mock.MagicMock() + + +class TestObjectListObjects(TestObject): + + def test_list_objects_no_options(self): + resp = [{'name': 'is-name'}] + self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) + + data = lib_object.list_objects( + self.app.restapi, + self.app.client_manager.object.endpoint, + fake_container, + ) + + # Check expected values + self.app.restapi.request.assert_called_with( + 'GET', + fake_url + '/' + fake_container + '?format=json', + ) + self.assertEqual(data, resp) + + def test_list_objects_marker(self): + resp = [{'name': 'is-name'}] + self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) + + data = lib_object.list_objects( + self.app.restapi, + self.app.client_manager.object.endpoint, + fake_container, + marker='next', + ) + + # Check expected values + self.app.restapi.request.assert_called_with( + 'GET', + fake_url + '/' + fake_container + '?format=json&marker=next', + ) + self.assertEqual(data, resp) + + def test_list_objects_limit(self): + resp = [{'name': 'is-name'}] + self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) + + data = lib_object.list_objects( + self.app.restapi, + self.app.client_manager.object.endpoint, + fake_container, + limit=5, + ) + + # Check expected values + self.app.restapi.request.assert_called_with( + 'GET', + fake_url + '/' + fake_container + '?format=json&limit=5', + ) + self.assertEqual(data, resp) + + def test_list_objects_end_marker(self): + resp = [{'name': 'is-name'}] + self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) + + data = lib_object.list_objects( + self.app.restapi, + self.app.client_manager.object.endpoint, + fake_container, + end_marker='last', + ) + + # Check expected values + self.app.restapi.request.assert_called_with( + 'GET', + fake_url + '/' + fake_container + '?format=json&end_marker=last', + ) + self.assertEqual(data, resp) + + def test_list_objects_delimiter(self): + resp = [{'name': 'is-name'}] + self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) + + data = lib_object.list_objects( + self.app.restapi, + self.app.client_manager.object.endpoint, + fake_container, + delimiter='|', + ) + + # Check expected values + # NOTE(dtroyer): requests handles the URL encoding and we're + # mocking that so use the otherwise-not-legal + # pipe '|' char in the response. + self.app.restapi.request.assert_called_with( + 'GET', + fake_url + '/' + fake_container + '?format=json&delimiter=|', + ) + self.assertEqual(data, resp) + + def test_list_objects_prefix(self): + resp = [{'name': 'is-name'}] + self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) + + data = lib_object.list_objects( + self.app.restapi, + self.app.client_manager.object.endpoint, + fake_container, + prefix='foo/', + ) + + # Check expected values + self.app.restapi.request.assert_called_with( + 'GET', + fake_url + '/' + fake_container + '?format=json&prefix=foo/', + ) + self.assertEqual(data, resp) + + def test_list_objects_path(self): + resp = [{'name': 'is-name'}] + self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) + + data = lib_object.list_objects( + self.app.restapi, + self.app.client_manager.object.endpoint, + fake_container, + path='next', + ) + + # Check expected values + self.app.restapi.request.assert_called_with( + 'GET', + fake_url + '/' + fake_container + '?format=json&path=next', + ) + self.assertEqual(data, resp) + + def test_list_objects_full_listing(self): + + def side_effect(*args, **kwargs): + rv = self.app.restapi.request.return_value + self.app.restapi.request.return_value = restapi.FakeResponse( + data=[], + ) + self.app.restapi.request.side_effect = None + return rv + + resp = [{'name': 'is-name'}] + self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) + self.app.restapi.request.side_effect = side_effect + + data = lib_object.list_objects( + self.app.restapi, + self.app.client_manager.object.endpoint, + fake_container, + full_listing=True, + ) + + # Check expected values + self.app.restapi.request.assert_called_with( + 'GET', + fake_url + '/' + fake_container + '?format=json&marker=is-name', + ) + self.assertEqual(data, resp) diff --git a/openstackclient/tests/utils.py b/openstackclient/tests/utils.py index ff7d8a336d..4516318908 100644 --- a/openstackclient/tests/utils.py +++ b/openstackclient/tests/utils.py @@ -71,6 +71,7 @@ def setUp(self): self.fake_stdout = fakes.FakeStdout() self.app = fakes.FakeApp(self.fake_stdout) self.app.client_manager = fakes.FakeClientManager() + self.app.restapi = fakes.FakeRESTApi() def check_parser(self, cmd, args, verify_args): cmd_parser = cmd.get_parser('check_parser') diff --git a/setup.cfg b/setup.cfg index ebfcb9fa87..e6fe8475ca 100644 --- a/setup.cfg +++ b/setup.cfg @@ -239,6 +239,10 @@ openstack.compute.v2 = server_unrescue = openstackclient.compute.v2.server:UnrescueServer server_unset = openstackclient.compute.v2.server:UnsetServer +openstack.object_store.v1 = + container_list = openstackclient.object.v1.container:ListContainer + object_list = openstackclient.object.v1.object:ListObject + openstack.volume.v1 = snapshot_create = openstackclient.volume.v1.snapshot:CreateSnapshot snapshot_delete = openstackclient.volume.v1.snapshot:DeleteSnapshot From 5e29928294f80cb437a9bcf5c459dd2aad1b0d27 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 29 Aug 2013 20:39:34 -0500 Subject: [PATCH 0005/3494] Update requirements.txt and test-requirements.txt Change-Id: I9c60d1d9097d35aa7c3d44168e370a9f30fd6621 --- requirements.txt | 6 +++--- test-requirements.txt | 15 ++++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/requirements.txt b/requirements.txt index 892e33e142..d956a9ebd1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ pbr>=0.5.21,<1.0 cliff>=1.4 -keyring>=1.6.1 +keyring>=1.6.1,<2.0 pycrypto>=2.6 python-glanceclient>=0.9.0 -python-keystoneclient>=0.3.0 +python-keystoneclient>=0.3.2 python-novaclient>=2.12.0 -python-cinderclient>=1.0.4 +python-cinderclient>=1.0.5 diff --git a/test-requirements.txt b/test-requirements.txt index 692e71a950..27de420d67 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,13 +1,14 @@ -# Install bounded pep8/pyflakes first, then let flake8 install -pep8==1.4.5 -pyflakes==0.7.2 -flake8==2.0 -hacking>=0.5.6,<0.7 +hacking>=0.5.6,<0.8 coverage>=3.6 discover -fixtures>=0.3.12 -mock>=0.8.0 +fixtures>=0.3.14 +httpretty>=0.6.3 +mock>=1.0 +mox3>=0.7.0 sphinx>=1.1.2 testrepository>=0.0.17 testtools>=0.9.32 +WebOb>=1.2.3,<1.3 + +Babel>=0.9.6 From c8723ce175b48bdde016a6efb4945b6b3766080f Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 29 Aug 2013 20:39:34 -0500 Subject: [PATCH 0006/3494] Update tox.ini for new tox 1.6 config Change-Id: I4363508f562f62b16c856bc072cdb4b37e37b418 --- tox.ini | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index e2c61af489..f6a5c338f9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,11 @@ [tox] +minversion = 1.6 envlist = py26,py27,py33,pep8 +skipdist = True [testenv] +usedevelop = True +install_command = pip install -U {opts} {packages} setenv = VIRTUAL_ENV={envdir} LANG=en_US.UTF-8 LANGUAGE=en_US:en @@ -17,7 +21,7 @@ commands = flake8 commands = {posargs} [testenv:cover] -commands = python setup.py testr --coverage --testr-args='{posargs}' +commands = python setup.py test --coverage --testr-args='{posargs}' [tox:jenkins] downloadcache = ~/cache/pip From eb405a88c47e91633ecb110122410aa24c24f9cd Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 28 Aug 2013 15:17:45 -0500 Subject: [PATCH 0007/3494] Refactor fake data for projects and users * Move fake data structures into tests/identity/fakes.py * Use fake clients correctly and support multiple client versions Change-Id: Icacbb2ca740b63937bd2c4442af61b620638b53e --- openstackclient/tests/fakes.py | 6 +- openstackclient/tests/identity/fakes.py | 36 +- .../tests/identity/test_identity.py | 30 +- .../{test_projects.py => test_project.py} | 263 +++++++++----- .../v2_0/{test_users.py => test_user.py} | 343 ++++++++++++------ .../tests/object/v1/lib/test_container.py | 2 + .../tests/object/v1/lib/test_object.py | 2 + openstackclient/tests/utils.py | 2 - 8 files changed, 470 insertions(+), 214 deletions(-) rename openstackclient/tests/identity/v2_0/{test_projects.py => test_project.py} (58%) rename openstackclient/tests/identity/v2_0/{test_users.py => test_user.py} (62%) diff --git a/openstackclient/tests/fakes.py b/openstackclient/tests/fakes.py index e0122022a8..22292a646f 100644 --- a/openstackclient/tests/fakes.py +++ b/openstackclient/tests/fakes.py @@ -37,6 +37,7 @@ def __init__(self, _stdout): self.stdin = sys.stdin self.stdout = _stdout or sys.stdout self.stderr = sys.stderr + self.restapi = None class FakeClientManager(object): @@ -44,11 +45,6 @@ def __init__(self): pass -class FakeRESTApi(object): - def __init__(self): - pass - - class FakeResource(object): def __init__(self, manager, info, loaded=False): self.manager = manager diff --git a/openstackclient/tests/identity/fakes.py b/openstackclient/tests/identity/fakes.py index b1e385e291..1c1ea72bc3 100644 --- a/openstackclient/tests/identity/fakes.py +++ b/openstackclient/tests/identity/fakes.py @@ -18,6 +18,38 @@ from openstackclient.tests import fakes +user_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' +user_name = 'paul' +user_description = 'Sir Paul' +user_email = 'paul@applecorps.com' + +project_id = '8-9-64' +project_name = 'beatles' +project_description = 'Fab Four' + +USER = { + 'id': user_id, + 'name': user_name, + 'tenantId': project_id, + 'email': user_email, + 'enabled': True, +} + +PROJECT = { + 'id': project_id, + 'name': project_name, + 'description': project_description, + 'enabled': True, +} + +PROJECT_2 = { + 'id': project_id + '-2222', + 'name': project_name + ' reprise', + 'description': project_description + 'plus four more', + 'enabled': True, +} + + class FakeIdentityv2Client(object): def __init__(self, **kwargs): self.tenants = mock.Mock() @@ -26,8 +58,8 @@ def __init__(self, **kwargs): self.users.resource_class = fakes.FakeResource(None, {}) self.ec2 = mock.Mock() self.ec2.resource_class = fakes.FakeResource(None, {}) - self.auth_tenant_id = 'fake-tenant' - self.auth_user_id = 'fake-user' + self.auth_token = kwargs['token'] + self.management_url = kwargs['endpoint'] class FakeIdentityv3Client(object): diff --git a/openstackclient/tests/identity/test_identity.py b/openstackclient/tests/identity/test_identity.py index 04bbd20f49..894f47baa2 100644 --- a/openstackclient/tests/identity/test_identity.py +++ b/openstackclient/tests/identity/test_identity.py @@ -22,27 +22,29 @@ AUTH_URL = "http://0.0.0.0" -class FakeClient(object): - def __init__(self, endpoint=None, **kwargs): - self.auth_token = AUTH_TOKEN - self.auth_url = AUTH_URL - - -class TestIdentity(utils.TestCase): +class TestIdentity(utils.TestCommand): def setUp(self): super(TestIdentity, self).setUp() api_version = {"identity": "2.0"} identity_client.API_VERSIONS = { - "2.0": "openstackclient.tests.identity.test_identity.FakeClient" + "2.0": "openstackclient.tests.identity.fakes.FakeIdentityv2Client" } - self.cm = clientmanager.ClientManager(token=AUTH_TOKEN, - url=AUTH_URL, - auth_url=AUTH_URL, - api_version=api_version) + self.app.client_manager = clientmanager.ClientManager( + token=AUTH_TOKEN, + url=AUTH_URL, + auth_url=AUTH_URL, + api_version=api_version, + ) def test_make_client(self): - self.assertEqual(self.cm.identity.auth_token, AUTH_TOKEN) - self.assertEqual(self.cm.identity.auth_url, AUTH_URL) + self.assertEqual( + self.app.client_manager.identity.auth_token, + AUTH_TOKEN, + ) + self.assertEqual( + self.app.client_manager.identity.management_url, + AUTH_URL, + ) diff --git a/openstackclient/tests/identity/v2_0/test_projects.py b/openstackclient/tests/identity/v2_0/test_project.py similarity index 58% rename from openstackclient/tests/identity/v2_0/test_projects.py rename to openstackclient/tests/identity/v2_0/test_project.py index c937028f1a..c6c9b8ee01 100644 --- a/openstackclient/tests/identity/v2_0/test_projects.py +++ b/openstackclient/tests/identity/v2_0/test_project.py @@ -18,42 +18,17 @@ from openstackclient.identity.v2_0 import project from openstackclient.tests import fakes from openstackclient.tests.identity import fakes as identity_fakes -from openstackclient.tests import utils +from openstackclient.tests.identity import test_identity -IDENTITY_API_VERSION = "2.0" - -user_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' -user_name = 'paul' -user_description = 'Sir Paul' - -project_id = '8-9-64' -project_name = 'beatles' -project_description = 'Fab Four' - -USER = { - 'id': user_id, - 'name': user_name, - 'tenantId': project_id, -} - -PROJECT = { - 'id': project_id, - 'name': project_name, - 'description': project_description, - 'enabled': True, -} - - -class TestProject(utils.TestCommand): +class TestProject(test_identity.TestIdentity): def setUp(self): super(TestProject, self).setUp() - self.app.client_manager.identity = \ - identity_fakes.FakeIdentityv2Client() # Get a shortcut to the TenantManager Mock self.projects_mock = self.app.client_manager.identity.tenants + self.projects_mock.reset_mock() class TestProjectCreate(TestProject): @@ -63,7 +38,7 @@ def setUp(self): self.projects_mock.create.return_value = fakes.FakeResource( None, - copy.deepcopy(PROJECT), + copy.deepcopy(identity_fakes.PROJECT), loaded=True, ) @@ -71,9 +46,11 @@ def setUp(self): self.cmd = project.CreateProject(self.app, None) def test_project_create_no_options(self): - arglist = [project_name] + arglist = [ + identity_fakes.project_name, + ] verifylist = [ - ('project_name', project_name), + ('project_name', identity_fakes.project_name), ('enable', False), ('disable', False), ] @@ -87,16 +64,30 @@ def test_project_create_no_options(self): 'description': None, 'enabled': True, } - self.projects_mock.create.assert_called_with(project_name, **kwargs) + self.projects_mock.create.assert_called_with( + identity_fakes.project_name, + **kwargs + ) collist = ('description', 'enabled', 'id', 'name') self.assertEqual(columns, collist) - datalist = (project_description, True, project_id, project_name) + datalist = ( + identity_fakes.project_description, + True, + identity_fakes.project_id, + identity_fakes.project_name, + ) self.assertEqual(data, datalist) def test_project_create_description(self): - arglist = ['--description', 'new desc', project_name] - verifylist = [('description', 'new desc')] + arglist = [ + '--description', 'new desc', + identity_fakes.project_name, + ] + verifylist = [ + ('project_name', identity_fakes.project_name), + ('description', 'new desc'), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # DisplayCommandBase.take_action() returns two tuples @@ -107,16 +98,31 @@ def test_project_create_description(self): 'description': 'new desc', 'enabled': True, } - self.projects_mock.create.assert_called_with(project_name, **kwargs) + self.projects_mock.create.assert_called_with( + identity_fakes.project_name, + **kwargs + ) collist = ('description', 'enabled', 'id', 'name') self.assertEqual(columns, collist) - datalist = (project_description, True, project_id, project_name) + datalist = ( + identity_fakes.project_description, + True, + identity_fakes.project_id, + identity_fakes.project_name, + ) self.assertEqual(data, datalist) def test_project_create_enable(self): - arglist = ['--enable', project_name] - verifylist = [('enable', True), ('disable', False)] + arglist = [ + '--enable', + identity_fakes.project_name, + ] + verifylist = [ + ('project_name', identity_fakes.project_name), + ('enable', True), + ('disable', False), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # DisplayCommandBase.take_action() returns two tuples @@ -127,16 +133,31 @@ def test_project_create_enable(self): 'description': None, 'enabled': True, } - self.projects_mock.create.assert_called_with(project_name, **kwargs) + self.projects_mock.create.assert_called_with( + identity_fakes.project_name, + **kwargs + ) collist = ('description', 'enabled', 'id', 'name') self.assertEqual(columns, collist) - datalist = (project_description, True, project_id, project_name) + datalist = ( + identity_fakes.project_description, + True, + identity_fakes.project_id, + identity_fakes.project_name, + ) self.assertEqual(data, datalist) def test_project_create_disable(self): - arglist = ['--disable', project_name] - verifylist = [('enable', False), ('disable', True)] + arglist = [ + '--disable', + identity_fakes.project_name, + ] + verifylist = [ + ('project_name', identity_fakes.project_name), + ('enable', False), + ('disable', True), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # DisplayCommandBase.take_action() returns two tuples @@ -147,11 +168,19 @@ def test_project_create_disable(self): 'description': None, 'enabled': False, } - self.projects_mock.create.assert_called_with(project_name, **kwargs) + self.projects_mock.create.assert_called_with( + identity_fakes.project_name, + **kwargs + ) collist = ('description', 'enabled', 'id', 'name') self.assertEqual(columns, collist) - datalist = (project_description, True, project_id, project_name) + datalist = ( + identity_fakes.project_description, + True, + identity_fakes.project_id, + identity_fakes.project_name, + ) self.assertEqual(data, datalist) @@ -163,7 +192,7 @@ def setUp(self): # This is the return value for utils.find_resource() self.projects_mock.get.return_value = fakes.FakeResource( None, - copy.deepcopy(PROJECT), + copy.deepcopy(identity_fakes.PROJECT), loaded=True, ) self.projects_mock.delete.return_value = None @@ -172,14 +201,20 @@ def setUp(self): self.cmd = project.DeleteProject(self.app, None) def test_project_delete_no_options(self): - arglist = [user_id] - verifylist = [('project', user_id)] + arglist = [ + identity_fakes.project_id, + ] + verifylist = [ + ('project', identity_fakes.project_id), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.run(parsed_args) self.assertEqual(result, 0) - self.projects_mock.delete.assert_called_with(project_id) + self.projects_mock.delete.assert_called_with( + identity_fakes.project_id, + ) class TestProjectList(TestProject): @@ -190,7 +225,7 @@ def setUp(self): self.projects_mock.list.return_value = [ fakes.FakeResource( None, - copy.deepcopy(PROJECT), + copy.deepcopy(identity_fakes.PROJECT), loaded=True, ), ] @@ -209,12 +244,19 @@ def test_project_list_no_options(self): collist = ('ID', 'Name') self.assertEqual(columns, collist) - datalist = ((project_id, project_name), ) + datalist = (( + identity_fakes.project_id, + identity_fakes.project_name, + ), ) self.assertEqual(tuple(data), datalist) def test_project_list_long(self): - arglist = ['--long'] - verifylist = [('long', True)] + arglist = [ + '--long', + ] + verifylist = [ + ('long', True), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # DisplayCommandBase.take_action() returns two tuples @@ -223,7 +265,12 @@ def test_project_list_long(self): collist = ('ID', 'Name', 'Description', 'Enabled') self.assertEqual(columns, collist) - datalist = ((project_id, project_name, project_description, True), ) + datalist = (( + identity_fakes.project_id, + identity_fakes.project_name, + identity_fakes.project_description, + True, + ), ) self.assertEqual(tuple(data), datalist) @@ -234,7 +281,7 @@ def setUp(self): self.projects_mock.get.return_value = fakes.FakeResource( None, - copy.deepcopy(PROJECT), + copy.deepcopy(identity_fakes.PROJECT), loaded=True, ) @@ -242,9 +289,11 @@ def setUp(self): self.cmd = project.SetProject(self.app, None) def test_project_set_no_options(self): - arglist = [project_name] + arglist = [ + identity_fakes.project_name, + ] verifylist = [ - ('project', project_name), + ('project', identity_fakes.project_name), ('enable', False), ('disable', False), ] @@ -255,15 +304,23 @@ def test_project_set_no_options(self): # Set expected values kwargs = { - 'description': project_description, + 'description': identity_fakes.project_description, 'enabled': True, - 'tenant_name': project_name, + 'tenant_name': identity_fakes.project_name, } - self.projects_mock.update.assert_called_with(project_id, **kwargs) + self.projects_mock.update.assert_called_with( + identity_fakes.project_id, + **kwargs + ) def test_project_set_name(self): - arglist = ['--name', 'qwerty', project_name] - verifylist = [('name', 'qwerty')] + arglist = [ + '--name', 'qwerty', + identity_fakes.project_name, + ] + verifylist = [ + ('name', 'qwerty'), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.run(parsed_args) @@ -271,15 +328,23 @@ def test_project_set_name(self): # Set expected values kwargs = { - 'description': project_description, + 'description': identity_fakes.project_description, 'enabled': True, 'tenant_name': 'qwerty', } - self.projects_mock.update.assert_called_with(project_id, **kwargs) + self.projects_mock.update.assert_called_with( + identity_fakes.project_id, + **kwargs + ) def test_project_set_description(self): - arglist = ['--description', 'new desc', project_name] - verifylist = [('description', 'new desc')] + arglist = [ + '--description', 'new desc', + identity_fakes.project_name, + ] + verifylist = [ + ('description', 'new desc'), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.run(parsed_args) @@ -289,13 +354,22 @@ def test_project_set_description(self): kwargs = { 'description': 'new desc', 'enabled': True, - 'tenant_name': project_name, + 'tenant_name': identity_fakes.project_name, } - self.projects_mock.update.assert_called_with(project_id, **kwargs) + self.projects_mock.update.assert_called_with( + identity_fakes.project_id, + **kwargs + ) def test_project_set_enable(self): - arglist = ['--enable', project_name] - verifylist = [('enable', True), ('disable', False)] + arglist = [ + '--enable', + identity_fakes.project_name, + ] + verifylist = [ + ('enable', True), + ('disable', False), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.run(parsed_args) @@ -303,15 +377,24 @@ def test_project_set_enable(self): # Set expected values kwargs = { - 'description': project_description, + 'description': identity_fakes.project_description, 'enabled': True, - 'tenant_name': project_name, + 'tenant_name': identity_fakes.project_name, } - self.projects_mock.update.assert_called_with(project_id, **kwargs) + self.projects_mock.update.assert_called_with( + identity_fakes.project_id, + **kwargs + ) def test_project_set_disable(self): - arglist = ['--disable', project_name] - verifylist = [('enable', False), ('disable', True)] + arglist = [ + '--disable', + identity_fakes.project_name, + ] + verifylist = [ + ('enable', False), + ('disable', True), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.run(parsed_args) @@ -319,11 +402,14 @@ def test_project_set_disable(self): # Set expected values kwargs = { - 'description': project_description, + 'description': identity_fakes.project_description, 'enabled': False, - 'tenant_name': project_name, + 'tenant_name': identity_fakes.project_name, } - self.projects_mock.update.assert_called_with(project_id, **kwargs) + self.projects_mock.update.assert_called_with( + identity_fakes.project_id, + **kwargs + ) class TestProjectShow(TestProject): @@ -333,7 +419,7 @@ def setUp(self): self.projects_mock.get.return_value = fakes.FakeResource( None, - copy.deepcopy(PROJECT), + copy.deepcopy(identity_fakes.PROJECT), loaded=True, ) @@ -341,15 +427,26 @@ def setUp(self): self.cmd = project.ShowProject(self.app, None) def test_project_show(self): - arglist = [user_id] - verifylist = [('project', user_id)] + arglist = [ + identity_fakes.project_id, + ] + verifylist = [ + ('project', identity_fakes.project_id), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # DisplayCommandBase.take_action() returns two tuples columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.get.assert_called_with(user_id) + self.projects_mock.get.assert_called_with( + identity_fakes.project_id, + ) collist = ('description', 'enabled', 'id', 'name') self.assertEqual(columns, collist) - datalist = (project_description, True, project_id, project_name) + datalist = ( + identity_fakes.project_description, + True, + identity_fakes.project_id, + identity_fakes.project_name, + ) self.assertEqual(data, datalist) diff --git a/openstackclient/tests/identity/v2_0/test_users.py b/openstackclient/tests/identity/v2_0/test_user.py similarity index 62% rename from openstackclient/tests/identity/v2_0/test_users.py rename to openstackclient/tests/identity/v2_0/test_user.py index 49a5262b56..529173da5d 100644 --- a/openstackclient/tests/identity/v2_0/test_users.py +++ b/openstackclient/tests/identity/v2_0/test_user.py @@ -18,54 +18,20 @@ from openstackclient.identity.v2_0 import user from openstackclient.tests import fakes from openstackclient.tests.identity import fakes as identity_fakes -from openstackclient.tests import utils +from openstackclient.tests.identity import test_identity -IDENTITY_API_VERSION = "2.0" - -user_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' -user_name = 'paul' -user_description = 'Sir Paul' -user_email = 'paul@applecorps.com' - -project_id = '8-9-64' -project_name = 'beatles' -project_description = 'Fab Four' - -USER = { - 'id': user_id, - 'name': user_name, - 'tenantId': project_id, - 'email': user_email, - 'enabled': True, -} - -PROJECT = { - 'id': project_id, - 'name': project_name, - 'description': project_description, - 'enabled': True, -} - -PROJECT_2 = { - 'id': project_id + '-2222', - 'name': project_name + ' reprise', - 'description': project_description + 'plus four more', - 'enabled': True, -} - - -class TestUser(utils.TestCommand): +class TestUser(test_identity.TestIdentity): def setUp(self): super(TestUser, self).setUp() - self.app.client_manager.identity = \ - identity_fakes.FakeIdentityv2Client() # Get a shortcut to the TenantManager Mock self.projects_mock = self.app.client_manager.identity.tenants + self.projects_mock.reset_mock() # Get a shortcut to the UserManager Mock self.users_mock = self.app.client_manager.identity.users + self.users_mock.reset_mock() class TestUserCreate(TestUser): @@ -75,12 +41,12 @@ def setUp(self): self.projects_mock.get.return_value = fakes.FakeResource( None, - copy.deepcopy(PROJECT), + copy.deepcopy(identity_fakes.PROJECT), loaded=True, ) self.users_mock.create.return_value = fakes.FakeResource( None, - copy.deepcopy(USER), + copy.deepcopy(identity_fakes.USER), loaded=True, ) @@ -88,9 +54,11 @@ def setUp(self): self.cmd = user.CreateUser(self.app, None) def test_user_create_no_options(self): - arglist = [user_name] + arglist = [ + identity_fakes.user_name, + ] verifylist = [ - ('name', user_name), + ('name', identity_fakes.user_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -104,7 +72,7 @@ def test_user_create_no_options(self): } # users.create(name, password, email, tenant_id=None, enabled=True) self.users_mock.create.assert_called_with( - user_name, + identity_fakes.user_name, None, None, **kwargs @@ -112,12 +80,24 @@ def test_user_create_no_options(self): collist = ('email', 'enabled', 'id', 'name', 'project_id') self.assertEqual(columns, collist) - datalist = (user_email, True, user_id, user_name, project_id) + datalist = ( + identity_fakes.user_email, + True, + identity_fakes.user_id, + identity_fakes.user_name, + identity_fakes.project_id, + ) self.assertEqual(data, datalist) def test_user_create_password(self): - arglist = ['--password', 'secret', user_name] - verifylist = [('password', 'secret')] + arglist = [ + '--password', 'secret', + identity_fakes.user_name, + ] + verifylist = [ + ('name', identity_fakes.user_name), + ('password', 'secret') + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # DisplayCommandBase.take_action() returns two tuples @@ -130,7 +110,7 @@ def test_user_create_password(self): } # users.create(name, password, email, tenant_id=None, enabled=True) self.users_mock.create.assert_called_with( - user_name, + identity_fakes.user_name, 'secret', None, **kwargs @@ -138,12 +118,24 @@ def test_user_create_password(self): collist = ('email', 'enabled', 'id', 'name', 'project_id') self.assertEqual(columns, collist) - datalist = (user_email, True, user_id, user_name, project_id) + datalist = ( + identity_fakes.user_email, + True, + identity_fakes.user_id, + identity_fakes.user_name, + identity_fakes.project_id, + ) self.assertEqual(data, datalist) def test_user_create_email(self): - arglist = ['--email', 'barney@example.com', user_name] - verifylist = [('email', 'barney@example.com')] + arglist = [ + '--email', 'barney@example.com', + identity_fakes.user_name, + ] + verifylist = [ + ('name', identity_fakes.user_name), + ('email', 'barney@example.com'), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # DisplayCommandBase.take_action() returns two tuples @@ -156,7 +148,7 @@ def test_user_create_email(self): } # users.create(name, password, email, tenant_id=None, enabled=True) self.users_mock.create.assert_called_with( - user_name, + identity_fakes.user_name, None, 'barney@example.com', **kwargs @@ -164,27 +156,39 @@ def test_user_create_email(self): collist = ('email', 'enabled', 'id', 'name', 'project_id') self.assertEqual(columns, collist) - datalist = (user_email, True, user_id, user_name, project_id) + datalist = ( + identity_fakes.user_email, + True, + identity_fakes.user_id, + identity_fakes.user_name, + identity_fakes.project_id, + ) self.assertEqual(data, datalist) def test_user_create_project(self): # Return the new project self.projects_mock.get.return_value = fakes.FakeResource( None, - copy.deepcopy(PROJECT_2), + copy.deepcopy(identity_fakes.PROJECT_2), loaded=True, ) # Set up to return an updated user - USER_2 = copy.deepcopy(USER) - USER_2['tenantId'] = PROJECT_2['id'] + USER_2 = copy.deepcopy(identity_fakes.USER) + USER_2['tenantId'] = identity_fakes.PROJECT_2['id'] self.users_mock.create.return_value = fakes.FakeResource( None, USER_2, loaded=True, ) - arglist = ['--project', PROJECT_2['name'], user_name] - verifylist = [('project', PROJECT_2['name'])] + arglist = [ + '--project', identity_fakes.PROJECT_2['name'], + identity_fakes.user_name, + ] + verifylist = [ + ('name', identity_fakes.user_name), + ('project', identity_fakes.PROJECT_2['name']), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # DisplayCommandBase.take_action() returns two tuples @@ -193,11 +197,11 @@ def test_user_create_project(self): # Set expected values kwargs = { 'enabled': True, - 'tenant_id': PROJECT_2['id'], + 'tenant_id': identity_fakes.PROJECT_2['id'], } # users.create(name, password, email, tenant_id=None, enabled=True) self.users_mock.create.assert_called_with( - user_name, + identity_fakes.user_name, None, None, **kwargs @@ -205,12 +209,25 @@ def test_user_create_project(self): collist = ('email', 'enabled', 'id', 'name', 'project_id') self.assertEqual(columns, collist) - datalist = (user_email, True, user_id, user_name, PROJECT_2['id']) + datalist = ( + identity_fakes.user_email, + True, + identity_fakes.user_id, + identity_fakes.user_name, + identity_fakes.PROJECT_2['id'], + ) self.assertEqual(data, datalist) def test_user_create_enable(self): - arglist = ['--enable', project_name] - verifylist = [('enable', True), ('disable', False)] + arglist = [ + '--enable', + identity_fakes.user_name, + ] + verifylist = [ + ('name', identity_fakes.user_name), + ('enable', True), + ('disable', False), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # DisplayCommandBase.take_action() returns two tuples @@ -223,7 +240,7 @@ def test_user_create_enable(self): } # users.create(name, password, email, tenant_id=None, enabled=True) self.users_mock.create.assert_called_with( - project_name, + identity_fakes.user_name, None, None, **kwargs @@ -231,12 +248,25 @@ def test_user_create_enable(self): collist = ('email', 'enabled', 'id', 'name', 'project_id') self.assertEqual(columns, collist) - datalist = (user_email, True, user_id, user_name, project_id) + datalist = ( + identity_fakes.user_email, + True, + identity_fakes.user_id, + identity_fakes.user_name, + identity_fakes.project_id, + ) self.assertEqual(data, datalist) def test_user_create_disable(self): - arglist = ['--disable', project_name] - verifylist = [('enable', False), ('disable', True)] + arglist = [ + '--disable', + identity_fakes.user_name, + ] + verifylist = [ + ('name', identity_fakes.user_name), + ('enable', False), + ('disable', True), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # DisplayCommandBase.take_action() returns two tuples @@ -249,7 +279,7 @@ def test_user_create_disable(self): } # users.create(name, password, email, tenant_id=None, enabled=True) self.users_mock.create.assert_called_with( - project_name, + identity_fakes.user_name, None, None, **kwargs @@ -257,7 +287,13 @@ def test_user_create_disable(self): collist = ('email', 'enabled', 'id', 'name', 'project_id') self.assertEqual(columns, collist) - datalist = (user_email, True, user_id, user_name, project_id) + datalist = ( + identity_fakes.user_email, + True, + identity_fakes.user_id, + identity_fakes.user_name, + identity_fakes.project_id, + ) self.assertEqual(data, datalist) @@ -269,7 +305,7 @@ def setUp(self): # This is the return value for utils.find_resource() self.users_mock.get.return_value = fakes.FakeResource( None, - copy.deepcopy(USER), + copy.deepcopy(identity_fakes.USER), loaded=True, ) self.users_mock.delete.return_value = None @@ -278,16 +314,20 @@ def setUp(self): self.cmd = user.DeleteUser(self.app, None) def test_user_delete_no_options(self): - arglist = [user_id] + arglist = [ + identity_fakes.user_id, + ] verifylist = [ - ('user', user_id), + ('user', identity_fakes.user_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # DisplayCommandBase.take_action() returns two tuples self.cmd.take_action(parsed_args) - self.users_mock.delete.assert_called_with(user_id) + self.users_mock.delete.assert_called_with( + identity_fakes.user_id, + ) class TestUserList(TestUser): @@ -295,10 +335,17 @@ class TestUserList(TestUser): def setUp(self): super(TestUserList, self).setUp() + self.projects_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ), + ] self.users_mock.list.return_value = [ fakes.FakeResource( None, - copy.deepcopy(USER), + copy.deepcopy(identity_fakes.USER), loaded=True, ), ] @@ -318,12 +365,19 @@ def test_user_list_no_options(self): collist = ('ID', 'Name') self.assertEqual(columns, collist) - datalist = ((user_id, user_name), ) + datalist = (( + identity_fakes.user_id, + identity_fakes.user_name, + ), ) self.assertEqual(tuple(data), datalist) def test_user_list_project(self): - arglist = ['--project', project_id] - verifylist = [('project', project_id)] + arglist = [ + '--project', identity_fakes.project_id, + ] + verifylist = [ + ('project', identity_fakes.project_id), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # DisplayCommandBase.take_action() returns two tuples @@ -333,12 +387,19 @@ def test_user_list_project(self): collist = ('ID', 'Name') self.assertEqual(columns, collist) - datalist = ((user_id, user_name), ) + datalist = (( + identity_fakes.user_id, + identity_fakes.user_name, + ), ) self.assertEqual(tuple(data), datalist) def test_user_list_long(self): - arglist = ['--long'] - verifylist = [('long', True)] + arglist = [ + '--long', + ] + verifylist = [ + ('long', True), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # DisplayCommandBase.take_action() returns two tuples @@ -348,7 +409,13 @@ def test_user_list_long(self): collist = ('ID', 'Name', 'Project', 'Email', 'Enabled') self.assertEqual(columns, collist) - datalist = ((user_id, user_name, project_id, user_email, True), ) + datalist = (( + identity_fakes.user_id, + identity_fakes.user_name, + identity_fakes.project_name, + identity_fakes.user_email, + True, + ), ) self.assertEqual(tuple(data), datalist) @@ -359,12 +426,12 @@ def setUp(self): self.projects_mock.get.return_value = fakes.FakeResource( None, - copy.deepcopy(PROJECT), + copy.deepcopy(identity_fakes.PROJECT), loaded=True, ) self.users_mock.get.return_value = fakes.FakeResource( None, - copy.deepcopy(USER), + copy.deepcopy(identity_fakes.USER), loaded=True, ) @@ -372,9 +439,11 @@ def setUp(self): self.cmd = user.SetUser(self.app, None) def test_user_set_no_options(self): - arglist = [user_name] + arglist = [ + identity_fakes.user_name, + ] verifylist = [ - ('user', user_name), + ('user', identity_fakes.user_name), ('enable', False), ('disable', False), ('project', None), @@ -384,11 +453,17 @@ def test_user_set_no_options(self): # DisplayCommandBase.take_action() returns two tuples self.cmd.take_action(parsed_args) + # SetUser doesn't call anything if no args are passed self.assertFalse(self.users_mock.update.called) def test_user_set_name(self): - arglist = ['--name', 'qwerty', user_name] - verifylist = [('name', 'qwerty')] + arglist = [ + '--name', 'qwerty', + identity_fakes.user_name, + ] + verifylist = [ + ('name', 'qwerty'), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # DisplayCommandBase.take_action() returns two tuples @@ -398,21 +473,37 @@ def test_user_set_name(self): kwargs = { 'name': 'qwerty', } - self.users_mock.update.assert_called_with(user_id, **kwargs) + self.users_mock.update.assert_called_with( + identity_fakes.user_id, + **kwargs + ) def test_user_set_password(self): - arglist = ['--password', 'secret', user_name] - verifylist = [('password', 'secret')] + arglist = [ + '--password', 'secret', + identity_fakes.user_name, + ] + verifylist = [ + ('password', 'secret'), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # DisplayCommandBase.take_action() returns two tuples self.cmd.take_action(parsed_args) - self.users_mock.update_password.assert_called_with(user_id, 'secret') + self.users_mock.update_password.assert_called_with( + identity_fakes.user_id, + 'secret', + ) def test_user_set_email(self): - arglist = ['--email', 'barney@example.com', user_name] - verifylist = [('email', 'barney@example.com')] + arglist = [ + '--email', 'barney@example.com', + identity_fakes.user_name, + ] + verifylist = [ + ('email', 'barney@example.com'), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # DisplayCommandBase.take_action() returns two tuples @@ -422,24 +513,38 @@ def test_user_set_email(self): kwargs = { 'email': 'barney@example.com', } - self.users_mock.update.assert_called_with(user_id, **kwargs) + self.users_mock.update.assert_called_with( + identity_fakes.user_id, + **kwargs + ) def test_user_set_project(self): - arglist = ['--project', project_id, user_name] - verifylist = [('project', project_id)] + arglist = [ + '--project', identity_fakes.project_id, + identity_fakes.user_name, + ] + verifylist = [ + ('project', identity_fakes.project_id), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # DisplayCommandBase.take_action() returns two tuples self.cmd.take_action(parsed_args) self.users_mock.update_tenant.assert_called_with( - user_id, - project_id, + identity_fakes.user_id, + identity_fakes.project_id, ) def test_user_set_enable(self): - arglist = ['--enable', user_name] - verifylist = [('enable', True), ('disable', False)] + arglist = [ + '--enable', + identity_fakes.user_name, + ] + verifylist = [ + ('enable', True), + ('disable', False), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # DisplayCommandBase.take_action() returns two tuples @@ -449,11 +554,20 @@ def test_user_set_enable(self): kwargs = { 'enabled': True, } - self.users_mock.update.assert_called_with(user_id, **kwargs) + self.users_mock.update.assert_called_with( + identity_fakes.user_id, + **kwargs + ) def test_user_set_disable(self): - arglist = ['--disable', user_name] - verifylist = [('enable', False), ('disable', True)] + arglist = [ + '--disable', + identity_fakes.user_name, + ] + verifylist = [ + ('enable', False), + ('disable', True), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # DisplayCommandBase.take_action() returns two tuples @@ -463,7 +577,10 @@ def test_user_set_disable(self): kwargs = { 'enabled': False, } - self.users_mock.update.assert_called_with(user_id, **kwargs) + self.users_mock.update.assert_called_with( + identity_fakes.user_id, + **kwargs + ) class TestUserShow(TestUser): @@ -473,7 +590,7 @@ def setUp(self): self.users_mock.get.return_value = fakes.FakeResource( None, - copy.deepcopy(USER), + copy.deepcopy(identity_fakes.USER), loaded=True, ) @@ -481,16 +598,26 @@ def setUp(self): self.cmd = user.ShowUser(self.app, None) def test_user_show(self): - arglist = [user_id] - verifylist = [('user', user_id)] + arglist = [ + identity_fakes.user_id, + ] + verifylist = [ + ('user', identity_fakes.user_id), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # DisplayCommandBase.take_action() returns two tuples columns, data = self.cmd.take_action(parsed_args) - self.users_mock.get.assert_called_with(user_id) + self.users_mock.get.assert_called_with(identity_fakes.user_id) collist = ('email', 'enabled', 'id', 'name', 'project_id') self.assertEqual(columns, collist) - datalist = (user_email, True, user_id, user_name, project_id) + datalist = ( + identity_fakes.user_email, + True, + identity_fakes.user_id, + identity_fakes.user_name, + identity_fakes.project_id, + ) self.assertEqual(data, datalist) diff --git a/openstackclient/tests/object/v1/lib/test_container.py b/openstackclient/tests/object/v1/lib/test_container.py index a241cc021f..1c0112c884 100644 --- a/openstackclient/tests/object/v1/lib/test_container.py +++ b/openstackclient/tests/object/v1/lib/test_container.py @@ -22,6 +22,7 @@ from openstackclient.object.v1.lib import container as lib_container from openstackclient.tests.common import test_restapi as restapi +from openstackclient.tests import fakes from openstackclient.tests import utils @@ -41,6 +42,7 @@ class TestObject(utils.TestCommand): def setUp(self): super(TestObject, self).setUp() + self.app.client_manager = fakes.FakeClientManager() self.app.client_manager.object = FakeClient() self.app.restapi = mock.MagicMock() diff --git a/openstackclient/tests/object/v1/lib/test_object.py b/openstackclient/tests/object/v1/lib/test_object.py index ef8ae18dc5..b4793cc24a 100644 --- a/openstackclient/tests/object/v1/lib/test_object.py +++ b/openstackclient/tests/object/v1/lib/test_object.py @@ -21,6 +21,7 @@ from openstackclient.object.v1.lib import object as lib_object from openstackclient.tests.common import test_restapi as restapi +from openstackclient.tests import fakes from openstackclient.tests import utils @@ -40,6 +41,7 @@ class TestObject(utils.TestCommand): def setUp(self): super(TestObject, self).setUp() + self.app.client_manager = fakes.FakeClientManager() self.app.client_manager.object = FakeClient() self.app.restapi = mock.MagicMock() diff --git a/openstackclient/tests/utils.py b/openstackclient/tests/utils.py index 4516318908..cf7fda6d56 100644 --- a/openstackclient/tests/utils.py +++ b/openstackclient/tests/utils.py @@ -70,8 +70,6 @@ def setUp(self): # Build up a fake app self.fake_stdout = fakes.FakeStdout() self.app = fakes.FakeApp(self.fake_stdout) - self.app.client_manager = fakes.FakeClientManager() - self.app.restapi = fakes.FakeRESTApi() def check_parser(self, cmd, args, verify_args): cmd_parser = cmd.get_parser('check_parser') From 44c97cc099a35af2d3d999f9799238fc72be071d Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 27 Aug 2013 16:57:30 -0500 Subject: [PATCH 0008/3494] Add Identity v2 role and service tests * Add current auth info (auth_ref) to ClientManager * Fix identity.v2_0.role.ListUserRole to get default user/project from ClientManager.auth_ref * Fix identity.v2_0.role.AddRole call to roles.add_user_role() Change-Id: Ie8bf41c491d97b0292a2b86bdc9b7580989a7f97 --- openstackclient/common/clientmanager.py | 5 +- openstackclient/identity/client.py | 3 +- openstackclient/identity/v2_0/role.py | 27 +- openstackclient/tests/identity/fakes.py | 51 ++- .../tests/identity/v2_0/test_role.py | 419 ++++++++++++++++++ .../tests/identity/v2_0/test_service.py | 243 ++++++++++ 6 files changed, 724 insertions(+), 24 deletions(-) create mode 100644 openstackclient/tests/identity/v2_0/test_role.py create mode 100644 openstackclient/tests/identity/v2_0/test_service.py diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index 690cabba7e..24b09beb71 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -1,4 +1,4 @@ -# Copyright 2012-2013 OpenStack, LLC. +# Copyright 2012-2013 OpenStack Foundation # # 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 @@ -62,8 +62,11 @@ def __init__(self, token=None, url=None, auth_url=None, project_name=None, self._api_version = api_version self._service_catalog = None + self.auth_ref = None + if not self._url: # Populate other password flow attributes + self.auth_ref = self.identity.auth_ref self._token = self.identity.auth_token self._service_catalog = self.identity.service_catalog diff --git a/openstackclient/identity/client.py b/openstackclient/identity/client.py index 8e2bda402e..8c9437ba5b 100644 --- a/openstackclient/identity/client.py +++ b/openstackclient/identity/client.py @@ -1,4 +1,4 @@ -# Copyright 2012-2013 OpenStack, LLC. +# Copyright 2012-2013 OpenStack Foundation # # 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 @@ -48,6 +48,7 @@ def make_client(instance): tenant_id=instance._project_id, auth_url=instance._auth_url, region_name=instance._region_name) + instance.auth_ref = client.auth_ref return client diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index 580525367c..60a1f947fe 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -22,6 +22,7 @@ from cliff import lister from cliff import show +from openstackclient.common import exceptions from openstackclient.common import utils @@ -59,9 +60,10 @@ def take_action(self, parsed_args): ) user = utils.find_resource(identity_client.users, parsed_args.user) role = identity_client.roles.add_user_role( - user, - role, - project) + user.id, + role.id, + project.id, + ) info = {} info.update(role._info) @@ -150,14 +152,23 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity + auth_ref = self.app.client_manager.auth_ref - # user-only roles are not supported in KSL so we are - # required to have a user and project; default to the - # values used for authentication if not specified + # Project and user are required, if not included in command args + # default to the values used for authentication. For token-flow + # authentication they must be included on the command line. if not parsed_args.project: - parsed_args.project = identity_client.auth_tenant_id + if self.app.client_manager.auth_ref: + parsed_args.project = auth_ref.project_id + else: + msg = "Project must be specified" + raise exceptions.CommandError(msg) if not parsed_args.user: - parsed_args.user = identity_client.auth_user_id + if self.app.client_manager.auth_ref: + parsed_args.user = auth_ref.user_id + else: + msg = "User must be specified" + raise exceptions.CommandError(msg) project = utils.find_resource( identity_client.tenants, diff --git a/openstackclient/tests/identity/fakes.py b/openstackclient/tests/identity/fakes.py index 1c1ea72bc3..ee34a7b421 100644 --- a/openstackclient/tests/identity/fakes.py +++ b/openstackclient/tests/identity/fakes.py @@ -17,24 +17,10 @@ from openstackclient.tests import fakes - -user_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' -user_name = 'paul' -user_description = 'Sir Paul' -user_email = 'paul@applecorps.com' - project_id = '8-9-64' project_name = 'beatles' project_description = 'Fab Four' -USER = { - 'id': user_id, - 'name': user_name, - 'tenantId': project_id, - 'email': user_email, - 'enabled': True, -} - PROJECT = { 'id': project_id, 'name': project_name, @@ -49,9 +35,46 @@ 'enabled': True, } +role_id = '1' +role_name = 'boss' + +ROLE = { + 'id': role_id, + 'name': role_name, +} + +service_id = '1925-10-11' +service_name = 'elmore' +service_description = 'Leonard, Elmore, rip' +service_type = 'author' + +SERVICE = { + 'id': service_id, + 'name': service_name, + 'description': service_description, + 'type': service_type, +} + +user_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' +user_name = 'paul' +user_description = 'Sir Paul' +user_email = 'paul@applecorps.com' + +USER = { + 'id': user_id, + 'name': user_name, + 'tenantId': project_id, + 'email': user_email, + 'enabled': True, +} + class FakeIdentityv2Client(object): def __init__(self, **kwargs): + self.roles = mock.Mock() + self.roles.resource_class = fakes.FakeResource(None, {}) + self.services = mock.Mock() + self.services.resource_class = fakes.FakeResource(None, {}) self.tenants = mock.Mock() self.tenants.resource_class = fakes.FakeResource(None, {}) self.users = mock.Mock() diff --git a/openstackclient/tests/identity/v2_0/test_role.py b/openstackclient/tests/identity/v2_0/test_role.py new file mode 100644 index 0000000000..99860288ea --- /dev/null +++ b/openstackclient/tests/identity/v2_0/test_role.py @@ -0,0 +1,419 @@ +# Copyright 2013 Nebula Inc. +# +# 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 copy +import mock + +from openstackclient.common import exceptions +from openstackclient.identity.v2_0 import role +from openstackclient.tests import fakes +from openstackclient.tests.identity import fakes as identity_fakes +from openstackclient.tests.identity import test_identity + + +class TestRole(test_identity.TestIdentity): + + def setUp(self): + super(TestRole, self).setUp() + + # Get a shortcut to the TenantManager Mock + self.projects_mock = self.app.client_manager.identity.tenants + self.projects_mock.reset_mock() + # Get a shortcut to the UserManager Mock + self.users_mock = self.app.client_manager.identity.users + self.users_mock.reset_mock() + # Get a shortcut to the RoleManager Mock + self.roles_mock = self.app.client_manager.identity.roles + self.roles_mock.reset_mock() + + +class TestRoleAdd(TestRole): + + def setUp(self): + super(TestRoleAdd, self).setUp() + + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + self.users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + self.roles_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE), + loaded=True, + ) + self.roles_mock.add_user_role.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE), + loaded=True, + ) + + # Get the command object to test + self.cmd = role.AddRole(self.app, None) + + def test_role_add(self): + arglist = [ + '--project', identity_fakes.project_name, + '--user', identity_fakes.user_name, + identity_fakes.role_name, + ] + verifylist = [ + ('role', identity_fakes.role_name), + ('project', identity_fakes.project_name), + ('user', identity_fakes.user_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # RoleManager.add_user_role(user, role, tenant=None) + self.roles_mock.add_user_role.assert_called_with( + identity_fakes.user_id, + identity_fakes.role_id, + identity_fakes.project_id, + ) + + collist = ('id', 'name') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.role_id, + identity_fakes.role_name, + ) + self.assertEqual(data, datalist) + + +class TestRoleCreate(TestRole): + + def setUp(self): + super(TestRoleCreate, self).setUp() + + self.roles_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE), + loaded=True, + ) + + # Get the command object to test + self.cmd = role.CreateRole(self.app, None) + + def test_role_create_no_options(self): + arglist = [ + identity_fakes.role_name, + ] + verifylist = [ + ('role_name', identity_fakes.role_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # RoleManager.create(name) + self.roles_mock.create.assert_called_with( + identity_fakes.role_name, + ) + + collist = ('id', 'name') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.role_id, + identity_fakes.role_name, + ) + self.assertEqual(data, datalist) + + +class TestRoleDelete(TestRole): + + def setUp(self): + super(TestRoleDelete, self).setUp() + + self.roles_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE), + loaded=True, + ) + self.roles_mock.delete.return_value = None + + # Get the command object to test + self.cmd = role.DeleteRole(self.app, None) + + def test_role_delete_no_options(self): + arglist = [ + identity_fakes.role_name, + ] + verifylist = [ + ('role', identity_fakes.role_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + self.roles_mock.delete.assert_called_with( + identity_fakes.role_id, + ) + + +class TestRoleList(TestRole): + + def setUp(self): + super(TestRoleList, self).setUp() + + self.roles_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = role.ListRole(self.app, None) + + def test_role_list_no_options(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.roles_mock.list.assert_called_with() + + collist = ('ID', 'Name') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.role_id, + identity_fakes.role_name, + ), ) + self.assertEqual(tuple(data), datalist) + + +class TestUserRoleList(TestRole): + + def setUp(self): + super(TestUserRoleList, self).setUp() + + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + self.users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + self.roles_mock.roles_for_user.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = role.ListUserRole(self.app, None) + + def test_user_role_list_no_options(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # This argument combination should raise a CommandError + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + + def test_user_role_list_no_options_def_creds(self): + auth_ref = self.app.client_manager.auth_ref = mock.MagicMock() + auth_ref.project_id.return_value = identity_fakes.project_id + auth_ref.user_id.return_value = identity_fakes.user_id + + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.roles_mock.roles_for_user.assert_called_with( + identity_fakes.user_id, + identity_fakes.project_id, + ) + + collist = ('ID', 'Name', 'Project', 'User') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.role_id, + identity_fakes.role_name, + identity_fakes.project_name, + identity_fakes.user_name, + ), ) + self.assertEqual(tuple(data), datalist) + + def test_user_role_list_project(self): + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT_2), + loaded=True, + ) + arglist = [ + '--project', identity_fakes.PROJECT_2['name'], + ] + verifylist = [ + ('project', identity_fakes.PROJECT_2['name']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # This argument combination should raise a CommandError + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + + def test_user_role_list_project_def_creds(self): + auth_ref = self.app.client_manager.auth_ref = mock.MagicMock() + auth_ref.project_id.return_value = identity_fakes.project_id + auth_ref.user_id.return_value = identity_fakes.user_id + + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT_2), + loaded=True, + ) + arglist = [ + '--project', identity_fakes.PROJECT_2['name'], + ] + verifylist = [ + ('project', identity_fakes.PROJECT_2['name']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.roles_mock.roles_for_user.assert_called_with( + identity_fakes.user_id, + identity_fakes.PROJECT_2['id'], + ) + + collist = ('ID', 'Name', 'Project', 'User') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.role_id, + identity_fakes.role_name, + identity_fakes.PROJECT_2['name'], + identity_fakes.user_name, + ), ) + self.assertEqual(tuple(data), datalist) + + +class TestRoleRemove(TestRole): + + def setUp(self): + super(TestRoleRemove, self).setUp() + + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + self.users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + self.roles_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE), + loaded=True, + ) + self.roles_mock.remove_user_role.return_value = None + + # Get the command object to test + self.cmd = role.RemoveRole(self.app, None) + + def test_role_remove(self): + arglist = [ + '--project', identity_fakes.project_name, + '--user', identity_fakes.user_name, + identity_fakes.role_name, + ] + verifylist = [ + ('role', identity_fakes.role_name), + ('project', identity_fakes.project_name), + ('user', identity_fakes.user_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # RoleManager.remove_user_role(user, role, tenant=None) + self.roles_mock.remove_user_role.assert_called_with( + identity_fakes.user_id, + identity_fakes.role_id, + identity_fakes.project_id, + ) + + +class TestRoleShow(TestRole): + + def setUp(self): + super(TestRoleShow, self).setUp() + + self.roles_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE), + loaded=True, + ) + + # Get the command object to test + self.cmd = role.ShowRole(self.app, None) + + def test_service_show(self): + arglist = [ + identity_fakes.role_name, + ] + verifylist = [ + ('role', identity_fakes.role_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # RoleManager.get(role) + self.roles_mock.get.assert_called_with( + identity_fakes.role_name, + ) + + collist = ('id', 'name') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.role_id, + identity_fakes.role_name, + ) + self.assertEqual(data, datalist) diff --git a/openstackclient/tests/identity/v2_0/test_service.py b/openstackclient/tests/identity/v2_0/test_service.py new file mode 100644 index 0000000000..8d6bc17153 --- /dev/null +++ b/openstackclient/tests/identity/v2_0/test_service.py @@ -0,0 +1,243 @@ +# Copyright 2013 Nebula Inc. +# +# 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 copy + +from openstackclient.identity.v2_0 import service +from openstackclient.tests import fakes +from openstackclient.tests.identity import fakes as identity_fakes +from openstackclient.tests.identity import test_identity + + +class TestService(test_identity.TestIdentity): + + def setUp(self): + super(TestService, self).setUp() + + # Get a shortcut to the ServiceManager Mock + self.services_mock = self.app.client_manager.identity.services + self.services_mock.reset_mock() + + +class TestServiceCreate(TestService): + + def setUp(self): + super(TestServiceCreate, self).setUp() + + self.services_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.SERVICE), + loaded=True, + ) + + # Get the command object to test + self.cmd = service.CreateService(self.app, None) + + def test_service_create_minimum_options(self): + arglist = [ + '--type', identity_fakes.service_type, + identity_fakes.service_name, + ] + verifylist = [ + ('type', identity_fakes.service_type), + ('name', identity_fakes.service_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # ServiceManager.create(name, service_type, description) + self.services_mock.create.assert_called_with( + identity_fakes.service_name, + identity_fakes.service_type, + None, + ) + + collist = ('description', 'id', 'name', 'type') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.service_description, + identity_fakes.service_id, + identity_fakes.service_name, + identity_fakes.service_type, + ) + self.assertEqual(data, datalist) + + def test_service_create_description(self): + arglist = [ + '--type', identity_fakes.service_type, + '--description', identity_fakes.service_description, + identity_fakes.service_name, + ] + verifylist = [ + ('type', identity_fakes.service_type), + ('description', identity_fakes.service_description), + ('name', identity_fakes.service_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # ServiceManager.create(name, service_type, description) + self.services_mock.create.assert_called_with( + identity_fakes.service_name, + identity_fakes.service_type, + identity_fakes.service_description, + ) + + collist = ('description', 'id', 'name', 'type') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.service_description, + identity_fakes.service_id, + identity_fakes.service_name, + identity_fakes.service_type, + ) + self.assertEqual(data, datalist) + + +class TestServiceDelete(TestService): + + def setUp(self): + super(TestServiceDelete, self).setUp() + + self.services_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.SERVICE), + loaded=True, + ) + self.services_mock.delete.return_value = None + + # Get the command object to test + self.cmd = service.DeleteService(self.app, None) + + def test_service_delete_no_options(self): + arglist = [ + identity_fakes.service_name, + ] + verifylist = [ + ('service', identity_fakes.service_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + self.services_mock.delete.assert_called_with( + identity_fakes.service_name, + ) + + +class TestServiceList(TestService): + + def setUp(self): + super(TestServiceList, self).setUp() + + self.services_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.SERVICE), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = service.ListService(self.app, None) + + def test_service_list_no_options(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.services_mock.list.assert_called_with() + + collist = ('ID', 'Name') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.service_id, + identity_fakes.service_name, + ), ) + self.assertEqual(tuple(data), datalist) + + def test_service_list_long(self): + arglist = [ + '--long', + ] + verifylist = [ + ('long', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.services_mock.list.assert_called_with() + + collist = ('ID', 'Name', 'Type', 'Description') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.service_id, + identity_fakes.service_name, + identity_fakes.service_type, + identity_fakes.service_description, + ), ) + self.assertEqual(tuple(data), datalist) + + +class TestServiceShow(TestService): + + def setUp(self): + super(TestServiceShow, self).setUp() + + self.services_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.SERVICE), + loaded=True, + ) + + # Get the command object to test + self.cmd = service.ShowService(self.app, None) + + def test_service_show(self): + arglist = [ + identity_fakes.service_name, + ] + verifylist = [ + ('service', identity_fakes.service_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # ServiceManager.get(id) + self.services_mock.get.assert_called_with( + identity_fakes.service_name, + ) + + collist = ('description', 'id', 'name', 'type') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.service_description, + identity_fakes.service_id, + identity_fakes.service_name, + identity_fakes.service_type, + ) + self.assertEqual(data, datalist) From 916bb68dfdedcd977c9d88fad650c440b8f6a449 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 5 Sep 2013 12:54:14 -0500 Subject: [PATCH 0009/3494] Add to clientmanager tests Change-Id: Iea59c494f31de9c3e1d662f89e6e2babcc8fbd61 --- .../tests/common/test_clientmanager.py | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/openstackclient/tests/common/test_clientmanager.py b/openstackclient/tests/common/test_clientmanager.py index 395f6ec33d..6aee711d3e 100644 --- a/openstackclient/tests/common/test_clientmanager.py +++ b/openstackclient/tests/common/test_clientmanager.py @@ -1,4 +1,4 @@ -# Copyright 2012-2013 OpenStack, LLC. +# Copyright 2012-2013 OpenStack Foundation # # 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 @@ -17,6 +17,10 @@ from openstackclient.tests import utils +AUTH_TOKEN = "foobar" +AUTH_URL = "http://0.0.0.0" + + class Container(object): attr = clientmanager.ClientCache(lambda x: object()) @@ -28,8 +32,27 @@ class TestClientManager(utils.TestCase): def setUp(self): super(TestClientManager, self).setUp() + api_version = {"identity": "2.0"} + + self.client_manager = clientmanager.ClientManager( + token=AUTH_TOKEN, + url=AUTH_URL, + auth_url=AUTH_URL, + api_version=api_version, + ) + def test_singleton(self): # NOTE(dtroyer): Verify that the ClientCache descriptor only invokes # the factory one time and always returns the same value after that. c = Container() self.assertEqual(c.attr, c.attr) + + def test_make_client_identity_default(self): + self.assertEqual( + self.client_manager.identity.auth_token, + AUTH_TOKEN, + ) + self.assertEqual( + self.client_manager.identity.management_url, + AUTH_URL, + ) From 7a0a7d67ed639cf664f02e1148c7b4a9348f4672 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 9 Sep 2013 14:52:45 -0500 Subject: [PATCH 0010/3494] Prepare for Identity v3 tests * Split identity/fakes.py for v2_0 and v3 * Split identity/test_identity.py for v2_0 and v3 * Fix issues in commands with enable/disable * Clean up v2 commands Change-Id: I6e536b6a130fc556dbd7dcf9f2e76d939ca1bc1c --- openstackclient/identity/v2_0/project.py | 22 ++++-- openstackclient/identity/v2_0/service.py | 34 +++++--- openstackclient/identity/v2_0/user.py | 75 +++++++++++++----- openstackclient/tests/fakes.py | 6 +- .../tests/identity/test_identity.py | 50 ------------ .../tests/identity/{ => v2_0}/fakes.py | 10 --- .../tests/identity/v2_0/test_identity.py | 31 ++++++++ .../tests/identity/v2_0/test_project.py | 38 ++++----- .../tests/identity/v2_0/test_role.py | 6 +- .../tests/identity/v2_0/test_service.py | 11 +-- .../tests/identity/v2_0/test_user.py | 79 +++++++++++++++---- openstackclient/tests/utils.py | 1 + 12 files changed, 222 insertions(+), 141 deletions(-) delete mode 100644 openstackclient/tests/identity/test_identity.py rename openstackclient/tests/identity/{ => v2_0}/fakes.py (85%) create mode 100644 openstackclient/tests/identity/v2_0/test_identity.py diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index 0721a7ce20..2d0acb8fe0 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -33,7 +33,7 @@ class CreateProject(show.ShowOne): def get_parser(self, prog_name): parser = super(CreateProject, self).get_parser(prog_name) parser.add_argument( - 'project_name', + 'name', metavar='', help='New project name', ) @@ -57,13 +57,14 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) - identity_client = self.app.client_manager.identity + enabled = True if parsed_args.disable: enabled = False + project = identity_client.tenants.create( - parsed_args.project_name, + parsed_args.name, description=parsed_args.description, enabled=enabled, ) @@ -90,10 +91,12 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity + project = utils.find_resource( identity_client.tenants, parsed_args.project, ) + identity_client.tenants.delete(project.id) return @@ -164,8 +167,14 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) - identity_client = self.app.client_manager.identity + + if (not parsed_args.name + and not parsed_args.description + and not parsed_args.enable + and not parsed_args.disable): + return + project = utils.find_resource( identity_client.tenants, parsed_args.project, @@ -180,7 +189,6 @@ def take_action(self, parsed_args): kwargs['enabled'] = True if parsed_args.disable: kwargs['enabled'] = False - if 'id' in kwargs: del kwargs['id'] if 'name' in kwargs: @@ -188,8 +196,8 @@ def take_action(self, parsed_args): kwargs['tenant_name'] = kwargs['name'] del kwargs['name'] - if len(kwargs): - identity_client.tenants.update(project.id, **kwargs) + identity_client.tenants.update(project.id, **kwargs) + return class ShowProject(show.ShowOne): diff --git a/openstackclient/identity/v2_0/service.py b/openstackclient/identity/v2_0/service.py index 2e81805bb2..92d1e09915 100644 --- a/openstackclient/identity/v2_0/service.py +++ b/openstackclient/identity/v2_0/service.py @@ -28,7 +28,7 @@ class CreateService(show.ShowOne): - """Create service command""" + """Create new service""" log = logging.getLogger(__name__ + '.CreateService') @@ -37,21 +37,25 @@ def get_parser(self, prog_name): parser.add_argument( 'name', metavar='', - help='New service name') + help='New service name', + ) parser.add_argument( '--type', metavar='', required=True, - help='New service type') + help='New service type (compute, image, identity, volume, etc)', + ) parser.add_argument( '--description', metavar='', - help='New service description') + help='New service description', + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity + service = identity_client.services.create( parsed_args.name, parsed_args.type, @@ -63,7 +67,7 @@ def take_action(self, parsed_args): class DeleteService(command.Command): - """Delete service command""" + """Delete service""" log = logging.getLogger(__name__ + '.DeleteService') @@ -71,19 +75,26 @@ def get_parser(self, prog_name): parser = super(DeleteService, self).get_parser(prog_name) parser.add_argument( 'service', - metavar='', - help='ID of service to delete') + metavar='', + help='Service to delete (name or ID)', + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity - identity_client.services.delete(parsed_args.service) + + service = utils.find_resource( + identity_client.services, + parsed_args.service, + ) + + identity_client.services.delete(service.id) return class ListService(lister.Lister): - """List service command""" + """List services""" log = logging.getLogger(__name__ + '.ListService') @@ -98,6 +109,7 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) + if parsed_args.long: columns = ('ID', 'Name', 'Type', 'Description') else: @@ -111,7 +123,7 @@ def take_action(self, parsed_args): class ShowService(show.ShowOne): - """Show cloud service information""" + """Show service details""" log = logging.getLogger(__name__ + '.ShowService') @@ -120,7 +132,7 @@ def get_parser(self, prog_name): parser.add_argument( 'service', metavar='', - help='Type, name or ID of service to display', + help='Service to display (type, name or ID)', ) parser.add_argument( '--catalog', diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index 7174d4ce74..371c45a998 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -26,7 +26,7 @@ class CreateUser(show.ShowOne): - """Create user command""" + """Create new user""" log = logging.getLogger(__name__ + '.CreateUser') @@ -35,15 +35,18 @@ def get_parser(self, prog_name): parser.add_argument( 'name', metavar='', - help='New user name') + help='New user name', + ) parser.add_argument( '--password', metavar='', - help='New user password') + help='New user password', + ) parser.add_argument( '--email', metavar='', - help='New user email address') + help='New user email address', + ) parser.add_argument( '--project', metavar='', @@ -65,6 +68,7 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity + if parsed_args.project: project_id = utils.find_resource( identity_client.tenants, @@ -72,9 +76,11 @@ def take_action(self, parsed_args): ).id else: project_id = None + enabled = True if parsed_args.disable: enabled = False + user = identity_client.users.create( parsed_args.name, parsed_args.password, @@ -95,7 +101,7 @@ def take_action(self, parsed_args): class DeleteUser(command.Command): - """Delete user command""" + """Delete user""" log = logging.getLogger(__name__ + '.DeleteUser') @@ -104,19 +110,25 @@ def get_parser(self, prog_name): parser.add_argument( 'user', metavar='', - help='Name or ID of user to delete') + help='User to delete (name or ID)', + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity - user = utils.find_resource(identity_client.users, parsed_args.user) + + user = utils.find_resource( + identity_client.users, + parsed_args.user, + ) + identity_client.users.delete(user.id) return class ListUser(lister.Lister): - """List user command""" + """List users""" log = logging.getLogger(__name__ + '.ListUser') @@ -191,7 +203,7 @@ def _format_project(project): class SetUser(command.Command): - """Set user command""" + """Set user properties""" log = logging.getLogger(__name__ + '.SetUser') @@ -200,19 +212,23 @@ def get_parser(self, prog_name): parser.add_argument( 'user', metavar='', - help='Name or ID of user to change') + help='User to change (name or ID)', + ) parser.add_argument( '--name', metavar='', - help='New user name') + help='New user name', + ) parser.add_argument( '--password', metavar='', - help='New user password') + help='New user password', + ) parser.add_argument( '--email', metavar='', - help='New user email address') + help='New user email address', + ) parser.add_argument( '--project', metavar='', @@ -233,9 +249,21 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) - identity_client = self.app.client_manager.identity - user = utils.find_resource(identity_client.users, parsed_args.user) + + if (not parsed_args.name + and not parsed_args.name + and not parsed_args.password + and not parsed_args.email + and not parsed_args.project + and not parsed_args.enable + and not parsed_args.disable): + return + + user = utils.find_resource( + identity_client.users, + parsed_args.user, + ) if parsed_args.password: identity_client.users.update_password( @@ -258,17 +286,18 @@ def take_action(self, parsed_args): kwargs['name'] = parsed_args.name if parsed_args.email: kwargs['email'] = parsed_args.email + kwargs['enabled'] = user.enabled if parsed_args.enable: kwargs['enabled'] = True if parsed_args.disable: kwargs['enabled'] = False - if len(kwargs): - identity_client.users.update(user.id, **kwargs) + identity_client.users.update(user.id, **kwargs) + return class ShowUser(show.ShowOne): - """Show user command""" + """Show user details""" log = logging.getLogger(__name__ + '.ShowUser') @@ -277,13 +306,19 @@ def get_parser(self, prog_name): parser.add_argument( 'user', metavar='', - help='Name or ID of user to display') + help='User to display (name or ID)', + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity - user = utils.find_resource(identity_client.users, parsed_args.user) + + user = utils.find_resource( + identity_client.users, + parsed_args.user, + ) + if 'tenantId' in user._info: user._info.update( {'project_id': user._info.pop('tenantId')} diff --git a/openstackclient/tests/fakes.py b/openstackclient/tests/fakes.py index 22292a646f..d6cf1d742f 100644 --- a/openstackclient/tests/fakes.py +++ b/openstackclient/tests/fakes.py @@ -42,7 +42,11 @@ def __init__(self, _stdout): class FakeClientManager(object): def __init__(self): - pass + self.compute = None + self.identity = None + self.image = None + self.volume = None + self.auth_ref = None class FakeResource(object): diff --git a/openstackclient/tests/identity/test_identity.py b/openstackclient/tests/identity/test_identity.py deleted file mode 100644 index 894f47baa2..0000000000 --- a/openstackclient/tests/identity/test_identity.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright 2013 OpenStack, LLC. -# -# 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 openstackclient.common import clientmanager -from openstackclient.identity import client as identity_client -from openstackclient.tests import utils - - -AUTH_TOKEN = "foobar" -AUTH_URL = "http://0.0.0.0" - - -class TestIdentity(utils.TestCommand): - def setUp(self): - super(TestIdentity, self).setUp() - - api_version = {"identity": "2.0"} - - identity_client.API_VERSIONS = { - "2.0": "openstackclient.tests.identity.fakes.FakeIdentityv2Client" - } - - self.app.client_manager = clientmanager.ClientManager( - token=AUTH_TOKEN, - url=AUTH_URL, - auth_url=AUTH_URL, - api_version=api_version, - ) - - def test_make_client(self): - self.assertEqual( - self.app.client_manager.identity.auth_token, - AUTH_TOKEN, - ) - self.assertEqual( - self.app.client_manager.identity.management_url, - AUTH_URL, - ) diff --git a/openstackclient/tests/identity/fakes.py b/openstackclient/tests/identity/v2_0/fakes.py similarity index 85% rename from openstackclient/tests/identity/fakes.py rename to openstackclient/tests/identity/v2_0/fakes.py index ee34a7b421..b1aeabd4ca 100644 --- a/openstackclient/tests/identity/fakes.py +++ b/openstackclient/tests/identity/v2_0/fakes.py @@ -83,13 +83,3 @@ def __init__(self, **kwargs): self.ec2.resource_class = fakes.FakeResource(None, {}) self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] - - -class FakeIdentityv3Client(object): - def __init__(self, **kwargs): - self.domains = mock.Mock() - self.domains.resource_class = fakes.FakeResource(None, {}) - self.projects = mock.Mock() - self.projects.resource_class = fakes.FakeResource(None, {}) - self.users = mock.Mock() - self.users.resource_class = fakes.FakeResource(None, {}) diff --git a/openstackclient/tests/identity/v2_0/test_identity.py b/openstackclient/tests/identity/v2_0/test_identity.py new file mode 100644 index 0000000000..8a50a48a06 --- /dev/null +++ b/openstackclient/tests/identity/v2_0/test_identity.py @@ -0,0 +1,31 @@ +# Copyright 2013 Nebula Inc. +# +# 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 openstackclient.tests.identity.v2_0 import fakes +from openstackclient.tests import utils + + +AUTH_TOKEN = "foobar" +AUTH_URL = "http://0.0.0.0" + + +class TestIdentityv2(utils.TestCommand): + def setUp(self): + super(TestIdentityv2, self).setUp() + + self.app.client_manager.identity = fakes.FakeIdentityv2Client( + endpoint=AUTH_URL, + token=AUTH_TOKEN, + ) diff --git a/openstackclient/tests/identity/v2_0/test_project.py b/openstackclient/tests/identity/v2_0/test_project.py index c6c9b8ee01..933bd0941d 100644 --- a/openstackclient/tests/identity/v2_0/test_project.py +++ b/openstackclient/tests/identity/v2_0/test_project.py @@ -17,11 +17,11 @@ from openstackclient.identity.v2_0 import project from openstackclient.tests import fakes -from openstackclient.tests.identity import fakes as identity_fakes -from openstackclient.tests.identity import test_identity +from openstackclient.tests.identity.v2_0 import fakes as identity_fakes +from openstackclient.tests.identity.v2_0 import test_identity -class TestProject(test_identity.TestIdentity): +class TestProject(test_identity.TestIdentityv2): def setUp(self): super(TestProject, self).setUp() @@ -50,9 +50,9 @@ def test_project_create_no_options(self): identity_fakes.project_name, ] verifylist = [ - ('project_name', identity_fakes.project_name), ('enable', False), ('disable', False), + ('name', identity_fakes.project_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -85,8 +85,8 @@ def test_project_create_description(self): identity_fakes.project_name, ] verifylist = [ - ('project_name', identity_fakes.project_name), ('description', 'new desc'), + ('name', identity_fakes.project_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -119,9 +119,9 @@ def test_project_create_enable(self): identity_fakes.project_name, ] verifylist = [ - ('project_name', identity_fakes.project_name), ('enable', True), ('disable', False), + ('name', identity_fakes.project_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -154,9 +154,9 @@ def test_project_create_disable(self): identity_fakes.project_name, ] verifylist = [ - ('project_name', identity_fakes.project_name), ('enable', False), ('disable', True), + ('name', identity_fakes.project_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -284,6 +284,11 @@ def setUp(self): copy.deepcopy(identity_fakes.PROJECT), loaded=True, ) + self.projects_mock.update.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) # Get the command object to test self.cmd = project.SetProject(self.app, None) @@ -302,17 +307,6 @@ def test_project_set_no_options(self): result = self.cmd.run(parsed_args) self.assertEqual(result, 0) - # Set expected values - kwargs = { - 'description': identity_fakes.project_description, - 'enabled': True, - 'tenant_name': identity_fakes.project_name, - } - self.projects_mock.update.assert_called_with( - identity_fakes.project_id, - **kwargs - ) - def test_project_set_name(self): arglist = [ '--name', 'qwerty', @@ -320,6 +314,9 @@ def test_project_set_name(self): ] verifylist = [ ('name', 'qwerty'), + ('enable', False), + ('disable', False), + ('project', identity_fakes.project_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -344,6 +341,9 @@ def test_project_set_description(self): ] verifylist = [ ('description', 'new desc'), + ('enable', False), + ('disable', False), + ('project', identity_fakes.project_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -369,6 +369,7 @@ def test_project_set_enable(self): verifylist = [ ('enable', True), ('disable', False), + ('project', identity_fakes.project_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -394,6 +395,7 @@ def test_project_set_disable(self): verifylist = [ ('enable', False), ('disable', True), + ('project', identity_fakes.project_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/identity/v2_0/test_role.py b/openstackclient/tests/identity/v2_0/test_role.py index 99860288ea..58de8e52ce 100644 --- a/openstackclient/tests/identity/v2_0/test_role.py +++ b/openstackclient/tests/identity/v2_0/test_role.py @@ -19,11 +19,11 @@ from openstackclient.common import exceptions from openstackclient.identity.v2_0 import role from openstackclient.tests import fakes -from openstackclient.tests.identity import fakes as identity_fakes -from openstackclient.tests.identity import test_identity +from openstackclient.tests.identity.v2_0 import fakes as identity_fakes +from openstackclient.tests.identity.v2_0 import test_identity -class TestRole(test_identity.TestIdentity): +class TestRole(test_identity.TestIdentityv2): def setUp(self): super(TestRole, self).setUp() diff --git a/openstackclient/tests/identity/v2_0/test_service.py b/openstackclient/tests/identity/v2_0/test_service.py index 8d6bc17153..f09c4137ad 100644 --- a/openstackclient/tests/identity/v2_0/test_service.py +++ b/openstackclient/tests/identity/v2_0/test_service.py @@ -17,11 +17,11 @@ from openstackclient.identity.v2_0 import service from openstackclient.tests import fakes -from openstackclient.tests.identity import fakes as identity_fakes -from openstackclient.tests.identity import test_identity +from openstackclient.tests.identity.v2_0 import fakes as identity_fakes +from openstackclient.tests.identity.v2_0 import test_identity -class TestService(test_identity.TestIdentity): +class TestService(test_identity.TestIdentityv2): def setUp(self): super(TestService, self).setUp() @@ -45,13 +45,14 @@ def setUp(self): # Get the command object to test self.cmd = service.CreateService(self.app, None) - def test_service_create_minimum_options(self): + def test_service_create_name_type(self): arglist = [ '--type', identity_fakes.service_type, identity_fakes.service_name, ] verifylist = [ ('type', identity_fakes.service_type), + ('description', None), ('name', identity_fakes.service_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -138,7 +139,7 @@ def test_service_delete_no_options(self): self.cmd.take_action(parsed_args) self.services_mock.delete.assert_called_with( - identity_fakes.service_name, + identity_fakes.service_id, ) diff --git a/openstackclient/tests/identity/v2_0/test_user.py b/openstackclient/tests/identity/v2_0/test_user.py index 529173da5d..2fe585ed96 100644 --- a/openstackclient/tests/identity/v2_0/test_user.py +++ b/openstackclient/tests/identity/v2_0/test_user.py @@ -17,11 +17,11 @@ from openstackclient.identity.v2_0 import user from openstackclient.tests import fakes -from openstackclient.tests.identity import fakes as identity_fakes -from openstackclient.tests.identity import test_identity +from openstackclient.tests.identity.v2_0 import fakes as identity_fakes +from openstackclient.tests.identity.v2_0 import test_identity -class TestUser(test_identity.TestIdentity): +class TestUser(test_identity.TestIdentityv2): def setUp(self): super(TestUser, self).setUp() @@ -29,6 +29,7 @@ def setUp(self): # Get a shortcut to the TenantManager Mock self.projects_mock = self.app.client_manager.identity.tenants self.projects_mock.reset_mock() + # Get a shortcut to the UserManager Mock self.users_mock = self.app.client_manager.identity.users self.users_mock.reset_mock() @@ -44,6 +45,7 @@ def setUp(self): copy.deepcopy(identity_fakes.PROJECT), loaded=True, ) + self.users_mock.create.return_value = fakes.FakeResource( None, copy.deepcopy(identity_fakes.USER), @@ -58,6 +60,8 @@ def test_user_create_no_options(self): identity_fakes.user_name, ] verifylist = [ + ('enable', False), + ('disable', False), ('name', identity_fakes.user_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -70,7 +74,7 @@ def test_user_create_no_options(self): 'enabled': True, 'tenant_id': None, } - # users.create(name, password, email, tenant_id=None, enabled=True) + # UserManager.create(name, password, email, tenant_id=, enabled=) self.users_mock.create.assert_called_with( identity_fakes.user_name, None, @@ -108,7 +112,7 @@ def test_user_create_password(self): 'enabled': True, 'tenant_id': None, } - # users.create(name, password, email, tenant_id=None, enabled=True) + # UserManager.create(name, password, email, tenant_id=, enabled=) self.users_mock.create.assert_called_with( identity_fakes.user_name, 'secret', @@ -146,7 +150,7 @@ def test_user_create_email(self): 'enabled': True, 'tenant_id': None, } - # users.create(name, password, email, tenant_id=None, enabled=True) + # UserManager.create(name, password, email, tenant_id=, enabled=) self.users_mock.create.assert_called_with( identity_fakes.user_name, None, @@ -199,7 +203,7 @@ def test_user_create_project(self): 'enabled': True, 'tenant_id': identity_fakes.PROJECT_2['id'], } - # users.create(name, password, email, tenant_id=None, enabled=True) + # UserManager.create(name, password, email, tenant_id=, enabled=) self.users_mock.create.assert_called_with( identity_fakes.user_name, None, @@ -238,7 +242,7 @@ def test_user_create_enable(self): 'enabled': True, 'tenant_id': None, } - # users.create(name, password, email, tenant_id=None, enabled=True) + # UserManager.create(name, password, email, tenant_id=, enabled=) self.users_mock.create.assert_called_with( identity_fakes.user_name, None, @@ -277,7 +281,7 @@ def test_user_create_disable(self): 'enabled': False, 'tenant_id': None, } - # users.create(name, password, email, tenant_id=None, enabled=True) + # UserManager.create(name, password, email, tenant_id=, enabled=) self.users_mock.create.assert_called_with( identity_fakes.user_name, None, @@ -342,6 +346,7 @@ def setUp(self): loaded=True, ), ] + self.users_mock.list.return_value = [ fakes.FakeResource( None, @@ -443,18 +448,18 @@ def test_user_set_no_options(self): identity_fakes.user_name, ] verifylist = [ - ('user', identity_fakes.user_name), + ('name', None), + ('password', None), + ('email', None), + ('project', None), ('enable', False), ('disable', False), - ('project', None), + ('user', identity_fakes.user_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # DisplayCommandBase.take_action() returns two tuples - self.cmd.take_action(parsed_args) - - # SetUser doesn't call anything if no args are passed - self.assertFalse(self.users_mock.update.called) + result = self.cmd.run(parsed_args) + self.assertEqual(result, 0) def test_user_set_name(self): arglist = [ @@ -463,6 +468,12 @@ def test_user_set_name(self): ] verifylist = [ ('name', 'qwerty'), + ('password', None), + ('email', None), + ('project', None), + ('enable', False), + ('disable', False), + ('user', identity_fakes.user_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -471,8 +482,10 @@ def test_user_set_name(self): # Set expected values kwargs = { + 'enabled': True, 'name': 'qwerty', } + # UserManager.update(user, **kwargs) self.users_mock.update.assert_called_with( identity_fakes.user_id, **kwargs @@ -484,13 +497,20 @@ def test_user_set_password(self): identity_fakes.user_name, ] verifylist = [ + ('name', None), ('password', 'secret'), + ('email', None), + ('project', None), + ('enable', False), + ('disable', False), + ('user', identity_fakes.user_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # DisplayCommandBase.take_action() returns two tuples self.cmd.take_action(parsed_args) + # UserManager.update_password(user, password) self.users_mock.update_password.assert_called_with( identity_fakes.user_id, 'secret', @@ -502,7 +522,13 @@ def test_user_set_email(self): identity_fakes.user_name, ] verifylist = [ + ('name', None), + ('password', None), ('email', 'barney@example.com'), + ('project', None), + ('enable', False), + ('disable', False), + ('user', identity_fakes.user_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -512,7 +538,9 @@ def test_user_set_email(self): # Set expected values kwargs = { 'email': 'barney@example.com', + 'enabled': True, } + # UserManager.update(user, **kwargs) self.users_mock.update.assert_called_with( identity_fakes.user_id, **kwargs @@ -524,13 +552,20 @@ def test_user_set_project(self): identity_fakes.user_name, ] verifylist = [ + ('name', None), + ('password', None), + ('email', None), ('project', identity_fakes.project_id), + ('enable', False), + ('disable', False), + ('user', identity_fakes.user_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # DisplayCommandBase.take_action() returns two tuples self.cmd.take_action(parsed_args) + # UserManager.update_tenant(user, tenant) self.users_mock.update_tenant.assert_called_with( identity_fakes.user_id, identity_fakes.project_id, @@ -542,8 +577,13 @@ def test_user_set_enable(self): identity_fakes.user_name, ] verifylist = [ + ('name', None), + ('password', None), + ('email', None), + ('project', None), ('enable', True), ('disable', False), + ('user', identity_fakes.user_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -554,6 +594,7 @@ def test_user_set_enable(self): kwargs = { 'enabled': True, } + # UserManager.update(user, **kwargs) self.users_mock.update.assert_called_with( identity_fakes.user_id, **kwargs @@ -565,8 +606,13 @@ def test_user_set_disable(self): identity_fakes.user_name, ] verifylist = [ + ('name', None), + ('password', None), + ('email', None), + ('project', None), ('enable', False), ('disable', True), + ('user', identity_fakes.user_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -577,6 +623,7 @@ def test_user_set_disable(self): kwargs = { 'enabled': False, } + # UserManager.update(user, **kwargs) self.users_mock.update.assert_called_with( identity_fakes.user_id, **kwargs diff --git a/openstackclient/tests/utils.py b/openstackclient/tests/utils.py index cf7fda6d56..ff7d8a336d 100644 --- a/openstackclient/tests/utils.py +++ b/openstackclient/tests/utils.py @@ -70,6 +70,7 @@ def setUp(self): # Build up a fake app self.fake_stdout = fakes.FakeStdout() self.app = fakes.FakeApp(self.fake_stdout) + self.app.client_manager = fakes.FakeClientManager() def check_parser(self, cmd, args, verify_args): cmd_parser = cmd.get_parser('check_parser') From 8898e020fb874871e4d120e686abac0a94afd392 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 9 Sep 2013 14:55:07 -0500 Subject: [PATCH 0011/3494] Identity v3 tests * Add project, user, role and service v3 tests * Fix issues in commands with enable/disable * Make commands and tests more consistent between versions * Make formatting and comments more consistent Change-Id: Id21e7a5abd7e421a7742f937861ec46b53095fc7 --- openstackclient/identity/v2_0/role.py | 10 +- openstackclient/identity/v3/project.py | 131 ++- openstackclient/identity/v3/role.py | 189 +++- openstackclient/identity/v3/service.py | 113 +- openstackclient/identity/v3/user.py | 86 +- .../tests/identity/v2_0/test_role.py | 10 +- openstackclient/tests/identity/v3/__init__.py | 14 + openstackclient/tests/identity/v3/fakes.py | 106 ++ .../tests/identity/v3/test_identity.py | 31 + .../tests/identity/v3/test_project.py | 531 ++++++++++ .../tests/identity/v3/test_role.py | 550 ++++++++++ .../tests/identity/v3/test_service.py | 408 ++++++++ .../tests/identity/v3/test_user.py | 986 ++++++++++++++++++ 13 files changed, 2982 insertions(+), 183 deletions(-) create mode 100644 openstackclient/tests/identity/v3/__init__.py create mode 100644 openstackclient/tests/identity/v3/fakes.py create mode 100644 openstackclient/tests/identity/v3/test_identity.py create mode 100644 openstackclient/tests/identity/v3/test_project.py create mode 100644 openstackclient/tests/identity/v3/test_role.py create mode 100644 openstackclient/tests/identity/v3/test_service.py create mode 100644 openstackclient/tests/identity/v3/test_user.py diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index 60a1f947fe..fdf211082d 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -103,13 +103,19 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help='Name or ID of role to delete') + help='Name or ID of role to delete', + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity - role = utils.find_resource(identity_client.roles, parsed_args.role) + + role = utils.find_resource( + identity_client.roles, + parsed_args.role, + ) + identity_client.roles.delete(role.id) return diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 05722b54d1..f245a88898 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -17,7 +17,6 @@ import logging import six -import sys from cliff import command from cliff import lister @@ -27,38 +26,38 @@ class CreateProject(show.ShowOne): - """Create project command""" + """Create new project""" log = logging.getLogger(__name__ + '.CreateProject') def get_parser(self, prog_name): parser = super(CreateProject, self).get_parser(prog_name) parser.add_argument( - 'project_name', + 'name', metavar='', - help='New project name') + help='New project name', + ) parser.add_argument( '--domain', metavar='', - help='References the domain name or ID which owns the project') + help='Domain owning the project (name or ID)', + ) parser.add_argument( '--description', metavar='', - help='New project description') - # FIXME (stevemar): need to update enabled/disabled as per Dolph's - # comments in 19999/4 + help='New project description', + ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', - dest='enabled', action='store_true', - default=True, - help='Enable project') + help='Enable project', + ) enable_group.add_argument( '--disable', - dest='enabled', - action='store_false', - help='Disable project') + action='store_true', + help='Disable project', + ) return parser def take_action(self, parsed_args): @@ -66,16 +65,23 @@ def take_action(self, parsed_args): identity_client = self.app.client_manager.identity if parsed_args.domain: - domain = utils.find_resource(identity_client.domains, - parsed_args.domain).id + domain = utils.find_resource( + identity_client.domains, + parsed_args.domain, + ).id else: domain = None + enabled = True + if parsed_args.disable: + enabled = False + project = identity_client.projects.create( - parsed_args.project_name, - domain=domain, + parsed_args.name, + domain, description=parsed_args.description, - enabled=parsed_args.enabled) + enabled=enabled, + ) info = {} info.update(project._info) @@ -83,7 +89,7 @@ def take_action(self, parsed_args): class DeleteProject(command.Command): - """Delete project command""" + """Delete project""" log = logging.getLogger(__name__ + '.DeleteProject') @@ -92,20 +98,25 @@ def get_parser(self, prog_name): parser.add_argument( 'project', metavar='', - help='Name or ID of project to delete') + help='Project to delete (name or ID)', + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity - project = utils.find_resource(identity_client.projects, - parsed_args.project) + + project = utils.find_resource( + identity_client.projects, + parsed_args.project, + ) + identity_client.projects.delete(project.id) return class ListProject(lister.Lister): - """List project command""" + """List projects""" log = logging.getLogger(__name__ + '.ListProject') @@ -115,7 +126,8 @@ def get_parser(self, prog_name): '--long', action='store_true', default=False, - help='Additional fields are listed in output') + help='List additional fields in output', + ) return parser def take_action(self, parsed_args): @@ -133,7 +145,7 @@ def take_action(self, parsed_args): class SetProject(command.Command): - """Set project command""" + """Set project properties""" log = logging.getLogger(__name__ + '.SetProject') @@ -142,54 +154,75 @@ def get_parser(self, prog_name): parser.add_argument( 'project', metavar='', - help='Name or ID of project to change') + help='Project to change (name or ID)', + ) parser.add_argument( '--name', metavar='', - help='New project name') + help='New project name', + ) parser.add_argument( '--domain', metavar='', - help='New domain name or ID that will now own the project') + help='New domain owning the project (name or ID)', + ) parser.add_argument( '--description', metavar='', - help='New project description') + help='New project description', + ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', - dest='enabled', action='store_true', - default=True, - help='Enable project (default)') + help='Enable project', + ) enable_group.add_argument( '--disable', - dest='enabled', - action='store_false', - help='Disable project') + action='store_true', + help='Disable project', + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity - project = utils.find_resource(identity_client.projects, - parsed_args.project) - kwargs = {} + + if (not parsed_args.name + and not parsed_args.description + and not parsed_args.domain + and not parsed_args.enable + and not parsed_args.disable): + return + + project = utils.find_resource( + identity_client.projects, + parsed_args.project, + ) + + kwargs = project._info if parsed_args.name: kwargs['name'] = parsed_args.name if parsed_args.domain: - domain = utils.find_resource( - identity_client.domains, parsed_args.domain).id - kwargs['domain'] = domain + kwargs['domain'] = utils.find_resource( + identity_client.domains, + parsed_args.domain, + ).id if parsed_args.description: kwargs['description'] = parsed_args.description - if 'enabled' in parsed_args: - kwargs['enabled'] = parsed_args.enabled - - if kwargs == {}: - sys.stdout.write("Project not updated, no arguments present") - return - project.update(**kwargs) + if parsed_args.enable: + kwargs['enabled'] = True + if parsed_args.disable: + kwargs['enabled'] = False + if 'id' in kwargs: + del kwargs['id'] + if 'domain_id' in kwargs: + # Hack around borken Identity API arg names + kwargs.update( + {'domain': kwargs.pop('domain_id')} + ) + + identity_client.projects.update(project.id, **kwargs) return diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index 5403d4cb8c..05bdbbfc01 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -53,7 +53,6 @@ def get_parser(self, prog_name): domain_or_project.add_argument( '--domain', metavar='', - default='default', help='Name or ID of domain associated with user or group', ) domain_or_project.add_argument( @@ -69,36 +68,69 @@ def take_action(self, parsed_args): if (not parsed_args.user and not parsed_args.domain and not parsed_args.group and not parsed_args.project): - sys.stderr.write("Role not added, no arguments present\n") return - role_id = utils.find_resource(identity_client.roles, - parsed_args.role).id + role = utils.find_resource( + identity_client.roles, + parsed_args.role, + ) if parsed_args.user and parsed_args.domain: - user = utils.find_resource(identity_client.users, - parsed_args.user) - domain = utils.find_resource(identity_client.domains, - parsed_args.domain) - identity_client.roles.grant(role_id, user=user, domain=domain) + user = utils.find_resource( + identity_client.users, + parsed_args.user, + ) + domain = utils.find_resource( + identity_client.domains, + parsed_args.domain, + ) + identity_client.roles.grant( + role.id, + user=user.id, + domain=domain.id, + ) elif parsed_args.user and parsed_args.project: - user = utils.find_resource(identity_client.users, - parsed_args.user) - project = utils.find_resource(identity_client.projects, - parsed_args.project) - identity_client.roles.grant(role_id, user=user, project=project) + user = utils.find_resource( + identity_client.users, + parsed_args.user, + ) + project = utils.find_resource( + identity_client.projects, + parsed_args.project, + ) + identity_client.roles.grant( + role.id, + user=user.id, + project=project.id, + ) elif parsed_args.group and parsed_args.domain: - group = utils.find_resource(identity_client.groups, - parsed_args.group) - domain = utils.find_resource(identity_client.domains, - parsed_args.domain) - identity_client.roles.grant(role_id, group=group, domain=domain) + group = utils.find_resource( + identity_client.groups, + parsed_args.group, + ) + domain = utils.find_resource( + identity_client.domains, + parsed_args.domain, + ) + identity_client.roles.grant( + role.id, + group=group.id, + domain=domain.id, + ) elif parsed_args.group and parsed_args.project: - group = utils.find_resource(identity_client.group, - parsed_args.group) - project = utils.find_resource(identity_client.projects, - parsed_args.project) - identity_client.roles.grant(role_id, group=group, project=project) + group = utils.find_resource( + identity_client.groups, + parsed_args.group, + ) + project = utils.find_resource( + identity_client.projects, + parsed_args.project, + ) + identity_client.roles.grant( + role.id, + group=group.id, + project=project.id, + ) else: sys.stderr.write("Role not added, incorrect set of arguments \ provided. See openstack --help for more details\n") @@ -122,6 +154,7 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity + role = identity_client.roles.create(parsed_args.name) return zip(*sorted(six.iteritems(role._info))) @@ -144,9 +177,13 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity - role_id = utils.find_resource(identity_client.roles, - parsed_args.role) - identity_client.roles.delete(role_id) + + role = utils.find_resource( + identity_client.roles, + parsed_args.role, + ) + + identity_client.roles.delete(role.id) return @@ -208,36 +245,69 @@ def take_action(self, parsed_args): if (not parsed_args.user and not parsed_args.domain and not parsed_args.group and not parsed_args.project): - sys.stdout.write("Role not updated, no arguments present\n") return - role_id = utils.find_resource(identity_client.roles, - parsed_args.role).id + role = utils.find_resource( + identity_client.roles, + parsed_args.role, + ) if parsed_args.user and parsed_args.domain: - user = utils.find_resource(identity_client.users, - parsed_args.user) - domain = utils.find_resource(identity_client.domains, - parsed_args.domain) - identity_client.roles.revoke(role_id, user=user, domain=domain) + user = utils.find_resource( + identity_client.users, + parsed_args.user, + ) + domain = utils.find_resource( + identity_client.domains, + parsed_args.domain, + ) + identity_client.roles.revoke( + role.id, + user=user.id, + domain=domain.id, + ) elif parsed_args.user and parsed_args.project: - user = utils.find_resource(identity_client.users, - parsed_args.user) - project = utils.find_resource(identity_client.projects, - parsed_args.project) - identity_client.roles.revoke(role_id, user=user, project=project) - elif parsed_args.group and parsed_args.project: - group = utils.find_resource(identity_client.group, - parsed_args.group) - project = utils.find_resource(identity_client.projects, - parsed_args.project) - identity_client.roles.revoke(role_id, group=group, project=project) + user = utils.find_resource( + identity_client.users, + parsed_args.user, + ) + project = utils.find_resource( + identity_client.projects, + parsed_args.project, + ) + identity_client.roles.revoke( + role.id, + user=user.id, + project=project.id, + ) elif parsed_args.group and parsed_args.domain: - group = utils.find_resource(identity_client.group, - parsed_args.group) - domain = utils.find_resource(identity_client.domains, - parsed_args.domain) - identity_client.roles.revoke(role_id, group=group, domain=domain) + group = utils.find_resource( + identity_client.groups, + parsed_args.group, + ) + domain = utils.find_resource( + identity_client.domains, + parsed_args.domain, + ) + identity_client.roles.revoke( + role.id, + group=group.id, + domain=domain.id, + ) + elif parsed_args.group and parsed_args.project: + group = utils.find_resource( + identity_client.groups, + parsed_args.group, + ) + project = utils.find_resource( + identity_client.projects, + parsed_args.project, + ) + identity_client.roles.revoke( + role.id, + group=group.id, + project=project.id, + ) else: sys.stderr.write("Role not removed, incorrect set of arguments \ provided. See openstack --help for more details\n") @@ -266,14 +336,16 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity - role_id = utils.find_resource(identity_client.roles, - parsed_args.role) if not parsed_args.name: - sys.stderr.write("Role not updated, no arguments present") return - identity_client.roles.update(role_id, parsed_args.name) + role = utils.find_resource( + identity_client.roles, + parsed_args.role, + ) + + identity_client.roles.update(role.id, name=parsed_args.name) return @@ -294,7 +366,10 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity - role = utils.find_resource(identity_client.roles, - parsed_args.role) + + role = utils.find_resource( + identity_client.roles, + parsed_args.role, + ) return zip(*sorted(six.iteritems(role._info))) diff --git a/openstackclient/identity/v3/service.py b/openstackclient/identity/v3/service.py index 77efbeadfc..7e3bfc6b2d 100644 --- a/openstackclient/identity/v3/service.py +++ b/openstackclient/identity/v3/service.py @@ -17,7 +17,6 @@ import logging import six -import sys from cliff import command from cliff import lister @@ -27,7 +26,7 @@ class CreateService(show.ShowOne): - """Create service command""" + """Create new service""" log = logging.getLogger(__name__ + '.CreateService') @@ -36,38 +35,45 @@ def get_parser(self, prog_name): parser.add_argument( 'type', metavar='', - help='New service type (compute, image, identity, volume, etc)') + help='New service type (compute, image, identity, volume, etc)', + ) parser.add_argument( '--name', metavar='', - help='New service name') + help='New service name', + ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', - dest='enabled', action='store_true', - default=True, - help='Enable user') + help='Enable project', + ) enable_group.add_argument( '--disable', - dest='enabled', - action='store_false', - help='Disable user') + action='store_true', + help='Disable project', + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity + + enabled = True + if parsed_args.disable: + enabled = False + service = identity_client.services.create( parsed_args.name, parsed_args.type, - parsed_args.enabled) + enabled, + ) return zip(*sorted(six.iteritems(service._info))) class DeleteService(command.Command): - """Delete service command""" + """Delete service""" log = logging.getLogger(__name__ + '.DeleteService') @@ -76,27 +82,31 @@ def get_parser(self, prog_name): parser.add_argument( 'service', metavar='', - help='Name or ID of service to delete') + help='Service to delete (name or ID)', + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity - service_id = utils.find_resource( - identity_client.services, parsed_args.service).id + service = utils.find_resource( + identity_client.services, + parsed_args.service, + ) - identity_client.services.delete(service_id) + identity_client.services.delete(service.id) return class ListService(lister.Lister): - """List service command""" + """List services""" log = logging.getLogger(__name__ + '.ListService') def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) + columns = ('ID', 'Name', 'Type', 'Enabled') data = self.app.client_manager.identity.services.list() return (columns, @@ -106,8 +116,8 @@ def take_action(self, parsed_args): ) for s in data)) -class SetService(show.ShowOne): - """Set service command""" +class SetService(command.Command): + """Set service properties""" log = logging.getLogger(__name__ + '.SetService') @@ -116,51 +126,67 @@ def get_parser(self, prog_name): parser.add_argument( 'service', metavar='', - help='Service name or ID to update') + help='Service to update (name or ID)', + ) parser.add_argument( '--type', metavar='', - help='New service type (compute, image, identity, volume, etc)') + help='New service type (compute, image, identity, volume, etc)', + ) parser.add_argument( '--name', metavar='', - help='New service name') + help='New service name', + ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', - dest='enabled', action='store_true', - default=True, - help='Enable user') + help='Enable project', + ) enable_group.add_argument( '--disable', - dest='enabled', - action='store_false', - help='Disable user') + action='store_true', + help='Disable project', + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity - service = utils.find_resource(identity_client.services, - parsed_args.service) - - if not parsed_args.name and not parsed_args.type: - sys.stdout.write("Service not updated, no arguments present") + if (not parsed_args.name + and not parsed_args.type + and not parsed_args.enable + and not parsed_args.disable): return - identity_client.services.update( - service, - parsed_args.name, - parsed_args.type, - parsed_args.enabled) + service = utils.find_resource( + identity_client.services, + parsed_args.service, + ) + + kwargs = service._info + if parsed_args.type: + kwargs['type'] = parsed_args.type + if parsed_args.name: + kwargs['name'] = parsed_args.name + if parsed_args.enable: + kwargs['enabled'] = True + if parsed_args.disable: + kwargs['enabled'] = False + if 'id' in kwargs: + del kwargs['id'] + identity_client.services.update( + service.id, + **kwargs + ) return class ShowService(show.ShowOne): - """Show service command""" + """Show service details""" log = logging.getLogger(__name__ + '.ShowService') @@ -169,14 +195,17 @@ def get_parser(self, prog_name): parser.add_argument( 'service', metavar='', - help='Type, name or ID of service to display') + help='Service to display (type, name or ID)', + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity - service = utils.find_resource(identity_client.services, - parsed_args.service) + service = utils.find_resource( + identity_client.services, + parsed_args.service, + ) return zip(*sorted(six.iteritems(service._info))) diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index b90527a3f5..54ffe561a3 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -27,7 +27,7 @@ class CreateUser(show.ShowOne): - """Create user command""" + """Create new user""" log = logging.getLogger(__name__ + '.CreateUser') @@ -51,7 +51,7 @@ def get_parser(self, prog_name): parser.add_argument( '--project', metavar='', - help='New default project name or ID', + help='Set default project (name or ID)', ) parser.add_argument( '--domain', @@ -66,15 +66,12 @@ def get_parser(self, prog_name): enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', - dest='enabled', action='store_true', - default=True, - help='Enable user', + help='Enable user (default)', ) enable_group.add_argument( '--disable', - dest='enabled', - action='store_false', + action='store_true', help='Disable user', ) return parser @@ -85,7 +82,9 @@ def take_action(self, parsed_args): if parsed_args.project: project_id = utils.find_resource( - identity_client.projects, parsed_args.project).id + identity_client.projects, + parsed_args.project, + ).id else: project_id = None @@ -95,14 +94,18 @@ def take_action(self, parsed_args): else: domain_id = None + enabled = True + if parsed_args.disable: + enabled = False + user = identity_client.users.create( parsed_args.name, - domain_id, - project_id, - parsed_args.password, - parsed_args.email, - parsed_args.description, - parsed_args.enabled + domain=domain_id, + default_project=project_id, + password=parsed_args.password, + email=parsed_args.email, + description=parsed_args.description, + enabled=enabled ) info = {} @@ -111,7 +114,7 @@ def take_action(self, parsed_args): class DeleteUser(command.Command): - """Delete user command""" + """Delete user""" log = logging.getLogger(__name__ + '.DeleteUser') @@ -120,15 +123,19 @@ def get_parser(self, prog_name): parser.add_argument( 'user', metavar='', - help='Name or ID of user to delete', + help='User to delete (name or ID)', ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity + user = utils.find_resource( - identity_client.users, parsed_args.user) + identity_client.users, + parsed_args.user, + ) + identity_client.users.delete(user.id) return @@ -245,7 +252,7 @@ def take_action(self, parsed_args): class SetUser(command.Command): - """Set user command""" + """Set user properties""" log = logging.getLogger(__name__ + '.SetUser') @@ -254,7 +261,7 @@ def get_parser(self, prog_name): parser.add_argument( 'user', metavar='', - help='Name or ID of user to change', + help='User to change (name or ID)', ) parser.add_argument( '--name', @@ -289,15 +296,12 @@ def get_parser(self, prog_name): enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', - dest='enabled', action='store_true', - default=True, help='Enable user (default)', ) enable_group.add_argument( '--disable', - dest='enabled', - action='store_false', + action='store_true', help='Disable user', ) return parser @@ -305,8 +309,23 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity + + if (not parsed_args.name + and not parsed_args.name + and not parsed_args.password + and not parsed_args.email + and not parsed_args.domain + and not parsed_args.project + and not parsed_args.description + and not parsed_args.enable + and not parsed_args.disable): + return + user = utils.find_resource( - identity_client.users, parsed_args.user) + identity_client.users, + parsed_args.user, + ) + kwargs = {} if parsed_args.name: kwargs['name'] = parsed_args.name @@ -324,18 +343,18 @@ def take_action(self, parsed_args): domain_id = utils.find_resource( identity_client.domains, parsed_args.domain).id kwargs['domainId'] = domain_id - if 'enabled' in parsed_args: - kwargs['enabled'] = parsed_args.enabled + kwargs['enabled'] = user.enabled + if parsed_args.enable: + kwargs['enabled'] = True + if parsed_args.disable: + kwargs['enabled'] = False - if not len(kwargs): - sys.stderr.write("User not updated, no arguments present") - return identity_client.users.update(user.id, **kwargs) return class ShowUser(show.ShowOne): - """Show user command""" + """Show user details""" log = logging.getLogger(__name__ + '.ShowUser') @@ -344,15 +363,18 @@ def get_parser(self, prog_name): parser.add_argument( 'user', metavar='', - help='Name or ID of user to display', + help='User to display (name or ID)', ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity + user = utils.find_resource( - identity_client.users, parsed_args.user) + identity_client.users, + parsed_args.user, + ) info = {} info.update(user._info) diff --git a/openstackclient/tests/identity/v2_0/test_role.py b/openstackclient/tests/identity/v2_0/test_role.py index 58de8e52ce..56e9d4cb54 100644 --- a/openstackclient/tests/identity/v2_0/test_role.py +++ b/openstackclient/tests/identity/v2_0/test_role.py @@ -31,9 +31,11 @@ def setUp(self): # Get a shortcut to the TenantManager Mock self.projects_mock = self.app.client_manager.identity.tenants self.projects_mock.reset_mock() + # Get a shortcut to the UserManager Mock self.users_mock = self.app.client_manager.identity.users self.users_mock.reset_mock() + # Get a shortcut to the RoleManager Mock self.roles_mock = self.app.client_manager.identity.roles self.roles_mock.reset_mock() @@ -49,11 +51,13 @@ def setUp(self): copy.deepcopy(identity_fakes.PROJECT), loaded=True, ) + self.users_mock.get.return_value = fakes.FakeResource( None, copy.deepcopy(identity_fakes.USER), loaded=True, ) + self.roles_mock.get.return_value = fakes.FakeResource( None, copy.deepcopy(identity_fakes.ROLE), @@ -75,9 +79,9 @@ def test_role_add(self): identity_fakes.role_name, ] verifylist = [ - ('role', identity_fakes.role_name), ('project', identity_fakes.project_name), ('user', identity_fakes.user_name), + ('role', identity_fakes.role_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -217,11 +221,13 @@ def setUp(self): copy.deepcopy(identity_fakes.PROJECT), loaded=True, ) + self.users_mock.get.return_value = fakes.FakeResource( None, copy.deepcopy(identity_fakes.USER), loaded=True, ) + self.roles_mock.roles_for_user.return_value = [ fakes.FakeResource( None, @@ -340,11 +346,13 @@ def setUp(self): copy.deepcopy(identity_fakes.PROJECT), loaded=True, ) + self.users_mock.get.return_value = fakes.FakeResource( None, copy.deepcopy(identity_fakes.USER), loaded=True, ) + self.roles_mock.get.return_value = fakes.FakeResource( None, copy.deepcopy(identity_fakes.ROLE), diff --git a/openstackclient/tests/identity/v3/__init__.py b/openstackclient/tests/identity/v3/__init__.py new file mode 100644 index 0000000000..c534c012e8 --- /dev/null +++ b/openstackclient/tests/identity/v3/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2013 OpenStack Foundation +# +# 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. +# diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py new file mode 100644 index 0000000000..1338553608 --- /dev/null +++ b/openstackclient/tests/identity/v3/fakes.py @@ -0,0 +1,106 @@ +# Copyright 2013 Nebula Inc. +# +# 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 mock + +from openstackclient.tests import fakes + + +domain_id = 'd1' +domain_name = 'oftheking' + +DOMAIN = { + 'id': domain_id, + 'name': domain_name, +} + +group_id = 'gr-010' +group_name = 'spencer davis' + +GROUP = { + 'id': group_id, + 'name': group_name, +} + +project_id = '8-9-64' +project_name = 'beatles' +project_description = 'Fab Four' + +PROJECT = { + 'id': project_id, + 'name': project_name, + 'description': project_description, + 'enabled': True, + 'domain_id': domain_id, +} + +PROJECT_2 = { + 'id': project_id + '-2222', + 'name': project_name + ' reprise', + 'description': project_description + 'plus four more', + 'enabled': True, + 'domain_id': domain_id, +} + +role_id = 'r1' +role_name = 'roller' + +ROLE = { + 'id': role_id, + 'name': role_name, +} + +service_id = 's-123' +service_name = 'Texaco' +service_type = 'gas' + +SERVICE = { + 'id': service_id, + 'name': service_name, + 'type': service_type, + 'enabled': True, +} + +user_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' +user_name = 'paul' +user_description = 'Sir Paul' +user_email = 'paul@applecorps.com' + +USER = { + 'id': user_id, + 'name': user_name, + 'project_id': project_id, + 'email': user_email, + 'enabled': True, + 'domain_id': domain_id, +} + + +class FakeIdentityv3Client(object): + def __init__(self, **kwargs): + self.domains = mock.Mock() + self.domains.resource_class = fakes.FakeResource(None, {}) + self.groups = mock.Mock() + self.groups.resource_class = fakes.FakeResource(None, {}) + self.projects = mock.Mock() + self.projects.resource_class = fakes.FakeResource(None, {}) + self.roles = mock.Mock() + self.roles.resource_class = fakes.FakeResource(None, {}) + self.services = mock.Mock() + self.services.resource_class = fakes.FakeResource(None, {}) + self.users = mock.Mock() + self.users.resource_class = fakes.FakeResource(None, {}) + self.auth_token = kwargs['token'] + self.management_url = kwargs['endpoint'] diff --git a/openstackclient/tests/identity/v3/test_identity.py b/openstackclient/tests/identity/v3/test_identity.py new file mode 100644 index 0000000000..4b55ee4540 --- /dev/null +++ b/openstackclient/tests/identity/v3/test_identity.py @@ -0,0 +1,31 @@ +# Copyright 2013 Nebula Inc. +# +# 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 openstackclient.tests.identity.v3 import fakes +from openstackclient.tests import utils + + +AUTH_TOKEN = "foobar" +AUTH_URL = "http://0.0.0.0" + + +class TestIdentityv3(utils.TestCommand): + def setUp(self): + super(TestIdentityv3, self).setUp() + + self.app.client_manager.identity = fakes.FakeIdentityv3Client( + endpoint=AUTH_URL, + token=AUTH_TOKEN, + ) diff --git a/openstackclient/tests/identity/v3/test_project.py b/openstackclient/tests/identity/v3/test_project.py new file mode 100644 index 0000000000..91c15e246d --- /dev/null +++ b/openstackclient/tests/identity/v3/test_project.py @@ -0,0 +1,531 @@ +# Copyright 2013 Nebula Inc. +# +# 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 copy + +from openstackclient.identity.v3 import project +from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes +from openstackclient.tests.identity.v3 import test_identity + + +class TestProject(test_identity.TestIdentityv3): + + def setUp(self): + super(TestProject, self).setUp() + + # Get a shortcut to the DomainManager Mock + self.domains_mock = self.app.client_manager.identity.domains + self.domains_mock.reset_mock() + + # Get a shortcut to the ProjectManager Mock + self.projects_mock = self.app.client_manager.identity.projects + self.projects_mock.reset_mock() + + +class TestProjectCreate(TestProject): + + def setUp(self): + super(TestProjectCreate, self).setUp() + + self.domains_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.DOMAIN), + loaded=True, + ) + + self.projects_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + + # Get the command object to test + self.cmd = project.CreateProject(self.app, None) + + def test_project_create_no_options(self): + arglist = [ + identity_fakes.project_name, + ] + verifylist = [ + ('enable', False), + ('disable', False), + ('name', identity_fakes.project_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'description': None, + 'enabled': True, + } + # ProjectManager.create(name, domain, description=, enabled=, **kwargs) + self.projects_mock.create.assert_called_with( + identity_fakes.project_name, + None, + **kwargs + ) + + collist = ('description', 'domain_id', 'enabled', 'id', 'name') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.project_description, + identity_fakes.domain_id, + True, + identity_fakes.project_id, + identity_fakes.project_name, + ) + self.assertEqual(data, datalist) + + def test_project_create_description(self): + arglist = [ + '--description', 'new desc', + identity_fakes.project_name, + ] + verifylist = [ + ('description', 'new desc'), + ('enable', False), + ('disable', False), + ('name', identity_fakes.project_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'description': 'new desc', + 'enabled': True, + } + # ProjectManager.create(name, domain, description=, enabled=, **kwargs) + self.projects_mock.create.assert_called_with( + identity_fakes.project_name, + None, + **kwargs + ) + + collist = ('description', 'domain_id', 'enabled', 'id', 'name') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.project_description, + identity_fakes.domain_id, + True, + identity_fakes.project_id, + identity_fakes.project_name, + ) + self.assertEqual(data, datalist) + + def test_project_create_domain(self): + arglist = [ + '--domain', identity_fakes.domain_name, + identity_fakes.project_name, + ] + verifylist = [ + ('domain', identity_fakes.domain_name), + ('enable', False), + ('disable', False), + ('name', identity_fakes.project_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'description': None, + 'enabled': True, + } + # ProjectManager.create(name, domain, description=, enabled=, **kwargs) + self.projects_mock.create.assert_called_with( + identity_fakes.project_name, + identity_fakes.domain_id, + **kwargs + ) + + collist = ('description', 'domain_id', 'enabled', 'id', 'name') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.project_description, + identity_fakes.domain_id, + True, + identity_fakes.project_id, + identity_fakes.project_name, + ) + self.assertEqual(data, datalist) + + def test_project_create_enable(self): + arglist = [ + '--enable', + identity_fakes.project_name, + ] + verifylist = [ + ('enable', True), + ('disable', False), + ('name', identity_fakes.project_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'description': None, + 'enabled': True, + } + # ProjectManager.create(name, domain, description=, enabled=, **kwargs) + self.projects_mock.create.assert_called_with( + identity_fakes.project_name, + None, + **kwargs + ) + + collist = ('description', 'domain_id', 'enabled', 'id', 'name') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.project_description, + identity_fakes.domain_id, + True, + identity_fakes.project_id, + identity_fakes.project_name, + ) + self.assertEqual(data, datalist) + + def test_project_create_disable(self): + arglist = [ + '--disable', + identity_fakes.project_name, + ] + verifylist = [ + ('enable', False), + ('disable', True), + ('name', identity_fakes.project_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'description': None, + 'enabled': False, + } + # ProjectManager.create(name, domain, description=, enabled=, **kwargs) + self.projects_mock.create.assert_called_with( + identity_fakes.project_name, + None, + **kwargs + ) + + collist = ('description', 'domain_id', 'enabled', 'id', 'name') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.project_description, + identity_fakes.domain_id, + True, + identity_fakes.project_id, + identity_fakes.project_name, + ) + self.assertEqual(data, datalist) + + +class TestProjectDelete(TestProject): + + def setUp(self): + super(TestProjectDelete, self).setUp() + + # This is the return value for utils.find_resource() + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + self.projects_mock.delete.return_value = None + + # Get the command object to test + self.cmd = project.DeleteProject(self.app, None) + + def test_project_delete_no_options(self): + arglist = [ + identity_fakes.project_id, + ] + verifylist = [ + ('project', identity_fakes.project_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(result, 0) + + self.projects_mock.delete.assert_called_with( + identity_fakes.project_id, + ) + + +class TestProjectList(TestProject): + + def setUp(self): + super(TestProjectList, self).setUp() + + self.projects_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = project.ListProject(self.app, None) + + def test_project_list_no_options(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + self.projects_mock.list.assert_called_with() + + collist = ('ID', 'Name') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.project_id, + identity_fakes.project_name, + ), ) + self.assertEqual(tuple(data), datalist) + + def test_project_list_long(self): + arglist = [ + '--long', + ] + verifylist = [ + ('long', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + self.projects_mock.list.assert_called_with() + + collist = ('ID', 'Name', 'Domain ID', 'Description', 'Enabled') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.project_id, + identity_fakes.project_name, + identity_fakes.domain_id, + identity_fakes.project_description, + True, + ), ) + self.assertEqual(tuple(data), datalist) + + +class TestProjectSet(TestProject): + + def setUp(self): + super(TestProjectSet, self).setUp() + + self.domains_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.DOMAIN), + loaded=True, + ) + + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + self.projects_mock.update.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + + # Get the command object to test + self.cmd = project.SetProject(self.app, None) + + def test_project_set_no_options(self): + arglist = [ + identity_fakes.project_name, + ] + verifylist = [ + ('project', identity_fakes.project_name), + ('enable', False), + ('disable', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(result, 0) + + def test_project_set_name(self): + arglist = [ + '--name', 'qwerty', + identity_fakes.project_name, + ] + verifylist = [ + ('name', 'qwerty'), + ('enable', False), + ('disable', False), + ('project', identity_fakes.project_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(result, 0) + + # Set expected values + kwargs = { + 'description': identity_fakes.project_description, + 'domain': identity_fakes.domain_id, + 'enabled': True, + 'name': 'qwerty', + } + # ProjectManager.update(project, name=, domain=, description=, + # enabled=, **kwargs) + self.projects_mock.update.assert_called_with( + identity_fakes.project_id, + **kwargs + ) + + def test_project_set_description(self): + arglist = [ + '--description', 'new desc', + identity_fakes.project_name, + ] + verifylist = [ + ('description', 'new desc'), + ('enable', False), + ('disable', False), + ('project', identity_fakes.project_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(result, 0) + + # Set expected values + kwargs = { + 'description': 'new desc', + 'domain': identity_fakes.domain_id, + 'enabled': True, + 'name': identity_fakes.project_name, + } + self.projects_mock.update.assert_called_with( + identity_fakes.project_id, + **kwargs + ) + + def test_project_set_enable(self): + arglist = [ + '--enable', + identity_fakes.project_name, + ] + verifylist = [ + ('enable', True), + ('disable', False), + ('project', identity_fakes.project_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(result, 0) + + # Set expected values + kwargs = { + 'description': identity_fakes.project_description, + 'domain': identity_fakes.domain_id, + 'enabled': True, + 'name': identity_fakes.project_name, + } + self.projects_mock.update.assert_called_with( + identity_fakes.project_id, + **kwargs + ) + + def test_project_set_disable(self): + arglist = [ + '--disable', + identity_fakes.project_name, + ] + verifylist = [ + ('enable', False), + ('disable', True), + ('project', identity_fakes.project_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(result, 0) + + # Set expected values + kwargs = { + 'description': identity_fakes.project_description, + 'domain': identity_fakes.domain_id, + 'enabled': False, + 'name': identity_fakes.project_name, + } + self.projects_mock.update.assert_called_with( + identity_fakes.project_id, + **kwargs + ) + + +class TestProjectShow(TestProject): + + def setUp(self): + super(TestProjectShow, self).setUp() + + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + + # Get the command object to test + self.cmd = project.ShowProject(self.app, None) + + def test_project_show(self): + arglist = [ + identity_fakes.project_id, + ] + verifylist = [ + ('project', identity_fakes.project_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + self.projects_mock.get.assert_called_with( + identity_fakes.project_id, + ) + + collist = ('description', 'domain_id', 'enabled', 'id', 'name') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.project_description, + identity_fakes.domain_id, + True, + identity_fakes.project_id, + identity_fakes.project_name, + ) + self.assertEqual(data, datalist) diff --git a/openstackclient/tests/identity/v3/test_role.py b/openstackclient/tests/identity/v3/test_role.py new file mode 100644 index 0000000000..ef2b3e057c --- /dev/null +++ b/openstackclient/tests/identity/v3/test_role.py @@ -0,0 +1,550 @@ +# Copyright 2013 Nebula Inc. +# +# 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 copy + +from openstackclient.identity.v3 import role +from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes +from openstackclient.tests.identity.v3 import test_identity + + +class TestRole(test_identity.TestIdentityv3): + + def setUp(self): + super(TestRole, self).setUp() + + # Get a shortcut to the UserManager Mock + self.users_mock = self.app.client_manager.identity.users + self.users_mock.reset_mock() + + # Get a shortcut to the UserManager Mock + self.groups_mock = self.app.client_manager.identity.groups + self.groups_mock.reset_mock() + + # Get a shortcut to the DomainManager Mock + self.domains_mock = self.app.client_manager.identity.domains + self.domains_mock.reset_mock() + + # Get a shortcut to the ProjectManager Mock + self.projects_mock = self.app.client_manager.identity.projects + self.projects_mock.reset_mock() + + # Get a shortcut to the RoleManager Mock + self.roles_mock = self.app.client_manager.identity.roles + self.roles_mock.reset_mock() + + +class TestRoleAdd(TestRole): + + def setUp(self): + super(TestRoleAdd, self).setUp() + + self.users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + + self.groups_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.GROUP), + loaded=True, + ) + + self.domains_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.DOMAIN), + loaded=True, + ) + + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + + self.roles_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE), + loaded=True, + ) + self.roles_mock.grant.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE), + loaded=True, + ) + + # Get the command object to test + self.cmd = role.AddRole(self.app, None) + + def test_role_add_user_domain(self): + arglist = [ + '--user', identity_fakes.user_name, + '--domain', identity_fakes.domain_name, + identity_fakes.role_name, + ] + verifylist = [ + ('user', identity_fakes.user_name), + ('group', None), + ('domain', identity_fakes.domain_name), + ('project', None), + ('role', identity_fakes.role_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(result, 0) + + # Set expected values + kwargs = { + 'user': identity_fakes.user_id, + 'domain': identity_fakes.domain_id, + } + # RoleManager.grant(role, user=, group=, domain=, project=) + self.roles_mock.grant.assert_called_with( + identity_fakes.role_id, + **kwargs + ) + + def test_role_add_user_project(self): + arglist = [ + '--user', identity_fakes.user_name, + '--project', identity_fakes.project_name, + identity_fakes.role_name, + ] + verifylist = [ + ('user', identity_fakes.user_name), + ('group', None), + ('domain', None), + ('project', identity_fakes.project_name), + ('role', identity_fakes.role_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(result, 0) + + # Set expected values + kwargs = { + 'user': identity_fakes.user_id, + 'project': identity_fakes.project_id, + } + # RoleManager.grant(role, user=, group=, domain=, project=) + self.roles_mock.grant.assert_called_with( + identity_fakes.role_id, + **kwargs + ) + + def test_role_add_group_domain(self): + arglist = [ + '--group', identity_fakes.group_name, + '--domain', identity_fakes.domain_name, + identity_fakes.role_name, + ] + verifylist = [ + ('user', None), + ('group', identity_fakes.group_name), + ('domain', identity_fakes.domain_name), + ('project', None), + ('role', identity_fakes.role_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(result, 0) + + # Set expected values + kwargs = { + 'group': identity_fakes.group_id, + 'domain': identity_fakes.domain_id, + } + # RoleManager.grant(role, user=, group=, domain=, project=) + self.roles_mock.grant.assert_called_with( + identity_fakes.role_id, + **kwargs + ) + + def test_role_add_group_project(self): + arglist = [ + '--group', identity_fakes.group_name, + '--project', identity_fakes.project_name, + identity_fakes.role_name, + ] + verifylist = [ + ('user', None), + ('group', identity_fakes.group_name), + ('domain', None), + ('project', identity_fakes.project_name), + ('role', identity_fakes.role_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(result, 0) + + # Set expected values + kwargs = { + 'group': identity_fakes.group_id, + 'project': identity_fakes.project_id, + } + # RoleManager.grant(role, user=, group=, domain=, project=) + self.roles_mock.grant.assert_called_with( + identity_fakes.role_id, + **kwargs + ) + + +class TestRoleCreate(TestRole): + + def setUp(self): + super(TestRoleCreate, self).setUp() + + self.roles_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE), + loaded=True, + ) + + # Get the command object to test + self.cmd = role.CreateRole(self.app, None) + + def test_role_create_no_options(self): + arglist = [ + identity_fakes.role_name, + ] + verifylist = [ + ('name', identity_fakes.role_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # RoleManager.create(name) + self.roles_mock.create.assert_called_with( + identity_fakes.role_name, + ) + + collist = ('id', 'name') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.role_id, + identity_fakes.role_name, + ) + self.assertEqual(data, datalist) + + +class TestRoleDelete(TestRole): + + def setUp(self): + super(TestRoleDelete, self).setUp() + + self.roles_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE), + loaded=True, + ) + self.roles_mock.delete.return_value = None + + # Get the command object to test + self.cmd = role.DeleteRole(self.app, None) + + def test_role_delete_no_options(self): + arglist = [ + identity_fakes.role_name, + ] + verifylist = [ + ('role', identity_fakes.role_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + self.roles_mock.delete.assert_called_with( + identity_fakes.role_id, + ) + + +class TestRoleList(TestRole): + + def setUp(self): + super(TestRoleList, self).setUp() + + self.roles_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = role.ListRole(self.app, None) + + def test_role_list_no_options(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.roles_mock.list.assert_called_with() + + collist = ('ID', 'Name') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.role_id, + identity_fakes.role_name, + ), ) + self.assertEqual(tuple(data), datalist) + + +class TestRoleRemove(TestRole): + + def setUp(self): + super(TestRoleRemove, self).setUp() + + self.users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + + self.groups_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.GROUP), + loaded=True, + ) + + self.domains_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.DOMAIN), + loaded=True, + ) + + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + + self.roles_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE), + loaded=True, + ) + self.roles_mock.revoke.return_value = None + + # Get the command object to test + self.cmd = role.RemoveRole(self.app, None) + + def test_role_remove_user_domain(self): + arglist = [ + '--user', identity_fakes.user_name, + '--domain', identity_fakes.domain_name, + identity_fakes.role_name, + ] + verifylist = [ + ('user', identity_fakes.user_name), + ('group', None), + ('domain', identity_fakes.domain_name), + ('project', None), + ('role', identity_fakes.role_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'user': identity_fakes.user_id, + 'domain': identity_fakes.domain_id, + } + # RoleManager.revoke(role, user=, group=, domain=, project=) + self.roles_mock.revoke.assert_called_with( + identity_fakes.role_id, + **kwargs + ) + + def test_role_remove_user_project(self): + arglist = [ + '--user', identity_fakes.user_name, + '--project', identity_fakes.project_name, + identity_fakes.role_name, + ] + verifylist = [ + ('user', identity_fakes.user_name), + ('group', None), + ('domain', None), + ('project', identity_fakes.project_name), + ('role', identity_fakes.role_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'user': identity_fakes.user_id, + 'project': identity_fakes.project_id, + } + # RoleManager.revoke(role, user=, group=, domain=, project=) + self.roles_mock.revoke.assert_called_with( + identity_fakes.role_id, + **kwargs + ) + + def test_role_remove_group_domain(self): + arglist = [ + '--group', identity_fakes.group_name, + '--domain', identity_fakes.domain_name, + identity_fakes.role_name, + ] + verifylist = [ + ('user', None), + ('group', identity_fakes.group_name), + ('domain', identity_fakes.domain_name), + ('project', None), + ('role', identity_fakes.role_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'group': identity_fakes.group_id, + 'domain': identity_fakes.domain_id, + } + # RoleManager.revoke(role, user=, group=, domain=, project=) + self.roles_mock.revoke.assert_called_with( + identity_fakes.role_id, + **kwargs + ) + + def test_role_remove_group_project(self): + arglist = [ + '--group', identity_fakes.group_name, + '--project', identity_fakes.project_name, + identity_fakes.role_name, + ] + verifylist = [ + ('user', None), + ('group', identity_fakes.group_name), + ('domain', None), + ('project', identity_fakes.project_name), + ('role', identity_fakes.role_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'group': identity_fakes.group_id, + 'project': identity_fakes.project_id, + } + # RoleManager.revoke(role, user=, group=, domain=, project=) + self.roles_mock.revoke.assert_called_with( + identity_fakes.role_id, + **kwargs + ) + + +class TestRoleSet(TestRole): + + def setUp(self): + super(TestRoleSet, self).setUp() + + self.roles_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE), + loaded=True, + ) + self.roles_mock.update.return_value = None + + # Get the command object to test + self.cmd = role.SetRole(self.app, None) + + def test_role_set_no_options(self): + arglist = [ + '--name', 'over', + identity_fakes.role_name, + ] + verifylist = [ + ('name', 'over'), + ('role', identity_fakes.role_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': 'over', + } + # RoleManager.update(role, name=) + self.roles_mock.update.assert_called_with( + identity_fakes.role_id, + **kwargs + ) + + +class TestRoleShow(TestRole): + + def setUp(self): + super(TestRoleShow, self).setUp() + + self.roles_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE), + loaded=True, + ) + + # Get the command object to test + self.cmd = role.ShowRole(self.app, None) + + def test_service_show(self): + arglist = [ + identity_fakes.role_name, + ] + verifylist = [ + ('role', identity_fakes.role_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # RoleManager.get(role) + self.roles_mock.get.assert_called_with( + identity_fakes.role_name, + ) + + collist = ('id', 'name') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.role_id, + identity_fakes.role_name, + ) + self.assertEqual(data, datalist) diff --git a/openstackclient/tests/identity/v3/test_service.py b/openstackclient/tests/identity/v3/test_service.py new file mode 100644 index 0000000000..1c3ae21e16 --- /dev/null +++ b/openstackclient/tests/identity/v3/test_service.py @@ -0,0 +1,408 @@ +# Copyright 2013 Nebula Inc. +# +# 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 copy + +from openstackclient.identity.v3 import service +from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes +from openstackclient.tests.identity.v3 import test_identity + + +class TestService(test_identity.TestIdentityv3): + + def setUp(self): + super(TestService, self).setUp() + + # Get a shortcut to the ServiceManager Mock + self.services_mock = self.app.client_manager.identity.services + self.services_mock.reset_mock() + + +class TestServiceCreate(TestService): + + def setUp(self): + super(TestServiceCreate, self).setUp() + + self.services_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.SERVICE), + loaded=True, + ) + + # Get the command object to test + self.cmd = service.CreateService(self.app, None) + + def test_service_create_name(self): + arglist = [ + '--name', identity_fakes.service_name, + identity_fakes.service_type, + ] + verifylist = [ + ('name', identity_fakes.service_name), + ('enable', False), + ('disable', False), + ('type', identity_fakes.service_type), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # ServiceManager.create(name, type, enabled=, **kwargs) + self.services_mock.create.assert_called_with( + identity_fakes.service_name, + identity_fakes.service_type, + True, + ) + + collist = ('enabled', 'id', 'name', 'type') + self.assertEqual(columns, collist) + datalist = ( + True, + identity_fakes.service_id, + identity_fakes.service_name, + identity_fakes.service_type, + ) + self.assertEqual(data, datalist) + + def test_service_create_enable(self): + arglist = [ + '--enable', + identity_fakes.service_type, + ] + verifylist = [ + ('name', None), + ('enable', True), + ('disable', False), + ('type', identity_fakes.service_type), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # ServiceManager.create(name, type, enabled=, **kwargs) + self.services_mock.create.assert_called_with( + None, + identity_fakes.service_type, + True, + ) + + collist = ('enabled', 'id', 'name', 'type') + self.assertEqual(columns, collist) + datalist = ( + True, + identity_fakes.service_id, + identity_fakes.service_name, + identity_fakes.service_type, + ) + self.assertEqual(data, datalist) + + def test_service_create_disable(self): + arglist = [ + '--disable', + identity_fakes.service_type, + ] + verifylist = [ + ('name', None), + ('enable', False), + ('disable', True), + ('type', identity_fakes.service_type), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # ServiceManager.create(name, type, enabled=, **kwargs) + self.services_mock.create.assert_called_with( + None, + identity_fakes.service_type, + False, + ) + + collist = ('enabled', 'id', 'name', 'type') + self.assertEqual(columns, collist) + datalist = ( + True, + identity_fakes.service_id, + identity_fakes.service_name, + identity_fakes.service_type, + ) + self.assertEqual(data, datalist) + + +class TestServiceDelete(TestService): + + def setUp(self): + super(TestServiceDelete, self).setUp() + + self.services_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.SERVICE), + loaded=True, + ) + self.services_mock.delete.return_value = None + + # Get the command object to test + self.cmd = service.DeleteService(self.app, None) + + def test_service_delete_no_options(self): + arglist = [ + identity_fakes.service_name, + ] + verifylist = [ + ('service', identity_fakes.service_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(result, 0) + + self.services_mock.delete.assert_called_with( + identity_fakes.service_id, + ) + + +class TestServiceList(TestService): + + def setUp(self): + super(TestServiceList, self).setUp() + + self.services_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.SERVICE), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = service.ListService(self.app, None) + + def test_service_list_no_options(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.services_mock.list.assert_called_with() + + collist = ('ID', 'Name', 'Type', 'Enabled') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.service_id, + identity_fakes.service_name, + identity_fakes.service_type, + True, + ), ) + self.assertEqual(tuple(data), datalist) + + +class TestServiceSet(TestService): + + def setUp(self): + super(TestServiceSet, self).setUp() + + self.services_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.SERVICE), + loaded=True, + ) + self.services_mock.update.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.SERVICE), + loaded=True, + ) + + # Get the command object to test + self.cmd = service.SetService(self.app, None) + + def test_service_set_no_options(self): + arglist = [ + identity_fakes.service_name, + ] + verifylist = [ + ('type', None), + ('name', None), + ('enable', False), + ('disable', False), + ('service', identity_fakes.service_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(result, 0) + + def test_service_set_type(self): + arglist = [ + '--type', identity_fakes.service_type, + identity_fakes.service_name, + ] + verifylist = [ + ('type', identity_fakes.service_type), + ('name', None), + ('enable', False), + ('disable', False), + ('service', identity_fakes.service_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(result, 0) + + # Set expected values + kwargs = { + 'name': identity_fakes.service_name, + 'type': identity_fakes.service_type, + 'enabled': True, + } + # ServiceManager.update(service, name=, type=, enabled=, **kwargs) + self.services_mock.update.assert_called_with( + identity_fakes.service_id, + **kwargs + ) + + def test_service_set_name(self): + arglist = [ + '--name', identity_fakes.service_name, + identity_fakes.service_name, + ] + verifylist = [ + ('type', None), + ('name', identity_fakes.service_name), + ('enable', False), + ('disable', False), + ('service', identity_fakes.service_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(result, 0) + + # Set expected values + kwargs = { + 'name': identity_fakes.service_name, + 'type': identity_fakes.service_type, + 'enabled': True, + } + # ServiceManager.update(service, name=, type=, enabled=, **kwargs) + self.services_mock.update.assert_called_with( + identity_fakes.service_id, + **kwargs + ) + + def test_service_set_enable(self): + arglist = [ + '--enable', + identity_fakes.service_name, + ] + verifylist = [ + ('type', None), + ('name', None), + ('enable', True), + ('disable', False), + ('service', identity_fakes.service_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(result, 0) + + # Set expected values + kwargs = { + 'name': identity_fakes.service_name, + 'type': identity_fakes.service_type, + 'enabled': True, + } + # ServiceManager.update(service, name=, type=, enabled=, **kwargs) + self.services_mock.update.assert_called_with( + identity_fakes.service_id, + **kwargs + ) + + def test_service_set_disable(self): + arglist = [ + '--disable', + identity_fakes.service_name, + ] + verifylist = [ + ('type', None), + ('name', None), + ('enable', False), + ('disable', True), + ('service', identity_fakes.service_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(result, 0) + + # Set expected values + kwargs = { + 'name': identity_fakes.service_name, + 'type': identity_fakes.service_type, + 'enabled': False, + } + # ServiceManager.update(service, name=, type=, enabled=, **kwargs) + self.services_mock.update.assert_called_with( + identity_fakes.service_id, + **kwargs + ) + + +class TestServiceShow(TestService): + + def setUp(self): + super(TestServiceShow, self).setUp() + + self.services_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.SERVICE), + loaded=True, + ) + + # Get the command object to test + self.cmd = service.ShowService(self.app, None) + + def test_service_show(self): + arglist = [ + identity_fakes.service_name, + ] + verifylist = [ + ('service', identity_fakes.service_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # ServiceManager.get(id) + self.services_mock.get.assert_called_with( + identity_fakes.service_name, + ) + + collist = ('enabled', 'id', 'name', 'type') + self.assertEqual(columns, collist) + datalist = ( + True, + identity_fakes.service_id, + identity_fakes.service_name, + identity_fakes.service_type, + ) + self.assertEqual(data, datalist) diff --git a/openstackclient/tests/identity/v3/test_user.py b/openstackclient/tests/identity/v3/test_user.py new file mode 100644 index 0000000000..8f195805c2 --- /dev/null +++ b/openstackclient/tests/identity/v3/test_user.py @@ -0,0 +1,986 @@ +# Copyright 2013 Nebula Inc. +# +# 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 copy + +from openstackclient.identity.v3 import user +from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes +from openstackclient.tests.identity.v3 import test_identity + + +class TestUser(test_identity.TestIdentityv3): + + def setUp(self): + super(TestUser, self).setUp() + + # Get a shortcut to the DomainManager Mock + self.domains_mock = self.app.client_manager.identity.domains + self.domains_mock.reset_mock() + + # Get a shortcut to the ProjectManager Mock + self.projects_mock = self.app.client_manager.identity.projects + self.projects_mock.reset_mock() + + # Get a shortcut to the RoleManager Mock + self.roles_mock = self.app.client_manager.identity.roles + self.roles_mock.reset_mock() + + # Get a shortcut to the UserManager Mock + self.users_mock = self.app.client_manager.identity.users + self.users_mock.reset_mock() + + +class TestUserCreate(TestUser): + + def setUp(self): + super(TestUserCreate, self).setUp() + + self.domains_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.DOMAIN), + loaded=True, + ) + + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + + self.users_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + + # Get the command object to test + self.cmd = user.CreateUser(self.app, None) + + def test_user_create_no_options(self): + arglist = [ + identity_fakes.user_name, + ] + verifylist = [ + ('enable', False), + ('disable', False), + ('name', identity_fakes.user_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'password': None, + } + + # UserManager.create(name, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + identity_fakes.user_name, + **kwargs + ) + + collist = ('domain_id', 'email', 'enabled', 'id', 'name', 'project_id') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.domain_id, + identity_fakes.user_email, + True, + identity_fakes.user_id, + identity_fakes.user_name, + identity_fakes.project_id, + ) + self.assertEqual(data, datalist) + + def test_user_create_password(self): + arglist = [ + '--password', 'secret', + identity_fakes.user_name, + ] + verifylist = [ + ('password', 'secret'), + ('enable', False), + ('disable', False), + ('name', identity_fakes.user_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'password': 'secret', + } + # UserManager.create(name, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + identity_fakes.user_name, + **kwargs + ) + + collist = ('domain_id', 'email', 'enabled', 'id', 'name', 'project_id') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.domain_id, + identity_fakes.user_email, + True, + identity_fakes.user_id, + identity_fakes.user_name, + identity_fakes.project_id, + ) + self.assertEqual(data, datalist) + + def test_user_create_email(self): + arglist = [ + '--email', 'barney@example.com', + identity_fakes.user_name, + ] + verifylist = [ + ('email', 'barney@example.com'), + ('enable', False), + ('disable', False), + ('name', identity_fakes.user_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'default_project': None, + 'description': None, + 'domain': None, + 'email': 'barney@example.com', + 'enabled': True, + 'password': None, + } + # UserManager.create(name, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + identity_fakes.user_name, + **kwargs + ) + + collist = ('domain_id', 'email', 'enabled', 'id', 'name', 'project_id') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.domain_id, + identity_fakes.user_email, + True, + identity_fakes.user_id, + identity_fakes.user_name, + identity_fakes.project_id, + ) + self.assertEqual(data, datalist) + + def test_user_create_project(self): + # Return the new project + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT_2), + loaded=True, + ) + # Set up to return an updated user + USER_2 = copy.deepcopy(identity_fakes.USER) + USER_2['project_id'] = identity_fakes.PROJECT_2['id'] + self.users_mock.create.return_value = fakes.FakeResource( + None, + USER_2, + loaded=True, + ) + + arglist = [ + '--project', identity_fakes.PROJECT_2['name'], + identity_fakes.user_name, + ] + verifylist = [ + ('project', identity_fakes.PROJECT_2['name']), + ('enable', False), + ('disable', False), + ('name', identity_fakes.user_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'default_project': identity_fakes.PROJECT_2['id'], + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'password': None, + } + # UserManager.create(name, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + identity_fakes.user_name, + **kwargs + ) + + collist = ('domain_id', 'email', 'enabled', 'id', 'name', 'project_id') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.domain_id, + identity_fakes.user_email, + True, + identity_fakes.user_id, + identity_fakes.user_name, + identity_fakes.PROJECT_2['id'], + ) + self.assertEqual(data, datalist) + + def test_user_create_domain(self): + arglist = [ + '--domain', identity_fakes.domain_name, + identity_fakes.user_name, + ] + verifylist = [ + ('domain', identity_fakes.domain_name), + ('enable', False), + ('disable', False), + ('name', identity_fakes.user_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'default_project': None, + 'description': None, + 'domain': identity_fakes.domain_id, + 'email': None, + 'enabled': True, + 'password': None, + } + # UserManager.create(name, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + identity_fakes.user_name, + **kwargs + ) + + collist = ('domain_id', 'email', 'enabled', 'id', 'name', 'project_id') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.domain_id, + identity_fakes.user_email, + True, + identity_fakes.user_id, + identity_fakes.user_name, + identity_fakes.project_id, + ) + self.assertEqual(data, datalist) + + def test_user_create_enable(self): + arglist = [ + '--enable', + identity_fakes.user_name, + ] + verifylist = [ + ('enable', True), + ('disable', False), + ('name', identity_fakes.user_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'password': None, + } + # UserManager.create(name, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + identity_fakes.user_name, + **kwargs + ) + + collist = ('domain_id', 'email', 'enabled', 'id', 'name', 'project_id') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.domain_id, + identity_fakes.user_email, + True, + identity_fakes.user_id, + identity_fakes.user_name, + identity_fakes.project_id, + ) + self.assertEqual(data, datalist) + + def test_user_create_disable(self): + arglist = [ + '--disable', + identity_fakes.user_name, + ] + verifylist = [ + ('name', identity_fakes.user_name), + ('enable', False), + ('disable', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': False, + 'password': None, + } + # users.create(name, password, email, tenant_id=None, enabled=True) + self.users_mock.create.assert_called_with( + identity_fakes.user_name, + **kwargs + ) + + collist = ('domain_id', 'email', 'enabled', 'id', 'name', 'project_id') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.domain_id, + identity_fakes.user_email, + True, + identity_fakes.user_id, + identity_fakes.user_name, + identity_fakes.project_id, + ) + self.assertEqual(data, datalist) + + +class TestUserDelete(TestUser): + + def setUp(self): + super(TestUserDelete, self).setUp() + + # This is the return value for utils.find_resource() + self.users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + self.users_mock.delete.return_value = None + + # Get the command object to test + self.cmd = user.DeleteUser(self.app, None) + + def test_user_delete_no_options(self): + arglist = [ + identity_fakes.user_id, + ] + verifylist = [ + ('user', identity_fakes.user_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + self.users_mock.delete.assert_called_with( + identity_fakes.user_id, + ) + + +class TestUserList(TestUser): + + def setUp(self): + super(TestUserList, self).setUp() + + self.domains_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.DOMAIN), + loaded=True, + ) + + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + self.projects_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ), + ] + + self.roles_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE), + loaded=True, + ), + ] + + self.users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + self.users_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = user.ListUser(self.app, None) + + def test_user_list_no_options(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.users_mock.list.assert_called_with() + + collist = ('ID', 'Name') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.user_id, + identity_fakes.user_name, + ), ) + self.assertEqual(tuple(data), datalist) + + def test_user_list_project(self): + arglist = [ + '--project', identity_fakes.project_id, + ] + verifylist = [ + ('project', identity_fakes.project_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.users_mock.list.assert_called_with() + + collist = ('ID', 'Name') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.user_id, + identity_fakes.user_name, + ), ) + self.assertEqual(tuple(data), datalist) + + def test_user_list_domain(self): + arglist = [ + '--domain', identity_fakes.domain_id, + ] + verifylist = [ + ('domain', identity_fakes.domain_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.users_mock.list.assert_called_with() + + collist = ('ID', 'Name') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.user_id, + identity_fakes.user_name, + ), ) + self.assertEqual(tuple(data), datalist) + + def test_user_list_role_user(self): + arglist = [ + '--role', + identity_fakes.user_id, + ] + verifylist = [ + ('role', True), + ('user', identity_fakes.user_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'domain': 'default', + 'user': self.users_mock.get(), + } + # RoleManager.list(user=, group=, domain=, project=, **kwargs) + self.roles_mock.list.assert_called_with( + **kwargs + ) + + collist = ('ID', 'Name') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.role_id, + identity_fakes.role_name, + ), ) + self.assertEqual(tuple(data), datalist) + + def test_user_list_role_domain(self): + arglist = [ + '--domain', identity_fakes.domain_name, + '--role', + identity_fakes.user_id, + ] + verifylist = [ + ('domain', identity_fakes.domain_name), + ('role', True), + ('user', identity_fakes.user_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'domain': self.domains_mock.get(), + 'user': self.users_mock.get(), + } + # RoleManager.list(user=, group=, domain=, project=, **kwargs) + self.roles_mock.list.assert_called_with( + **kwargs + ) + + collist = ('ID', 'Name', 'Domain', 'User') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.role_id, + identity_fakes.role_name, + identity_fakes.domain_name, + identity_fakes.user_name, + ), ) + self.assertEqual(tuple(data), datalist) + + def test_user_list_role_project(self): + arglist = [ + '--project', identity_fakes.project_name, + '--role', + identity_fakes.user_id, + ] + verifylist = [ + ('project', identity_fakes.project_name), + ('role', True), + ('user', identity_fakes.user_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'project': self.projects_mock.get(), + 'user': self.users_mock.get(), + } + # RoleManager.list(user=, group=, domain=, project=, **kwargs) + self.roles_mock.list.assert_called_with( + **kwargs + ) + + collist = ('ID', 'Name', 'Project', 'User') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.role_id, + identity_fakes.role_name, + identity_fakes.project_name, + identity_fakes.user_name, + ), ) + self.assertEqual(tuple(data), datalist) + + def test_user_list_long(self): + arglist = [ + '--long', + ] + verifylist = [ + ('long', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.users_mock.list.assert_called_with() + + collist = ( + 'ID', + 'Name', + 'Project Id', + 'Domain Id', + 'Description', + 'Email', + 'Enabled', + ) + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.user_id, + identity_fakes.user_name, + identity_fakes.project_id, + identity_fakes.domain_id, + '', + identity_fakes.user_email, + True, + ), ) + self.assertEqual(tuple(data), datalist) + + +class TestUserSet(TestUser): + + def setUp(self): + super(TestUserSet, self).setUp() + + self.domains_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.DOMAIN), + loaded=True, + ) + + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + + self.users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + self.users_mock.update.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + + # Get the command object to test + self.cmd = user.SetUser(self.app, None) + + def test_user_set_no_options(self): + arglist = [ + identity_fakes.user_name, + ] + verifylist = [ + ('name', None), + ('password', None), + ('email', None), + ('domain', None), + ('project', None), + ('enable', False), + ('disable', False), + ('user', identity_fakes.user_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(result, 0) + + def test_user_set_name(self): + arglist = [ + '--name', 'qwerty', + identity_fakes.user_name, + ] + verifylist = [ + ('name', 'qwerty'), + ('password', None), + ('email', None), + ('domain', None), + ('project', None), + ('enable', False), + ('disable', False), + ('user', identity_fakes.user_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': True, + 'name': 'qwerty', + } + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + identity_fakes.user_id, + **kwargs + ) + + def test_user_set_password(self): + arglist = [ + '--password', 'secret', + identity_fakes.user_name, + ] + verifylist = [ + ('name', None), + ('password', 'secret'), + ('email', None), + ('domain', None), + ('project', None), + ('enable', False), + ('disable', False), + ('user', identity_fakes.user_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': True, + 'password': 'secret', + } + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + identity_fakes.user_id, + **kwargs + ) + + def test_user_set_email(self): + arglist = [ + '--email', 'barney@example.com', + identity_fakes.user_name, + ] + verifylist = [ + ('name', None), + ('password', None), + ('email', 'barney@example.com'), + ('domain', None), + ('project', None), + ('enable', False), + ('disable', False), + ('user', identity_fakes.user_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': True, + 'email': 'barney@example.com', + } + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + identity_fakes.user_id, + **kwargs + ) + + def test_user_set_domain(self): + arglist = [ + '--domain', identity_fakes.domain_id, + identity_fakes.user_name, + ] + verifylist = [ + ('name', None), + ('password', None), + ('email', None), + ('domain', identity_fakes.domain_id), + ('project', None), + ('enable', False), + ('disable', False), + ('user', identity_fakes.user_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': True, + 'domainId': identity_fakes.domain_id, + } + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + identity_fakes.user_id, + **kwargs + ) + + def test_user_set_project(self): + arglist = [ + '--project', identity_fakes.project_id, + identity_fakes.user_name, + ] + verifylist = [ + ('name', None), + ('password', None), + ('email', None), + ('domain', None), + ('project', identity_fakes.project_id), + ('enable', False), + ('disable', False), + ('user', identity_fakes.user_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': True, + 'projectId': identity_fakes.project_id, + } + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + identity_fakes.user_id, + **kwargs + ) + + def test_user_set_enable(self): + arglist = [ + '--enable', + identity_fakes.user_name, + ] + verifylist = [ + ('name', None), + ('password', None), + ('email', None), + ('domain', None), + ('project', None), + ('enable', True), + ('disable', False), + ('user', identity_fakes.user_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': True, + } + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + identity_fakes.user_id, + **kwargs + ) + + def test_user_set_disable(self): + arglist = [ + '--disable', + identity_fakes.user_name, + ] + verifylist = [ + ('name', None), + ('password', None), + ('email', None), + ('domain', None), + ('project', None), + ('enable', False), + ('disable', True), + ('user', identity_fakes.user_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': False, + } + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + identity_fakes.user_id, + **kwargs + ) + + +class TestUserShow(TestUser): + + def setUp(self): + super(TestUserShow, self).setUp() + + self.users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + + # Get the command object to test + self.cmd = user.ShowUser(self.app, None) + + def test_user_show(self): + arglist = [ + identity_fakes.user_id, + ] + verifylist = [ + ('user', identity_fakes.user_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.users_mock.get.assert_called_with(identity_fakes.user_id) + + collist = ('domain_id', 'email', 'enabled', 'id', 'name', 'project_id') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.domain_id, + identity_fakes.user_email, + True, + identity_fakes.user_id, + identity_fakes.user_name, + identity_fakes.project_id, + ) + self.assertEqual(data, datalist) From 6fe687fdf662a7495b20a1d94f27bf557525af58 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 12 Sep 2013 14:49:41 -0500 Subject: [PATCH 0012/3494] Delay authentication to handle commands that do not require it * Move the auth to OpenStackShell.prepare_to_run_command() and skip it if the command's auth_required == False * Default auth_required = True for all commands * Do authentication up-front for interactive use as OpenStackShell.prepare_to_run_command() is not called Change-Id: Id330092f242af624f430d469882d15f4a22f4e37 --- openstackclient/shell.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 6cb7c1ee2c..6797790708 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -22,6 +22,7 @@ import sys from cliff import app +from cliff import command from cliff import help import openstackclient @@ -64,6 +65,11 @@ class OpenStackShell(app.App): log = logging.getLogger(__name__) def __init__(self): + # Patch command.Command to add a default auth_required = True + command.Command.auth_required = True + # But not help + help.HelpCommand.auth_required = False + super(OpenStackShell, self).__init__( description=__doc__.strip(), version=openstackclient.__version__, @@ -383,20 +389,13 @@ def initialize_app(self, argv): # Set up common client session self.restapi = restapi.RESTApi() - # If the user is not asking for help, make sure they - # have given us auth. - cmd_name = None - if argv: - cmd_info = self.command_manager.find_command(argv) - cmd_factory, cmd_name, sub_argv = cmd_info - if self.interactive_mode or cmd_name != 'help': - self.authenticate_user() - self.restapi.set_auth(self.client_manager.identity.auth_token) - def prepare_to_run_command(self, cmd): """Set up auth and API versions""" self.log.debug('prepare_to_run_command %s', cmd.__class__.__name__) - self.log.debug("api: %s" % cmd.api if hasattr(cmd, 'api') else None) + + if cmd.auth_required: + self.authenticate_user() + self.restapi.set_auth(self.client_manager.identity.auth_token) return def clean_up(self, cmd, result, err): @@ -404,6 +403,13 @@ def clean_up(self, cmd, result, err): if err: self.log.debug('got an error: %s', err) + def interact(self): + # NOTE(dtroyer): Maintain the old behaviour for interactive use as + # this path does not call prepare_to_run_command() + self.authenticate_user() + self.restapi.set_auth(self.client_manager.identity.auth_token) + super(OpenStackShell, self).interact() + def main(argv=sys.argv[1:]): try: From 1c4d3b320ffcf46140d4475ee2591a45f296268e Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 20 Sep 2013 10:33:15 -0500 Subject: [PATCH 0013/3494] Fix security group entrypoints Change-Id: I0590dde67b1121523d03742ce57093f2c5bacc72 --- setup.cfg | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/setup.cfg b/setup.cfg index e6fe8475ca..8e935192ca 100644 --- a/setup.cfg +++ b/setup.cfg @@ -206,14 +206,14 @@ openstack.compute.v2 = project_usage_list = openstackclient.compute.v2.usage:ListUsage - security_group_create = openstackclient.compute.v2.secgroup:CreateSecurityGroup - security_group_delete = openstackclient.compute.v2.secgroup:DeleteSecurityGroup - security_group_list = openstackclient.compute.v2.secgroup:ListSecurityGroup - security_group_set = openstackclient.compute.v2.secgroup:SetSecurityGroup - security_group_show = openstackclient.compute.v2.secgroup:ShowSecurityGroup - security_group_rule_create = openstackclient.compute.v2.secgroup:CreateSecurityGroupRule - security_group_rule_delete = openstackclient.compute.v2.secgroup:DeleteSecurityGroupRule - security_group_rule_list = openstackclient.compute.v2.secgroup:ListSecurityGroupRule + security_group_create = openstackclient.compute.v2.security_group:CreateSecurityGroup + security_group_delete = openstackclient.compute.v2.security_group:DeleteSecurityGroup + security_group_list = openstackclient.compute.v2.security_group:ListSecurityGroup + security_group_set = openstackclient.compute.v2.security_group:SetSecurityGroup + security_group_show = openstackclient.compute.v2.security_group:ShowSecurityGroup + security_group_rule_create = openstackclient.compute.v2.security_group:CreateSecurityGroupRule + security_group_rule_delete = openstackclient.compute.v2.security_group:DeleteSecurityGroupRule + security_group_rule_list = openstackclient.compute.v2.security_group:ListSecurityGroupRule server_add_security_group = openstackclient.compute.v2.server:AddServerSecurityGroup server_add_volume = openstackclient.compute.v2.server:AddServerVolume From ae8d64b337c8b448a3daeddf84edeb522372d9ad Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 20 Sep 2013 10:37:50 -0500 Subject: [PATCH 0014/3494] Sort entrypoints in setup.cfg Change-Id: I72b0e069334c290cdc4d46cff0ba66d751c0edb4 --- setup.cfg | 164 +++++++++++++++++++++++++++--------------------------- 1 file changed, 82 insertions(+), 82 deletions(-) diff --git a/setup.cfg b/setup.cfg index 8e935192ca..bcf4877e9a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,6 +32,88 @@ openstack.common = quota_set = openstackclient.common.quota:SetQuota quota_show = openstackclient.common.quota:ShowQuota +openstack.compute.v2 = + agent_create = openstackclient.compute.v2.agent:CreateAgent + agent_delete = openstackclient.compute.v2.agent:DeleteAgent + agent_list = openstackclient.compute.v2.agent:ListAgent + agent_set = openstackclient.compute.v2.agent:SetAgent + + aggregate_add_host = openstackclient.compute.v2.aggregate:AddAggregateHost + aggregate_create = openstackclient.compute.v2.aggregate:CreateAggregate + aggregate_delete = openstackclient.compute.v2.aggregate:DeleteAggregate + aggregate_list = openstackclient.compute.v2.aggregate:ListAggregate + aggregate_remove_host = openstackclient.compute.v2.aggregate:RemoveAggregateHost + aggregate_set = openstackclient.compute.v2.aggregate:SetAggregate + aggregate_show = openstackclient.compute.v2.aggregate:ShowAggregate + + compute_service_list = openstackclient.compute.v2.service:ListService + compute_service_set = openstackclient.compute.v2.service:SetService + + console_log_show = openstackclient.compute.v2.console:ShowConsoleLog + console_url_show = openstackclient.compute.v2.console:ShowConsoleURL + + flavor_create = openstackclient.compute.v2.flavor:CreateFlavor + flavor_delete = openstackclient.compute.v2.flavor:DeleteFlavor + flavor_list = openstackclient.compute.v2.flavor:ListFlavor + flavor_show = openstackclient.compute.v2.flavor:ShowFlavor + + host_list = openstackclient.compute.v2.host:ListHost + host_show = openstackclient.compute.v2.host:ShowHost + + hypervisor_list = openstackclient.compute.v2.hypervisor:ListHypervisor + hypervisor_show = openstackclient.compute.v2.hypervisor:ShowHypervisor + + ip_fixed_add = openstackclient.compute.v2.fixedip:AddFixedIP + ip_fixed_remove = openstackclient.compute.v2.fixedip:RemoveFixedIP + + ip_floating_add = openstackclient.compute.v2.floatingip:AddFloatingIP + ip_floating_create = openstackclient.compute.v2.floatingip:CreateFloatingIP + ip_floating_delete = openstackclient.compute.v2.floatingip:DeleteFloatingIP + ip_floating_list = openstackclient.compute.v2.floatingip:ListFloatingIP + ip_floating_remove = openstackclient.compute.v2.floatingip:RemoveFloatingIP + + ip_floating_pool_list = openstackclient.compute.v2.floatingippool:ListFloatingIPPool + + keypair_create = openstackclient.compute.v2.keypair:CreateKeypair + keypair_delete = openstackclient.compute.v2.keypair:DeleteKeypair + keypair_list = openstackclient.compute.v2.keypair:ListKeypair + keypair_show = openstackclient.compute.v2.keypair:ShowKeypair + + project_usage_list = openstackclient.compute.v2.usage:ListUsage + + security_group_create = openstackclient.compute.v2.security_group:CreateSecurityGroup + security_group_delete = openstackclient.compute.v2.security_group:DeleteSecurityGroup + security_group_list = openstackclient.compute.v2.security_group:ListSecurityGroup + security_group_set = openstackclient.compute.v2.security_group:SetSecurityGroup + security_group_show = openstackclient.compute.v2.security_group:ShowSecurityGroup + security_group_rule_create = openstackclient.compute.v2.security_group:CreateSecurityGroupRule + security_group_rule_delete = openstackclient.compute.v2.security_group:DeleteSecurityGroupRule + security_group_rule_list = openstackclient.compute.v2.security_group:ListSecurityGroupRule + + server_add_security_group = openstackclient.compute.v2.server:AddServerSecurityGroup + server_add_volume = openstackclient.compute.v2.server:AddServerVolume + server_create = openstackclient.compute.v2.server:CreateServer + server_delete = openstackclient.compute.v2.server:DeleteServer + server_list = openstackclient.compute.v2.server:ListServer + server_lock = openstackclient.compute.v2.server:LockServer + server_migrate = openstackclient.compute.v2.server:MigrateServer + server_pause = openstackclient.compute.v2.server:PauseServer + server_reboot = openstackclient.compute.v2.server:RebootServer + server_rebuild = openstackclient.compute.v2.server:RebuildServer + server_remove_security_group = openstackclient.compute.v2.server:RemoveServerSecurityGroup + server_remove_volume = openstackclient.compute.v2.server:RemoveServerVolume + server_rescue = openstackclient.compute.v2.server:RescueServer + server_resize = openstackclient.compute.v2.server:ResizeServer + server_resume = openstackclient.compute.v2.server:ResumeServer + server_set = openstackclient.compute.v2.server:SetServer + server_show = openstackclient.compute.v2.server:ShowServer + server_ssh = openstackclient.compute.v2.server:SshServer + server_suspend = openstackclient.compute.v2.server:SuspendServer + server_unlock = openstackclient.compute.v2.server:UnlockServer + server_unpause = openstackclient.compute.v2.server:UnpauseServer + server_unrescue = openstackclient.compute.v2.server:UnrescueServer + server_unset = openstackclient.compute.v2.server:UnsetServer + openstack.identity.v2_0 = ec2_credentials_create = openstackclient.identity.v2_0.ec2creds:CreateEC2Creds ec2_credentials_delete = openstackclient.identity.v2_0.ec2creds:DeleteEC2Creds @@ -157,88 +239,6 @@ openstack.image.v2 = image_save = openstackclient.image.v2.image:SaveImage image_show = openstackclient.image.v2.image:ShowImage -openstack.compute.v2 = - agent_create = openstackclient.compute.v2.agent:CreateAgent - agent_delete = openstackclient.compute.v2.agent:DeleteAgent - agent_list = openstackclient.compute.v2.agent:ListAgent - agent_set = openstackclient.compute.v2.agent:SetAgent - - aggregate_add_host = openstackclient.compute.v2.aggregate:AddAggregateHost - aggregate_create = openstackclient.compute.v2.aggregate:CreateAggregate - aggregate_delete = openstackclient.compute.v2.aggregate:DeleteAggregate - aggregate_list = openstackclient.compute.v2.aggregate:ListAggregate - aggregate_remove_host = openstackclient.compute.v2.aggregate:RemoveAggregateHost - aggregate_set = openstackclient.compute.v2.aggregate:SetAggregate - aggregate_show = openstackclient.compute.v2.aggregate:ShowAggregate - - compute_service_list = openstackclient.compute.v2.service:ListService - compute_service_set = openstackclient.compute.v2.service:SetService - - console_log_show = openstackclient.compute.v2.console:ShowConsoleLog - console_url_show = openstackclient.compute.v2.console:ShowConsoleURL - - ip_fixed_add = openstackclient.compute.v2.fixedip:AddFixedIP - ip_fixed_remove = openstackclient.compute.v2.fixedip:RemoveFixedIP - - flavor_create = openstackclient.compute.v2.flavor:CreateFlavor - flavor_delete = openstackclient.compute.v2.flavor:DeleteFlavor - flavor_list = openstackclient.compute.v2.flavor:ListFlavor - flavor_show = openstackclient.compute.v2.flavor:ShowFlavor - - ip_floating_add = openstackclient.compute.v2.floatingip:AddFloatingIP - ip_floating_create = openstackclient.compute.v2.floatingip:CreateFloatingIP - ip_floating_delete = openstackclient.compute.v2.floatingip:DeleteFloatingIP - ip_floating_list = openstackclient.compute.v2.floatingip:ListFloatingIP - ip_floating_remove = openstackclient.compute.v2.floatingip:RemoveFloatingIP - - ip_floating_pool_list = openstackclient.compute.v2.floatingippool:ListFloatingIPPool - - host_list = openstackclient.compute.v2.host:ListHost - host_show = openstackclient.compute.v2.host:ShowHost - - hypervisor_list = openstackclient.compute.v2.hypervisor:ListHypervisor - hypervisor_show = openstackclient.compute.v2.hypervisor:ShowHypervisor - - keypair_create = openstackclient.compute.v2.keypair:CreateKeypair - keypair_delete = openstackclient.compute.v2.keypair:DeleteKeypair - keypair_list = openstackclient.compute.v2.keypair:ListKeypair - keypair_show = openstackclient.compute.v2.keypair:ShowKeypair - - project_usage_list = openstackclient.compute.v2.usage:ListUsage - - security_group_create = openstackclient.compute.v2.security_group:CreateSecurityGroup - security_group_delete = openstackclient.compute.v2.security_group:DeleteSecurityGroup - security_group_list = openstackclient.compute.v2.security_group:ListSecurityGroup - security_group_set = openstackclient.compute.v2.security_group:SetSecurityGroup - security_group_show = openstackclient.compute.v2.security_group:ShowSecurityGroup - security_group_rule_create = openstackclient.compute.v2.security_group:CreateSecurityGroupRule - security_group_rule_delete = openstackclient.compute.v2.security_group:DeleteSecurityGroupRule - security_group_rule_list = openstackclient.compute.v2.security_group:ListSecurityGroupRule - - server_add_security_group = openstackclient.compute.v2.server:AddServerSecurityGroup - server_add_volume = openstackclient.compute.v2.server:AddServerVolume - server_create = openstackclient.compute.v2.server:CreateServer - server_delete = openstackclient.compute.v2.server:DeleteServer - server_list = openstackclient.compute.v2.server:ListServer - server_lock = openstackclient.compute.v2.server:LockServer - server_migrate = openstackclient.compute.v2.server:MigrateServer - server_pause = openstackclient.compute.v2.server:PauseServer - server_reboot = openstackclient.compute.v2.server:RebootServer - server_rebuild = openstackclient.compute.v2.server:RebuildServer - server_remove_security_group = openstackclient.compute.v2.server:RemoveServerSecurityGroup - server_remove_volume = openstackclient.compute.v2.server:RemoveServerVolume - server_rescue = openstackclient.compute.v2.server:RescueServer - server_resize = openstackclient.compute.v2.server:ResizeServer - server_resume = openstackclient.compute.v2.server:ResumeServer - server_set = openstackclient.compute.v2.server:SetServer - server_show = openstackclient.compute.v2.server:ShowServer - server_ssh = openstackclient.compute.v2.server:SshServer - server_suspend = openstackclient.compute.v2.server:SuspendServer - server_unlock = openstackclient.compute.v2.server:UnlockServer - server_unpause = openstackclient.compute.v2.server:UnpauseServer - server_unrescue = openstackclient.compute.v2.server:UnrescueServer - server_unset = openstackclient.compute.v2.server:UnsetServer - openstack.object_store.v1 = container_list = openstackclient.object.v1.container:ListContainer object_list = openstackclient.object.v1.object:ListObject From 74f4e3138996e258d4bdce1a162a5dade62a0c15 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 20 Sep 2013 10:57:31 -0500 Subject: [PATCH 0015/3494] Update release notes for 0.2.2 Change-Id: I59cbee4c147d5f849a7f07224e83ddd751212077 --- doc/source/releases.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/source/releases.rst b/doc/source/releases.rst index 5d5366be77..1387d326b8 100644 --- a/doc/source/releases.rst +++ b/doc/source/releases.rst @@ -2,6 +2,12 @@ Release Notes ============= +0.2.2 (20 Sep 2013) +=================== + +* add object-store list commands and API library +* add test structure + 0.2.1 (06 Aug 2013) =================== From ad59b03be6af9da31230689af268139b12b548e7 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 30 Aug 2013 17:55:37 -0500 Subject: [PATCH 0016/3494] Add object-store show commands * Add lib.container.show_container() and lib.object.show_object() * Add container and object show commands Change-Id: I963d664c55b59739453345f0f353aa2eaf1bf70e --- openstackclient/object/v1/container.py | 29 ++++++- openstackclient/object/v1/lib/container.py | 36 +++++++++ openstackclient/object/v1/lib/object.py | 51 ++++++++++++ openstackclient/object/v1/object.py | 34 ++++++++ .../tests/object/test_container.py | 46 +++++++++++ openstackclient/tests/object/test_object.py | 51 ++++++++++++ .../tests/object/v1/lib/test_container.py | 59 ++++++++++---- .../tests/object/v1/lib/test_object.py | 77 ++++++++++++++++++- setup.cfg | 2 + 9 files changed, 368 insertions(+), 17 deletions(-) diff --git a/openstackclient/object/v1/container.py b/openstackclient/object/v1/container.py index 8c4db66ae4..68b14fc56d 100644 --- a/openstackclient/object/v1/container.py +++ b/openstackclient/object/v1/container.py @@ -17,8 +17,10 @@ import logging +import six from cliff import lister +from cliff import show from openstackclient.common import utils from openstackclient.object.v1.lib import container as lib_container @@ -91,10 +93,35 @@ def take_action(self, parsed_args): self.app.client_manager.object.endpoint, **kwargs ) - #print "data: %s" % data return (columns, (utils.get_dict_properties( s, columns, formatters={}, ) for s in data)) + + +class ShowContainer(show.ShowOne): + """Show container information""" + + log = logging.getLogger(__name__ + '.ShowContainer') + + def get_parser(self, prog_name): + parser = super(ShowContainer, self).get_parser(prog_name) + parser.add_argument( + 'container', + metavar='', + help='Container name to display', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + + data = lib_container.show_container( + self.app.restapi, + self.app.client_manager.object.endpoint, + parsed_args.container, + ) + + return zip(*sorted(six.iteritems(data))) diff --git a/openstackclient/object/v1/lib/container.py b/openstackclient/object/v1/lib/container.py index f30533c804..5103d9d4ae 100644 --- a/openstackclient/object/v1/lib/container.py +++ b/openstackclient/object/v1/lib/container.py @@ -16,6 +16,11 @@ """Object v1 API library""" +try: + from urllib.parse import urlparse +except ImportError: + from urlparse import urlparse + def list_containers( api, @@ -75,3 +80,34 @@ def list_containers( url = "%s?%s" % (object_url, query) response = api.request('GET', url) return response.json() + + +def show_container( + api, + url, + container, +): + """Get container details + + :param api: a restapi object + :param url: endpoint + :param container: name of container to show + :returns: dict of returned headers + """ + + object_url = "%s/%s" % (url, container) + url_parts = urlparse(url) + response = api.request('HEAD', object_url) + data = { + 'account': url_parts.path.split('/')[-1], + 'container': container, + } + data['object_count'] = response.headers.get( + 'x-container-object-count', None) + data['bytes_used'] = response.headers.get('x-container-bytes-used', None) + data['read_acl'] = response.headers.get('x-container-read', None) + data['write_acl'] = response.headers.get('x-container-write', None) + data['sync_to'] = response.headers.get('x-container-sync-to', None) + data['sync_key'] = response.headers.get('x-container-sync-key', None) + + return data diff --git a/openstackclient/object/v1/lib/object.py b/openstackclient/object/v1/lib/object.py index d7c4a1ce37..8ad5e5a51a 100644 --- a/openstackclient/object/v1/lib/object.py +++ b/openstackclient/object/v1/lib/object.py @@ -16,6 +16,13 @@ """Object v1 API library""" +import six + +try: + from urllib.parse import urlparse +except ImportError: + from urlparse import urlparse + def list_objects( api, @@ -95,3 +102,47 @@ def list_objects( url = "%s/%s?%s" % (object_url, container, query) response = api.request('GET', url) return response.json() + + +def show_object( + api, + url, + container, + obj, +): + """Get object details + + :param api: a restapi object + :param url: endpoint + :param container: container name to get a listing for + :returns: dict of object properties + """ + + object_url = "%s/%s/%s" % (url, container, obj) + url_parts = urlparse(url) + response = api.request('HEAD', object_url) + data = { + 'account': url_parts.path.split('/')[-1], + 'container': container, + 'object': obj, + } + #print "data: %s" % data + data['content-type'] = response.headers.get('content-type', None) + if 'content-length' in response.headers: + data['content-length'] = response.headers.get('content-length', None) + if 'last-modified' in response.headers: + data['last-modified'] = response.headers.get('last-modified', None) + if 'etag' in response.headers: + data['etag'] = response.headers.get('etag', None) + if 'x-object-manifest' in response.headers: + data['x-object-manifest'] = response.headers.get( + 'x-object-manifest', None) + for key, value in six.iteritems(response.headers): + if key.startswith('x-object-meta-'): + data[key[len('x-object-meta-'):].title()] = value + elif key not in ( + 'content-type', 'content-length', 'last-modified', + 'etag', 'date', 'x-object-manifest'): + data[key.title()] = value + + return data diff --git a/openstackclient/object/v1/object.py b/openstackclient/object/v1/object.py index c6bd755b32..426a52ad61 100644 --- a/openstackclient/object/v1/object.py +++ b/openstackclient/object/v1/object.py @@ -17,8 +17,10 @@ import logging +import six from cliff import lister +from cliff import show from openstackclient.common import utils from openstackclient.object.v1.lib import object as lib_object @@ -116,3 +118,35 @@ def take_action(self, parsed_args): s, columns, formatters={}, ) for s in data)) + + +class ShowObject(show.ShowOne): + """Show object information""" + + log = logging.getLogger(__name__ + '.ShowObject') + + def get_parser(self, prog_name): + parser = super(ShowObject, self).get_parser(prog_name) + parser.add_argument( + 'container', + metavar='', + help='Container name for object to display', + ) + parser.add_argument( + 'object', + metavar='', + help='Object name to display', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + + data = lib_object.show_object( + self.app.restapi, + self.app.client_manager.object.endpoint, + parsed_args.container, + parsed_args.object, + ) + + return zip(*sorted(six.iteritems(data))) diff --git a/openstackclient/tests/object/test_container.py b/openstackclient/tests/object/test_container.py index 9b53e36037..24d6763376 100644 --- a/openstackclient/tests/object/test_container.py +++ b/openstackclient/tests/object/test_container.py @@ -313,3 +313,49 @@ def test_object_list_containers_all(self, c_mock): (object_fakes.container_name_3, ), ) self.assertEqual(tuple(data), datalist) + + +@mock.patch( + 'openstackclient.object.v1.container.lib_container.show_container' +) +class TestContainerShow(TestObject): + + def setUp(self): + super(TestContainerShow, self).setUp() + + # Get the command object to test + self.cmd = container.ShowContainer(self.app, None) + + def test_container_show(self, c_mock): + c_mock.return_value = copy.deepcopy(object_fakes.CONTAINER) + + arglist = [ + object_fakes.container_name, + ] + verifylist = [ + ('container', object_fakes.container_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + } + # lib.container.show_container(api, url, container) + c_mock.assert_called_with( + self.app.restapi, + AUTH_URL, + object_fakes.container_name, + **kwargs + ) + + collist = ('bytes', 'count', 'name') + self.assertEqual(columns, collist) + datalist = ( + object_fakes.container_bytes, + object_fakes.container_count, + object_fakes.container_name, + ) + self.assertEqual(data, datalist) diff --git a/openstackclient/tests/object/test_object.py b/openstackclient/tests/object/test_object.py index ddd5b592a9..1ceb0a59dd 100644 --- a/openstackclient/tests/object/test_object.py +++ b/openstackclient/tests/object/test_object.py @@ -360,3 +360,54 @@ def test_object_list_objects_all(self, o_mock): (object_fakes.object_name_2, ), ) self.assertEqual(tuple(data), datalist) + + +@mock.patch( + 'openstackclient.object.v1.object.lib_object.show_object' +) +class TestObjectShow(TestObject): + + def setUp(self): + super(TestObjectShow, self).setUp() + + # Get the command object to test + self.cmd = obj.ShowObject(self.app, None) + + def test_object_show(self, c_mock): + c_mock.return_value = copy.deepcopy(object_fakes.OBJECT) + + arglist = [ + object_fakes.container_name, + object_fakes.object_name_1, + ] + verifylist = [ + ('container', object_fakes.container_name), + ('object', object_fakes.object_name_1), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + } + # lib.container.show_container(api, url, container) + c_mock.assert_called_with( + self.app.restapi, + AUTH_URL, + object_fakes.container_name, + object_fakes.object_name_1, + **kwargs + ) + + collist = ('bytes', 'content_type', 'hash', 'last_modified', 'name') + self.assertEqual(columns, collist) + datalist = ( + object_fakes.object_bytes_1, + object_fakes.object_content_type_1, + object_fakes.object_hash_1, + object_fakes.object_modified_1, + object_fakes.object_name_1, + ) + self.assertEqual(data, datalist) diff --git a/openstackclient/tests/object/v1/lib/test_container.py b/openstackclient/tests/object/v1/lib/test_container.py index 1c0112c884..3b9976a1f3 100644 --- a/openstackclient/tests/object/v1/lib/test_container.py +++ b/openstackclient/tests/object/v1/lib/test_container.py @@ -15,19 +15,17 @@ """Test Object API library module""" -from __future__ import unicode_literals - import mock - from openstackclient.object.v1.lib import container as lib_container from openstackclient.tests.common import test_restapi as restapi from openstackclient.tests import fakes from openstackclient.tests import utils +fake_account = 'q12we34r' fake_auth = '11223344556677889900' -fake_url = 'http://gopher.com' +fake_url = 'http://gopher.com/v1/' + fake_account fake_container = 'rainbarrel' @@ -38,18 +36,18 @@ def __init__(self, endpoint=None, **kwargs): self.token = fake_auth -class TestObject(utils.TestCommand): +class TestContainer(utils.TestCommand): def setUp(self): - super(TestObject, self).setUp() + super(TestContainer, self).setUp() self.app.client_manager = fakes.FakeClientManager() self.app.client_manager.object = FakeClient() self.app.restapi = mock.MagicMock() -class TestObjectListContainers(TestObject): +class TestContainerList(TestContainer): - def test_list_containers_no_options(self): + def test_container_list_no_options(self): resp = [{'name': 'is-name'}] self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) @@ -65,7 +63,7 @@ def test_list_containers_no_options(self): ) self.assertEqual(data, resp) - def test_list_containers_marker(self): + def test_container_list_marker(self): resp = [{'name': 'is-name'}] self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) @@ -82,7 +80,7 @@ def test_list_containers_marker(self): ) self.assertEqual(data, resp) - def test_list_containers_limit(self): + def test_container_list_limit(self): resp = [{'name': 'is-name'}] self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) @@ -99,7 +97,7 @@ def test_list_containers_limit(self): ) self.assertEqual(data, resp) - def test_list_containers_end_marker(self): + def test_container_list_end_marker(self): resp = [{'name': 'is-name'}] self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) @@ -116,7 +114,7 @@ def test_list_containers_end_marker(self): ) self.assertEqual(data, resp) - def test_list_containers_prefix(self): + def test_container_list_prefix(self): resp = [{'name': 'is-name'}] self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) @@ -133,7 +131,7 @@ def test_list_containers_prefix(self): ) self.assertEqual(data, resp) - def test_list_containers_full_listing(self): + def test_container_list_full_listing(self): def side_effect(*args, **kwargs): rv = self.app.restapi.request.return_value @@ -159,3 +157,38 @@ def side_effect(*args, **kwargs): fake_url + '?format=json&marker=is-name', ) self.assertEqual(data, resp) + + +class TestContainerShow(TestContainer): + + def test_container_show_no_options(self): + resp = { + 'x-container-object-count': 1, + 'x-container-bytes-used': 577, + } + self.app.restapi.request.return_value = \ + restapi.FakeResponse(headers=resp) + + data = lib_container.show_container( + self.app.restapi, + self.app.client_manager.object.endpoint, + 'is-name', + ) + + # Check expected values + self.app.restapi.request.assert_called_with( + 'HEAD', + fake_url + '/is-name', + ) + + data_expected = { + 'account': fake_account, + 'container': 'is-name', + 'object_count': 1, + 'bytes_used': 577, + 'read_acl': None, + 'write_acl': None, + 'sync_to': None, + 'sync_key': None, + } + self.assertEqual(data, data_expected) diff --git a/openstackclient/tests/object/v1/lib/test_object.py b/openstackclient/tests/object/v1/lib/test_object.py index b4793cc24a..0104183e03 100644 --- a/openstackclient/tests/object/v1/lib/test_object.py +++ b/openstackclient/tests/object/v1/lib/test_object.py @@ -15,8 +15,6 @@ """Test Object API library module""" -from __future__ import unicode_literals - import mock from openstackclient.object.v1.lib import object as lib_object @@ -25,10 +23,12 @@ from openstackclient.tests import utils +fake_account = 'q12we34r' fake_auth = '11223344556677889900' -fake_url = 'http://gopher.com' +fake_url = 'http://gopher.com/v1/' + fake_account fake_container = 'rainbarrel' +fake_object = 'raindrop' class FakeClient(object): @@ -203,3 +203,74 @@ def side_effect(*args, **kwargs): fake_url + '/' + fake_container + '?format=json&marker=is-name', ) self.assertEqual(data, resp) + + +class TestObjectShowObjects(TestObject): + + def test_object_show_no_options(self): + resp = { + 'content-type': 'text/alpha', + } + self.app.restapi.request.return_value = \ + restapi.FakeResponse(headers=resp) + + data = lib_object.show_object( + self.app.restapi, + self.app.client_manager.object.endpoint, + fake_container, + fake_object, + ) + + # Check expected values + self.app.restapi.request.assert_called_with( + 'HEAD', + fake_url + '/%s/%s' % (fake_container, fake_object), + ) + + data_expected = { + 'account': fake_account, + 'container': fake_container, + 'object': fake_object, + 'content-type': 'text/alpha', + } + self.assertEqual(data, data_expected) + + def test_object_show_all_options(self): + resp = { + 'content-type': 'text/alpha', + 'content-length': 577, + 'last-modified': '20130101', + 'etag': 'qaz', + 'x-object-manifest': None, + 'x-object-meta-wife': 'Wilma', + 'x-tra-header': 'yabba-dabba-do', + } + self.app.restapi.request.return_value = \ + restapi.FakeResponse(headers=resp) + + data = lib_object.show_object( + self.app.restapi, + self.app.client_manager.object.endpoint, + fake_container, + fake_object, + ) + + # Check expected values + self.app.restapi.request.assert_called_with( + 'HEAD', + fake_url + '/%s/%s' % (fake_container, fake_object), + ) + + data_expected = { + 'account': fake_account, + 'container': fake_container, + 'object': fake_object, + 'content-type': 'text/alpha', + 'content-length': 577, + 'last-modified': '20130101', + 'etag': 'qaz', + 'x-object-manifest': None, + 'Wife': 'Wilma', + 'X-Tra-Header': 'yabba-dabba-do', + } + self.assertEqual(data, data_expected) diff --git a/setup.cfg b/setup.cfg index bcf4877e9a..2ac659f442 100644 --- a/setup.cfg +++ b/setup.cfg @@ -241,7 +241,9 @@ openstack.image.v2 = openstack.object_store.v1 = container_list = openstackclient.object.v1.container:ListContainer + container_show = openstackclient.object.v1.container:ShowContainer object_list = openstackclient.object.v1.object:ListObject + object_show = openstackclient.object.v1.object:ShowObject openstack.volume.v1 = snapshot_create = openstackclient.volume.v1.snapshot:CreateSnapshot From bca4cf95789fc30577c796fdf349d072ef087f25 Mon Sep 17 00:00:00 2001 From: OpenStack Jenkins Date: Tue, 1 Oct 2013 16:15:07 +0000 Subject: [PATCH 0017/3494] Updated from global requirements Change-Id: Ic3b5de6a54951b4f9a6449f97aa1ab9c395a2f08 --- requirements.txt | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index d956a9ebd1..d906f1b66f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ pbr>=0.5.21,<1.0 -cliff>=1.4 +cliff>=1.4.3 keyring>=1.6.1,<2.0 pycrypto>=2.6 python-glanceclient>=0.9.0 python-keystoneclient>=0.3.2 -python-novaclient>=2.12.0 +python-novaclient>=2.15.0 python-cinderclient>=1.0.5 diff --git a/setup.py b/setup.py index 2a0786a8b2..70c2b3f32b 100644 --- a/setup.py +++ b/setup.py @@ -18,5 +18,5 @@ import setuptools setuptools.setup( - setup_requires=['pbr>=0.5.21,<1.0'], + setup_requires=['pbr'], pbr=True) From 3f9c68f1c6585c1d7f31b75c8719efc47230d86f Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 7 Oct 2013 12:23:00 -0500 Subject: [PATCH 0018/3494] Add options to support TLS certificate verification Add --os-cacert and --verify|--insecure options using the same sematics as the other project CLIs. --verify is included for completeness. Bug: 1236608 Change-Id: I8a116d790db5aa4cb17a2207efedce7cb229eba3 --- openstackclient/common/clientmanager.py | 12 +++++++++- openstackclient/common/restapi.py | 1 + openstackclient/compute/client.py | 4 ++-- openstackclient/identity/client.py | 5 ++++- openstackclient/image/client.py | 7 +++++- openstackclient/shell.py | 29 +++++++++++++++++++++++-- openstackclient/volume/client.py | 2 ++ 7 files changed, 53 insertions(+), 7 deletions(-) diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index 24b09beb71..85f544e4dc 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -50,7 +50,7 @@ class ClientManager(object): def __init__(self, token=None, url=None, auth_url=None, project_name=None, project_id=None, username=None, password=None, - region_name=None, api_version=None): + region_name=None, verify=True, api_version=None): self._token = token self._url = url self._auth_url = auth_url @@ -62,6 +62,16 @@ def __init__(self, token=None, url=None, auth_url=None, project_name=None, self._api_version = api_version self._service_catalog = None + # verify is the Requests-compatible form + self._verify = verify + # also store in the form used by the legacy client libs + self._cacert = None + if verify is True or verify is False: + self._insecure = not verify + else: + self._cacert = verify + self._insecure = True + self.auth_ref = None if not self._url: diff --git a/openstackclient/common/restapi.py b/openstackclient/common/restapi.py index 4cea5a06cc..a45c842607 100644 --- a/openstackclient/common/restapi.py +++ b/openstackclient/common/restapi.py @@ -53,6 +53,7 @@ def __init__( os_auth=None, user_agent=USER_AGENT, debug=None, + verify=True, **kwargs ): self.set_auth(os_auth) diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py index 9bd40a4fc5..4d3b1b7155 100644 --- a/openstackclient/compute/client.py +++ b/openstackclient/compute/client.py @@ -38,8 +38,8 @@ def make_client(instance): api_key=instance._password, project_id=instance._project_name, auth_url=instance._auth_url, - # FIXME(dhellmann): add constructor argument for this - insecure=False, + cacert=instance._cacert, + insecure=instance._insecure, region_name=instance._region_name, # FIXME(dhellmann): get endpoint_type from option? endpoint_type='publicURL', diff --git a/openstackclient/identity/client.py b/openstackclient/identity/client.py index 8c9437ba5b..4814bc3e2e 100644 --- a/openstackclient/identity/client.py +++ b/openstackclient/identity/client.py @@ -47,7 +47,10 @@ def make_client(instance): tenant_name=instance._project_name, tenant_id=instance._project_id, auth_url=instance._auth_url, - region_name=instance._region_name) + region_name=instance._region_name, + cacert=instance._cacert, + insecure=instance._insecure, + ) instance.auth_ref = client.auth_ref return client diff --git a/openstackclient/image/client.py b/openstackclient/image/client.py index 70bef1c8a6..d56ca3b2ff 100644 --- a/openstackclient/image/client.py +++ b/openstackclient/image/client.py @@ -40,7 +40,12 @@ def make_client(instance): if not instance._url: instance._url = instance.get_endpoint_for_service_type(API_NAME) - return image_client(instance._url, token=instance._token) + return image_client( + instance._url, + token=instance._token, + cacert=instance._cacert, + insecure=instance._insecure, + ) # NOTE(dtroyer): glanceclient.v1.image.ImageManager() doesn't have a find() diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 6797790708..d0905fd9f7 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -79,6 +79,9 @@ def __init__(self): # password flow auth self.auth_client = None + # Assume TLS host certificate verification is enabled + self.verify = True + # NOTE(dtroyer): This hack changes the help action that Cliff # automatically adds to the parser so we can defer # its execution until after the api-versioned commands @@ -158,6 +161,22 @@ def build_option_parser(self, description, version): metavar='', default=env('OS_REGION_NAME'), help='Authentication region name (Env: OS_REGION_NAME)') + parser.add_argument( + '--os-cacert', + metavar='', + default=env('OS_CACERT'), + help='CA certificate bundle file (Env: OS_CACERT)') + verify_group = parser.add_mutually_exclusive_group() + verify_group.add_argument( + '--verify', + action='store_true', + help='Verify server certificate (default)', + ) + verify_group.add_argument( + '--insecure', + action='store_true', + help='Disable server certificate verification', + ) parser.add_argument( '--os-default-domain', metavar='', @@ -299,7 +318,9 @@ def authenticate_user(self): username=self.options.os_username, password=self.options.os_password, region_name=self.options.os_region_name, - api_version=self.api_version) + verify=self.verify, + api_version=self.api_version, + ) return def init_keyring_backend(self): @@ -387,7 +408,11 @@ def initialize_app(self, argv): self.DeferredHelpAction(self.parser, self.parser, None, None) # Set up common client session - self.restapi = restapi.RESTApi() + if self.options.os_cacert: + self.verify = self.options.os_cacert + else: + self.verify = not self.options.insecure + self.restapi = restapi.RESTApi(verify=self.verify) def prepare_to_run_command(self, cmd): """Set up auth and API versions""" diff --git a/openstackclient/volume/client.py b/openstackclient/volume/client.py index 92f3b14a48..626b23f1c1 100644 --- a/openstackclient/volume/client.py +++ b/openstackclient/volume/client.py @@ -40,6 +40,8 @@ def make_client(instance): api_key=instance._password, project_id=instance._project_name, auth_url=instance._auth_url, + cacert=instance._cacert, + insecure=instance._insecure, ) return client From fa649f4654e49c2c2a25e8ae2d71ead555da9ea7 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 11 Oct 2013 11:52:37 -0500 Subject: [PATCH 0019/3494] Sync oslo-incubator for py33 fixes Change-Id: I261ec6bb34b29169ba3547305deab051f85a3d4d --- .../openstack/common/gettextutils.py | 198 ++++++++++++++---- tools/install_venv_common.py | 46 +--- 2 files changed, 156 insertions(+), 88 deletions(-) diff --git a/openstackclient/openstack/common/gettextutils.py b/openstackclient/openstack/common/gettextutils.py index 2dd5449e6c..d4c93f4a3d 100644 --- a/openstackclient/openstack/common/gettextutils.py +++ b/openstackclient/openstack/common/gettextutils.py @@ -1,8 +1,8 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2012 Red Hat, Inc. -# All Rights Reserved. # Copyright 2013 IBM Corp. +# All Rights Reserved. # # 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 @@ -26,22 +26,46 @@ import copy import gettext -import logging.handlers +import logging import os import re -import UserString +try: + import UserString as _userString +except ImportError: + import collections as _userString +from babel import localedata import six _localedir = os.environ.get('openstackclient'.upper() + '_LOCALEDIR') _t = gettext.translation('openstackclient', localedir=_localedir, fallback=True) +_AVAILABLE_LANGUAGES = {} +USE_LAZY = False + + +def enable_lazy(): + """Convenience function for configuring _() to use lazy gettext + + Call this at the start of execution to enable the gettextutils._ + function to use lazy gettext functionality. This is useful if + your project is importing _ directly instead of using the + gettextutils.install() way of importing the _ function. + """ + global USE_LAZY + USE_LAZY = True + def _(msg): - return _t.ugettext(msg) + if USE_LAZY: + return Message(msg, 'openstackclient') + else: + if six.PY3: + return _t.gettext(msg) + return _t.ugettext(msg) -def install(domain): +def install(domain, lazy=False): """Install a _() function using the given translation domain. Given a translation domain, install a _() function using gettext's @@ -51,52 +75,60 @@ def install(domain): overriding the default localedir (e.g. /usr/share/locale) using a translation-domain-specific environment variable (e.g. NOVA_LOCALEDIR). - """ - gettext.install(domain, - localedir=os.environ.get(domain.upper() + '_LOCALEDIR'), - unicode=True) - - -""" -Lazy gettext functionality. - -The following is an attempt to introduce a deferred way -to do translations on messages in OpenStack. We attempt to -override the standard _() function and % (format string) operation -to build Message objects that can later be translated when we have -more information. Also included is an example LogHandler that -translates Messages to an associated locale, effectively allowing -many logs, each with their own locale. -""" - - -def get_lazy_gettext(domain): - """Assemble and return a lazy gettext function for a given domain. - Factory method for a project/module to get a lazy gettext function - for its own translation domain (i.e. nova, glance, cinder, etc.) + :param domain: the translation domain + :param lazy: indicates whether or not to install the lazy _() function. + The lazy _() introduces a way to do deferred translation + of messages by installing a _ that builds Message objects, + instead of strings, which can then be lazily translated into + any available locale. """ - - def _lazy_gettext(msg): - """Create and return a Message object. - - Message encapsulates a string so that we can translate it later when - needed. - """ - return Message(msg, domain) - - return _lazy_gettext + if lazy: + # NOTE(mrodden): Lazy gettext functionality. + # + # The following introduces a deferred way to do translations on + # messages in OpenStack. We override the standard _() function + # and % (format string) operation to build Message objects that can + # later be translated when we have more information. + # + # Also included below is an example LocaleHandler that translates + # Messages to an associated locale, effectively allowing many logs, + # each with their own locale. + + def _lazy_gettext(msg): + """Create and return a Message object. + + Lazy gettext function for a given domain, it is a factory method + for a project/module to get a lazy gettext function for its own + translation domain (i.e. nova, glance, cinder, etc.) + + Message encapsulates a string so that we can translate + it later when needed. + """ + return Message(msg, domain) + + from six import moves + moves.builtins.__dict__['_'] = _lazy_gettext + else: + localedir = '%s_LOCALEDIR' % domain.upper() + if six.PY3: + gettext.install(domain, + localedir=os.environ.get(localedir)) + else: + gettext.install(domain, + localedir=os.environ.get(localedir), + unicode=True) -class Message(UserString.UserString, object): +class Message(_userString.UserString, object): """Class used to encapsulate translatable messages.""" def __init__(self, msg, domain): # _msg is the gettext msgid and should never change self._msg = msg self._left_extra_msg = '' self._right_extra_msg = '' + self._locale = None self.params = None - self.locale = None self.domain = domain @property @@ -116,8 +148,13 @@ def data(self): localedir=localedir, fallback=True) + if six.PY3: + ugettext = lang.gettext + else: + ugettext = lang.ugettext + full_msg = (self._left_extra_msg + - lang.ugettext(self._msg) + + ugettext(self._msg) + self._right_extra_msg) if self.params is not None: @@ -125,12 +162,39 @@ def data(self): return six.text_type(full_msg) + @property + def locale(self): + return self._locale + + @locale.setter + def locale(self, value): + self._locale = value + if not self.params: + return + + # This Message object may have been constructed with one or more + # Message objects as substitution parameters, given as a single + # Message, or a tuple or Map containing some, so when setting the + # locale for this Message we need to set it for those Messages too. + if isinstance(self.params, Message): + self.params.locale = value + return + if isinstance(self.params, tuple): + for param in self.params: + if isinstance(param, Message): + param.locale = value + return + if isinstance(self.params, dict): + for param in self.params.values(): + if isinstance(param, Message): + param.locale = value + def _save_dictionary_parameter(self, dict_param): full_msg = self.data # look for %(blah) fields in string; # ignore %% and deal with the # case where % is first character on the line - keys = re.findall('(?:[^%]|^)%\((\w*)\)[a-z]', full_msg) + keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', full_msg) # if we don't find any %(blah) blocks but have a %s if not keys and re.findall('(?:[^%]|^)%[a-z]', full_msg): @@ -143,7 +207,7 @@ def _save_dictionary_parameter(self, dict_param): params[key] = copy.deepcopy(dict_param[key]) except TypeError: # cast uncopyable thing to unicode string - params[key] = unicode(dict_param[key]) + params[key] = six.text_type(dict_param[key]) return params @@ -162,7 +226,7 @@ def _save_parameters(self, other): try: self.params = copy.deepcopy(other) except TypeError: - self.params = unicode(other) + self.params = six.text_type(other) return self @@ -171,11 +235,13 @@ def __unicode__(self): return self.data def __str__(self): + if six.PY3: + return self.__unicode__() return self.data.encode('utf-8') def __getstate__(self): to_copy = ['_msg', '_right_extra_msg', '_left_extra_msg', - 'domain', 'params', 'locale'] + 'domain', 'params', '_locale'] new_dict = self.__dict__.fromkeys(to_copy) for attr in to_copy: new_dict[attr] = copy.deepcopy(self.__dict__[attr]) @@ -229,7 +295,47 @@ def __getattribute__(self, name): if name in ops: return getattr(self.data, name) else: - return UserString.UserString.__getattribute__(self, name) + return _userString.UserString.__getattribute__(self, name) + + +def get_available_languages(domain): + """Lists the available languages for the given translation domain. + + :param domain: the domain to get languages for + """ + if domain in _AVAILABLE_LANGUAGES: + return copy.copy(_AVAILABLE_LANGUAGES[domain]) + + localedir = '%s_LOCALEDIR' % domain.upper() + find = lambda x: gettext.find(domain, + localedir=os.environ.get(localedir), + languages=[x]) + + # NOTE(mrodden): en_US should always be available (and first in case + # order matters) since our in-line message strings are en_US + language_list = ['en_US'] + # NOTE(luisg): Babel <1.0 used a function called list(), which was + # renamed to locale_identifiers() in >=1.0, the requirements master list + # requires >=0.9.6, uncapped, so defensively work with both. We can remove + # this check when the master list updates to >=1.0, and all projects udpate + list_identifiers = (getattr(localedata, 'list', None) or + getattr(localedata, 'locale_identifiers')) + locale_identifiers = list_identifiers() + for i in locale_identifiers: + if find(i) is not None: + language_list.append(i) + _AVAILABLE_LANGUAGES[domain] = language_list + return copy.copy(language_list) + + +def get_localized_message(message, user_locale): + """Gets a localized version of the given message in the given locale.""" + if isinstance(message, Message): + if user_locale: + message.locale = user_locale + return six.text_type(message) + else: + return message class LocaleHandler(logging.Handler): diff --git a/tools/install_venv_common.py b/tools/install_venv_common.py index f428c1e021..1bab88a3fd 100644 --- a/tools/install_venv_common.py +++ b/tools/install_venv_common.py @@ -114,15 +114,12 @@ def install_dependencies(self): print('Installing dependencies with pip (this can take a while)...') # First things first, make sure our venv has the latest pip and - # setuptools. - self.pip_install('pip>=1.3') + # setuptools and pbr + self.pip_install('pip>=1.4') self.pip_install('setuptools') + self.pip_install('pbr') - self.pip_install('-r', self.requirements) - self.pip_install('-r', self.test_requirements) - - def post_process(self): - self.get_distro().post_process() + self.pip_install('-r', self.requirements, '-r', self.test_requirements) def parse_args(self, argv): """Parses command-line arguments.""" @@ -156,14 +153,6 @@ def install_virtualenv(self): ' requires virtualenv, please install it using your' ' favorite package management tool' % self.project) - def post_process(self): - """Any distribution-specific post-processing gets done here. - - In particular, this is useful for applying patches to code inside - the venv. - """ - pass - class Fedora(Distro): """This covers all Fedora-based distributions. @@ -175,10 +164,6 @@ def check_pkg(self, pkg): return self.run_command_with_code(['rpm', '-q', pkg], check_exit_code=False)[1] == 0 - def apply_patch(self, originalfile, patchfile): - self.run_command(['patch', '-N', originalfile, patchfile], - check_exit_code=False) - def install_virtualenv(self): if self.check_cmd('virtualenv'): return @@ -187,26 +172,3 @@ def install_virtualenv(self): self.die("Please install 'python-virtualenv'.") super(Fedora, self).install_virtualenv() - - def post_process(self): - """Workaround for a bug in eventlet. - - This currently affects RHEL6.1, but the fix can safely be - applied to all RHEL and Fedora distributions. - - This can be removed when the fix is applied upstream. - - Nova: https://bugs.launchpad.net/nova/+bug/884915 - Upstream: https://bitbucket.org/eventlet/eventlet/issue/89 - RHEL: https://bugzilla.redhat.com/958868 - """ - - # Install "patch" program if it's not there - if not self.check_pkg('patch'): - self.die("Please install 'patch'.") - - # Apply the eventlet patch - self.apply_patch(os.path.join(self.venv, 'lib', self.py_version, - 'site-packages', - 'eventlet/green/subprocess.py'), - 'contrib/redhat-eventlet.patch') From a1bda219a5160acca6423aea932a9f8103049373 Mon Sep 17 00:00:00 2001 From: OpenStack Jenkins Date: Wed, 16 Oct 2013 12:21:38 +0000 Subject: [PATCH 0020/3494] Updated from global requirements Change-Id: I2a306dd8edc030d3f989e9947dec784f502b3953 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 27de420d67..77788e6c5e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -11,4 +11,4 @@ testrepository>=0.0.17 testtools>=0.9.32 WebOb>=1.2.3,<1.3 -Babel>=0.9.6 +Babel>=1.3 From 1fa1330e1d8bc9361bbe8c88490a20f9b8fddec1 Mon Sep 17 00:00:00 2001 From: Dirk Mueller Date: Thu, 24 Oct 2013 14:24:33 +0200 Subject: [PATCH 0021/3494] Adjust to non-deprecated names in Keyring 1.6.+ Keyring 1.1 moved the concrete backend implementations into their own modules. As we depend on 1.6.1+, we can make use of the new name and remove the old one without deprecation-fallback. Change-Id: I0682b13fc9f488b3f3d9fd057f712909fcd48bc4 --- openstackclient/common/openstackkeyring.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstackclient/common/openstackkeyring.py b/openstackclient/common/openstackkeyring.py index e7431e5488..34c994b743 100644 --- a/openstackclient/common/openstackkeyring.py +++ b/openstackclient/common/openstackkeyring.py @@ -24,7 +24,7 @@ KEYRING_FILE = os.path.join(os.path.expanduser('~'), '.openstack-keyring.cfg') -class OpenstackKeyring(keyring.backend.BasicFileKeyring): +class OpenstackKeyring(keyring.backends.file.BaseKeyring): """Openstack Keyring to store encrypted password.""" filename = KEYRING_FILE From 9137cc304d73aa60548fa255d2bf668600aa0ec4 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 11 Apr 2013 16:02:53 -0500 Subject: [PATCH 0022/3494] Do lookups for user, project in volume create This required https://review.openstack.org/26323 in keystoneclient, merged long ago... Also adds some tests for 'volume create' Change-Id: I55bededbc20b5dcf2833c59eb2b6b069703d8a9a --- openstackclient/tests/volume/test_volume.py | 51 ---- openstackclient/tests/volume/v1/__init__.py | 14 + openstackclient/tests/volume/v1/fakes.py | 44 +++ .../tests/volume/v1/test_volume.py | 37 +++ .../tests/volume/v1/test_volumecmd.py | 269 ++++++++++++++++++ openstackclient/volume/v1/volume.py | 27 +- 6 files changed, 383 insertions(+), 59 deletions(-) delete mode 100644 openstackclient/tests/volume/test_volume.py create mode 100644 openstackclient/tests/volume/v1/__init__.py create mode 100644 openstackclient/tests/volume/v1/fakes.py create mode 100644 openstackclient/tests/volume/v1/test_volume.py create mode 100644 openstackclient/tests/volume/v1/test_volumecmd.py diff --git a/openstackclient/tests/volume/test_volume.py b/openstackclient/tests/volume/test_volume.py deleted file mode 100644 index 548fbf7413..0000000000 --- a/openstackclient/tests/volume/test_volume.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2013 OpenStack, LLC. -# -# 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 mock - -from openstackclient.common import clientmanager -from openstackclient.tests import utils -from openstackclient.volume import client as volume_client - - -AUTH_TOKEN = "foobar" -AUTH_URL = "http://0.0.0.0" - - -class FakeClient(object): - def __init__(self, endpoint=None, **kwargs): - self.client = mock.MagicMock() - self.client.auth_token = AUTH_TOKEN - self.client.auth_url = AUTH_URL - - -class TestVolume(utils.TestCase): - def setUp(self): - super(TestVolume, self).setUp() - - api_version = {"volume": "1"} - - volume_client.API_VERSIONS = { - "1": "openstackclient.tests.volume.test_volume.FakeClient" - } - - self.cm = clientmanager.ClientManager(token=AUTH_TOKEN, - url=AUTH_URL, - auth_url=AUTH_URL, - api_version=api_version) - - def test_make_client(self): - self.assertEqual(self.cm.volume.client.auth_token, AUTH_TOKEN) - self.assertEqual(self.cm.volume.client.auth_url, AUTH_URL) diff --git a/openstackclient/tests/volume/v1/__init__.py b/openstackclient/tests/volume/v1/__init__.py new file mode 100644 index 0000000000..c534c012e8 --- /dev/null +++ b/openstackclient/tests/volume/v1/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2013 OpenStack Foundation +# +# 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. +# diff --git a/openstackclient/tests/volume/v1/fakes.py b/openstackclient/tests/volume/v1/fakes.py new file mode 100644 index 0000000000..a382dbb8b7 --- /dev/null +++ b/openstackclient/tests/volume/v1/fakes.py @@ -0,0 +1,44 @@ +# Copyright 2013 Nebula Inc. +# +# 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 mock + +from openstackclient.tests import fakes + +volume_id = 'vvvvvvvv-vvvv-vvvv-vvvvvvvv' +volume_name = 'nigel' +volume_description = 'Nigel Tufnel' +volume_size = 120 +volume_metadata = {} + +VOLUME = { + 'id': volume_id, + 'display_name': volume_name, + 'display_description': volume_description, + 'size': volume_size, + 'status': '', + 'attach_status': 'detatched', + 'metadata': volume_metadata, +} + + +class FakeVolumev1Client(object): + def __init__(self, **kwargs): + self.volumes = mock.Mock() + self.volumes.resource_class = fakes.FakeResource(None, {}) + self.services = mock.Mock() + self.services.resource_class = fakes.FakeResource(None, {}) + self.auth_token = kwargs['token'] + self.management_url = kwargs['endpoint'] diff --git a/openstackclient/tests/volume/v1/test_volume.py b/openstackclient/tests/volume/v1/test_volume.py new file mode 100644 index 0000000000..58024f0bfd --- /dev/null +++ b/openstackclient/tests/volume/v1/test_volume.py @@ -0,0 +1,37 @@ +# Copyright 2013 OpenStack, LLC. +# +# 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 openstackclient.tests.identity.v2_0 import fakes as identity_fakes +from openstackclient.tests import utils +from openstackclient.tests.volume.v1 import fakes + + +AUTH_TOKEN = "foobar" +AUTH_URL = "http://0.0.0.0" + + +class TestVolumev1(utils.TestCommand): + def setUp(self): + super(TestVolumev1, self).setUp() + + self.app.client_manager.volume = fakes.FakeVolumev1Client( + endpoint=AUTH_URL, + token=AUTH_TOKEN, + ) + + self.app.client_manager.identity = identity_fakes.FakeIdentityv2Client( + endpoint=AUTH_URL, + token=AUTH_TOKEN, + ) diff --git a/openstackclient/tests/volume/v1/test_volumecmd.py b/openstackclient/tests/volume/v1/test_volumecmd.py new file mode 100644 index 0000000000..1f5ed882aa --- /dev/null +++ b/openstackclient/tests/volume/v1/test_volumecmd.py @@ -0,0 +1,269 @@ +# Copyright 2013 Nebula Inc. +# +# 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 copy + +from openstackclient.tests import fakes +from openstackclient.tests.identity.v2_0 import fakes as identity_fakes +from openstackclient.tests.volume.v1 import fakes as volume_fakes +from openstackclient.tests.volume.v1 import test_volume +from openstackclient.volume.v1 import volume + + +class TestVolume(test_volume.TestVolumev1): + + def setUp(self): + super(TestVolume, self).setUp() + + # Get a shortcut to the VolumeManager Mock + self.volumes_mock = self.app.client_manager.volume.volumes + self.volumes_mock.reset_mock() + + # Get a shortcut to the TenantManager Mock + self.projects_mock = self.app.client_manager.identity.tenants + self.projects_mock.reset_mock() + + # Get a shortcut to the UserManager Mock + self.users_mock = self.app.client_manager.identity.users + self.users_mock.reset_mock() + + +# TODO(dtroyer): The volume create tests are incomplete, only the minimal +# options and the options that require additional processing +# are implemented at this time. + +class TestVolumeCreate(TestVolume): + + def setUp(self): + super(TestVolumeCreate, self).setUp() + + self.volumes_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.VOLUME), + loaded=True, + ) + + # Get the command object to test + self.cmd = volume.CreateVolume(self.app, None) + + def test_volume_create_min_options(self): + arglist = [ + '--size', str(volume_fakes.volume_size), + volume_fakes.volume_name, + ] + verifylist = [ + ('size', volume_fakes.volume_size), + ('name', volume_fakes.volume_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + #kwargs = { + # 'metadata': volume_fakes.volume_metadata, + #} + # VolumeManager.create(size, snapshot_id=, source_volid=, + # display_name=, display_description=, + # volume_type=, user_id=, + # project_id=, availability_zone=, + # metadata=, imageRef=) + self.volumes_mock.create.assert_called_with( + volume_fakes.volume_size, + None, + None, + volume_fakes.volume_name, + None, + None, + None, + None, + None, + None, + None, + ) + + collist = ( + 'attach_status', + 'display_description', + 'display_name', + 'id', + 'properties', + 'size', + 'status', + ) + self.assertEqual(columns, collist) + datalist = ( + 'detatched', + volume_fakes.volume_description, + volume_fakes.volume_name, + volume_fakes.volume_id, + '', + volume_fakes.volume_size, + '', + ) + self.assertEqual(data, datalist) + + def test_volume_create_user_project_id(self): + # Return a project + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + # Return a user + self.users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + + arglist = [ + '--size', str(volume_fakes.volume_size), + '--project', identity_fakes.project_id, + '--user', identity_fakes.user_id, + volume_fakes.volume_name, + ] + verifylist = [ + ('size', volume_fakes.volume_size), + ('project', identity_fakes.project_id), + ('user', identity_fakes.user_id), + ('name', volume_fakes.volume_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + #kwargs = { + # 'metadata': volume_fakes.volume_metadata, + #} + # VolumeManager.create(size, snapshot_id=, source_volid=, + # display_name=, display_description=, + # volume_type=, user_id=, + # project_id=, availability_zone=, + # metadata=, imageRef=) + self.volumes_mock.create.assert_called_with( + volume_fakes.volume_size, + None, + None, + volume_fakes.volume_name, + #volume_fakes.volume_description, + None, + None, + identity_fakes.user_id, + identity_fakes.project_id, + None, + None, + None, + ) + + collist = ( + 'attach_status', + 'display_description', + 'display_name', + 'id', + 'properties', + 'size', + 'status', + ) + self.assertEqual(columns, collist) + datalist = ( + 'detatched', + volume_fakes.volume_description, + volume_fakes.volume_name, + volume_fakes.volume_id, + '', + volume_fakes.volume_size, + '', + ) + self.assertEqual(data, datalist) + + def test_volume_create_user_project_name(self): + # Return a project + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + # Return a user + self.users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + + arglist = [ + '--size', str(volume_fakes.volume_size), + '--project', identity_fakes.project_name, + '--user', identity_fakes.user_name, + volume_fakes.volume_name, + ] + verifylist = [ + ('size', volume_fakes.volume_size), + ('project', identity_fakes.project_name), + ('user', identity_fakes.user_name), + ('name', volume_fakes.volume_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + #kwargs = { + # 'metadata': volume_fakes.volume_metadata, + #} + # VolumeManager.create(size, snapshot_id=, source_volid=, + # display_name=, display_description=, + # volume_type=, user_id=, + # project_id=, availability_zone=, + # metadata=, imageRef=) + self.volumes_mock.create.assert_called_with( + volume_fakes.volume_size, + None, + None, + volume_fakes.volume_name, + #volume_fakes.volume_description, + None, + None, + identity_fakes.user_id, + identity_fakes.project_id, + None, + None, + None, + ) + + collist = ( + 'attach_status', + 'display_description', + 'display_name', + 'id', + 'properties', + 'size', + 'status', + ) + self.assertEqual(columns, collist) + datalist = ( + 'detatched', + volume_fakes.volume_description, + volume_fakes.volume_name, + volume_fakes.volume_id, + '', + volume_fakes.volume_size, + '', + ) + self.assertEqual(data, datalist) diff --git a/openstackclient/volume/v1/volume.py b/openstackclient/volume/v1/volume.py index c6690fd66f..0253bc1dad 100644 --- a/openstackclient/volume/v1/volume.py +++ b/openstackclient/volume/v1/volume.py @@ -61,14 +61,14 @@ def get_parser(self, prog_name): help='Type of volume', ) parser.add_argument( - '--user-id', - metavar='', - help='Override user id derived from context (admin only)', + '--user', + metavar='', + help='Specify a different user (admin only)', ) parser.add_argument( - '--project-id', - metavar='', - help='Override project id derived from context (admin only)', + '--project', + metavar='', + help='Specify a diffeent project (admin only)', ) parser.add_argument( '--availability-zone', @@ -98,6 +98,7 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity volume_client = self.app.client_manager.volume source_volume = None @@ -107,6 +108,16 @@ def take_action(self, parsed_args): parsed_args.source, ).id + project = None + if parsed_args.project: + project = utils.find_resource( + identity_client.tenants, parsed_args.project).id + + user = None + if parsed_args.user: + user = utils.find_resource( + identity_client.users, parsed_args.user).id + volume = volume_client.volumes.create( parsed_args.size, parsed_args.snapshot_id, @@ -114,8 +125,8 @@ def take_action(self, parsed_args): parsed_args.name, parsed_args.description, parsed_args.volume_type, - parsed_args.user_id, - parsed_args.project_id, + user, + project, parsed_args.availability_zone, parsed_args.property, parsed_args.image From 6ecf5bf4e3b3e93b0d8c4e86931c8a2a4ec081f9 Mon Sep 17 00:00:00 2001 From: OpenStack Jenkins Date: Tue, 5 Nov 2013 09:54:51 +0000 Subject: [PATCH 0023/3494] Updated from global requirements Change-Id: I421ab7a5b0c0224122cc747141956bc1282f2b07 --- requirements.txt | 4 ++-- test-requirements.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index d906f1b66f..e515041350 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,6 @@ cliff>=1.4.3 keyring>=1.6.1,<2.0 pycrypto>=2.6 python-glanceclient>=0.9.0 -python-keystoneclient>=0.3.2 +python-keystoneclient>=0.4.1 python-novaclient>=2.15.0 -python-cinderclient>=1.0.5 +python-cinderclient>=1.0.6 diff --git a/test-requirements.txt b/test-requirements.txt index 77788e6c5e..b2bed22955 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,4 @@ -hacking>=0.5.6,<0.8 +hacking>=0.8.0,<0.9 coverage>=3.6 discover From 68094619be604174a80938a1df8d08635284f262 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 7 Nov 2013 20:56:03 -0600 Subject: [PATCH 0024/3494] Remove httpretty from test requirements We don't use it... Change-Id: I41466da5153a8bdd0e4b4dd5774a9711bff3b7f5 --- test-requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 77788e6c5e..02defd7588 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,7 +3,6 @@ hacking>=0.5.6,<0.8 coverage>=3.6 discover fixtures>=0.3.14 -httpretty>=0.6.3 mock>=1.0 mox3>=0.7.0 sphinx>=1.1.2 From c946192e37111aa381097256c1fd1fb91e356783 Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Mon, 11 Nov 2013 11:09:04 -0800 Subject: [PATCH 0025/3494] Update URL for global hacking doc and fix typos * related to I579e7c889f3addc2cd40bce0c584bbc70bf435e2 Change-Id: I519155d0a47564ce18a9cd930378a3c4feaa7a77 --- HACKING.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/HACKING.rst b/HACKING.rst index 15dca8f877..8744a93ac4 100644 --- a/HACKING.rst +++ b/HACKING.rst @@ -1,9 +1,9 @@ OpenStack Style Commandments ============================ -Step 0: Read https://github.com/openstack-dev/hacking/blob/master/HACKING.rst -Step 1: Read http://www.python.org/dev/peps/pep-0008/ one more time -Step 2: Read on +- Step 1: Read the OpenStack Style Commandments + http://docs.openstack.org/developer/hacking/ +- Step 2: Read on General ------- @@ -27,7 +27,7 @@ Calling Methods --------------- Deviation! When breaking up method calls due to the 79 char line length limit, -use the alternate 4 space indent. With the frist argument on the succeeding +use the alternate 4 space indent. With the first argument on the succeeding line all arguments will then be vertically aligned. Use the same convention used with other data structure literals and terminate the method call with the last argument line ending with a comma and the closing paren on its own @@ -87,7 +87,7 @@ commandline arguments, etc.) should be presumed to be encoded as utf-8. Python 3.x Compatibility ------------------------ -OpenStackClient strives to be Python 3.3 compatibile. Common guidelines: +OpenStackClient strives to be Python 3.3 compatible. Common guidelines: * Convert print statements to functions: print statements should be converted to an appropriate log or other output mechanism. @@ -97,7 +97,7 @@ OpenStackClient strives to be Python 3.3 compatibile. Common guidelines: Running Tests ------------- -Note: Oh boy, are we behing on writing tests. But they are coming! +Note: Oh boy, are we behind on writing tests. But they are coming! The testing system is based on a combination of tox and testr. If you just want to run the whole suite, run `tox` and all will be fine. However, if From 98eaccc431865b911d64ee549c226b389315ca5d Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Fri, 1 Nov 2013 13:54:44 -0600 Subject: [PATCH 0026/3494] change execute to run Change-Id: I23a210c8771c206df14d2713a2e72ccd92402c43 --- openstackclient/shell.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/openstackclient/shell.py b/openstackclient/shell.py index d0905fd9f7..78e16cd626 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -20,6 +20,7 @@ import logging import os import sys +import traceback from cliff import app from cliff import command @@ -75,6 +76,9 @@ def __init__(self): version=openstackclient.__version__, command_manager=commandmanager.CommandManager('openstack.cli')) + # Until we have command line arguments parsed, dump any stack traces + self.dump_stack_trace = True + # This is instantiated in initialize_app() only when using # password flow auth self.auth_client = None @@ -111,6 +115,18 @@ def __init__(self): help="show this help message and exit", ) + def run(self, argv): + try: + super(OpenStackShell, self).run(argv) + except Exception as e: + if not logging.getLogger('').handlers: + logging.basicConfig() + if self.dump_stack_trace: + self.log.error(traceback.format_exc(e)) + else: + self.log.error('Exception raised: ' + str(e)) + return 1 + def build_option_parser(self, description, version): parser = super(OpenStackShell, self).build_option_parser( description, @@ -365,8 +381,10 @@ def initialize_app(self, argv): requests_log = logging.getLogger("requests") if self.options.debug: requests_log.setLevel(logging.DEBUG) + self.dump_stack_trace = True else: requests_log.setLevel(logging.WARNING) + self.dump_stack_trace = False # Save default domain self.default_domain = self.options.os_default_domain @@ -437,11 +455,7 @@ def interact(self): def main(argv=sys.argv[1:]): - try: - return OpenStackShell().run(argv) - except Exception: - return 1 - + return OpenStackShell().run(argv) if __name__ == "__main__": sys.exit(main(sys.argv[1:])) From 6460f1eb359d37dc43bdbb7d3eacc6c3f5cd7ede Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 15 Nov 2013 17:40:09 -0600 Subject: [PATCH 0027/3494] Complete basic test infrastructure This finally gets all of the API tests into a common framework regarding test classes and so forth. Change-Id: If675347129c50dcba0bfc5b6c58f5a2ca57ff46c --- openstackclient/image/v1/image.py | 2 +- openstackclient/tests/compute/test_compute.py | 50 ---- .../v2/__init__.py} | 19 +- openstackclient/tests/compute/v2/fakes.py | 55 ++++ .../tests/compute/v2/test_server.py | 63 ++++ openstackclient/tests/fakes.py | 4 + openstackclient/tests/identity/v2_0/fakes.py | 12 + .../tests/identity/v2_0/test_project.py | 3 +- .../tests/identity/v2_0/test_role.py | 3 +- .../tests/identity/v2_0/test_service.py | 3 +- .../tests/identity/v2_0/test_user.py | 3 +- openstackclient/tests/identity/v3/fakes.py | 11 + .../tests/identity/v3/test_project.py | 3 +- .../tests/identity/v3/test_role.py | 3 +- .../tests/identity/v3/test_service.py | 3 +- .../tests/identity/v3/test_user.py | 3 +- openstackclient/tests/image/test_image.py | 51 ---- .../test_identity.py => image/v1/__init__.py} | 19 +- openstackclient/tests/image/v1/fakes.py | 46 +++ openstackclient/tests/image/v1/test_image.py | 63 ++++ openstackclient/tests/image/v2/__init__.py | 14 + openstackclient/tests/image/v2/fakes.py | 46 +++ openstackclient/tests/image/v2/test_image.py | 63 ++++ .../tests/object/{ => v1}/fakes.py | 0 .../tests/object/{ => v1}/test_container.py | 2 +- .../tests/object/{ => v1}/test_object.py | 2 +- openstackclient/tests/volume/v1/fakes.py | 18 ++ .../tests/volume/v1/test_volume.py | 257 ++++++++++++++++- .../tests/volume/v1/test_volumecmd.py | 269 ------------------ 29 files changed, 652 insertions(+), 438 deletions(-) delete mode 100644 openstackclient/tests/compute/test_compute.py rename openstackclient/tests/{identity/v3/test_identity.py => compute/v2/__init__.py} (57%) create mode 100644 openstackclient/tests/compute/v2/fakes.py create mode 100644 openstackclient/tests/compute/v2/test_server.py delete mode 100644 openstackclient/tests/image/test_image.py rename openstackclient/tests/{identity/v2_0/test_identity.py => image/v1/__init__.py} (56%) create mode 100644 openstackclient/tests/image/v1/fakes.py create mode 100644 openstackclient/tests/image/v1/test_image.py create mode 100644 openstackclient/tests/image/v2/__init__.py create mode 100644 openstackclient/tests/image/v2/fakes.py create mode 100644 openstackclient/tests/image/v2/test_image.py rename openstackclient/tests/object/{ => v1}/fakes.py (100%) rename openstackclient/tests/object/{ => v1}/test_container.py (99%) rename openstackclient/tests/object/{ => v1}/test_object.py (99%) delete mode 100644 openstackclient/tests/volume/v1/test_volumecmd.py diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index 8827d0797e..40f9fce1a8 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -213,7 +213,7 @@ def take_action(self, parsed_args): image_client.images, parsed_args.image, ) - image_client.images.delete(image) + image_client.images.delete(image.id) class ListImage(lister.Lister): diff --git a/openstackclient/tests/compute/test_compute.py b/openstackclient/tests/compute/test_compute.py deleted file mode 100644 index 9d2061d284..0000000000 --- a/openstackclient/tests/compute/test_compute.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright 2013 OpenStack, LLC. -# -# 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 mock - -from openstackclient.common import clientmanager -from openstackclient.compute import client as compute_client -from openstackclient.tests import utils - - -AUTH_TOKEN = "foobar" -AUTH_URL = "http://0.0.0.0" - - -class FakeClient(object): - def __init__(self, endpoint=None, **kwargs): - self.client = mock.MagicMock() - self.client.auth_url = AUTH_URL - - -class TestCompute(utils.TestCase): - def setUp(self): - super(TestCompute, self).setUp() - - api_version = {"compute": "2"} - - compute_client.API_VERSIONS = { - "2": "openstackclient.tests.compute.test_compute.FakeClient" - } - - self.cm = clientmanager.ClientManager(token=AUTH_TOKEN, - url=AUTH_URL, - auth_url=AUTH_URL, - api_version=api_version) - - def test_make_client(self): - self.assertEqual(self.cm.compute.client.auth_token, AUTH_TOKEN) - self.assertEqual(self.cm.compute.client.auth_url, AUTH_URL) diff --git a/openstackclient/tests/identity/v3/test_identity.py b/openstackclient/tests/compute/v2/__init__.py similarity index 57% rename from openstackclient/tests/identity/v3/test_identity.py rename to openstackclient/tests/compute/v2/__init__.py index 4b55ee4540..c534c012e8 100644 --- a/openstackclient/tests/identity/v3/test_identity.py +++ b/openstackclient/tests/compute/v2/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2013 Nebula Inc. +# Copyright 2013 OpenStack Foundation # # 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 @@ -12,20 +12,3 @@ # License for the specific language governing permissions and limitations # under the License. # - -from openstackclient.tests.identity.v3 import fakes -from openstackclient.tests import utils - - -AUTH_TOKEN = "foobar" -AUTH_URL = "http://0.0.0.0" - - -class TestIdentityv3(utils.TestCommand): - def setUp(self): - super(TestIdentityv3, self).setUp() - - self.app.client_manager.identity = fakes.FakeIdentityv3Client( - endpoint=AUTH_URL, - token=AUTH_TOKEN, - ) diff --git a/openstackclient/tests/compute/v2/fakes.py b/openstackclient/tests/compute/v2/fakes.py new file mode 100644 index 0000000000..8154449fbf --- /dev/null +++ b/openstackclient/tests/compute/v2/fakes.py @@ -0,0 +1,55 @@ +# Copyright 2013 Nebula Inc. +# +# 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 mock + +from openstackclient.tests import fakes +from openstackclient.tests import utils + + +image_id = 'im1' + +IMAGE = { + 'id': image_id, +} + + +server_id = 'serv1' +server_name = 'waiter' + +SERVER = { + 'id': server_id, + 'name': server_name, +} + + +class FakeComputev2Client(object): + def __init__(self, **kwargs): + self.images = mock.Mock() + self.images.resource_class = fakes.FakeResource(None, {}) + self.servers = mock.Mock() + self.servers.resource_class = fakes.FakeResource(None, {}) + self.auth_token = kwargs['token'] + self.management_url = kwargs['endpoint'] + + +class TestComputev2(utils.TestCommand): + def setUp(self): + super(TestComputev2, self).setUp() + + self.app.client_manager.compute = FakeComputev2Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) diff --git a/openstackclient/tests/compute/v2/test_server.py b/openstackclient/tests/compute/v2/test_server.py new file mode 100644 index 0000000000..7e68808e1d --- /dev/null +++ b/openstackclient/tests/compute/v2/test_server.py @@ -0,0 +1,63 @@ +# Copyright 2013 Nebula Inc. +# +# 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 copy + +from openstackclient.compute.v2 import server +from openstackclient.tests.compute.v2 import fakes as compute_fakes +from openstackclient.tests import fakes + + +class TestServer(compute_fakes.TestComputev2): + + def setUp(self): + super(TestServer, self).setUp() + + # Get a shortcut to the ServerManager Mock + self.servers_mock = self.app.client_manager.compute.servers + self.servers_mock.reset_mock() + + +class TestServerDelete(TestServer): + + def setUp(self): + super(TestServerDelete, self).setUp() + + # This is the return value for utils.find_resource() + self.servers_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(compute_fakes.SERVER), + loaded=True, + ) + self.servers_mock.delete.return_value = None + + # Get the command object to test + self.cmd = server.DeleteServer(self.app, None) + + def test_server_delete_no_options(self): + arglist = [ + compute_fakes.server_id, + ] + verifylist = [ + ('server', compute_fakes.server_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + self.servers_mock.delete.assert_called_with( + compute_fakes.server_id, + ) diff --git a/openstackclient/tests/fakes.py b/openstackclient/tests/fakes.py index d6cf1d742f..8ab4546588 100644 --- a/openstackclient/tests/fakes.py +++ b/openstackclient/tests/fakes.py @@ -16,6 +16,10 @@ import sys +AUTH_TOKEN = "foobar" +AUTH_URL = "http://0.0.0.0" + + class FakeStdout: def __init__(self): self.content = [] diff --git a/openstackclient/tests/identity/v2_0/fakes.py b/openstackclient/tests/identity/v2_0/fakes.py index b1aeabd4ca..80febd29a4 100644 --- a/openstackclient/tests/identity/v2_0/fakes.py +++ b/openstackclient/tests/identity/v2_0/fakes.py @@ -16,6 +16,8 @@ import mock from openstackclient.tests import fakes +from openstackclient.tests import utils + project_id = '8-9-64' project_name = 'beatles' @@ -83,3 +85,13 @@ def __init__(self, **kwargs): self.ec2.resource_class = fakes.FakeResource(None, {}) self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] + + +class TestIdentityv2(utils.TestCommand): + def setUp(self): + super(TestIdentityv2, self).setUp() + + self.app.client_manager.identity = FakeIdentityv2Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) diff --git a/openstackclient/tests/identity/v2_0/test_project.py b/openstackclient/tests/identity/v2_0/test_project.py index 933bd0941d..30f4278bea 100644 --- a/openstackclient/tests/identity/v2_0/test_project.py +++ b/openstackclient/tests/identity/v2_0/test_project.py @@ -18,10 +18,9 @@ from openstackclient.identity.v2_0 import project from openstackclient.tests import fakes from openstackclient.tests.identity.v2_0 import fakes as identity_fakes -from openstackclient.tests.identity.v2_0 import test_identity -class TestProject(test_identity.TestIdentityv2): +class TestProject(identity_fakes.TestIdentityv2): def setUp(self): super(TestProject, self).setUp() diff --git a/openstackclient/tests/identity/v2_0/test_role.py b/openstackclient/tests/identity/v2_0/test_role.py index 56e9d4cb54..d515bd7cdb 100644 --- a/openstackclient/tests/identity/v2_0/test_role.py +++ b/openstackclient/tests/identity/v2_0/test_role.py @@ -20,10 +20,9 @@ from openstackclient.identity.v2_0 import role from openstackclient.tests import fakes from openstackclient.tests.identity.v2_0 import fakes as identity_fakes -from openstackclient.tests.identity.v2_0 import test_identity -class TestRole(test_identity.TestIdentityv2): +class TestRole(identity_fakes.TestIdentityv2): def setUp(self): super(TestRole, self).setUp() diff --git a/openstackclient/tests/identity/v2_0/test_service.py b/openstackclient/tests/identity/v2_0/test_service.py index f09c4137ad..6c93574bf5 100644 --- a/openstackclient/tests/identity/v2_0/test_service.py +++ b/openstackclient/tests/identity/v2_0/test_service.py @@ -18,10 +18,9 @@ from openstackclient.identity.v2_0 import service from openstackclient.tests import fakes from openstackclient.tests.identity.v2_0 import fakes as identity_fakes -from openstackclient.tests.identity.v2_0 import test_identity -class TestService(test_identity.TestIdentityv2): +class TestService(identity_fakes.TestIdentityv2): def setUp(self): super(TestService, self).setUp() diff --git a/openstackclient/tests/identity/v2_0/test_user.py b/openstackclient/tests/identity/v2_0/test_user.py index 2fe585ed96..a18d13d821 100644 --- a/openstackclient/tests/identity/v2_0/test_user.py +++ b/openstackclient/tests/identity/v2_0/test_user.py @@ -18,10 +18,9 @@ from openstackclient.identity.v2_0 import user from openstackclient.tests import fakes from openstackclient.tests.identity.v2_0 import fakes as identity_fakes -from openstackclient.tests.identity.v2_0 import test_identity -class TestUser(test_identity.TestIdentityv2): +class TestUser(identity_fakes.TestIdentityv2): def setUp(self): super(TestUser, self).setUp() diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index 1338553608..9d40d9dbec 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -16,6 +16,7 @@ import mock from openstackclient.tests import fakes +from openstackclient.tests import utils domain_id = 'd1' @@ -104,3 +105,13 @@ def __init__(self, **kwargs): self.users.resource_class = fakes.FakeResource(None, {}) self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] + + +class TestIdentityv3(utils.TestCommand): + def setUp(self): + super(TestIdentityv3, self).setUp() + + self.app.client_manager.identity = FakeIdentityv3Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) diff --git a/openstackclient/tests/identity/v3/test_project.py b/openstackclient/tests/identity/v3/test_project.py index 91c15e246d..02cb41bedb 100644 --- a/openstackclient/tests/identity/v3/test_project.py +++ b/openstackclient/tests/identity/v3/test_project.py @@ -18,10 +18,9 @@ from openstackclient.identity.v3 import project from openstackclient.tests import fakes from openstackclient.tests.identity.v3 import fakes as identity_fakes -from openstackclient.tests.identity.v3 import test_identity -class TestProject(test_identity.TestIdentityv3): +class TestProject(identity_fakes.TestIdentityv3): def setUp(self): super(TestProject, self).setUp() diff --git a/openstackclient/tests/identity/v3/test_role.py b/openstackclient/tests/identity/v3/test_role.py index ef2b3e057c..040c39dd1b 100644 --- a/openstackclient/tests/identity/v3/test_role.py +++ b/openstackclient/tests/identity/v3/test_role.py @@ -18,10 +18,9 @@ from openstackclient.identity.v3 import role from openstackclient.tests import fakes from openstackclient.tests.identity.v3 import fakes as identity_fakes -from openstackclient.tests.identity.v3 import test_identity -class TestRole(test_identity.TestIdentityv3): +class TestRole(identity_fakes.TestIdentityv3): def setUp(self): super(TestRole, self).setUp() diff --git a/openstackclient/tests/identity/v3/test_service.py b/openstackclient/tests/identity/v3/test_service.py index 1c3ae21e16..10d249c5db 100644 --- a/openstackclient/tests/identity/v3/test_service.py +++ b/openstackclient/tests/identity/v3/test_service.py @@ -18,10 +18,9 @@ from openstackclient.identity.v3 import service from openstackclient.tests import fakes from openstackclient.tests.identity.v3 import fakes as identity_fakes -from openstackclient.tests.identity.v3 import test_identity -class TestService(test_identity.TestIdentityv3): +class TestService(identity_fakes.TestIdentityv3): def setUp(self): super(TestService, self).setUp() diff --git a/openstackclient/tests/identity/v3/test_user.py b/openstackclient/tests/identity/v3/test_user.py index 8f195805c2..4321b04799 100644 --- a/openstackclient/tests/identity/v3/test_user.py +++ b/openstackclient/tests/identity/v3/test_user.py @@ -18,10 +18,9 @@ from openstackclient.identity.v3 import user from openstackclient.tests import fakes from openstackclient.tests.identity.v3 import fakes as identity_fakes -from openstackclient.tests.identity.v3 import test_identity -class TestUser(test_identity.TestIdentityv3): +class TestUser(identity_fakes.TestIdentityv3): def setUp(self): super(TestUser, self).setUp() diff --git a/openstackclient/tests/image/test_image.py b/openstackclient/tests/image/test_image.py deleted file mode 100644 index f4c8d72e37..0000000000 --- a/openstackclient/tests/image/test_image.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2013 OpenStack, LLC. -# -# 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 mock - -from openstackclient.common import clientmanager -from openstackclient.image import client as image_client -from openstackclient.tests import utils - - -AUTH_TOKEN = "foobar" -AUTH_URL = "http://0.0.0.0" - - -class FakeClient(object): - def __init__(self, endpoint=None, **kwargs): - self.client = mock.MagicMock() - self.client.auth_token = AUTH_TOKEN - self.client.auth_url = AUTH_URL - - -class TestImage(utils.TestCase): - def setUp(self): - super(TestImage, self).setUp() - - api_version = {"image": "2"} - - image_client.API_VERSIONS = { - "2": "openstackclient.tests.image.test_image.FakeClient" - } - - self.cm = clientmanager.ClientManager(token=AUTH_TOKEN, - url=AUTH_URL, - auth_url=AUTH_URL, - api_version=api_version) - - def test_make_client(self): - self.assertEqual(self.cm.image.client.auth_token, AUTH_TOKEN) - self.assertEqual(self.cm.image.client.auth_url, AUTH_URL) diff --git a/openstackclient/tests/identity/v2_0/test_identity.py b/openstackclient/tests/image/v1/__init__.py similarity index 56% rename from openstackclient/tests/identity/v2_0/test_identity.py rename to openstackclient/tests/image/v1/__init__.py index 8a50a48a06..ebf59b327e 100644 --- a/openstackclient/tests/identity/v2_0/test_identity.py +++ b/openstackclient/tests/image/v1/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2013 Nebula Inc. +# Copyright 2013 OpenStack, LLC. # # 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 @@ -12,20 +12,3 @@ # License for the specific language governing permissions and limitations # under the License. # - -from openstackclient.tests.identity.v2_0 import fakes -from openstackclient.tests import utils - - -AUTH_TOKEN = "foobar" -AUTH_URL = "http://0.0.0.0" - - -class TestIdentityv2(utils.TestCommand): - def setUp(self): - super(TestIdentityv2, self).setUp() - - self.app.client_manager.identity = fakes.FakeIdentityv2Client( - endpoint=AUTH_URL, - token=AUTH_TOKEN, - ) diff --git a/openstackclient/tests/image/v1/fakes.py b/openstackclient/tests/image/v1/fakes.py new file mode 100644 index 0000000000..ea2af84ccd --- /dev/null +++ b/openstackclient/tests/image/v1/fakes.py @@ -0,0 +1,46 @@ +# Copyright 2013 Nebula Inc. +# +# 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 mock + +from openstackclient.tests import fakes +from openstackclient.tests import utils + + +image_id = 'im1' +image_name = 'graven' + +IMAGE = { + 'id': image_id, + 'name': image_name +} + + +class FakeImagev1Client(object): + def __init__(self, **kwargs): + self.images = mock.Mock() + self.images.resource_class = fakes.FakeResource(None, {}) + self.auth_token = kwargs['token'] + self.management_url = kwargs['endpoint'] + + +class TestImagev1(utils.TestCommand): + def setUp(self): + super(TestImagev1, self).setUp() + + self.app.client_manager.image = FakeImagev1Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) diff --git a/openstackclient/tests/image/v1/test_image.py b/openstackclient/tests/image/v1/test_image.py new file mode 100644 index 0000000000..a410674d71 --- /dev/null +++ b/openstackclient/tests/image/v1/test_image.py @@ -0,0 +1,63 @@ +# Copyright 2013 Nebula Inc. +# +# 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 copy + +from openstackclient.image.v1 import image +from openstackclient.tests import fakes +from openstackclient.tests.image.v1 import fakes as image_fakes + + +class TestImage(image_fakes.TestImagev1): + + def setUp(self): + super(TestImage, self).setUp() + + # Get a shortcut to the ServerManager Mock + self.images_mock = self.app.client_manager.image.images + self.images_mock.reset_mock() + + +class TestImageDelete(TestImage): + + def setUp(self): + super(TestImageDelete, self).setUp() + + # This is the return value for utils.find_resource() + self.images_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(image_fakes.IMAGE), + loaded=True, + ) + self.images_mock.delete.return_value = None + + # Get the command object to test + self.cmd = image.DeleteImage(self.app, None) + + def test_image_delete_no_options(self): + arglist = [ + image_fakes.image_id, + ] + verifylist = [ + ('image', image_fakes.image_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + self.images_mock.delete.assert_called_with( + image_fakes.image_id, + ) diff --git a/openstackclient/tests/image/v2/__init__.py b/openstackclient/tests/image/v2/__init__.py new file mode 100644 index 0000000000..ebf59b327e --- /dev/null +++ b/openstackclient/tests/image/v2/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2013 OpenStack, LLC. +# +# 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. +# diff --git a/openstackclient/tests/image/v2/fakes.py b/openstackclient/tests/image/v2/fakes.py new file mode 100644 index 0000000000..df6b80726a --- /dev/null +++ b/openstackclient/tests/image/v2/fakes.py @@ -0,0 +1,46 @@ +# Copyright 2013 Nebula Inc. +# +# 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 mock + +from openstackclient.tests import fakes +from openstackclient.tests import utils + + +image_id = 'im1' +image_name = 'graven' + +IMAGE = { + 'id': image_id, + 'name': image_name +} + + +class FakeImagev2Client(object): + def __init__(self, **kwargs): + self.images = mock.Mock() + self.images.resource_class = fakes.FakeResource(None, {}) + self.auth_token = kwargs['token'] + self.management_url = kwargs['endpoint'] + + +class TestImagev2(utils.TestCommand): + def setUp(self): + super(TestImagev2, self).setUp() + + self.app.client_manager.image = FakeImagev2Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) diff --git a/openstackclient/tests/image/v2/test_image.py b/openstackclient/tests/image/v2/test_image.py new file mode 100644 index 0000000000..ef84e2c04e --- /dev/null +++ b/openstackclient/tests/image/v2/test_image.py @@ -0,0 +1,63 @@ +# Copyright 2013 Nebula Inc. +# +# 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 copy + +from openstackclient.image.v1 import image +from openstackclient.tests import fakes +from openstackclient.tests.image.v2 import fakes as image_fakes + + +class TestImage(image_fakes.TestImagev2): + + def setUp(self): + super(TestImage, self).setUp() + + # Get a shortcut to the ServerManager Mock + self.images_mock = self.app.client_manager.image.images + self.images_mock.reset_mock() + + +class TestImageDelete(TestImage): + + def setUp(self): + super(TestImageDelete, self).setUp() + + # This is the return value for utils.find_resource() + self.images_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(image_fakes.IMAGE), + loaded=True, + ) + self.images_mock.delete.return_value = None + + # Get the command object to test + self.cmd = image.DeleteImage(self.app, None) + + def test_image_delete_no_options(self): + arglist = [ + image_fakes.image_id, + ] + verifylist = [ + ('image', image_fakes.image_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + self.images_mock.delete.assert_called_with( + image_fakes.image_id, + ) diff --git a/openstackclient/tests/object/fakes.py b/openstackclient/tests/object/v1/fakes.py similarity index 100% rename from openstackclient/tests/object/fakes.py rename to openstackclient/tests/object/v1/fakes.py diff --git a/openstackclient/tests/object/test_container.py b/openstackclient/tests/object/v1/test_container.py similarity index 99% rename from openstackclient/tests/object/test_container.py rename to openstackclient/tests/object/v1/test_container.py index 24d6763376..2090b88029 100644 --- a/openstackclient/tests/object/test_container.py +++ b/openstackclient/tests/object/v1/test_container.py @@ -18,7 +18,7 @@ from openstackclient.common import clientmanager from openstackclient.object.v1 import container -from openstackclient.tests.object import fakes as object_fakes +from openstackclient.tests.object.v1 import fakes as object_fakes from openstackclient.tests import utils diff --git a/openstackclient/tests/object/test_object.py b/openstackclient/tests/object/v1/test_object.py similarity index 99% rename from openstackclient/tests/object/test_object.py rename to openstackclient/tests/object/v1/test_object.py index 1ceb0a59dd..52b5ebd086 100644 --- a/openstackclient/tests/object/test_object.py +++ b/openstackclient/tests/object/v1/test_object.py @@ -18,7 +18,7 @@ from openstackclient.common import clientmanager from openstackclient.object.v1 import object as obj -from openstackclient.tests.object import fakes as object_fakes +from openstackclient.tests.object.v1 import fakes as object_fakes from openstackclient.tests import utils diff --git a/openstackclient/tests/volume/v1/fakes.py b/openstackclient/tests/volume/v1/fakes.py index a382dbb8b7..b25dfaf70a 100644 --- a/openstackclient/tests/volume/v1/fakes.py +++ b/openstackclient/tests/volume/v1/fakes.py @@ -16,6 +16,9 @@ import mock from openstackclient.tests import fakes +from openstackclient.tests.identity.v2_0 import fakes as identity_fakes +from openstackclient.tests import utils + volume_id = 'vvvvvvvv-vvvv-vvvv-vvvvvvvv' volume_name = 'nigel' @@ -42,3 +45,18 @@ def __init__(self, **kwargs): self.services.resource_class = fakes.FakeResource(None, {}) self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] + + +class TestVolumev1(utils.TestCommand): + def setUp(self): + super(TestVolumev1, self).setUp() + + self.app.client_manager.volume = FakeVolumev1Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) + + self.app.client_manager.identity = identity_fakes.FakeIdentityv2Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) diff --git a/openstackclient/tests/volume/v1/test_volume.py b/openstackclient/tests/volume/v1/test_volume.py index 58024f0bfd..4e033dfeea 100644 --- a/openstackclient/tests/volume/v1/test_volume.py +++ b/openstackclient/tests/volume/v1/test_volume.py @@ -1,4 +1,4 @@ -# Copyright 2013 OpenStack, LLC. +# Copyright 2013 Nebula Inc. # # 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 @@ -13,25 +13,256 @@ # under the License. # +import copy + +from openstackclient.tests import fakes from openstackclient.tests.identity.v2_0 import fakes as identity_fakes -from openstackclient.tests import utils -from openstackclient.tests.volume.v1 import fakes +from openstackclient.tests.volume.v1 import fakes as volume_fakes +from openstackclient.volume.v1 import volume + + +class TestVolume(volume_fakes.TestVolumev1): + + def setUp(self): + super(TestVolume, self).setUp() + # Get a shortcut to the VolumeManager Mock + self.volumes_mock = self.app.client_manager.volume.volumes + self.volumes_mock.reset_mock() -AUTH_TOKEN = "foobar" -AUTH_URL = "http://0.0.0.0" + # Get a shortcut to the TenantManager Mock + self.projects_mock = self.app.client_manager.identity.tenants + self.projects_mock.reset_mock() + # Get a shortcut to the UserManager Mock + self.users_mock = self.app.client_manager.identity.users + self.users_mock.reset_mock() + + +# TODO(dtroyer): The volume create tests are incomplete, only the minimal +# options and the options that require additional processing +# are implemented at this time. + +class TestVolumeCreate(TestVolume): -class TestVolumev1(utils.TestCommand): def setUp(self): - super(TestVolumev1, self).setUp() + super(TestVolumeCreate, self).setUp() + + self.volumes_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.VOLUME), + loaded=True, + ) + + # Get the command object to test + self.cmd = volume.CreateVolume(self.app, None) + + def test_volume_create_min_options(self): + arglist = [ + '--size', str(volume_fakes.volume_size), + volume_fakes.volume_name, + ] + verifylist = [ + ('size', volume_fakes.volume_size), + ('name', volume_fakes.volume_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + #kwargs = { + # 'metadata': volume_fakes.volume_metadata, + #} + # VolumeManager.create(size, snapshot_id=, source_volid=, + # display_name=, display_description=, + # volume_type=, user_id=, + # project_id=, availability_zone=, + # metadata=, imageRef=) + self.volumes_mock.create.assert_called_with( + volume_fakes.volume_size, + None, + None, + volume_fakes.volume_name, + None, + None, + None, + None, + None, + None, + None, + ) + + collist = ( + 'attach_status', + 'display_description', + 'display_name', + 'id', + 'properties', + 'size', + 'status', + ) + self.assertEqual(columns, collist) + datalist = ( + 'detatched', + volume_fakes.volume_description, + volume_fakes.volume_name, + volume_fakes.volume_id, + '', + volume_fakes.volume_size, + '', + ) + self.assertEqual(data, datalist) + + def test_volume_create_user_project_id(self): + # Return a project + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + # Return a user + self.users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + + arglist = [ + '--size', str(volume_fakes.volume_size), + '--project', identity_fakes.project_id, + '--user', identity_fakes.user_id, + volume_fakes.volume_name, + ] + verifylist = [ + ('size', volume_fakes.volume_size), + ('project', identity_fakes.project_id), + ('user', identity_fakes.user_id), + ('name', volume_fakes.volume_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.app.client_manager.volume = fakes.FakeVolumev1Client( - endpoint=AUTH_URL, - token=AUTH_TOKEN, + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + #kwargs = { + # 'metadata': volume_fakes.volume_metadata, + #} + # VolumeManager.create(size, snapshot_id=, source_volid=, + # display_name=, display_description=, + # volume_type=, user_id=, + # project_id=, availability_zone=, + # metadata=, imageRef=) + self.volumes_mock.create.assert_called_with( + volume_fakes.volume_size, + None, + None, + volume_fakes.volume_name, + #volume_fakes.volume_description, + None, + None, + identity_fakes.user_id, + identity_fakes.project_id, + None, + None, + None, + ) + + collist = ( + 'attach_status', + 'display_description', + 'display_name', + 'id', + 'properties', + 'size', + 'status', + ) + self.assertEqual(columns, collist) + datalist = ( + 'detatched', + volume_fakes.volume_description, + volume_fakes.volume_name, + volume_fakes.volume_id, + '', + volume_fakes.volume_size, + '', ) + self.assertEqual(data, datalist) - self.app.client_manager.identity = identity_fakes.FakeIdentityv2Client( - endpoint=AUTH_URL, - token=AUTH_TOKEN, + def test_volume_create_user_project_name(self): + # Return a project + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + # Return a user + self.users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + + arglist = [ + '--size', str(volume_fakes.volume_size), + '--project', identity_fakes.project_name, + '--user', identity_fakes.user_name, + volume_fakes.volume_name, + ] + verifylist = [ + ('size', volume_fakes.volume_size), + ('project', identity_fakes.project_name), + ('user', identity_fakes.user_name), + ('name', volume_fakes.volume_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + #kwargs = { + # 'metadata': volume_fakes.volume_metadata, + #} + # VolumeManager.create(size, snapshot_id=, source_volid=, + # display_name=, display_description=, + # volume_type=, user_id=, + # project_id=, availability_zone=, + # metadata=, imageRef=) + self.volumes_mock.create.assert_called_with( + volume_fakes.volume_size, + None, + None, + volume_fakes.volume_name, + #volume_fakes.volume_description, + None, + None, + identity_fakes.user_id, + identity_fakes.project_id, + None, + None, + None, + ) + + collist = ( + 'attach_status', + 'display_description', + 'display_name', + 'id', + 'properties', + 'size', + 'status', + ) + self.assertEqual(columns, collist) + datalist = ( + 'detatched', + volume_fakes.volume_description, + volume_fakes.volume_name, + volume_fakes.volume_id, + '', + volume_fakes.volume_size, + '', ) + self.assertEqual(data, datalist) diff --git a/openstackclient/tests/volume/v1/test_volumecmd.py b/openstackclient/tests/volume/v1/test_volumecmd.py deleted file mode 100644 index 1f5ed882aa..0000000000 --- a/openstackclient/tests/volume/v1/test_volumecmd.py +++ /dev/null @@ -1,269 +0,0 @@ -# Copyright 2013 Nebula Inc. -# -# 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 copy - -from openstackclient.tests import fakes -from openstackclient.tests.identity.v2_0 import fakes as identity_fakes -from openstackclient.tests.volume.v1 import fakes as volume_fakes -from openstackclient.tests.volume.v1 import test_volume -from openstackclient.volume.v1 import volume - - -class TestVolume(test_volume.TestVolumev1): - - def setUp(self): - super(TestVolume, self).setUp() - - # Get a shortcut to the VolumeManager Mock - self.volumes_mock = self.app.client_manager.volume.volumes - self.volumes_mock.reset_mock() - - # Get a shortcut to the TenantManager Mock - self.projects_mock = self.app.client_manager.identity.tenants - self.projects_mock.reset_mock() - - # Get a shortcut to the UserManager Mock - self.users_mock = self.app.client_manager.identity.users - self.users_mock.reset_mock() - - -# TODO(dtroyer): The volume create tests are incomplete, only the minimal -# options and the options that require additional processing -# are implemented at this time. - -class TestVolumeCreate(TestVolume): - - def setUp(self): - super(TestVolumeCreate, self).setUp() - - self.volumes_mock.create.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.VOLUME), - loaded=True, - ) - - # Get the command object to test - self.cmd = volume.CreateVolume(self.app, None) - - def test_volume_create_min_options(self): - arglist = [ - '--size', str(volume_fakes.volume_size), - volume_fakes.volume_name, - ] - verifylist = [ - ('size', volume_fakes.volume_size), - ('name', volume_fakes.volume_name), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # DisplayCommandBase.take_action() returns two tuples - columns, data = self.cmd.take_action(parsed_args) - - # Set expected values - #kwargs = { - # 'metadata': volume_fakes.volume_metadata, - #} - # VolumeManager.create(size, snapshot_id=, source_volid=, - # display_name=, display_description=, - # volume_type=, user_id=, - # project_id=, availability_zone=, - # metadata=, imageRef=) - self.volumes_mock.create.assert_called_with( - volume_fakes.volume_size, - None, - None, - volume_fakes.volume_name, - None, - None, - None, - None, - None, - None, - None, - ) - - collist = ( - 'attach_status', - 'display_description', - 'display_name', - 'id', - 'properties', - 'size', - 'status', - ) - self.assertEqual(columns, collist) - datalist = ( - 'detatched', - volume_fakes.volume_description, - volume_fakes.volume_name, - volume_fakes.volume_id, - '', - volume_fakes.volume_size, - '', - ) - self.assertEqual(data, datalist) - - def test_volume_create_user_project_id(self): - # Return a project - self.projects_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.PROJECT), - loaded=True, - ) - # Return a user - self.users_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.USER), - loaded=True, - ) - - arglist = [ - '--size', str(volume_fakes.volume_size), - '--project', identity_fakes.project_id, - '--user', identity_fakes.user_id, - volume_fakes.volume_name, - ] - verifylist = [ - ('size', volume_fakes.volume_size), - ('project', identity_fakes.project_id), - ('user', identity_fakes.user_id), - ('name', volume_fakes.volume_name), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # DisplayCommandBase.take_action() returns two tuples - columns, data = self.cmd.take_action(parsed_args) - - # Set expected values - #kwargs = { - # 'metadata': volume_fakes.volume_metadata, - #} - # VolumeManager.create(size, snapshot_id=, source_volid=, - # display_name=, display_description=, - # volume_type=, user_id=, - # project_id=, availability_zone=, - # metadata=, imageRef=) - self.volumes_mock.create.assert_called_with( - volume_fakes.volume_size, - None, - None, - volume_fakes.volume_name, - #volume_fakes.volume_description, - None, - None, - identity_fakes.user_id, - identity_fakes.project_id, - None, - None, - None, - ) - - collist = ( - 'attach_status', - 'display_description', - 'display_name', - 'id', - 'properties', - 'size', - 'status', - ) - self.assertEqual(columns, collist) - datalist = ( - 'detatched', - volume_fakes.volume_description, - volume_fakes.volume_name, - volume_fakes.volume_id, - '', - volume_fakes.volume_size, - '', - ) - self.assertEqual(data, datalist) - - def test_volume_create_user_project_name(self): - # Return a project - self.projects_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.PROJECT), - loaded=True, - ) - # Return a user - self.users_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.USER), - loaded=True, - ) - - arglist = [ - '--size', str(volume_fakes.volume_size), - '--project', identity_fakes.project_name, - '--user', identity_fakes.user_name, - volume_fakes.volume_name, - ] - verifylist = [ - ('size', volume_fakes.volume_size), - ('project', identity_fakes.project_name), - ('user', identity_fakes.user_name), - ('name', volume_fakes.volume_name), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # DisplayCommandBase.take_action() returns two tuples - columns, data = self.cmd.take_action(parsed_args) - - # Set expected values - #kwargs = { - # 'metadata': volume_fakes.volume_metadata, - #} - # VolumeManager.create(size, snapshot_id=, source_volid=, - # display_name=, display_description=, - # volume_type=, user_id=, - # project_id=, availability_zone=, - # metadata=, imageRef=) - self.volumes_mock.create.assert_called_with( - volume_fakes.volume_size, - None, - None, - volume_fakes.volume_name, - #volume_fakes.volume_description, - None, - None, - identity_fakes.user_id, - identity_fakes.project_id, - None, - None, - None, - ) - - collist = ( - 'attach_status', - 'display_description', - 'display_name', - 'id', - 'properties', - 'size', - 'status', - ) - self.assertEqual(columns, collist) - datalist = ( - 'detatched', - volume_fakes.volume_description, - volume_fakes.volume_name, - volume_fakes.volume_id, - '', - volume_fakes.volume_size, - '', - ) - self.assertEqual(data, datalist) From 200ed62054847336235288c7785424be416bed06 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 18 Nov 2013 17:10:39 -0600 Subject: [PATCH 0028/3494] Add server image create command Translation of 'nova image-create', with tests! Change-Id: I8a833aeff6f291e4774063ed235876eb2ba9c13c --- openstackclient/compute/v2/server.py | 67 ++++++++++++++ openstackclient/tests/compute/v2/fakes.py | 13 ++- .../tests/compute/v2/test_server.py | 91 +++++++++++++++++++ openstackclient/tests/image/v2/fakes.py | 5 +- setup.cfg | 1 + 5 files changed, 169 insertions(+), 8 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 1de9f1bade..87f5f6896d 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -384,6 +384,73 @@ def take_action(self, parsed_args): return zip(*sorted(six.iteritems(details))) +class CreateServerImage(show.ShowOne): + """Create a new disk image from a running server""" + + log = logging.getLogger(__name__ + '.CreateServerImage') + + def get_parser(self, prog_name): + parser = super(CreateServerImage, self).get_parser(prog_name) + parser.add_argument( + 'server', + metavar=' Date: Tue, 19 Nov 2013 10:10:54 +0100 Subject: [PATCH 0029/3494] Support building wheels (PEP-427) With that, building and uploading wheels to PyPI is only one "python setup.py bdist_wheel" away. Change-Id: I8c8565f55e7a3434e1a1972a797a6cd7dba8a581 --- setup.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.cfg b/setup.cfg index 2ac659f442..35a4d207e3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -278,3 +278,6 @@ all_files = 1 [upload_sphinx] upload-dir = doc/build/html + +[wheel] +universal = 1 From 07bbfd5770aa28e272bdba5ac1ed4269905401af Mon Sep 17 00:00:00 2001 From: Noorul Islam K M Date: Tue, 19 Nov 2013 19:57:02 +0530 Subject: [PATCH 0030/3494] Fix typo Change-Id: I7bca8b76c6746121314e688e9ed3825e04350b8d --- openstackclient/common/commandmanager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstackclient/common/commandmanager.py b/openstackclient/common/commandmanager.py index e366034aab..06073d93cf 100644 --- a/openstackclient/common/commandmanager.py +++ b/openstackclient/common/commandmanager.py @@ -25,7 +25,7 @@ class CommandManager(cliff.commandmanager.CommandManager): - """Alters Cliff's default CommandManager behaviour to load additiona + """Alters Cliff's default CommandManager behaviour to load additional command groups after initialization. """ def _load_commands(self, group=None): From 9062811d10f2ab660ce38f9bd20be9c52daa9479 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 20 Nov 2013 18:02:09 -0600 Subject: [PATCH 0031/3494] Expand support for command extensions Allows client libraries to have complete access to the rest of the OSC ClientManager. In addition, extension libraries can define global options (for API version options/env vars) and define versioned API entry points similar to the in-repo commands. The changes to ClientManager exposed some issues in the existing object api tests that needed to be cleaned up. Change-Id: Ic9662edf34c5dd130a2f1a69d2454adefc1f8a95 --- openstackclient/common/clientmanager.py | 33 ++++++-- openstackclient/compute/client.py | 16 ++++ openstackclient/identity/client.py | 2 + openstackclient/image/client.py | 16 ++++ openstackclient/object/client.py | 18 +++- openstackclient/shell.py | 83 +++++++------------ openstackclient/tests/fakes.py | 1 + openstackclient/tests/object/v1/fakes.py | 20 +++++ .../tests/object/v1/lib/test_container.py | 21 ++--- .../tests/object/v1/lib/test_object.py | 27 +++--- .../tests/object/v1/test_container.py | 12 +-- .../tests/object/v1/test_object.py | 18 +--- openstackclient/volume/client.py | 16 ++++ setup.cfg | 8 +- 14 files changed, 172 insertions(+), 119 deletions(-) diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index 85f544e4dc..a0224064da 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -16,12 +16,10 @@ """Manage access to the clients, including authenticating when needed.""" import logging +import pkg_resources +import sys -from openstackclient.compute import client as compute_client from openstackclient.identity import client as identity_client -from openstackclient.image import client as image_client -from openstackclient.object import client as object_client -from openstackclient.volume import client as volume_client LOG = logging.getLogger(__name__) @@ -42,11 +40,7 @@ def __get__(self, instance, owner): class ClientManager(object): """Manages access to API clients, including authentication.""" - compute = ClientCache(compute_client.make_client) identity = ClientCache(identity_client.make_client) - image = ClientCache(image_client.make_client) - object = ClientCache(object_client.make_client) - volume = ClientCache(volume_client.make_client) def __init__(self, token=None, url=None, auth_url=None, project_name=None, project_id=None, username=None, password=None, @@ -93,3 +87,26 @@ def get_endpoint_for_service_type(self, service_type): # Hope we were given the correct URL. endpoint = self._url return endpoint + + +def get_extension_modules(group): + """Add extension clients""" + mod_list = [] + for ep in pkg_resources.iter_entry_points(group): + LOG.debug('found extension %r' % ep.name) + + __import__(ep.module_name) + module = sys.modules[ep.module_name] + mod_list.append(module) + init_func = getattr(module, 'Initialize', None) + if init_func: + init_func('x') + + setattr( + ClientManager, + ep.name, + ClientCache( + getattr(sys.modules[ep.module_name], 'make_client', None) + ), + ) + return mod_list diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py index 4d3b1b7155..4ccb2f6d40 100644 --- a/openstackclient/compute/client.py +++ b/openstackclient/compute/client.py @@ -19,6 +19,8 @@ LOG = logging.getLogger(__name__) +DEFAULT_COMPUTE_API_VERSION = '2' +API_VERSION_OPTION = 'os_compute_api_version' API_NAME = 'compute' API_VERSIONS = { '1.1': 'novaclient.v1_1.client.Client', @@ -60,3 +62,17 @@ def make_client(instance): client.client.service_catalog = instance._service_catalog client.client.auth_token = instance._token return client + + +def build_option_parser(parser): + """Hook to add global options""" + parser.add_argument( + '--os-compute-api-version', + metavar='', + default=utils.env( + 'OS_COMPUTE_API_VERSION', + default=DEFAULT_COMPUTE_API_VERSION), + help='Compute API version, default=' + + DEFAULT_COMPUTE_API_VERSION + + ' (Env: OS_COMPUTE_API_VERSION)') + return parser diff --git a/openstackclient/identity/client.py b/openstackclient/identity/client.py index 4814bc3e2e..305d4cc450 100644 --- a/openstackclient/identity/client.py +++ b/openstackclient/identity/client.py @@ -21,6 +21,8 @@ LOG = logging.getLogger(__name__) +DEFAULT_IDENTITY_API_VERSION = '2.0' +API_VERSION_OPTION = 'os_identity_api_version' API_NAME = 'identity' API_VERSIONS = { '2.0': 'openstackclient.identity.client.IdentityClientv2_0', diff --git a/openstackclient/image/client.py b/openstackclient/image/client.py index d56ca3b2ff..9edffded90 100644 --- a/openstackclient/image/client.py +++ b/openstackclient/image/client.py @@ -23,6 +23,8 @@ LOG = logging.getLogger(__name__) +DEFAULT_IMAGE_API_VERSION = '1' +API_VERSION_OPTION = 'os_image_api_version' API_NAME = "image" API_VERSIONS = { "1": "openstackclient.image.client.Client_v1", @@ -48,6 +50,20 @@ def make_client(instance): ) +def build_option_parser(parser): + """Hook to add global options""" + parser.add_argument( + '--os-image-api-version', + metavar='', + default=utils.env( + 'OS_IMAGE_API_VERSION', + default=DEFAULT_IMAGE_API_VERSION), + help='Image API version, default=' + + DEFAULT_IMAGE_API_VERSION + + ' (Env: OS_IMAGE_API_VERSION)') + return parser + + # NOTE(dtroyer): glanceclient.v1.image.ImageManager() doesn't have a find() # method so add one here until the common client libs arrive # A similar subclass will be required for v2 diff --git a/openstackclient/object/client.py b/openstackclient/object/client.py index a83a5c0a3d..1a5363b17c 100644 --- a/openstackclient/object/client.py +++ b/openstackclient/object/client.py @@ -21,7 +21,9 @@ LOG = logging.getLogger(__name__) -API_NAME = 'object-store' +DEFAULT_OBJECT_API_VERSION = '1' +API_VERSION_OPTION = 'os_object_api_version' +API_NAME = 'object' API_VERSIONS = { '1': 'openstackclient.object.client.ObjectClientv1', } @@ -45,6 +47,20 @@ def make_client(instance): return client +def build_option_parser(parser): + """Hook to add global options""" + parser.add_argument( + '--os-object-api-version', + metavar='', + default=utils.env( + 'OS_OBJECT_API_VERSION', + default=DEFAULT_OBJECT_API_VERSION), + help='Object API version, default=' + + DEFAULT_OBJECT_API_VERSION + + ' (Env: OS_OBJECT_API_VERSION)') + return parser + + class ObjectClientv1(object): def __init__( diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 78e16cd626..f8a47ca664 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -33,15 +33,11 @@ from openstackclient.common import openstackkeyring from openstackclient.common import restapi from openstackclient.common import utils +from openstackclient.identity import client as identity_client KEYRING_SERVICE = 'openstack' -DEFAULT_COMPUTE_API_VERSION = '2' -DEFAULT_IDENTITY_API_VERSION = '2.0' -DEFAULT_IMAGE_API_VERSION = '1' -DEFAULT_OBJECT_API_VERSION = '1' -DEFAULT_VOLUME_API_VERSION = '1' DEFAULT_DOMAIN = 'default' @@ -86,6 +82,15 @@ def __init__(self): # Assume TLS host certificate verification is enabled self.verify = True + # Get list of extension modules + self.ext_modules = clientmanager.get_extension_modules( + 'openstack.cli.extension', + ) + + # Loop through extensions to get parser additions + for mod in self.ext_modules: + self.parser = mod.build_option_parser(self.parser) + # NOTE(dtroyer): This hack changes the help action that Cliff # automatically adds to the parser so we can defer # its execution until after the api-versioned commands @@ -202,51 +207,6 @@ def build_option_parser(self, description, version): help='Default domain ID, default=' + DEFAULT_DOMAIN + ' (Env: OS_DEFAULT_DOMAIN)') - parser.add_argument( - '--os-identity-api-version', - metavar='', - default=env( - 'OS_IDENTITY_API_VERSION', - default=DEFAULT_IDENTITY_API_VERSION), - help='Identity API version, default=' + - DEFAULT_IDENTITY_API_VERSION + - ' (Env: OS_IDENTITY_API_VERSION)') - parser.add_argument( - '--os-compute-api-version', - metavar='', - default=env( - 'OS_COMPUTE_API_VERSION', - default=DEFAULT_COMPUTE_API_VERSION), - help='Compute API version, default=' + - DEFAULT_COMPUTE_API_VERSION + - ' (Env: OS_COMPUTE_API_VERSION)') - parser.add_argument( - '--os-image-api-version', - metavar='', - default=env( - 'OS_IMAGE_API_VERSION', - default=DEFAULT_IMAGE_API_VERSION), - help='Image API version, default=' + - DEFAULT_IMAGE_API_VERSION + - ' (Env: OS_IMAGE_API_VERSION)') - parser.add_argument( - '--os-object-api-version', - metavar='', - default=env( - 'OS_OBJECT_API_VERSION', - default=DEFAULT_OBJECT_API_VERSION), - help='Object API version, default=' + - DEFAULT_OBJECT_API_VERSION + - ' (Env: OS_OBJECT_API_VERSION)') - parser.add_argument( - '--os-volume-api-version', - metavar='', - default=env( - 'OS_VOLUME_API_VERSION', - default=DEFAULT_VOLUME_API_VERSION), - help='Volume API version, default=' + - DEFAULT_VOLUME_API_VERSION + - ' (Env: OS_VOLUME_API_VERSION)') parser.add_argument( '--os-token', metavar='', @@ -270,6 +230,16 @@ def build_option_parser(self, description, version): help='Use keyring to store password, ' 'default=False (Env: OS_USE_KEYRING)') + parser.add_argument( + '--os-identity-api-version', + metavar='', + default=env( + 'OS_IDENTITY_API_VERSION', + default=identity_client.DEFAULT_IDENTITY_API_VERSION), + help='Identity API version, default=' + + identity_client.DEFAULT_IDENTITY_API_VERSION + + ' (Env: OS_IDENTITY_API_VERSION)') + return parser def authenticate_user(self): @@ -391,17 +361,20 @@ def initialize_app(self, argv): # Stash selected API versions for later self.api_version = { - 'compute': self.options.os_compute_api_version, 'identity': self.options.os_identity_api_version, - 'image': self.options.os_image_api_version, - 'object-store': self.options.os_object_api_version, - 'volume': self.options.os_volume_api_version, } + # Loop through extensions to get API versions + for mod in self.ext_modules: + ver = getattr(self.options, mod.API_VERSION_OPTION, None) + if ver: + self.api_version[mod.API_NAME] = ver + self.log.debug('%s API version %s' % (mod.API_NAME, ver)) # Add the API version-specific commands for api in self.api_version.keys(): version = '.v' + self.api_version[api].replace('.', '_') cmd_group = 'openstack.' + api.replace('-', '_') + version + self.log.debug('command group %s' % cmd_group) self.command_manager.add_command_group(cmd_group) # Commands that span multiple APIs @@ -420,6 +393,8 @@ def initialize_app(self, argv): # } self.command_manager.add_command_group( 'openstack.extension') + # call InitializeXxx() here + # set up additional clients to stuff in to client_manager?? # Handle deferred help and exit if self.options.deferred_help: diff --git a/openstackclient/tests/fakes.py b/openstackclient/tests/fakes.py index 8ab4546588..bb89f7628f 100644 --- a/openstackclient/tests/fakes.py +++ b/openstackclient/tests/fakes.py @@ -49,6 +49,7 @@ def __init__(self): self.compute = None self.identity = None self.image = None + self.object = None self.volume = None self.auth_ref = None diff --git a/openstackclient/tests/object/v1/fakes.py b/openstackclient/tests/object/v1/fakes.py index fbc784aae5..37c35d3ba9 100644 --- a/openstackclient/tests/object/v1/fakes.py +++ b/openstackclient/tests/object/v1/fakes.py @@ -13,6 +13,10 @@ # under the License. # +from openstackclient.tests import fakes +from openstackclient.tests import utils + + container_name = 'bit-bucket' container_bytes = 1024 container_count = 1 @@ -65,3 +69,19 @@ 'content_type': object_content_type_2, 'last_modified': object_modified_2, } + + +class FakeObjectv1Client(object): + def __init__(self, **kwargs): + self.endpoint = kwargs['endpoint'] + self.token = kwargs['token'] + + +class TestObjectv1(utils.TestCommand): + def setUp(self): + super(TestObjectv1, self).setUp() + + self.app.client_manager.object = FakeObjectv1Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) diff --git a/openstackclient/tests/object/v1/lib/test_container.py b/openstackclient/tests/object/v1/lib/test_container.py index 3b9976a1f3..c3fdea72b0 100644 --- a/openstackclient/tests/object/v1/lib/test_container.py +++ b/openstackclient/tests/object/v1/lib/test_container.py @@ -19,8 +19,7 @@ from openstackclient.object.v1.lib import container as lib_container from openstackclient.tests.common import test_restapi as restapi -from openstackclient.tests import fakes -from openstackclient.tests import utils +from openstackclient.tests.object.v1 import fakes as object_fakes fake_account = 'q12we34r' @@ -36,12 +35,10 @@ def __init__(self, endpoint=None, **kwargs): self.token = fake_auth -class TestContainer(utils.TestCommand): +class TestContainer(object_fakes.TestObjectv1): def setUp(self): super(TestContainer, self).setUp() - self.app.client_manager = fakes.FakeClientManager() - self.app.client_manager.object = FakeClient() self.app.restapi = mock.MagicMock() @@ -53,7 +50,7 @@ def test_container_list_no_options(self): data = lib_container.list_containers( self.app.restapi, - self.app.client_manager.object.endpoint, + fake_url, ) # Check expected values @@ -69,7 +66,7 @@ def test_container_list_marker(self): data = lib_container.list_containers( self.app.restapi, - self.app.client_manager.object.endpoint, + fake_url, marker='next', ) @@ -86,7 +83,7 @@ def test_container_list_limit(self): data = lib_container.list_containers( self.app.restapi, - self.app.client_manager.object.endpoint, + fake_url, limit=5, ) @@ -103,7 +100,7 @@ def test_container_list_end_marker(self): data = lib_container.list_containers( self.app.restapi, - self.app.client_manager.object.endpoint, + fake_url, end_marker='last', ) @@ -120,7 +117,7 @@ def test_container_list_prefix(self): data = lib_container.list_containers( self.app.restapi, - self.app.client_manager.object.endpoint, + fake_url, prefix='foo/', ) @@ -147,7 +144,7 @@ def side_effect(*args, **kwargs): data = lib_container.list_containers( self.app.restapi, - self.app.client_manager.object.endpoint, + fake_url, full_listing=True, ) @@ -171,7 +168,7 @@ def test_container_show_no_options(self): data = lib_container.show_container( self.app.restapi, - self.app.client_manager.object.endpoint, + fake_url, 'is-name', ) diff --git a/openstackclient/tests/object/v1/lib/test_object.py b/openstackclient/tests/object/v1/lib/test_object.py index 0104183e03..ef93877aea 100644 --- a/openstackclient/tests/object/v1/lib/test_object.py +++ b/openstackclient/tests/object/v1/lib/test_object.py @@ -19,8 +19,7 @@ from openstackclient.object.v1.lib import object as lib_object from openstackclient.tests.common import test_restapi as restapi -from openstackclient.tests import fakes -from openstackclient.tests import utils +from openstackclient.tests.object.v1 import fakes as object_fakes fake_account = 'q12we34r' @@ -37,12 +36,10 @@ def __init__(self, endpoint=None, **kwargs): self.token = fake_auth -class TestObject(utils.TestCommand): +class TestObject(object_fakes.TestObjectv1): def setUp(self): super(TestObject, self).setUp() - self.app.client_manager = fakes.FakeClientManager() - self.app.client_manager.object = FakeClient() self.app.restapi = mock.MagicMock() @@ -54,7 +51,7 @@ def test_list_objects_no_options(self): data = lib_object.list_objects( self.app.restapi, - self.app.client_manager.object.endpoint, + fake_url, fake_container, ) @@ -71,7 +68,7 @@ def test_list_objects_marker(self): data = lib_object.list_objects( self.app.restapi, - self.app.client_manager.object.endpoint, + fake_url, fake_container, marker='next', ) @@ -89,7 +86,7 @@ def test_list_objects_limit(self): data = lib_object.list_objects( self.app.restapi, - self.app.client_manager.object.endpoint, + fake_url, fake_container, limit=5, ) @@ -107,7 +104,7 @@ def test_list_objects_end_marker(self): data = lib_object.list_objects( self.app.restapi, - self.app.client_manager.object.endpoint, + fake_url, fake_container, end_marker='last', ) @@ -125,7 +122,7 @@ def test_list_objects_delimiter(self): data = lib_object.list_objects( self.app.restapi, - self.app.client_manager.object.endpoint, + fake_url, fake_container, delimiter='|', ) @@ -146,7 +143,7 @@ def test_list_objects_prefix(self): data = lib_object.list_objects( self.app.restapi, - self.app.client_manager.object.endpoint, + fake_url, fake_container, prefix='foo/', ) @@ -164,7 +161,7 @@ def test_list_objects_path(self): data = lib_object.list_objects( self.app.restapi, - self.app.client_manager.object.endpoint, + fake_url, fake_container, path='next', ) @@ -192,7 +189,7 @@ def side_effect(*args, **kwargs): data = lib_object.list_objects( self.app.restapi, - self.app.client_manager.object.endpoint, + fake_url, fake_container, full_listing=True, ) @@ -216,7 +213,7 @@ def test_object_show_no_options(self): data = lib_object.show_object( self.app.restapi, - self.app.client_manager.object.endpoint, + fake_url, fake_container, fake_object, ) @@ -250,7 +247,7 @@ def test_object_show_all_options(self): data = lib_object.show_object( self.app.restapi, - self.app.client_manager.object.endpoint, + fake_url, fake_container, fake_object, ) diff --git a/openstackclient/tests/object/v1/test_container.py b/openstackclient/tests/object/v1/test_container.py index 2090b88029..e4d90fd934 100644 --- a/openstackclient/tests/object/v1/test_container.py +++ b/openstackclient/tests/object/v1/test_container.py @@ -16,10 +16,8 @@ import copy import mock -from openstackclient.common import clientmanager from openstackclient.object.v1 import container from openstackclient.tests.object.v1 import fakes as object_fakes -from openstackclient.tests import utils AUTH_TOKEN = "foobar" @@ -32,18 +30,10 @@ def __init__(self, endpoint=None, **kwargs): self.token = AUTH_TOKEN -class TestObject(utils.TestCommand): +class TestObject(object_fakes.TestObjectv1): def setUp(self): super(TestObject, self).setUp() - api_version = {"object-store": "1"} - self.app.client_manager = clientmanager.ClientManager( - token=AUTH_TOKEN, - url=AUTH_URL, - auth_url=AUTH_URL, - api_version=api_version, - ) - class TestObjectClient(TestObject): diff --git a/openstackclient/tests/object/v1/test_object.py b/openstackclient/tests/object/v1/test_object.py index 52b5ebd086..9987c2d48a 100644 --- a/openstackclient/tests/object/v1/test_object.py +++ b/openstackclient/tests/object/v1/test_object.py @@ -16,34 +16,18 @@ import copy import mock -from openstackclient.common import clientmanager from openstackclient.object.v1 import object as obj from openstackclient.tests.object.v1 import fakes as object_fakes -from openstackclient.tests import utils AUTH_TOKEN = "foobar" AUTH_URL = "http://0.0.0.0" -class FakeClient(object): - def __init__(self, endpoint=None, **kwargs): - self.endpoint = AUTH_URL - self.token = AUTH_TOKEN - - -class TestObject(utils.TestCommand): +class TestObject(object_fakes.TestObjectv1): def setUp(self): super(TestObject, self).setUp() - api_version = {"object-store": "1"} - self.app.client_manager = clientmanager.ClientManager( - token=AUTH_TOKEN, - url=AUTH_URL, - auth_url=AUTH_URL, - api_version=api_version, - ) - class TestObjectClient(TestObject): diff --git a/openstackclient/volume/client.py b/openstackclient/volume/client.py index 626b23f1c1..e04e8cd7b8 100644 --- a/openstackclient/volume/client.py +++ b/openstackclient/volume/client.py @@ -20,6 +20,8 @@ LOG = logging.getLogger(__name__) +DEFAULT_VOLUME_API_VERSION = '1' +API_VERSION_OPTION = 'os_volume_api_version' API_NAME = "volume" API_VERSIONS = { "1": "cinderclient.v1.client.Client" @@ -45,3 +47,17 @@ def make_client(instance): ) return client + + +def build_option_parser(parser): + """Hook to add global options""" + parser.add_argument( + '--os-volume-api-version', + metavar='', + default=utils.env( + 'OS_VOLUME_API_VERSION', + default=DEFAULT_VOLUME_API_VERSION), + help='Volume API version, default=' + + DEFAULT_VOLUME_API_VERSION + + ' (Env: OS_VOLUME_API_VERSION)') + return parser diff --git a/setup.cfg b/setup.cfg index 95a585be53..795d965dea 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,6 +27,12 @@ console_scripts = openstack.cli = +openstack.cli.extension = + compute = openstackclient.compute.client + image = openstackclient.image.client + object = openstackclient.object.client + volume = openstackclient.volume.client + openstack.common = limits_show = openstackclient.common.limits:ShowLimits quota_set = openstackclient.common.quota:SetQuota @@ -240,7 +246,7 @@ openstack.image.v2 = image_save = openstackclient.image.v2.image:SaveImage image_show = openstackclient.image.v2.image:ShowImage -openstack.object_store.v1 = +openstack.object.v1 = container_list = openstackclient.object.v1.container:ListContainer container_show = openstackclient.object.v1.container:ShowContainer object_list = openstackclient.object.v1.object:ListObject From 935781fdf961d0501b7400acbe4c86bdd9f284f2 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 25 Nov 2013 14:46:52 -0600 Subject: [PATCH 0032/3494] Restore Object API name 'object-store' It's used in the service catalog, doh! Change-Id: If8f6db49c84756fd8e58cc68910160da4cd99b5d --- openstackclient/object/client.py | 2 +- openstackclient/object/v1/container.py | 4 ++-- openstackclient/object/v1/object.py | 4 ++-- openstackclient/tests/object/v1/fakes.py | 2 +- openstackclient/tests/object/v1/test_container.py | 10 ++++++++-- openstackclient/tests/object/v1/test_object.py | 10 ++++++++-- setup.cfg | 4 ++-- 7 files changed, 24 insertions(+), 12 deletions(-) diff --git a/openstackclient/object/client.py b/openstackclient/object/client.py index 1a5363b17c..273bea6ee8 100644 --- a/openstackclient/object/client.py +++ b/openstackclient/object/client.py @@ -23,7 +23,7 @@ DEFAULT_OBJECT_API_VERSION = '1' API_VERSION_OPTION = 'os_object_api_version' -API_NAME = 'object' +API_NAME = 'object-store' API_VERSIONS = { '1': 'openstackclient.object.client.ObjectClientv1', } diff --git a/openstackclient/object/v1/container.py b/openstackclient/object/v1/container.py index 68b14fc56d..fcfbd78351 100644 --- a/openstackclient/object/v1/container.py +++ b/openstackclient/object/v1/container.py @@ -90,7 +90,7 @@ def take_action(self, parsed_args): data = lib_container.list_containers( self.app.restapi, - self.app.client_manager.object.endpoint, + self.app.client_manager.object_store.endpoint, **kwargs ) @@ -120,7 +120,7 @@ def take_action(self, parsed_args): data = lib_container.show_container( self.app.restapi, - self.app.client_manager.object.endpoint, + self.app.client_manager.object_store.endpoint, parsed_args.container, ) diff --git a/openstackclient/object/v1/object.py b/openstackclient/object/v1/object.py index 426a52ad61..f6a770302b 100644 --- a/openstackclient/object/v1/object.py +++ b/openstackclient/object/v1/object.py @@ -108,7 +108,7 @@ def take_action(self, parsed_args): data = lib_object.list_objects( self.app.restapi, - self.app.client_manager.object.endpoint, + self.app.client_manager.object_store.endpoint, parsed_args.container, **kwargs ) @@ -144,7 +144,7 @@ def take_action(self, parsed_args): data = lib_object.show_object( self.app.restapi, - self.app.client_manager.object.endpoint, + self.app.client_manager.object_store.endpoint, parsed_args.container, parsed_args.object, ) diff --git a/openstackclient/tests/object/v1/fakes.py b/openstackclient/tests/object/v1/fakes.py index 37c35d3ba9..87f6cab694 100644 --- a/openstackclient/tests/object/v1/fakes.py +++ b/openstackclient/tests/object/v1/fakes.py @@ -81,7 +81,7 @@ class TestObjectv1(utils.TestCommand): def setUp(self): super(TestObjectv1, self).setUp() - self.app.client_manager.object = FakeObjectv1Client( + self.app.client_manager.object_store = FakeObjectv1Client( endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN, ) diff --git a/openstackclient/tests/object/v1/test_container.py b/openstackclient/tests/object/v1/test_container.py index e4d90fd934..4afb10063f 100644 --- a/openstackclient/tests/object/v1/test_container.py +++ b/openstackclient/tests/object/v1/test_container.py @@ -38,8 +38,14 @@ def setUp(self): class TestObjectClient(TestObject): def test_make_client(self): - self.assertEqual(self.app.client_manager.object.endpoint, AUTH_URL) - self.assertEqual(self.app.client_manager.object.token, AUTH_TOKEN) + self.assertEqual( + self.app.client_manager.object_store.endpoint, + AUTH_URL, + ) + self.assertEqual( + self.app.client_manager.object_store.token, + AUTH_TOKEN, + ) @mock.patch( diff --git a/openstackclient/tests/object/v1/test_object.py b/openstackclient/tests/object/v1/test_object.py index 9987c2d48a..bea0d2708d 100644 --- a/openstackclient/tests/object/v1/test_object.py +++ b/openstackclient/tests/object/v1/test_object.py @@ -32,8 +32,14 @@ def setUp(self): class TestObjectClient(TestObject): def test_make_client(self): - self.assertEqual(self.app.client_manager.object.endpoint, AUTH_URL) - self.assertEqual(self.app.client_manager.object.token, AUTH_TOKEN) + self.assertEqual( + self.app.client_manager.object_store.endpoint, + AUTH_URL, + ) + self.assertEqual( + self.app.client_manager.object_store.token, + AUTH_TOKEN, + ) @mock.patch( diff --git a/setup.cfg b/setup.cfg index 795d965dea..38205f53c6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,7 +30,7 @@ openstack.cli = openstack.cli.extension = compute = openstackclient.compute.client image = openstackclient.image.client - object = openstackclient.object.client + object_store = openstackclient.object.client volume = openstackclient.volume.client openstack.common = @@ -246,7 +246,7 @@ openstack.image.v2 = image_save = openstackclient.image.v2.image:SaveImage image_show = openstackclient.image.v2.image:ShowImage -openstack.object.v1 = +openstack.object_store.v1 = container_list = openstackclient.object.v1.container:ListContainer container_show = openstackclient.object.v1.container:ShowContainer object_list = openstackclient.object.v1.object:ListObject From 5dcc3b6164fea72c236ec339938c0117b2330bb6 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 27 Nov 2013 14:17:53 -0700 Subject: [PATCH 0033/3494] Add return Closes-Bug: 1246356 Change-Id: I70999a91062b9c61e5f420b1ed33a45086b62fd4 --- openstackclient/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstackclient/shell.py b/openstackclient/shell.py index f8a47ca664..4ac7683f2e 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -122,7 +122,7 @@ def __init__(self): def run(self, argv): try: - super(OpenStackShell, self).run(argv) + return super(OpenStackShell, self).run(argv) except Exception as e: if not logging.getLogger('').handlers: logging.basicConfig() From f2dbe2e43716925f592db831d95fc5783abcecc9 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 25 Nov 2013 13:39:30 -0600 Subject: [PATCH 0034/3494] Bring RESTApi closer to ithe imminent keystoneclient.Session Prepare to use the (soon to be) common Session from keystoneclient * Rework RESTApi to eventually be a subclass of keystoneclient.Session Change-Id: I68e610f8b19a3f6267a93f7bf3de54a228be68aa --- openstackclient/common/restapi.py | 297 +++++++++++++----- openstackclient/object/v1/lib/container.py | 20 +- openstackclient/object/v1/lib/object.py | 24 +- openstackclient/shell.py | 5 +- openstackclient/tests/common/test_restapi.py | 124 +++++--- .../tests/object/v1/lib/test_container.py | 80 +++-- .../tests/object/v1/lib/test_object.py | 107 ++++--- 7 files changed, 427 insertions(+), 230 deletions(-) diff --git a/openstackclient/common/restapi.py b/openstackclient/common/restapi.py index a45c842607..1bb64fae03 100644 --- a/openstackclient/common/restapi.py +++ b/openstackclient/common/restapi.py @@ -25,14 +25,15 @@ from urllib import urlencode +USER_AGENT = 'RAPI' + _logger = logging.getLogger(__name__) class RESTApi(object): - """A REST api client that handles the interface from us to the server + """A REST API client that handles the interface from us to the server - RESTApi is an extension of a requests.Session that knows - how to do: + RESTApi is requests.Session wrapper that knows how to do: * JSON serialization/deserialization * log requests in 'curl' format * basic API boilerplate for create/delete/list/set/show verbs @@ -46,26 +47,49 @@ class RESTApi(object): it communicates with, such as the available endpoints, API versions, etc. """ - USER_AGENT = 'RAPI' - def __init__( self, - os_auth=None, + session=None, + auth_header=None, user_agent=USER_AGENT, - debug=None, verify=True, - **kwargs + logger=None, + debug=None, ): - self.set_auth(os_auth) + """Construct a new REST client + + :param object session: A Session object to be used for + communicating with the identity service. + :param string auth_header: A token from an initialized auth_reference + to be used in the X-Auth-Token header + :param string user_agent: Set the User-Agent header in the requests + :param boolean/string verify: If ``True``, the SSL cert will be + verified. A CA_BUNDLE path can also be + provided. + :param logging.Logger logger: A logger to output to. (optional) + :param boolean debug: Enables debug logging of all request and + responses to identity service. + default False (optional) + """ + + self.set_auth(auth_header) self.debug = debug - self.session = requests.Session(**kwargs) - self.set_header('User-Agent', user_agent) - self.set_header('Content-Type', 'application/json') + if not session: + # We create a default session object + session = requests.Session() + self.session = session + self.session.verify = verify + self.session.user_agent = user_agent + + if logger: + self.logger = logger + else: + self.logger = _logger - def set_auth(self, os_auth): + def set_auth(self, auth_header): """Sets the current auth blob""" - self.os_auth = os_auth + self.auth_header = auth_header def set_header(self, header, content): """Sets passed in headers into the session headers @@ -78,37 +102,154 @@ def set_header(self, header, content): self.session.headers[header] = content def request(self, method, url, **kwargs): - if self.os_auth: - self.session.headers.setdefault('X-Auth-Token', self.os_auth) - if 'data' in kwargs and isinstance(kwargs['data'], type({})): - kwargs['data'] = json.dumps(kwargs['data']) - log_request(method, url, headers=self.session.headers, **kwargs) + """Make an authenticated (if token available) request + + :param method: Request HTTP method + :param url: Request URL + :param data: Request body + :param json: Request body to be encoded as JSON + Overwrites ``data`` argument if present + """ + + kwargs.setdefault('headers', {}) + if self.auth_header: + kwargs['headers']['X-Auth-Token'] = self.auth_header + + if 'json' in kwargs and isinstance(kwargs['json'], type({})): + kwargs['data'] = json.dumps(kwargs.pop('json')) + kwargs['headers']['Content-Type'] = 'application/json' + + kwargs.setdefault('allow_redirects', True) + + if self.debug: + self._log_request(method, url, **kwargs) + response = self.session.request(method, url, **kwargs) - log_response(response) + + if self.debug: + self._log_response(response) + return self._error_handler(response) + def _error_handler(self, response): + if response.status_code < 200 or response.status_code >= 300: + self.logger.debug( + "ERROR: %s", + response.text, + ) + response.raise_for_status() + return response + + # Convenience methods to mimic the ones provided by requests.Session + + def delete(self, url, **kwargs): + """Send a DELETE request. Returns :class:`requests.Response` object. + + :param url: Request URL + :param \*\*kwargs: Optional arguments passed to ``request`` + """ + + return self.request('DELETE', url, **kwargs) + + def get(self, url, **kwargs): + """Send a GET request. Returns :class:`requests.Response` object. + + :param url: Request URL + :param \*\*kwargs: Optional arguments passed to ``request`` + """ + + return self.request('GET', url, **kwargs) + + def head(self, url, **kwargs): + """Send a HEAD request. Returns :class:`requests.Response` object. + + :param url: Request URL + :param \*\*kwargs: Optional arguments passed to ``request`` + """ + + kwargs.setdefault('allow_redirects', False) + return self.request('HEAD', url, **kwargs) + + def options(self, url, **kwargs): + """Send an OPTIONS request. Returns :class:`requests.Response` object. + + :param url: Request URL + :param \*\*kwargs: Optional arguments passed to ``request`` + """ + + return self.request('OPTIONS', url, **kwargs) + + def patch(self, url, data=None, json=None, **kwargs): + """Send a PUT request. Returns :class:`requests.Response` object. + + :param url: Request URL + :param data: Request body + :param json: Request body to be encoded as JSON + Overwrites ``data`` argument if present + :param \*\*kwargs: Optional arguments passed to ``request`` + """ + + return self.request('PATCH', url, data=data, json=json, **kwargs) + + def post(self, url, data=None, json=None, **kwargs): + """Send a POST request. Returns :class:`requests.Response` object. + + :param url: Request URL + :param data: Request body + :param json: Request body to be encoded as JSON + Overwrites ``data`` argument if present + :param \*\*kwargs: Optional arguments passed to ``request`` + """ + + return self.request('POST', url, data=data, json=json, **kwargs) + + def put(self, url, data=None, json=None, **kwargs): + """Send a PUT request. Returns :class:`requests.Response` object. + + :param url: Request URL + :param data: Request body + :param json: Request body to be encoded as JSON + Overwrites ``data`` argument if present + :param \*\*kwargs: Optional arguments passed to ``request`` + """ + + return self.request('PUT', url, data=data, json=json, **kwargs) + + # Command verb methods + def create(self, url, data=None, response_key=None, **kwargs): - response = self.request('POST', url, data=data, **kwargs) + """Create a new object via a POST request + + :param url: Request URL + :param data: Request body, wil be JSON encoded + :param response_key: Dict key in response body to extract + :param \*\*kwargs: Optional arguments passed to ``request`` + """ + + response = self.request('POST', url, json=data, **kwargs) if response_key: return response.json()[response_key] else: return response.json() - #with self.completion_cache('human_id', self.resource_class, mode="a"): - # with self.completion_cache('uuid', self.resource_class, mode="a"): - # return self.resource_class(self, body[response_key]) + def list(self, url, data=None, response_key=None, **kwargs): + """Retrieve a list of objects via a GET or POST request - def delete(self, url): - self.request('DELETE', url) + :param url: Request URL + :param data: Request body, will be JSON encoded + :param response_key: Dict key in response body to extract + :param \*\*kwargs: Optional arguments passed to ``request`` + """ - def list(self, url, data=None, response_key=None, **kwargs): if data: - response = self.request('POST', url, data=data, **kwargs) + response = self.request('POST', url, json=data, **kwargs) else: - kwargs.setdefault('allow_redirects', True) response = self.request('GET', url, **kwargs) - return response.json()[response_key] + if response_key: + return response.json()[response_key] + else: + return response.json() ###hack this for keystone!!! #data = body[response_key] @@ -120,70 +261,70 @@ def list(self, url, data=None, response_key=None, **kwargs): # except KeyError: # pass - #with self.completion_cache('human_id', obj_class, mode="w"): - # with self.completion_cache('uuid', obj_class, mode="w"): - # return [obj_class(self, res, loaded=True) - # for res in data if res] - def set(self, url, data=None, response_key=None, **kwargs): - response = self.request('PUT', url, data=data) + """Update an object via a PUT request + + :param url: Request URL + :param data: Request body + :param json: Request body to be encoded as JSON + Overwrites ``data`` argument if present + :param \*\*kwargs: Optional arguments passed to ``request`` + """ + + response = self.request('PUT', url, json=data) if data: if response_key: return response.json()[response_key] else: return response.json() else: + # Nothing to do here return None def show(self, url, response_key=None, **kwargs): + """Retrieve a single object via a GET request + + :param url: Request URL + :param response_key: Dict key in response body to extract + :param \*\*kwargs: Optional arguments passed to ``request`` + """ + response = self.request('GET', url, **kwargs) if response_key: return response.json()[response_key] else: return response.json() - def _error_handler(self, response): - if response.status_code < 200 or response.status_code >= 300: - _logger.debug( - "ERROR: %s", + def _log_request(self, method, url, **kwargs): + if 'params' in kwargs and kwargs['params'] != {}: + url += '?' + urlencode(kwargs['params']) + + string_parts = [ + "curl -i", + "-X '%s'" % method, + "'%s'" % url, + ] + + for element in kwargs['headers']: + header = " -H '%s: %s'" % (element, kwargs['headers'][element]) + string_parts.append(header) + + self.logger.debug("REQ: %s" % " ".join(string_parts)) + if 'data' in kwargs: + self.logger.debug(" REQ BODY: %r\n" % (kwargs['data'])) + + def _log_response(self, response): + self.logger.debug( + "RESP: [%s] %r\n", + response.status_code, + response.headers, + ) + if response._content_consumed: + self.logger.debug( + " RESP BODY: %s\n", response.text, ) - response.raise_for_status() - return response - - -def log_request(method, url, **kwargs): - # put in an early exit if debugging is not enabled? - if 'params' in kwargs and kwargs['params'] != {}: - url += '?' + urlencode(kwargs['params']) - - string_parts = [ - "curl -i", - "-X '%s'" % method, - "'%s'" % url, - ] - - for element in kwargs['headers']: - header = " -H '%s: %s'" % (element, kwargs['headers'][element]) - string_parts.append(header) - - _logger.debug("REQ: %s" % " ".join(string_parts)) - if 'data' in kwargs: - _logger.debug("REQ BODY: %s\n" % (kwargs['data'])) - - -def log_response(response): - _logger.debug( - "RESP: [%s] %s\n", - response.status_code, - response.headers, - ) - if response._content_consumed: - _logger.debug( - "RESP BODY: %s\n", - response.text, + self.logger.debug( + " encoding: %s", + response.encoding, ) - _logger.debug( - "encoding: %s", - response.encoding, - ) diff --git a/openstackclient/object/v1/lib/container.py b/openstackclient/object/v1/lib/container.py index 5103d9d4ae..0bae23493f 100644 --- a/openstackclient/object/v1/lib/container.py +++ b/openstackclient/object/v1/lib/container.py @@ -67,19 +67,18 @@ def list_containers( data.extend(listing) return data - object_url = url - query = "format=json" + params = { + 'format': 'json', + } if marker: - query += '&marker=%s' % marker + params['marker'] = marker if limit: - query += '&limit=%d' % limit + params['limit'] = limit if end_marker: - query += '&end_marker=%s' % end_marker + params['end_marker'] = end_marker if prefix: - query += '&prefix=%s' % prefix - url = "%s?%s" % (object_url, query) - response = api.request('GET', url) - return response.json() + params['prefix'] = prefix + return api.list(url, params=params) def show_container( @@ -95,9 +94,8 @@ def show_container( :returns: dict of returned headers """ - object_url = "%s/%s" % (url, container) + response = api.head("%s/%s" % (url, container)) url_parts = urlparse(url) - response = api.request('HEAD', object_url) data = { 'account': url_parts.path.split('/')[-1], 'container': container, diff --git a/openstackclient/object/v1/lib/object.py b/openstackclient/object/v1/lib/object.py index 8ad5e5a51a..6f9c9d63d3 100644 --- a/openstackclient/object/v1/lib/object.py +++ b/openstackclient/object/v1/lib/object.py @@ -86,22 +86,23 @@ def list_objects( return data object_url = url - query = "format=json" + params = { + 'format': 'json', + } if marker: - query += '&marker=%s' % marker + params['marker'] = marker if limit: - query += '&limit=%d' % limit + params['limit'] = limit if end_marker: - query += '&end_marker=%s' % end_marker + params['end_marker'] = end_marker if delimiter: - query += '&delimiter=%s' % delimiter + params['delimiter'] = delimiter if prefix: - query += '&prefix=%s' % prefix + params['prefix'] = prefix if path: - query += '&path=%s' % path - url = "%s/%s?%s" % (object_url, container, query) - response = api.request('GET', url) - return response.json() + params['path'] = path + url = "%s/%s" % (object_url, container) + return api.list(url, params=params) def show_object( @@ -118,9 +119,8 @@ def show_object( :returns: dict of object properties """ - object_url = "%s/%s/%s" % (url, container, obj) + response = api.head("%s/%s/%s" % (url, container, obj)) url_parts = urlparse(url) - response = api.request('HEAD', object_url) data = { 'account': url_parts.path.split('/')[-1], 'container': container, diff --git a/openstackclient/shell.py b/openstackclient/shell.py index f8a47ca664..b508d37903 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -405,7 +405,10 @@ def initialize_app(self, argv): self.verify = self.options.os_cacert else: self.verify = not self.options.insecure - self.restapi = restapi.RESTApi(verify=self.verify) + self.restapi = restapi.RESTApi( + verify=self.verify, + debug=self.options.debug, + ) def prepare_to_run_command(self, cmd): """Set up auth and API versions""" diff --git a/openstackclient/tests/common/test_restapi.py b/openstackclient/tests/common/test_restapi.py index 4b83ffa460..c1e02fcbcf 100644 --- a/openstackclient/tests/common/test_restapi.py +++ b/openstackclient/tests/common/test_restapi.py @@ -23,6 +23,8 @@ from openstackclient.common import restapi from openstackclient.tests import utils +fake_user_agent = 'test_rapi' + fake_auth = '11223344556677889900' fake_url = 'http://gopher.com' fake_key = 'gopher' @@ -47,6 +49,9 @@ fake_gopher_tosh, ] } +fake_headers = { + 'User-Agent': fake_user_agent, +} class FakeResponse(requests.Response): @@ -68,11 +73,15 @@ def test_request_get(self, session_mock): request=mock.MagicMock(return_value=resp), ) - api = restapi.RESTApi() + api = restapi.RESTApi( + user_agent=fake_user_agent, + ) gopher = api.request('GET', fake_url) session_mock.return_value.request.assert_called_with( 'GET', fake_url, + headers={}, + allow_redirects=True, ) self.assertEqual(gopher.status_code, 200) self.assertEqual(gopher.json(), fake_gopher_single) @@ -83,11 +92,15 @@ def test_request_get_return_300(self, session_mock): request=mock.MagicMock(return_value=resp), ) - api = restapi.RESTApi() + api = restapi.RESTApi( + user_agent=fake_user_agent, + ) gopher = api.request('GET', fake_url) session_mock.return_value.request.assert_called_with( 'GET', fake_url, + headers={}, + allow_redirects=True, ) self.assertEqual(gopher.status_code, 300) self.assertEqual(gopher.json(), fake_gopher_single) @@ -98,11 +111,15 @@ def test_request_get_fail_404(self, session_mock): request=mock.MagicMock(return_value=resp), ) - api = restapi.RESTApi() + api = restapi.RESTApi( + user_agent=fake_user_agent, + ) self.assertRaises(requests.HTTPError, api.request, 'GET', fake_url) session_mock.return_value.request.assert_called_with( 'GET', fake_url, + headers={}, + allow_redirects=True, ) def test_request_get_auth(self, session_mock): @@ -112,66 +129,67 @@ def test_request_get_auth(self, session_mock): headers=mock.MagicMock(return_value={}), ) - api = restapi.RESTApi(os_auth=fake_auth) - gopher = api.request('GET', fake_url) - session_mock.return_value.headers.setdefault.assert_called_with( - 'X-Auth-Token', - fake_auth, + api = restapi.RESTApi( + auth_header=fake_auth, + user_agent=fake_user_agent, ) + gopher = api.request('GET', fake_url) + #session_mock.return_value.headers.setdefault.assert_called_with( + # 'X-Auth-Token', + # fake_auth, + #) session_mock.return_value.request.assert_called_with( 'GET', fake_url, + headers={ + 'X-Auth-Token': fake_auth, + }, + allow_redirects=True, ) self.assertEqual(gopher.json(), fake_gopher_single) - def test_request_get_header(self, session_mock): + def test_request_post(self, session_mock): resp = FakeResponse(data=fake_gopher_single) session_mock.return_value = mock.MagicMock( request=mock.MagicMock(return_value=resp), - headers=mock.MagicMock(return_value={}), ) - api = restapi.RESTApi(user_agent='fake_agent') - api.set_header('X-Fake-Header', 'wb') - gopher = api.request('GET', fake_url) - session_mock.return_value.headers.__setitem__.assert_any_call( - 'Content-Type', - 'application/json', - ) - session_mock.return_value.headers.__setitem__.assert_any_call( - 'User-Agent', - 'fake_agent', - ) - session_mock.return_value.headers.__setitem__.assert_any_call( - 'X-Fake-Header', - 'wb', + api = restapi.RESTApi( + user_agent=fake_user_agent, ) + data = fake_gopher_tosh + gopher = api.request('POST', fake_url, json=data) session_mock.return_value.request.assert_called_with( - 'GET', + 'POST', fake_url, + headers={ + 'Content-Type': 'application/json', + }, + allow_redirects=True, + data=json.dumps(data), ) self.assertEqual(gopher.json(), fake_gopher_single) - api.set_header('X-Fake-Header', None) - session_mock.return_value.headers.__delitem__.assert_any_call( - 'X-Fake-Header', - ) + # Methods + # TODO(dtroyer): add the other method methods - def test_request_post(self, session_mock): - resp = FakeResponse(data=fake_gopher_single) + def test_delete(self, session_mock): + resp = FakeResponse(status_code=200, data=None) session_mock.return_value = mock.MagicMock( request=mock.MagicMock(return_value=resp), ) api = restapi.RESTApi() - data = fake_gopher_tosh - gopher = api.request('POST', fake_url, data=data) + gopher = api.delete(fake_url) session_mock.return_value.request.assert_called_with( - 'POST', + 'DELETE', fake_url, - data=json.dumps(data), + headers=mock.ANY, + allow_redirects=True, ) - self.assertEqual(gopher.json(), fake_gopher_single) + self.assertEqual(gopher.status_code, 200) + + # Commands def test_create(self, session_mock): resp = FakeResponse(data=fake_gopher_single) @@ -187,6 +205,8 @@ def test_create(self, session_mock): session_mock.return_value.request.assert_called_with( 'POST', fake_url, + headers=mock.ANY, + allow_redirects=True, data=json.dumps(data), ) self.assertEqual(gopher, fake_gopher_single) @@ -196,24 +216,12 @@ def test_create(self, session_mock): session_mock.return_value.request.assert_called_with( 'POST', fake_url, + headers=mock.ANY, + allow_redirects=True, data=json.dumps(data), ) self.assertEqual(gopher, fake_gopher_mac) - def test_delete(self, session_mock): - resp = FakeResponse(data=None) - session_mock.return_value = mock.MagicMock( - request=mock.MagicMock(return_value=resp), - ) - - api = restapi.RESTApi() - gopher = api.delete(fake_url) - session_mock.return_value.request.assert_called_with( - 'DELETE', - fake_url, - ) - self.assertEqual(gopher, None) - def test_list(self, session_mock): resp = FakeResponse(data=fake_gopher_list) session_mock.return_value = mock.MagicMock( @@ -226,6 +234,7 @@ def test_list(self, session_mock): session_mock.return_value.request.assert_called_with( 'GET', fake_url, + headers=mock.ANY, allow_redirects=True, ) self.assertEqual(gopher, [fake_gopher_mac, fake_gopher_tosh]) @@ -237,6 +246,8 @@ def test_list(self, session_mock): session_mock.return_value.request.assert_called_with( 'POST', fake_url, + headers=mock.ANY, + allow_redirects=True, data=json.dumps(data), ) self.assertEqual(gopher, [fake_gopher_mac, fake_gopher_tosh]) @@ -248,6 +259,7 @@ def test_list(self, session_mock): session_mock.return_value.request.assert_called_with( 'GET', fake_url, + headers=mock.ANY, allow_redirects=True, params=params, ) @@ -270,7 +282,9 @@ def test_set(self, session_mock): session_mock.return_value.request.assert_called_with( 'PUT', fake_url, - data=None, + headers=mock.ANY, + allow_redirects=True, + json=None, ) self.assertEqual(gopher, None) @@ -279,6 +293,8 @@ def test_set(self, session_mock): session_mock.return_value.request.assert_called_with( 'PUT', fake_url, + headers=mock.ANY, + allow_redirects=True, data=json.dumps(data), ) self.assertEqual(gopher, fake_gopher_single) @@ -291,6 +307,8 @@ def test_set(self, session_mock): session_mock.return_value.request.assert_called_with( 'PUT', fake_url, + headers=mock.ANY, + allow_redirects=True, data=json.dumps(data), ) self.assertEqual(gopher, fake_gopher_mac) @@ -308,6 +326,8 @@ def test_show(self, session_mock): session_mock.return_value.request.assert_called_with( 'GET', fake_url, + headers=mock.ANY, + allow_redirects=True, ) self.assertEqual(gopher, fake_gopher_single) @@ -316,5 +336,7 @@ def test_show(self, session_mock): session_mock.return_value.request.assert_called_with( 'GET', fake_url, + headers=mock.ANY, + allow_redirects=True, ) self.assertEqual(gopher, fake_gopher_mac) diff --git a/openstackclient/tests/object/v1/lib/test_container.py b/openstackclient/tests/object/v1/lib/test_container.py index c3fdea72b0..f7355592dd 100644 --- a/openstackclient/tests/object/v1/lib/test_container.py +++ b/openstackclient/tests/object/v1/lib/test_container.py @@ -46,7 +46,7 @@ class TestContainerList(TestContainer): def test_container_list_no_options(self): resp = [{'name': 'is-name'}] - self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) + self.app.restapi.list.return_value = resp data = lib_container.list_containers( self.app.restapi, @@ -54,15 +54,17 @@ def test_container_list_no_options(self): ) # Check expected values - self.app.restapi.request.assert_called_with( - 'GET', - fake_url + '?format=json', + self.app.restapi.list.assert_called_with( + fake_url, + params={ + 'format': 'json', + } ) self.assertEqual(data, resp) def test_container_list_marker(self): resp = [{'name': 'is-name'}] - self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) + self.app.restapi.list.return_value = resp data = lib_container.list_containers( self.app.restapi, @@ -71,15 +73,18 @@ def test_container_list_marker(self): ) # Check expected values - self.app.restapi.request.assert_called_with( - 'GET', - fake_url + '?format=json&marker=next', + self.app.restapi.list.assert_called_with( + fake_url, + params={ + 'format': 'json', + 'marker': 'next', + } ) self.assertEqual(data, resp) def test_container_list_limit(self): resp = [{'name': 'is-name'}] - self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) + self.app.restapi.list.return_value = resp data = lib_container.list_containers( self.app.restapi, @@ -88,15 +93,18 @@ def test_container_list_limit(self): ) # Check expected values - self.app.restapi.request.assert_called_with( - 'GET', - fake_url + '?format=json&limit=5', + self.app.restapi.list.assert_called_with( + fake_url, + params={ + 'format': 'json', + 'limit': 5, + } ) self.assertEqual(data, resp) def test_container_list_end_marker(self): resp = [{'name': 'is-name'}] - self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) + self.app.restapi.list.return_value = resp data = lib_container.list_containers( self.app.restapi, @@ -105,15 +113,18 @@ def test_container_list_end_marker(self): ) # Check expected values - self.app.restapi.request.assert_called_with( - 'GET', - fake_url + '?format=json&end_marker=last', + self.app.restapi.list.assert_called_with( + fake_url, + params={ + 'format': 'json', + 'end_marker': 'last', + } ) self.assertEqual(data, resp) def test_container_list_prefix(self): resp = [{'name': 'is-name'}] - self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) + self.app.restapi.list.return_value = resp data = lib_container.list_containers( self.app.restapi, @@ -122,25 +133,26 @@ def test_container_list_prefix(self): ) # Check expected values - self.app.restapi.request.assert_called_with( - 'GET', - fake_url + '?format=json&prefix=foo/', + self.app.restapi.list.assert_called_with( + fake_url, + params={ + 'format': 'json', + 'prefix': 'foo/', + } ) self.assertEqual(data, resp) def test_container_list_full_listing(self): def side_effect(*args, **kwargs): - rv = self.app.restapi.request.return_value - self.app.restapi.request.return_value = restapi.FakeResponse( - data=[], - ) - self.app.restapi.request.side_effect = None + rv = self.app.restapi.list.return_value + self.app.restapi.list.return_value = [] + self.app.restapi.list.side_effect = None return rv resp = [{'name': 'is-name'}] - self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) - self.app.restapi.request.side_effect = side_effect + self.app.restapi.list.return_value = resp + self.app.restapi.list.side_effect = side_effect data = lib_container.list_containers( self.app.restapi, @@ -149,9 +161,12 @@ def side_effect(*args, **kwargs): ) # Check expected values - self.app.restapi.request.assert_called_with( - 'GET', - fake_url + '?format=json&marker=is-name', + self.app.restapi.list.assert_called_with( + fake_url, + params={ + 'format': 'json', + 'marker': 'is-name', + } ) self.assertEqual(data, resp) @@ -163,7 +178,7 @@ def test_container_show_no_options(self): 'x-container-object-count': 1, 'x-container-bytes-used': 577, } - self.app.restapi.request.return_value = \ + self.app.restapi.head.return_value = \ restapi.FakeResponse(headers=resp) data = lib_container.show_container( @@ -173,8 +188,7 @@ def test_container_show_no_options(self): ) # Check expected values - self.app.restapi.request.assert_called_with( - 'HEAD', + self.app.restapi.head.assert_called_with( fake_url + '/is-name', ) diff --git a/openstackclient/tests/object/v1/lib/test_object.py b/openstackclient/tests/object/v1/lib/test_object.py index ef93877aea..064efb53ed 100644 --- a/openstackclient/tests/object/v1/lib/test_object.py +++ b/openstackclient/tests/object/v1/lib/test_object.py @@ -47,7 +47,7 @@ class TestObjectListObjects(TestObject): def test_list_objects_no_options(self): resp = [{'name': 'is-name'}] - self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) + self.app.restapi.list.return_value = resp data = lib_object.list_objects( self.app.restapi, @@ -56,15 +56,17 @@ def test_list_objects_no_options(self): ) # Check expected values - self.app.restapi.request.assert_called_with( - 'GET', - fake_url + '/' + fake_container + '?format=json', + self.app.restapi.list.assert_called_with( + fake_url + '/' + fake_container, + params={ + 'format': 'json', + } ) self.assertEqual(data, resp) def test_list_objects_marker(self): resp = [{'name': 'is-name'}] - self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) + self.app.restapi.list.return_value = resp data = lib_object.list_objects( self.app.restapi, @@ -74,15 +76,18 @@ def test_list_objects_marker(self): ) # Check expected values - self.app.restapi.request.assert_called_with( - 'GET', - fake_url + '/' + fake_container + '?format=json&marker=next', + self.app.restapi.list.assert_called_with( + fake_url + '/' + fake_container, + params={ + 'format': 'json', + 'marker': 'next', + } ) self.assertEqual(data, resp) def test_list_objects_limit(self): resp = [{'name': 'is-name'}] - self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) + self.app.restapi.list.return_value = resp data = lib_object.list_objects( self.app.restapi, @@ -92,15 +97,18 @@ def test_list_objects_limit(self): ) # Check expected values - self.app.restapi.request.assert_called_with( - 'GET', - fake_url + '/' + fake_container + '?format=json&limit=5', + self.app.restapi.list.assert_called_with( + fake_url + '/' + fake_container, + params={ + 'format': 'json', + 'limit': 5, + } ) self.assertEqual(data, resp) def test_list_objects_end_marker(self): resp = [{'name': 'is-name'}] - self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) + self.app.restapi.list.return_value = resp data = lib_object.list_objects( self.app.restapi, @@ -110,15 +118,18 @@ def test_list_objects_end_marker(self): ) # Check expected values - self.app.restapi.request.assert_called_with( - 'GET', - fake_url + '/' + fake_container + '?format=json&end_marker=last', + self.app.restapi.list.assert_called_with( + fake_url + '/' + fake_container, + params={ + 'format': 'json', + 'end_marker': 'last', + } ) self.assertEqual(data, resp) def test_list_objects_delimiter(self): resp = [{'name': 'is-name'}] - self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) + self.app.restapi.list.return_value = resp data = lib_object.list_objects( self.app.restapi, @@ -131,15 +142,18 @@ def test_list_objects_delimiter(self): # NOTE(dtroyer): requests handles the URL encoding and we're # mocking that so use the otherwise-not-legal # pipe '|' char in the response. - self.app.restapi.request.assert_called_with( - 'GET', - fake_url + '/' + fake_container + '?format=json&delimiter=|', + self.app.restapi.list.assert_called_with( + fake_url + '/' + fake_container, + params={ + 'format': 'json', + 'delimiter': '|', + } ) self.assertEqual(data, resp) def test_list_objects_prefix(self): resp = [{'name': 'is-name'}] - self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) + self.app.restapi.list.return_value = resp data = lib_object.list_objects( self.app.restapi, @@ -149,15 +163,18 @@ def test_list_objects_prefix(self): ) # Check expected values - self.app.restapi.request.assert_called_with( - 'GET', - fake_url + '/' + fake_container + '?format=json&prefix=foo/', + self.app.restapi.list.assert_called_with( + fake_url + '/' + fake_container, + params={ + 'format': 'json', + 'prefix': 'foo/', + } ) self.assertEqual(data, resp) def test_list_objects_path(self): resp = [{'name': 'is-name'}] - self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) + self.app.restapi.list.return_value = resp data = lib_object.list_objects( self.app.restapi, @@ -167,25 +184,26 @@ def test_list_objects_path(self): ) # Check expected values - self.app.restapi.request.assert_called_with( - 'GET', - fake_url + '/' + fake_container + '?format=json&path=next', + self.app.restapi.list.assert_called_with( + fake_url + '/' + fake_container, + params={ + 'format': 'json', + 'path': 'next', + } ) self.assertEqual(data, resp) def test_list_objects_full_listing(self): def side_effect(*args, **kwargs): - rv = self.app.restapi.request.return_value - self.app.restapi.request.return_value = restapi.FakeResponse( - data=[], - ) - self.app.restapi.request.side_effect = None + rv = self.app.restapi.list.return_value + self.app.restapi.list.return_value = [] + self.app.restapi.list.side_effect = None return rv resp = [{'name': 'is-name'}] - self.app.restapi.request.return_value = restapi.FakeResponse(data=resp) - self.app.restapi.request.side_effect = side_effect + self.app.restapi.list.return_value = resp + self.app.restapi.list.side_effect = side_effect data = lib_object.list_objects( self.app.restapi, @@ -195,9 +213,12 @@ def side_effect(*args, **kwargs): ) # Check expected values - self.app.restapi.request.assert_called_with( - 'GET', - fake_url + '/' + fake_container + '?format=json&marker=is-name', + self.app.restapi.list.assert_called_with( + fake_url + '/' + fake_container, + params={ + 'format': 'json', + 'marker': 'is-name', + } ) self.assertEqual(data, resp) @@ -208,7 +229,7 @@ def test_object_show_no_options(self): resp = { 'content-type': 'text/alpha', } - self.app.restapi.request.return_value = \ + self.app.restapi.head.return_value = \ restapi.FakeResponse(headers=resp) data = lib_object.show_object( @@ -219,8 +240,7 @@ def test_object_show_no_options(self): ) # Check expected values - self.app.restapi.request.assert_called_with( - 'HEAD', + self.app.restapi.head.assert_called_with( fake_url + '/%s/%s' % (fake_container, fake_object), ) @@ -242,7 +262,7 @@ def test_object_show_all_options(self): 'x-object-meta-wife': 'Wilma', 'x-tra-header': 'yabba-dabba-do', } - self.app.restapi.request.return_value = \ + self.app.restapi.head.return_value = \ restapi.FakeResponse(headers=resp) data = lib_object.show_object( @@ -253,8 +273,7 @@ def test_object_show_all_options(self): ) # Check expected values - self.app.restapi.request.assert_called_with( - 'HEAD', + self.app.restapi.head.assert_called_with( fake_url + '/%s/%s' % (fake_container, fake_object), ) From 74a27056b346e04dbad91fd632045223b16ef6ec Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 3 Dec 2013 17:24:40 -0600 Subject: [PATCH 0035/3494] Update OSC's CommandManager subclass cliff.commandmanager.CommandManager gained an option, update openstackclient.common.commandmanager.ComamndManager to match. Also add CommandManager.get_command_groups() to return a list of the currently loaded command groups. I expect this to be useful in upcoming client diagnostic commands for plugins/extensions. If these turn out to be generally useful we'll propose them to upstream cliff. Change-Id: Ic15a7ca0ef975ca679e753be861be7c628b8e10c --- openstackclient/common/commandmanager.py | 18 ++++++++++++++++-- .../tests/common/test_commandmanager.py | 19 ++++++++++++++++++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/openstackclient/common/commandmanager.py b/openstackclient/common/commandmanager.py index 06073d93cf..553bc920ab 100644 --- a/openstackclient/common/commandmanager.py +++ b/openstackclient/common/commandmanager.py @@ -1,4 +1,4 @@ -# Copyright 2012-2013 OpenStack, LLC. +# Copyright 2012-2013 OpenStack Foundation # # 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 @@ -28,15 +28,29 @@ class CommandManager(cliff.commandmanager.CommandManager): """Alters Cliff's default CommandManager behaviour to load additional command groups after initialization. """ + def __init__(self, namespace, convert_underscores=True): + self.group_list = [] + super(CommandManager, self).__init__(namespace, convert_underscores) + def _load_commands(self, group=None): if not group: group = self.namespace + self.group_list.append(group) for ep in pkg_resources.iter_entry_points(group): LOG.debug('found command %r' % ep.name) - self.commands[ep.name.replace('_', ' ')] = ep + cmd_name = ( + ep.name.replace('_', ' ') + if self.convert_underscores + else ep.name + ) + self.commands[cmd_name] = ep return def add_command_group(self, group=None): """Adds another group of command entrypoints""" if group: self._load_commands(group) + + def get_command_groups(self): + """Returns a list of the loaded command groups""" + return self.group_list diff --git a/openstackclient/tests/common/test_commandmanager.py b/openstackclient/tests/common/test_commandmanager.py index 4953c29754..088ea21ea8 100644 --- a/openstackclient/tests/common/test_commandmanager.py +++ b/openstackclient/tests/common/test_commandmanager.py @@ -1,4 +1,4 @@ -# Copyright 2012-2013 OpenStack, LLC. +# Copyright 2012-2013 OpenStack Foundation # # 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 @@ -40,9 +40,11 @@ def _load_commands(self, group=None): if not group: self.commands['one'] = FAKE_CMD_ONE self.commands['two'] = FAKE_CMD_TWO + self.group_list.append(self.namespace) else: self.commands['alpha'] = FAKE_CMD_ALPHA self.commands['beta'] = FAKE_CMD_BETA + self.group_list.append(group) class TestCommandManager(utils.TestCase): @@ -69,3 +71,18 @@ def test_add_command_group(self): # Ensure that the original commands were not overwritten cmd_two, name, args = mgr.find_command(['two']) self.assertEqual(cmd_two, FAKE_CMD_TWO) + + def test_get_command_groups(self): + mgr = FakeCommandManager('test') + + # Make sure add_command() still functions + mock_cmd_one = mock.Mock() + mgr.add_command('mock', mock_cmd_one) + cmd_mock, name, args = mgr.find_command(['mock']) + self.assertEqual(cmd_mock, mock_cmd_one) + + # Load another command group + mgr.add_command_group('latin') + + gl = mgr.get_command_groups() + self.assertEqual(['test', 'latin'], gl) From a93cc3fae26a6d4346194ec20ec37c2134801389 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 15 Nov 2013 16:48:52 -0600 Subject: [PATCH 0036/3494] Add module list command Lists versions of installed python modules (Origianlly proposed as 'version list') Change-Id: I76a51d3d6783f46ef2daa0a41626019a880a2a50 --- openstackclient/common/module.py | 60 ++++++++++++++ openstackclient/tests/common/test_module.py | 88 +++++++++++++++++++++ openstackclient/tests/fakes.py | 6 ++ setup.cfg | 1 + 4 files changed, 155 insertions(+) create mode 100644 openstackclient/common/module.py create mode 100644 openstackclient/tests/common/test_module.py diff --git a/openstackclient/common/module.py b/openstackclient/common/module.py new file mode 100644 index 0000000000..4a7f062698 --- /dev/null +++ b/openstackclient/common/module.py @@ -0,0 +1,60 @@ +# Copyright 2013 Nebula Inc. +# +# 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. +# + +"""Module action implementation""" + +import logging +import six +import sys + +from cliff import show + + +class ListModule(show.ShowOne): + """List module versions""" + + auth_required = False + log = logging.getLogger(__name__ + '.ListModule') + + def get_parser(self, prog_name): + parser = super(ListModule, self).get_parser(prog_name) + parser.add_argument( + '--all', + action='store_true', + default=False, + help='Show all modules that have version information', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + + data = {} + # Get module versions + mods = sys.modules + for k in mods.keys(): + k = k.split('.')[0] + # TODO(dtroyer): Need a better way to decide which modules to + # show for the default (not --all) invocation. + # It should be just the things we actually care + # about like client and plugin modules... + if (parsed_args.all or 'client' in k): + try: + data[k] = mods[k].__version__ + except AttributeError: + # aw, just skip it + pass + + return zip(*sorted(six.iteritems(data))) diff --git a/openstackclient/tests/common/test_module.py b/openstackclient/tests/common/test_module.py new file mode 100644 index 0000000000..ce1592e41c --- /dev/null +++ b/openstackclient/tests/common/test_module.py @@ -0,0 +1,88 @@ +# Copyright 2013 Nebula Inc. +# +# 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. +# + +"""Test module module""" + +import mock + +from openstackclient.common import module as osc_module +from openstackclient.tests import fakes +from openstackclient.tests import utils + + +# NOTE(dtroyer): module_1 must match the version list filter (not --all) +# currently == '*client*' +module_name_1 = 'fakeclient' +module_version_1 = '0.1.2' +MODULE_1 = { + '__version__': module_version_1, +} + +module_name_2 = 'zlib' +module_version_2 = '1.1' +MODULE_2 = { + '__version__': module_version_2, +} + +MODULES = { + module_name_1: fakes.FakeModule(module_name_1, module_version_1), + module_name_2: fakes.FakeModule(module_name_2, module_version_2), +} + + +@mock.patch.dict( + 'openstackclient.common.module.sys.modules', + values=MODULES, + clear=True, +) +class TestModuleList(utils.TestCommand): + + def setUp(self): + super(TestModuleList, self).setUp() + + # Get the command object to test + self.cmd = osc_module.ListModule(self.app, None) + + def test_module_list_no_options(self): + arglist = [] + verifylist = [ + ('all', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Additional modules may be present, just check our additions + self.assertTrue(module_name_1 in columns) + self.assertTrue(module_version_1 in data) + + def test_module_list_all(self): + arglist = [ + '--all', + ] + verifylist = [ + ('all', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Additional modules may be present, just check our additions + self.assertTrue(module_name_1 in columns) + self.assertTrue(module_name_2 in columns) + self.assertTrue(module_version_1 in data) + self.assertTrue(module_version_2 in data) diff --git a/openstackclient/tests/fakes.py b/openstackclient/tests/fakes.py index 8ab4546588..3e8aada915 100644 --- a/openstackclient/tests/fakes.py +++ b/openstackclient/tests/fakes.py @@ -53,6 +53,12 @@ def __init__(self): self.auth_ref = None +class FakeModule(object): + def __init__(self, name, version): + self.name = name + self.__version__ = version + + class FakeResource(object): def __init__(self, manager, info, loaded=False): self.manager = manager diff --git a/setup.cfg b/setup.cfg index 95a585be53..1d7af29c81 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,6 +26,7 @@ console_scripts = openstack = openstackclient.shell:main openstack.cli = + module_list = openstackclient.common.module:ListModule openstack.common = limits_show = openstackclient.common.limits:ShowLimits From f227092a0687c0af3f55e3f0a2e154a491e4d846 Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Wed, 4 Dec 2013 14:44:09 +0100 Subject: [PATCH 0037/3494] Add missing requests and six requirements From global-requirements: requests>=1.1, six>=1.4.1 Change-Id: I536adc511931229a268ba81f81aef1aed59b33eb --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index e515041350..eee138a465 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,5 @@ python-glanceclient>=0.9.0 python-keystoneclient>=0.4.1 python-novaclient>=2.15.0 python-cinderclient>=1.0.6 +requests>=1.1 +six>=1.4.1 From 4f1ebe8069a9b8e78f05eb4b3a0ccb7069b7c1f3 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 5 Dec 2013 13:23:44 -0600 Subject: [PATCH 0038/3494] Update docs for plugins and release notes * Fill out the existing command and man page * Add a plugins page. * Begin the release notes for 0.3.0 Change-Id: I4527fed28a10a9d79fc8f6c1d925a4bf0d0a7a36 --- doc/source/commands.rst | 37 ++++++++++++------- doc/source/index.rst | 1 + doc/source/man/openstack.rst | 69 ++++++++++++++++++++++++++++++++---- doc/source/plugins.rst | 46 ++++++++++++++++++++++++ doc/source/releases.rst | 12 +++++++ 5 files changed, 146 insertions(+), 19 deletions(-) create mode 100644 doc/source/plugins.rst diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 7cd0595108..0b93c64d98 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -10,15 +10,24 @@ OpenStackClient has a consistent and predictable format for all of its commands. Commands take the form:: - openstack [] [] [] + openstack [] [] [] -* All long options names begin with two dashes ('--') and use a single dash ('-') internally between words (--like-this) +* All long options names begin with two dashes ('--') and use a single dash + ('-') internally between words (--like-this). Underscores ('_') are not used + in option names. Global Options -------------- -Global options are global in the sense that the apply to every command invocation regardless of action to be performed. This includes authentication credentials and API version selection. Most global options have a corresponding environment variable that may also be used to set the value. If both are present, the command-line option takes priority. The environment variable names are derived from the option name by dropping the leading dashes ('--'), converting each embedded dash ('-') to an underscore ('_'), and converting to upper case. +Global options are global in the sense that they apply to every command +invocation regardless of action to be performed. They include authentication +credentials and API version selection. Most global options have a corresponding +environment variable that may also be used to set the value. If both are +present, the command-line option takes priority. The environment variable +names are derived from the option name by dropping the leading dashes ('--'), +converting each embedded dash ('-') to an underscore ('_'), and converting +to upper case. For example, ``--os-username`` can be set from the environment via ``OS_USERNAME``. @@ -26,9 +35,12 @@ For example, ``--os-username`` can be set from the environment via ``OS_USERNAME Command Object(s) and Action ---------------------------- -Commands consist of an object described by one or more words followed by an action. -In commands requiring two objects be acted upon, the primary object appears ahead of the action and the secondary object appears after the action. -If both objects have cooresponding positional arguments the arguments appear in the same order as the objects. In badly formed English it is expressed as "(Take) object1 (and perform) action (using) object2 (to it)." +Commands consist of an object described by one or more words followed by +an action. Commands that require two objects have the primary object ahead +of the action and the secondary object after the action. Any positional +arguments identifying the objects shall appear in the same order as the +objects. In badly formed English it is expressed as "(Take) object1 +(and perform) action (using) object2 (to it)." :: @@ -44,22 +56,23 @@ Examples:: Command Arguments and Options ----------------------------- -Commands have their own set of options distinct from the global options. They follow the -same style as the global options and always appear between the command and any positional arguemnts the command may require. +Each command may have its own set of options distinct from the global options. +They follow the same style as the global options and always appear between +the command and any positional arguments the command requires. Implementation ============== -The command structure is designed to support seamless addition of extension -command modules via entry points. The extensions are assumed to be subclasses -of Cliff's command.Command object. +The command structure is designed to support seamless addition of plugin +command modules via ``setuptools`` entry points. The plugin commands must +be subclasses of Cliff's command.Command object. Command Entry Points -------------------- -Commands are added to the client using setuptools's entry points in ``setup.cfg``. +Commands are added to the client using ``setuptools`` entry points in ``setup.cfg``. There is a single common group ``openstack.cli`` for commands that are not versioned, and a group for each combination of OpenStack API and version that is supported. For example, to support Identity API v3 there is a group called diff --git a/doc/source/index.rst b/doc/source/index.rst index fcb053d621..f42ea1b720 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -12,6 +12,7 @@ Contents: releases commands + plugins man/openstack Getting Started diff --git a/doc/source/man/openstack.rst b/doc/source/man/openstack.rst index c797f285a5..3a780e2472 100644 --- a/doc/source/man/openstack.rst +++ b/doc/source/man/openstack.rst @@ -1,7 +1,8 @@ -========= +==================== :program:`openstack` -========= +==================== +OpenStack Command Line SYNOPSIS ======== @@ -10,6 +11,7 @@ SYNOPSIS :program:`openstack help` +:program:`openstack` --help DESCRIPTION @@ -21,9 +23,8 @@ a distinct and consistent command structure. :program:`openstack` uses a similar authentication scheme as the OpenStack project CLIs, with the credential information supplied either as environment variables or as options on the -command line. The primary difference is a preference for using -``OS_PROJECT_NAME``/``OS_PROJECT_ID`` over the old tenant-based names. The old names work -for now though. +command line. The primary difference is the use of 'project' in the name of the options +``OS_PROJECT_NAME``/``OS_PROJECT_ID`` over the old tenant-based names. :: @@ -57,7 +58,22 @@ OPTIONS Authentication region name :option:`--os-default-domain ` - Default domain ID (defaults to 'default') + Default domain ID (Default: 'default') + +:options:`--os-use-keyring` + Use keyring to store password (default: False) + +:option:`--os-cacert ` + CA certificate bundle file + +:option:`--verify|--insecure` + Verify or ignore server certificate (default: verify) + +:option:`--os-identity-api-version ` + Identity API version (Default: 2.0) + +:option:`--os-XXXX-api-version ` + Additional API version options will be presend depending on the installed API libraries. NOTES @@ -78,6 +94,35 @@ To get a description of a specific command:: openstack help +:option:`complete` + Print the bash completion functions for the current command set. + +:option:`help ` + Print help for an individual command + + +EXAMPLES +======== + +Show the detailed information for server ``appweb01``:: + + openstack --os-tenant-name ExampleCo --os-username demo --os-password secrete --os-auth-url http://localhost:5000:/v2.0 server show appweb01 + +The same command if the auth environment variables (:envvar:`OS_AUTH_URL`, :envvar:`OS_PROJECT_NAME`, +:envvar:`OS_USERNAME`, :envvar:`OS_PASSWORD`) are set:: + + openstack server show appweb01 + +Create a new image:: + + openstack image create \ + --disk-format=qcow2 \ + --container-format=bare \ + --public \ + --copy-from http://somewhere.net/foo.img \ + foo + + FILES ===== @@ -95,6 +140,12 @@ The following environment variables can be set to alter the behaviour of :progra :envvar:`OS_PASSWORD` Set the password +:envvar:`OS_PROJECT_NAME` + Set the project name + +:envvar:`OS_AUTH_URL` + Set the authentication URL + BUGS ==== @@ -124,4 +175,8 @@ http://www.apache.org/licenses/LICENSE-2.0 SEE ALSO ======== -The OpenStack project CLIs, the OpenStack API references. +The `OpenStackClient page `_ +in the `OpenStack Wiki `_ contains further +documentation. + +The individual OpenStack project CLIs, the OpenStack API references. diff --git a/doc/source/plugins.rst b/doc/source/plugins.rst new file mode 100644 index 0000000000..5cea16cf87 --- /dev/null +++ b/doc/source/plugins.rst @@ -0,0 +1,46 @@ +======= +Plugins +======= + +The OpenStackClient plugin system is designed so that the plugin need only be +properly installed for OSC to find and use it. It utilizes the +``setuptools`` entry points mechanism to advertise to OSC the +plugin module and supported commands. + +Implementation +-------------- + +Plugins are discovered by enumerating the entry points +found under ``openstack.cli.extension`` and initializing the specified +client module. + +:: + + [entry_points] + openstack.cli.extension = + oscplugin = oscplugin.client + +The client module must implement the following interface functions: + +* ``API_NAME`` - A string containing the plugin API name; this is + the name of the entry point declaring the plugin client module + (``oscplugin = ...`` in the example above) and the group name for + the plugin commands (``openstack.oscplugin.v1 =`` in the example below) +* ``API_VERSION_OPTION`` (optional) - If set, the name of the API + version attribute; this must be a valid Python identifier and + match the destination set in ``build_option_parser()``. +* ``API_VERSIONS`` - A dict mapping a version string to the client class +* ``build_option_parser(parser)`` - Hook to add global options to the parser +* ``make_client(instance)`` - Hook to create the client object + +OSC enumerates the plugin commands from the entry points in the usual manner +defined for the API version: + +:: + + openstack.oscplugin.v1 = + plugin_list = oscplugin.v1.plugin:ListPlugin + plugin_show = oscplugin.v1.plugin:ShowPlugin + +Note that OSC defines the group name as ``openstack..v`` +so the version should not contain the leading 'v' character. diff --git a/doc/source/releases.rst b/doc/source/releases.rst index 1387d326b8..f10aefbe76 100644 --- a/doc/source/releases.rst +++ b/doc/source/releases.rst @@ -2,6 +2,18 @@ Release Notes ============= +0.3.0 (xx Dec 2013) +=================== + +* add new command plugin structure +* complete converting base test classes +* add options to support TLS cetificate verification +* add object-store show commands for container and object + +.. commented to save format of bug fix +.. * 1254168_: OS_REGION_NAME is not used +.. _1254168: https://bugs.launchpad.net/python-openstackclient/+bug/1254168 + 0.2.2 (20 Sep 2013) =================== From c645049c24340b49b30e5a41ba4566a2d2a9a3d0 Mon Sep 17 00:00:00 2001 From: OpenStack Jenkins Date: Tue, 10 Dec 2013 23:45:49 +0000 Subject: [PATCH 0039/3494] Updated from global requirements Change-Id: I065a67d560efca0907da9fcaa8d5ce4712dfa2c1 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 63de8be05b..a71bad62e2 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,7 +5,7 @@ discover fixtures>=0.3.14 mock>=1.0 mox3>=0.7.0 -sphinx>=1.1.2 +sphinx>=1.1.2,<1.2 testrepository>=0.0.17 testtools>=0.9.32 WebOb>=1.2.3,<1.3 From 4595ca136523e6f3f28595e5a7d038beaacc0c44 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 17 Dec 2013 09:20:59 -0600 Subject: [PATCH 0040/3494] Remove mox3 requirement mox3 is only used got py3 testing when converting tests from mox, all OSC tests are new so we don't need it. Change-Id: I2fae539e99143f91048c95d1e46cfbd7b0e9bdb0 --- test-requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index a71bad62e2..34beccaeb3 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,7 +4,6 @@ coverage>=3.6 discover fixtures>=0.3.14 mock>=1.0 -mox3>=0.7.0 sphinx>=1.1.2,<1.2 testrepository>=0.0.17 testtools>=0.9.32 From de27c1b455624b6123b26bfdd022d2763c541a25 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 17 Dec 2013 09:22:17 -0600 Subject: [PATCH 0041/3494] Release notes for 0.3.0 release Change-Id: I6f025b745378613eb674e13dd503e57d049a3e50 --- doc/source/releases.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/releases.rst b/doc/source/releases.rst index f10aefbe76..7d74318e11 100644 --- a/doc/source/releases.rst +++ b/doc/source/releases.rst @@ -2,7 +2,7 @@ Release Notes ============= -0.3.0 (xx Dec 2013) +0.3.0 (17 Dec 2013) =================== * add new command plugin structure From 12f31eed2f9f12e7fd60c238a338bb45eaee3516 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Thu, 19 Dec 2013 20:06:54 -0700 Subject: [PATCH 0042/3494] Closes-Bug: #1262321 Remove the unimplemented post_process method call Change-Id: Iaed526cc25a651008a66ad7f0050070ab2b4c595 --- tools/install_venv.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/install_venv.py b/tools/install_venv.py index a5891d6710..770ae73c5e 100644 --- a/tools/install_venv.py +++ b/tools/install_venv.py @@ -59,7 +59,6 @@ def main(argv): install.check_dependencies() install.create_virtualenv(no_site_packages=options.no_site_packages) install.install_dependencies() - install.post_process() print_help() From 202c3e375bf3ac528a5e8e1a1d1d83d45b57b9bf Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 25 Dec 2013 09:30:05 -0700 Subject: [PATCH 0043/3494] Closes-Bug: #1262322 Make links clickable Change-Id: I61302ff5274cdaa09801cb9b0dc9bfd353ac687f --- README.rst | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/README.rst b/README.rst index b0f3b6b7f4..00c920ba36 100644 --- a/README.rst +++ b/README.rst @@ -7,24 +7,27 @@ It is a thin wrapper to the stock python-*client modules that implement the actual REST API client actions. This is an implementation of the design goals shown in -http://wiki.openstack.org/UnifiedCLI. The primary goal is to provide +`OpenStack Client Wiki`_. The primary goal is to provide a unified shell command structure and a common language to describe -operations in OpenStack. +operations in OpenStack. The master repository is on GitHub_. + +.. _OpenStack Client Wiki: https://wiki.openstack.org/wiki/OpenStackClient +.. _GitHub: https://github.com/openstack/python-openstackclient python-openstackclient is designed to add support for API extensions via a plugin mechanism. -For release management:: - - * https://launchpad.net/python-openstackclient - -For blueprints and feature specifications:: - - * https://blueprints.launchpad.net/python-openstackclient - -For issue tracking:: - - * https://bugs.launchpad.net/python-openstackclient +* `Release management`_ +* `Blueprints and feature specifications`_ +* `Issue tracking`_ +* `PyPi`_ +* `Developer Docs`_ +.. _release management: https://launchpad.net/python-openstackclient +.. _Blueprints and feature specifications: https://blueprints.launchpad.net/python-openstackclient +.. _Issue tracking: https://bugs.launchpad.net/python-openstackclient +.. _PyPi: https://pypi.python.org/pypi/python-openstackclient +.. _Developer Docs: http://docs.openstack.org/developer/python-openstackclient/ +.. _install virtualenv: tools/install_venv.py Note ==== @@ -37,7 +40,7 @@ Getting Started =============== We recommend using a virtualenv to install the client. This description -uses the `install_venv.py`_ script to create the virtualenv:: +uses the `install virtualenv`_ script to create the virtualenv:: python tools/install_venv.py source .venv/bin/activate From a5e087e7a9b88e2ce698ddc32d89e1462509fbb5 Mon Sep 17 00:00:00 2001 From: Florent Flament Date: Wed, 18 Dec 2013 15:07:03 +0000 Subject: [PATCH 0044/3494] Displaying curl commands for nova and cinder calls When using the -v option, displays curl equivalent commands and http messages exchanged with the nova and cinder API servers. Displays the same messages as those displayed with the --debug option of python-novaclient and python-cinderclient. Implements: blueprint curl-commands-in-debugging-messages for nova and cinder related calls Change-Id: Ibc8ef79d874334585b81d652b9c7df9e874fffa9 --- openstackclient/common/utils.py | 14 ++++++++++++++ openstackclient/compute/client.py | 7 ++++++- openstackclient/volume/client.py | 5 +++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py index 91a20895b2..94ea22253a 100644 --- a/openstackclient/common/utils.py +++ b/openstackclient/common/utils.py @@ -15,6 +15,7 @@ """Common client utilities""" +import logging import os import six import sys @@ -215,3 +216,16 @@ def wait_for_status(status_f, callback(progress) time.sleep(sleep_time) return retval + + +def get_effective_log_level(): + """Returns the lowest logging level considered by logging handlers + + Retrieve an return the smallest log level set among the root + logger's handlers (in case of multiple handlers). + """ + root_log = logging.getLogger() + min_log_lvl = logging.CRITICAL + for handler in root_log.handlers: + min_log_lvl = min(min_log_lvl, handler.level) + return min_log_lvl diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py index 4ccb2f6d40..765a48db99 100644 --- a/openstackclient/compute/client.py +++ b/openstackclient/compute/client.py @@ -35,6 +35,10 @@ def make_client(instance): instance._api_version[API_NAME], API_VERSIONS) LOG.debug('instantiating compute client: %s' % compute_client) + + # Set client http_log_debug to True if verbosity level is high enough + http_log_debug = utils.get_effective_log_level() <= logging.DEBUG + client = compute_client( username=instance._username, api_key=instance._password, @@ -49,7 +53,8 @@ def make_client(instance): extensions=[], service_type=API_NAME, # FIXME(dhellmann): what is service_name? - service_name='') + service_name='', + http_log_debug=http_log_debug) # Populate the Nova client to skip another auth query to Identity if instance._url: diff --git a/openstackclient/volume/client.py b/openstackclient/volume/client.py index e04e8cd7b8..a53203f1a8 100644 --- a/openstackclient/volume/client.py +++ b/openstackclient/volume/client.py @@ -37,6 +37,10 @@ def make_client(instance): ) LOG.debug('instantiating volume client') + + # Set client http_log_debug to True if verbosity level is high enough + http_log_debug = utils.get_effective_log_level() <= logging.DEBUG + client = volume_client( username=instance._username, api_key=instance._password, @@ -44,6 +48,7 @@ def make_client(instance): auth_url=instance._auth_url, cacert=instance._cacert, insecure=instance._insecure, + http_log_debug=http_log_debug ) return client From 7b999d786c827922ea66b61a41c89123417ed407 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 6 Jan 2014 09:42:40 -0600 Subject: [PATCH 0045/3494] Fix image set properties error Change-Id: Ia290935c8a040221caf1a46ca29a7bb2e5df1ce6 --- openstackclient/image/v1/image.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index 40f9fce1a8..05006d69a5 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -313,6 +313,7 @@ def get_parser(self, prog_name): "--property", dest="properties", metavar="", + default={}, action=parseractions.KeyValueAction, help="Set property on this image " '(repeat option to set multiple properties)', From 420b10ee6dc8c40a7936001381080e4b6628e900 Mon Sep 17 00:00:00 2001 From: Paul Belanger Date: Mon, 9 Dec 2013 20:01:04 -0500 Subject: [PATCH 0046/3494] Add support for specifying custom domains Add the ability to pass user_domain_id / user_domain_name, domain_id / domain_name, and project_domain_id / project_domain_name to keystone. These parameters are the first step needed to getting multi-domain support working via the CLI. Closes-Bug: #1198171 Change-Id: I81a8534913978ff1cce01ec02741ae477e8c5fa4 Signed-off-by: Paul Belanger Signed-off-by: Bo Tang --- openstackclient/common/clientmanager.py | 16 ++- openstackclient/identity/client.py | 6 + openstackclient/shell.py | 63 +++++++- openstackclient/tests/test_shell.py | 182 +++++++++++++++++++++++- 4 files changed, 254 insertions(+), 13 deletions(-) diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index a0224064da..b6dab253a3 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -42,16 +42,26 @@ class ClientManager(object): """Manages access to API clients, including authentication.""" identity = ClientCache(identity_client.make_client) - def __init__(self, token=None, url=None, auth_url=None, project_name=None, - project_id=None, username=None, password=None, - region_name=None, verify=True, api_version=None): + def __init__(self, token=None, url=None, auth_url=None, + domain_id=None, domain_name=None, + project_name=None, project_id=None, + username=None, password=None, + user_domain_id=None, user_domain_name=None, + project_domain_id=None, project_domain_name=None, + region_name=None, api_version=None, verify=True): self._token = token self._url = url self._auth_url = auth_url + self._domain_id = domain_id + self._domain_name = domain_name self._project_name = project_name self._project_id = project_id self._username = username self._password = password + self._user_domain_id = user_domain_id + self._user_domain_name = user_domain_name + self._project_domain_id = project_domain_id + self._project_domain_name = project_domain_name self._region_name = region_name self._api_version = api_version self._service_catalog = None diff --git a/openstackclient/identity/client.py b/openstackclient/identity/client.py index 305d4cc450..b19388ccb2 100644 --- a/openstackclient/identity/client.py +++ b/openstackclient/identity/client.py @@ -46,6 +46,12 @@ def make_client(instance): client = identity_client( username=instance._username, password=instance._password, + user_domain_id=instance._user_domain_id, + user_domain_name=instance._user_domain_name, + project_domain_id=instance._project_domain_id, + project_domain_name=instance._project_domain_name, + domain_id=instance._domain_id, + domain_name=instance._domain_name, tenant_name=instance._project_name, tenant_id=instance._project_id, auth_url=instance._auth_url, diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 73766a6ed9..76cc3c6a90 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -143,11 +143,26 @@ def build_option_parser(self, description, version): metavar='', default=env('OS_AUTH_URL'), help='Authentication URL (Env: OS_AUTH_URL)') + parser.add_argument( + '--os-domain-name', + metavar='', + default=env('OS_DOMAIN_NAME'), + help='Domain name of the requested domain-level' + 'authorization scope (Env: OS_DOMAIN_NAME)', + ) + parser.add_argument( + '--os-domain-id', + metavar='', + default=env('OS_DOMAIN_ID'), + help='Domain ID of the requested domain-level' + 'authorization scope (Env: OS_DOMAIN_ID)', + ) parser.add_argument( '--os-project-name', metavar='', default=env('OS_PROJECT_NAME', default=env('OS_TENANT_NAME')), - help='Authentication project name (Env: OS_PROJECT_NAME)', + help='Project name of the requested project-level' + 'authorization scope (Env: OS_PROJECT_NAME)', ) parser.add_argument( '--os-tenant-name', @@ -159,7 +174,8 @@ def build_option_parser(self, description, version): '--os-project-id', metavar='', default=env('OS_PROJECT_ID', default=env('OS_TENANT_ID')), - help='Authentication project ID (Env: OS_PROJECT_ID)', + help='Project ID of the requested project-level' + 'authorization scope (Env: OS_PROJECT_ID)', ) parser.add_argument( '--os-tenant-id', @@ -177,6 +193,30 @@ def build_option_parser(self, description, version): metavar='', default=utils.env('OS_PASSWORD'), help='Authentication password (Env: OS_PASSWORD)') + parser.add_argument( + '--os-user-domain-name', + metavar='', + default=utils.env('OS_USER_DOMAIN_NAME'), + help='Domain name of the user (Env: OS_USER_DOMAIN_NAME)') + parser.add_argument( + '--os-user-domain-id', + metavar='', + default=utils.env('OS_USER_DOMAIN_ID'), + help='Domain ID of the user (Env: OS_USER_DOMAIN_ID)') + parser.add_argument( + '--os-project-domain-name', + metavar='', + default=utils.env('OS_PROJECT_DOMAIN_NAME'), + help='Domain name of the project which is the requested ' + 'project-level authorization scope ' + '(Env: OS_PROJECT_DOMAIN_NAME)') + parser.add_argument( + '--os-project-domain-id', + metavar='', + default=utils.env('OS_PROJECT_DOMAIN_ID'), + help='Domain ID of the project which is the requested ' + 'project-level authorization scope ' + '(Env: OS_PROJECT_DOMAIN_ID)') parser.add_argument( '--os-region-name', metavar='', @@ -284,11 +324,16 @@ def authenticate_user(self): " either --os-password, or env[OS_PASSWORD], " " or prompted response") - if not (self.options.os_project_id - or self.options.os_project_name): + if not ((self.options.os_project_id + or self.options.os_project_name) or + (self.options.os_domain_id + or self.options.os_domain_name)): raise exc.CommandError( - "You must provide a project id via" - " either --os-project-id or via env[OS_PROJECT_ID]") + "You must provide authentication scope as a project " + "or a domain via --os-project-id or env[OS_PROJECT_ID], " + "--os-project-name or env[OS_PROJECT_NAME], " + "--os-domain-id or env[OS_DOMAIN_ID], or" + "--os-domain-name or env[OS_DOMAIN_NAME].") if not self.options.os_auth_url: raise exc.CommandError( @@ -299,8 +344,14 @@ def authenticate_user(self): token=self.options.os_token, url=self.options.os_url, auth_url=self.options.os_auth_url, + domain_id=self.options.os_domain_id, + domain_name=self.options.os_domain_name, project_name=self.options.os_project_name, project_id=self.options.os_project_id, + user_domain_id=self.options.os_user_domain_id, + user_domain_name=self.options.os_user_domain_name, + project_domain_id=self.options.os_project_domain_id, + project_domain_name=self.options.os_project_domain_name, username=self.options.os_username, password=self.options.os_password, region_name=self.options.os_region_name, diff --git a/openstackclient/tests/test_shell.py b/openstackclient/tests/test_shell.py index be9c5d49b1..9253f701fe 100644 --- a/openstackclient/tests/test_shell.py +++ b/openstackclient/tests/test_shell.py @@ -20,13 +20,19 @@ from openstackclient.tests import utils -DEFAULT_USERNAME = "username" -DEFAULT_PASSWORD = "password" +DEFAULT_AUTH_URL = "http://127.0.0.1:5000/v2.0/" DEFAULT_PROJECT_ID = "xxxx-yyyy-zzzz" DEFAULT_PROJECT_NAME = "project" -DEFAULT_TOKEN = "token" +DEFAULT_DOMAIN_ID = "aaaa-bbbb-cccc" +DEFAULT_DOMAIN_NAME = "domain" +DEFAULT_USER_DOMAIN_ID = "aaaa-bbbb-cccc" +DEFAULT_USER_DOMAIN_NAME = "domain" +DEFAULT_PROJECT_DOMAIN_ID = "aaaa-bbbb-cccc" +DEFAULT_PROJECT_DOMAIN_NAME = "domain" +DEFAULT_USERNAME = "username" +DEFAULT_PASSWORD = "password" DEFAULT_REGION_NAME = "ZZ9_Plural_Z_Alpha" -DEFAULT_AUTH_URL = "http://127.0.0.1:5000/v2.0/" +DEFAULT_TOKEN = "token" DEFAULT_SERVICE_URL = "http://127.0.0.1:8771/v3.0/" DEFAULT_COMPUTE_API_VERSION = "2" @@ -78,6 +84,18 @@ def _assert_password_auth(self, cmd_options, default_args): default_args["project_id"]) self.assertEqual(_shell.options.os_project_name, default_args["project_name"]) + self.assertEqual(_shell.options.os_domain_id, + default_args["domain_id"]) + self.assertEqual(_shell.options.os_domain_name, + default_args["domain_name"]) + self.assertEqual(_shell.options.os_user_domain_id, + default_args["user_domain_id"]) + self.assertEqual(_shell.options.os_user_domain_name, + default_args["user_domain_name"]) + self.assertEqual(_shell.options.os_project_domain_id, + default_args["project_domain_id"]) + self.assertEqual(_shell.options.os_project_domain_name, + default_args["project_domain_name"]) self.assertEqual(_shell.options.os_username, default_args["username"]) self.assertEqual(_shell.options.os_password, @@ -151,6 +169,12 @@ def test_only_url_flow(self): "auth_url": DEFAULT_AUTH_URL, "project_id": "", "project_name": "", + "domain_id": "", + "domain_name": "", + "user_domain_id": "", + "user_domain_name": "", + "project_domain_id": "", + "project_domain_name": "", "username": "", "password": "", "region_name": "" @@ -163,6 +187,12 @@ def test_only_project_id_flow(self): "auth_url": "", "project_id": DEFAULT_PROJECT_ID, "project_name": "", + "domain_id": "", + "domain_name": "", + "user_domain_id": "", + "user_domain_name": "", + "project_domain_id": "", + "project_domain_name": "", "username": "", "password": "", "region_name": "" @@ -175,6 +205,12 @@ def test_only_project_name_flow(self): "auth_url": "", "project_id": "", "project_name": DEFAULT_PROJECT_NAME, + "domain_id": "", + "domain_name": "", + "user_domain_id": "", + "user_domain_name": "", + "project_domain_id": "", + "project_domain_name": "", "username": "", "password": "", "region_name": "" @@ -187,6 +223,12 @@ def test_only_tenant_id_flow(self): "auth_url": "", "project_id": DEFAULT_PROJECT_ID, "project_name": "", + "domain_id": "", + "domain_name": "", + "user_domain_id": "", + "user_domain_name": "", + "project_domain_id": "", + "project_domain_name": "", "username": "", "password": "", "region_name": "" @@ -199,6 +241,120 @@ def test_only_tenant_name_flow(self): "auth_url": "", "project_id": "", "project_name": DEFAULT_PROJECT_NAME, + "domain_id": "", + "domain_name": "", + "user_domain_id": "", + "user_domain_name": "", + "project_domain_id": "", + "project_domain_name": "", + "username": "", + "password": "", + "region_name": "" + } + self._assert_password_auth(flag, kwargs) + + def test_only_domain_id_flow(self): + flag = "--os-domain-id " + DEFAULT_DOMAIN_ID + kwargs = { + "auth_url": "", + "project_id": "", + "project_name": "", + "domain_id": DEFAULT_DOMAIN_ID, + "domain_name": "", + "user_domain_id": "", + "user_domain_name": "", + "project_domain_id": "", + "project_domain_name": "", + "username": "", + "password": "", + "region_name": "" + } + self._assert_password_auth(flag, kwargs) + + def test_only_domain_name_flow(self): + flag = "--os-domain-name " + DEFAULT_DOMAIN_NAME + kwargs = { + "auth_url": "", + "project_id": "", + "project_name": "", + "domain_id": "", + "domain_name": DEFAULT_DOMAIN_NAME, + "user_domain_id": "", + "user_domain_name": "", + "project_domain_id": "", + "project_domain_name": "", + "username": "", + "password": "", + "region_name": "" + } + self._assert_password_auth(flag, kwargs) + + def test_only_user_domain_id_flow(self): + flag = "--os-user-domain-id " + DEFAULT_USER_DOMAIN_ID + kwargs = { + "auth_url": "", + "project_id": "", + "project_name": "", + "domain_id": "", + "domain_name": "", + "user_domain_id": DEFAULT_USER_DOMAIN_ID, + "user_domain_name": "", + "project_domain_id": "", + "project_domain_name": "", + "username": "", + "password": "", + "region_name": "" + } + self._assert_password_auth(flag, kwargs) + + def test_only_user_domain_name_flow(self): + flag = "--os-user-domain-name " + DEFAULT_USER_DOMAIN_NAME + kwargs = { + "auth_url": "", + "project_id": "", + "project_name": "", + "domain_id": "", + "domain_name": "", + "user_domain_id": "", + "user_domain_name": DEFAULT_USER_DOMAIN_NAME, + "project_domain_id": "", + "project_domain_name": "", + "username": "", + "password": "", + "region_name": "" + } + self._assert_password_auth(flag, kwargs) + + def test_only_project_domain_id_flow(self): + flag = "--os-project-domain-id " + DEFAULT_PROJECT_DOMAIN_ID + kwargs = { + "auth_url": "", + "project_id": "", + "project_name": "", + "domain_id": "", + "domain_name": "", + "user_domain_id": "", + "user_domain_name": "", + "project_domain_id": DEFAULT_PROJECT_DOMAIN_ID, + "project_domain_name": "", + "username": "", + "password": "", + "region_name": "" + } + self._assert_password_auth(flag, kwargs) + + def test_only_project_domain_name_flow(self): + flag = "--os-project-domain-name " + DEFAULT_PROJECT_DOMAIN_NAME + kwargs = { + "auth_url": "", + "project_id": "", + "project_name": "", + "domain_id": "", + "domain_name": "", + "user_domain_id": "", + "user_domain_name": "", + "project_domain_id": "", + "project_domain_name": DEFAULT_PROJECT_DOMAIN_NAME, "username": "", "password": "", "region_name": "" @@ -211,6 +367,12 @@ def test_only_username_flow(self): "auth_url": "", "project_id": "", "project_name": "", + "domain_id": "", + "domain_name": "", + "user_domain_id": "", + "user_domain_name": "", + "project_domain_id": "", + "project_domain_name": "", "username": DEFAULT_USERNAME, "password": "", "region_name": "" @@ -223,6 +385,12 @@ def test_only_password_flow(self): "auth_url": "", "project_id": "", "project_name": "", + "domain_id": "", + "domain_name": "", + "user_domain_id": "", + "user_domain_name": "", + "project_domain_id": "", + "project_domain_name": "", "username": "", "password": DEFAULT_PASSWORD, "region_name": "" @@ -235,6 +403,12 @@ def test_only_region_name_flow(self): "auth_url": "", "project_id": "", "project_name": "", + "domain_id": "", + "domain_name": "", + "user_domain_id": "", + "user_domain_name": "", + "project_domain_id": "", + "project_domain_name": "", "username": "", "password": "", "region_name": DEFAULT_REGION_NAME From 9e31f8ea147cc51e2a8b22658f50e769e9866fdb Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 9 Jan 2014 16:54:40 -0600 Subject: [PATCH 0047/3494] Fix errant underscores Change-Id: I71b8c8df14b85e3042220e3593a9732ee6cefe15 --- openstackclient/image/v1/image.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index 40f9fce1a8..c6096b2de7 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -48,9 +48,9 @@ def get_parser(self, prog_name): help="Name of image", ) parser.add_argument( - "--disk_format", + "--disk-format", default="raw", - metavar="", + metavar="", help="Disk format of image", ) parser.add_argument( @@ -66,7 +66,7 @@ def get_parser(self, prog_name): parser.add_argument( "--container-format", default="bare", - metavar="", + metavar="", help="Container format of image", ) parser.add_argument( @@ -82,17 +82,17 @@ def get_parser(self, prog_name): ) parser.add_argument( "--min-disk", - metavar="", + metavar="", help="Minimum size of disk needed to boot image in gigabytes", ) parser.add_argument( "--min-ram", - metavar="", + metavar="", help="Minimum amount of ram needed to boot image in megabytes", ) parser.add_argument( "--location", - metavar="", + metavar="", help="URL where the data for this image already resides", ) parser.add_argument( @@ -107,7 +107,7 @@ def get_parser(self, prog_name): ) parser.add_argument( "--copy-from", - metavar="", + metavar="", help="Similar to --location, but this indicates that the image" " should immediately be copied from the data store", ) @@ -301,12 +301,12 @@ def get_parser(self, prog_name): ) parser.add_argument( "--min-disk", - metavar="", + metavar="", help="Minimum size of disk needed to boot image in gigabytes", ) parser.add_argument( "--min-ram", - metavar="", + metavar="", help="Minimum amount of ram needed to boot image in megabytes", ) parser.add_argument( From 81d33a524dd53f233bf72ea1eae4bea1058ceeaf Mon Sep 17 00:00:00 2001 From: Sascha Peilicke Date: Thu, 16 Jan 2014 09:21:39 +0100 Subject: [PATCH 0048/3494] Sync with global requirements Change-Id: Ie47804617ab9a11a91efd96c7989f0207e47e120 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index eee138a465..6f9742edf0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ pbr>=0.5.21,<1.0 cliff>=1.4.3 -keyring>=1.6.1,<2.0 +keyring>=1.6.1,<2.0,>=2.1 pycrypto>=2.6 python-glanceclient>=0.9.0 python-keystoneclient>=0.4.1 From 4848d3ca3aff4d148fb1b0aa0aa54e1eb3f3600f Mon Sep 17 00:00:00 2001 From: Qiu Yu Date: Wed, 15 Jan 2014 17:03:59 +0800 Subject: [PATCH 0049/3494] Add token create subcommand for identity v2 api Implements token create subcommand which is an equivalent of keystone token-get command. Original "wrap" parameter for keystone token-get is not implemented yet due to cliff Bug #1269299 This is a part of: blueprint add-identity-token-support Change-Id: I9e4de93306f2f5959717b5219621da03961524d8 --- openstackclient/identity/v2_0/token.py | 38 +++++++++++++ openstackclient/tests/identity/v2_0/fakes.py | 11 ++++ .../tests/identity/v2_0/test_token.py | 56 +++++++++++++++++++ setup.cfg | 2 + 4 files changed, 107 insertions(+) create mode 100644 openstackclient/identity/v2_0/token.py create mode 100644 openstackclient/tests/identity/v2_0/test_token.py diff --git a/openstackclient/identity/v2_0/token.py b/openstackclient/identity/v2_0/token.py new file mode 100644 index 0000000000..a0433c9654 --- /dev/null +++ b/openstackclient/identity/v2_0/token.py @@ -0,0 +1,38 @@ +# Copyright 2014 eBay Inc. +# +# 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. +# + +"""Identity v2 Token action implementations""" + +import logging +import six + +from cliff import show + + +class CreateToken(show.ShowOne): + """Create token command""" + + log = logging.getLogger(__name__ + '.CreateToken') + + def get_parser(self, prog_name): + parser = super(CreateToken, self).get_parser(prog_name) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity + token = identity_client.service_catalog.get_token() + token['project_id'] = token.pop('tenant_id') + return zip(*sorted(six.iteritems(token))) diff --git a/openstackclient/tests/identity/v2_0/fakes.py b/openstackclient/tests/identity/v2_0/fakes.py index 80febd29a4..231fa1a5e7 100644 --- a/openstackclient/tests/identity/v2_0/fakes.py +++ b/openstackclient/tests/identity/v2_0/fakes.py @@ -70,11 +70,22 @@ 'enabled': True, } +token_expires = '2014-01-01T00:00:00Z' +token_id = 'tttttttt-tttt-tttt-tttt-tttttttttttt' + +TOKEN = { + 'expires': token_expires, + 'id': token_id, + 'tenant_id': project_id, + 'user_id': user_id, +} + class FakeIdentityv2Client(object): def __init__(self, **kwargs): self.roles = mock.Mock() self.roles.resource_class = fakes.FakeResource(None, {}) + self.service_catalog = mock.Mock() self.services = mock.Mock() self.services.resource_class = fakes.FakeResource(None, {}) self.tenants = mock.Mock() diff --git a/openstackclient/tests/identity/v2_0/test_token.py b/openstackclient/tests/identity/v2_0/test_token.py new file mode 100644 index 0000000000..a156cdc641 --- /dev/null +++ b/openstackclient/tests/identity/v2_0/test_token.py @@ -0,0 +1,56 @@ +# Copyright 2014 eBay Inc. +# +# 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 openstackclient.identity.v2_0 import token +from openstackclient.tests.identity.v2_0 import fakes as identity_fakes + + +class TestToken(identity_fakes.TestIdentityv2): + + def setUp(self): + super(TestToken, self).setUp() + + # Get a shortcut to the Service Catalog Mock + self.sc_mock = self.app.client_manager.identity.service_catalog + self.sc_mock.reset_mock() + + +class TestTokenCreate(TestToken): + + def setUp(self): + super(TestTokenCreate, self).setUp() + + self.sc_mock.get_token.return_value = identity_fakes.TOKEN + self.cmd = token.CreateToken(self.app, None) + + def test_token_create(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.sc_mock.get_token.assert_called_with() + + collist = ('expires', 'id', 'project_id', 'user_id') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.token_expires, + identity_fakes.token_id, + identity_fakes.project_id, + identity_fakes.user_id, + ) + self.assertEqual(data, datalist) diff --git a/setup.cfg b/setup.cfg index 116c6927ab..434b7f70a3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -151,6 +151,8 @@ openstack.identity.v2_0 = service_list =openstackclient.identity.v2_0.service:ListService service_show =openstackclient.identity.v2_0.service:ShowService + token_create =openstackclient.identity.v2_0.token:CreateToken + user_role_list = openstackclient.identity.v2_0.role:ListUserRole user_create = openstackclient.identity.v2_0.user:CreateUser From ad4367839f53f1d00cde80bfcb396cfc3d8f9c7d Mon Sep 17 00:00:00 2001 From: Alexander Ignatov Date: Mon, 20 Jan 2014 17:27:02 +0400 Subject: [PATCH 0050/3494] Remove copyright from empty files According to policy change in HACKING: http://docs.openstack.org/developer/hacking/#openstack-licensing empty files should no longer contain copyright notices. Change-Id: Iba09a00f24dfbd1cd03c1c9f70ea216788e64d93 Closes-Bug: #1262424 --- openstackclient/common/__init__.py | 14 -------------- openstackclient/compute/__init__.py | 14 -------------- openstackclient/compute/v2/__init__.py | 14 -------------- openstackclient/identity/__init__.py | 14 -------------- openstackclient/identity/v2_0/__init__.py | 14 -------------- openstackclient/identity/v3/__init__.py | 14 -------------- openstackclient/image/__init__.py | 14 -------------- openstackclient/image/v1/__init__.py | 14 -------------- openstackclient/image/v2/__init__.py | 14 -------------- openstackclient/object/__init__.py | 12 ------------ openstackclient/object/v1/__init__.py | 12 ------------ openstackclient/object/v1/lib/__init__.py | 12 ------------ openstackclient/tests/__init__.py | 14 -------------- openstackclient/tests/common/__init__.py | 14 -------------- openstackclient/tests/compute/__init__.py | 14 -------------- openstackclient/tests/compute/v2/__init__.py | 14 -------------- openstackclient/tests/identity/__init__.py | 14 -------------- openstackclient/tests/identity/v2_0/__init__.py | 14 -------------- openstackclient/tests/identity/v3/__init__.py | 14 -------------- openstackclient/tests/image/__init__.py | 14 -------------- openstackclient/tests/image/v1/__init__.py | 14 -------------- openstackclient/tests/image/v2/__init__.py | 14 -------------- openstackclient/tests/object/__init__.py | 12 ------------ openstackclient/tests/object/v1/__init__.py | 12 ------------ openstackclient/tests/object/v1/lib/__init__.py | 12 ------------ openstackclient/tests/volume/__init__.py | 14 -------------- openstackclient/tests/volume/v1/__init__.py | 14 -------------- openstackclient/volume/__init__.py | 14 -------------- openstackclient/volume/v1/__init__.py | 14 -------------- 29 files changed, 394 deletions(-) diff --git a/openstackclient/common/__init__.py b/openstackclient/common/__init__.py index 85ac2501b8..e69de29bb2 100644 --- a/openstackclient/common/__init__.py +++ b/openstackclient/common/__init__.py @@ -1,14 +0,0 @@ -# Copyright 2012-2013 OpenStack, LLC. -# -# 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. -# diff --git a/openstackclient/compute/__init__.py b/openstackclient/compute/__init__.py index 85ac2501b8..e69de29bb2 100644 --- a/openstackclient/compute/__init__.py +++ b/openstackclient/compute/__init__.py @@ -1,14 +0,0 @@ -# Copyright 2012-2013 OpenStack, LLC. -# -# 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. -# diff --git a/openstackclient/compute/v2/__init__.py b/openstackclient/compute/v2/__init__.py index 85ac2501b8..e69de29bb2 100644 --- a/openstackclient/compute/v2/__init__.py +++ b/openstackclient/compute/v2/__init__.py @@ -1,14 +0,0 @@ -# Copyright 2012-2013 OpenStack, LLC. -# -# 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. -# diff --git a/openstackclient/identity/__init__.py b/openstackclient/identity/__init__.py index 85ac2501b8..e69de29bb2 100644 --- a/openstackclient/identity/__init__.py +++ b/openstackclient/identity/__init__.py @@ -1,14 +0,0 @@ -# Copyright 2012-2013 OpenStack, LLC. -# -# 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. -# diff --git a/openstackclient/identity/v2_0/__init__.py b/openstackclient/identity/v2_0/__init__.py index 85ac2501b8..e69de29bb2 100644 --- a/openstackclient/identity/v2_0/__init__.py +++ b/openstackclient/identity/v2_0/__init__.py @@ -1,14 +0,0 @@ -# Copyright 2012-2013 OpenStack, LLC. -# -# 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. -# diff --git a/openstackclient/identity/v3/__init__.py b/openstackclient/identity/v3/__init__.py index 85ac2501b8..e69de29bb2 100644 --- a/openstackclient/identity/v3/__init__.py +++ b/openstackclient/identity/v3/__init__.py @@ -1,14 +0,0 @@ -# Copyright 2012-2013 OpenStack, LLC. -# -# 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. -# diff --git a/openstackclient/image/__init__.py b/openstackclient/image/__init__.py index 85ac2501b8..e69de29bb2 100644 --- a/openstackclient/image/__init__.py +++ b/openstackclient/image/__init__.py @@ -1,14 +0,0 @@ -# Copyright 2012-2013 OpenStack, LLC. -# -# 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. -# diff --git a/openstackclient/image/v1/__init__.py b/openstackclient/image/v1/__init__.py index ebf59b327e..e69de29bb2 100644 --- a/openstackclient/image/v1/__init__.py +++ b/openstackclient/image/v1/__init__.py @@ -1,14 +0,0 @@ -# Copyright 2013 OpenStack, LLC. -# -# 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. -# diff --git a/openstackclient/image/v2/__init__.py b/openstackclient/image/v2/__init__.py index 85ac2501b8..e69de29bb2 100644 --- a/openstackclient/image/v2/__init__.py +++ b/openstackclient/image/v2/__init__.py @@ -1,14 +0,0 @@ -# Copyright 2012-2013 OpenStack, LLC. -# -# 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. -# diff --git a/openstackclient/object/__init__.py b/openstackclient/object/__init__.py index 02be10cd89..e69de29bb2 100644 --- a/openstackclient/object/__init__.py +++ b/openstackclient/object/__init__.py @@ -1,12 +0,0 @@ -# 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. -# diff --git a/openstackclient/object/v1/__init__.py b/openstackclient/object/v1/__init__.py index 02be10cd89..e69de29bb2 100644 --- a/openstackclient/object/v1/__init__.py +++ b/openstackclient/object/v1/__init__.py @@ -1,12 +0,0 @@ -# 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. -# diff --git a/openstackclient/object/v1/lib/__init__.py b/openstackclient/object/v1/lib/__init__.py index 02be10cd89..e69de29bb2 100644 --- a/openstackclient/object/v1/lib/__init__.py +++ b/openstackclient/object/v1/lib/__init__.py @@ -1,12 +0,0 @@ -# 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. -# diff --git a/openstackclient/tests/__init__.py b/openstackclient/tests/__init__.py index 85ac2501b8..e69de29bb2 100644 --- a/openstackclient/tests/__init__.py +++ b/openstackclient/tests/__init__.py @@ -1,14 +0,0 @@ -# Copyright 2012-2013 OpenStack, LLC. -# -# 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. -# diff --git a/openstackclient/tests/common/__init__.py b/openstackclient/tests/common/__init__.py index c534c012e8..e69de29bb2 100644 --- a/openstackclient/tests/common/__init__.py +++ b/openstackclient/tests/common/__init__.py @@ -1,14 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# -# 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. -# diff --git a/openstackclient/tests/compute/__init__.py b/openstackclient/tests/compute/__init__.py index ebf59b327e..e69de29bb2 100644 --- a/openstackclient/tests/compute/__init__.py +++ b/openstackclient/tests/compute/__init__.py @@ -1,14 +0,0 @@ -# Copyright 2013 OpenStack, LLC. -# -# 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. -# diff --git a/openstackclient/tests/compute/v2/__init__.py b/openstackclient/tests/compute/v2/__init__.py index c534c012e8..e69de29bb2 100644 --- a/openstackclient/tests/compute/v2/__init__.py +++ b/openstackclient/tests/compute/v2/__init__.py @@ -1,14 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# -# 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. -# diff --git a/openstackclient/tests/identity/__init__.py b/openstackclient/tests/identity/__init__.py index c534c012e8..e69de29bb2 100644 --- a/openstackclient/tests/identity/__init__.py +++ b/openstackclient/tests/identity/__init__.py @@ -1,14 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# -# 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. -# diff --git a/openstackclient/tests/identity/v2_0/__init__.py b/openstackclient/tests/identity/v2_0/__init__.py index c534c012e8..e69de29bb2 100644 --- a/openstackclient/tests/identity/v2_0/__init__.py +++ b/openstackclient/tests/identity/v2_0/__init__.py @@ -1,14 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# -# 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. -# diff --git a/openstackclient/tests/identity/v3/__init__.py b/openstackclient/tests/identity/v3/__init__.py index c534c012e8..e69de29bb2 100644 --- a/openstackclient/tests/identity/v3/__init__.py +++ b/openstackclient/tests/identity/v3/__init__.py @@ -1,14 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# -# 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. -# diff --git a/openstackclient/tests/image/__init__.py b/openstackclient/tests/image/__init__.py index ebf59b327e..e69de29bb2 100644 --- a/openstackclient/tests/image/__init__.py +++ b/openstackclient/tests/image/__init__.py @@ -1,14 +0,0 @@ -# Copyright 2013 OpenStack, LLC. -# -# 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. -# diff --git a/openstackclient/tests/image/v1/__init__.py b/openstackclient/tests/image/v1/__init__.py index ebf59b327e..e69de29bb2 100644 --- a/openstackclient/tests/image/v1/__init__.py +++ b/openstackclient/tests/image/v1/__init__.py @@ -1,14 +0,0 @@ -# Copyright 2013 OpenStack, LLC. -# -# 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. -# diff --git a/openstackclient/tests/image/v2/__init__.py b/openstackclient/tests/image/v2/__init__.py index ebf59b327e..e69de29bb2 100644 --- a/openstackclient/tests/image/v2/__init__.py +++ b/openstackclient/tests/image/v2/__init__.py @@ -1,14 +0,0 @@ -# Copyright 2013 OpenStack, LLC. -# -# 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. -# diff --git a/openstackclient/tests/object/__init__.py b/openstackclient/tests/object/__init__.py index 02be10cd89..e69de29bb2 100644 --- a/openstackclient/tests/object/__init__.py +++ b/openstackclient/tests/object/__init__.py @@ -1,12 +0,0 @@ -# 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. -# diff --git a/openstackclient/tests/object/v1/__init__.py b/openstackclient/tests/object/v1/__init__.py index 02be10cd89..e69de29bb2 100644 --- a/openstackclient/tests/object/v1/__init__.py +++ b/openstackclient/tests/object/v1/__init__.py @@ -1,12 +0,0 @@ -# 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. -# diff --git a/openstackclient/tests/object/v1/lib/__init__.py b/openstackclient/tests/object/v1/lib/__init__.py index 02be10cd89..e69de29bb2 100644 --- a/openstackclient/tests/object/v1/lib/__init__.py +++ b/openstackclient/tests/object/v1/lib/__init__.py @@ -1,12 +0,0 @@ -# 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. -# diff --git a/openstackclient/tests/volume/__init__.py b/openstackclient/tests/volume/__init__.py index ebf59b327e..e69de29bb2 100644 --- a/openstackclient/tests/volume/__init__.py +++ b/openstackclient/tests/volume/__init__.py @@ -1,14 +0,0 @@ -# Copyright 2013 OpenStack, LLC. -# -# 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. -# diff --git a/openstackclient/tests/volume/v1/__init__.py b/openstackclient/tests/volume/v1/__init__.py index c534c012e8..e69de29bb2 100644 --- a/openstackclient/tests/volume/v1/__init__.py +++ b/openstackclient/tests/volume/v1/__init__.py @@ -1,14 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# -# 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. -# diff --git a/openstackclient/volume/__init__.py b/openstackclient/volume/__init__.py index 85ac2501b8..e69de29bb2 100644 --- a/openstackclient/volume/__init__.py +++ b/openstackclient/volume/__init__.py @@ -1,14 +0,0 @@ -# Copyright 2012-2013 OpenStack, LLC. -# -# 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. -# diff --git a/openstackclient/volume/v1/__init__.py b/openstackclient/volume/v1/__init__.py index 85ac2501b8..e69de29bb2 100644 --- a/openstackclient/volume/v1/__init__.py +++ b/openstackclient/volume/v1/__init__.py @@ -1,14 +0,0 @@ -# Copyright 2012-2013 OpenStack, LLC. -# -# 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 350718f3bbad1e3b5da45bbb4e61e4af5a11e944 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 4 Dec 2013 18:37:50 -0600 Subject: [PATCH 0051/3494] Remove remaining print statements I think these are the last two stragglers, including debugging lines Change-Id: Ic3dd98480211d0f7d3cc951bec5cd54f902a101f --- openstackclient/compute/v2/usage.py | 7 +++++-- openstackclient/object/v1/lib/object.py | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/openstackclient/compute/v2/usage.py b/openstackclient/compute/v2/usage.py index f60c2105b5..3083576945 100644 --- a/openstackclient/compute/v2/usage.py +++ b/openstackclient/compute/v2/usage.py @@ -17,6 +17,7 @@ import datetime import logging +import sys from cliff import lister @@ -95,8 +96,10 @@ def _format_project(project): pass if len(usage_list) > 0: - print("Usage from %s to %s:" % (start.strftime(dateformat), - end.strftime(dateformat))) + sys.stdout.write("Usage from %s to %s:" % ( + start.strftime(dateformat), + end.strftime(dateformat), + )) return (column_headers, (utils.get_item_properties( diff --git a/openstackclient/object/v1/lib/object.py b/openstackclient/object/v1/lib/object.py index 8ad5e5a51a..840ff0c074 100644 --- a/openstackclient/object/v1/lib/object.py +++ b/openstackclient/object/v1/lib/object.py @@ -126,7 +126,6 @@ def show_object( 'container': container, 'object': obj, } - #print "data: %s" % data data['content-type'] = response.headers.get('content-type', None) if 'content-length' in response.headers: data['content-length'] = response.headers.get('content-length', None) From bc2395eb473a203d11df52d48968b6ab61e2c95e Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Thu, 23 Jan 2014 09:33:06 -0700 Subject: [PATCH 0052/3494] Fix keyring issue where there were name space problems The import of keyring conflicted with a string named keyring Change-Id: I7416ea1cf453a126dd03dba8bc2900cad35ed2da Closes-bug: #1271987 --- openstackclient/common/openstackkeyring.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openstackclient/common/openstackkeyring.py b/openstackclient/common/openstackkeyring.py index 34c994b743..e60ce71ca6 100644 --- a/openstackclient/common/openstackkeyring.py +++ b/openstackclient/common/openstackkeyring.py @@ -56,5 +56,5 @@ def decrypt(self, password_encrypted): def os_keyring(): """Initialize the openstack keyring.""" - keyring = 'openstackclient.common.openstackkeyring.OpenstackKeyring' - return keyring.core.load_keyring(None, keyring) + ring = 'openstackclient.common.openstackkeyring.OpenstackKeyring' + return keyring.core.load_keyring(None, ring) From 0aeb357fc24b312c6ba8632cc8019f7ea3ec32fd Mon Sep 17 00:00:00 2001 From: OpenStack Jenkins Date: Fri, 24 Jan 2014 22:40:56 +0000 Subject: [PATCH 0053/3494] Updated from global requirements Change-Id: I98929876d5a21a990009398d9a8259c54d893e7e --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6f9742edf0..cfbc598b57 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ cliff>=1.4.3 keyring>=1.6.1,<2.0,>=2.1 pycrypto>=2.6 python-glanceclient>=0.9.0 -python-keystoneclient>=0.4.1 +python-keystoneclient>=0.4.2 python-novaclient>=2.15.0 python-cinderclient>=1.0.6 requests>=1.1 From a8d828f330119502fc18107c264f2944548a7fb9 Mon Sep 17 00:00:00 2001 From: Qiu Yu Date: Wed, 29 Jan 2014 15:57:18 +0800 Subject: [PATCH 0054/3494] Add token create subcommand for identity v3 api Implements token create subcommand which is an equivalent of keystone token-get command. Original "wrap" parameter for keystone token-get is not implemented yet due to cliff Bug #1269299 This is a part of: blueprint add-identity-token-support Change-Id: I2255021c9d1f10f757686583b1ebe40b5f3a9ecb --- openstackclient/identity/v3/token.py | 18 +++++ openstackclient/tests/identity/v3/fakes.py | 18 +++++ .../tests/identity/v3/test_token.py | 79 +++++++++++++++++++ setup.cfg | 2 + 4 files changed, 117 insertions(+) create mode 100644 openstackclient/tests/identity/v3/test_token.py diff --git a/openstackclient/identity/v3/token.py b/openstackclient/identity/v3/token.py index ba667be3b3..68f9ffef47 100644 --- a/openstackclient/identity/v3/token.py +++ b/openstackclient/identity/v3/token.py @@ -185,6 +185,24 @@ def take_action(self, parsed_args): return zip(*sorted(six.iteritems(request_token))) +class CreateToken(show.ShowOne): + """Create token command""" + + log = logging.getLogger(__name__ + '.CreateToken') + + def get_parser(self, prog_name): + parser = super(CreateToken, self).get_parser(prog_name) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity + token = identity_client.service_catalog.get_token() + if 'tenant_id' in token: + token['project_id'] = token.pop('tenant_id') + return zip(*sorted(six.iteritems(token))) + + class DeleteAccessToken(command.Command): """Delete access token command""" diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index 9d40d9dbec..f2696ef8c7 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -88,6 +88,23 @@ 'domain_id': domain_id, } +token_expires = '2014-01-01T00:00:00Z' +token_id = 'tttttttt-tttt-tttt-tttt-tttttttttttt' + +TOKEN_WITH_TENANT_ID = { + 'expires': token_expires, + 'id': token_id, + 'tenant_id': project_id, + 'user_id': user_id, +} + +TOKEN_WITH_DOMAIN_ID = { + 'expires': token_expires, + 'id': token_id, + 'domain_id': domain_id, + 'user_id': user_id, +} + class FakeIdentityv3Client(object): def __init__(self, **kwargs): @@ -101,6 +118,7 @@ def __init__(self, **kwargs): self.roles.resource_class = fakes.FakeResource(None, {}) self.services = mock.Mock() self.services.resource_class = fakes.FakeResource(None, {}) + self.service_catalog = mock.Mock() self.users = mock.Mock() self.users.resource_class = fakes.FakeResource(None, {}) self.auth_token = kwargs['token'] diff --git a/openstackclient/tests/identity/v3/test_token.py b/openstackclient/tests/identity/v3/test_token.py new file mode 100644 index 0000000000..7e1d1669b9 --- /dev/null +++ b/openstackclient/tests/identity/v3/test_token.py @@ -0,0 +1,79 @@ +# Copyright 2014 eBay Inc. +# +# 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 openstackclient.identity.v3 import token +from openstackclient.tests.identity.v3 import fakes as identity_fakes + + +class TestToken(identity_fakes.TestIdentityv3): + + def setUp(self): + super(TestToken, self).setUp() + + # Get a shortcut to the Service Catalog Mock + self.sc_mock = self.app.client_manager.identity.service_catalog + self.sc_mock.reset_mock() + + +class TestTokenCreate(TestToken): + + def setUp(self): + super(TestTokenCreate, self).setUp() + + self.cmd = token.CreateToken(self.app, None) + + def test_token_create_with_project_id(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.sc_mock.get_token.return_value = \ + identity_fakes.TOKEN_WITH_TENANT_ID + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.sc_mock.get_token.assert_called_with() + + collist = ('expires', 'id', 'project_id', 'user_id') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.token_expires, + identity_fakes.token_id, + identity_fakes.project_id, + identity_fakes.user_id, + ) + self.assertEqual(data, datalist) + + def test_token_create_with_domain_id(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.sc_mock.get_token.return_value = \ + identity_fakes.TOKEN_WITH_DOMAIN_ID + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.sc_mock.get_token.assert_called_with() + + collist = ('domain_id', 'expires', 'id', 'user_id') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.domain_id, + identity_fakes.token_expires, + identity_fakes.token_id, + identity_fakes.user_id, + ) + self.assertEqual(data, datalist) diff --git a/setup.cfg b/setup.cfg index 434b7f70a3..a2c34e145a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -229,6 +229,8 @@ openstack.identity.v3 = service_show = openstackclient.identity.v3.service:ShowService service_set = openstackclient.identity.v3.service:SetService + token_create = openstackclient.identity.v3.token:CreateToken + user_create = openstackclient.identity.v3.user:CreateUser user_delete = openstackclient.identity.v3.user:DeleteUser user_list = openstackclient.identity.v3.user:ListUser From 8aa0b07fbc4dd3cd2c7d99513407b652eaa39d85 Mon Sep 17 00:00:00 2001 From: Shane Wang Date: Fri, 7 Feb 2014 13:25:43 +0800 Subject: [PATCH 0055/3494] Fix misspellings in python openstackclient Fix misspellings detected by: * pip install misspellings * git ls-files | grep -v locale | misspellings -f - Change-Id: Ic0d3efa26eb9a05ce16a8319c142f5bd1ce23821 Closes-Bug: #1257295 --- openstackclient/common/openstackkeyring.py | 2 +- openstackclient/tests/volume/v1/fakes.py | 2 +- openstackclient/tests/volume/v1/test_volume.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openstackclient/common/openstackkeyring.py b/openstackclient/common/openstackkeyring.py index e60ce71ca6..115e564440 100644 --- a/openstackclient/common/openstackkeyring.py +++ b/openstackclient/common/openstackkeyring.py @@ -37,7 +37,7 @@ def _init_crypter(self): block_size = 32 padding = '0' - # init the cipher with the class name, upto block_size + # init the cipher with the class name, up to block_size password = __name__[block_size:] password = password + (block_size - len(password) % block_size) * padding diff --git a/openstackclient/tests/volume/v1/fakes.py b/openstackclient/tests/volume/v1/fakes.py index b25dfaf70a..d6ef0d399e 100644 --- a/openstackclient/tests/volume/v1/fakes.py +++ b/openstackclient/tests/volume/v1/fakes.py @@ -32,7 +32,7 @@ 'display_description': volume_description, 'size': volume_size, 'status': '', - 'attach_status': 'detatched', + 'attach_status': 'detached', 'metadata': volume_metadata, } diff --git a/openstackclient/tests/volume/v1/test_volume.py b/openstackclient/tests/volume/v1/test_volume.py index 4e033dfeea..554e2b2a36 100644 --- a/openstackclient/tests/volume/v1/test_volume.py +++ b/openstackclient/tests/volume/v1/test_volume.py @@ -105,7 +105,7 @@ def test_volume_create_min_options(self): ) self.assertEqual(columns, collist) datalist = ( - 'detatched', + 'detached', volume_fakes.volume_description, volume_fakes.volume_name, volume_fakes.volume_id, @@ -181,7 +181,7 @@ def test_volume_create_user_project_id(self): ) self.assertEqual(columns, collist) datalist = ( - 'detatched', + 'detached', volume_fakes.volume_description, volume_fakes.volume_name, volume_fakes.volume_id, @@ -257,7 +257,7 @@ def test_volume_create_user_project_name(self): ) self.assertEqual(columns, collist) datalist = ( - 'detatched', + 'detached', volume_fakes.volume_description, volume_fakes.volume_name, volume_fakes.volume_id, From ecc4fb330dea487b7ac86050eb971d97cea895d6 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Tue, 21 Jan 2014 10:45:52 -0700 Subject: [PATCH 0056/3494] Glance client no longer isa http client If the client has-a http_client, then is must not be an is-a. This has been tested with the current version of glanceclient and the master branch. Closes-Bug: #1269821 Change-Id: I14d67eb094bfb4c2dbc07106343488298b6a9409 --- openstackclient/image/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstackclient/image/client.py b/openstackclient/image/client.py index 9edffded90..ba48a0e967 100644 --- a/openstackclient/image/client.py +++ b/openstackclient/image/client.py @@ -73,7 +73,7 @@ class Client_v1(gc_v1_client.Client): def __init__(self, *args, **kwargs): super(Client_v1, self).__init__(*args, **kwargs) - self.images = ImageManager_v1(self) + self.images = ImageManager_v1(getattr(self, 'http_client', self)) class ImageManager_v1(gc_v1_images.ImageManager): From 380d78c8564ca81c60d46176af622fd92a7b1582 Mon Sep 17 00:00:00 2001 From: Jeremy Stanley Date: Mon, 10 Feb 2014 03:10:52 +0000 Subject: [PATCH 0057/3494] Remove tox locale overrides * tox.ini: The LANG, LANGUAGE and LC_ALL environment overrides were introduced originally during the testr migration in an attempt to be conservative about the possibility that locale settings in the calling environment could cause consistency problems for test runs. In actuality, this should be unnecessary and any place where it does cause issues ought to be considered an actual bug. Also, having these in the configuration actively causes older pip to have problems with non-ASCII content in some package metadata files under Python 3, so drop it now. Change-Id: I89ff5c22be053f09defb04b3ec589d74bffcae9d Closes-Bug: #1277495 --- tox.ini | 3 --- 1 file changed, 3 deletions(-) diff --git a/tox.ini b/tox.ini index f6a5c338f9..751cbd6820 100644 --- a/tox.ini +++ b/tox.ini @@ -7,9 +7,6 @@ skipdist = True usedevelop = True install_command = pip install -U {opts} {packages} setenv = VIRTUAL_ENV={envdir} - LANG=en_US.UTF-8 - LANGUAGE=en_US:en - LC_ALL=C deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = python setup.py testr --testr-args='{posargs}' From 3a5abf743c66f36faf45f710720fe28546b04ea2 Mon Sep 17 00:00:00 2001 From: Cyril Roelandt Date: Tue, 11 Feb 2014 02:22:20 +0100 Subject: [PATCH 0058/3494] Use six.iteritems() rather than dict.iteritems() This is compatible with both Python 2 and 3. Change-Id: I6fe3e9bf9ece699badbdb9933118af90642a91e9 --- openstackclient/tests/fakes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openstackclient/tests/fakes.py b/openstackclient/tests/fakes.py index 01214243b8..4c50c0be88 100644 --- a/openstackclient/tests/fakes.py +++ b/openstackclient/tests/fakes.py @@ -13,6 +13,7 @@ # under the License. # +import six import sys @@ -68,7 +69,7 @@ def __init__(self, manager, info, loaded=False): self._loaded = loaded def _add_details(self, info): - for (k, v) in info.iteritems(): + for (k, v) in six.iteritems(info): setattr(self, k, v) def __repr__(self): From eaa4c3e1a6a4d2fd3139695db88a9f28d64a506f Mon Sep 17 00:00:00 2001 From: Cyril Roelandt Date: Tue, 11 Feb 2014 15:44:54 +0100 Subject: [PATCH 0059/3494] Python 3: fix a syntax error "raise AttributeError, name" is invalid in Python 3. Change-Id: Id61bd3747f49c2bd810cbfeae56506e7ed9d2bd0 --- openstackclient/identity/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstackclient/identity/client.py b/openstackclient/identity/client.py index b19388ccb2..a71dfc7add 100644 --- a/openstackclient/identity/client.py +++ b/openstackclient/identity/client.py @@ -70,4 +70,4 @@ def __getattr__(self, name): if name == "projects": return self.tenants else: - raise AttributeError, name + raise AttributeError(name) From 9dc3eb5b18612831cba7f44b592481056635a476 Mon Sep 17 00:00:00 2001 From: Cyril Roelandt Date: Tue, 11 Feb 2014 17:31:37 +0100 Subject: [PATCH 0060/3494] FakeResponse: use a default status code When running some tests from test_restapi.py, the following error happens: TypeError: unorderable types: NoneType() < int() In Python 2, comparing NoneType and integers is possible: >>> None < 2 True But in Python 3, it's not allowed. Fix this by using a default status code. Change-Id: Ic0fad5c68f3bf2dd8a2b98423549903f982192c9 --- openstackclient/tests/common/test_restapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstackclient/tests/common/test_restapi.py b/openstackclient/tests/common/test_restapi.py index c1e02fcbcf..2c1d5ddd70 100644 --- a/openstackclient/tests/common/test_restapi.py +++ b/openstackclient/tests/common/test_restapi.py @@ -55,7 +55,7 @@ class FakeResponse(requests.Response): - def __init__(self, headers={}, status_code=None, data=None, encoding=None): + def __init__(self, headers={}, status_code=200, data=None, encoding=None): super(FakeResponse, self).__init__() self.status_code = status_code From d8bdd2b5ed2c890df84718be6f977a8d93eddb86 Mon Sep 17 00:00:00 2001 From: tanlin Date: Thu, 13 Feb 2014 16:42:04 +0800 Subject: [PATCH 0061/3494] Rename Openstack to OpenStack Change-Id: I9e5b245141290a4b642900fbc46b98bd4f44c321 --- openstackclient/common/openstackkeyring.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openstackclient/common/openstackkeyring.py b/openstackclient/common/openstackkeyring.py index e60ce71ca6..bf9d5b7173 100644 --- a/openstackclient/common/openstackkeyring.py +++ b/openstackclient/common/openstackkeyring.py @@ -13,7 +13,7 @@ # under the License. # -"""Keyring backend for Openstack, to store encrypted password in a file.""" +"""Keyring backend for OpenStack, to store encrypted password in a file.""" from Crypto.Cipher import AES @@ -24,8 +24,8 @@ KEYRING_FILE = os.path.join(os.path.expanduser('~'), '.openstack-keyring.cfg') -class OpenstackKeyring(keyring.backends.file.BaseKeyring): - """Openstack Keyring to store encrypted password.""" +class OpenStackKeyring(keyring.backends.file.BaseKeyring): + """OpenStack Keyring to store encrypted password.""" filename = KEYRING_FILE def supported(self): @@ -56,5 +56,5 @@ def decrypt(self, password_encrypted): def os_keyring(): """Initialize the openstack keyring.""" - ring = 'openstackclient.common.openstackkeyring.OpenstackKeyring' + ring = 'openstackclient.common.openstackkeyring.OpenStackKeyring' return keyring.core.load_keyring(None, ring) From 5f9e7d09cb0c6efd01816253611092c1bbc51495 Mon Sep 17 00:00:00 2001 From: Cyril Roelandt Date: Tue, 18 Feb 2014 00:49:10 +0100 Subject: [PATCH 0062/3494] Python 3: the content of a FakeResponse must be bytes Encode '_content' if necessary. Change-Id: I25c1e1cd5330f0519bf062be840045d0ef520b28 --- openstackclient/tests/common/test_restapi.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openstackclient/tests/common/test_restapi.py b/openstackclient/tests/common/test_restapi.py index 2c1d5ddd70..291818c108 100644 --- a/openstackclient/tests/common/test_restapi.py +++ b/openstackclient/tests/common/test_restapi.py @@ -19,6 +19,7 @@ import mock import requests +import six from openstackclient.common import restapi from openstackclient.tests import utils @@ -62,6 +63,8 @@ def __init__(self, headers={}, status_code=200, data=None, encoding=None): self.headers.update(headers) self._content = json.dumps(data) + if not isinstance(self._content, six.binary_type): + self._content = self._content.encode() @mock.patch('openstackclient.common.restapi.requests.Session') From 99cea54741fa9364bd1790cdc0bc85e1ba19ef75 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 8 Jan 2014 10:42:49 -0600 Subject: [PATCH 0063/3494] Update oslo incubator bits * update gettextutils.py, strutils.py, install_venv_common.py * remove cfg.py, openstackkeyring oslo-incubator commit 630d3959b9d001ca18bd2ed1cf757f2eb44a336f Change-Id: I0ae9b9dc72ec88ed64a8c353b9c51734ee2cd24c --- openstack-common.conf | 2 - openstackclient/openstack/common/cfg.py | 1731 ----------------- .../openstack/common/gettextutils.py | 497 +++-- openstackclient/openstack/common/strutils.py | 104 +- tools/install_venv_common.py | 2 - 5 files changed, 368 insertions(+), 1968 deletions(-) delete mode 100644 openstackclient/openstack/common/cfg.py diff --git a/openstack-common.conf b/openstack-common.conf index 5e55d5867c..2b26cf209b 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -1,10 +1,8 @@ [DEFAULT] # The list of modules to copy from openstack-common -module=cfg module=iniparser module=install_venv_common -module=openstackkeyring module=strutils # The base module to hold the copy of openstack.common diff --git a/openstackclient/openstack/common/cfg.py b/openstackclient/openstack/common/cfg.py deleted file mode 100644 index cba3145a59..0000000000 --- a/openstackclient/openstack/common/cfg.py +++ /dev/null @@ -1,1731 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 Red Hat, Inc. -# -# 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. - -r""" -Configuration options which may be set on the command line or in config files. - -The schema for each option is defined using the Opt sub-classes, e.g.: - -:: - - common_opts = [ - cfg.StrOpt('bind_host', - default='0.0.0.0', - help='IP address to listen on'), - cfg.IntOpt('bind_port', - default=9292, - help='Port number to listen on') - ] - -Options can be strings, integers, floats, booleans, lists or 'multi strings':: - - enabled_apis_opt = cfg.ListOpt('enabled_apis', - default=['ec2', 'osapi_compute'], - help='List of APIs to enable by default') - - DEFAULT_EXTENSIONS = [ - 'nova.api.openstack.compute.contrib.standard_extensions' - ] - osapi_compute_extension_opt = cfg.MultiStrOpt('osapi_compute_extension', - default=DEFAULT_EXTENSIONS) - -Option schemas are registered with the config manager at runtime, but before -the option is referenced:: - - class ExtensionManager(object): - - enabled_apis_opt = cfg.ListOpt(...) - - def __init__(self, conf): - self.conf = conf - self.conf.register_opt(enabled_apis_opt) - ... - - def _load_extensions(self): - for ext_factory in self.conf.osapi_compute_extension: - .... - -A common usage pattern is for each option schema to be defined in the module or -class which uses the option:: - - opts = ... - - def add_common_opts(conf): - conf.register_opts(opts) - - def get_bind_host(conf): - return conf.bind_host - - def get_bind_port(conf): - return conf.bind_port - -An option may optionally be made available via the command line. Such options -must registered with the config manager before the command line is parsed (for -the purposes of --help and CLI arg validation):: - - cli_opts = [ - cfg.BoolOpt('verbose', - short='v', - default=False, - help='Print more verbose output'), - cfg.BoolOpt('debug', - short='d', - default=False, - help='Print debugging output'), - ] - - def add_common_opts(conf): - conf.register_cli_opts(cli_opts) - -The config manager has two CLI options defined by default, --config-file -and --config-dir:: - - class ConfigOpts(object): - - def __call__(self, ...): - - opts = [ - MultiStrOpt('config-file', - ...), - StrOpt('config-dir', - ...), - ] - - self.register_cli_opts(opts) - -Option values are parsed from any supplied config files using -openstack.common.iniparser. If none are specified, a default set is used -e.g. glance-api.conf and glance-common.conf:: - - glance-api.conf: - [DEFAULT] - bind_port = 9292 - - glance-common.conf: - [DEFAULT] - bind_host = 0.0.0.0 - -Option values in config files override those on the command line. Config files -are parsed in order, with values in later files overriding those in earlier -files. - -The parsing of CLI args and config files is initiated by invoking the config -manager e.g.:: - - conf = ConfigOpts() - conf.register_opt(BoolOpt('verbose', ...)) - conf(sys.argv[1:]) - if conf.verbose: - ... - -Options can be registered as belonging to a group:: - - rabbit_group = cfg.OptGroup(name='rabbit', - title='RabbitMQ options') - - rabbit_host_opt = cfg.StrOpt('host', - default='localhost', - help='IP/hostname to listen on'), - rabbit_port_opt = cfg.IntOpt('port', - default=5672, - help='Port number to listen on') - - def register_rabbit_opts(conf): - conf.register_group(rabbit_group) - # options can be registered under a group in either of these ways: - conf.register_opt(rabbit_host_opt, group=rabbit_group) - conf.register_opt(rabbit_port_opt, group='rabbit') - -If it no group attributes are required other than the group name, the group -need not be explicitly registered e.g. - - def register_rabbit_opts(conf): - # The group will automatically be created, equivalent calling:: - # conf.register_group(OptGroup(name='rabbit')) - conf.register_opt(rabbit_port_opt, group='rabbit') - -If no group is specified, options belong to the 'DEFAULT' section of config -files:: - - glance-api.conf: - [DEFAULT] - bind_port = 9292 - ... - - [rabbit] - host = localhost - port = 5672 - use_ssl = False - userid = guest - password = guest - virtual_host = / - -Command-line options in a group are automatically prefixed with the -group name:: - - --rabbit-host localhost --rabbit-port 9999 - -Option values in the default group are referenced as attributes/properties on -the config manager; groups are also attributes on the config manager, with -attributes for each of the options associated with the group:: - - server.start(app, conf.bind_port, conf.bind_host, conf) - - self.connection = kombu.connection.BrokerConnection( - hostname=conf.rabbit.host, - port=conf.rabbit.port, - ...) - -Option values may reference other values using PEP 292 string substitution:: - - opts = [ - cfg.StrOpt('state_path', - default=os.path.join(os.path.dirname(__file__), '../'), - help='Top-level directory for maintaining nova state'), - cfg.StrOpt('sqlite_db', - default='nova.sqlite', - help='file name for sqlite'), - cfg.StrOpt('sql_connection', - default='sqlite:///$state_path/$sqlite_db', - help='connection string for sql database'), - ] - -Note that interpolation can be avoided by using '$$'. - -Options may be declared as required so that an error is raised if the user -does not supply a value for the option. - -Options may be declared as secret so that their values are not leaked into -log files:: - - opts = [ - cfg.StrOpt('s3_store_access_key', secret=True), - cfg.StrOpt('s3_store_secret_key', secret=True), - ... - ] - -This module also contains a global instance of the ConfigOpts class -in order to support a common usage pattern in OpenStack:: - - from openstackclient.openstack.common import cfg - - opts = [ - cfg.StrOpt('bind_host', default='0.0.0.0'), - cfg.IntOpt('bind_port', default=9292), - ] - - CONF = cfg.CONF - CONF.register_opts(opts) - - def start(server, app): - server.start(app, CONF.bind_port, CONF.bind_host) - -Positional command line arguments are supported via a 'positional' Opt -constructor argument:: - - >>> conf = ConfigOpts() - >>> conf.register_cli_opt(MultiStrOpt('bar', positional=True)) - True - >>> conf(['a', 'b']) - >>> conf.bar - ['a', 'b'] - -It is also possible to use argparse "sub-parsers" to parse additional -command line arguments using the SubCommandOpt class: - - >>> def add_parsers(subparsers): - ... list_action = subparsers.add_parser('list') - ... list_action.add_argument('id') - ... - >>> conf = ConfigOpts() - >>> conf.register_cli_opt(SubCommandOpt('action', handler=add_parsers)) - True - >>> conf(args=['list', '10']) - >>> conf.action.name, conf.action.id - ('list', '10') - -""" - -import argparse -import collections -import copy -import functools -import glob -import os -import string -import sys - -from openstackclient.openstack.common import iniparser - - -class Error(Exception): - """Base class for cfg exceptions.""" - - def __init__(self, msg=None): - self.msg = msg - - def __str__(self): - return self.msg - - -class ArgsAlreadyParsedError(Error): - """Raised if a CLI opt is registered after parsing.""" - - def __str__(self): - ret = "arguments already parsed" - if self.msg: - ret += ": " + self.msg - return ret - - -class NoSuchOptError(Error, AttributeError): - """Raised if an opt which doesn't exist is referenced.""" - - def __init__(self, opt_name, group=None): - self.opt_name = opt_name - self.group = group - - def __str__(self): - if self.group is None: - return "no such option: %s" % self.opt_name - else: - return "no such option in group %s: %s" % (self.group.name, - self.opt_name) - - -class NoSuchGroupError(Error): - """Raised if a group which doesn't exist is referenced.""" - - def __init__(self, group_name): - self.group_name = group_name - - def __str__(self): - return "no such group: %s" % self.group_name - - -class DuplicateOptError(Error): - """Raised if multiple opts with the same name are registered.""" - - def __init__(self, opt_name): - self.opt_name = opt_name - - def __str__(self): - return "duplicate option: %s" % self.opt_name - - -class RequiredOptError(Error): - """Raised if an option is required but no value is supplied by the user.""" - - def __init__(self, opt_name, group=None): - self.opt_name = opt_name - self.group = group - - def __str__(self): - if self.group is None: - return "value required for option: %s" % self.opt_name - else: - return "value required for option: %s.%s" % (self.group.name, - self.opt_name) - - -class TemplateSubstitutionError(Error): - """Raised if an error occurs substituting a variable in an opt value.""" - - def __str__(self): - return "template substitution error: %s" % self.msg - - -class ConfigFilesNotFoundError(Error): - """Raised if one or more config files are not found.""" - - def __init__(self, config_files): - self.config_files = config_files - - def __str__(self): - return ('Failed to read some config files: %s' % - string.join(self.config_files, ',')) - - -class ConfigFileParseError(Error): - """Raised if there is an error parsing a config file.""" - - def __init__(self, config_file, msg): - self.config_file = config_file - self.msg = msg - - def __str__(self): - return 'Failed to parse %s: %s' % (self.config_file, self.msg) - - -class ConfigFileValueError(Error): - """Raised if a config file value does not match its opt type.""" - pass - - -def _fixpath(p): - """Apply tilde expansion and absolutization to a path.""" - return os.path.abspath(os.path.expanduser(p)) - - -def _get_config_dirs(project=None): - """Return a list of directors where config files may be located. - - :param project: an optional project name - - If a project is specified, following directories are returned:: - - ~/.${project}/ - ~/ - /etc/${project}/ - /etc/ - - Otherwise, these directories:: - - ~/ - /etc/ - """ - cfg_dirs = [ - _fixpath(os.path.join('~', '.' + project)) if project else None, - _fixpath('~'), - os.path.join('/etc', project) if project else None, - '/etc' - ] - - return filter(bool, cfg_dirs) - - -def _search_dirs(dirs, basename, extension=""): - """Search a list of directories for a given filename. - - Iterator over the supplied directories, returning the first file - found with the supplied name and extension. - - :param dirs: a list of directories - :param basename: the filename, e.g. 'glance-api' - :param extension: the file extension, e.g. '.conf' - :returns: the path to a matching file, or None - """ - for d in dirs: - path = os.path.join(d, '%s%s' % (basename, extension)) - if os.path.exists(path): - return path - - -def find_config_files(project=None, prog=None, extension='.conf'): - """Return a list of default configuration files. - - :param project: an optional project name - :param prog: the program name, defaulting to the basename of sys.argv[0] - :param extension: the type of the config file - - We default to two config files: [${project}.conf, ${prog}.conf] - - And we look for those config files in the following directories:: - - ~/.${project}/ - ~/ - /etc/${project}/ - /etc/ - - We return an absolute path for (at most) one of each the default config - files, for the topmost directory it exists in. - - For example, if project=foo, prog=bar and /etc/foo/foo.conf, /etc/bar.conf - and ~/.foo/bar.conf all exist, then we return ['/etc/foo/foo.conf', - '~/.foo/bar.conf'] - - If no project name is supplied, we only look for ${prog.conf}. - """ - if prog is None: - prog = os.path.basename(sys.argv[0]) - - cfg_dirs = _get_config_dirs(project) - - config_files = [] - if project: - config_files.append(_search_dirs(cfg_dirs, project, extension)) - config_files.append(_search_dirs(cfg_dirs, prog, extension)) - - return filter(bool, config_files) - - -def _is_opt_registered(opts, opt): - """Check whether an opt with the same name is already registered. - - The same opt may be registered multiple times, with only the first - registration having any effect. However, it is an error to attempt - to register a different opt with the same name. - - :param opts: the set of opts already registered - :param opt: the opt to be registered - :returns: True if the opt was previously registered, False otherwise - :raises: DuplicateOptError if a naming conflict is detected - """ - if opt.dest in opts: - if opts[opt.dest]['opt'] != opt: - raise DuplicateOptError(opt.name) - return True - else: - return False - - -def set_defaults(opts, **kwargs): - for opt in opts: - if opt.dest in kwargs: - opt.default = kwargs[opt.dest] - break - - -class Opt(object): - - """Base class for all configuration options. - - An Opt object has no public methods, but has a number of public string - properties: - - name: - the name of the option, which may include hyphens - dest: - the (hyphen-less) ConfigOpts property which contains the option value - short: - a single character CLI option name - default: - the default value of the option - positional: - True if the option is a positional CLI argument - metavar: - the name shown as the argument to a CLI option in --help output - help: - an string explaining how the options value is used - """ - multi = False - - def __init__(self, name, dest=None, short=None, default=None, - positional=False, metavar=None, help=None, - secret=False, required=False, deprecated_name=None): - """Construct an Opt object. - - The only required parameter is the option's name. However, it is - common to also supply a default and help string for all options. - - :param name: the option's name - :param dest: the name of the corresponding ConfigOpts property - :param short: a single character CLI option name - :param default: the default value of the option - :param positional: True if the option is a positional CLI argument - :param metavar: the option argument to show in --help - :param help: an explanation of how the option is used - :param secret: true iff the value should be obfuscated in log output - :param required: true iff a value must be supplied for this option - :param deprecated_name: deprecated name option. Acts like an alias - """ - self.name = name - if dest is None: - self.dest = self.name.replace('-', '_') - else: - self.dest = dest - self.short = short - self.default = default - self.positional = positional - self.metavar = metavar - self.help = help - self.secret = secret - self.required = required - if deprecated_name is not None: - self.deprecated_name = deprecated_name.replace('-', '_') - else: - self.deprecated_name = None - - def __ne__(self, another): - return vars(self) != vars(another) - - def _get_from_config_parser(self, cparser, section): - """Retrieves the option value from a MultiConfigParser object. - - This is the method ConfigOpts uses to look up the option value from - config files. Most opt types override this method in order to perform - type appropriate conversion of the returned value. - - :param cparser: a ConfigParser object - :param section: a section name - """ - return self._cparser_get_with_deprecated(cparser, section) - - def _cparser_get_with_deprecated(self, cparser, section): - """If cannot find option as dest try deprecated_name alias.""" - if self.deprecated_name is not None: - return cparser.get(section, [self.dest, self.deprecated_name]) - return cparser.get(section, [self.dest]) - - def _add_to_cli(self, parser, group=None): - """Makes the option available in the command line interface. - - This is the method ConfigOpts uses to add the opt to the CLI interface - as appropriate for the opt type. Some opt types may extend this method, - others may just extend the helper methods it uses. - - :param parser: the CLI option parser - :param group: an optional OptGroup object - """ - container = self._get_argparse_container(parser, group) - kwargs = self._get_argparse_kwargs(group) - prefix = self._get_argparse_prefix('', group) - self._add_to_argparse(container, self.name, self.short, kwargs, prefix, - self.positional, self.deprecated_name) - - def _add_to_argparse(self, container, name, short, kwargs, prefix='', - positional=False, deprecated_name=None): - """Add an option to an argparse parser or group. - - :param container: an argparse._ArgumentGroup object - :param name: the opt name - :param short: the short opt name - :param kwargs: the keyword arguments for add_argument() - :param prefix: an optional prefix to prepend to the opt name - :param position: whether the optional is a positional CLI argument - :raises: DuplicateOptError if a naming confict is detected - """ - def hyphen(arg): - return arg if not positional else '' - - args = [hyphen('--') + prefix + name] - if short: - args.append(hyphen('-') + short) - if deprecated_name: - args.append(hyphen('--') + prefix + deprecated_name) - - try: - container.add_argument(*args, **kwargs) - except argparse.ArgumentError as e: - raise DuplicateOptError(e) - - def _get_argparse_container(self, parser, group): - """Returns an argparse._ArgumentGroup. - - :param parser: an argparse.ArgumentParser - :param group: an (optional) OptGroup object - :returns: an argparse._ArgumentGroup if group is given, else parser - """ - if group is not None: - return group._get_argparse_group(parser) - else: - return parser - - def _get_argparse_kwargs(self, group, **kwargs): - """Build a dict of keyword arguments for argparse's add_argument(). - - Most opt types extend this method to customize the behaviour of the - options added to argparse. - - :param group: an optional group - :param kwargs: optional keyword arguments to add to - :returns: a dict of keyword arguments - """ - if not self.positional: - dest = self.dest - if group is not None: - dest = group.name + '_' + dest - kwargs['dest'] = dest - else: - kwargs['nargs'] = '?' - kwargs.update({'default': None, - 'metavar': self.metavar, - 'help': self.help, }) - return kwargs - - def _get_argparse_prefix(self, prefix, group): - """Build a prefix for the CLI option name, if required. - - CLI options in a group are prefixed with the group's name in order - to avoid conflicts between similarly named options in different - groups. - - :param prefix: an existing prefix to append to (e.g. 'no' or '') - :param group: an optional OptGroup object - :returns: a CLI option prefix including the group name, if appropriate - """ - if group is not None: - return group.name + '-' + prefix - else: - return prefix - - -class StrOpt(Opt): - """ - String opts do not have their values transformed and are returned as - str objects. - """ - pass - - -class BoolOpt(Opt): - - """ - Bool opts are set to True or False on the command line using --optname or - --noopttname respectively. - - In config files, boolean values are case insensitive and can be set using - 1/0, yes/no, true/false or on/off. - """ - - _boolean_states = {'1': True, 'yes': True, 'true': True, 'on': True, - '0': False, 'no': False, 'false': False, 'off': False} - - def __init__(self, *args, **kwargs): - if 'positional' in kwargs: - raise ValueError('positional boolean args not supported') - super(BoolOpt, self).__init__(*args, **kwargs) - - def _get_from_config_parser(self, cparser, section): - """Retrieve the opt value as a boolean from ConfigParser.""" - def convert_bool(v): - value = self._boolean_states.get(v.lower()) - if value is None: - raise ValueError('Unexpected boolean value %r' % v) - - return value - - return [convert_bool(v) for v in - self._cparser_get_with_deprecated(cparser, section)] - - def _add_to_cli(self, parser, group=None): - """Extends the base class method to add the --nooptname option.""" - super(BoolOpt, self)._add_to_cli(parser, group) - self._add_inverse_to_argparse(parser, group) - - def _add_inverse_to_argparse(self, parser, group): - """Add the --nooptname option to the option parser.""" - container = self._get_argparse_container(parser, group) - kwargs = self._get_argparse_kwargs(group, action='store_false') - prefix = self._get_argparse_prefix('no', group) - kwargs["help"] = "The inverse of --" + self.name - self._add_to_argparse(container, self.name, None, kwargs, prefix, - self.positional, self.deprecated_name) - - def _get_argparse_kwargs(self, group, action='store_true', **kwargs): - """Extends the base argparse keyword dict for boolean options.""" - - kwargs = super(BoolOpt, self)._get_argparse_kwargs(group, **kwargs) - - # metavar has no effect for BoolOpt - if 'metavar' in kwargs: - del kwargs['metavar'] - - if action != 'store_true': - action = 'store_false' - - kwargs['action'] = action - - return kwargs - - -class IntOpt(Opt): - - """Int opt values are converted to integers using the int() builtin.""" - - def _get_from_config_parser(self, cparser, section): - """Retrieve the opt value as a integer from ConfigParser.""" - return [int(v) for v in self._cparser_get_with_deprecated(cparser, - section)] - - def _get_argparse_kwargs(self, group, **kwargs): - """Extends the base argparse keyword dict for integer options.""" - return super(IntOpt, - self)._get_argparse_kwargs(group, type=int, **kwargs) - - -class FloatOpt(Opt): - - """Float opt values are converted to floats using the float() builtin.""" - - def _get_from_config_parser(self, cparser, section): - """Retrieve the opt value as a float from ConfigParser.""" - return [float(v) for v in - self._cparser_get_with_deprecated(cparser, section)] - - def _get_argparse_kwargs(self, group, **kwargs): - """Extends the base argparse keyword dict for float options.""" - return super(FloatOpt, self)._get_argparse_kwargs(group, - type=float, **kwargs) - - -class ListOpt(Opt): - - """ - List opt values are simple string values separated by commas. The opt value - is a list containing these strings. - """ - - class _StoreListAction(argparse.Action): - """ - An argparse action for parsing an option value into a list. - """ - def __call__(self, parser, namespace, values, option_string=None): - if values is not None: - values = [a.strip() for a in values.split(',')] - setattr(namespace, self.dest, values) - - def _get_from_config_parser(self, cparser, section): - """Retrieve the opt value as a list from ConfigParser.""" - return [[a.strip() for a in v.split(',')] for v in - self._cparser_get_with_deprecated(cparser, section)] - - def _get_argparse_kwargs(self, group, **kwargs): - """Extends the base argparse keyword dict for list options.""" - return Opt._get_argparse_kwargs(self, - group, - action=ListOpt._StoreListAction, - **kwargs) - - -class MultiStrOpt(Opt): - - """ - Multistr opt values are string opts which may be specified multiple times. - The opt value is a list containing all the string values specified. - """ - multi = True - - def _get_argparse_kwargs(self, group, **kwargs): - """Extends the base argparse keyword dict for multi str options.""" - kwargs = super(MultiStrOpt, self)._get_argparse_kwargs(group) - if not self.positional: - kwargs['action'] = 'append' - else: - kwargs['nargs'] = '*' - return kwargs - - def _cparser_get_with_deprecated(self, cparser, section): - """If cannot find option as dest try deprecated_name alias.""" - if self.deprecated_name is not None: - return cparser.get(section, [self.dest, self.deprecated_name], - multi=True) - return cparser.get(section, [self.dest], multi=True) - - -class SubCommandOpt(Opt): - - """ - Sub-command options allow argparse sub-parsers to be used to parse - additional command line arguments. - - The handler argument to the SubCommandOpt contructor is a callable - which is supplied an argparse subparsers object. Use this handler - callable to add sub-parsers. - - The opt value is SubCommandAttr object with the name of the chosen - sub-parser stored in the 'name' attribute and the values of other - sub-parser arguments available as additional attributes. - """ - - def __init__(self, name, dest=None, handler=None, - title=None, description=None, help=None): - """Construct an sub-command parsing option. - - This behaves similarly to other Opt sub-classes but adds a - 'handler' argument. The handler is a callable which is supplied - an subparsers object when invoked. The add_parser() method on - this subparsers object can be used to register parsers for - sub-commands. - - :param name: the option's name - :param dest: the name of the corresponding ConfigOpts property - :param title: title of the sub-commands group in help output - :param description: description of the group in help output - :param help: a help string giving an overview of available sub-commands - """ - super(SubCommandOpt, self).__init__(name, dest=dest, help=help) - self.handler = handler - self.title = title - self.description = description - - def _add_to_cli(self, parser, group=None): - """Add argparse sub-parsers and invoke the handler method.""" - dest = self.dest - if group is not None: - dest = group.name + '_' + dest - - subparsers = parser.add_subparsers(dest=dest, - title=self.title, - description=self.description, - help=self.help) - - if self.handler is not None: - self.handler(subparsers) - - -class OptGroup(object): - - """ - Represents a group of opts. - - CLI opts in the group are automatically prefixed with the group name. - - Each group corresponds to a section in config files. - - An OptGroup object has no public methods, but has a number of public string - properties: - - name: - the name of the group - title: - the group title as displayed in --help - help: - the group description as displayed in --help - """ - - def __init__(self, name, title=None, help=None): - """Constructs an OptGroup object. - - :param name: the group name - :param title: the group title for --help - :param help: the group description for --help - """ - self.name = name - if title is None: - self.title = "%s options" % title - else: - self.title = title - self.help = help - - self._opts = {} # dict of dicts of (opt:, override:, default:) - self._argparse_group = None - - def _register_opt(self, opt, cli=False): - """Add an opt to this group. - - :param opt: an Opt object - :param cli: whether this is a CLI option - :returns: False if previously registered, True otherwise - :raises: DuplicateOptError if a naming conflict is detected - """ - if _is_opt_registered(self._opts, opt): - return False - - self._opts[opt.dest] = {'opt': opt, 'cli': cli} - - return True - - def _unregister_opt(self, opt): - """Remove an opt from this group. - - :param opt: an Opt object - """ - if opt.dest in self._opts: - del self._opts[opt.dest] - - def _get_argparse_group(self, parser): - if self._argparse_group is None: - """Build an argparse._ArgumentGroup for this group.""" - self._argparse_group = parser.add_argument_group(self.title, - self.help) - return self._argparse_group - - def _clear(self): - """Clear this group's option parsing state.""" - self._argparse_group = None - - -class ParseError(iniparser.ParseError): - def __init__(self, msg, lineno, line, filename): - super(ParseError, self).__init__(msg, lineno, line) - self.filename = filename - - def __str__(self): - return 'at %s:%d, %s: %r' % (self.filename, self.lineno, - self.msg, self.line) - - -class ConfigParser(iniparser.BaseParser): - def __init__(self, filename, sections): - super(ConfigParser, self).__init__() - self.filename = filename - self.sections = sections - self.section = None - - def parse(self): - with open(self.filename) as f: - return super(ConfigParser, self).parse(f) - - def new_section(self, section): - self.section = section - self.sections.setdefault(self.section, {}) - - def assignment(self, key, value): - if not self.section: - raise self.error_no_section() - - self.sections[self.section].setdefault(key, []) - self.sections[self.section][key].append('\n'.join(value)) - - def parse_exc(self, msg, lineno, line=None): - return ParseError(msg, lineno, line, self.filename) - - def error_no_section(self): - return self.parse_exc('Section must be started before assignment', - self.lineno) - - -class MultiConfigParser(object): - def __init__(self): - self.parsed = [] - - def read(self, config_files): - read_ok = [] - - for filename in config_files: - sections = {} - parser = ConfigParser(filename, sections) - - try: - parser.parse() - except IOError: - continue - self.parsed.insert(0, sections) - read_ok.append(filename) - - return read_ok - - def get(self, section, names, multi=False): - rvalue = [] - for sections in self.parsed: - if section not in sections: - continue - for name in names: - if name in sections[section]: - if multi: - rvalue = sections[section][name] + rvalue - else: - return sections[section][name] - if multi and rvalue != []: - return rvalue - raise KeyError - - -class ConfigOpts(collections.Mapping): - - """ - Config options which may be set on the command line or in config files. - - ConfigOpts is a configuration option manager with APIs for registering - option schemas, grouping options, parsing option values and retrieving - the values of options. - """ - - def __init__(self): - """Construct a ConfigOpts object.""" - self._opts = {} # dict of dicts of (opt:, override:, default:) - self._groups = {} - - self._args = None - - self._oparser = None - self._cparser = None - self._cli_values = {} - self.__cache = {} - self._config_opts = [] - - def _pre_setup(self, project, prog, version, usage, default_config_files): - """Initialize a ConfigCliParser object for option parsing.""" - - if prog is None: - prog = os.path.basename(sys.argv[0]) - - if default_config_files is None: - default_config_files = find_config_files(project, prog) - - self._oparser = argparse.ArgumentParser(prog=prog, usage=usage) - self._oparser.add_argument('--version', - action='version', - version=version) - - return prog, default_config_files - - def _setup(self, project, prog, version, usage, default_config_files): - """Initialize a ConfigOpts object for option parsing.""" - - self._config_opts = [ - MultiStrOpt('config-file', - default=default_config_files, - metavar='PATH', - help='Path to a config file to use. Multiple config ' - 'files can be specified, with values in later ' - 'files taking precedence. The default files ' - ' used are: %s' % (default_config_files, )), - StrOpt('config-dir', - metavar='DIR', - help='Path to a config directory to pull *.conf ' - 'files from. This file set is sorted, so as to ' - 'provide a predictable parse order if individual ' - 'options are over-ridden. The set is parsed after ' - 'the file(s), if any, specified via --config-file, ' - 'hence over-ridden options in the directory take ' - 'precedence.'), - ] - self.register_cli_opts(self._config_opts) - - self.project = project - self.prog = prog - self.version = version - self.usage = usage - self.default_config_files = default_config_files - - def __clear_cache(f): - @functools.wraps(f) - def __inner(self, *args, **kwargs): - if kwargs.pop('clear_cache', True): - self.__cache.clear() - return f(self, *args, **kwargs) - - return __inner - - def __call__(self, - args=None, - project=None, - prog=None, - version=None, - usage=None, - default_config_files=None): - """Parse command line arguments and config files. - - Calling a ConfigOpts object causes the supplied command line arguments - and config files to be parsed, causing opt values to be made available - as attributes of the object. - - The object may be called multiple times, each time causing the previous - set of values to be overwritten. - - Automatically registers the --config-file option with either a supplied - list of default config files, or a list from find_config_files(). - - If the --config-dir option is set, any *.conf files from this - directory are pulled in, after all the file(s) specified by the - --config-file option. - - :param args: command line arguments (defaults to sys.argv[1:]) - :param project: the toplevel project name, used to locate config files - :param prog: the name of the program (defaults to sys.argv[0] basename) - :param version: the program version (for --version) - :param usage: a usage string (%prog will be expanded) - :param default_config_files: config files to use by default - :returns: the list of arguments left over after parsing options - :raises: SystemExit, ConfigFilesNotFoundError, ConfigFileParseError, - RequiredOptError, DuplicateOptError - """ - - self.clear() - - prog, default_config_files = self._pre_setup(project, - prog, - version, - usage, - default_config_files) - - self._setup(project, prog, version, usage, default_config_files) - - self._cli_values = self._parse_cli_opts(args) - - self._parse_config_files() - - self._check_required_opts() - - def __getattr__(self, name): - """Look up an option value and perform string substitution. - - :param name: the opt name (or 'dest', more precisely) - :returns: the option value (after string subsititution) or a GroupAttr - :raises: NoSuchOptError,ConfigFileValueError,TemplateSubstitutionError - """ - return self._get(name) - - def __getitem__(self, key): - """Look up an option value and perform string substitution.""" - return self.__getattr__(key) - - def __contains__(self, key): - """Return True if key is the name of a registered opt or group.""" - return key in self._opts or key in self._groups - - def __iter__(self): - """Iterate over all registered opt and group names.""" - for key in self._opts.keys() + self._groups.keys(): - yield key - - def __len__(self): - """Return the number of options and option groups.""" - return len(self._opts) + len(self._groups) - - def reset(self): - """Clear the object state and unset overrides and defaults.""" - self._unset_defaults_and_overrides() - self.clear() - - @__clear_cache - def clear(self): - """Clear the state of the object to before it was called. - - Any subparsers added using the add_cli_subparsers() will also be - removed as a side-effect of this method. - """ - self._args = None - self._cli_values.clear() - self._oparser = argparse.ArgumentParser() - self._cparser = None - self.unregister_opts(self._config_opts) - for group in self._groups.values(): - group._clear() - - @__clear_cache - def register_opt(self, opt, group=None, cli=False): - """Register an option schema. - - Registering an option schema makes any option value which is previously - or subsequently parsed from the command line or config files available - as an attribute of this object. - - :param opt: an instance of an Opt sub-class - :param cli: whether this is a CLI option - :param group: an optional OptGroup object or group name - :return: False if the opt was already register, True otherwise - :raises: DuplicateOptError - """ - if group is not None: - group = self._get_group(group, autocreate=True) - return group._register_opt(opt, cli) - - if _is_opt_registered(self._opts, opt): - return False - - self._opts[opt.dest] = {'opt': opt, 'cli': cli} - - return True - - @__clear_cache - def register_opts(self, opts, group=None): - """Register multiple option schemas at once.""" - for opt in opts: - self.register_opt(opt, group, clear_cache=False) - - @__clear_cache - def register_cli_opt(self, opt, group=None): - """Register a CLI option schema. - - CLI option schemas must be registered before the command line and - config files are parsed. This is to ensure that all CLI options are - show in --help and option validation works as expected. - - :param opt: an instance of an Opt sub-class - :param group: an optional OptGroup object or group name - :return: False if the opt was already register, True otherwise - :raises: DuplicateOptError, ArgsAlreadyParsedError - """ - if self._args is not None: - raise ArgsAlreadyParsedError("cannot register CLI option") - - return self.register_opt(opt, group, cli=True, clear_cache=False) - - @__clear_cache - def register_cli_opts(self, opts, group=None): - """Register multiple CLI option schemas at once.""" - for opt in opts: - self.register_cli_opt(opt, group, clear_cache=False) - - def register_group(self, group): - """Register an option group. - - An option group must be registered before options can be registered - with the group. - - :param group: an OptGroup object - """ - if group.name in self._groups: - return - - self._groups[group.name] = copy.copy(group) - - @__clear_cache - def unregister_opt(self, opt, group=None): - """Unregister an option. - - :param opt: an Opt object - :param group: an optional OptGroup object or group name - :raises: ArgsAlreadyParsedError, NoSuchGroupError - """ - if self._args is not None: - raise ArgsAlreadyParsedError("reset before unregistering options") - - if group is not None: - self._get_group(group)._unregister_opt(opt) - elif opt.dest in self._opts: - del self._opts[opt.dest] - - @__clear_cache - def unregister_opts(self, opts, group=None): - """Unregister multiple CLI option schemas at once.""" - for opt in opts: - self.unregister_opt(opt, group, clear_cache=False) - - def import_opt(self, name, module_str, group=None): - """Import an option definition from a module. - - Import a module and check that a given option is registered. - - This is intended for use with global configuration objects - like cfg.CONF where modules commonly register options with - CONF at module load time. If one module requires an option - defined by another module it can use this method to explicitly - declare the dependency. - - :param name: the name/dest of the opt - :param module_str: the name of a module to import - :param group: an option OptGroup object or group name - :raises: NoSuchOptError, NoSuchGroupError - """ - __import__(module_str) - self._get_opt_info(name, group) - - @__clear_cache - def set_override(self, name, override, group=None): - """Override an opt value. - - Override the command line, config file and default values of a - given option. - - :param name: the name/dest of the opt - :param override: the override value - :param group: an option OptGroup object or group name - :raises: NoSuchOptError, NoSuchGroupError - """ - opt_info = self._get_opt_info(name, group) - opt_info['override'] = override - - @__clear_cache - def set_default(self, name, default, group=None): - """Override an opt's default value. - - Override the default value of given option. A command line or - config file value will still take precedence over this default. - - :param name: the name/dest of the opt - :param default: the default value - :param group: an option OptGroup object or group name - :raises: NoSuchOptError, NoSuchGroupError - """ - opt_info = self._get_opt_info(name, group) - opt_info['default'] = default - - @__clear_cache - def clear_override(self, name, group=None): - """Clear an override an opt value. - - Clear a previously set override of the command line, config file - and default values of a given option. - - :param name: the name/dest of the opt - :param group: an option OptGroup object or group name - :raises: NoSuchOptError, NoSuchGroupError - """ - opt_info = self._get_opt_info(name, group) - opt_info.pop('override', None) - - @__clear_cache - def clear_default(self, name, group=None): - """Clear an override an opt's default value. - - Clear a previously set override of the default value of given option. - - :param name: the name/dest of the opt - :param group: an option OptGroup object or group name - :raises: NoSuchOptError, NoSuchGroupError - """ - opt_info = self._get_opt_info(name, group) - opt_info.pop('default', None) - - def _all_opt_infos(self): - """A generator function for iteration opt infos.""" - for info in self._opts.values(): - yield info, None - for group in self._groups.values(): - for info in group._opts.values(): - yield info, group - - def _all_cli_opts(self): - """A generator function for iterating CLI opts.""" - for info, group in self._all_opt_infos(): - if info['cli']: - yield info['opt'], group - - def _unset_defaults_and_overrides(self): - """Unset any default or override on all options.""" - for info, group in self._all_opt_infos(): - info.pop('default', None) - info.pop('override', None) - - def find_file(self, name): - """Locate a file located alongside the config files. - - Search for a file with the supplied basename in the directories - which we have already loaded config files from and other known - configuration directories. - - The directory, if any, supplied by the config_dir option is - searched first. Then the config_file option is iterated over - and each of the base directories of the config_files values - are searched. Failing both of these, the standard directories - searched by the module level find_config_files() function is - used. The first matching file is returned. - - :param basename: the filename, e.g. 'policy.json' - :returns: the path to a matching file, or None - """ - dirs = [] - if self.config_dir: - dirs.append(_fixpath(self.config_dir)) - - for cf in reversed(self.config_file): - dirs.append(os.path.dirname(_fixpath(cf))) - - dirs.extend(_get_config_dirs(self.project)) - - return _search_dirs(dirs, name) - - def log_opt_values(self, logger, lvl): - """Log the value of all registered opts. - - It's often useful for an app to log its configuration to a log file at - startup for debugging. This method dumps to the entire config state to - the supplied logger at a given log level. - - :param logger: a logging.Logger object - :param lvl: the log level (e.g. logging.DEBUG) arg to logger.log() - """ - logger.log(lvl, "*" * 80) - logger.log(lvl, "Configuration options gathered from:") - logger.log(lvl, "command line args: %s", self._args) - logger.log(lvl, "config files: %s", self.config_file) - logger.log(lvl, "=" * 80) - - def _sanitize(opt, value): - """Obfuscate values of options declared secret""" - return value if not opt.secret else '*' * len(str(value)) - - for opt_name in sorted(self._opts): - opt = self._get_opt_info(opt_name)['opt'] - logger.log(lvl, "%-30s = %s", opt_name, - _sanitize(opt, getattr(self, opt_name))) - - for group_name in self._groups: - group_attr = self.GroupAttr(self, self._get_group(group_name)) - for opt_name in sorted(self._groups[group_name]._opts): - opt = self._get_opt_info(opt_name, group_name)['opt'] - logger.log(lvl, "%-30s = %s", - "%s.%s" % (group_name, opt_name), - _sanitize(opt, getattr(group_attr, opt_name))) - - logger.log(lvl, "*" * 80) - - def print_usage(self, file=None): - """Print the usage message for the current program.""" - self._oparser.print_usage(file) - - def print_help(self, file=None): - """Print the help message for the current program.""" - self._oparser.print_help(file) - - def _get(self, name, group=None): - if isinstance(group, OptGroup): - key = (group.name, name) - else: - key = (group, name) - try: - return self.__cache[key] - except KeyError: - value = self._substitute(self._do_get(name, group)) - self.__cache[key] = value - return value - - def _do_get(self, name, group=None): - """Look up an option value. - - :param name: the opt name (or 'dest', more precisely) - :param group: an OptGroup - :returns: the option value, or a GroupAttr object - :raises: NoSuchOptError, NoSuchGroupError, ConfigFileValueError, - TemplateSubstitutionError - """ - if group is None and name in self._groups: - return self.GroupAttr(self, self._get_group(name)) - - info = self._get_opt_info(name, group) - opt = info['opt'] - - if isinstance(opt, SubCommandOpt): - return self.SubCommandAttr(self, group, opt.dest) - - if 'override' in info: - return info['override'] - - values = [] - if self._cparser is not None: - section = group.name if group is not None else 'DEFAULT' - try: - value = opt._get_from_config_parser(self._cparser, section) - except KeyError: - pass - except ValueError as ve: - raise ConfigFileValueError(str(ve)) - else: - if not opt.multi: - # No need to continue since the last value wins - return value[-1] - values.extend(value) - - name = name if group is None else group.name + '_' + name - value = self._cli_values.get(name) - if value is not None: - if not opt.multi: - return value - - # argparse ignores default=None for nargs='*' - if opt.positional and not value: - value = opt.default - - return value + values - - if values: - return values - - if 'default' in info: - return info['default'] - - return opt.default - - def _substitute(self, value): - """Perform string template substitution. - - Substitute any template variables (e.g. $foo, ${bar}) in the supplied - string value(s) with opt values. - - :param value: the string value, or list of string values - :returns: the substituted string(s) - """ - if isinstance(value, list): - return [self._substitute(i) for i in value] - elif isinstance(value, str): - tmpl = string.Template(value) - return tmpl.safe_substitute(self.StrSubWrapper(self)) - else: - return value - - def _get_group(self, group_or_name, autocreate=False): - """Looks up a OptGroup object. - - Helper function to return an OptGroup given a parameter which can - either be the group's name or an OptGroup object. - - The OptGroup object returned is from the internal dict of OptGroup - objects, which will be a copy of any OptGroup object that users of - the API have access to. - - :param group_or_name: the group's name or the OptGroup object itself - :param autocreate: whether to auto-create the group if it's not found - :raises: NoSuchGroupError - """ - group = group_or_name if isinstance(group_or_name, OptGroup) else None - group_name = group.name if group else group_or_name - - if group_name not in self._groups: - if group is not None or not autocreate: - raise NoSuchGroupError(group_name) - - self.register_group(OptGroup(name=group_name)) - - return self._groups[group_name] - - def _get_opt_info(self, opt_name, group=None): - """Return the (opt, override, default) dict for an opt. - - :param opt_name: an opt name/dest - :param group: an optional group name or OptGroup object - :raises: NoSuchOptError, NoSuchGroupError - """ - if group is None: - opts = self._opts - else: - group = self._get_group(group) - opts = group._opts - - if opt_name not in opts: - raise NoSuchOptError(opt_name, group) - - return opts[opt_name] - - def _parse_config_files(self): - """Parse the config files from --config-file and --config-dir. - - :raises: ConfigFilesNotFoundError, ConfigFileParseError - """ - config_files = list(self.config_file) - - if self.config_dir: - config_dir_glob = os.path.join(self.config_dir, '*.conf') - config_files += sorted(glob.glob(config_dir_glob)) - - config_files = [_fixpath(p) for p in config_files] - - self._cparser = MultiConfigParser() - - try: - read_ok = self._cparser.read(config_files) - except iniparser.ParseError as pe: - raise ConfigFileParseError(pe.filename, str(pe)) - - if read_ok != config_files: - not_read_ok = filter(lambda f: f not in read_ok, config_files) - raise ConfigFilesNotFoundError(not_read_ok) - - def _check_required_opts(self): - """Check that all opts marked as required have values specified. - - :raises: RequiredOptError - """ - for info, group in self._all_opt_infos(): - opt = info['opt'] - - if opt.required: - if 'default' in info or 'override' in info: - continue - - if self._get(opt.dest, group) is None: - raise RequiredOptError(opt.name, group) - - def _parse_cli_opts(self, args): - """Parse command line options. - - Initializes the command line option parser and parses the supplied - command line arguments. - - :param args: the command line arguments - :returns: a dict of parsed option values - :raises: SystemExit, DuplicateOptError - - """ - self._args = args - - for opt, group in self._all_cli_opts(): - opt._add_to_cli(self._oparser, group) - - return vars(self._oparser.parse_args(args)) - - class GroupAttr(collections.Mapping): - - """ - A helper class representing the option values of a group as a mapping - and attributes. - """ - - def __init__(self, conf, group): - """Construct a GroupAttr object. - - :param conf: a ConfigOpts object - :param group: an OptGroup object - """ - self._conf = conf - self._group = group - - def __getattr__(self, name): - """Look up an option value and perform template substitution.""" - return self._conf._get(name, self._group) - - def __getitem__(self, key): - """Look up an option value and perform string substitution.""" - return self.__getattr__(key) - - def __contains__(self, key): - """Return True if key is the name of a registered opt or group.""" - return key in self._group._opts - - def __iter__(self): - """Iterate over all registered opt and group names.""" - for key in self._group._opts.keys(): - yield key - - def __len__(self): - """Return the number of options and option groups.""" - return len(self._group._opts) - - class SubCommandAttr(object): - - """ - A helper class representing the name and arguments of an argparse - sub-parser. - """ - - def __init__(self, conf, group, dest): - """Construct a SubCommandAttr object. - - :param conf: a ConfigOpts object - :param group: an OptGroup object - :param dest: the name of the sub-parser - """ - self._conf = conf - self._group = group - self._dest = dest - - def __getattr__(self, name): - """Look up a sub-parser name or argument value.""" - if name == 'name': - name = self._dest - if self._group is not None: - name = self._group.name + '_' + name - return self._conf._cli_values[name] - - if name in self._conf: - raise DuplicateOptError(name) - - try: - return self._conf._cli_values[name] - except KeyError: - raise NoSuchOptError(name) - - class StrSubWrapper(object): - - """ - A helper class exposing opt values as a dict for string substitution. - """ - - def __init__(self, conf): - """Construct a StrSubWrapper object. - - :param conf: a ConfigOpts object - """ - self.conf = conf - - def __getitem__(self, key): - """Look up an opt value from the ConfigOpts object. - - :param key: an opt name - :returns: an opt value - :raises: TemplateSubstitutionError if attribute is a group - """ - value = getattr(self.conf, key) - if isinstance(value, self.conf.GroupAttr): - raise TemplateSubstitutionError( - 'substituting group %s not supported' % key) - return value - - -CONF = ConfigOpts() diff --git a/openstackclient/openstack/common/gettextutils.py b/openstackclient/openstack/common/gettextutils.py index d4c93f4a3d..2b15854862 100644 --- a/openstackclient/openstack/common/gettextutils.py +++ b/openstackclient/openstack/common/gettextutils.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2012 Red Hat, Inc. # Copyright 2013 IBM Corp. # All Rights Reserved. @@ -25,14 +23,12 @@ """ import copy +import functools import gettext -import logging +import locale +from logging import handlers import os import re -try: - import UserString as _userString -except ImportError: - import collections as _userString from babel import localedata import six @@ -40,6 +36,17 @@ _localedir = os.environ.get('openstackclient'.upper() + '_LOCALEDIR') _t = gettext.translation('openstackclient', localedir=_localedir, fallback=True) +# We use separate translation catalogs for each log level, so set up a +# mapping between the log level name and the translator. The domain +# for the log level is project_name + "-log-" + log_level so messages +# for each level end up in their own catalog. +_t_log_levels = dict( + (level, gettext.translation('openstackclient' + '-log-' + level, + localedir=_localedir, + fallback=True)) + for level in ['info', 'warning', 'error', 'critical'] +) + _AVAILABLE_LANGUAGES = {} USE_LAZY = False @@ -58,13 +65,35 @@ def enable_lazy(): def _(msg): if USE_LAZY: - return Message(msg, 'openstackclient') + return Message(msg, domain='openstackclient') else: if six.PY3: return _t.gettext(msg) return _t.ugettext(msg) +def _log_translation(msg, level): + """Build a single translation of a log message + """ + if USE_LAZY: + return Message(msg, domain='openstackclient' + '-log-' + level) + else: + translator = _t_log_levels[level] + if six.PY3: + return translator.gettext(msg) + return translator.ugettext(msg) + +# Translators for log levels. +# +# The abbreviated names are meant to reflect the usual use of a short +# name like '_'. The "L" is for "log" and the other letter comes from +# the level. +_LI = functools.partial(_log_translation, level='info') +_LW = functools.partial(_log_translation, level='warning') +_LE = functools.partial(_log_translation, level='error') +_LC = functools.partial(_log_translation, level='critical') + + def install(domain, lazy=False): """Install a _() function using the given translation domain. @@ -90,11 +119,6 @@ def install(domain, lazy=False): # messages in OpenStack. We override the standard _() function # and % (format string) operation to build Message objects that can # later be translated when we have more information. - # - # Also included below is an example LocaleHandler that translates - # Messages to an associated locale, effectively allowing many logs, - # each with their own locale. - def _lazy_gettext(msg): """Create and return a Message object. @@ -105,7 +129,7 @@ def _lazy_gettext(msg): Message encapsulates a string so that we can translate it later when needed. """ - return Message(msg, domain) + return Message(msg, domain=domain) from six import moves moves.builtins.__dict__['_'] = _lazy_gettext @@ -120,182 +144,169 @@ def _lazy_gettext(msg): unicode=True) -class Message(_userString.UserString, object): - """Class used to encapsulate translatable messages.""" - def __init__(self, msg, domain): - # _msg is the gettext msgid and should never change - self._msg = msg - self._left_extra_msg = '' - self._right_extra_msg = '' - self._locale = None - self.params = None - self.domain = domain - - @property - def data(self): - # NOTE(mrodden): this should always resolve to a unicode string - # that best represents the state of the message currently - - localedir = os.environ.get(self.domain.upper() + '_LOCALEDIR') - if self.locale: - lang = gettext.translation(self.domain, - localedir=localedir, - languages=[self.locale], - fallback=True) - else: - # use system locale for translations - lang = gettext.translation(self.domain, - localedir=localedir, - fallback=True) +class Message(six.text_type): + """A Message object is a unicode object that can be translated. + + Translation of Message is done explicitly using the translate() method. + For all non-translation intents and purposes, a Message is simply unicode, + and can be treated as such. + """ + def __new__(cls, msgid, msgtext=None, params=None, + domain='openstackclient', *args): + """Create a new Message object. + + In order for translation to work gettext requires a message ID, this + msgid will be used as the base unicode text. It is also possible + for the msgid and the base unicode text to be different by passing + the msgtext parameter. + """ + # If the base msgtext is not given, we use the default translation + # of the msgid (which is in English) just in case the system locale is + # not English, so that the base text will be in that locale by default. + if not msgtext: + msgtext = Message._translate_msgid(msgid, domain) + # We want to initialize the parent unicode with the actual object that + # would have been plain unicode if 'Message' was not enabled. + msg = super(Message, cls).__new__(cls, msgtext) + msg.msgid = msgid + msg.domain = domain + msg.params = params + return msg + + def translate(self, desired_locale=None): + """Translate this message to the desired locale. + + :param desired_locale: The desired locale to translate the message to, + if no locale is provided the message will be + translated to the system's default locale. + + :returns: the translated message in unicode + """ + + translated_message = Message._translate_msgid(self.msgid, + self.domain, + desired_locale) + if self.params is None: + # No need for more translation + return translated_message + + # This Message object may have been formatted with one or more + # Message objects as substitution arguments, given either as a single + # argument, part of a tuple, or as one or more values in a dictionary. + # When translating this Message we need to translate those Messages too + translated_params = _translate_args(self.params, desired_locale) + + translated_message = translated_message % translated_params + + return translated_message + + @staticmethod + def _translate_msgid(msgid, domain, desired_locale=None): + if not desired_locale: + system_locale = locale.getdefaultlocale() + # If the system locale is not available to the runtime use English + if not system_locale[0]: + desired_locale = 'en_US' + else: + desired_locale = system_locale[0] + + locale_dir = os.environ.get(domain.upper() + '_LOCALEDIR') + lang = gettext.translation(domain, + localedir=locale_dir, + languages=[desired_locale], + fallback=True) if six.PY3: - ugettext = lang.gettext + translator = lang.gettext else: - ugettext = lang.ugettext - - full_msg = (self._left_extra_msg + - ugettext(self._msg) + - self._right_extra_msg) - - if self.params is not None: - full_msg = full_msg % self.params - - return six.text_type(full_msg) - - @property - def locale(self): - return self._locale - - @locale.setter - def locale(self, value): - self._locale = value - if not self.params: - return - - # This Message object may have been constructed with one or more - # Message objects as substitution parameters, given as a single - # Message, or a tuple or Map containing some, so when setting the - # locale for this Message we need to set it for those Messages too. - if isinstance(self.params, Message): - self.params.locale = value - return - if isinstance(self.params, tuple): - for param in self.params: - if isinstance(param, Message): - param.locale = value - return - if isinstance(self.params, dict): - for param in self.params.values(): - if isinstance(param, Message): - param.locale = value - - def _save_dictionary_parameter(self, dict_param): - full_msg = self.data - # look for %(blah) fields in string; - # ignore %% and deal with the - # case where % is first character on the line - keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', full_msg) - - # if we don't find any %(blah) blocks but have a %s - if not keys and re.findall('(?:[^%]|^)%[a-z]', full_msg): - # apparently the full dictionary is the parameter - params = copy.deepcopy(dict_param) - else: - params = {} - for key in keys: - try: - params[key] = copy.deepcopy(dict_param[key]) - except TypeError: - # cast uncopyable thing to unicode string - params[key] = six.text_type(dict_param[key]) + translator = lang.ugettext - return params + translated_message = translator(msgid) + return translated_message - def _save_parameters(self, other): - # we check for None later to see if - # we actually have parameters to inject, - # so encapsulate if our parameter is actually None + def __mod__(self, other): + # When we mod a Message we want the actual operation to be performed + # by the parent class (i.e. unicode()), the only thing we do here is + # save the original msgid and the parameters in case of a translation + params = self._sanitize_mod_params(other) + unicode_mod = super(Message, self).__mod__(params) + modded = Message(self.msgid, + msgtext=unicode_mod, + params=params, + domain=self.domain) + return modded + + def _sanitize_mod_params(self, other): + """Sanitize the object being modded with this Message. + + - Add support for modding 'None' so translation supports it + - Trim the modded object, which can be a large dictionary, to only + those keys that would actually be used in a translation + - Snapshot the object being modded, in case the message is + translated, it will be used as it was when the Message was created + """ if other is None: - self.params = (other, ) + params = (other,) elif isinstance(other, dict): - self.params = self._save_dictionary_parameter(other) + params = self._trim_dictionary_parameters(other) else: - # fallback to casting to unicode, - # this will handle the problematic python code-like - # objects that cannot be deep-copied - try: - self.params = copy.deepcopy(other) - except TypeError: - self.params = six.text_type(other) - - return self - - # overrides to be more string-like - def __unicode__(self): - return self.data - - def __str__(self): - if six.PY3: - return self.__unicode__() - return self.data.encode('utf-8') + params = self._copy_param(other) + return params - def __getstate__(self): - to_copy = ['_msg', '_right_extra_msg', '_left_extra_msg', - 'domain', 'params', '_locale'] - new_dict = self.__dict__.fromkeys(to_copy) - for attr in to_copy: - new_dict[attr] = copy.deepcopy(self.__dict__[attr]) + def _trim_dictionary_parameters(self, dict_param): + """Return a dict that only has matching entries in the msgid.""" + # NOTE(luisg): Here we trim down the dictionary passed as parameters + # to avoid carrying a lot of unnecessary weight around in the message + # object, for example if someone passes in Message() % locals() but + # only some params are used, and additionally we prevent errors for + # non-deepcopyable objects by unicoding() them. + + # Look for %(param) keys in msgid; + # Skip %% and deal with the case where % is first character on the line + keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', self.msgid) + + # If we don't find any %(param) keys but have a %s + if not keys and re.findall('(?:[^%]|^)%[a-z]', self.msgid): + # Apparently the full dictionary is the parameter + params = self._copy_param(dict_param) + else: + params = {} + # Save our existing parameters as defaults to protect + # ourselves from losing values if we are called through an + # (erroneous) chain that builds a valid Message with + # arguments, and then does something like "msg % kwds" + # where kwds is an empty dictionary. + src = {} + if isinstance(self.params, dict): + src.update(self.params) + src.update(dict_param) + for key in keys: + params[key] = self._copy_param(src[key]) - return new_dict + return params - def __setstate__(self, state): - for (k, v) in state.items(): - setattr(self, k, v) + def _copy_param(self, param): + try: + return copy.deepcopy(param) + except TypeError: + # Fallback to casting to unicode this will handle the + # python code-like objects that can't be deep-copied + return six.text_type(param) - # operator overloads def __add__(self, other): - copied = copy.deepcopy(self) - copied._right_extra_msg += other.__str__() - return copied + msg = _('Message objects do not support addition.') + raise TypeError(msg) def __radd__(self, other): - copied = copy.deepcopy(self) - copied._left_extra_msg += other.__str__() - return copied + return self.__add__(other) - def __mod__(self, other): - # do a format string to catch and raise - # any possible KeyErrors from missing parameters - self.data % other - copied = copy.deepcopy(self) - return copied._save_parameters(other) - - def __mul__(self, other): - return self.data * other - - def __rmul__(self, other): - return other * self.data - - def __getitem__(self, key): - return self.data[key] - - def __getslice__(self, start, end): - return self.data.__getslice__(start, end) - - def __getattribute__(self, name): - # NOTE(mrodden): handle lossy operations that we can't deal with yet - # These override the UserString implementation, since UserString - # uses our __class__ attribute to try and build a new message - # after running the inner data string through the operation. - # At that point, we have lost the gettext message id and can just - # safely resolve to a string instead. - ops = ['capitalize', 'center', 'decode', 'encode', - 'expandtabs', 'ljust', 'lstrip', 'replace', 'rjust', 'rstrip', - 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill'] - if name in ops: - return getattr(self.data, name) - else: - return _userString.UserString.__getattribute__(self, name) + def __str__(self): + # NOTE(luisg): Logging in python 2.6 tries to str() log records, + # and it expects specifically a UnicodeError in order to proceed. + msg = _('Message objects do not support str() because they may ' + 'contain non-ascii characters. ' + 'Please use unicode() or translate() instead.') + raise UnicodeError(msg) def get_available_languages(domain): @@ -317,49 +328,147 @@ def get_available_languages(domain): # NOTE(luisg): Babel <1.0 used a function called list(), which was # renamed to locale_identifiers() in >=1.0, the requirements master list # requires >=0.9.6, uncapped, so defensively work with both. We can remove - # this check when the master list updates to >=1.0, and all projects udpate + # this check when the master list updates to >=1.0, and update all projects list_identifiers = (getattr(localedata, 'list', None) or getattr(localedata, 'locale_identifiers')) locale_identifiers = list_identifiers() + for i in locale_identifiers: if find(i) is not None: language_list.append(i) + + # NOTE(luisg): Babel>=1.0,<1.3 has a bug where some OpenStack supported + # locales (e.g. 'zh_CN', and 'zh_TW') aren't supported even though they + # are perfectly legitimate locales: + # https://github.com/mitsuhiko/babel/issues/37 + # In Babel 1.3 they fixed the bug and they support these locales, but + # they are still not explicitly "listed" by locale_identifiers(). + # That is why we add the locales here explicitly if necessary so that + # they are listed as supported. + aliases = {'zh': 'zh_CN', + 'zh_Hant_HK': 'zh_HK', + 'zh_Hant': 'zh_TW', + 'fil': 'tl_PH'} + for (locale, alias) in six.iteritems(aliases): + if locale in language_list and alias not in language_list: + language_list.append(alias) + _AVAILABLE_LANGUAGES[domain] = language_list return copy.copy(language_list) -def get_localized_message(message, user_locale): - """Gets a localized version of the given message in the given locale.""" +def translate(obj, desired_locale=None): + """Gets the translated unicode representation of the given object. + + If the object is not translatable it is returned as-is. + If the locale is None the object is translated to the system locale. + + :param obj: the object to translate + :param desired_locale: the locale to translate the message to, if None the + default system locale will be used + :returns: the translated object in unicode, or the original object if + it could not be translated + """ + message = obj + if not isinstance(message, Message): + # If the object to translate is not already translatable, + # let's first get its unicode representation + message = six.text_type(obj) if isinstance(message, Message): - if user_locale: - message.locale = user_locale - return six.text_type(message) - else: - return message + # Even after unicoding() we still need to check if we are + # running with translatable unicode before translating + return message.translate(desired_locale) + return obj + + +def _translate_args(args, desired_locale=None): + """Translates all the translatable elements of the given arguments object. + + This method is used for translating the translatable values in method + arguments which include values of tuples or dictionaries. + If the object is not a tuple or a dictionary the object itself is + translated if it is translatable. + + If the locale is None the object is translated to the system locale. + + :param args: the args to translate + :param desired_locale: the locale to translate the args to, if None the + default system locale will be used + :returns: a new args object with the translated contents of the original + """ + if isinstance(args, tuple): + return tuple(translate(v, desired_locale) for v in args) + if isinstance(args, dict): + translated_dict = {} + for (k, v) in six.iteritems(args): + translated_v = translate(v, desired_locale) + translated_dict[k] = translated_v + return translated_dict + return translate(args, desired_locale) + + +class TranslationHandler(handlers.MemoryHandler): + """Handler that translates records before logging them. + + The TranslationHandler takes a locale and a target logging.Handler object + to forward LogRecord objects to after translating them. This handler + depends on Message objects being logged, instead of regular strings. + The handler can be configured declaratively in the logging.conf as follows: -class LocaleHandler(logging.Handler): - """Handler that can have a locale associated to translate Messages. + [handlers] + keys = translatedlog, translator - A quick example of how to utilize the Message class above. - LocaleHandler takes a locale and a target logging.Handler object - to forward LogRecord objects to after translating the internal Message. + [handler_translatedlog] + class = handlers.WatchedFileHandler + args = ('/var/log/api-localized.log',) + formatter = context + + [handler_translator] + class = openstack.common.log.TranslationHandler + target = translatedlog + args = ('zh_CN',) + + If the specified locale is not available in the system, the handler will + log in the default locale. """ - def __init__(self, locale, target): - """Initialize a LocaleHandler + def __init__(self, locale=None, target=None): + """Initialize a TranslationHandler :param locale: locale to use for translating messages :param target: logging.Handler object to forward LogRecord objects to after translation """ - logging.Handler.__init__(self) + # NOTE(luisg): In order to allow this handler to be a wrapper for + # other handlers, such as a FileHandler, and still be able to + # configure it using logging.conf, this handler has to extend + # MemoryHandler because only the MemoryHandlers' logging.conf + # parsing is implemented such that it accepts a target handler. + handlers.MemoryHandler.__init__(self, capacity=0, target=target) self.locale = locale - self.target = target + + def setFormatter(self, fmt): + self.target.setFormatter(fmt) def emit(self, record): - if isinstance(record.msg, Message): - # set the locale and resolve to a string - record.msg.locale = self.locale + # We save the message from the original record to restore it + # after translation, so other handlers are not affected by this + original_msg = record.msg + original_args = record.args + + try: + self._translate_and_log_record(record) + finally: + record.msg = original_msg + record.args = original_args + + def _translate_and_log_record(self, record): + record.msg = translate(record.msg, self.locale) + + # In addition to translating the message, we also need to translate + # arguments that were passed to the log method that were not part + # of the main message e.g., log.info(_('Some message %s'), this_one)) + record.args = _translate_args(record.args, self.locale) self.target.emit(record) diff --git a/openstackclient/openstack/common/strutils.py b/openstackclient/openstack/common/strutils.py index e3f26a7876..33fca54b26 100644 --- a/openstackclient/openstack/common/strutils.py +++ b/openstackclient/openstack/common/strutils.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2011 OpenStack Foundation. # All Rights Reserved. # @@ -19,25 +17,31 @@ System-level utilities and helper functions. """ +import math import re import sys import unicodedata import six -from openstackclient.openstack.common.gettextutils import _ # noqa +from openstackclient.openstack.common.gettextutils import _ -# Used for looking up extensions of text -# to their 'multiplied' byte amount -BYTE_MULTIPLIERS = { - '': 1, - 't': 1024 ** 4, - 'g': 1024 ** 3, - 'm': 1024 ** 2, - 'k': 1024, +UNIT_PREFIX_EXPONENT = { + 'k': 1, + 'K': 1, + 'Ki': 1, + 'M': 2, + 'Mi': 2, + 'G': 3, + 'Gi': 3, + 'T': 4, + 'Ti': 4, +} +UNIT_SYSTEM_INFO = { + 'IEC': (1024, re.compile(r'(^[-+]?\d*\.?\d+)([KMGT]i?)?(b|bit|B)$')), + 'SI': (1000, re.compile(r'(^[-+]?\d*\.?\d+)([kMGT])?(b|bit|B)$')), } -BYTE_REGEX = re.compile(r'(^-?\d+)(\D*)') TRUE_STRINGS = ('1', 't', 'true', 'on', 'y', 'yes') FALSE_STRINGS = ('0', 'f', 'false', 'off', 'n', 'no') @@ -60,12 +64,12 @@ def int_from_bool_as_string(subject): return bool_from_string(subject) and 1 or 0 -def bool_from_string(subject, strict=False): +def bool_from_string(subject, strict=False, default=False): """Interpret a string as a boolean. A case-insensitive match is performed such that strings matching 't', 'true', 'on', 'y', 'yes', or '1' are considered True and, when - `strict=False`, anything else is considered False. + `strict=False`, anything else returns the value specified by 'default'. Useful for JSON-decoded stuff and config file parsing. @@ -90,7 +94,7 @@ def bool_from_string(subject, strict=False): 'acceptable': acceptable} raise ValueError(msg) else: - return False + return default def safe_decode(text, incoming=None, errors='strict'): @@ -101,7 +105,7 @@ def safe_decode(text, incoming=None, errors='strict'): values http://docs.python.org/2/library/codecs.html :returns: text or a unicode `incoming` encoded representation of it. - :raises TypeError: If text is not an isntance of str + :raises TypeError: If text is not an instance of str """ if not isinstance(text, six.string_types): raise TypeError("%s can't be decoded" % type(text)) @@ -144,7 +148,7 @@ def safe_encode(text, incoming=None, values http://docs.python.org/2/library/codecs.html :returns: text or a bytestring `encoding` encoded representation of it. - :raises TypeError: If text is not an isntance of str + :raises TypeError: If text is not an instance of str """ if not isinstance(text, six.string_types): raise TypeError("%s can't be encoded" % type(text)) @@ -154,43 +158,65 @@ def safe_encode(text, incoming=None, sys.getdefaultencoding()) if isinstance(text, six.text_type): - return text.encode(encoding, errors) + if six.PY3: + return text.encode(encoding, errors).decode(incoming) + else: + return text.encode(encoding, errors) elif text and encoding != incoming: # Decode text before encoding it with `encoding` text = safe_decode(text, incoming, errors) - return text.encode(encoding, errors) + if six.PY3: + return text.encode(encoding, errors).decode(incoming) + else: + return text.encode(encoding, errors) return text -def to_bytes(text, default=0): - """Converts a string into an integer of bytes. +def string_to_bytes(text, unit_system='IEC', return_int=False): + """Converts a string into an float representation of bytes. + + The units supported for IEC :: + + Kb(it), Kib(it), Mb(it), Mib(it), Gb(it), Gib(it), Tb(it), Tib(it) + KB, KiB, MB, MiB, GB, GiB, TB, TiB - Looks at the last characters of the text to determine - what conversion is needed to turn the input text into a byte number. - Supports "B, K(B), M(B), G(B), and T(B)". (case insensitive) + The units supported for SI :: + + kb(it), Mb(it), Gb(it), Tb(it) + kB, MB, GB, TB + + Note that the SI unit system does not support capital letter 'K' :param text: String input for bytes size conversion. - :param default: Default return value when text is blank. + :param unit_system: Unit system for byte size conversion. + :param return_int: If True, returns integer representation of text + in bytes. (default: decimal) + :returns: Numerical representation of text in bytes. + :raises ValueError: If text has an invalid value. """ - match = BYTE_REGEX.search(text) + try: + base, reg_ex = UNIT_SYSTEM_INFO[unit_system] + except KeyError: + msg = _('Invalid unit system: "%s"') % unit_system + raise ValueError(msg) + match = reg_ex.match(text) if match: - magnitude = int(match.group(1)) - mult_key_org = match.group(2) - if not mult_key_org: - return magnitude - elif text: + magnitude = float(match.group(1)) + unit_prefix = match.group(2) + if match.group(3) in ['b', 'bit']: + magnitude /= 8 + else: msg = _('Invalid string format: %s') % text - raise TypeError(msg) + raise ValueError(msg) + if not unit_prefix: + res = magnitude else: - return default - mult_key = mult_key_org.lower().replace('b', '', 1) - multiplier = BYTE_MULTIPLIERS.get(mult_key) - if multiplier is None: - msg = _('Unknown byte multiplier: %s') % mult_key_org - raise TypeError(msg) - return magnitude * multiplier + res = magnitude * pow(base, UNIT_PREFIX_EXPONENT[unit_prefix]) + if return_int: + return int(math.ceil(res)) + return res def to_slug(value, incoming=None, errors="strict"): diff --git a/tools/install_venv_common.py b/tools/install_venv_common.py index 1bab88a3fd..46822e3293 100644 --- a/tools/install_venv_common.py +++ b/tools/install_venv_common.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2013 OpenStack Foundation # Copyright 2013 IBM Corp. # From ada9d35cbe477d7225d217c6c1926b5fb8aa0a92 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Thu, 20 Feb 2014 09:29:38 -0700 Subject: [PATCH 0064/3494] Fix format errors in nova security group rule list * port range was throwing exception for None to/from ports * ip_range didn't always have cidr causing error * ip_protocol None at times and looked bad Closes-Bug #1256935 Change-Id: I451a0f038a3e9646bca3f278c5d6f6d7e3097a83 --- openstackclient/compute/v2/security_group.py | 22 +++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/openstackclient/compute/v2/security_group.py b/openstackclient/compute/v2/security_group.py index a1dc786d71..be64bd3ab2 100644 --- a/openstackclient/compute/v2/security_group.py +++ b/openstackclient/compute/v2/security_group.py @@ -31,15 +31,23 @@ def _xform_security_group_rule(sgroup): info = {} info.update(sgroup) - info.update( - {'port_range': "%u:%u" % ( - info.pop('from_port'), - info.pop('to_port'), - )} - ) - info['ip_range'] = info['ip_range']['cidr'] + from_port = info.pop('from_port') + to_port = info.pop('to_port') + if isinstance(from_port, int) and isinstance(to_port, int): + port_range = {'port_range': "%u:%u" % (from_port, to_port)} + elif from_port is None and to_port is None: + port_range = {'port_range': ""} + else: + port_range = {'port_range': "%s:%s" % (from_port, to_port)} + info.update(port_range) + if 'cidr' in info['ip_range']: + info['ip_range'] = info['ip_range']['cidr'] + else: + info['ip_range'] = '' if info['ip_protocol'] == 'icmp': info['port_range'] = '' + elif info['ip_protocol'] is None: + info['ip_protocol'] = '' return info From 50432931562ca697a2b88db2bb2f0b1da91fb28a Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Fri, 21 Feb 2014 19:22:32 +0100 Subject: [PATCH 0065/3494] Fix some help strings This fixes some errors and inconsistencies I found reviewing the help strings: * Capitalize help strings * Add missing space between words (in multi-line strings) * Improve wording Change-Id: I2fb31ab4191c330146e31c1a9651115a6657769a --- openstackclient/compute/v2/agent.py | 2 +- openstackclient/compute/v2/server.py | 24 +++++++++++------------ openstackclient/compute/v2/usage.py | 2 +- openstackclient/identity/v2_0/endpoint.py | 2 +- openstackclient/identity/v2_0/service.py | 2 +- openstackclient/identity/v2_0/user.py | 2 +- openstackclient/identity/v3/endpoint.py | 2 +- openstackclient/identity/v3/group.py | 2 +- openstackclient/identity/v3/policy.py | 2 +- openstackclient/identity/v3/token.py | 6 +++--- openstackclient/identity/v3/user.py | 2 +- openstackclient/shell.py | 10 +++++----- openstackclient/volume/v1/backup.py | 2 +- openstackclient/volume/v1/type.py | 2 +- openstackclient/volume/v1/volume.py | 2 +- 15 files changed, 32 insertions(+), 32 deletions(-) diff --git a/openstackclient/compute/v2/agent.py b/openstackclient/compute/v2/agent.py index b79ebbe761..c8fb6ccc0d 100644 --- a/openstackclient/compute/v2/agent.py +++ b/openstackclient/compute/v2/agent.py @@ -136,7 +136,7 @@ def get_parser(self, prog_name): parser.add_argument( "id", metavar="", - help="ID of the agent build") + help="ID of the agent") parser.add_argument( "version", metavar="", diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 87f5f6896d..808741fdca 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -483,40 +483,40 @@ def get_parser(self, prog_name): parser.add_argument( '--reservation-id', metavar='', - help='only return instances that match the reservation') + help='Only return instances that match the reservation') parser.add_argument( '--ip', metavar='', - help='regular expression to match IP address') + help='Regular expression to match IP addresses') parser.add_argument( '--ip6', metavar='', - help='regular expression to match IPv6 address') + help='Regular expression to match IPv6 addresses') parser.add_argument( '--name', metavar='', - help='regular expression to match name') + help='Regular expression to match names') parser.add_argument( '--status', metavar='', # FIXME(dhellmann): Add choices? - help='search by server status') + help='Search by server status') parser.add_argument( '--flavor', metavar='', - help='search by flavor ID') + help='Search by flavor ID') parser.add_argument( '--image', metavar='', - help='search by image ID') + help='Search by image ID') parser.add_argument( '--host', metavar='', - help='search by hostname') + help='Search by hostname') parser.add_argument( '--instance-name', metavar='', - help='regular expression to match instance name (admin only)') + help='Regular expression to match instance name (admin only)') parser.add_argument( '--all-projects', action='store_true', @@ -526,7 +526,7 @@ def get_parser(self, prog_name): '--long', action='store_true', default=False, - help='Additional fields are listed in output') + help='List additional fields in output') return parser def take_action(self, parsed_args): @@ -1181,14 +1181,14 @@ def get_parser(self, prog_name): dest='ipv4', action='store_true', default=False, - help='Use only IPv4 addresses only', + help='Use only IPv4 addresses', ) ip_group.add_argument( '-6', dest='ipv6', action='store_true', default=False, - help='Use only IPv6 addresses only', + help='Use only IPv6 addresses', ) type_group = parser.add_mutually_exclusive_group() type_group.add_argument( diff --git a/openstackclient/compute/v2/usage.py b/openstackclient/compute/v2/usage.py index 3083576945..1dfe8c0ae4 100644 --- a/openstackclient/compute/v2/usage.py +++ b/openstackclient/compute/v2/usage.py @@ -35,7 +35,7 @@ def get_parser(self, prog_name): "--start", metavar="", default=None, - help="Usage range start date ex 2012-01-20" + help="Usage range start date, ex 2012-01-20" " (default: 4 weeks ago)." ) parser.add_argument( diff --git a/openstackclient/identity/v2_0/endpoint.py b/openstackclient/identity/v2_0/endpoint.py index 5a050fa190..0319c2680a 100644 --- a/openstackclient/identity/v2_0/endpoint.py +++ b/openstackclient/identity/v2_0/endpoint.py @@ -106,7 +106,7 @@ def get_parser(self, prog_name): '--long', action='store_true', default=False, - help='Additional fields are listed in output') + help='List additional fields in output') return parser def take_action(self, parsed_args): diff --git a/openstackclient/identity/v2_0/service.py b/openstackclient/identity/v2_0/service.py index 92d1e09915..ea45f63431 100644 --- a/openstackclient/identity/v2_0/service.py +++ b/openstackclient/identity/v2_0/service.py @@ -104,7 +104,7 @@ def get_parser(self, prog_name): '--long', action='store_true', default=False, - help='Additional fields are listed in output') + help='List additional fields in output') return parser def take_action(self, parsed_args): diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index 371c45a998..55ba5ab64f 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -143,7 +143,7 @@ def get_parser(self, prog_name): '--long', action='store_true', default=False, - help='Additional fields are listed in output') + help='List additional fields in output') return parser def take_action(self, parsed_args): diff --git a/openstackclient/identity/v3/endpoint.py b/openstackclient/identity/v3/endpoint.py index 43da07aaad..e0b1f1a3cf 100644 --- a/openstackclient/identity/v3/endpoint.py +++ b/openstackclient/identity/v3/endpoint.py @@ -120,7 +120,7 @@ def get_parser(self, prog_name): '--long', action='store_true', default=False, - help='Additional fields are listed in output') + help='List additional fields in output') return parser def take_action(self, parsed_args): diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index b5d24ef5f8..6c059b5df7 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -197,7 +197,7 @@ def get_parser(self, prog_name): '--long', action='store_true', default=False, - help='Additional fields are listed in output', + help='List additional fields in output', ) return parser diff --git a/openstackclient/identity/v3/policy.py b/openstackclient/identity/v3/policy.py index cdbb1cf299..a760d8cdd8 100644 --- a/openstackclient/identity/v3/policy.py +++ b/openstackclient/identity/v3/policy.py @@ -37,7 +37,7 @@ def get_parser(self, prog_name): '--type', metavar='', default="application/json", - help='New MIME Type of the policy blob - i.e.: application/json', + help='New MIME type of the policy blob - i.e.: application/json', ) parser.add_argument( 'blob_file', diff --git a/openstackclient/identity/v3/token.py b/openstackclient/identity/v3/token.py index 68f9ffef47..3cc78cd7a8 100644 --- a/openstackclient/identity/v3/token.py +++ b/openstackclient/identity/v3/token.py @@ -213,12 +213,12 @@ def get_parser(self, prog_name): parser.add_argument( 'user', metavar='', - help='Name or Id of user', + help='Name or ID of user', ) parser.add_argument( 'access_key', metavar='', - help='Access Token to be deleted', + help='Access token to be deleted', ) return parser @@ -243,7 +243,7 @@ def get_parser(self, prog_name): parser.add_argument( 'user', metavar='', - help='Name or Id of user', + help='Name or ID of user', ) return parser diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 54ffe561a3..2cb1b4d2f5 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -174,7 +174,7 @@ def get_parser(self, prog_name): '--long', action='store_true', default=False, - help='Additional fields are listed in output', + help='List additional fields in output', ) return parser diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 76cc3c6a90..49307992c7 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -117,7 +117,7 @@ def __init__(self): action='store_true', dest='deferred_help', default=False, - help="show this help message and exit", + help="Show this help message and exit", ) def run(self, argv): @@ -147,21 +147,21 @@ def build_option_parser(self, description, version): '--os-domain-name', metavar='', default=env('OS_DOMAIN_NAME'), - help='Domain name of the requested domain-level' + help='Domain name of the requested domain-level ' 'authorization scope (Env: OS_DOMAIN_NAME)', ) parser.add_argument( '--os-domain-id', metavar='', default=env('OS_DOMAIN_ID'), - help='Domain ID of the requested domain-level' + help='Domain ID of the requested domain-level ' 'authorization scope (Env: OS_DOMAIN_ID)', ) parser.add_argument( '--os-project-name', metavar='', default=env('OS_PROJECT_NAME', default=env('OS_TENANT_NAME')), - help='Project name of the requested project-level' + help='Project name of the requested project-level ' 'authorization scope (Env: OS_PROJECT_NAME)', ) parser.add_argument( @@ -174,7 +174,7 @@ def build_option_parser(self, description, version): '--os-project-id', metavar='', default=env('OS_PROJECT_ID', default=env('OS_TENANT_ID')), - help='Project ID of the requested project-level' + help='Project ID of the requested project-level ' 'authorization scope (Env: OS_PROJECT_ID)', ) parser.add_argument( diff --git a/openstackclient/volume/v1/backup.py b/openstackclient/volume/v1/backup.py index dbf6eb7370..ac34749baf 100644 --- a/openstackclient/volume/v1/backup.py +++ b/openstackclient/volume/v1/backup.py @@ -41,7 +41,7 @@ def get_parser(self, prog_name): '--container', metavar='', required=False, - help='Optional Backup container name.', + help='Optional backup container name.', ) parser.add_argument( '--name', diff --git a/openstackclient/volume/v1/type.py b/openstackclient/volume/v1/type.py index ecf22781b4..edacb397e5 100644 --- a/openstackclient/volume/v1/type.py +++ b/openstackclient/volume/v1/type.py @@ -100,7 +100,7 @@ def get_parser(self, prog_name): '--long', action='store_true', default=False, - help='Additional fields are listed in output') + help='List additional fields in output') return parser def take_action(self, parsed_args): diff --git a/openstackclient/volume/v1/volume.py b/openstackclient/volume/v1/volume.py index 0253bc1dad..928ed76be0 100644 --- a/openstackclient/volume/v1/volume.py +++ b/openstackclient/volume/v1/volume.py @@ -68,7 +68,7 @@ def get_parser(self, prog_name): parser.add_argument( '--project', metavar='', - help='Specify a diffeent project (admin only)', + help='Specify a different project (admin only)', ) parser.add_argument( '--availability-zone', From 033f27fe4dc4455c2f07978a273fd65faa653b67 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 19 Feb 2014 19:30:56 -0700 Subject: [PATCH 0066/3494] Add ability to prompt for passwords for user create and set * Add get_password method to the utilities * Add --password-prompt option * Call the get_password method if a prompt is requested * Various tests Change-Id: I1786ad531e2a2fbcc21b8bc86aac0ccd7985995a Closes-Bug: 1100116 --- openstackclient/common/utils.py | 16 ++++ openstackclient/identity/v2_0/user.py | 17 ++++ openstackclient/identity/v3/user.py | 17 ++++ openstackclient/tests/common/test_utils.py | 59 +++++++++++++ .../tests/identity/v2_0/test_user.py | 73 ++++++++++++++++ .../tests/identity/v3/test_user.py | 87 +++++++++++++++++++ 6 files changed, 269 insertions(+) create mode 100644 openstackclient/tests/common/test_utils.py diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py index 94ea22253a..7cd877ef61 100644 --- a/openstackclient/common/utils.py +++ b/openstackclient/common/utils.py @@ -15,6 +15,7 @@ """Common client utilities""" +import getpass import logging import os import six @@ -229,3 +230,18 @@ def get_effective_log_level(): for handler in root_log.handlers: min_log_lvl = min(min_log_lvl, handler.level) return min_log_lvl + + +def get_password(stdin): + if hasattr(stdin, 'isatty') and stdin.isatty(): + try: + while True: + first_pass = getpass.getpass("User password: ") + second_pass = getpass.getpass("Repeat user password: ") + if first_pass == second_pass: + return first_pass + print("The passwords entered were not the same") + except EOFError: # Ctl-D + raise exceptions.CommandError("Error reading password.") + raise exceptions.CommandError("There was a request to be prompted for a" + " password and a terminal was not detected.") diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index 371c45a998..abcafdd013 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -42,6 +42,12 @@ def get_parser(self, prog_name): metavar='', help='New user password', ) + parser.add_argument( + '--password-prompt', + dest="password_prompt", + action="store_true", + help='Prompt interactively for password', + ) parser.add_argument( '--email', metavar='', @@ -80,6 +86,8 @@ def take_action(self, parsed_args): enabled = True if parsed_args.disable: enabled = False + if parsed_args.password_prompt: + parsed_args.password = utils.get_password(self.app.stdin) user = identity_client.users.create( parsed_args.name, @@ -224,6 +232,12 @@ def get_parser(self, prog_name): metavar='', help='New user password', ) + parser.add_argument( + '--password-prompt', + dest="password_prompt", + action="store_true", + help='Prompt interactively for password', + ) parser.add_argument( '--email', metavar='', @@ -251,6 +265,9 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity + if parsed_args.password_prompt: + parsed_args.password = utils.get_password(self.app.stdin) + if (not parsed_args.name and not parsed_args.name and not parsed_args.password diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 54ffe561a3..7e710ac014 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -43,6 +43,12 @@ def get_parser(self, prog_name): metavar='', help='New user password', ) + parser.add_argument( + '--password-prompt', + dest="password_prompt", + action="store_true", + help='Prompt interactively for password', + ) parser.add_argument( '--email', metavar='', @@ -97,6 +103,8 @@ def take_action(self, parsed_args): enabled = True if parsed_args.disable: enabled = False + if parsed_args.password_prompt: + parsed_args.password = utils.get_password(self.app.stdin) user = identity_client.users.create( parsed_args.name, @@ -273,6 +281,12 @@ def get_parser(self, prog_name): metavar='', help='New user password', ) + parser.add_argument( + '--password-prompt', + dest="password_prompt", + action="store_true", + help='Prompt interactively for password', + ) parser.add_argument( '--email', metavar='', @@ -310,6 +324,9 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity + if parsed_args.password_prompt: + parsed_args.password = utils.get_password(self.app.stdin) + if (not parsed_args.name and not parsed_args.name and not parsed_args.password diff --git a/openstackclient/tests/common/test_utils.py b/openstackclient/tests/common/test_utils.py new file mode 100644 index 0000000000..0ad4ca9bdb --- /dev/null +++ b/openstackclient/tests/common/test_utils.py @@ -0,0 +1,59 @@ +# Copyright 2012-2013 OpenStack, LLC. +# +# 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 mock + +from openstackclient.common import exceptions +from openstackclient.common import utils +from openstackclient.tests import utils as test_utils + +PASSWORD = "Pa$$w0rd" +WASSPORD = "Wa$$p0rd" +DROWSSAP = "dr0w$$aP" + + +class TestUtils(test_utils.TestCase): + + def test_get_password_good(self): + with mock.patch("getpass.getpass", return_value=PASSWORD): + mock_stdin = mock.Mock() + mock_stdin.isatty = mock.Mock() + mock_stdin.isatty.return_value = True + self.assertEqual(utils.get_password(mock_stdin), PASSWORD) + + def test_get_password_bad_once(self): + answers = [PASSWORD, WASSPORD, DROWSSAP, DROWSSAP] + with mock.patch("getpass.getpass", side_effect=answers): + mock_stdin = mock.Mock() + mock_stdin.isatty = mock.Mock() + mock_stdin.isatty.return_value = True + self.assertEqual(utils.get_password(mock_stdin), DROWSSAP) + + def test_get_password_no_tty(self): + mock_stdin = mock.Mock() + mock_stdin.isatty = mock.Mock() + mock_stdin.isatty.return_value = False + self.assertRaises(exceptions.CommandError, + utils.get_password, + mock_stdin) + + def test_get_password_cntrl_d(self): + with mock.patch("getpass.getpass", side_effect=EOFError()): + mock_stdin = mock.Mock() + mock_stdin.isatty = mock.Mock() + mock_stdin.isatty.return_value = True + self.assertRaises(exceptions.CommandError, + utils.get_password, + mock_stdin) diff --git a/openstackclient/tests/identity/v2_0/test_user.py b/openstackclient/tests/identity/v2_0/test_user.py index a18d13d821..dfb358ffd0 100644 --- a/openstackclient/tests/identity/v2_0/test_user.py +++ b/openstackclient/tests/identity/v2_0/test_user.py @@ -14,6 +14,7 @@ # import copy +import mock from openstackclient.identity.v2_0 import user from openstackclient.tests import fakes @@ -99,6 +100,7 @@ def test_user_create_password(self): ] verifylist = [ ('name', identity_fakes.user_name), + ('password_prompt', False), ('password', 'secret') ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -130,6 +132,47 @@ def test_user_create_password(self): ) self.assertEqual(data, datalist) + def test_user_create_password_prompt(self): + arglist = [ + '--password-prompt', + identity_fakes.user_name, + ] + verifylist = [ + ('name', identity_fakes.user_name), + ('password_prompt', True) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + mocker = mock.Mock() + mocker.return_value = 'abc123' + with mock.patch("openstackclient.common.utils.get_password", mocker): + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': True, + 'tenant_id': None, + } + # UserManager.create(name, password, email, tenant_id=, enabled=) + self.users_mock.create.assert_called_with( + identity_fakes.user_name, + 'abc123', + None, + **kwargs + ) + + collist = ('email', 'enabled', 'id', 'name', 'project_id') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.user_email, + True, + identity_fakes.user_id, + identity_fakes.user_name, + identity_fakes.project_id, + ) + self.assertEqual(data, datalist) + def test_user_create_email(self): arglist = [ '--email', 'barney@example.com', @@ -498,6 +541,7 @@ def test_user_set_password(self): verifylist = [ ('name', None), ('password', 'secret'), + ('password_prompt', False), ('email', None), ('project', None), ('enable', False), @@ -515,6 +559,35 @@ def test_user_set_password(self): 'secret', ) + def test_user_set_password_prompt(self): + arglist = [ + '--password-prompt', + identity_fakes.user_name, + ] + verifylist = [ + ('name', None), + ('password', None), + ('password_prompt', True), + ('email', None), + ('project', None), + ('enable', False), + ('disable', False), + ('user', identity_fakes.user_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + mocker = mock.Mock() + mocker.return_value = 'abc123' + with mock.patch("openstackclient.common.utils.get_password", mocker): + self.cmd.take_action(parsed_args) + + # UserManager.update_password(user, password) + self.users_mock.update_password.assert_called_with( + identity_fakes.user_id, + 'abc123', + ) + def test_user_set_email(self): arglist = [ '--email', 'barney@example.com', diff --git a/openstackclient/tests/identity/v3/test_user.py b/openstackclient/tests/identity/v3/test_user.py index 4321b04799..af7b2f70c1 100644 --- a/openstackclient/tests/identity/v3/test_user.py +++ b/openstackclient/tests/identity/v3/test_user.py @@ -14,6 +14,7 @@ # import copy +import mock from openstackclient.identity.v3 import user from openstackclient.tests import fakes @@ -118,6 +119,7 @@ def test_user_create_password(self): ] verifylist = [ ('password', 'secret'), + ('password_prompt', False), ('enable', False), ('disable', False), ('name', identity_fakes.user_name), @@ -155,6 +157,54 @@ def test_user_create_password(self): ) self.assertEqual(data, datalist) + def test_user_create_password_prompt(self): + arglist = [ + '--password-prompt', + identity_fakes.user_name, + ] + verifylist = [ + ('password', None), + ('password_prompt', True), + ('enable', False), + ('disable', False), + ('name', identity_fakes.user_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + mocker = mock.Mock() + mocker.return_value = 'abc123' + with mock.patch("openstackclient.common.utils.get_password", mocker): + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'default_project': None, + 'description': None, + 'domain': None, + 'email': None, + 'enabled': True, + 'password': 'abc123', + } + # UserManager.create(name, domain=, project=, password=, email=, + # description=, enabled=, default_project=) + self.users_mock.create.assert_called_with( + identity_fakes.user_name, + **kwargs + ) + + collist = ('domain_id', 'email', 'enabled', 'id', 'name', 'project_id') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.domain_id, + identity_fakes.user_email, + True, + identity_fakes.user_id, + identity_fakes.user_name, + identity_fakes.project_id, + ) + self.assertEqual(data, datalist) + def test_user_create_email(self): arglist = [ '--email', 'barney@example.com', @@ -761,6 +811,7 @@ def test_user_set_password(self): verifylist = [ ('name', None), ('password', 'secret'), + ('password_prompt', False), ('email', None), ('domain', None), ('project', None), @@ -785,6 +836,42 @@ def test_user_set_password(self): **kwargs ) + def test_user_set_password_prompt(self): + arglist = [ + '--password-prompt', + identity_fakes.user_name, + ] + verifylist = [ + ('name', None), + ('password', None), + ('password_prompt', True), + ('email', None), + ('domain', None), + ('project', None), + ('enable', False), + ('disable', False), + ('user', identity_fakes.user_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + mocker = mock.Mock() + mocker.return_value = 'abc123' + with mock.patch("openstackclient.common.utils.get_password", mocker): + self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': True, + 'password': 'abc123', + } + # UserManager.update(user, name=, domain=, project=, password=, + # email=, description=, enabled=, default_project=) + self.users_mock.update.assert_called_with( + identity_fakes.user_id, + **kwargs + ) + def test_user_set_email(self): arglist = [ '--email', 'barney@example.com', From eddab621094e5da9f9c6bf2ae27a872625c8e135 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Fri, 21 Feb 2014 17:22:26 -0700 Subject: [PATCH 0067/3494] Fix volume commands with multiple regions The region_name was not passed into the the client causing volume commands to fail if there were multiple regions. Change-Id: I066dbbc4852f412e017daeeb16a3f186d3f91d2f Closes-Bug: #1241177 --- openstackclient/volume/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openstackclient/volume/client.py b/openstackclient/volume/client.py index a53203f1a8..2d82437268 100644 --- a/openstackclient/volume/client.py +++ b/openstackclient/volume/client.py @@ -48,6 +48,7 @@ def make_client(instance): auth_url=instance._auth_url, cacert=instance._cacert, insecure=instance._insecure, + region_name=instance._region_name, http_log_debug=http_log_debug ) From 125393210bd40d70d77a4667922a4a33f569a7ef Mon Sep 17 00:00:00 2001 From: OpenStack Jenkins Date: Sun, 23 Feb 2014 09:31:55 +0000 Subject: [PATCH 0068/3494] Updated from global requirements Change-Id: Icad0fcf4125b4aefac7ed8f0df5bef2c6ee64594 --- requirements.txt | 4 ++-- test-requirements.txt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements.txt b/requirements.txt index cfbc598b57..3bf92e09d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ -pbr>=0.5.21,<1.0 +pbr>=0.6,<1.0 cliff>=1.4.3 keyring>=1.6.1,<2.0,>=2.1 pycrypto>=2.6 python-glanceclient>=0.9.0 -python-keystoneclient>=0.4.2 +python-keystoneclient>=0.6.0 python-novaclient>=2.15.0 python-cinderclient>=1.0.6 requests>=1.1 diff --git a/test-requirements.txt b/test-requirements.txt index 34beccaeb3..7e1fa06b51 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,8 +5,8 @@ discover fixtures>=0.3.14 mock>=1.0 sphinx>=1.1.2,<1.2 -testrepository>=0.0.17 -testtools>=0.9.32 -WebOb>=1.2.3,<1.3 +testrepository>=0.0.18 +testtools>=0.9.34 +WebOb>=1.2.3 Babel>=1.3 From 8be3e249b6bb63188e8f31874827bb9c4b45ff40 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Wed, 26 Feb 2014 13:26:45 +1000 Subject: [PATCH 0069/3494] Use cacert values when creating identity client These were ignored when the client was created with a username and password. Change-Id: Id7557a5b07a41c7f79ab1a05ede385da31889940 Closes-Bug: #1284957 --- openstackclient/identity/client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openstackclient/identity/client.py b/openstackclient/identity/client.py index a71dfc7add..b58098e90a 100644 --- a/openstackclient/identity/client.py +++ b/openstackclient/identity/client.py @@ -40,7 +40,10 @@ def make_client(instance): LOG.debug('instantiating identity client: token flow') client = identity_client( endpoint=instance._url, - token=instance._token) + token=instance._token, + cacert=instance._cacert, + insecure=instance._insecure, + ) else: LOG.debug('instantiating identity client: password flow') client = identity_client( From e9015b94f91d3ff6972109fb21a2b5e7fd3b9a79 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Thu, 27 Feb 2014 10:55:37 -0700 Subject: [PATCH 0070/3494] In anticipation of network agents, rename compute Rename the compute agents in anticipation of network agents Change-Id: I201121915638d89dfbe46a7e0026aa4c2777e590 Closes-Bug: #1285800 --- openstackclient/compute/v2/agent.py | 8 ++++---- setup.cfg | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openstackclient/compute/v2/agent.py b/openstackclient/compute/v2/agent.py index c8fb6ccc0d..ae7ba9620c 100644 --- a/openstackclient/compute/v2/agent.py +++ b/openstackclient/compute/v2/agent.py @@ -26,7 +26,7 @@ class CreateAgent(show.ShowOne): - """Create agent command""" + """Create compute agent command""" log = logging.getLogger(__name__ + ".CreateAgent") @@ -75,7 +75,7 @@ def take_action(self, parsed_args): class DeleteAgent(command.Command): - """Delete agent command""" + """Delete compute agent command""" log = logging.getLogger(__name__ + ".DeleteAgent") @@ -95,7 +95,7 @@ def take_action(self, parsed_args): class ListAgent(lister.Lister): - """List agent command""" + """List compute agent command""" log = logging.getLogger(__name__ + ".ListAgent") @@ -127,7 +127,7 @@ def take_action(self, parsed_args): class SetAgent(show.ShowOne): - """Set agent command""" + """Set compute agent command""" log = logging.getLogger(__name__ + ".SetAgent") diff --git a/setup.cfg b/setup.cfg index a2c34e145a..8852757be0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,10 +40,10 @@ openstack.common = quota_show = openstackclient.common.quota:ShowQuota openstack.compute.v2 = - agent_create = openstackclient.compute.v2.agent:CreateAgent - agent_delete = openstackclient.compute.v2.agent:DeleteAgent - agent_list = openstackclient.compute.v2.agent:ListAgent - agent_set = openstackclient.compute.v2.agent:SetAgent + compute_agent_create = openstackclient.compute.v2.agent:CreateAgent + compute_agent_delete = openstackclient.compute.v2.agent:DeleteAgent + compute_agent_list = openstackclient.compute.v2.agent:ListAgent + compute_agent_set = openstackclient.compute.v2.agent:SetAgent aggregate_add_host = openstackclient.compute.v2.aggregate:AddAggregateHost aggregate_create = openstackclient.compute.v2.aggregate:CreateAggregate From 038269cf7fbbfad9ccc9d85a762e2ff4f7d97d11 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 21 Feb 2014 12:46:07 -0600 Subject: [PATCH 0071/3494] Update release notes for 0.3.1 * update README * update man page * fix doc errors Change-Id: I5682654bf482289879c8ba9016e348f2b2782971 --- README.rst | 40 +++++++------ doc/source/commands.rst | 30 ++++++++++ doc/source/man/openstack.rst | 107 ++++++++++++++++++++++++----------- doc/source/releases.rst | 23 ++++++-- 4 files changed, 147 insertions(+), 53 deletions(-) diff --git a/README.rst b/README.rst index 00c920ba36..937c0fee25 100644 --- a/README.rst +++ b/README.rst @@ -2,8 +2,9 @@ OpenStack Client ================ -python-openstackclient is a unified command-line client for the OpenStack APIs. -It is a thin wrapper to the stock python-*client modules that implement the +OpenStackclient (aka ``python-openstackclient``) is a command-line client for +the OpenStack APIs. +It is primarily a wrapper to the stock python-*client modules that implement the actual REST API client actions. This is an implementation of the design goals shown in @@ -14,8 +15,7 @@ operations in OpenStack. The master repository is on GitHub_. .. _OpenStack Client Wiki: https://wiki.openstack.org/wiki/OpenStackClient .. _GitHub: https://github.com/openstack/python-openstackclient -python-openstackclient is designed to add support for API extensions via a -plugin mechanism. +OpenStackclient has a plugin mechanism to add support for API extensions. * `Release management`_ * `Blueprints and feature specifications`_ @@ -32,32 +32,41 @@ plugin mechanism. Note ==== -OpenStackClient is considered to be alpha release quality as of the 0.2 release; +OpenStackClient is considered to be beta release quality as of the 0.3 release; no assurances are made at this point for ongoing compatibility in command forms or output. We do not, however, expect any major changes at this point. Getting Started =============== -We recommend using a virtualenv to install the client. This description -uses the `install virtualenv`_ script to create the virtualenv:: +OpenStackclient can be installed from PyPI using pip:: + + pip install python-openstackclient + +Developers can use the `install virtualenv`_ script to create the virtualenv:: python tools/install_venv.py source .venv/bin/activate python setup.py develop -Unit tests can be ran simply by running:: - - run_tests.sh +Unit tests are now run using tox. The ``run_test.sh`` script provides compatability +but is generally considered deprecated. The client can be called interactively by simply typing:: openstack -Alternatively command line parameters can be called non-interactively:: +There are a few variants on getting help. A list of global options and supported +commands is shown with ``--help``:: openstack --help +There is also a ``help`` command that can be used to get help text for a specific +command:: + + openstack help + openstack help server create + Configuration ============= @@ -116,10 +125,9 @@ The source is maintained in the ``doc/source`` folder using .. _reStructuredText: http://docutils.sourceforge.net/rst.html .. _Sphinx: http://sphinx.pocoo.org/ -* Building Manually:: +Building Manually:: - $ export DJANGO_SETTINGS_MODULE=local.local_settings - $ python doc/generate_autodoc_index.py - $ sphinx-build -b html doc/source build/sphinx/html + cd doc + make html -Results are in the `build/sphinx/html` directory. +Results are in the ``build/html`` directory. diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 0b93c64d98..31e7c14831 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -61,6 +61,36 @@ They follow the same style as the global options and always appear between the command and any positional arguments the command requires. +Actions +------- + +The actions used by OpenStackClient are defined below to provide a consistent meaning to each action. Many of them have logical opposite actions. Those actions with an opposite action are noted in parens if applicable. + +* authorize - authorize a token (used in OAuth) +* add (remove) - add some object to a container object; the command is built in the order of "container add object" ( ), the positional arguments appear in the same order +* attach (detach) - deprecated; use add/remove +* create (delete) - create a new occurrance of the specified object +* delete (create) - delete a specific occurrance of the specified object +* detach (attach) - deprecated; use add/remove +* list - display summary information about multiple objects +* lock (unlock) +* migrate - move a server to a different host; --live performs a live migration if possible +* pause (unpause) - stop a server and leave it in memory +* reboot - forcibly reboot a server +* rebuild - rebuild a server using (most of) the same arguments as in the original create +* remove (add) - remove an object from a group of objects +* rescue (unrescue) - reboot a server in a special rescue mode allowing access to the original disks +* resize - change a server's flavor +* resume (suspend) - return a suspended server to running state +* save - download an object locally +* set (unset) - set a property on the object, formerly called metadata +* show - display detailed information about the specific object +* suspend (resume) - stop a server and save to disk freeing memory +* unlock (lock) +* unpause (pause) - return a paused server to running state +* unrescue (rescue) - return a server to normal boot mode +* unset (set) - remove an attribute of the object + Implementation ============== diff --git a/doc/source/man/openstack.rst b/doc/source/man/openstack.rst index 3a780e2472..74db68154a 100644 --- a/doc/source/man/openstack.rst +++ b/doc/source/man/openstack.rst @@ -18,7 +18,7 @@ DESCRIPTION =========== :program:`openstack` provides a common command-line interface to OpenStack APIs. It is generally -equivalent to the CLIs provided by the OpenStack project client librariess, but with +equivalent to the CLIs provided by the OpenStack project client libraries, but with a distinct and consistent command structure. :program:`openstack` uses a similar authentication scheme as the OpenStack project CLIs, with @@ -37,49 +37,51 @@ command line. The primary difference is the use of 'project' in the name of the OPTIONS ======= +:program:`openstack` takes global options that control overall behaviour and command-specific options that control the command operation. Most global options have a corresponding environment variable that may also be used to set the value. If both are present, the command-line option takes priority. The environment variable names are derived from the option name by dropping the leading dashes ('--'), converting each embedded dash ('-') to an underscore ('_'), and converting to upper case. + :program:`openstack` recognizes the following global topions: -:option:`--os-auth-url ` +:option:`--os-auth-url` Authentication URL -:option:`--os-project-name ` - Authentication project name (only one of :option:`--os-project-name` or :option:`--os-project-id` need be supplied) +:option:`--os-domain-name` | :option:`--os-domain-id` + Domain-level authorization scope (name or ID) + +:option:`--os-project-name` | :option:`--os-project-id` + Project-level authentication scope (name or ID) -:option:`--os-project-id ` - Authentication tenant ID (only one of :option:`--os-project-name` or :option:`--os-project-id` need be supplied) +:option:`--os-project-domain-name` | :option:`--os-project-domain-id` + Domain name or id containing project -:option:`--os-username ` +:option:`--os-username` Authentication username -:option:`--os-password ` +:option:`--os-user-domain-name` | :option:`--os-user-domain-id` + Domain name or id containing user + +:option:`--os-password` Authentication password -:option:`--os-region-name ` +:option:`--os-region-name` Authentication region name -:option:`--os-default-domain ` +:option:`--os-default-domain` Default domain ID (Default: 'default') -:options:`--os-use-keyring` +:option:`--os-use-keyring` Use keyring to store password (default: False) -:option:`--os-cacert ` +:option:`--os-cacert` CA certificate bundle file -:option:`--verify|--insecure` +:option:`--verify` | :option:`--insecure` Verify or ignore server certificate (default: verify) -:option:`--os-identity-api-version ` +:option:`--os-identity-api-version` Identity API version (Default: 2.0) -:option:`--os-XXXX-api-version ` - Additional API version options will be presend depending on the installed API libraries. - - -NOTES -===== - -[This section intentionally left blank. So there.] +:option:`--os-XXXX-api-version` + Additional API version options will be available depending on the installed API libraries. COMMANDS @@ -87,7 +89,7 @@ COMMANDS To get a list of the available commands:: - openstack -h + openstack --help To get a description of a specific command:: @@ -101,12 +103,23 @@ To get a description of a specific command:: Print help for an individual command +NOTES +===== + +The command list displayed in help output reflects the API versions selected. For +example, to see Identity v3 commands ``OS_IDENTITY_API_VERSION`` must be set to ``3``. + + EXAMPLES ======== Show the detailed information for server ``appweb01``:: - openstack --os-tenant-name ExampleCo --os-username demo --os-password secrete --os-auth-url http://localhost:5000:/v2.0 server show appweb01 + openstack \ + --os-project-name ExampleCo \ + --os-username demo --os-password secrete \ + --os-auth-url http://localhost:5000:/v2.0 \ + server show appweb01 The same command if the auth environment variables (:envvar:`OS_AUTH_URL`, :envvar:`OS_PROJECT_NAME`, :envvar:`OS_USERNAME`, :envvar:`OS_PASSWORD`) are set:: @@ -126,25 +139,53 @@ Create a new image:: FILES ===== - :file:`~/.openstack` +:file:`~/.openstack` + Placeholder for future local state directory. This directory is intended to be shared among multiple OpenStack-related applications; contents are namespaced with an identifier for the app that owns it. Shared contents (such as :file:`~/.openstack/cache`) have no prefix and the contents must be portable. ENVIRONMENT VARIABLES ===================== -The following environment variables can be set to alter the behaviour of :program:`openstack` +The following environment variables can be set to alter the behaviour of :program:`openstack`. Most of them have corresponding command-line options that take precedence if set. + +:envvar:`OS_AUTH_URL` + Authentication URL + +:envvar:`OS_DOMAIN_NAME` + Domain-level authorization scope (name or ID) + +:envvar:`OS_PROJECT_NAME` + Project-level authentication scope (name or ID) + +:envvar:`OS_PROJECT_DOMAIN_NAME` + Domain name or id containing project :envvar:`OS_USERNAME` - Set the username + Authentication username + +:envvar:`OS_USER_DOMAIN_NAME` + Domain name or id containing user :envvar:`OS_PASSWORD` - Set the password + Authentication password -:envvar:`OS_PROJECT_NAME` - Set the project name +:envvar:`OS_REGION_NAME` + Authentication region name -:envvar:`OS_AUTH_URL` - Set the authentication URL +:envvar:`OS_DEFAULT_DOMAIN` + Default domain ID (Default: ‘default’) + +:envvar:`OS_USE_KEYRING` + Use keyring to store password (default: False) + +:envvar:`OS_CACERT` + CA certificate bundle file + +:envvar:`OS_IDENTITY_API_VERISON` + Identity API version (Default: 2.0) + +:envvar:`OS_XXXX_API_VERISON` + Additional API version options will be available depending on the installed API libraries. BUGS @@ -163,7 +204,7 @@ Please refer to the AUTHORS file distributed with OpenStackClient. COPYRIGHT ========= -Copyright 2011-2013 OpenStack Foundation and the authors listed in the AUTHORS file. +Copyright 2011-2014 OpenStack Foundation and the authors listed in the AUTHORS file. LICENSE diff --git a/doc/source/releases.rst b/doc/source/releases.rst index 7d74318e11..34bdce62db 100644 --- a/doc/source/releases.rst +++ b/doc/source/releases.rst @@ -2,6 +2,25 @@ Release Notes ============= +0.3.1 (28 Feb 2014) +=================== + +* add ``token create`` command +* internal changes for Python 3.3 compatability +* Bug 1100116_: Prompt interactive user for passwords in ``user create`` and ``user set`` +* Bug 1198171_: add domain support options for Identity v3 +* Bug 1241177_: Fix region handling in volume commands +* Bug 1256935_: Clean up ``security group rule list`` output format +* Bug 1269821_: Fix for unreleased Glance client change in internal class structure +* Bug 1284957_: Correctly pass ``--cacert`` and ``--insecure`` to Identity client in token flow auth + +.. _1100116: https://bugs.launchpad.net/ubuntu/+source/python-keystoneclient/+bug/1100116 +.. _1198171: https://bugs.launchpad.net/keystone/+bug/1198171 +.. _1241177: https://bugs.launchpad.net/python-openstackclient/+bug/1241177 +.. _1256935: https://bugs.launchpad.net/python-openstackclient/+bug/1256935 +.. _1269821: https://bugs.launchpad.net/python-openstackclient/+bug/1269821 +.. _1284957: https://bugs.launchpad.net/python-openstackclient/+bug/1284957 + 0.3.0 (17 Dec 2013) =================== @@ -10,10 +29,6 @@ Release Notes * add options to support TLS cetificate verification * add object-store show commands for container and object -.. commented to save format of bug fix -.. * 1254168_: OS_REGION_NAME is not used -.. _1254168: https://bugs.launchpad.net/python-openstackclient/+bug/1254168 - 0.2.2 (20 Sep 2013) =================== From e6e0dbf754c4dbc631e5c797b50d8032481a1a27 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 26 Feb 2014 12:13:01 -0700 Subject: [PATCH 0072/3494] Add --volume option to image create command Add ability to create an image from a volume. * Added --volume command to image create * Added --force option to image create * Added block to access volume manager in image create * Tests added for the volume option Change-Id: I3910a2b5e04acd0d15dd230747ba6ebca07aa316 Closes-Bug: #1207615 --- openstackclient/image/v1/image.py | 59 ++++++++++----- openstackclient/tests/image/v1/test_image.py | 76 ++++++++++++++++++++ 2 files changed, 119 insertions(+), 16 deletions(-) diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index 026b583cba..8c1501bdb0 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -111,6 +111,19 @@ def get_parser(self, prog_name): help="Similar to --location, but this indicates that the image" " should immediately be copied from the data store", ) + parser.add_argument( + "--volume", + metavar="", + help="Create the image from the specified volume", + ) + parser.add_argument( + "--force", + dest='force', + action='store_true', + default=False, + help="If the image is created from a volume, force creation of the" + " image even if volume is in use.", + ) parser.add_argument( "--property", dest="properties", @@ -162,7 +175,9 @@ def take_action(self, parsed_args): args.pop("variables") if "location" not in args and "copy_from" not in args: - if "file" in args: + if "volume" in args: + pass + elif "file" in args: args["data"] = open(args.pop("file"), "rb") else: args["data"] = None @@ -171,23 +186,35 @@ def take_action(self, parsed_args): msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) args["data"] = sys.stdin - image_client = self.app.client_manager.image - try: - image = utils.find_resource( - image_client.images, - parsed_args.name, - ) - except exceptions.CommandError: - # This is normal for a create or reserve (create w/o an image) - image = image_client.images.create(**args) + if "volume" in args: + volume_client = self.app.client_manager.volume + source_volume = utils.find_resource(volume_client.volumes, + parsed_args.volume) + response, body = volume_client.volumes.upload_to_image( + source_volume, + parsed_args.force, + parsed_args.name, + parsed_args.container_format, + parsed_args.disk_format) + info = body['os-volume_upload_image'] else: - # It must be an update - # If an image is specified via --file, --location or --copy-from - # let the API handle it - image = image_client.images.update(image, **args) + image_client = self.app.client_manager.image + try: + image = utils.find_resource( + image_client.images, + parsed_args.name, + ) + except exceptions.CommandError: + # This is normal for a create or reserve (create w/o an image) + image = image_client.images.create(**args) + else: + # It must be an update + # If an image is specified via --file, --location or + # --copy-from let the API handle it + image = image_client.images.update(image, **args) - info = {} - info.update(image._info) + info = {} + info.update(image._info) return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/tests/image/v1/test_image.py b/openstackclient/tests/image/v1/test_image.py index a410674d71..d7547f7630 100644 --- a/openstackclient/tests/image/v1/test_image.py +++ b/openstackclient/tests/image/v1/test_image.py @@ -14,6 +14,7 @@ # import copy +import mock from openstackclient.image.v1 import image from openstackclient.tests import fakes @@ -30,6 +31,81 @@ def setUp(self): self.images_mock.reset_mock() +class TestImageCreate(TestImage): + + def setUp(self): + super(TestImageCreate, self).setUp() + self.images_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(image_fakes.IMAGE), + loaded=True, + ) + self.cmd = image.CreateImage(self.app, None) + + def test_create_volume(self): + arglist = [ + '--volume', 'volly', + image_fakes.image_name, + ] + verifylist = [ + ('volume', 'volly'), + ('name', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.app.client_manager.volume = mock.Mock() + self.app.client_manager.volume.volumes = mock.Mock() + volumes = self.app.client_manager.volume.volumes + volumes.upload_to_image = mock.Mock() + response = {"id": 'volume_id', + "updated_at": 'updated_at', + "status": 'uploading', + "display_description": 'desc', + "size": 'size', + "volume_type": 'volume_type', + "image_id": 'image1', + "container_format": parsed_args.container_format, + "disk_format": parsed_args.disk_format, + "image_name": parsed_args.name} + full_response = {"os-volume_upload_image": response} + volumes.upload_to_image.return_value = (201, full_response) + volume_resource = fakes.FakeResource( + None, + copy.deepcopy({'id': 'vol1', 'name': 'volly'}), + loaded=True, + ) + volumes.get.return_value = volume_resource + results = self.cmd.take_action(parsed_args) + volumes.upload_to_image.assert_called_with( + volume_resource, + False, + image_fakes.image_name, + 'bare', + 'raw', + ) + expects = [('container_format', + 'disk_format', + 'display_description', + 'id', + 'image_id', + 'image_name', + 'size', + 'status', + 'updated_at', + 'volume_type'), + ('bare', + 'raw', + 'desc', + 'volume_id', + 'image1', + 'graven', + 'size', + 'uploading', + 'updated_at', + 'volume_type')] + for expected, result in zip(expects, results): + self.assertEqual(expected, result) + + class TestImageDelete(TestImage): def setUp(self): From 64a33b0aa59a1d22fadc41bffebfab04dd6c2cc7 Mon Sep 17 00:00:00 2001 From: OpenStack Jenkins Date: Wed, 5 Mar 2014 19:30:08 +0000 Subject: [PATCH 0073/3494] Updated from global requirements Change-Id: Ic90d9682a9c15795928c0c5b64c41bd06d74243a --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 3bf92e09d0..73c98d8aeb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ keyring>=1.6.1,<2.0,>=2.1 pycrypto>=2.6 python-glanceclient>=0.9.0 python-keystoneclient>=0.6.0 -python-novaclient>=2.15.0 +python-novaclient>=2.16.0 python-cinderclient>=1.0.6 requests>=1.1 -six>=1.4.1 +six>=1.5.2 From 70e6333e7d4b3fa87496ff3a888527693875273b Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Thu, 6 Mar 2014 12:50:37 -0700 Subject: [PATCH 0074/3494] Add ability to set key value pairs in projects Add supporto of extra key value pairs for projects (aka tenants) * Added option --property key=value to create and set commands * Support for versions v2 and v3 Change-Id: I84064b8b308579d1b66c83b1ed3d1a37614ec087 Closes-Bug: #1220280 --- openstackclient/identity/v2_0/project.py | 22 ++++++ openstackclient/identity/v3/project.py | 22 ++++++ .../tests/identity/v2_0/test_project.py | 65 +++++++++++++++++ .../tests/identity/v3/test_project.py | 69 +++++++++++++++++++ 4 files changed, 178 insertions(+) diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index 2d0acb8fe0..60a52ad4a6 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -22,6 +22,7 @@ from cliff import lister from cliff import show +from openstackclient.common import parseractions from openstackclient.common import utils @@ -53,6 +54,13 @@ def get_parser(self, prog_name): action='store_true', help='Disable project', ) + parser.add_argument( + '--property', + metavar='', + action=parseractions.KeyValueAction, + help='Property to add for this project ' + '(repeat option to set multiple properties)', + ) return parser def take_action(self, parsed_args): @@ -62,11 +70,15 @@ def take_action(self, parsed_args): enabled = True if parsed_args.disable: enabled = False + kwargs = {} + if parsed_args.property: + kwargs = parsed_args.property.copy() project = identity_client.tenants.create( parsed_args.name, description=parsed_args.description, enabled=enabled, + **kwargs ) info = {} @@ -163,6 +175,13 @@ def get_parser(self, prog_name): action='store_true', help='Disable project', ) + parser.add_argument( + '--property', + metavar='', + action=parseractions.KeyValueAction, + help='Property to add for this project ' + '(repeat option to set multiple properties)', + ) return parser def take_action(self, parsed_args): @@ -172,6 +191,7 @@ def take_action(self, parsed_args): if (not parsed_args.name and not parsed_args.description and not parsed_args.enable + and not parsed_args.property and not parsed_args.disable): return @@ -189,6 +209,8 @@ def take_action(self, parsed_args): kwargs['enabled'] = True if parsed_args.disable: kwargs['enabled'] = False + if parsed_args.property: + kwargs.update(parsed_args.property) if 'id' in kwargs: del kwargs['id'] if 'name' in kwargs: diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index f245a88898..ebae733df9 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -22,6 +22,7 @@ from cliff import lister from cliff import show +from openstackclient.common import parseractions from openstackclient.common import utils @@ -58,6 +59,13 @@ def get_parser(self, prog_name): action='store_true', help='Disable project', ) + parser.add_argument( + '--property', + metavar='', + action=parseractions.KeyValueAction, + help='Property to add for this project ' + '(repeat option to set multiple properties)', + ) return parser def take_action(self, parsed_args): @@ -75,12 +83,16 @@ def take_action(self, parsed_args): enabled = True if parsed_args.disable: enabled = False + kwargs = {} + if parsed_args.property: + kwargs = parsed_args.property.copy() project = identity_client.projects.create( parsed_args.name, domain, description=parsed_args.description, enabled=enabled, + **kwargs ) info = {} @@ -182,6 +194,13 @@ def get_parser(self, prog_name): action='store_true', help='Disable project', ) + parser.add_argument( + '--property', + metavar='', + action=parseractions.KeyValueAction, + help='Property to add for this project ' + '(repeat option to set multiple properties)', + ) return parser def take_action(self, parsed_args): @@ -192,6 +211,7 @@ def take_action(self, parsed_args): and not parsed_args.description and not parsed_args.domain and not parsed_args.enable + and not parsed_args.property and not parsed_args.disable): return @@ -214,6 +234,8 @@ def take_action(self, parsed_args): kwargs['enabled'] = True if parsed_args.disable: kwargs['enabled'] = False + if parsed_args.property: + kwargs.update(parsed_args.property) if 'id' in kwargs: del kwargs['id'] if 'domain_id' in kwargs: diff --git a/openstackclient/tests/identity/v2_0/test_project.py b/openstackclient/tests/identity/v2_0/test_project.py index 30f4278bea..d046cd4796 100644 --- a/openstackclient/tests/identity/v2_0/test_project.py +++ b/openstackclient/tests/identity/v2_0/test_project.py @@ -182,6 +182,43 @@ def test_project_create_disable(self): ) self.assertEqual(data, datalist) + def test_project_create_property(self): + arglist = [ + '--property', 'fee=fi', + '--property', 'fo=fum', + identity_fakes.project_name, + ] + verifylist = [ + ('property', {'fee': 'fi', 'fo': 'fum'}), + ('name', identity_fakes.project_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'description': None, + 'enabled': True, + 'fee': 'fi', + 'fo': 'fum', + } + self.projects_mock.create.assert_called_with( + identity_fakes.project_name, + **kwargs + ) + + collist = ('description', 'enabled', 'id', 'name') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.project_description, + True, + identity_fakes.project_id, + identity_fakes.project_name, + ) + self.assertEqual(data, datalist) + class TestProjectDelete(TestProject): @@ -412,6 +449,34 @@ def test_project_set_disable(self): **kwargs ) + def test_project_set_property(self): + arglist = [ + '--property', 'fee=fi', + '--property', 'fo=fum', + identity_fakes.project_name, + ] + verifylist = [ + ('property', {'fee': 'fi', 'fo': 'fum'}), + ('project', identity_fakes.project_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(result, 0) + + # Set expected values + kwargs = { + 'description': identity_fakes.project_description, + 'enabled': True, + 'tenant_name': identity_fakes.project_name, + 'fee': 'fi', + 'fo': 'fum', + } + self.projects_mock.update.assert_called_with( + identity_fakes.project_id, + **kwargs + ) + class TestProjectShow(TestProject): diff --git a/openstackclient/tests/identity/v3/test_project.py b/openstackclient/tests/identity/v3/test_project.py index 02cb41bedb..517c73c594 100644 --- a/openstackclient/tests/identity/v3/test_project.py +++ b/openstackclient/tests/identity/v3/test_project.py @@ -245,6 +245,46 @@ def test_project_create_disable(self): ) self.assertEqual(data, datalist) + def test_project_create_property(self): + arglist = [ + '--property', 'fee=fi', + '--property', 'fo=fum', + identity_fakes.project_name, + ] + verifylist = [ + ('property', {'fee': 'fi', 'fo': 'fum'}), + ('name', identity_fakes.project_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'description': None, + 'enabled': True, + 'fee': 'fi', + 'fo': 'fum', + } + # ProjectManager.create(name, domain, description=, enabled=, **kwargs) + self.projects_mock.create.assert_called_with( + identity_fakes.project_name, + None, + **kwargs + ) + + collist = ('description', 'domain_id', 'enabled', 'id', 'name') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.project_description, + identity_fakes.domain_id, + True, + identity_fakes.project_id, + identity_fakes.project_name, + ) + self.assertEqual(data, datalist) + class TestProjectDelete(TestProject): @@ -488,6 +528,35 @@ def test_project_set_disable(self): **kwargs ) + def test_project_set_property(self): + arglist = [ + '--property', 'fee=fi', + '--property', 'fo=fum', + identity_fakes.project_name, + ] + verifylist = [ + ('property', {'fee': 'fi', 'fo': 'fum'}), + ('project', identity_fakes.project_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(result, 0) + + # Set expected values + kwargs = { + 'description': identity_fakes.project_description, + 'domain': identity_fakes.domain_id, + 'enabled': True, + 'name': identity_fakes.project_name, + 'fee': 'fi', + 'fo': 'fum', + } + self.projects_mock.update.assert_called_with( + identity_fakes.project_id, + **kwargs + ) + class TestProjectShow(TestProject): From 211cd31d7ac2c7768cc871ac7c9228f7713b8dfc Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Thu, 6 Mar 2014 13:56:10 -0700 Subject: [PATCH 0075/3494] Make bash comple command best effort to authorize If authorization fails for the complete command, generate the bash complete anyway. * Added best_effort flag to command * Attempts to authorize for bash complete, but if it fails, it tries anyway Change-Id: I796258f8044f42abc6a51164d920a26f73397962 Partial-Bug: #1283550 --- openstackclient/shell.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 49307992c7..cc4570a178 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -24,6 +24,7 @@ from cliff import app from cliff import command +from cliff import complete from cliff import help import openstackclient @@ -64,8 +65,10 @@ class OpenStackShell(app.App): def __init__(self): # Patch command.Command to add a default auth_required = True command.Command.auth_required = True + command.Command.best_effort = False # But not help help.HelpCommand.auth_required = False + complete.CompleteCommand.best_effort = True super(OpenStackShell, self).__init__( description=__doc__.strip(), @@ -465,7 +468,15 @@ def prepare_to_run_command(self, cmd): """Set up auth and API versions""" self.log.debug('prepare_to_run_command %s', cmd.__class__.__name__) - if cmd.auth_required: + if not cmd.auth_required: + return + if cmd.best_effort: + try: + self.authenticate_user() + self.restapi.set_auth(self.client_manager.identity.auth_token) + except Exception: + pass + else: self.authenticate_user() self.restapi.set_auth(self.client_manager.identity.auth_token) return From 14548aa69a25f6a57c0d5bbccae2d5243a99dfef Mon Sep 17 00:00:00 2001 From: Alex Holden Date: Fri, 7 Mar 2014 09:37:59 -0800 Subject: [PATCH 0076/3494] Fixed spelling errors - occurance to occurence Change-Id: Ie0550a1168448d85043d9b4943edd732b1f10307 --- doc/source/commands.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 31e7c14831..e0cfee7a47 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -69,8 +69,8 @@ The actions used by OpenStackClient are defined below to provide a consistent me * authorize - authorize a token (used in OAuth) * add (remove) - add some object to a container object; the command is built in the order of "container add object" ( ), the positional arguments appear in the same order * attach (detach) - deprecated; use add/remove -* create (delete) - create a new occurrance of the specified object -* delete (create) - delete a specific occurrance of the specified object +* create (delete) - create a new occurrence of the specified object +* delete (create) - delete a specific occurrence of the specified object * detach (attach) - deprecated; use add/remove * list - display summary information about multiple objects * lock (unlock) From 97667079dc286f4b32c7ce4f0440bb9109758772 Mon Sep 17 00:00:00 2001 From: Alex Holden Date: Fri, 7 Mar 2014 09:39:02 -0800 Subject: [PATCH 0077/3494] Fixed Spelling errors - compatability to compatibility Change-Id: I9da380cef8b798e21fd35882763bd05f5cf1e33e --- doc/source/releases.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/releases.rst b/doc/source/releases.rst index 34bdce62db..85a29ba9ff 100644 --- a/doc/source/releases.rst +++ b/doc/source/releases.rst @@ -6,7 +6,7 @@ Release Notes =================== * add ``token create`` command -* internal changes for Python 3.3 compatability +* internal changes for Python 3.3 compatibility * Bug 1100116_: Prompt interactive user for passwords in ``user create`` and ``user set`` * Bug 1198171_: add domain support options for Identity v3 * Bug 1241177_: Fix region handling in volume commands From 00188f092c76fd5bfaf0f6a67cd0b52ed473313b Mon Sep 17 00:00:00 2001 From: Alex Holden Date: Fri, 7 Mar 2014 09:40:29 -0800 Subject: [PATCH 0078/3494] Fixed spelling error, compatability to compatibility Change-Id: I72c1254666a741ffe1070cf8275af889fa323f52 --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 937c0fee25..86e224e0bb 100644 --- a/README.rst +++ b/README.rst @@ -49,7 +49,7 @@ Developers can use the `install virtualenv`_ script to create the virtualenv:: source .venv/bin/activate python setup.py develop -Unit tests are now run using tox. The ``run_test.sh`` script provides compatability +Unit tests are now run using tox. The ``run_test.sh`` script provides compatibility but is generally considered deprecated. The client can be called interactively by simply typing:: From 3435f188a400b21891981c4024c26bcefc7be861 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Wed, 19 Feb 2014 14:44:03 -0600 Subject: [PATCH 0079/3494] add interface and url to endpoint list endpoint list is not terribly useful without these details Change-Id: I65b0bdf7667d73ceaad5856171678cabcde003f3 --- openstackclient/identity/v3/endpoint.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/openstackclient/identity/v3/endpoint.py b/openstackclient/identity/v3/endpoint.py index e0b1f1a3cf..055f9fe2ee 100644 --- a/openstackclient/identity/v3/endpoint.py +++ b/openstackclient/identity/v3/endpoint.py @@ -114,23 +114,11 @@ class ListEndpoint(lister.Lister): log = logging.getLogger(__name__ + '.ListEndpoint') - def get_parser(self, prog_name): - parser = super(ListEndpoint, self).get_parser(prog_name) - parser.add_argument( - '--long', - action='store_true', - default=False, - help='List additional fields in output') - return parser - def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity - if parsed_args.long: - columns = ('ID', 'Region', 'Service Name', 'Service Type', - 'Enabled', 'Interface', 'URL') - else: - columns = ('ID', 'Region', 'Service Name', 'Enabled') + columns = ('ID', 'Region', 'Service Name', 'Service Type', + 'Enabled', 'Interface', 'URL') data = identity_client.endpoints.list() for ep in data: From 27ebdeb57dd78564770ef92b95bc659b25e10a69 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 7 Mar 2014 15:34:28 -0600 Subject: [PATCH 0080/3494] Fix 'keypair show' command output The attempt to get the data dict out of the keypair resource object uses a key 'keypair. This is incorrect, no key is required. Closes-Bug: 1289594 Change-Id: I7887119c1d800d389cb6f63ea7847bea1e25bb52 --- openstackclient/compute/v2/keypair.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstackclient/compute/v2/keypair.py b/openstackclient/compute/v2/keypair.py index d68dae0643..8a91f68208 100644 --- a/openstackclient/compute/v2/keypair.py +++ b/openstackclient/compute/v2/keypair.py @@ -146,7 +146,7 @@ def take_action(self, parsed_args): parsed_args.name) info = {} - info.update(keypair._info['keypair']) + info.update(keypair._info) if not parsed_args.public_key: del info['public_key'] return zip(*sorted(six.iteritems(info))) From 65f094e73802380b967c75c126c0938281973707 Mon Sep 17 00:00:00 2001 From: OpenStack Jenkins Date: Mon, 10 Mar 2014 21:37:10 +0000 Subject: [PATCH 0081/3494] Updated from global requirements Change-Id: I73f40a5fd569d0a5e341aabbece1885a7478d7f5 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 73c98d8aeb..5547d89676 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ keyring>=1.6.1,<2.0,>=2.1 pycrypto>=2.6 python-glanceclient>=0.9.0 python-keystoneclient>=0.6.0 -python-novaclient>=2.16.0 +python-novaclient>=2.17.0 python-cinderclient>=1.0.6 requests>=1.1 six>=1.5.2 From 0c0803d363aeae9e8a6eb2cdaf4b5baedee416dc Mon Sep 17 00:00:00 2001 From: Steven Hardy Date: Fri, 7 Mar 2014 18:25:41 +0000 Subject: [PATCH 0082/3494] identity v3 allow project list filtering by domain The underlying keystoneclient interface allows filtering by domain, so support it in the cli interface because it makes project list much nicer to use in a multi-domain deployment. Change-Id: If3f5cf1205c1e9cf314f8286a3ae81bda4456b8f Closes-Bug: #1289513 --- openstackclient/identity/v3/project.py | 14 ++++++++- .../tests/identity/v3/test_project.py | 29 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index ebae733df9..d3618fb3ce 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -140,15 +140,27 @@ def get_parser(self, prog_name): default=False, help='List additional fields in output', ) + parser.add_argument( + '--domain', + metavar='', + help='Filter by a specific domain', + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity if parsed_args.long: columns = ('ID', 'Name', 'Domain ID', 'Description', 'Enabled') else: columns = ('ID', 'Name') - data = self.app.client_manager.identity.projects.list() + kwargs = {} + if parsed_args.domain: + kwargs['domain'] = utils.find_resource( + identity_client.domains, + parsed_args.domain, + ).id + data = identity_client.projects.list(**kwargs) return (columns, (utils.get_item_properties( s, columns, diff --git a/openstackclient/tests/identity/v3/test_project.py b/openstackclient/tests/identity/v3/test_project.py index 517c73c594..0479d37996 100644 --- a/openstackclient/tests/identity/v3/test_project.py +++ b/openstackclient/tests/identity/v3/test_project.py @@ -376,6 +376,35 @@ def test_project_list_long(self): ), ) self.assertEqual(tuple(data), datalist) + def test_project_list_domain(self): + arglist = [ + '--domain', identity_fakes.domain_name, + ] + verifylist = [ + ('domain', identity_fakes.domain_name), + ] + + self.domains_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.DOMAIN), + loaded=True, + ) + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + self.projects_mock.list.assert_called_with( + domain=identity_fakes.domain_id) + + collist = ('ID', 'Name') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.project_id, + identity_fakes.project_name, + ), ) + self.assertEqual(tuple(data), datalist) + class TestProjectSet(TestProject): From 4900c64d091ffe4ec9050cd71845b12db49bd293 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Mon, 17 Mar 2014 17:32:41 -0600 Subject: [PATCH 0083/3494] Produce a useful error message for NoUniqueMatch Most of the CLIs use a NoUniqueMatch, so produce a useful error message if that happens. Added some tests for find_resource as well. Change-Id: I85ba61d5f6d1be5bd336a1cc4b02501492905f33 Closes-Bug: #1293846 --- openstackclient/common/utils.py | 4 ++ openstackclient/tests/common/test_utils.py | 72 ++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py index 7cd877ef61..d7702a3f0f 100644 --- a/openstackclient/common/utils.py +++ b/openstackclient/common/utils.py @@ -74,6 +74,10 @@ def find_resource(manager, name_or_id): msg = "No %s with a name or ID of '%s' exists." % \ (manager.resource_class.__name__.lower(), name_or_id) raise exceptions.CommandError(msg) + if type(ex).__name__ == 'NoUniqueMatch': + msg = "More than one %s exists with the name '%s'." % \ + (manager.resource_class.__name__.lower(), name_or_id) + raise exceptions.CommandError(msg) else: raise diff --git a/openstackclient/tests/common/test_utils.py b/openstackclient/tests/common/test_utils.py index 0ad4ca9bdb..3650746bc0 100644 --- a/openstackclient/tests/common/test_utils.py +++ b/openstackclient/tests/common/test_utils.py @@ -57,3 +57,75 @@ def test_get_password_cntrl_d(self): self.assertRaises(exceptions.CommandError, utils.get_password, mock_stdin) + + +class NoUniqueMatch(Exception): + pass + + +class TestFindResource(test_utils.TestCase): + def setUp(self): + super(TestFindResource, self).setUp() + self.name = 'legos' + self.expected = mock.Mock() + self.manager = mock.Mock() + self.manager.resource_class = mock.Mock() + self.manager.resource_class.__name__ = 'lego' + + def test_find_resource_get_int(self): + self.manager.get = mock.Mock(return_value=self.expected) + result = utils.find_resource(self.manager, 1) + self.assertEqual(self.expected, result) + self.manager.get.assert_called_with(1) + + def test_find_resource_get_int_string(self): + self.manager.get = mock.Mock(return_value=self.expected) + result = utils.find_resource(self.manager, "2") + self.assertEqual(self.expected, result) + self.manager.get.assert_called_with(2) + + def test_find_resource_get_uuid(self): + uuid = '9a0dc2a0-ad0d-11e3-a5e2-0800200c9a66' + self.manager.get = mock.Mock(return_value=self.expected) + result = utils.find_resource(self.manager, uuid) + self.assertEqual(self.expected, result) + self.manager.get.assert_called_with(uuid) + + def test_find_resource_get_whatever(self): + self.manager.get = mock.Mock(return_value=self.expected) + result = utils.find_resource(self.manager, 'whatever') + self.assertEqual(self.expected, result) + self.manager.get.assert_called_with('whatever') + + def test_find_resource_find(self): + self.manager.get = mock.Mock(side_effect=Exception('Boom!')) + self.manager.find = mock.Mock(return_value=self.expected) + result = utils.find_resource(self.manager, self.name) + self.assertEqual(self.expected, result) + self.manager.get.assert_called_with(self.name) + self.manager.find.assert_called_with(name=self.name) + + def test_find_resource_find_not_found(self): + self.manager.get = mock.Mock(side_effect=Exception('Boom!')) + self.manager.find = mock.Mock(side_effect= + exceptions.NotFound(404, "2")) + result = self.assertRaises(exceptions.CommandError, + utils.find_resource, + self.manager, + self.name) + self.assertEqual("No lego with a name or ID of 'legos' exists.", + str(result)) + self.manager.get.assert_called_with(self.name) + self.manager.find.assert_called_with(display_name=self.name) + + def test_find_resource_find_no_unique(self): + self.manager.get = mock.Mock(side_effect=Exception('Boom!')) + self.manager.find = mock.Mock(side_effect=NoUniqueMatch()) + result = self.assertRaises(exceptions.CommandError, + utils.find_resource, + self.manager, + self.name) + self.assertEqual("More than one lego exists with the name 'legos'.", + str(result)) + self.manager.get.assert_called_with(self.name) + self.manager.find.assert_called_with(display_name=self.name) From 8117256d7dc1464e86382db5c480d33b01131854 Mon Sep 17 00:00:00 2001 From: Chris Johnson Date: Fri, 21 Mar 2014 11:16:25 -0400 Subject: [PATCH 0084/3494] Correct display of project/tenant id on display of credentials This change corrects the display of ec2 credentails within the ListEC2Creds method. Added explicit headers and corrected data listt o specify tenant_id instead of project id. Change-Id: I2ea579082bee800d774f202bdc38e2d546e57e77 Closes-Bug: #1292337 --- openstackclient/identity/v2_0/ec2creds.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openstackclient/identity/v2_0/ec2creds.py b/openstackclient/identity/v2_0/ec2creds.py index cb3a51658f..cb60b677ca 100644 --- a/openstackclient/identity/v2_0/ec2creds.py +++ b/openstackclient/identity/v2_0/ec2creds.py @@ -135,9 +135,11 @@ def take_action(self, parsed_args): # Get the user from the current auth user = identity_client.auth_user_id - columns = ('Access', 'Secret', 'Project ID', 'User ID') + columns = ('access', 'secret', 'tenant_id', 'user_id') + column_headers = ('Access', 'Secret', 'Project ID', 'User ID') data = identity_client.ec2.list(user) - return (columns, + + return (column_headers, (utils.get_item_properties( s, columns, formatters={}, From bea6e6ac23e893a85f4b0a49bab52934aca86726 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 12 Mar 2014 11:51:17 -0600 Subject: [PATCH 0085/3494] Make endpoint commands more consistent Make endpoints more consistent across create, show, etc * Make the name option required for create * Use a common function to fetch services by id, name or type * Have show work by endpoint id or by service id, type or name * Have show display all the fields by default * Remove capability to filter queries by attribute value pairs Change-Id: Idaa4b8d930ba859fd62de777e44a10b1ed58c79b Partial-Bug: #1184012 --- openstackclient/identity/common.py | 38 +++++++++ openstackclient/identity/v2_0/endpoint.py | 97 ++++++----------------- openstackclient/identity/v2_0/service.py | 28 +------ openstackclient/identity/v3/endpoint.py | 13 ++- openstackclient/identity/v3/service.py | 16 +--- 5 files changed, 74 insertions(+), 118 deletions(-) create mode 100644 openstackclient/identity/common.py diff --git a/openstackclient/identity/common.py b/openstackclient/identity/common.py new file mode 100644 index 0000000000..6aeaa3c387 --- /dev/null +++ b/openstackclient/identity/common.py @@ -0,0 +1,38 @@ +# Copyright 2012-2013 OpenStack Foundation +# +# 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. +# + +"""Common identity code""" + +from keystoneclient import exceptions as identity_exc +from openstackclient.common import exceptions +from openstackclient.common import utils + + +def find_service(identity_client, name_type_or_id): + """Find a service by id, name or type.""" + + try: + # search for the usual ID or name + return utils.find_resource(identity_client.services, name_type_or_id) + except exceptions.CommandError: + try: + # search for service type + return identity_client.services.find(type=name_type_or_id) + # FIXME(dtroyer): This exception should eventually come from + # common client exceptions + except identity_exc.NotFound: + msg = ("No service with a type, name or ID of '%s' exists." + % name_type_or_id) + raise exceptions.CommandError(msg) diff --git a/openstackclient/identity/v2_0/endpoint.py b/openstackclient/identity/v2_0/endpoint.py index 0319c2680a..4ee1636ff0 100644 --- a/openstackclient/identity/v2_0/endpoint.py +++ b/openstackclient/identity/v2_0/endpoint.py @@ -22,9 +22,8 @@ from cliff import lister from cliff import show -from keystoneclient import exceptions as identity_exc -from openstackclient.common import exceptions from openstackclient.common import utils +from openstackclient.identity import common class CreateEndpoint(show.ShowOne): @@ -45,6 +44,7 @@ def get_parser(self, prog_name): parser.add_argument( '--publicurl', metavar='', + required=True, help='New endpoint public URL') parser.add_argument( '--adminurl', @@ -59,8 +59,7 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity - service = utils.find_resource(identity_client.services, - parsed_args.service) + service = common.find_service(identity_client, parsed_args.service) endpoint = identity_client.endpoints.create( parsed_args.region, service.id, @@ -120,8 +119,7 @@ def take_action(self, parsed_args): data = identity_client.endpoints.list() for ep in data: - service = utils.find_resource( - identity_client.services, ep.service_id) + service = common.find_service(identity_client, ep.service_id) ep.service_name = service.name ep.service_type = service.type return (columns, @@ -139,77 +137,30 @@ class ShowEndpoint(show.ShowOne): def get_parser(self, prog_name): parser = super(ShowEndpoint, self).get_parser(prog_name) parser.add_argument( - 'service', - metavar='', - help='Name or ID of service endpoint to display') - parser.add_argument( - '--type', - metavar='', - default='publicURL', - help='Endpoint type: publicURL, internalURL, adminURL ' + - '(default publicURL)') - parser.add_argument( - '--attr', - metavar='', - help='Endpoint attribute to use for selection') - parser.add_argument( - '--value', - metavar='', - help='Value of endpoint attribute to use for selection') - parser.add_argument( - '--all', - action='store_true', - default=False, - help='Show all endpoints for this service') + 'endpoint_or_service', + metavar='', + help='Endpoint ID or name, type or ID of service to display') return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity - - if not parsed_args.all: - # Find endpoint filtered by a specific attribute or service type - kwargs = { - 'service_type': parsed_args.service, - 'endpoint_type': parsed_args.type, - } - if parsed_args.attr and parsed_args.value: - kwargs.update({ - 'attr': parsed_args.attr, - 'filter_value': parsed_args.value, - }) - elif parsed_args.attr or parsed_args.value: - msg = 'Both --attr and --value required' - raise exceptions.CommandError(msg) - - url = identity_client.service_catalog.url_for(**kwargs) - info = {'%s.%s' % (parsed_args.service, parsed_args.type): url} - return zip(*sorted(six.iteritems(info))) - else: - # The Identity 2.0 API doesn't support retrieving a single - # endpoint so we have to do this ourselves - try: - service = utils.find_resource(identity_client.services, - parsed_args.service) - except exceptions.CommandError: - try: - # search for service type - service = identity_client.services.find( - type=parsed_args.service) - # FIXME(dtroyer): This exception should eventually come from - # common client exceptions - except identity_exc.NotFound: - msg = "No service with a type, name or ID of '%s' exists" \ - % parsed_args.service - raise exceptions.CommandError(msg) - - data = identity_client.endpoints.list() + data = identity_client.endpoints.list() + match = None + for ep in data: + if ep.id == parsed_args.endpoint_or_service: + match = ep + service = common.find_service(identity_client, ep.service_id) + if match is None: + service = common.find_service(identity_client, + parsed_args.endpoint_or_service) for ep in data: if ep.service_id == service.id: - info = {} - info.update(ep._info) - service = utils.find_resource(identity_client.services, - ep.service_id) - info['service_name'] = service.name - info['service_type'] = service.type - return zip(*sorted(six.iteritems(info))) + match = ep + if match is None: + return None + info = {} + info.update(match._info) + info['service_name'] = service.name + info['service_type'] = service.type + return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/identity/v2_0/service.py b/openstackclient/identity/v2_0/service.py index ea45f63431..d61804c80e 100644 --- a/openstackclient/identity/v2_0/service.py +++ b/openstackclient/identity/v2_0/service.py @@ -22,9 +22,9 @@ from cliff import lister from cliff import show -from keystoneclient import exceptions as identity_exc from openstackclient.common import exceptions from openstackclient.common import utils +from openstackclient.identity import common class CreateService(show.ShowOne): @@ -83,12 +83,7 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity - - service = utils.find_resource( - identity_client.services, - parsed_args.service, - ) - + service = common.find_service(identity_client, parsed_args.service) identity_client.services.delete(service.id) return @@ -159,24 +154,7 @@ def take_action(self, parsed_args): "exists." % (parsed_args.service)) raise exceptions.CommandError(msg) else: - try: - # search for the usual ID or name - service = utils.find_resource( - identity_client.services, - parsed_args.service, - ) - except exceptions.CommandError: - try: - # search for service type - service = identity_client.services.find( - type=parsed_args.service) - # FIXME(dtroyer): This exception should eventually come from - # common client exceptions - except identity_exc.NotFound: - msg = ("No service with a type, name or ID of '%s' exists." - % parsed_args.service) - raise exceptions.CommandError(msg) - + service = common.find_service(identity_client, parsed_args.service) info = {} info.update(service._info) return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/identity/v3/endpoint.py b/openstackclient/identity/v3/endpoint.py index 055f9fe2ee..a51383ea54 100644 --- a/openstackclient/identity/v3/endpoint.py +++ b/openstackclient/identity/v3/endpoint.py @@ -24,6 +24,7 @@ from cliff import show from openstackclient.common import utils +from openstackclient.identity import common class CreateEndpoint(show.ShowOne): @@ -69,8 +70,7 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity - service = utils.find_resource(identity_client.services, - parsed_args.service) + service = common.find_service(identity_client, parsed_args.service) endpoint = identity_client.endpoints.create( service.id, @@ -122,8 +122,7 @@ def take_action(self, parsed_args): data = identity_client.endpoints.list() for ep in data: - service = utils.find_resource( - identity_client.services, ep.service_id) + service = common.find_service(identity_client, ep.service_id) ep.service_name = service.name ep.service_type = service.type return (columns, @@ -182,8 +181,7 @@ def take_action(self, parsed_args): identity_client = self.app.client_manager.identity endpoint = utils.find_resource(identity_client.endpoints, parsed_args.endpoint) - service = utils.find_resource(identity_client.services, - parsed_args.service) + service = common.find_service(identity_client, parsed_args.service) if (not parsed_args.interface and not parsed_args.url and not parsed_args.service and not parsed_args.region): @@ -221,8 +219,7 @@ def take_action(self, parsed_args): endpoint = utils.find_resource(identity_client.endpoints, parsed_args.endpoint) - service = utils.find_resource(identity_client.services, - endpoint.service_id) + service = common.find_service(identity_client, endpoint.service_id) info = {} info.update(endpoint._info) diff --git a/openstackclient/identity/v3/service.py b/openstackclient/identity/v3/service.py index 7e3bfc6b2d..8576a6e6f9 100644 --- a/openstackclient/identity/v3/service.py +++ b/openstackclient/identity/v3/service.py @@ -23,6 +23,7 @@ from cliff import show from openstackclient.common import utils +from openstackclient.identity import common class CreateService(show.ShowOne): @@ -90,10 +91,7 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity - service = utils.find_resource( - identity_client.services, - parsed_args.service, - ) + service = common.find_service(identity_client, parsed_args.service) identity_client.services.delete(service.id) return @@ -161,10 +159,7 @@ def take_action(self, parsed_args): and not parsed_args.disable): return - service = utils.find_resource( - identity_client.services, - parsed_args.service, - ) + service = common.find_service(identity_client, parsed_args.service) kwargs = service._info if parsed_args.type: @@ -203,9 +198,6 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity - service = utils.find_resource( - identity_client.services, - parsed_args.service, - ) + service = common.find_service(identity_client, parsed_args.service) return zip(*sorted(six.iteritems(service._info))) From e72072adc3b62b5ef8e3076169fed19dea9995f7 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Mon, 31 Mar 2014 14:48:42 -0600 Subject: [PATCH 0086/3494] Fix the project option to user list so it filters The --project option to the user list command was not implemented * Allow users to be filted by project * Support id or name of project with the find_resource command * Make sure the report does not contain duplicates Change-Id: Ic0e10cccd7749d38a7d4b80bbdc68e61a660084b Closes-Bug: #1177255 --- openstackclient/identity/v2_0/user.py | 19 +++++++++++++++++-- .../tests/identity/v2_0/test_user.py | 12 +++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index 628be4b85c..688306baa1 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -156,6 +156,7 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity def _format_project(project): if not project: @@ -165,6 +166,14 @@ def _format_project(project): else: return project + project = None + if parsed_args.project: + project = utils.find_resource( + identity_client.tenants, + parsed_args.project, + ) + project = project.id + if parsed_args.long: columns = ( 'ID', @@ -183,14 +192,20 @@ def _format_project(project): # Cache the project list project_cache = {} try: - for p in self.app.client_manager.identity.tenants.list(): + for p in identity_client.tenants.list(): project_cache[p.id] = p except Exception: # Just forget it if there's any trouble pass else: columns = column_headers = ('ID', 'Name') - data = self.app.client_manager.identity.users.list() + data = identity_client.users.list(tenant_id=project) + + if parsed_args.project: + d = {} + for s in data: + d[s.id] = s + data = d.values() if parsed_args.long: # FIXME(dtroyer): Sometimes user objects have 'tenant_id' instead diff --git a/openstackclient/tests/identity/v2_0/test_user.py b/openstackclient/tests/identity/v2_0/test_user.py index dfb358ffd0..e191431c94 100644 --- a/openstackclient/tests/identity/v2_0/test_user.py +++ b/openstackclient/tests/identity/v2_0/test_user.py @@ -381,6 +381,11 @@ class TestUserList(TestUser): def setUp(self): super(TestUserList, self).setUp() + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT_2), + loaded=True, + ) self.projects_mock.list.return_value = [ fakes.FakeResource( None, @@ -408,7 +413,7 @@ def test_user_list_no_options(self): # DisplayCommandBase.take_action() returns two tuples columns, data = self.cmd.take_action(parsed_args) - self.users_mock.list.assert_called_with() + self.users_mock.list.assert_called_with(tenant_id=None) collist = ('ID', 'Name') self.assertEqual(columns, collist) @@ -426,11 +431,12 @@ def test_user_list_project(self): ('project', identity_fakes.project_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) + project_id = identity_fakes.PROJECT_2['id'] # DisplayCommandBase.take_action() returns two tuples columns, data = self.cmd.take_action(parsed_args) - self.users_mock.list.assert_called_with() + self.users_mock.list.assert_called_with(tenant_id=project_id) collist = ('ID', 'Name') self.assertEqual(columns, collist) @@ -452,7 +458,7 @@ def test_user_list_long(self): # DisplayCommandBase.take_action() returns two tuples columns, data = self.cmd.take_action(parsed_args) - self.users_mock.list.assert_called_with() + self.users_mock.list.assert_called_with(tenant_id=None) collist = ('ID', 'Name', 'Project', 'Email', 'Enabled') self.assertEqual(columns, collist) From 022b6d95d167405fa6534680c8a7fe449b35ce77 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Fri, 4 Apr 2014 08:09:43 +1000 Subject: [PATCH 0087/3494] Pass arguments to v3 keystoneclient by kwarg Keystoneclient has added the positional decorator which emits a warning if arguments aren't passed by keyword. This means we are getting warnings in certain places in openstackclient. Change-Id: Ic5446cd6f122cbb56fce543011386d53bc31fe18 Closes-Bug: #1302199 --- openstackclient/identity/v3/credential.py | 6 +-- openstackclient/identity/v3/domain.py | 4 +- openstackclient/identity/v3/endpoint.py | 20 ++++----- openstackclient/identity/v3/group.py | 2 +- openstackclient/identity/v3/policy.py | 2 +- openstackclient/identity/v3/project.py | 4 +- openstackclient/identity/v3/role.py | 2 +- openstackclient/identity/v3/service.py | 6 +-- openstackclient/identity/v3/user.py | 2 +- .../tests/identity/v3/test_project.py | 42 +++++++++++-------- .../tests/identity/v3/test_role.py | 9 +++- .../tests/identity/v3/test_service.py | 24 +++++------ .../tests/identity/v3/test_user.py | 32 +++++++------- 13 files changed, 83 insertions(+), 72 deletions(-) diff --git a/openstackclient/identity/v3/credential.py b/openstackclient/identity/v3/credential.py index b82825f00b..93f67f6c08 100644 --- a/openstackclient/identity/v3/credential.py +++ b/openstackclient/identity/v3/credential.py @@ -68,9 +68,9 @@ def take_action(self, parsed_args): else: project = None credential = identity_client.credentials.create( - user_id, - parsed_args.type, - parsed_args.data, + user=user_id, + type=parsed_args.type, + blob=parsed_args.data, project=project) return zip(*sorted(six.iteritems(credential._info))) diff --git a/openstackclient/identity/v3/domain.py b/openstackclient/identity/v3/domain.py index 1e9a4a2a0b..a74b12e295 100644 --- a/openstackclient/identity/v3/domain.py +++ b/openstackclient/identity/v3/domain.py @@ -61,8 +61,8 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity domain = identity_client.domains.create( - parsed_args.name, - parsed_args.description, + name=parsed_args.name, + description=parsed_args.description, enabled=parsed_args.enabled, ) diff --git a/openstackclient/identity/v3/endpoint.py b/openstackclient/identity/v3/endpoint.py index 055f9fe2ee..64334c636f 100644 --- a/openstackclient/identity/v3/endpoint.py +++ b/openstackclient/identity/v3/endpoint.py @@ -73,11 +73,11 @@ def take_action(self, parsed_args): parsed_args.service) endpoint = identity_client.endpoints.create( - service.id, - parsed_args.url, - parsed_args.interface, - parsed_args.region, - parsed_args.enabled + service=service.id, + url=parsed_args.url, + interface=parsed_args.interface, + region=parsed_args.region, + enabled=parsed_args.enabled ) info = {} @@ -192,11 +192,11 @@ def take_action(self, parsed_args): identity_client.endpoints.update( endpoint.id, - service.id, - parsed_args.url, - parsed_args.interface, - parsed_args.region, - parsed_args.enabled + service=service.id, + url=parsed_args.url, + interface=parsed_args.interface, + region=parsed_args.region, + enabled=parsed_args.enabled ) return diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index 6c059b5df7..38d810cb87 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -133,7 +133,7 @@ def take_action(self, parsed_args): else: domain = None group = identity_client.groups.create( - parsed_args.name, + name=parsed_args.name, domain=domain, description=parsed_args.description) diff --git a/openstackclient/identity/v3/policy.py b/openstackclient/identity/v3/policy.py index a760d8cdd8..249ba1ee22 100644 --- a/openstackclient/identity/v3/policy.py +++ b/openstackclient/identity/v3/policy.py @@ -52,7 +52,7 @@ def take_action(self, parsed_args): identity_client = self.app.client_manager.identity policy = identity_client.policies.create( - blob, type=parsed_args.type + blob=blob, type=parsed_args.type ) return zip(*sorted(six.iteritems(policy._info))) diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index d3618fb3ce..36787bb0e2 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -88,8 +88,8 @@ def take_action(self, parsed_args): kwargs = parsed_args.property.copy() project = identity_client.projects.create( - parsed_args.name, - domain, + name=parsed_args.name, + domain=domain, description=parsed_args.description, enabled=enabled, **kwargs diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index 05bdbbfc01..664a05dcc9 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -155,7 +155,7 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity - role = identity_client.roles.create(parsed_args.name) + role = identity_client.roles.create(name=parsed_args.name) return zip(*sorted(six.iteritems(role._info))) diff --git a/openstackclient/identity/v3/service.py b/openstackclient/identity/v3/service.py index 7e3bfc6b2d..63997d1cd7 100644 --- a/openstackclient/identity/v3/service.py +++ b/openstackclient/identity/v3/service.py @@ -64,9 +64,9 @@ def take_action(self, parsed_args): enabled = False service = identity_client.services.create( - parsed_args.name, - parsed_args.type, - enabled, + name=parsed_args.name, + type=parsed_args.type, + enabled=enabled, ) return zip(*sorted(six.iteritems(service._info))) diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 060eeca7c4..a5209020a6 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -107,7 +107,7 @@ def take_action(self, parsed_args): parsed_args.password = utils.get_password(self.app.stdin) user = identity_client.users.create( - parsed_args.name, + name=parsed_args.name, domain=domain_id, default_project=project_id, password=parsed_args.password, diff --git a/openstackclient/tests/identity/v3/test_project.py b/openstackclient/tests/identity/v3/test_project.py index 0479d37996..e0420a1e80 100644 --- a/openstackclient/tests/identity/v3/test_project.py +++ b/openstackclient/tests/identity/v3/test_project.py @@ -70,13 +70,14 @@ def test_project_create_no_options(self): # Set expected values kwargs = { + 'name': identity_fakes.project_name, + 'domain': None, 'description': None, 'enabled': True, } - # ProjectManager.create(name, domain, description=, enabled=, **kwargs) + # ProjectManager.create(name=, domain=, description=, + # enabled=, **kwargs) self.projects_mock.create.assert_called_with( - identity_fakes.project_name, - None, **kwargs ) @@ -109,13 +110,14 @@ def test_project_create_description(self): # Set expected values kwargs = { + 'name': identity_fakes.project_name, + 'domain': None, 'description': 'new desc', 'enabled': True, } - # ProjectManager.create(name, domain, description=, enabled=, **kwargs) + # ProjectManager.create(name=, domain=, description=, + # enabled=, **kwargs) self.projects_mock.create.assert_called_with( - identity_fakes.project_name, - None, **kwargs ) @@ -148,13 +150,14 @@ def test_project_create_domain(self): # Set expected values kwargs = { + 'name': identity_fakes.project_name, + 'domain': identity_fakes.domain_id, 'description': None, 'enabled': True, } - # ProjectManager.create(name, domain, description=, enabled=, **kwargs) + # ProjectManager.create(name=, domain=, description=, + # enabled=, **kwargs) self.projects_mock.create.assert_called_with( - identity_fakes.project_name, - identity_fakes.domain_id, **kwargs ) @@ -186,13 +189,14 @@ def test_project_create_enable(self): # Set expected values kwargs = { + 'name': identity_fakes.project_name, + 'domain': None, 'description': None, 'enabled': True, } - # ProjectManager.create(name, domain, description=, enabled=, **kwargs) + # ProjectManager.create(name=, domain=, description=, + # enabled=, **kwargs) self.projects_mock.create.assert_called_with( - identity_fakes.project_name, - None, **kwargs ) @@ -224,13 +228,14 @@ def test_project_create_disable(self): # Set expected values kwargs = { + 'name': identity_fakes.project_name, + 'domain': None, 'description': None, 'enabled': False, } - # ProjectManager.create(name, domain, description=, enabled=, **kwargs) + # ProjectManager.create(name=, domain=, + # description=, enabled=, **kwargs) self.projects_mock.create.assert_called_with( - identity_fakes.project_name, - None, **kwargs ) @@ -262,15 +267,16 @@ def test_project_create_property(self): # Set expected values kwargs = { + 'name': identity_fakes.project_name, + 'domain': None, 'description': None, 'enabled': True, 'fee': 'fi', 'fo': 'fum', } - # ProjectManager.create(name, domain, description=, enabled=, **kwargs) + # ProjectManager.create(name=, domain=, description=, + # enabled=, **kwargs) self.projects_mock.create.assert_called_with( - identity_fakes.project_name, - None, **kwargs ) diff --git a/openstackclient/tests/identity/v3/test_role.py b/openstackclient/tests/identity/v3/test_role.py index 040c39dd1b..0c0551e11f 100644 --- a/openstackclient/tests/identity/v3/test_role.py +++ b/openstackclient/tests/identity/v3/test_role.py @@ -232,9 +232,14 @@ def test_role_create_no_options(self): # DisplayCommandBase.take_action() returns two tuples columns, data = self.cmd.take_action(parsed_args) - # RoleManager.create(name) + # Set expected values + kwargs = { + 'name': identity_fakes.role_name, + } + + # RoleManager.create(name=) self.roles_mock.create.assert_called_with( - identity_fakes.role_name, + **kwargs ) collist = ('id', 'name') diff --git a/openstackclient/tests/identity/v3/test_service.py b/openstackclient/tests/identity/v3/test_service.py index 10d249c5db..6733f7faef 100644 --- a/openstackclient/tests/identity/v3/test_service.py +++ b/openstackclient/tests/identity/v3/test_service.py @@ -60,11 +60,11 @@ def test_service_create_name(self): # DisplayCommandBase.take_action() returns two tuples columns, data = self.cmd.take_action(parsed_args) - # ServiceManager.create(name, type, enabled=, **kwargs) + # ServiceManager.create(name=, type=, enabled=, **kwargs) self.services_mock.create.assert_called_with( - identity_fakes.service_name, - identity_fakes.service_type, - True, + name=identity_fakes.service_name, + type=identity_fakes.service_type, + enabled=True, ) collist = ('enabled', 'id', 'name', 'type') @@ -93,11 +93,11 @@ def test_service_create_enable(self): # DisplayCommandBase.take_action() returns two tuples columns, data = self.cmd.take_action(parsed_args) - # ServiceManager.create(name, type, enabled=, **kwargs) + # ServiceManager.create(name=, type=, enabled=, **kwargs) self.services_mock.create.assert_called_with( - None, - identity_fakes.service_type, - True, + name=None, + type=identity_fakes.service_type, + enabled=True, ) collist = ('enabled', 'id', 'name', 'type') @@ -126,11 +126,11 @@ def test_service_create_disable(self): # DisplayCommandBase.take_action() returns two tuples columns, data = self.cmd.take_action(parsed_args) - # ServiceManager.create(name, type, enabled=, **kwargs) + # ServiceManager.create(name=, type=, enabled=, **kwargs) self.services_mock.create.assert_called_with( - None, - identity_fakes.service_type, - False, + name=None, + type=identity_fakes.service_type, + enabled=False, ) collist = ('enabled', 'id', 'name', 'type') diff --git a/openstackclient/tests/identity/v3/test_user.py b/openstackclient/tests/identity/v3/test_user.py index af7b2f70c1..093d919b1b 100644 --- a/openstackclient/tests/identity/v3/test_user.py +++ b/openstackclient/tests/identity/v3/test_user.py @@ -85,6 +85,7 @@ def test_user_create_no_options(self): # Set expected values kwargs = { + 'name': identity_fakes.user_name, 'default_project': None, 'description': None, 'domain': None, @@ -93,10 +94,9 @@ def test_user_create_no_options(self): 'password': None, } - # UserManager.create(name, domain=, project=, password=, email=, + # UserManager.create(name=, domain=, project=, password=, email=, # description=, enabled=, default_project=) self.users_mock.create.assert_called_with( - identity_fakes.user_name, **kwargs ) @@ -131,6 +131,7 @@ def test_user_create_password(self): # Set expected values kwargs = { + 'name': identity_fakes.user_name, 'default_project': None, 'description': None, 'domain': None, @@ -138,10 +139,9 @@ def test_user_create_password(self): 'enabled': True, 'password': 'secret', } - # UserManager.create(name, domain=, project=, password=, email=, + # UserManager.create(name=, domain=, project=, password=, email=, # description=, enabled=, default_project=) self.users_mock.create.assert_called_with( - identity_fakes.user_name, **kwargs ) @@ -179,6 +179,7 @@ def test_user_create_password_prompt(self): # Set expected values kwargs = { + 'name': identity_fakes.user_name, 'default_project': None, 'description': None, 'domain': None, @@ -186,10 +187,9 @@ def test_user_create_password_prompt(self): 'enabled': True, 'password': 'abc123', } - # UserManager.create(name, domain=, project=, password=, email=, + # UserManager.create(name=, domain=, project=, password=, email=, # description=, enabled=, default_project=) self.users_mock.create.assert_called_with( - identity_fakes.user_name, **kwargs ) @@ -223,6 +223,7 @@ def test_user_create_email(self): # Set expected values kwargs = { + 'name': identity_fakes.user_name, 'default_project': None, 'description': None, 'domain': None, @@ -230,10 +231,9 @@ def test_user_create_email(self): 'enabled': True, 'password': None, } - # UserManager.create(name, domain=, project=, password=, email=, + # UserManager.create(name=, domain=, project=, password=, email=, # description=, enabled=, default_project=) self.users_mock.create.assert_called_with( - identity_fakes.user_name, **kwargs ) @@ -282,6 +282,7 @@ def test_user_create_project(self): # Set expected values kwargs = { + 'name': identity_fakes.user_name, 'default_project': identity_fakes.PROJECT_2['id'], 'description': None, 'domain': None, @@ -289,10 +290,9 @@ def test_user_create_project(self): 'enabled': True, 'password': None, } - # UserManager.create(name, domain=, project=, password=, email=, + # UserManager.create(name=, domain=, project=, password=, email=, # description=, enabled=, default_project=) self.users_mock.create.assert_called_with( - identity_fakes.user_name, **kwargs ) @@ -326,6 +326,7 @@ def test_user_create_domain(self): # Set expected values kwargs = { + 'name': identity_fakes.user_name, 'default_project': None, 'description': None, 'domain': identity_fakes.domain_id, @@ -333,10 +334,9 @@ def test_user_create_domain(self): 'enabled': True, 'password': None, } - # UserManager.create(name, domain=, project=, password=, email=, + # UserManager.create(name=, domain=, project=, password=, email=, # description=, enabled=, default_project=) self.users_mock.create.assert_called_with( - identity_fakes.user_name, **kwargs ) @@ -369,6 +369,7 @@ def test_user_create_enable(self): # Set expected values kwargs = { + 'name': identity_fakes.user_name, 'default_project': None, 'description': None, 'domain': None, @@ -376,10 +377,9 @@ def test_user_create_enable(self): 'enabled': True, 'password': None, } - # UserManager.create(name, domain=, project=, password=, email=, + # UserManager.create(name=, domain=, project=, password=, email=, # description=, enabled=, default_project=) self.users_mock.create.assert_called_with( - identity_fakes.user_name, **kwargs ) @@ -412,6 +412,7 @@ def test_user_create_disable(self): # Set expected values kwargs = { + 'name': identity_fakes.user_name, 'default_project': None, 'description': None, 'domain': None, @@ -419,9 +420,8 @@ def test_user_create_disable(self): 'enabled': False, 'password': None, } - # users.create(name, password, email, tenant_id=None, enabled=True) + # users.create(name=, password, email, tenant_id=None, enabled=True) self.users_mock.create.assert_called_with( - identity_fakes.user_name, **kwargs ) From 01db6d977f518d6a348a89549ca566c4aa780e79 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 1 Apr 2014 10:30:03 -0500 Subject: [PATCH 0088/3494] move read_blob_file_contents to utils Thinking ahead, a few other upcoming keystone features could benefit from reading contents from a file. Thus, moving the function from policy to utils. Change-Id: I713ab0e5a00c949ad996daf83b775a7c19044888 --- openstackclient/common/utils.py | 10 ++++++++++ openstackclient/identity/v3/policy.py | 10 ++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py index d7702a3f0f..bc9ed26412 100644 --- a/openstackclient/common/utils.py +++ b/openstackclient/common/utils.py @@ -249,3 +249,13 @@ def get_password(stdin): raise exceptions.CommandError("Error reading password.") raise exceptions.CommandError("There was a request to be prompted for a" " password and a terminal was not detected.") + + +def read_blob_file_contents(blob_file): + try: + with open(blob_file) as file: + blob = file.read().strip() + return blob + except IOError: + msg = "Error occurred trying to read from file %s" + raise exceptions.CommandError(msg % blob_file) diff --git a/openstackclient/identity/v3/policy.py b/openstackclient/identity/v3/policy.py index a760d8cdd8..641ff9c83c 100644 --- a/openstackclient/identity/v3/policy.py +++ b/openstackclient/identity/v3/policy.py @@ -48,7 +48,7 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) - blob = _read_blob_file_contents(parsed_args.blob_file) + blob = utils.read_blob_file_contents(parsed_args.blob_file) identity_client = self.app.client_manager.identity policy = identity_client.policies.create( @@ -138,7 +138,7 @@ def take_action(self, parsed_args): blob = None if parsed_args.blob_file: - blob = _read_blob_file_contents(parsed_args.blob_file) + blob = utils.read_blob_file_contents(parsed_args.blob_file) kwargs = {} if blob: @@ -174,9 +174,3 @@ def take_action(self, parsed_args): parsed_args.policy) return zip(*sorted(six.iteritems(policy._info))) - - -def _read_blob_file_contents(blob_file): - with open(blob_file) as file: - blob = file.read().strip() - return blob From dde02b8a38daa46e3680bd512ea549d265faa00c Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sun, 20 Apr 2014 09:59:03 +0000 Subject: [PATCH 0089/3494] Updated from global requirements Change-Id: Ib4416938530bd1037cc4b7e84bf81475d91e6b16 --- requirements.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5547d89676..5fa919fcb3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ -pbr>=0.6,<1.0 +pbr>=0.6,!=0.7,<1.0 cliff>=1.4.3 -keyring>=1.6.1,<2.0,>=2.1 +keyring>=2.1 pycrypto>=2.6 python-glanceclient>=0.9.0 -python-keystoneclient>=0.6.0 +python-keystoneclient>=0.7.0 python-novaclient>=2.17.0 python-cinderclient>=1.0.6 requests>=1.1 -six>=1.5.2 +six>=1.6.0 From ef9496a4fc9b8600dac88666b7159119e663642c Mon Sep 17 00:00:00 2001 From: Marek Denis Date: Wed, 9 Apr 2014 19:05:36 +0200 Subject: [PATCH 0090/3494] Implement CRUD operations for Identity Providers Operations for: * adding Identity Provider * listing Identity Providers * showing Identity Provider * updating Identity Provider * deleting Identity Provider Change-Id: I4557168309f93e4670116b5c3c0e29252ff0c40f Implements: bp/add-openstackclient-federation-crud --- .../identity/v3/identity_provider.py | 180 +++++++++ openstackclient/tests/identity/v3/fakes.py | 27 ++ .../identity/v3/test_identity_provider.py | 368 ++++++++++++++++++ setup.cfg | 6 + 4 files changed, 581 insertions(+) create mode 100644 openstackclient/identity/v3/identity_provider.py create mode 100644 openstackclient/tests/identity/v3/test_identity_provider.py diff --git a/openstackclient/identity/v3/identity_provider.py b/openstackclient/identity/v3/identity_provider.py new file mode 100644 index 0000000000..f577c31432 --- /dev/null +++ b/openstackclient/identity/v3/identity_provider.py @@ -0,0 +1,180 @@ +# 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. +# + +"""Identity v3 IdentityProvider action implementations""" + +import logging +import six +import sys + +from cliff import command +from cliff import lister +from cliff import show + +from openstackclient.common import utils + + +class CreateIdentityProvider(show.ShowOne): + """Create identity_provider command""" + + log = logging.getLogger(__name__ + '.CreateIdentityProvider') + + def get_parser(self, prog_name): + parser = super(CreateIdentityProvider, self).get_parser(prog_name) + parser.add_argument( + 'identity_provider_id', + metavar='', + help='New identity provider ID (must be unique)' + ) + parser.add_argument( + '--description', + metavar='', + help='New identity provider description', + ) + + enable_identity_provider = parser.add_mutually_exclusive_group() + enable_identity_provider.add_argument( + '--enable', + dest='enabled', + action='store_true', + default=True, + help='Enable identity provider', + ) + enable_identity_provider.add_argument( + '--disable', + dest='enabled', + action='store_false', + help='Disable the identity provider', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity + idp = identity_client.identity_providers.create( + parsed_args.identity_provider_id, + description=parsed_args.description, + enabled=parsed_args.enabled) + info = {} + info.update(idp._info) + return zip(*sorted(six.iteritems(info))) + + +class DeleteIdentityProvider(command.Command): + """Delete identity provider""" + + log = logging.getLogger(__name__ + '.DeleteIdentityProvider') + + def get_parser(self, prog_name): + parser = super(DeleteIdentityProvider, self).get_parser(prog_name) + parser.add_argument( + 'identity_provider', + metavar='', + help='ID of the identity provider to be deleted', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity + identity_client.identity_providers.delete( + parsed_args.identity_provider) + return + + +class ListIdentityProvider(lister.Lister): + """List identity providers""" + + log = logging.getLogger(__name__ + '.ListIdentityProvider') + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + columns = ('ID', 'Enabled', 'Description') + data = self.app.client_manager.identity.identity_providers.list() + return (columns, + (utils.get_item_properties( + s, columns, + formatters={}, + ) for s in data)) + + +class SetIdentityProvider(command.Command): + """Set identity provider""" + + log = logging.getLogger(__name__ + '.SetIdentityProvider') + + def get_parser(self, prog_name): + parser = super(SetIdentityProvider, self).get_parser(prog_name) + parser.add_argument( + 'identity_provider', + metavar='', + help='ID of the identity provider to be changed', + ) + + enable_identity_provider = parser.add_mutually_exclusive_group() + enable_identity_provider.add_argument( + '--enable', + action='store_true', + help='Enable the identity provider', + ) + enable_identity_provider.add_argument( + '--disable', + action='store_true', + help='Disable the identity provider', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity + + if parsed_args.enable is True: + enabled = True + elif parsed_args.disable is True: + enabled = False + else: + sys.stdout.write("Identity Provider not updated, " + "no arguments present") + return (None, None) + + identity_provider = identity_client.identity_providers.update( + parsed_args.identity_provider, enabled=enabled) + info = {} + info.update(identity_provider._info) + return zip(*sorted(six.iteritems(info))) + + +class ShowIdentityProvider(show.ShowOne): + """Show identity provider""" + + log = logging.getLogger(__name__ + '.ShowIdentityProvider') + + def get_parser(self, prog_name): + parser = super(ShowIdentityProvider, self).get_parser(prog_name) + parser.add_argument( + 'identity_provider', + metavar='', + help='ID of the identity provider to be displayed', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity + identity_provider = utils.find_resource( + identity_client.identity_providers, + parsed_args.identity_provider) + + info = {} + info.update(identity_provider._info) + return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index f2696ef8c7..ffa89a5f46 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -105,6 +105,15 @@ 'user_id': user_id, } +idp_id = 'test_idp' +idp_description = 'super exciting IdP description' + +IDENTITY_PROVIDER = { + 'id': idp_id, + 'enabled': True, + 'description': idp_description +} + class FakeIdentityv3Client(object): def __init__(self, **kwargs): @@ -125,6 +134,14 @@ def __init__(self, **kwargs): self.management_url = kwargs['endpoint'] +class FakeFederatedClient(FakeIdentityv3Client): + def __init__(self, **kwargs): + super(FakeFederatedClient, self).__init__(**kwargs) + + self.identity_providers = mock.Mock() + self.identity_providers.resource_class = fakes.FakeResource(None, {}) + + class TestIdentityv3(utils.TestCommand): def setUp(self): super(TestIdentityv3, self).setUp() @@ -133,3 +150,13 @@ def setUp(self): endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN, ) + + +class TestFederatedIdentity(utils.TestCommand): + def setUp(self): + super(TestFederatedIdentity, self).setUp() + + self.app.client_manager.identity = FakeFederatedClient( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN + ) diff --git a/openstackclient/tests/identity/v3/test_identity_provider.py b/openstackclient/tests/identity/v3/test_identity_provider.py new file mode 100644 index 0000000000..41015b69ab --- /dev/null +++ b/openstackclient/tests/identity/v3/test_identity_provider.py @@ -0,0 +1,368 @@ +# Copyright 2014 CERN. +# +# 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 copy + +from openstackclient.identity.v3 import identity_provider +from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes + + +class TestIdentityProvider(identity_fakes.TestFederatedIdentity): + + def setUp(self): + super(TestIdentityProvider, self).setUp() + + self.identity_providers_mock = self.app.client_manager.\ + identity.identity_providers + + self.identity_providers_mock.reset_mock() + + +class TestIdentityProviderCreate(TestIdentityProvider): + + def setUp(self): + super(TestIdentityProviderCreate, self).setUp() + + self.identity_providers_mock.create.return_value = \ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.IDENTITY_PROVIDER), + loaded=True + ) + + self.cmd = identity_provider.CreateIdentityProvider( + self.app, None) + + def test_create_identity_provider_no_options(self): + arglist = [ + identity_fakes.idp_id + ] + verifylist = [ + ('identity_provider_id', identity_fakes.idp_id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': True, + 'description': None, + } + + self.identity_providers_mock.create.assert_called_with( + identity_fakes.idp_id, **kwargs) + + collist = ('description', 'enabled', 'id') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.idp_description, + True, + identity_fakes.idp_id, + ) + self.assertEqual(data, datalist) + + def test_create_identity_provider_description(self): + arglist = ['--description', identity_fakes.idp_description, + identity_fakes.idp_id] + verifylist = [ + ('identity_provider_id', identity_fakes.idp_id), + ('description', identity_fakes.idp_description) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'description': identity_fakes.idp_description, + 'enabled': True, + } + + self.identity_providers_mock.create.assert_called_with( + identity_fakes.idp_id, **kwargs) + + collist = ('description', 'enabled', 'id') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.idp_description, True, identity_fakes.idp_id, + ) + self.assertEqual(data, datalist) + + def test_create_identity_provider_disabled(self): + + # Prepare FakeResource object + IDENTITY_PROVIDER = copy.deepcopy(identity_fakes.IDENTITY_PROVIDER) + IDENTITY_PROVIDER['enabled'] = False + IDENTITY_PROVIDER['description'] = None + + self.identity_providers_mock.create.return_value = \ + fakes.FakeResource( + None, + IDENTITY_PROVIDER, + loaded=True + ) + arglist = ['--disable', + identity_fakes.idp_id] + verifylist = [ + ('identity_provider_id', identity_fakes.idp_id), + + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': False, + 'description': None + } + + self.identity_providers_mock.create.assert_called_with( + identity_fakes.idp_id, **kwargs) + + collist = ('description', 'enabled', 'id') + self.assertEqual(columns, collist) + datalist = ( + None, + False, + identity_fakes.idp_id, + ) + self.assertEqual(data, datalist) + + +class TestIdentityProviderDelete(TestIdentityProvider): + + def setUp(self): + super(TestIdentityProviderDelete, self).setUp() + + # This is the return value for utils.find_resource() + self.identity_providers_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.IDENTITY_PROVIDER), + loaded=True) + + self.identity_providers_mock.delete.return_value = None + self.cmd = identity_provider.DeleteIdentityProvider( + self.app, None) + + def test_delete_identity_provider(self): + arglist = [ + identity_fakes.idp_id + ] + verifylist = [ + ('identity_provider', identity_fakes.idp_id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.identity_providers_mock.delete.assert_called_with( + identity_fakes.idp_id, + ) + + +class TestIdentityProviderList(TestIdentityProvider): + + def setUp(self): + super(TestIdentityProviderList, self).setUp() + + self.identity_providers_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.IDENTITY_PROVIDER), + loaded=True, + ) + self.identity_providers_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.IDENTITY_PROVIDER), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = identity_provider.ListIdentityProvider(self.app, None) + + def test_identity_provider_list_no_options(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.identity_providers_mock.list.assert_called_with() + + collist = ('ID', 'Enabled', 'Description') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.idp_id, + True, + identity_fakes.idp_description + + ), ) + self.assertEqual(tuple(data), datalist) + + +class TestIdentityProviderShow(TestIdentityProvider): + + def setUp(self): + super(TestIdentityProviderShow, self).setUp() + + self.identity_providers_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.IDENTITY_PROVIDER), + loaded=True + ) + + # Get the command object to test + self.cmd = identity_provider.ShowIdentityProvider(self.app, None) + + def test_identity_provider_show(self): + arglist = [ + identity_fakes.idp_id + ] + verifylist = [ + ('identity_provider', identity_fakes.idp_id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.identity_providers_mock.get.assert_called_with( + identity_fakes.idp_id) + + collist = ('description', 'enabled', 'id' ) + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.idp_description, + True, + identity_fakes.idp_id + ) + self.assertEqual(data, datalist) + + +class TestIdentityProviderSet(TestIdentityProvider): + + def setUp(self): + super(TestIdentityProviderSet, self).setUp() + self.cmd = identity_provider.SetIdentityProvider(self.app, None) + + def test_identity_provider_disable(self): + """Disable Identity Provider + + Set Identity Provider's ``enabled`` attribute to False. + + """ + def prepare(self): + """Prepare fake return objects before the test is executed""" + updated_idp = copy.deepcopy(identity_fakes.IDENTITY_PROVIDER) + updated_idp['enabled'] = False + resources = fakes.FakeResource( + None, + updated_idp, + loaded=True + ) + self.identity_providers_mock.update.return_value = resources + + prepare(self) + arglist = [ + '--disable', identity_fakes.idp_id + ] + verifylist = [ + ('identity_provider', identity_fakes.idp_id), + ('enable', False), + ('disable', True) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.identity_providers_mock.update.assert_called_with( + identity_fakes.idp_id, enabled=False) + collist = ('description', 'enabled', 'id' ) + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.idp_description, + False, + identity_fakes.idp_id + ) + self.assertEqual(datalist, data) + + def test_identity_provider_enable(self): + """Enable Identity Provider. + + Set Identity Provider's ``enabled`` attribute to True. + + """ + def prepare(self): + """Prepare fake return objects before the test is executed""" + resources = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.IDENTITY_PROVIDER), + loaded=True + ) + self.identity_providers_mock.update.return_value = resources + + prepare(self) + arglist = [ + '--enable', identity_fakes.idp_id + ] + verifylist = [ + ('identity_provider', identity_fakes.idp_id), + ('enable', True), + ('disable', False) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.identity_providers_mock.update.assert_called_with( + identity_fakes.idp_id, enabled=True) + collist = ('description', 'enabled', 'id' ) + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.idp_description, + True, + identity_fakes.idp_id + ) + self.assertEqual(data, datalist) + + def test_identity_provider_no_options(self): + def prepare(self): + """Prepare fake return objects before the test is executed""" + resources = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.IDENTITY_PROVIDER), + loaded=True + ) + self.identity_providers_mock.get.return_value = resources + + resources = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.IDENTITY_PROVIDER), + loaded=True + ) + self.identity_providers_mock.update.return_value = resources + + prepare(self) + arglist = [ + identity_fakes.idp_id + ] + verifylist = [ + ('identity_provider', identity_fakes.idp_id), + ('enable', False), + ('disable', False) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + # expect take_action() to return (None, None) as + # neither --enable nor --disable was specified + self.assertEqual(columns, None) + self.assertEqual(data, None) diff --git a/setup.cfg b/setup.cfg index a2c34e145a..d2a404afe9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -200,6 +200,12 @@ openstack.identity.v3 = group_set = openstackclient.identity.v3.group:SetGroup group_show = openstackclient.identity.v3.group:ShowGroup + identity_provider_create = openstackclient.identity.v3.identity_provider:CreateIdentityProvider + identity_provider_delete = openstackclient.identity.v3.identity_provider:DeleteIdentityProvider + identity_provider_list = openstackclient.identity.v3.identity_provider:ListIdentityProvider + identity_provider_set = openstackclient.identity.v3.identity_provider:SetIdentityProvider + identity_provider_show = openstackclient.identity.v3.identity_provider:ShowIdentityProvider + policy_create = openstackclient.identity.v3.policy:CreatePolicy policy_delete = openstackclient.identity.v3.policy:DeletePolicy policy_list = openstackclient.identity.v3.policy:ListPolicy From 6c5f2e39e23a11236986b119974b90bf15f73878 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 1 May 2014 13:50:49 +0000 Subject: [PATCH 0091/3494] Updated from global requirements Change-Id: Idde32a0bdcee8843c09a968dff69b246b5e784f7 --- requirements.txt | 2 +- setup.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5fa919fcb3..122d8896cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ cliff>=1.4.3 keyring>=2.1 pycrypto>=2.6 python-glanceclient>=0.9.0 -python-keystoneclient>=0.7.0 +python-keystoneclient>=0.8.0 python-novaclient>=2.17.0 python-cinderclient>=1.0.6 requests>=1.1 diff --git a/setup.py b/setup.py index 70c2b3f32b..736375744d 100644 --- a/setup.py +++ b/setup.py @@ -17,6 +17,14 @@ # THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT import setuptools +# In python < 2.7.4, a lazy loading of package `pbr` will break +# setuptools if some other modules registered functions in `atexit`. +# solution from: http://bugs.python.org/issue15881#msg170215 +try: + import multiprocessing # noqa +except ImportError: + pass + setuptools.setup( setup_requires=['pbr'], pbr=True) From 2cc3a2fdbddb10cc26ffb49e4a7cfa114a1e9e53 Mon Sep 17 00:00:00 2001 From: Yejia Xu Date: Mon, 5 May 2014 00:11:17 +0000 Subject: [PATCH 0092/3494] Skip auth in cinderclient cinderclient can't work well with keystone v3 auth info. We should do it in openstackclient just like compute extension. Closes-Bug: #1315963 Change-Id: I46f794c5315f6a9fe1d9a0e5dc7b84f067d7f792 --- openstackclient/volume/client.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openstackclient/volume/client.py b/openstackclient/volume/client.py index 2d82437268..7cf828b440 100644 --- a/openstackclient/volume/client.py +++ b/openstackclient/volume/client.py @@ -52,6 +52,17 @@ def make_client(instance): http_log_debug=http_log_debug ) + # Populate the Cinder client to skip another auth query to Identity + if instance._url: + # token flow + client.client.management_url = instance._url + else: + # password flow + client.client.management_url = instance.get_endpoint_for_service_type( + API_NAME) + client.client.service_catalog = instance._service_catalog + client.client.auth_token = instance._token + return client From 37231b5801162c9fcbd2a704a6660021dda6327d Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Tue, 22 Apr 2014 09:08:52 -0600 Subject: [PATCH 0093/3494] volume type create should display properties The volume type create command should properly output the properties. The code was doing a create on the volume type and then setting the properties, but it was printing out the volume object from the create. Change-Id: I23c8a0182e77bb71903ad87c1b01ba2b62405f3b Closes-Bug: #1303978 --- openstackclient/volume/v1/type.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/openstackclient/volume/v1/type.py b/openstackclient/volume/v1/type.py index edacb397e5..b199b7c3e0 100644 --- a/openstackclient/volume/v1/type.py +++ b/openstackclient/volume/v1/type.py @@ -50,16 +50,11 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) volume_client = self.app.client_manager.volume - volume_type = volume_client.volume_types.create( - parsed_args.name - ) + volume_type = volume_client.volume_types.create(parsed_args.name) + volume_type._info.pop('extra_specs') if parsed_args.property: - volume_type.set_keys(parsed_args.property) - # Map 'extra_specs' column to 'properties' - volume_type._info.update( - {'properties': utils.format_dict( - volume_type._info.pop('extra_specs'))} - ) + result = volume_type.set_keys(parsed_args.property) + volume_type._info.update({'properties': utils.format_dict(result)}) info = {} info.update(volume_type._info) From f7f8fe4e7c4229562bed04faf21a1cf6e7d3a56b Mon Sep 17 00:00:00 2001 From: Yejia Xu Date: Wed, 7 May 2014 02:13:03 +0000 Subject: [PATCH 0094/3494] Fix help message for `ip floating delete` Previously, the help message incorrectly had the string "IP address to add to server". This should probably read "IP address to delete". Change-Id: If592b736448199f84c30e0cbc8110a0a76e2c140 Closes-Bug: #1316877 --- openstackclient/compute/v2/floatingip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstackclient/compute/v2/floatingip.py b/openstackclient/compute/v2/floatingip.py index 7ed847f5f4..3edc809ef2 100644 --- a/openstackclient/compute/v2/floatingip.py +++ b/openstackclient/compute/v2/floatingip.py @@ -89,7 +89,7 @@ def get_parser(self, prog_name): parser.add_argument( "ip_address", metavar="", - help="IP address to add to server", + help="IP address to delete", ) return parser From 3b57117f8efc0c945f730c5e8a598768ac774114 Mon Sep 17 00:00:00 2001 From: Yejia Xu Date: Wed, 7 May 2014 01:49:15 +0000 Subject: [PATCH 0095/3494] Display all server log when --lines option is None Without --lines option, `console log show xxx` cmd will break. Change-Id: I4027aacac245e6916c1808fd9f878fb708c8a5f0 Closes-Bug: #1316870 --- openstackclient/compute/v2/console.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openstackclient/compute/v2/console.py b/openstackclient/compute/v2/console.py index 8f49c5134a..032168b906 100644 --- a/openstackclient/compute/v2/console.py +++ b/openstackclient/compute/v2/console.py @@ -55,9 +55,13 @@ def take_action(self, parsed_args): compute_client.servers, parsed_args.server, ) - # NOTE(dtroyer): get_console_output() appears to shortchange the - # output by one line - data = server.get_console_output(length=parsed_args.lines + 1) + length = parsed_args.lines + if length: + # NOTE(dtroyer): get_console_output() appears to shortchange the + # output by one line + length += 1 + + data = server.get_console_output(length=length) sys.stdout.write(data) return From da5e31dbb629c26f54e476ca3587455c3a17cdcb Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 8 May 2014 10:58:17 -0500 Subject: [PATCH 0096/3494] Fix server image create The final find_resource() call errored because servers.create_image() returns an image ID rather than an Image resource. Reset expectations and arguments. Change-Id: I1b9132f66091f9df76198724156acb7a6fb2f6fe --- openstackclient/compute/v2/server.py | 8 ++++---- openstackclient/tests/compute/v2/test_server.py | 6 +----- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 808741fdca..9cff03d678 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -393,7 +393,7 @@ def get_parser(self, prog_name): parser = super(CreateServerImage, self).get_parser(prog_name) parser.add_argument( 'server', - metavar=' Date: Fri, 9 May 2014 09:28:53 -0500 Subject: [PATCH 0097/3494] Change volume create --volume-type to --type This makes it consistent with the other --type options in OSC. Also add a few more volume_create tests. Change-Id: I50ef927932cabf157ecdfd6c4faa1914b4fdf413 --- openstackclient/tests/volume/v1/fakes.py | 11 +- .../tests/volume/v1/test_volume.py | 151 ++++++++++++++++-- openstackclient/volume/v1/volume.py | 14 +- 3 files changed, 158 insertions(+), 18 deletions(-) diff --git a/openstackclient/tests/volume/v1/fakes.py b/openstackclient/tests/volume/v1/fakes.py index d6ef0d399e..3567eca507 100644 --- a/openstackclient/tests/volume/v1/fakes.py +++ b/openstackclient/tests/volume/v1/fakes.py @@ -24,7 +24,14 @@ volume_name = 'nigel' volume_description = 'Nigel Tufnel' volume_size = 120 -volume_metadata = {} +volume_type = 'to-eleven' +volume_zone = 'stonehenge' +volume_metadata = { + 'Alpha': 'a', + 'Beta': 'b', + 'Gamma': 'g', +} +volume_metadata_str = "Alpha='a', Beta='b', Gamma='g'" VOLUME = { 'id': volume_id, @@ -33,6 +40,8 @@ 'size': volume_size, 'status': '', 'attach_status': 'detached', + 'availability_zone': volume_zone, + 'volume_type': volume_type, 'metadata': volume_metadata, } diff --git a/openstackclient/tests/volume/v1/test_volume.py b/openstackclient/tests/volume/v1/test_volume.py index 554e2b2a36..d881598af3 100644 --- a/openstackclient/tests/volume/v1/test_volume.py +++ b/openstackclient/tests/volume/v1/test_volume.py @@ -71,10 +71,6 @@ def test_volume_create_min_options(self): # DisplayCommandBase.take_action() returns two tuples columns, data = self.cmd.take_action(parsed_args) - # Set expected values - #kwargs = { - # 'metadata': volume_fakes.volume_metadata, - #} # VolumeManager.create(size, snapshot_id=, source_volid=, # display_name=, display_description=, # volume_type=, user_id=, @@ -96,22 +92,90 @@ def test_volume_create_min_options(self): collist = ( 'attach_status', + 'availability_zone', 'display_description', 'display_name', 'id', 'properties', 'size', 'status', + 'type', ) self.assertEqual(columns, collist) datalist = ( 'detached', + volume_fakes.volume_zone, volume_fakes.volume_description, volume_fakes.volume_name, volume_fakes.volume_id, + volume_fakes.volume_metadata_str, + volume_fakes.volume_size, '', + volume_fakes.volume_type, + ) + self.assertEqual(data, datalist) + + def test_volume_create_options(self): + arglist = [ + '--size', str(volume_fakes.volume_size), + '--description', volume_fakes.volume_description, + '--type', volume_fakes.volume_type, + '--availability-zone', volume_fakes.volume_zone, + volume_fakes.volume_name, + ] + verifylist = [ + ('size', volume_fakes.volume_size), + ('description', volume_fakes.volume_description), + ('type', volume_fakes.volume_type), + ('availability_zone', volume_fakes.volume_zone), + ('name', volume_fakes.volume_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # VolumeManager.create(size, snapshot_id=, source_volid=, + # display_name=, display_description=, + # volume_type=, user_id=, + # project_id=, availability_zone=, + # metadata=, imageRef=) + self.volumes_mock.create.assert_called_with( + volume_fakes.volume_size, + None, + None, + volume_fakes.volume_name, + volume_fakes.volume_description, + volume_fakes.volume_type, + None, + None, + volume_fakes.volume_zone, + None, + None, + ) + + collist = ( + 'attach_status', + 'availability_zone', + 'display_description', + 'display_name', + 'id', + 'properties', + 'size', + 'status', + 'type', + ) + self.assertEqual(columns, collist) + datalist = ( + 'detached', + volume_fakes.volume_zone, + volume_fakes.volume_description, + volume_fakes.volume_name, + volume_fakes.volume_id, + volume_fakes.volume_metadata_str, volume_fakes.volume_size, '', + volume_fakes.volume_type, ) self.assertEqual(data, datalist) @@ -146,10 +210,6 @@ def test_volume_create_user_project_id(self): # DisplayCommandBase.take_action() returns two tuples columns, data = self.cmd.take_action(parsed_args) - # Set expected values - #kwargs = { - # 'metadata': volume_fakes.volume_metadata, - #} # VolumeManager.create(size, snapshot_id=, source_volid=, # display_name=, display_description=, # volume_type=, user_id=, @@ -172,22 +232,26 @@ def test_volume_create_user_project_id(self): collist = ( 'attach_status', + 'availability_zone', 'display_description', 'display_name', 'id', 'properties', 'size', 'status', + 'type', ) self.assertEqual(columns, collist) datalist = ( 'detached', + volume_fakes.volume_zone, volume_fakes.volume_description, volume_fakes.volume_name, volume_fakes.volume_id, - '', + volume_fakes.volume_metadata_str, volume_fakes.volume_size, '', + volume_fakes.volume_type, ) self.assertEqual(data, datalist) @@ -222,10 +286,6 @@ def test_volume_create_user_project_name(self): # DisplayCommandBase.take_action() returns two tuples columns, data = self.cmd.take_action(parsed_args) - # Set expected values - #kwargs = { - # 'metadata': volume_fakes.volume_metadata, - #} # VolumeManager.create(size, snapshot_id=, source_volid=, # display_name=, display_description=, # volume_type=, user_id=, @@ -248,21 +308,86 @@ def test_volume_create_user_project_name(self): collist = ( 'attach_status', + 'availability_zone', 'display_description', 'display_name', 'id', 'properties', 'size', 'status', + 'type', ) self.assertEqual(columns, collist) datalist = ( 'detached', + volume_fakes.volume_zone, volume_fakes.volume_description, volume_fakes.volume_name, volume_fakes.volume_id, + volume_fakes.volume_metadata_str, + volume_fakes.volume_size, '', + volume_fakes.volume_type, + ) + self.assertEqual(data, datalist) + + def test_volume_create_properties(self): + arglist = [ + '--property', 'Alpha=a', + '--property', 'Beta=b', + '--size', str(volume_fakes.volume_size), + volume_fakes.volume_name, + ] + verifylist = [ + ('property', {'Alpha': 'a', 'Beta': 'b'}), + ('size', volume_fakes.volume_size), + ('name', volume_fakes.volume_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # VolumeManager.create(size, snapshot_id=, source_volid=, + # display_name=, display_description=, + # volume_type=, user_id=, + # project_id=, availability_zone=, + # metadata=, imageRef=) + self.volumes_mock.create.assert_called_with( + volume_fakes.volume_size, + None, + None, + volume_fakes.volume_name, + None, + None, + None, + None, + None, + {'Alpha': 'a', 'Beta': 'b'}, + None, + ) + + collist = ( + 'attach_status', + 'availability_zone', + 'display_description', + 'display_name', + 'id', + 'properties', + 'size', + 'status', + 'type', + ) + self.assertEqual(columns, collist) + datalist = ( + 'detached', + volume_fakes.volume_zone, + volume_fakes.volume_description, + volume_fakes.volume_name, + volume_fakes.volume_id, + volume_fakes.volume_metadata_str, volume_fakes.volume_size, '', + volume_fakes.volume_type, ) self.assertEqual(data, datalist) diff --git a/openstackclient/volume/v1/volume.py b/openstackclient/volume/v1/volume.py index 928ed76be0..cad53eb111 100644 --- a/openstackclient/volume/v1/volume.py +++ b/openstackclient/volume/v1/volume.py @@ -56,7 +56,7 @@ def get_parser(self, prog_name): help='Description of the volume', ) parser.add_argument( - '--volume-type', + '--type', metavar='', help='Type of volume', ) @@ -124,7 +124,7 @@ def take_action(self, parsed_args): source_volume, parsed_args.name, parsed_args.description, - parsed_args.volume_type, + parsed_args.type, user, project, parsed_args.availability_zone, @@ -133,7 +133,10 @@ def take_action(self, parsed_args): ) # Map 'metadata' column to 'properties' volume._info.update( - {'properties': utils.format_dict(volume._info.pop('metadata'))} + { + 'properties': utils.format_dict(volume._info.pop('metadata')), + 'type': volume._info.pop('volume_type'), + }, ) return zip(*sorted(six.iteritems(volume._info))) @@ -331,7 +334,10 @@ def take_action(self, parsed_args): volume = utils.find_resource(volume_client.volumes, parsed_args.volume) # Map 'metadata' column to 'properties' volume._info.update( - {'properties': utils.format_dict(volume._info.pop('metadata'))} + { + 'properties': utils.format_dict(volume._info.pop('metadata')), + 'type': volume._info.pop('volume_type'), + }, ) if 'os-vol-tenant-attr:tenant_id' in volume._info: volume._info.update( From dc44ec1ddf09177430d79f7c91c9ba36c3a44a9d Mon Sep 17 00:00:00 2001 From: Matt Fischer Date: Thu, 15 May 2014 12:20:30 -0400 Subject: [PATCH 0098/3494] Add tests for identity endpoints Change-Id: If15cc74fafbbe52fa86aa353f2598aa31daf0695 Closes-Bug: #1319450 --- openstackclient/tests/identity/v2_0/fakes.py | 22 ++ .../tests/identity/v2_0/test_endpoint.py | 266 ++++++++++++++++++ 2 files changed, 288 insertions(+) create mode 100644 openstackclient/tests/identity/v2_0/test_endpoint.py diff --git a/openstackclient/tests/identity/v2_0/fakes.py b/openstackclient/tests/identity/v2_0/fakes.py index 231fa1a5e7..59860d22a4 100644 --- a/openstackclient/tests/identity/v2_0/fakes.py +++ b/openstackclient/tests/identity/v2_0/fakes.py @@ -80,6 +80,26 @@ 'user_id': user_id, } +endpoint_name = service_name +endpoint_adminurl = 'https://admin.example.com/v2/UUID' +endpoint_region = 'RegionOne' +endpoint_internalurl = 'https://internal.example.com/v2/UUID' +endpoint_type = service_type +endpoint_id = '11b41ee1b00841128b7333d4bf1a6140' +endpoint_publicurl = 'https://public.example.com/v2/UUID' +endpoint_service_id = service_id + +ENDPOINT = { + 'service_name': endpoint_name, + 'adminurl': endpoint_adminurl, + 'region': endpoint_region, + 'internalurl': endpoint_internalurl, + 'service_type': endpoint_type, + 'id': endpoint_id, + 'publicurl': endpoint_publicurl, + 'service_id': endpoint_service_id, +} + class FakeIdentityv2Client(object): def __init__(self, **kwargs): @@ -94,6 +114,8 @@ def __init__(self, **kwargs): self.users.resource_class = fakes.FakeResource(None, {}) self.ec2 = mock.Mock() self.ec2.resource_class = fakes.FakeResource(None, {}) + self.endpoints = mock.Mock() + self.endpoints.resource_class = fakes.FakeResource(None, {}) self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] diff --git a/openstackclient/tests/identity/v2_0/test_endpoint.py b/openstackclient/tests/identity/v2_0/test_endpoint.py new file mode 100644 index 0000000000..0d7db0aa1a --- /dev/null +++ b/openstackclient/tests/identity/v2_0/test_endpoint.py @@ -0,0 +1,266 @@ +# 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 copy + +from openstackclient.identity.v2_0 import endpoint +from openstackclient.tests import fakes +from openstackclient.tests.identity.v2_0 import fakes as identity_fakes + + +class TestEndpoint(identity_fakes.TestIdentityv2): + + def setUp(self): + super(TestEndpoint, self).setUp() + + # Get a shortcut to the EndpointManager Mock + self.endpoints_mock = self.app.client_manager.identity.endpoints + self.endpoints_mock.reset_mock() + + # Get a shortcut to the ServiceManager Mock + self.services_mock = self.app.client_manager.identity.services + self.services_mock.reset_mock() + + +class TestEndpointCreate(TestEndpoint): + + def setUp(self): + super(TestEndpointCreate, self).setUp() + + self.endpoints_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ENDPOINT), + loaded=True, + ) + + self.services_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.SERVICE), + loaded=True, + ) + + # Get the command object to test + self.cmd = endpoint.CreateEndpoint(self.app, None) + + def test_endpoint_create(self): + arglist = [ + '--publicurl', identity_fakes.endpoint_publicurl, + '--internalurl', identity_fakes.endpoint_internalurl, + '--adminurl', identity_fakes.endpoint_adminurl, + '--region', identity_fakes.endpoint_region, + identity_fakes.endpoint_name, + ] + verifylist = [ + ('adminurl', identity_fakes.endpoint_adminurl), + ('internalurl', identity_fakes.endpoint_internalurl), + ('publicurl', identity_fakes.endpoint_publicurl), + ('region', identity_fakes.endpoint_region), + ('service', identity_fakes.service_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # EndpointManager.create(region, service_id, publicurl, adminurl, + # internalurl) + self.endpoints_mock.create.assert_called_with( + identity_fakes.endpoint_region, + identity_fakes.service_id, + identity_fakes.endpoint_publicurl, + identity_fakes.endpoint_adminurl, + identity_fakes.endpoint_internalurl, + ) + + collist = ('adminurl', 'id', 'internalurl', 'publicurl', + 'region', 'service_id', 'service_name', 'service_type') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.endpoint_adminurl, + identity_fakes.endpoint_id, + identity_fakes.endpoint_internalurl, + identity_fakes.endpoint_publicurl, + identity_fakes.endpoint_region, + identity_fakes.service_id, + identity_fakes.service_name, + identity_fakes.service_type, + ) + + self.assertEqual(data, datalist) + + +class TestEndpointDelete(TestEndpoint): + + def setUp(self): + super(TestEndpointDelete, self).setUp() + + self.endpoints_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ENDPOINT), + loaded=True, + ) + + self.services_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.SERVICE), + loaded=True, + ) + + self.endpoints_mock.delete.return_value = None + + # Get the command object to test + self.cmd = endpoint.DeleteEndpoint(self.app, None) + + def test_endpoint_delete_no_options(self): + arglist = [ + identity_fakes.endpoint_id, + ] + verifylist = [ + ('endpoint', identity_fakes.endpoint_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + self.endpoints_mock.delete.assert_called_with( + identity_fakes.endpoint_id, + ) + + +class TestEndpointList(TestEndpoint): + + def setUp(self): + super(TestEndpointList, self).setUp() + + self.endpoints_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ENDPOINT), + loaded=True, + ), + ] + + self.services_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.SERVICE), + loaded=True, + ) + + # Get the command object to test + self.cmd = endpoint.ListEndpoint(self.app, None) + + def test_endpoint_list_no_options(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.endpoints_mock.list.assert_called_with() + + collist = ('ID', 'Region', 'Service Name', 'Service Type') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.endpoint_id, + identity_fakes.endpoint_region, + identity_fakes.service_name, + identity_fakes.service_type, + ), ) + self.assertEqual(tuple(data), datalist) + + def test_endpoint_list_long(self): + arglist = [ + '--long', + ] + verifylist = [ + ('long', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.endpoints_mock.list.assert_called_with() + + collist = ('ID', 'Region', 'Service Name', 'Service Type', + 'PublicURL', 'AdminURL', 'InternalURL') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.endpoint_id, + identity_fakes.endpoint_region, + identity_fakes.service_name, + identity_fakes.service_type, + identity_fakes.endpoint_publicurl, + identity_fakes.endpoint_adminurl, + identity_fakes.endpoint_internalurl, + ), ) + self.assertEqual(tuple(data), datalist) + + +class TestEndpointShow(TestEndpoint): + + def setUp(self): + super(TestEndpointShow, self).setUp() + + self.endpoints_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ENDPOINT), + loaded=True, + ), + ] + + self.services_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.SERVICE), + loaded=True, + ) + + # Get the command object to test + self.cmd = endpoint.ShowEndpoint(self.app, None) + + def test_endpoint_show(self): + arglist = [ + identity_fakes.endpoint_name, + ] + verifylist = [ + ('endpoint_or_service', identity_fakes.endpoint_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # EndpointManager.list() + self.endpoints_mock.list.assert_called_with() + # ServiceManager.get(name) + self.services_mock.get.assert_called_with( + identity_fakes.service_name, + ) + + collist = ('adminurl', 'id', 'internalurl', 'publicurl', + 'region', 'service_id', 'service_name', 'service_type') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.endpoint_adminurl, + identity_fakes.endpoint_id, + identity_fakes.endpoint_internalurl, + identity_fakes.endpoint_publicurl, + identity_fakes.endpoint_region, + identity_fakes.service_id, + identity_fakes.service_name, + identity_fakes.service_type, + ) + self.assertEqual(data, datalist) From 3b485de6b0495d1e9f6d659463a187d73a783594 Mon Sep 17 00:00:00 2001 From: Christian Berendt Date: Tue, 20 May 2014 13:11:19 +0200 Subject: [PATCH 0099/3494] replace string format arguments with function parameters There are files containing string format arguments inside logging messages. Using logging function parameters should be preferred. Change-Id: Ic749ac9eb55564ed631d57055a5a4dfc3aebd169 --- openstackclient/common/clientmanager.py | 2 +- openstackclient/common/commandmanager.py | 2 +- openstackclient/common/limits.py | 2 +- openstackclient/common/module.py | 2 +- openstackclient/common/quota.py | 4 +- openstackclient/compute/client.py | 2 +- openstackclient/compute/v2/agent.py | 8 +-- openstackclient/compute/v2/aggregate.py | 14 ++--- openstackclient/compute/v2/console.py | 4 +- openstackclient/compute/v2/fixedip.py | 4 +- openstackclient/compute/v2/flavor.py | 8 +-- openstackclient/compute/v2/floatingip.py | 10 ++-- openstackclient/compute/v2/floatingippool.py | 2 +- openstackclient/compute/v2/host.py | 4 +- openstackclient/compute/v2/hypervisor.py | 4 +- openstackclient/compute/v2/keypair.py | 8 +-- openstackclient/compute/v2/security_group.py | 16 ++--- openstackclient/compute/v2/server.py | 58 +++++++++---------- openstackclient/compute/v2/service.py | 4 +- openstackclient/compute/v2/usage.py | 2 +- openstackclient/identity/v2_0/ec2creds.py | 8 +-- openstackclient/identity/v2_0/endpoint.py | 8 +-- openstackclient/identity/v2_0/project.py | 10 ++-- openstackclient/identity/v2_0/role.py | 14 ++--- openstackclient/identity/v2_0/service.py | 8 +-- openstackclient/identity/v2_0/token.py | 2 +- openstackclient/identity/v2_0/user.py | 10 ++-- openstackclient/identity/v3/consumer.py | 10 ++-- openstackclient/identity/v3/credential.py | 10 ++-- openstackclient/identity/v3/domain.py | 10 ++-- openstackclient/identity/v3/endpoint.py | 10 ++-- openstackclient/identity/v3/group.py | 16 ++--- .../identity/v3/identity_provider.py | 10 ++-- openstackclient/identity/v3/policy.py | 10 ++-- openstackclient/identity/v3/project.py | 10 ++-- openstackclient/identity/v3/role.py | 14 ++--- openstackclient/identity/v3/service.py | 10 ++-- openstackclient/identity/v3/token.py | 14 ++--- openstackclient/identity/v3/user.py | 10 ++-- openstackclient/image/v1/image.py | 12 ++-- openstackclient/image/v2/image.py | 8 +-- openstackclient/object/v1/container.py | 4 +- openstackclient/object/v1/object.py | 4 +- openstackclient/shell.py | 5 +- openstackclient/volume/v1/backup.py | 10 ++-- openstackclient/volume/v1/snapshot.py | 10 ++-- openstackclient/volume/v1/type.py | 10 ++-- openstackclient/volume/v1/volume.py | 12 ++-- 48 files changed, 215 insertions(+), 214 deletions(-) diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index b6dab253a3..353a0a1988 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -103,7 +103,7 @@ def get_extension_modules(group): """Add extension clients""" mod_list = [] for ep in pkg_resources.iter_entry_points(group): - LOG.debug('found extension %r' % ep.name) + LOG.debug('found extension %r', ep.name) __import__(ep.module_name) module = sys.modules[ep.module_name] diff --git a/openstackclient/common/commandmanager.py b/openstackclient/common/commandmanager.py index 553bc920ab..204b943bcf 100644 --- a/openstackclient/common/commandmanager.py +++ b/openstackclient/common/commandmanager.py @@ -37,7 +37,7 @@ def _load_commands(self, group=None): group = self.namespace self.group_list.append(group) for ep in pkg_resources.iter_entry_points(group): - LOG.debug('found command %r' % ep.name) + LOG.debug('found command %r', ep.name) cmd_name = ( ep.name.replace('_', ' ') if self.convert_underscores diff --git a/openstackclient/common/limits.py b/openstackclient/common/limits.py index bbc15228e8..9c9458ab91 100644 --- a/openstackclient/common/limits.py +++ b/openstackclient/common/limits.py @@ -52,7 +52,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute volume_client = self.app.client_manager.volume diff --git a/openstackclient/common/module.py b/openstackclient/common/module.py index 4a7f062698..7f9c52db22 100644 --- a/openstackclient/common/module.py +++ b/openstackclient/common/module.py @@ -39,7 +39,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) data = {} # Get module versions diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index fd482da9e6..e011fd3619 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -79,7 +79,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute volume_client = self.app.client_manager.volume @@ -148,7 +148,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute volume_client = self.app.client_manager.volume diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py index 765a48db99..3dacee88cf 100644 --- a/openstackclient/compute/client.py +++ b/openstackclient/compute/client.py @@ -34,7 +34,7 @@ def make_client(instance): API_NAME, instance._api_version[API_NAME], API_VERSIONS) - LOG.debug('instantiating compute client: %s' % compute_client) + LOG.debug('instantiating compute client: %s', compute_client) # Set client http_log_debug to True if verbosity level is high enough http_log_debug = utils.get_effective_log_level() <= logging.DEBUG diff --git a/openstackclient/compute/v2/agent.py b/openstackclient/compute/v2/agent.py index ae7ba9620c..14c4b2c7f5 100644 --- a/openstackclient/compute/v2/agent.py +++ b/openstackclient/compute/v2/agent.py @@ -60,7 +60,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute args = ( parsed_args.os, @@ -88,7 +88,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute compute_client.agents.delete(parsed_args.id) return @@ -108,7 +108,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute columns = ( "Agent ID", @@ -152,7 +152,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute args = ( parsed_args.id, diff --git a/openstackclient/compute/v2/aggregate.py b/openstackclient/compute/v2/aggregate.py index d786d7e5b0..8fff4e6fcf 100644 --- a/openstackclient/compute/v2/aggregate.py +++ b/openstackclient/compute/v2/aggregate.py @@ -47,7 +47,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute @@ -89,7 +89,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute @@ -124,7 +124,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute data = utils.find_resource( @@ -150,7 +150,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute @@ -207,7 +207,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute @@ -257,7 +257,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute aggregate = utils.find_resource( @@ -304,7 +304,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute data = utils.find_resource( diff --git a/openstackclient/compute/v2/console.py b/openstackclient/compute/v2/console.py index 032168b906..e1f84e2326 100644 --- a/openstackclient/compute/v2/console.py +++ b/openstackclient/compute/v2/console.py @@ -48,7 +48,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute server = utils.find_resource( @@ -104,7 +104,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute server = utils.find_resource( compute_client.servers, diff --git a/openstackclient/compute/v2/fixedip.py b/openstackclient/compute/v2/fixedip.py index c41fed45c4..d105e3915f 100644 --- a/openstackclient/compute/v2/fixedip.py +++ b/openstackclient/compute/v2/fixedip.py @@ -42,7 +42,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute network = utils.find_resource( @@ -75,7 +75,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute server = utils.find_resource( diff --git a/openstackclient/compute/v2/flavor.py b/openstackclient/compute/v2/flavor.py index d1d08d8df0..7342979628 100644 --- a/openstackclient/compute/v2/flavor.py +++ b/openstackclient/compute/v2/flavor.py @@ -93,7 +93,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute args = ( @@ -128,7 +128,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute flavor = utils.find_resource(compute_client.flavors, parsed_args.flavor) @@ -142,7 +142,7 @@ class ListFlavor(lister.Lister): log = logging.getLogger(__name__ + ".ListFlavor") def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute columns = ( "ID", @@ -177,7 +177,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute flavor = utils.find_resource(compute_client.flavors, parsed_args.flavor)._info.copy() diff --git a/openstackclient/compute/v2/floatingip.py b/openstackclient/compute/v2/floatingip.py index 3edc809ef2..72b19c6cf5 100644 --- a/openstackclient/compute/v2/floatingip.py +++ b/openstackclient/compute/v2/floatingip.py @@ -45,7 +45,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute server = utils.find_resource( @@ -70,7 +70,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute floating_ip = compute_client.floating_ips.create(parsed_args.pool) @@ -94,7 +94,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute floating_ip = utils.find_resource( @@ -112,7 +112,7 @@ class ListFloatingIP(lister.Lister): log = logging.getLogger(__name__ + '.ListFloatingIP') def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute columns = ('ID', 'Pool', 'IP', 'Fixed IP', 'Instance ID') @@ -146,7 +146,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute server = utils.find_resource( diff --git a/openstackclient/compute/v2/floatingippool.py b/openstackclient/compute/v2/floatingippool.py index e1da97c3d8..d5e8d0dd62 100644 --- a/openstackclient/compute/v2/floatingippool.py +++ b/openstackclient/compute/v2/floatingippool.py @@ -28,7 +28,7 @@ class ListFloatingIPPool(lister.Lister): log = logging.getLogger(__name__ + '.ListFloatingIPPool') def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute columns = ('Name',) diff --git a/openstackclient/compute/v2/host.py b/openstackclient/compute/v2/host.py index 44f457d933..4f7273883f 100644 --- a/openstackclient/compute/v2/host.py +++ b/openstackclient/compute/v2/host.py @@ -36,7 +36,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute columns = ( "Host Name", @@ -64,7 +64,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute columns = ( "Host", diff --git a/openstackclient/compute/v2/hypervisor.py b/openstackclient/compute/v2/hypervisor.py index 535062e8da..334987e289 100644 --- a/openstackclient/compute/v2/hypervisor.py +++ b/openstackclient/compute/v2/hypervisor.py @@ -39,7 +39,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute columns = ( "ID", @@ -71,7 +71,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute hypervisor = utils.find_resource(compute_client.hypervisors, parsed_args.id)._info.copy() diff --git a/openstackclient/compute/v2/keypair.py b/openstackclient/compute/v2/keypair.py index 8a91f68208..972443a43c 100644 --- a/openstackclient/compute/v2/keypair.py +++ b/openstackclient/compute/v2/keypair.py @@ -48,7 +48,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute public_key = parsed_args.public_key @@ -93,7 +93,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute compute_client.keypairs.delete(parsed_args.name) return @@ -105,7 +105,7 @@ class ListKeypair(lister.Lister): log = logging.getLogger(__name__ + ".ListKeypair") def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute columns = ( "Name", @@ -140,7 +140,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute keypair = utils.find_resource(compute_client.keypairs, parsed_args.name) diff --git a/openstackclient/compute/v2/security_group.py b/openstackclient/compute/v2/security_group.py index be64bd3ab2..0ba55c9834 100644 --- a/openstackclient/compute/v2/security_group.py +++ b/openstackclient/compute/v2/security_group.py @@ -71,7 +71,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute @@ -100,7 +100,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute data = utils.find_resource( @@ -134,7 +134,7 @@ def _get_project(project_id): except KeyError: return project_id - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute columns = ( @@ -187,7 +187,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute data = utils.find_resource( @@ -228,7 +228,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute info = {} @@ -286,7 +286,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute group = utils.find_resource( @@ -340,7 +340,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute group = utils.find_resource( @@ -374,7 +374,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute group = utils.find_resource( diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 808741fdca..5eba838714 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -121,7 +121,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute volume_client = self.app.client_manager.volume @@ -162,7 +162,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute @@ -278,7 +278,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute # Lookup parsed_args.image @@ -363,8 +363,8 @@ def take_action(self, parsed_args): scheduler_hints=hints, config_drive=config_drive) - self.log.debug('boot_args: %s' % boot_args) - self.log.debug('boot_kwargs: %s' % boot_kwargs) + self.log.debug('boot_args: %s', boot_args) + self.log.debug('boot_kwargs: %s', boot_kwargs) server = compute_client.servers.create(*boot_args, **boot_kwargs) if parsed_args.wait: @@ -375,7 +375,7 @@ def take_action(self, parsed_args): ): sys.stdout.write('\n') else: - self.log.error('Error creating server: %s' % + self.log.error('Error creating server: %s', parsed_args.server_name) sys.stdout.write('\nError creating server') raise SystemExit @@ -409,7 +409,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute image_client = self.app.client_manager.image server = utils.find_resource( @@ -435,7 +435,7 @@ def take_action(self, parsed_args): sys.stdout.write('\n') else: self.log.error( - 'Error creating server snapshot: %s' % + 'Error creating server snapshot: %s', parsed_args.image_name, ) sys.stdout.write('\nError creating server snapshot') @@ -465,7 +465,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute server = utils.find_resource( compute_client.servers, parsed_args.server) @@ -530,7 +530,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute search_opts = { 'reservation_id': parsed_args.reservation_id, @@ -600,7 +600,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute utils.find_resource( @@ -672,7 +672,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute @@ -716,7 +716,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute utils.find_resource( @@ -762,7 +762,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute server = utils.find_resource( compute_client.servers, parsed_args.server) @@ -811,7 +811,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute # Lookup parsed_args.image @@ -856,7 +856,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute @@ -892,7 +892,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute volume_client = self.app.client_manager.volume @@ -927,7 +927,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute server = utils.find_resource( @@ -968,7 +968,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute server = utils.find_resource( @@ -1013,7 +1013,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute utils.find_resource( @@ -1054,7 +1054,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute server = utils.find_resource( @@ -1102,7 +1102,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute server = utils.find_resource(compute_client.servers, parsed_args.server) @@ -1224,7 +1224,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute server = utils.find_resource( @@ -1272,7 +1272,7 @@ def take_action(self, parsed_args): cmd += " -v" cmd += " %s@%s" - self.log.debug("ssh command: %s" % (cmd % (login, ip_address))) + self.log.debug("ssh command: %s", (cmd % (login, ip_address))) os.system(cmd % (login, ip_address)) @@ -1291,7 +1291,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute utils.find_resource( @@ -1315,7 +1315,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute utils.find_resource( @@ -1339,7 +1339,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute utils.find_resource( @@ -1363,7 +1363,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute utils.find_resource( @@ -1395,7 +1395,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute server = utils.find_resource( compute_client.servers, diff --git a/openstackclient/compute/v2/service.py b/openstackclient/compute/v2/service.py index 5e57e0aa5e..4b2ebac6ba 100644 --- a/openstackclient/compute/v2/service.py +++ b/openstackclient/compute/v2/service.py @@ -40,7 +40,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute columns = ( "Binary", @@ -88,7 +88,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute columns = ( "Host", diff --git a/openstackclient/compute/v2/usage.py b/openstackclient/compute/v2/usage.py index 1dfe8c0ae4..f7e11bcaca 100644 --- a/openstackclient/compute/v2/usage.py +++ b/openstackclient/compute/v2/usage.py @@ -47,7 +47,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) def _format_project(project): if not project: diff --git a/openstackclient/identity/v2_0/ec2creds.py b/openstackclient/identity/v2_0/ec2creds.py index cb60b677ca..74c9d5ebab 100644 --- a/openstackclient/identity/v2_0/ec2creds.py +++ b/openstackclient/identity/v2_0/ec2creds.py @@ -46,7 +46,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.project: @@ -93,7 +93,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.user: @@ -123,7 +123,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.user: @@ -166,7 +166,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.user: diff --git a/openstackclient/identity/v2_0/endpoint.py b/openstackclient/identity/v2_0/endpoint.py index 4ee1636ff0..36f52cad4d 100644 --- a/openstackclient/identity/v2_0/endpoint.py +++ b/openstackclient/identity/v2_0/endpoint.py @@ -57,7 +57,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity service = common.find_service(identity_client, parsed_args.service) endpoint = identity_client.endpoints.create( @@ -88,7 +88,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity identity_client.endpoints.delete(parsed_args.endpoint) return @@ -109,7 +109,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.long: columns = ('ID', 'Region', 'Service Name', 'Service Type', @@ -143,7 +143,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity data = identity_client.endpoints.list() match = None diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index 60a52ad4a6..2ee0938e0c 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -64,7 +64,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity enabled = True @@ -101,7 +101,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity project = utils.find_resource( @@ -129,7 +129,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) if parsed_args.long: columns = ('ID', 'Name', 'Description', 'Enabled') else: @@ -185,7 +185,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if (not parsed_args.name @@ -236,7 +236,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity project = utils.find_resource( identity_client.tenants, diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index fdf211082d..faf48ed95f 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -51,7 +51,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity role = utils.find_resource(identity_client.roles, parsed_args.role) project = utils.find_resource( @@ -84,7 +84,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity role = identity_client.roles.create(parsed_args.role_name) @@ -108,7 +108,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity role = utils.find_resource( @@ -126,7 +126,7 @@ class ListRole(lister.Lister): log = logging.getLogger(__name__ + '.ListRole') def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) columns = ('ID', 'Name') data = self.app.client_manager.identity.roles.list() return (columns, @@ -156,7 +156,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity auth_ref = self.app.client_manager.auth_ref @@ -228,7 +228,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity role = utils.find_resource(identity_client.roles, parsed_args.role) project = utils.find_resource( @@ -256,7 +256,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity role = utils.find_resource(identity_client.roles, parsed_args.role) diff --git a/openstackclient/identity/v2_0/service.py b/openstackclient/identity/v2_0/service.py index d61804c80e..138ed3b097 100644 --- a/openstackclient/identity/v2_0/service.py +++ b/openstackclient/identity/v2_0/service.py @@ -53,7 +53,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity service = identity_client.services.create( @@ -81,7 +81,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity service = common.find_service(identity_client, parsed_args.service) identity_client.services.delete(service.id) @@ -103,7 +103,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) if parsed_args.long: columns = ('ID', 'Name', 'Type', 'Description') @@ -138,7 +138,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.catalog: diff --git a/openstackclient/identity/v2_0/token.py b/openstackclient/identity/v2_0/token.py index a0433c9654..55e9f2dd1e 100644 --- a/openstackclient/identity/v2_0/token.py +++ b/openstackclient/identity/v2_0/token.py @@ -31,7 +31,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity token = identity_client.service_catalog.get_token() token['project_id'] = token.pop('tenant_id') diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index 688306baa1..60af6ddb98 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -72,7 +72,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.project: @@ -123,7 +123,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity user = utils.find_resource( @@ -155,7 +155,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity def _format_project(project): @@ -277,7 +277,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.password_prompt: @@ -343,7 +343,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity user = utils.find_resource( diff --git a/openstackclient/identity/v3/consumer.py b/openstackclient/identity/v3/consumer.py index ddeae6189a..11814e5cd3 100644 --- a/openstackclient/identity/v3/consumer.py +++ b/openstackclient/identity/v3/consumer.py @@ -41,7 +41,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity consumer = identity_client.consumers.create_consumer( parsed_args.description @@ -66,7 +66,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity consumer = utils.find_resource( identity_client.consumers, parsed_args.consumer) @@ -80,7 +80,7 @@ class ListConsumer(lister.Lister): log = logging.getLogger(__name__ + '.ListConsumer') def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) columns = ('ID', 'Description') data = self.app.client_manager.identity.consumers.list_consumers() return (columns, @@ -110,7 +110,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity consumer = utils.find_resource( identity_client.consumers, parsed_args.consumer) @@ -147,7 +147,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity consumer = utils.find_resource( identity_client.consumers, parsed_args.consumer) diff --git a/openstackclient/identity/v3/credential.py b/openstackclient/identity/v3/credential.py index 93f67f6c08..43d16c2962 100644 --- a/openstackclient/identity/v3/credential.py +++ b/openstackclient/identity/v3/credential.py @@ -58,7 +58,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity user_id = utils.find_resource(identity_client.users, parsed_args.user).id @@ -91,7 +91,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity identity_client.credentials.delete(parsed_args.credential) return @@ -103,7 +103,7 @@ class ListCredential(lister.Lister): log = logging.getLogger(__name__ + '.ListCredential') def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) columns = ('ID', 'Type', 'User ID', 'Data', 'Project ID') data = self.app.client_manager.identity.credentials.list() return (columns, @@ -149,7 +149,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity user_id = utils.find_resource(identity_client.users, parsed_args.user).id @@ -187,7 +187,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity credential = utils.find_resource(identity_client.credentials, parsed_args.credential) diff --git a/openstackclient/identity/v3/domain.py b/openstackclient/identity/v3/domain.py index a74b12e295..f976384738 100644 --- a/openstackclient/identity/v3/domain.py +++ b/openstackclient/identity/v3/domain.py @@ -58,7 +58,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity domain = identity_client.domains.create( name=parsed_args.name, @@ -84,7 +84,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity domain = utils.find_resource(identity_client.domains, parsed_args.domain) @@ -98,7 +98,7 @@ class ListDomain(lister.Lister): log = logging.getLogger(__name__ + '.ListDomain') def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) columns = ('ID', 'Name', 'Enabled', 'Description') data = self.app.client_manager.identity.domains.list() return (columns, @@ -147,7 +147,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity domain = utils.find_resource(identity_client.domains, parsed_args.domain) @@ -181,7 +181,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity domain = utils.find_resource(identity_client.domains, parsed_args.domain) diff --git a/openstackclient/identity/v3/endpoint.py b/openstackclient/identity/v3/endpoint.py index 3cd5e086e3..93d77be339 100644 --- a/openstackclient/identity/v3/endpoint.py +++ b/openstackclient/identity/v3/endpoint.py @@ -68,7 +68,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity service = common.find_service(identity_client, parsed_args.service) @@ -101,7 +101,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity endpoint_id = utils.find_resource(identity_client.endpoints, parsed_args.endpoint).id @@ -115,7 +115,7 @@ class ListEndpoint(lister.Lister): log = logging.getLogger(__name__ + '.ListEndpoint') def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity columns = ('ID', 'Region', 'Service Name', 'Service Type', 'Enabled', 'Interface', 'URL') @@ -177,7 +177,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity endpoint = utils.find_resource(identity_client.endpoints, parsed_args.endpoint) @@ -214,7 +214,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity endpoint = utils.find_resource(identity_client.endpoints, parsed_args.endpoint) diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index 38d810cb87..f51129c917 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -46,7 +46,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity user_id = utils.find_resource(identity_client.users, @@ -84,7 +84,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity user_id = utils.find_resource(identity_client.users, @@ -125,7 +125,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.domain: domain = utils.find_resource(identity_client.domains, @@ -156,7 +156,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity group = utils.find_resource(identity_client.groups, parsed_args.group) identity_client.groups.delete(group.id) @@ -202,7 +202,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.role: @@ -292,7 +292,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity user_id = utils.find_resource(identity_client.users, @@ -336,7 +336,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity group = utils.find_resource(identity_client.groups, parsed_args.group) kwargs = {} @@ -370,7 +370,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity group = utils.find_resource(identity_client.groups, parsed_args.group) diff --git a/openstackclient/identity/v3/identity_provider.py b/openstackclient/identity/v3/identity_provider.py index f577c31432..b60678b51f 100644 --- a/openstackclient/identity/v3/identity_provider.py +++ b/openstackclient/identity/v3/identity_provider.py @@ -59,7 +59,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity idp = identity_client.identity_providers.create( parsed_args.identity_provider_id, @@ -85,7 +85,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity identity_client.identity_providers.delete( parsed_args.identity_provider) @@ -98,7 +98,7 @@ class ListIdentityProvider(lister.Lister): log = logging.getLogger(__name__ + '.ListIdentityProvider') def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) columns = ('ID', 'Enabled', 'Description') data = self.app.client_manager.identity.identity_providers.list() return (columns, @@ -135,7 +135,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.enable is True: @@ -169,7 +169,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity identity_provider = utils.find_resource( identity_client.identity_providers, diff --git a/openstackclient/identity/v3/policy.py b/openstackclient/identity/v3/policy.py index 3e3fd77144..87f3cbe974 100644 --- a/openstackclient/identity/v3/policy.py +++ b/openstackclient/identity/v3/policy.py @@ -47,7 +47,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) blob = utils.read_blob_file_contents(parsed_args.blob_file) identity_client = self.app.client_manager.identity @@ -73,7 +73,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity identity_client.policies.delete(parsed_args.policy) return @@ -95,7 +95,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) if parsed_args.include_blob: columns = ('ID', 'Type', 'Blob') else: @@ -133,7 +133,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity blob = None @@ -168,7 +168,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity policy = utils.find_resource(identity_client.policies, parsed_args.policy) diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 36787bb0e2..04961a18e5 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -69,7 +69,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.domain: @@ -115,7 +115,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity project = utils.find_resource( @@ -148,7 +148,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.long: columns = ('ID', 'Name', 'Domain ID', 'Description', 'Enabled') @@ -216,7 +216,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if (not parsed_args.name @@ -274,7 +274,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity project = utils.find_resource(identity_client.projects, parsed_args.project) diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index 664a05dcc9..69c0aa6ae4 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -63,7 +63,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if (not parsed_args.user and not parsed_args.domain @@ -152,7 +152,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity role = identity_client.roles.create(name=parsed_args.name) @@ -175,7 +175,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity role = utils.find_resource( @@ -193,7 +193,7 @@ class ListRole(lister.Lister): log = logging.getLogger(__name__ + '.ListRole') def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) columns = ('ID', 'Name') data = self.app.client_manager.identity.roles.list() return (columns, @@ -240,7 +240,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if (not parsed_args.user and not parsed_args.domain @@ -334,7 +334,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if not parsed_args.name: @@ -364,7 +364,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity role = utils.find_resource( diff --git a/openstackclient/identity/v3/service.py b/openstackclient/identity/v3/service.py index f1aaca8717..88301edc05 100644 --- a/openstackclient/identity/v3/service.py +++ b/openstackclient/identity/v3/service.py @@ -57,7 +57,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity enabled = True @@ -88,7 +88,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity service = common.find_service(identity_client, parsed_args.service) @@ -103,7 +103,7 @@ class ListService(lister.Lister): log = logging.getLogger(__name__ + '.ListService') def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) columns = ('ID', 'Name', 'Type', 'Enabled') data = self.app.client_manager.identity.services.list() @@ -150,7 +150,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if (not parsed_args.name @@ -195,7 +195,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity service = common.find_service(identity_client, parsed_args.service) diff --git a/openstackclient/identity/v3/token.py b/openstackclient/identity/v3/token.py index 3cc78cd7a8..a84d724a1c 100644 --- a/openstackclient/identity/v3/token.py +++ b/openstackclient/identity/v3/token.py @@ -60,7 +60,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) token_client = self.app.client_manager.identity.tokens keystone_token = token_client.authenticate_access_token( parsed_args.consumer_key, parsed_args.consumer_secret, @@ -84,7 +84,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) token_client = self.app.client_manager.identity.tokens verifier_pin = token_client.authorize_request_token( @@ -134,7 +134,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) token_client = self.app.client_manager.identity.tokens access_token = token_client.create_access_token( parsed_args.consumer_key, parsed_args.consumer_secret, @@ -175,7 +175,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) token_client = self.app.client_manager.identity.tokens request_token = token_client.create_request_token( parsed_args.consumer_key, @@ -195,7 +195,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity token = identity_client.service_catalog.get_token() if 'tenant_id' in token: @@ -223,7 +223,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity user = utils.find_resource( @@ -248,7 +248,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity user = utils.find_resource( diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index a5209020a6..e628e884f3 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -83,7 +83,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.project: @@ -136,7 +136,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity user = utils.find_resource( @@ -187,7 +187,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.role: @@ -321,7 +321,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.password_prompt: @@ -385,7 +385,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity user = utils.find_resource( diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index 8c1501bdb0..a78f7baa79 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -163,7 +163,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) # NOTE(jk0): Since create() takes kwargs, it's easiest to just make a # copy of parsed_args and remove what we don't need. @@ -233,7 +233,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) image_client = self.app.client_manager.image image = utils.find_resource( @@ -258,7 +258,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) image_client = self.app.client_manager.image @@ -292,7 +292,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) image_client = self.app.client_manager.image image = utils.find_resource( @@ -376,7 +376,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) # NOTE(jk0): Since create() takes kwargs, it's easiest to just make a # copy of parsed_args and remove what we don't need. @@ -417,7 +417,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) image_client = self.app.client_manager.image image = utils.find_resource( diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index e84e0d0191..08897b2bbf 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -41,7 +41,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) image_client = self.app.client_manager.image image = utils.find_resource( @@ -66,7 +66,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) image_client = self.app.client_manager.image @@ -100,7 +100,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) image_client = self.app.client_manager.image image = utils.find_resource( @@ -127,7 +127,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug("take_action(%s)" % parsed_args) + self.log.debug("take_action(%s)", parsed_args) image_client = self.app.client_manager.image image = utils.find_resource( diff --git a/openstackclient/object/v1/container.py b/openstackclient/object/v1/container.py index fcfbd78351..1e252aaf4f 100644 --- a/openstackclient/object/v1/container.py +++ b/openstackclient/object/v1/container.py @@ -69,7 +69,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) if parsed_args.long: columns = ('Name', 'Bytes', 'Count') @@ -116,7 +116,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) data = lib_container.show_container( self.app.restapi, diff --git a/openstackclient/object/v1/object.py b/openstackclient/object/v1/object.py index f6a770302b..ee30c84210 100644 --- a/openstackclient/object/v1/object.py +++ b/openstackclient/object/v1/object.py @@ -79,7 +79,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) if parsed_args.long: columns = ( @@ -140,7 +140,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) data = lib_object.show_object( self.app.restapi, diff --git a/openstackclient/shell.py b/openstackclient/shell.py index cc4570a178..719ee4803b 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -422,13 +422,14 @@ def initialize_app(self, argv): ver = getattr(self.options, mod.API_VERSION_OPTION, None) if ver: self.api_version[mod.API_NAME] = ver - self.log.debug('%s API version %s' % (mod.API_NAME, ver)) + self.log.debug('%(name)s API version %(version)s', + {'name': mod.API_NAME, 'version': ver}) # Add the API version-specific commands for api in self.api_version.keys(): version = '.v' + self.api_version[api].replace('.', '_') cmd_group = 'openstack.' + api.replace('-', '_') + version - self.log.debug('command group %s' % cmd_group) + self.log.debug('command group %s', cmd_group) self.command_manager.add_command_group(cmd_group) # Commands that span multiple APIs diff --git a/openstackclient/volume/v1/backup.py b/openstackclient/volume/v1/backup.py index ac34749baf..992fa7be71 100644 --- a/openstackclient/volume/v1/backup.py +++ b/openstackclient/volume/v1/backup.py @@ -57,7 +57,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume volume_id = utils.find_resource(volume_client.volumes, parsed_args.volume).id @@ -87,7 +87,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume backup_id = utils.find_resource(volume_client.backups, parsed_args.backup).id @@ -101,7 +101,7 @@ class ListBackup(lister.Lister): log = logging.getLogger(__name__ + '.ListBackup') def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) columns = ( 'ID', 'Display Name', @@ -135,7 +135,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume backup = utils.find_resource(volume_client.backups, parsed_args.backup) @@ -159,7 +159,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume backup = utils.find_resource(volume_client.backups, parsed_args.backup) diff --git a/openstackclient/volume/v1/snapshot.py b/openstackclient/volume/v1/snapshot.py index d3a56b759e..9cc3c4c1d5 100644 --- a/openstackclient/volume/v1/snapshot.py +++ b/openstackclient/volume/v1/snapshot.py @@ -59,7 +59,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume volume_id = utils.find_resource(volume_client.volumes, parsed_args.volume).id @@ -88,7 +88,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume snapshot_id = utils.find_resource(volume_client.volume_snapshots, parsed_args.snapshot).id @@ -102,7 +102,7 @@ class ListSnapshot(lister.Lister): log = logging.getLogger(__name__ + '.ListSnapshot') def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) columns = ( 'ID', 'Display Name', @@ -140,7 +140,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume snapshot = utils.find_resource(volume_client.volume_snapshots, parsed_args.snapshot) @@ -171,7 +171,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume snapshot = utils.find_resource(volume_client.volume_snapshots, parsed_args.snapshot) diff --git a/openstackclient/volume/v1/type.py b/openstackclient/volume/v1/type.py index b199b7c3e0..71bfc9eaba 100644 --- a/openstackclient/volume/v1/type.py +++ b/openstackclient/volume/v1/type.py @@ -48,7 +48,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume volume_type = volume_client.volume_types.create(parsed_args.name) volume_type._info.pop('extra_specs') @@ -76,7 +76,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume volume_type_id = utils.find_resource( volume_client.volume_types, parsed_args.volume_type).id @@ -99,7 +99,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) if parsed_args.long: columns = ('ID', 'Name', 'Extra Specs') column_headers = ('ID', 'Name', 'Properties') @@ -136,7 +136,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume volume_type = utils.find_resource( volume_client.volume_types, parsed_args.volume_type) @@ -170,7 +170,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume volume_type = utils.find_resource( volume_client.volume_types, diff --git a/openstackclient/volume/v1/volume.py b/openstackclient/volume/v1/volume.py index cad53eb111..3e4af56ce3 100644 --- a/openstackclient/volume/v1/volume.py +++ b/openstackclient/volume/v1/volume.py @@ -96,7 +96,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity volume_client = self.app.client_manager.volume @@ -164,7 +164,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume volume = utils.find_resource( volume_client.volumes, parsed_args.volume) @@ -207,7 +207,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) if parsed_args.long: columns = ( @@ -293,7 +293,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume volume = utils.find_resource(volume_client.volumes, parsed_args.volume) @@ -329,7 +329,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume volume = utils.find_resource(volume_client.volumes, parsed_args.volume) # Map 'metadata' column to 'properties' @@ -370,7 +370,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume volume = utils.find_resource( volume_client.volumes, parsed_args.volume) From a8087a6c8b5946ecf25f019e183b26579c3475a8 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Wed, 21 May 2014 07:47:29 -0700 Subject: [PATCH 0100/3494] Fixed several typos throughout the codebase Change-Id: I048ee857fc1215fea7f60978364894e1b5abdf66 --- README.rst | 2 +- openstackclient/identity/v2_0/project.py | 2 +- openstackclient/identity/v3/project.py | 2 +- openstackclient/tests/common/test_parseractions.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 86e224e0bb..6da3d6a592 100644 --- a/README.rst +++ b/README.rst @@ -94,7 +94,7 @@ prompted to provide one securely. If keyring is enabled, the password entered in the prompt is stored in keyring. From next time, the password is read from keyring, if it is not provided above (in plaintext). -The token flow variation for authentication uses an already-aquired token +The token flow variation for authentication uses an already-acquired token and a URL pointing directly to the service API that presumably was acquired from the Service Catalog:: diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index 60a52ad4a6..410d2d37f1 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -214,7 +214,7 @@ def take_action(self, parsed_args): if 'id' in kwargs: del kwargs['id'] if 'name' in kwargs: - # Hack around borken Identity API arg names + # Hack around broken Identity API arg names kwargs['tenant_name'] = kwargs['name'] del kwargs['name'] diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 36787bb0e2..e7c99ec5ac 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -251,7 +251,7 @@ def take_action(self, parsed_args): if 'id' in kwargs: del kwargs['id'] if 'domain_id' in kwargs: - # Hack around borken Identity API arg names + # Hack around broken Identity API arg names kwargs.update( {'domain': kwargs.pop('domain_id')} ) diff --git a/openstackclient/tests/common/test_parseractions.py b/openstackclient/tests/common/test_parseractions.py index 705e7e9c36..f26f2891f6 100644 --- a/openstackclient/tests/common/test_parseractions.py +++ b/openstackclient/tests/common/test_parseractions.py @@ -88,7 +88,7 @@ def test_error_values(self): failhere = None actual = getattr(results, 'property', {}) - # Verify non-existant red key + # Verify non-existent red key try: failhere = actual['red'] except Exception as e: From d6321c0893d529af1548da79a985f337bce7069f Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Fri, 23 May 2014 10:17:42 -0600 Subject: [PATCH 0101/3494] Add token delete command for identity v2 Identity v2 has undocumented support for token delete and keystoneclient also has support. Change-Id: Ib98d17958ceb88f7b63471691dee71fdb884ce2e Closes-Bug: #1318442 --- openstackclient/identity/v2_0/token.py | 22 +++++++++++++++++++ openstackclient/tests/identity/v2_0/fakes.py | 2 ++ .../tests/identity/v2_0/test_token.py | 21 ++++++++++++++++++ setup.cfg | 1 + 4 files changed, 46 insertions(+) diff --git a/openstackclient/identity/v2_0/token.py b/openstackclient/identity/v2_0/token.py index a0433c9654..793354cf6f 100644 --- a/openstackclient/identity/v2_0/token.py +++ b/openstackclient/identity/v2_0/token.py @@ -18,6 +18,7 @@ import logging import six +from cliff import command from cliff import show @@ -36,3 +37,24 @@ def take_action(self, parsed_args): token = identity_client.service_catalog.get_token() token['project_id'] = token.pop('tenant_id') return zip(*sorted(six.iteritems(token))) + + +class DeleteToken(command.Command): + """Delete token command""" + + log = logging.getLogger(__name__ + '.DeleteToken') + + def get_parser(self, prog_name): + parser = super(DeleteToken, self).get_parser(prog_name) + parser.add_argument( + 'token', + metavar='', + help='Token to be deleted', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity + identity_client.tokens.delete(parsed_args.token) + return diff --git a/openstackclient/tests/identity/v2_0/fakes.py b/openstackclient/tests/identity/v2_0/fakes.py index 231fa1a5e7..e57a5b3006 100644 --- a/openstackclient/tests/identity/v2_0/fakes.py +++ b/openstackclient/tests/identity/v2_0/fakes.py @@ -90,6 +90,8 @@ def __init__(self, **kwargs): self.services.resource_class = fakes.FakeResource(None, {}) self.tenants = mock.Mock() self.tenants.resource_class = fakes.FakeResource(None, {}) + self.tokens = mock.Mock() + self.tokens.resource_class = fakes.FakeResource(None, {}) self.users = mock.Mock() self.users.resource_class = fakes.FakeResource(None, {}) self.ec2 = mock.Mock() diff --git a/openstackclient/tests/identity/v2_0/test_token.py b/openstackclient/tests/identity/v2_0/test_token.py index a156cdc641..e1967537b2 100644 --- a/openstackclient/tests/identity/v2_0/test_token.py +++ b/openstackclient/tests/identity/v2_0/test_token.py @@ -54,3 +54,24 @@ def test_token_create(self): identity_fakes.user_id, ) self.assertEqual(data, datalist) + + +class TestTokenDelete(TestToken): + + TOKEN = 'fob' + + def setUp(self): + super(TestTokenDelete, self).setUp() + self.tokens_mock = self.app.client_manager.identity.tokens + self.tokens_mock.reset_mock() + self.tokens_mock.delete.return_value = True + self.cmd = token.DeleteToken(self.app, None) + + def test_token_create(self): + arglist = [self.TOKEN] + verifylist = [('token', self.TOKEN)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.tokens_mock.delete.assert_called_with(self.TOKEN) diff --git a/setup.cfg b/setup.cfg index 5a4fa743fe..a1252cb8fe 100644 --- a/setup.cfg +++ b/setup.cfg @@ -152,6 +152,7 @@ openstack.identity.v2_0 = service_show =openstackclient.identity.v2_0.service:ShowService token_create =openstackclient.identity.v2_0.token:CreateToken + token_delete =openstackclient.identity.v2_0.token:DeleteToken user_role_list = openstackclient.identity.v2_0.role:ListUserRole From 58f80e4c7544c0ee064c1629b3f128a628fc71d8 Mon Sep 17 00:00:00 2001 From: henriquetruta Date: Wed, 28 May 2014 11:09:46 -0300 Subject: [PATCH 0102/3494] Add role assignments list support to identity v3 The assignments manager and its test class were created. Some fake stubs were also added on the fakes.py module. The "openstack role assignment list" command was created. Change-Id: Iae94f4fee608ea3e09ff38961ad22edc38efb89c Implements: blueprint roles-assignment-list Closes-Bug: 1246310 --- .../identity/v3/role_assignment.py | 156 +++++++ openstackclient/tests/identity/v3/fakes.py | 27 ++ .../tests/identity/v3/test_role_assignment.py | 388 ++++++++++++++++++ setup.cfg | 1 + 4 files changed, 572 insertions(+) create mode 100644 openstackclient/identity/v3/role_assignment.py create mode 100644 openstackclient/tests/identity/v3/test_role_assignment.py diff --git a/openstackclient/identity/v3/role_assignment.py b/openstackclient/identity/v3/role_assignment.py new file mode 100644 index 0000000000..5cc97e8d0f --- /dev/null +++ b/openstackclient/identity/v3/role_assignment.py @@ -0,0 +1,156 @@ +# 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. +# + +"""Identity v3 Assignment action implementations """ + +import logging + +from cliff import lister + +from openstackclient.common import utils + + +class ListRoleAssignment(lister.Lister): + """Lists role assignments according to the given filters""" + + log = logging.getLogger(__name__ + '.ListRoleAssignment') + + def get_parser(self, prog_name): + parser = super(ListRoleAssignment, self).get_parser(prog_name) + parser.add_argument( + '--effective', + action="store_true", + default=False, + help='Returns only effective role assignments', + ) + parser.add_argument( + '--role', + metavar='', + help='Name or ID of role to filter', + ) + user_or_group = parser.add_mutually_exclusive_group() + user_or_group.add_argument( + '--user', + metavar='', + help='Name or ID of user to filter', + ) + user_or_group.add_argument( + '--group', + metavar='', + help='Name or ID of group to filter', + ) + domain_or_project = parser.add_mutually_exclusive_group() + domain_or_project.add_argument( + '--domain', + metavar='', + help='Name or ID of domain to filter', + ) + domain_or_project.add_argument( + '--project', + metavar='', + help='Name or ID of project to filter', + ) + + return parser + + def _as_tuple(self, assignment): + return (assignment.role, assignment.user, assignment.group, + assignment.project, assignment.domain) + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity + + role = None + if parsed_args.role: + role = utils.find_resource( + identity_client.roles, + parsed_args.role, + ) + + user = None + if parsed_args.user: + user = utils.find_resource( + identity_client.users, + parsed_args.user, + ) + + domain = None + if parsed_args.domain: + domain = utils.find_resource( + identity_client.domains, + parsed_args.domain, + ) + + project = None + if parsed_args.project: + project = utils.find_resource( + identity_client.projects, + parsed_args.project, + ) + + group = None + if parsed_args.group: + group = utils.find_resource( + identity_client.groups, + parsed_args.group, + ) + + effective = True if parsed_args.effective else False + self.log.debug('take_action(%s)' % parsed_args) + columns = ('Role', 'User', 'Group', 'Project', 'Domain') + data = identity_client.role_assignments.list( + domain=domain, + user=user, + group=group, + project=project, + role=role, + effective=effective) + + data_parsed = [] + for assignment in data: + # Removing the extra "scope" layer in the assignment json + scope = assignment.scope + if 'project' in scope: + setattr(assignment, 'project', scope['project']['id']) + assignment.domain = '' + elif 'domain' in scope: + setattr(assignment, 'domain', scope['domain']['id']) + assignment.project = '' + + else: + assignment.domain = '' + assignment.project = '' + + del assignment.scope + + if hasattr(assignment, 'user'): + setattr(assignment, 'user', assignment.user['id']) + assignment.group = '' + elif hasattr(assignment, 'group'): + setattr(assignment, 'group', assignment.group['id']) + assignment.user = '' + else: + assignment.user = '' + assignment.group = '' + + if hasattr(assignment, 'role'): + setattr(assignment, 'role', assignment.role['id']) + else: + assignment.role = '' + + # Creating a tuple from data object fields + # (including the blank ones) + data_parsed.append(self._as_tuple(assignment)) + + return columns, tuple(data_parsed) diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index ffa89a5f46..86ef2f0c75 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -114,6 +114,31 @@ 'description': idp_description } +#Assignments +ASSIGNMENT_WITH_PROJECT_ID_AND_USER_ID = { + 'scope': {'project': {'id': project_id}}, + 'user': {'id': user_id}, + 'role': {'id': role_id}, +} + +ASSIGNMENT_WITH_PROJECT_ID_AND_GROUP_ID = { + 'scope': {'project': {'id': project_id}}, + 'group': {'id': group_id}, + 'role': {'id': role_id}, +} + +ASSIGNMENT_WITH_DOMAIN_ID_AND_USER_ID = { + 'scope': {'domain': {'id': domain_id}}, + 'user': {'id': user_id}, + 'role': {'id': role_id}, +} + +ASSIGNMENT_WITH_DOMAIN_ID_AND_GROUP_ID = { + 'scope': {'domain': {'id': domain_id}}, + 'group': {'id': group_id}, + 'role': {'id': role_id}, +} + class FakeIdentityv3Client(object): def __init__(self, **kwargs): @@ -130,6 +155,8 @@ def __init__(self, **kwargs): self.service_catalog = mock.Mock() self.users = mock.Mock() self.users.resource_class = fakes.FakeResource(None, {}) + self.role_assignments = mock.Mock() + self.role_assignments.resource_class = fakes.FakeResource(None, {}) self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] diff --git a/openstackclient/tests/identity/v3/test_role_assignment.py b/openstackclient/tests/identity/v3/test_role_assignment.py new file mode 100644 index 0000000000..6497ca8ed3 --- /dev/null +++ b/openstackclient/tests/identity/v3/test_role_assignment.py @@ -0,0 +1,388 @@ +# 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 copy + +from openstackclient.identity.v3 import role_assignment +from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes + + +class TestRoleAssignment(identity_fakes.TestIdentityv3): + + def setUp(self): + super(TestRoleAssignment, self).setUp() + + +class TestRoleAssignmentList(TestRoleAssignment): + + def setUp(self): + super(TestRoleAssignment, self).setUp() + + # Get a shortcut to the UserManager Mock + self.users_mock = self.app.client_manager.identity.users + self.users_mock.reset_mock() + + # Get a shortcut to the GroupManager Mock + self.groups_mock = self.app.client_manager.identity.groups + self.groups_mock.reset_mock() + + # Get a shortcut to the DomainManager Mock + self.domains_mock = self.app.client_manager.identity.domains + self.domains_mock.reset_mock() + + # Get a shortcut to the ProjectManager Mock + self.projects_mock = self.app.client_manager.identity.projects + self.projects_mock.reset_mock() + + # Get a shortcut to the RoleManager Mock + self.roles_mock = self.app.client_manager.identity.roles + self.roles_mock.reset_mock() + + self.role_assignments_mock = self.app.client_manager.identity.\ + role_assignments + self.role_assignments_mock.reset_mock() + + # Get the command object to test + self.cmd = role_assignment.ListRoleAssignment(self.app, None) + + def test_role_assignment_list_no_filters(self): + + self.role_assignments_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy( + identity_fakes.ASSIGNMENT_WITH_PROJECT_ID_AND_USER_ID), + loaded=True, + ), + fakes.FakeResource( + None, + copy.deepcopy( + identity_fakes.ASSIGNMENT_WITH_PROJECT_ID_AND_GROUP_ID), + loaded=True, + ), + ] + + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.role_assignments_mock.list.assert_called_with( + domain=None, + group=None, + effective=False, + role=None, + user=None, + project=None) + + collist = ('Role', 'User', 'Group', 'Project', 'Domain') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.role_id, + identity_fakes.user_id, + '', + identity_fakes.project_id, + '' + ), (identity_fakes.role_id, + '', + identity_fakes.group_id, + identity_fakes.project_id, + '' + ),) + self.assertEqual(tuple(data), datalist) + + def test_role_assignment_list_user(self): + + self.role_assignments_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy( + identity_fakes.ASSIGNMENT_WITH_DOMAIN_ID_AND_USER_ID), + loaded=True, + ), + fakes.FakeResource( + None, + copy.deepcopy( + identity_fakes.ASSIGNMENT_WITH_PROJECT_ID_AND_USER_ID), + loaded=True, + ), + ] + + arglist = [ + '--user', identity_fakes.user_name + ] + verifylist = [ + ('user', identity_fakes.user_name), + ('group', None), + ('domain', None), + ('project', None), + ('role', None), + ('effective', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.role_assignments_mock.list.assert_called_with( + domain=None, + user=self.users_mock.get(), + group=None, + project=None, + role=None, + effective=False) + + collist = ('Role', 'User', 'Group', 'Project', 'Domain') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.role_id, + identity_fakes.user_id, + '', + '', + identity_fakes.domain_id + ), (identity_fakes.role_id, + identity_fakes.user_id, + '', + identity_fakes.project_id, + '' + ),) + self.assertEqual(tuple(data), datalist) + + def test_role_assignment_list_group(self): + + self.role_assignments_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy( + identity_fakes.ASSIGNMENT_WITH_DOMAIN_ID_AND_GROUP_ID), + loaded=True, + ), + fakes.FakeResource( + None, + copy.deepcopy( + identity_fakes.ASSIGNMENT_WITH_PROJECT_ID_AND_GROUP_ID), + loaded=True, + ), + ] + + arglist = [ + '--group', identity_fakes.group_name + ] + verifylist = [ + ('user', None), + ('group', identity_fakes.group_name), + ('domain', None), + ('project', None), + ('role', None), + ('effective', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.role_assignments_mock.list.assert_called_with( + domain=None, + group=self.groups_mock.get(), + effective=False, + project=None, + role=None, + user=None) + + collist = ('Role', 'User', 'Group', 'Project', 'Domain') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.role_id, + '', + identity_fakes.group_id, + '', + identity_fakes.domain_id + ), (identity_fakes.role_id, + '', + identity_fakes.group_id, + identity_fakes.project_id, + '' + ),) + self.assertEqual(tuple(data), datalist) + + def test_role_assignment_list_domain(self): + + self.role_assignments_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy( + identity_fakes.ASSIGNMENT_WITH_DOMAIN_ID_AND_USER_ID), + loaded=True, + ), + fakes.FakeResource( + None, + copy.deepcopy( + identity_fakes.ASSIGNMENT_WITH_DOMAIN_ID_AND_GROUP_ID), + loaded=True, + ), + ] + + arglist = [ + '--domain', identity_fakes.domain_name + ] + verifylist = [ + ('user', None), + ('group', None), + ('domain', identity_fakes.domain_name), + ('project', None), + ('role', None), + ('effective', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.role_assignments_mock.list.assert_called_with( + domain=self.domains_mock.get(), + group=None, + effective=False, + project=None, + role=None, + user=None) + + collist = ('Role', 'User', 'Group', 'Project', 'Domain') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.role_id, + identity_fakes.user_id, + '', + '', + identity_fakes.domain_id + ), (identity_fakes.role_id, + '', + identity_fakes.group_id, + '', + identity_fakes.domain_id + ),) + self.assertEqual(tuple(data), datalist) + + def test_role_assignment_list_project(self): + + self.role_assignments_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy( + identity_fakes.ASSIGNMENT_WITH_PROJECT_ID_AND_USER_ID), + loaded=True, + ), + fakes.FakeResource( + None, + copy.deepcopy( + identity_fakes.ASSIGNMENT_WITH_PROJECT_ID_AND_GROUP_ID), + loaded=True, + ), + ] + + arglist = [ + '--project', identity_fakes.project_name + ] + verifylist = [ + ('user', None), + ('group', None), + ('domain', None), + ('project', identity_fakes.project_name), + ('role', None), + ('effective', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.role_assignments_mock.list.assert_called_with( + domain=None, + group=None, + effective=False, + project=self.projects_mock.get(), + role=None, + user=None) + + collist = ('Role', 'User', 'Group', 'Project', 'Domain') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.role_id, + identity_fakes.user_id, + '', + identity_fakes.project_id, + '' + ), (identity_fakes.role_id, + '', + identity_fakes.group_id, + identity_fakes.project_id, + '' + ),) + self.assertEqual(tuple(data), datalist) + + def test_role_assignment_list_effective(self): + + self.role_assignments_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy( + identity_fakes.ASSIGNMENT_WITH_PROJECT_ID_AND_USER_ID), + loaded=True, + ), + fakes.FakeResource( + None, + copy.deepcopy( + identity_fakes.ASSIGNMENT_WITH_DOMAIN_ID_AND_USER_ID), + loaded=True, + ), + ] + + arglist = ['--effective'] + verifylist = [ + ('user', None), + ('group', None), + ('domain', None), + ('project', None), + ('role', None), + ('effective', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.role_assignments_mock.list.assert_called_with( + domain=None, + group=None, + effective=True, + project=None, + role=None, + user=None) + + collist = ('Role', 'User', 'Group', 'Project', 'Domain') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.role_id, + identity_fakes.user_id, + '', + identity_fakes.project_id, + '' + ), (identity_fakes.role_id, + identity_fakes.user_id, + '', + '', + identity_fakes.domain_id, + ),) + self.assertEqual(tuple(data), datalist) diff --git a/setup.cfg b/setup.cfg index 5a4fa743fe..3ef54551b1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -228,6 +228,7 @@ openstack.identity.v3 = role_remove = openstackclient.identity.v3.role:RemoveRole role_show = openstackclient.identity.v3.role:ShowRole role_set = openstackclient.identity.v3.role:SetRole + role_assignment_list = openstackclient.identity.v3.role_assignment:ListRoleAssignment service_create = openstackclient.identity.v3.service:CreateService service_delete = openstackclient.identity.v3.service:DeleteService From 4ae4dc35bda42a972c1d1480e89cda67bf39636d Mon Sep 17 00:00:00 2001 From: Matt Fischer Date: Tue, 13 May 2014 13:04:02 -0400 Subject: [PATCH 0103/3494] Add support for extension list - Add support in the common section for extension list. This only supports Identity for now. Once the APIs for volume and compute are supported in the respective APIs, they will be added. Once network is added to this client, it will be added (the API already supports it). - Include extension fakes for volume and compute for pre-enablement. Change-Id: Iebb0156a779887d2ab06488a2a27b70b56369376 Closes-Bug: #1319115 --- openstackclient/common/extension.py | 78 +++++++++++ .../tests/common/test_extension.py | 128 ++++++++++++++++++ openstackclient/tests/compute/v2/fakes.py | 21 +++ openstackclient/tests/identity/v2_0/fakes.py | 22 +++ openstackclient/tests/volume/v1/fakes.py | 22 +++ setup.cfg | 1 + 6 files changed, 272 insertions(+) create mode 100644 openstackclient/common/extension.py create mode 100644 openstackclient/tests/common/test_extension.py diff --git a/openstackclient/common/extension.py b/openstackclient/common/extension.py new file mode 100644 index 0000000000..a8b1a6b08b --- /dev/null +++ b/openstackclient/common/extension.py @@ -0,0 +1,78 @@ +# Copyright 2012-2013 OpenStack Foundation +# +# 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. +# + +"""Extension action implementations""" + +import logging + +from cliff import lister + +from openstackclient.common import exceptions as exc +from openstackclient.common import utils + + +class ListExtension(lister.Lister): + """List extension command""" + + # TODO(mfisch): add support for volume and compute + # when the underlying APIs support it. Add support + # for network when it's added to openstackclient. + + log = logging.getLogger(__name__ + '.ListExtension') + + def get_parser(self, prog_name): + parser = super(ListExtension, self).get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + default=False, + help='List additional fields in output') + parser.add_argument( + '--identity', + action='store_true', + default=False, + help='List extensions for the Identity API') + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + + if parsed_args.long: + columns = ('Name', 'Namespace', 'Description', + 'Alias', 'Updated', 'Links') + else: + columns = ('Name', 'Alias', 'Description') + + data = [] + + # by default we want to show everything, unless the + # user specifies one or more of the APIs to show + # for now, only identity is supported + show_all = (not parsed_args.identity) + + if parsed_args.identity or show_all: + identity_client = self.app.client_manager.identity + try: + data += identity_client.extensions.list() + except Exception: + raise exc.CommandError( + "Extensions list not supported by" + " identity API") + + return (columns, + (utils.get_item_properties( + s, columns, + formatters={}, + ) for s in data)) diff --git a/openstackclient/tests/common/test_extension.py b/openstackclient/tests/common/test_extension.py new file mode 100644 index 0000000000..2e6e7050f8 --- /dev/null +++ b/openstackclient/tests/common/test_extension.py @@ -0,0 +1,128 @@ +# 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 copy + +from openstackclient.common import extension +from openstackclient.tests import fakes +from openstackclient.tests import utils + +from openstackclient.tests.identity.v2_0 import fakes as identity_fakes + + +class TestExtension(utils.TestCommand): + + def setUp(self): + super(TestExtension, self).setUp() + + self.app.client_manager.identity = identity_fakes.FakeIdentityv2Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) + + # Get shortcuts to the ExtensionManager Mocks + self.identity_extensions_mock = ( + self.app.client_manager.identity.extensions) + self.identity_extensions_mock.reset_mock() + + +class TestExtensionList(TestExtension): + + def setUp(self): + super(TestExtensionList, self).setUp() + + self.identity_extensions_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.EXTENSION), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = extension.ListExtension(self.app, None) + + def test_extension_list_no_options(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # no args should output from all services + self.identity_extensions_mock.list.assert_called_with() + + collist = ('Name', 'Alias', 'Description') + self.assertEqual(columns, collist) + datalist = ( + ( + identity_fakes.extension_name, + identity_fakes.extension_alias, + identity_fakes.extension_description, + ), + ) + self.assertEqual(tuple(data), datalist) + + def test_extension_list_long(self): + arglist = [ + '--long', + ] + verifylist = [ + ('long', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # no args should output from all services + self.identity_extensions_mock.list.assert_called_with() + + collist = ('Name', 'Namespace', 'Description', 'Alias', 'Updated', + 'Links') + self.assertEqual(columns, collist) + datalist = ( + ( + identity_fakes.extension_name, + identity_fakes.extension_namespace, + identity_fakes.extension_description, + identity_fakes.extension_alias, + identity_fakes.extension_updated, + identity_fakes.extension_links, + ), + ) + self.assertEqual(tuple(data), datalist) + + def test_extension_list_identity(self): + arglist = [ + '--identity', + ] + verifylist = [ + ('identity', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.identity_extensions_mock.list.assert_called_with() + + collist = ('Name', 'Alias', 'Description') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.extension_name, + identity_fakes.extension_alias, + identity_fakes.extension_description, + ), ) + self.assertEqual(tuple(data), datalist) diff --git a/openstackclient/tests/compute/v2/fakes.py b/openstackclient/tests/compute/v2/fakes.py index 03ebd67c99..cef5ee9052 100644 --- a/openstackclient/tests/compute/v2/fakes.py +++ b/openstackclient/tests/compute/v2/fakes.py @@ -28,6 +28,25 @@ 'name': server_name, } +extension_name = 'Multinic' +extension_namespace = 'http://docs.openstack.org/compute/ext/'\ + 'multinic/api/v1.1' +extension_description = 'Multiple network support' +extension_updated = '2014-01-07T12:00:0-00:00' +extension_alias = 'NMN' +extension_links = '[{"href":'\ + '"https://github.com/openstack/compute-api", "type":'\ + ' "text/html", "rel": "describedby"}]' + +EXTENSION = { + 'name': extension_name, + 'namespace': extension_namespace, + 'description': extension_description, + 'updated': extension_updated, + 'alias': extension_alias, + 'links': extension_links, +} + class FakeComputev2Client(object): def __init__(self, **kwargs): @@ -35,6 +54,8 @@ def __init__(self, **kwargs): self.images.resource_class = fakes.FakeResource(None, {}) self.servers = mock.Mock() self.servers.resource_class = fakes.FakeResource(None, {}) + self.extensions = mock.Mock() + self.extensions.resource_class = fakes.FakeResource(None, {}) self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] diff --git a/openstackclient/tests/identity/v2_0/fakes.py b/openstackclient/tests/identity/v2_0/fakes.py index 59860d22a4..8413dd1ef0 100644 --- a/openstackclient/tests/identity/v2_0/fakes.py +++ b/openstackclient/tests/identity/v2_0/fakes.py @@ -100,6 +100,26 @@ 'service_id': endpoint_service_id, } +extension_name = 'OpenStack Keystone User CRUD' +extension_namespace = 'http://docs.openstack.org/identity/'\ + 'api/ext/OS-KSCRUD/v1.0' +extension_description = 'OpenStack extensions to Keystone v2.0 API'\ + ' enabling User Operations.' +extension_updated = '2013-07-07T12:00:0-00:00' +extension_alias = 'OS-KSCRUD' +extension_links = '[{"href":'\ + '"https://github.com/openstack/identity-api", "type":'\ + ' "text/html", "rel": "describedby"}]' + +EXTENSION = { + 'name': extension_name, + 'namespace': extension_namespace, + 'description': extension_description, + 'updated': extension_updated, + 'alias': extension_alias, + 'links': extension_links, +} + class FakeIdentityv2Client(object): def __init__(self, **kwargs): @@ -116,6 +136,8 @@ def __init__(self, **kwargs): self.ec2.resource_class = fakes.FakeResource(None, {}) self.endpoints = mock.Mock() self.endpoints.resource_class = fakes.FakeResource(None, {}) + self.extensions = mock.Mock() + self.extensions.resource_class = fakes.FakeResource(None, {}) self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] diff --git a/openstackclient/tests/volume/v1/fakes.py b/openstackclient/tests/volume/v1/fakes.py index 3567eca507..c0ffbd3409 100644 --- a/openstackclient/tests/volume/v1/fakes.py +++ b/openstackclient/tests/volume/v1/fakes.py @@ -45,6 +45,26 @@ 'metadata': volume_metadata, } +extension_name = 'SchedulerHints' +extension_namespace = 'http://docs.openstack.org/'\ + 'block-service/ext/scheduler-hints/api/v2' +extension_description = 'Pass arbitrary key/value'\ + 'pairs to the scheduler.' +extension_updated = '2014-02-07T12:00:0-00:00' +extension_alias = 'OS-SCH-HNT' +extension_links = '[{"href":'\ + '"https://github.com/openstack/block-api", "type":'\ + ' "text/html", "rel": "describedby"}]' + +EXTENSION = { + 'name': extension_name, + 'namespace': extension_namespace, + 'description': extension_description, + 'updated': extension_updated, + 'alias': extension_alias, + 'links': extension_links, +} + class FakeVolumev1Client(object): def __init__(self, **kwargs): @@ -52,6 +72,8 @@ def __init__(self, **kwargs): self.volumes.resource_class = fakes.FakeResource(None, {}) self.services = mock.Mock() self.services.resource_class = fakes.FakeResource(None, {}) + self.extensions = mock.Mock() + self.extensions.resource_class = fakes.FakeResource(None, {}) self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] diff --git a/setup.cfg b/setup.cfg index 5a4fa743fe..b27a9cb4ed 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,6 +38,7 @@ openstack.common = limits_show = openstackclient.common.limits:ShowLimits quota_set = openstackclient.common.quota:SetQuota quota_show = openstackclient.common.quota:ShowQuota + extension_list = openstackclient.common.extension:ListExtension openstack.compute.v2 = compute_agent_create = openstackclient.compute.v2.agent:CreateAgent From f78a3f1653c634e40071fb96750b9ca64865c058 Mon Sep 17 00:00:00 2001 From: Qiu Yu Date: Wed, 29 Jan 2014 22:54:01 +0800 Subject: [PATCH 0104/3494] Refactor role list subcommand for identity v3 api Currently parts of user list and group list command are actually functioning as role listing, which is quite counter intuitive and misleading. This refactor change move role related logic to a single place of role list command. It now allows role grants listing for user/group + domain/project combinations. If no user or group specified, it will list all roles in the system, which is the default behaviour. Change-Id: I4ced6df4b76f018d01000d28b4281ad9f252ffcc --- openstackclient/identity/v3/group.py | 80 +------- openstackclient/identity/v3/role.py | 106 +++++++++- openstackclient/identity/v3/user.py | 87 +------- .../tests/identity/v3/test_role.py | 187 ++++++++++++++++++ .../tests/identity/v3/test_user.py | 175 ---------------- 5 files changed, 302 insertions(+), 333 deletions(-) diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index f51129c917..c5a4401722 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -176,23 +176,6 @@ def get_parser(self, prog_name): nargs='?', help='Name or ID of group to list [required with --role]', ) - parser.add_argument( - '--role', - action='store_true', - default=False, - help='List the roles assigned to ', - ) - domain_or_project = parser.add_mutually_exclusive_group() - domain_or_project.add_argument( - '--domain', - metavar='', - help='Filter list by [Only valid with --role]', - ) - domain_or_project.add_argument( - '--project', - metavar='', - help='Filter list by [Only valid with --role]', - ) parser.add_argument( '--long', action='store_true', @@ -205,65 +188,12 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - if parsed_args.role: - # List roles belonging to group - - # Group is required here, bail if it is not supplied - if not parsed_args.group: - sys.stderr.write('Error: Group must be specified') - # TODO(dtroyer): This lists the commands...I want it to - # show the help for _this_ command. - self.app.DeferredHelpAction( - self.app.parser, - self.app.parser, - None, - None, - ) - return ([], []) - - group = utils.find_resource( - identity_client.groups, - parsed_args.group, - ) - - if parsed_args.domain: - columns = ('ID', 'Name', 'Domain', 'Group') - domain = utils.find_resource( - identity_client.domains, - parsed_args.domain, - ) - data = identity_client.roles.list( - group=group, - domain=domain, - ) - for group_role in data: - group_role.group = group.name - group_role.domain = domain.name - elif parsed_args.project: - columns = ('ID', 'Name', 'Project', 'Group') - project = utils.find_resource( - identity_client.projects, - parsed_args.project, - ) - data = identity_client.roles.list( - group=group, - project=project, - ) - for group_role in data: - group_role.group = group.name - group_role.project = project.name - else: - # TODO(dtroyer): raise exception here, this really is an error - sys.stderr.write("Error: Must specify --domain or --project " - "with --role\n") - return ([], []) + # List groups + if parsed_args.long: + columns = ('ID', 'Name', 'Domain ID', 'Description') else: - # List groups - if parsed_args.long: - columns = ('ID', 'Name', 'Domain ID', 'Description') - else: - columns = ('ID', 'Name') - data = identity_client.groups.list() + columns = ('ID', 'Name') + data = identity_client.groups.list() return (columns, (utils.get_item_properties( diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index 69c0aa6ae4..2aabc00cc2 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -192,10 +192,110 @@ class ListRole(lister.Lister): log = logging.getLogger(__name__ + '.ListRole') + def get_parser(self, prog_name): + parser = super(ListRole, self).get_parser(prog_name) + domain_or_project = parser.add_mutually_exclusive_group() + domain_or_project.add_argument( + '--domain', + metavar='', + help='Filter role list by ', + ) + domain_or_project.add_argument( + '--project', + metavar='', + help='Filter role list by ', + ) + user_or_group = parser.add_mutually_exclusive_group() + user_or_group.add_argument( + '--user', + metavar='', + help='Name or ID of user to list roles asssigned to', + ) + user_or_group.add_argument( + '--group', + metavar='', + help='Name or ID of group to list roles asssigned to', + ) + return parser + def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) - columns = ('ID', 'Name') - data = self.app.client_manager.identity.roles.list() + self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity + + if parsed_args.user: + user = utils.find_resource( + identity_client.users, + parsed_args.user, + ) + elif parsed_args.group: + group = utils.find_resource( + identity_client.groups, + parsed_args.group, + ) + + if parsed_args.domain: + domain = utils.find_resource( + identity_client.domains, + parsed_args.domain, + ) + elif parsed_args.project: + project = utils.find_resource( + identity_client.projects, + parsed_args.project, + ) + + # no user or group specified, list all roles in the system + if not parsed_args.user and not parsed_args.group: + columns = ('ID', 'Name') + data = identity_client.roles.list() + elif parsed_args.user and parsed_args.domain: + columns = ('ID', 'Name', 'Domain', 'User') + data = identity_client.roles.list( + user=user, + domain=domain, + ) + for user_role in data: + user_role.user = user.name + user_role.domain = domain.name + elif parsed_args.user and parsed_args.project: + columns = ('ID', 'Name', 'Project', 'User') + data = identity_client.roles.list( + user=user, + project=project, + ) + for user_role in data: + user_role.user = user.name + user_role.project = project.name + elif parsed_args.user: + columns = ('ID', 'Name') + data = identity_client.roles.list( + user=user, + domain='default', + ) + elif parsed_args.group and parsed_args.domain: + columns = ('ID', 'Name', 'Domain', 'Group') + data = identity_client.roles.list( + group=group, + domain=domain, + ) + for group_role in data: + group_role.group = group.name + group_role.domain = domain.name + elif parsed_args.group and parsed_args.project: + columns = ('ID', 'Name', 'Project', 'Group') + data = identity_client.roles.list( + group=group, + project=project, + ) + for group_role in data: + group_role.group = group.name + group_role.project = project.name + else: + sys.stderr.write("Error: If a user or group is specified, either " + "--domain or --project must also be specified to " + "list role grants.\n") + return ([], []) + return (columns, (utils.get_item_properties( s, columns, diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index e628e884f3..c4adb225ca 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -17,7 +17,6 @@ import logging import six -import sys from cliff import command from cliff import lister @@ -161,23 +160,6 @@ def get_parser(self, prog_name): nargs='?', help='Name or ID of user to list [required with --role]', ) - parser.add_argument( - '--role', - action='store_true', - default=False, - help='List the roles assigned to ', - ) - domain_or_project = parser.add_mutually_exclusive_group() - domain_or_project.add_argument( - '--domain', - metavar='', - help='Filter list by [Only valid with --role]', - ) - domain_or_project.add_argument( - '--project', - metavar='', - help='Filter list by [Only valid with --role]', - ) parser.add_argument( '--long', action='store_true', @@ -187,70 +169,15 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) - identity_client = self.app.client_manager.identity + self.log.debug('take_action(%s)' % parsed_args) - if parsed_args.role: - # List roles belonging to user - - # User is required here, bail if it is not supplied - if not parsed_args.user: - sys.stderr.write('Error: User must be specified') - return ([], []) - - user = utils.find_resource( - identity_client.users, - parsed_args.user, - ) - - # List a user's roles - if not parsed_args.domain and not parsed_args.project: - columns = ('ID', 'Name') - data = identity_client.roles.list( - user=user, - domain='default', - ) - # List a user's roles on a domain - elif parsed_args.user and parsed_args.domain: - columns = ('ID', 'Name', 'Domain', 'User') - domain = utils.find_resource( - identity_client.domains, - parsed_args.domain, - ) - data = identity_client.roles.list( - user=user, - domain=domain, - ) - for user_role in data: - user_role.user = user.name - user_role.domain = domain.name - # List a user's roles on a project - elif parsed_args.user and parsed_args.project: - columns = ('ID', 'Name', 'Project', 'User') - project = utils.find_resource( - identity_client.projects, - parsed_args.project, - ) - data = identity_client.roles.list( - user=user, - project=project, - ) - for user_role in data: - user_role.user = user.name - user_role.project = project.name - else: - # TODO(dtroyer): raise exception here, this really is an error - sys.stderr.write("Error: Must specify --domain or --project " - "with --role\n") - return ([], []) + # List users + if parsed_args.long: + columns = ('ID', 'Name', 'Project Id', 'Domain Id', + 'Description', 'Email', 'Enabled') else: - # List users - if parsed_args.long: - columns = ('ID', 'Name', 'Project Id', 'Domain Id', - 'Description', 'Email', 'Enabled') - else: - columns = ('ID', 'Name') - data = self.app.client_manager.identity.users.list() + columns = ('ID', 'Name') + data = self.app.client_manager.identity.users.list() return (columns, (utils.get_item_properties( diff --git a/openstackclient/tests/identity/v3/test_role.py b/openstackclient/tests/identity/v3/test_role.py index 0c0551e11f..fa02ecb9ca 100644 --- a/openstackclient/tests/identity/v3/test_role.py +++ b/openstackclient/tests/identity/v3/test_role.py @@ -296,6 +296,27 @@ def setUp(self): ), ] + self.domains_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.DOMAIN), + loaded=True, + ) + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + self.users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + self.groups_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.GROUP), + loaded=True, + ) + # Get the command object to test self.cmd = role.ListRole(self.app, None) @@ -317,6 +338,172 @@ def test_role_list_no_options(self): ), ) self.assertEqual(tuple(data), datalist) + def test_user_list_user(self): + arglist = [ + '--user', identity_fakes.user_id, + ] + verifylist = [ + ('user', identity_fakes.user_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'domain': 'default', + 'user': self.users_mock.get(), + } + # RoleManager.list(user=, group=, domain=, project=, **kwargs) + self.roles_mock.list.assert_called_with( + **kwargs + ) + + collist = ('ID', 'Name') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.role_id, + identity_fakes.role_name, + ), ) + self.assertEqual(tuple(data), datalist) + + def test_role_list_domain_user(self): + arglist = [ + '--domain', identity_fakes.domain_name, + '--user', identity_fakes.user_id, + ] + verifylist = [ + ('domain', identity_fakes.domain_name), + ('user', identity_fakes.user_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'domain': self.domains_mock.get(), + 'user': self.users_mock.get(), + } + # RoleManager.list(user=, group=, domain=, project=, **kwargs) + self.roles_mock.list.assert_called_with( + **kwargs + ) + + collist = ('ID', 'Name', 'Domain', 'User') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.role_id, + identity_fakes.role_name, + identity_fakes.domain_name, + identity_fakes.user_name, + ), ) + self.assertEqual(tuple(data), datalist) + + def test_role_list_domain_group(self): + arglist = [ + '--domain', identity_fakes.domain_name, + '--group', identity_fakes.group_id, + ] + verifylist = [ + ('domain', identity_fakes.domain_name), + ('group', identity_fakes.group_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'domain': self.domains_mock.get(), + 'group': self.groups_mock.get(), + } + # RoleManager.list(user=, group=, domain=, project=, **kwargs) + self.roles_mock.list.assert_called_with( + **kwargs + ) + + collist = ('ID', 'Name', 'Domain', 'Group') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.role_id, + identity_fakes.role_name, + identity_fakes.domain_name, + identity_fakes.group_name, + ), ) + self.assertEqual(tuple(data), datalist) + + def test_role_list_project_user(self): + arglist = [ + '--project', identity_fakes.project_name, + '--user', identity_fakes.user_id, + ] + verifylist = [ + ('project', identity_fakes.project_name), + ('user', identity_fakes.user_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'project': self.projects_mock.get(), + 'user': self.users_mock.get(), + } + # RoleManager.list(user=, group=, domain=, project=, **kwargs) + self.roles_mock.list.assert_called_with( + **kwargs + ) + + collist = ('ID', 'Name', 'Project', 'User') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.role_id, + identity_fakes.role_name, + identity_fakes.project_name, + identity_fakes.user_name, + ), ) + self.assertEqual(tuple(data), datalist) + + def test_role_list_project_group(self): + arglist = [ + '--project', identity_fakes.project_name, + '--group', identity_fakes.group_id, + ] + verifylist = [ + ('project', identity_fakes.project_name), + ('group', identity_fakes.group_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'project': self.projects_mock.get(), + 'group': self.groups_mock.get(), + } + # RoleManager.list(user=, group=, domain=, project=, **kwargs) + self.roles_mock.list.assert_called_with( + **kwargs + ) + + collist = ('ID', 'Name', 'Project', 'Group') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.role_id, + identity_fakes.role_name, + identity_fakes.project_name, + identity_fakes.group_name, + ), ) + self.assertEqual(tuple(data), datalist) + class TestRoleRemove(TestRole): diff --git a/openstackclient/tests/identity/v3/test_user.py b/openstackclient/tests/identity/v3/test_user.py index 093d919b1b..b9c060d767 100644 --- a/openstackclient/tests/identity/v3/test_user.py +++ b/openstackclient/tests/identity/v3/test_user.py @@ -476,33 +476,6 @@ class TestUserList(TestUser): def setUp(self): super(TestUserList, self).setUp() - self.domains_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.DOMAIN), - loaded=True, - ) - - self.projects_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.PROJECT), - loaded=True, - ) - self.projects_mock.list.return_value = [ - fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.PROJECT), - loaded=True, - ), - ] - - self.roles_mock.list.return_value = [ - fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.ROLE), - loaded=True, - ), - ] - self.users_mock.get.return_value = fakes.FakeResource( None, copy.deepcopy(identity_fakes.USER), @@ -537,154 +510,6 @@ def test_user_list_no_options(self): ), ) self.assertEqual(tuple(data), datalist) - def test_user_list_project(self): - arglist = [ - '--project', identity_fakes.project_id, - ] - verifylist = [ - ('project', identity_fakes.project_id), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # DisplayCommandBase.take_action() returns two tuples - columns, data = self.cmd.take_action(parsed_args) - - self.users_mock.list.assert_called_with() - - collist = ('ID', 'Name') - self.assertEqual(columns, collist) - datalist = (( - identity_fakes.user_id, - identity_fakes.user_name, - ), ) - self.assertEqual(tuple(data), datalist) - - def test_user_list_domain(self): - arglist = [ - '--domain', identity_fakes.domain_id, - ] - verifylist = [ - ('domain', identity_fakes.domain_id), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # DisplayCommandBase.take_action() returns two tuples - columns, data = self.cmd.take_action(parsed_args) - - self.users_mock.list.assert_called_with() - - collist = ('ID', 'Name') - self.assertEqual(columns, collist) - datalist = (( - identity_fakes.user_id, - identity_fakes.user_name, - ), ) - self.assertEqual(tuple(data), datalist) - - def test_user_list_role_user(self): - arglist = [ - '--role', - identity_fakes.user_id, - ] - verifylist = [ - ('role', True), - ('user', identity_fakes.user_id), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # DisplayCommandBase.take_action() returns two tuples - columns, data = self.cmd.take_action(parsed_args) - - # Set expected values - kwargs = { - 'domain': 'default', - 'user': self.users_mock.get(), - } - # RoleManager.list(user=, group=, domain=, project=, **kwargs) - self.roles_mock.list.assert_called_with( - **kwargs - ) - - collist = ('ID', 'Name') - self.assertEqual(columns, collist) - datalist = (( - identity_fakes.role_id, - identity_fakes.role_name, - ), ) - self.assertEqual(tuple(data), datalist) - - def test_user_list_role_domain(self): - arglist = [ - '--domain', identity_fakes.domain_name, - '--role', - identity_fakes.user_id, - ] - verifylist = [ - ('domain', identity_fakes.domain_name), - ('role', True), - ('user', identity_fakes.user_id), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # DisplayCommandBase.take_action() returns two tuples - columns, data = self.cmd.take_action(parsed_args) - - # Set expected values - kwargs = { - 'domain': self.domains_mock.get(), - 'user': self.users_mock.get(), - } - # RoleManager.list(user=, group=, domain=, project=, **kwargs) - self.roles_mock.list.assert_called_with( - **kwargs - ) - - collist = ('ID', 'Name', 'Domain', 'User') - self.assertEqual(columns, collist) - datalist = (( - identity_fakes.role_id, - identity_fakes.role_name, - identity_fakes.domain_name, - identity_fakes.user_name, - ), ) - self.assertEqual(tuple(data), datalist) - - def test_user_list_role_project(self): - arglist = [ - '--project', identity_fakes.project_name, - '--role', - identity_fakes.user_id, - ] - verifylist = [ - ('project', identity_fakes.project_name), - ('role', True), - ('user', identity_fakes.user_id), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # DisplayCommandBase.take_action() returns two tuples - columns, data = self.cmd.take_action(parsed_args) - - # Set expected values - kwargs = { - 'project': self.projects_mock.get(), - 'user': self.users_mock.get(), - } - # RoleManager.list(user=, group=, domain=, project=, **kwargs) - self.roles_mock.list.assert_called_with( - **kwargs - ) - - collist = ('ID', 'Name', 'Project', 'User') - self.assertEqual(columns, collist) - datalist = (( - identity_fakes.role_id, - identity_fakes.role_name, - identity_fakes.project_name, - identity_fakes.user_name, - ), ) - self.assertEqual(tuple(data), datalist) - def test_user_list_long(self): arglist = [ '--long', From 0059f045a9958b1748fcec51799d8cffc0831440 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 13 Jun 2014 16:56:32 -0500 Subject: [PATCH 0105/3494] Ignore most of the new hacking 0.9.2 rules So we can update requriements.txt. But fix a couple of easy ones: * Fix E251 (1 occurrance) * Fix E131 (1 occurrance) Change-Id: I62aaa423aa6da9e9f0ca026ec586b51cc6a6df03 --- openstackclient/compute/v2/usage.py | 2 +- openstackclient/tests/common/test_utils.py | 5 +++-- tox.ini | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/openstackclient/compute/v2/usage.py b/openstackclient/compute/v2/usage.py index f7e11bcaca..ed98af2672 100644 --- a/openstackclient/compute/v2/usage.py +++ b/openstackclient/compute/v2/usage.py @@ -36,7 +36,7 @@ def get_parser(self, prog_name): metavar="", default=None, help="Usage range start date, ex 2012-01-20" - " (default: 4 weeks ago)." + " (default: 4 weeks ago)." ) parser.add_argument( "--end", diff --git a/openstackclient/tests/common/test_utils.py b/openstackclient/tests/common/test_utils.py index 3650746bc0..fbc1a926eb 100644 --- a/openstackclient/tests/common/test_utils.py +++ b/openstackclient/tests/common/test_utils.py @@ -107,8 +107,9 @@ def test_find_resource_find(self): def test_find_resource_find_not_found(self): self.manager.get = mock.Mock(side_effect=Exception('Boom!')) - self.manager.find = mock.Mock(side_effect= - exceptions.NotFound(404, "2")) + self.manager.find = mock.Mock( + side_effect=exceptions.NotFound(404, "2") + ) result = self.assertRaises(exceptions.CommandError, utils.find_resource, self.manager, diff --git a/tox.ini b/tox.ini index 751cbd6820..cb95893591 100644 --- a/tox.ini +++ b/tox.ini @@ -24,6 +24,6 @@ commands = python setup.py test --coverage --testr-args='{posargs}' downloadcache = ~/cache/pip [flake8] -ignore = E126,E202,W602,H302,H402 +ignore = E126,E202,W602,H302,H402,E265,H305,H307,H405,H904 show-source = True exclude = .venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,tools From 67354f651b064c7c67ad749bf75196d59b851d18 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 7 May 2014 10:54:38 -0500 Subject: [PATCH 0106/3494] Clean up logging levels The following logging levels are set according to the combination of --verbose, --quiet and --debug options: verbose_level logging level options 0 --quiet ERROR 1 (none) WARNING 2 --verbose INFO 3+ --verbose --verbose DEBUG or --debug Logging levels for the requests and iso8601 modules are forced to ERROR. This is the first step in bp use-logging-not-print The difference between '--debug' and '--verbose --verbose' is --debug triggers cliff's exception handling and traceback display. Change-Id: Ide2233b3316471d279260fb1e7255a6ca2072023 --- openstackclient/common/commandmanager.py | 1 - openstackclient/shell.py | 53 ++++++++++++++++++++---- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/openstackclient/common/commandmanager.py b/openstackclient/common/commandmanager.py index 204b943bcf..aa238a23f0 100644 --- a/openstackclient/common/commandmanager.py +++ b/openstackclient/common/commandmanager.py @@ -37,7 +37,6 @@ def _load_commands(self, group=None): group = self.namespace self.group_list.append(group) for ep in pkg_resources.iter_entry_points(group): - LOG.debug('found command %r', ep.name) cmd_name = ( ep.name.replace('_', ' ') if self.convert_underscores diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 719ee4803b..67eaca55e6 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -123,6 +123,50 @@ def __init__(self): help="Show this help message and exit", ) + def configure_logging(self): + """Configure logging for the app + + Cliff sets some defaults we don't want so re-work it a bit + """ + + if self.options.debug: + # --debug forces verbose_level 3 + # Set this here so cliff.app.configure_logging() can work + self.options.verbose_level = 3 + + super(OpenStackShell, self).configure_logging() + root_logger = logging.getLogger('') + + # Requests logs some stuff at INFO that we don't want + # unless we have DEBUG + requests_log = logging.getLogger("requests") + requests_log.setLevel(logging.ERROR) + + # Other modules we don't want DEBUG output for so + # don't reset them below + iso8601_log = logging.getLogger("iso8601") + iso8601_log.setLevel(logging.ERROR) + + # Set logging to the requested level + self.dump_stack_trace = False + if self.options.verbose_level == 0: + # --quiet + root_logger.setLevel(logging.ERROR) + elif self.options.verbose_level == 1: + # This is the default case, no --debug, --verbose or --quiet + root_logger.setLevel(logging.WARNING) + elif self.options.verbose_level == 2: + # One --verbose + root_logger.setLevel(logging.INFO) + elif self.options.verbose_level >= 3: + # Two or more --verbose + root_logger.setLevel(logging.DEBUG) + requests_log.setLevel(logging.DEBUG) + + if self.options.debug: + # --debug forces traceback + self.dump_stack_trace = True + def run(self, argv): try: return super(OpenStackShell, self).run(argv) @@ -401,15 +445,6 @@ def initialize_app(self, argv): super(OpenStackShell, self).initialize_app(argv) - # Set requests logging to a useful level - requests_log = logging.getLogger("requests") - if self.options.debug: - requests_log.setLevel(logging.DEBUG) - self.dump_stack_trace = True - else: - requests_log.setLevel(logging.WARNING) - self.dump_stack_trace = False - # Save default domain self.default_domain = self.options.os_default_domain From 5805596013435835842f9b050485f554e8758a02 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 13 Jun 2014 22:57:28 +0000 Subject: [PATCH 0107/3494] Updated from global requirements Change-Id: I44f13a22528824a8b24ffb3b0e3075e870e5ee58 --- requirements.txt | 2 +- test-requirements.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 122d8896cd..422b4a015b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ cliff>=1.4.3 keyring>=2.1 pycrypto>=2.6 python-glanceclient>=0.9.0 -python-keystoneclient>=0.8.0 +python-keystoneclient>=0.9.0 python-novaclient>=2.17.0 python-cinderclient>=1.0.6 requests>=1.1 diff --git a/test-requirements.txt b/test-requirements.txt index 7e1fa06b51..1b872c3b30 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,10 +1,10 @@ -hacking>=0.8.0,<0.9 +hacking>=0.9.1,<0.10 coverage>=3.6 discover fixtures>=0.3.14 mock>=1.0 -sphinx>=1.1.2,<1.2 +sphinx>=1.1.2,!=1.2.0,<1.3 testrepository>=0.0.18 testtools>=0.9.34 WebOb>=1.2.3 From d5aaba9d8284ea1cafe137b367ef9c9297b31e75 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 9 Dec 2013 17:50:07 -0600 Subject: [PATCH 0108/3494] Refactor oauth1 code for updates The keystoneclient code for oauth1 support has changed. As such, we should remove the delete, list and authenticate functions, since they are not in keystoneclient. Also, we must now pass in the project id when creating a request token. Additionally we must now pass in roles when authorizing a request token. Added functional tests to ensure output and input args are the same. bp add-oauth-support Change-Id: I559c18a73ad95a0c8b7a6a95f463b78334186f61 --- openstackclient/identity/v3/consumer.py | 23 +- openstackclient/identity/v3/token.py | 156 +++----------- openstackclient/tests/identity/v3/fakes.py | 61 ++++++ .../tests/identity/v3/test_consumer.py | 200 ++++++++++++++++++ .../tests/identity/v3/test_oauth.py | 152 +++++++++++++ setup.cfg | 3 - 6 files changed, 451 insertions(+), 144 deletions(-) create mode 100644 openstackclient/tests/identity/v3/test_consumer.py create mode 100644 openstackclient/tests/identity/v3/test_oauth.py diff --git a/openstackclient/identity/v3/consumer.py b/openstackclient/identity/v3/consumer.py index 11814e5cd3..7f54603540 100644 --- a/openstackclient/identity/v3/consumer.py +++ b/openstackclient/identity/v3/consumer.py @@ -43,7 +43,7 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - consumer = identity_client.consumers.create_consumer( + consumer = identity_client.oauth1.consumers.create( parsed_args.description ) info = {} @@ -69,8 +69,8 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity consumer = utils.find_resource( - identity_client.consumers, parsed_args.consumer) - identity_client.consumers.delete_consumer(consumer.id) + identity_client.oauth1.consumers, parsed_args.consumer) + identity_client.oauth1.consumers.delete(consumer.id) return @@ -82,7 +82,7 @@ class ListConsumer(lister.Lister): def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) columns = ('ID', 'Description') - data = self.app.client_manager.identity.consumers.list_consumers() + data = self.app.client_manager.identity.oauth1.consumers.list() return (columns, (utils.get_item_properties( s, columns, @@ -113,7 +113,7 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity consumer = utils.find_resource( - identity_client.consumers, parsed_args.consumer) + identity_client.oauth1.consumers, parsed_args.consumer) kwargs = {} if parsed_args.description: kwargs['description'] = parsed_args.description @@ -122,14 +122,9 @@ def take_action(self, parsed_args): sys.stdout.write("Consumer not updated, no arguments present") return - consumer = identity_client.consumers.update_consumer( - consumer.id, - **kwargs - ) - - info = {} - info.update(consumer._info) - return zip(*sorted(six.iteritems(info))) + consumer = identity_client.oauth1.consumers.update( + consumer.id, **kwargs) + return class ShowConsumer(show.ShowOne): @@ -150,7 +145,7 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity consumer = utils.find_resource( - identity_client.consumers, parsed_args.consumer) + identity_client.oauth1.consumers, parsed_args.consumer) info = {} info.update(consumer._info) diff --git a/openstackclient/identity/v3/token.py b/openstackclient/identity/v3/token.py index a84d724a1c..b038e3fe42 100644 --- a/openstackclient/identity/v3/token.py +++ b/openstackclient/identity/v3/token.py @@ -18,55 +18,8 @@ import logging import six -from cliff import command -from cliff import lister from cliff import show -from openstackclient.common import utils - - -class AuthenticateAccessToken(show.ShowOne): - """Authenticate access token to receive keystone token""" - - api = 'identity' - log = logging.getLogger(__name__ + '.AuthenticateAccessToken') - - def get_parser(self, prog_name): - parser = super(AuthenticateAccessToken, self).get_parser(prog_name) - parser.add_argument( - '--consumer-key', - metavar='', - help='Consumer key', - required=True - ) - parser.add_argument( - '--consumer-secret', - metavar='', - help='Consumer secret', - required=True - ) - parser.add_argument( - '--access-key', - metavar='', - help='Access token key', - required=True - ) - parser.add_argument( - '--access-secret', - metavar='', - help='Access token secret', - required=True - ) - return parser - - def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) - token_client = self.app.client_manager.identity.tokens - keystone_token = token_client.authenticate_access_token( - parsed_args.consumer_key, parsed_args.consumer_secret, - parsed_args.access_key, parsed_args.access_secret) - return zip(*sorted(six.iteritems(keystone_token))) - class AuthorizeRequestToken(show.ShowOne): """Authorize request token command""" @@ -78,17 +31,28 @@ def get_parser(self, prog_name): parser.add_argument( '--request-key', metavar='', - help='Consumer key', + help='Request token key', + required=True + ) + parser.add_argument( + '--role-ids', + metavar='', + help='Requested role IDs', required=True ) return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) - token_client = self.app.client_manager.identity.tokens + self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity - verifier_pin = token_client.authorize_request_token( - parsed_args.request_key) + roles = [] + for r_id in parsed_args.role_ids.split(): + roles.append(r_id) + + verifier_pin = identity_client.oauth1.request_tokens.authorize( + parsed_args.request_key, + roles) info = {} info.update(verifier_pin._info) return zip(*sorted(six.iteritems(info))) @@ -134,13 +98,15 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) - token_client = self.app.client_manager.identity.tokens - access_token = token_client.create_access_token( + self.log.debug('take_action(%s)' % parsed_args) + token_client = self.app.client_manager.identity.oauth1.access_tokens + access_token = token_client.create( parsed_args.consumer_key, parsed_args.consumer_secret, parsed_args.request_key, parsed_args.request_secret, parsed_args.verifier) - return zip(*sorted(six.iteritems(access_token))) + info = {} + info.update(access_token._info) + return zip(*sorted(six.iteritems(info))) class CreateRequestToken(show.ShowOne): @@ -162,27 +128,24 @@ def get_parser(self, prog_name): help='Consumer secret', required=True ) - parser.add_argument( - '--role-ids', - metavar='', - help='Requested role IDs', - ) parser.add_argument( '--project-id', metavar='', help='Requested project ID', + required=True ) return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) - token_client = self.app.client_manager.identity.tokens - request_token = token_client.create_request_token( + self.log.debug('take_action(%s)' % parsed_args) + token_client = self.app.client_manager.identity.oauth1.request_tokens + request_token = token_client.create( parsed_args.consumer_key, parsed_args.consumer_secret, - parsed_args.role_ids, parsed_args.project_id) - return zip(*sorted(six.iteritems(request_token))) + info = {} + info.update(request_token._info) + return zip(*sorted(six.iteritems(info))) class CreateToken(show.ShowOne): @@ -201,64 +164,3 @@ def take_action(self, parsed_args): if 'tenant_id' in token: token['project_id'] = token.pop('tenant_id') return zip(*sorted(six.iteritems(token))) - - -class DeleteAccessToken(command.Command): - """Delete access token command""" - - log = logging.getLogger(__name__ + '.DeleteAccessToken') - - def get_parser(self, prog_name): - parser = super(DeleteAccessToken, self).get_parser(prog_name) - parser.add_argument( - 'user', - metavar='', - help='Name or ID of user', - ) - parser.add_argument( - 'access_key', - metavar='', - help='Access token to be deleted', - ) - return parser - - def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) - - identity_client = self.app.client_manager.identity - user = utils.find_resource( - identity_client.users, parsed_args.user).id - identity_client.tokens.delete_access_token(user, - parsed_args.access_key) - return - - -class ListAccessToken(lister.Lister): - """List access tokens command""" - - log = logging.getLogger(__name__ + '.ListAccessToken') - - def get_parser(self, prog_name): - parser = super(ListAccessToken, self).get_parser(prog_name) - parser.add_argument( - 'user', - metavar='', - help='Name or ID of user', - ) - return parser - - def take_action(self, parsed_args): - self.log.debug('take_action(%s)', parsed_args) - - identity_client = self.app.client_manager.identity - user = utils.find_resource( - identity_client.users, parsed_args.user).id - - columns = ('ID', 'Consumer ID', 'Expires At', - 'Project Id', 'Authorizing User Id') - data = identity_client.tokens.list_access_tokens(user) - return (columns, - (utils.get_item_properties( - s, columns, - formatters={}, - ) for s in data)) diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index 86ef2f0c75..711a423d25 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -139,6 +139,43 @@ 'role': {'id': role_id}, } +consumer_id = 'test consumer id' +consumer_description = 'someone we trust' +consumer_secret = 'test consumer secret' + +OAUTH_CONSUMER = { + 'id': consumer_id, + 'secret': consumer_secret, + 'description': consumer_description +} + +access_token_id = 'test access token id' +access_token_secret = 'test access token secret' +access_token_expires = '2014-05-18T03:13:18.152071Z' + +OAUTH_ACCESS_TOKEN = { + 'id': access_token_id, + 'expires': access_token_expires, + 'key': access_token_id, + 'secret': access_token_secret +} + +request_token_id = 'test request token id' +request_token_secret = 'test request token secret' +request_token_expires = '2014-05-17T11:10:51.511336Z' + +OAUTH_REQUEST_TOKEN = { + 'id': request_token_id, + 'expires': request_token_expires, + 'key': request_token_id, + 'secret': request_token_secret +} + +oauth_verifier_pin = '6d74XaDS' +OAUTH_VERIFIER = { + 'oauth_verifier': oauth_verifier_pin +} + class FakeIdentityv3Client(object): def __init__(self, **kwargs): @@ -146,6 +183,8 @@ def __init__(self, **kwargs): self.domains.resource_class = fakes.FakeResource(None, {}) self.groups = mock.Mock() self.groups.resource_class = fakes.FakeResource(None, {}) + self.oauth1 = mock.Mock() + self.oauth1.resource_class = fakes.FakeResource(None, {}) self.projects = mock.Mock() self.projects.resource_class = fakes.FakeResource(None, {}) self.roles = mock.Mock() @@ -169,6 +208,18 @@ def __init__(self, **kwargs): self.identity_providers.resource_class = fakes.FakeResource(None, {}) +class FakeOAuth1Client(FakeIdentityv3Client): + def __init__(self, **kwargs): + super(FakeOAuth1Client, self).__init__(**kwargs) + + self.access_tokens = mock.Mock() + self.access_tokens.resource_class = fakes.FakeResource(None, {}) + self.consumers = mock.Mock() + self.consumers.resource_class = fakes.FakeResource(None, {}) + self.request_tokens = mock.Mock() + self.request_tokens.resource_class = fakes.FakeResource(None, {}) + + class TestIdentityv3(utils.TestCommand): def setUp(self): super(TestIdentityv3, self).setUp() @@ -187,3 +238,13 @@ def setUp(self): endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN ) + + +class TestOAuth1(utils.TestCommand): + def setUp(self): + super(TestOAuth1, self).setUp() + + self.app.client_manager.identity = FakeOAuth1Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN + ) diff --git a/openstackclient/tests/identity/v3/test_consumer.py b/openstackclient/tests/identity/v3/test_consumer.py new file mode 100644 index 0000000000..a1095709d7 --- /dev/null +++ b/openstackclient/tests/identity/v3/test_consumer.py @@ -0,0 +1,200 @@ +# 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 copy + +from openstackclient.identity.v3 import consumer +from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes + + +class TestOAuth1(identity_fakes.TestOAuth1): + + def setUp(self): + super(TestOAuth1, self).setUp() + identity_client = self.app.client_manager.identity + self.consumers_mock = identity_client.oauth1.consumers + self.consumers_mock.reset_mock() + + +class TestConsumerCreate(TestOAuth1): + + def setUp(self): + super(TestConsumerCreate, self).setUp() + + self.consumers_mock.create.return_value = fakes.FakeResource( + None, copy.deepcopy(identity_fakes.OAUTH_CONSUMER), + loaded=True) + + self.cmd = consumer.CreateConsumer(self.app, None) + + def test_create_consumer(self): + arglist = [ + '--description', identity_fakes.consumer_description + ] + verifylist = [ + ('description', identity_fakes.consumer_description) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.consumers_mock.create.assert_called_with( + identity_fakes.consumer_description) + + collist = ('description', 'id', 'secret') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.consumer_description, + identity_fakes.consumer_id, + identity_fakes.consumer_secret + ) + self.assertEqual(data, datalist) + + +class TestConsumerDelete(TestOAuth1): + + def setUp(self): + super(TestConsumerDelete, self).setUp() + + # This is the return value for utils.find_resource() + self.consumers_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.OAUTH_CONSUMER), + loaded=True) + + self.consumers_mock.delete.return_value = None + self.cmd = consumer.DeleteConsumer(self.app, None) + + def test_delete_consumer(self): + arglist = [ + identity_fakes.consumer_id + ] + verifylist = [ + ('consumer', identity_fakes.consumer_id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(result, 0) + + self.consumers_mock.delete.assert_called_with( + identity_fakes.consumer_id, + ) + + +class TestConsumerList(TestOAuth1): + + def setUp(self): + super(TestConsumerList, self).setUp() + + self.consumers_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.OAUTH_CONSUMER), + loaded=True, + ) + self.consumers_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.OAUTH_CONSUMER), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = consumer.ListConsumer(self.app, None) + + def test_consumer_list(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + self.consumers_mock.list.assert_called_with() + + collist = ('ID', 'Description') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.consumer_id, + identity_fakes.consumer_description + ), ) + self.assertEqual(tuple(data), datalist) + + +class TestConsumerShow(TestOAuth1): + + def setUp(self): + super(TestConsumerShow, self).setUp() + + consumer_no_secret = copy.deepcopy(identity_fakes.OAUTH_CONSUMER) + del consumer_no_secret['secret'] + self.consumers_mock.get.return_value = fakes.FakeResource( + None, consumer_no_secret, loaded=True) + + # Get the command object to test + self.cmd = consumer.ShowConsumer(self.app, None) + + def test_consumer_show(self): + arglist = [ + identity_fakes.consumer_id + ] + verifylist = [ + ('consumer', identity_fakes.consumer_id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.consumers_mock.get.assert_called_with( + identity_fakes.consumer_id) + + collist = ('description', 'id' ) + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.consumer_description, + identity_fakes.consumer_id + ) + self.assertEqual(data, datalist) + + +class TestConsumerSet(TestOAuth1): + + def setUp(self): + super(TestConsumerSet, self).setUp() + + self.consumers_mock.get.return_value = fakes.FakeResource( + None, copy.deepcopy(identity_fakes.OAUTH_CONSUMER), loaded=True) + + consumer_updated = copy.deepcopy(identity_fakes.OAUTH_CONSUMER) + consumer_updated['description'] = "consumer new description" + self.consumers_mock.update.return_value = fakes.FakeResource( + None, consumer_updated, loaded=True) + + self.cmd = consumer.SetConsumer(self.app, None) + + def test_consumer_update(self): + new_description = "consumer new description" + + arglist = [ + '--description', new_description, + identity_fakes.consumer_id + ] + verifylist = [ + ('description', new_description), + ('consumer', identity_fakes.consumer_id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.run(parsed_args) + self.assertEqual(result, 0) + + kwargs = {'description': new_description} + self.consumers_mock.update.assert_called_with( + identity_fakes.consumer_id, **kwargs) diff --git a/openstackclient/tests/identity/v3/test_oauth.py b/openstackclient/tests/identity/v3/test_oauth.py new file mode 100644 index 0000000000..5a52864584 --- /dev/null +++ b/openstackclient/tests/identity/v3/test_oauth.py @@ -0,0 +1,152 @@ +# 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 copy + +from openstackclient.identity.v3 import token +from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes + + +class TestOAuth1(identity_fakes.TestOAuth1): + + def setUp(self): + super(TestOAuth1, self).setUp() + identity_client = self.app.client_manager.identity + self.access_tokens_mock = identity_client.oauth1.access_tokens + self.access_tokens_mock.reset_mock() + self.request_tokens_mock = identity_client.oauth1.request_tokens + self.request_tokens_mock.reset_mock() + + +class TestRequestTokenCreate(TestOAuth1): + + def setUp(self): + super(TestRequestTokenCreate, self).setUp() + + self.request_tokens_mock.create.return_value = fakes.FakeResource( + None, copy.deepcopy(identity_fakes.OAUTH_REQUEST_TOKEN), + loaded=True) + + self.cmd = token.CreateRequestToken(self.app, None) + + def test_create_request_tokens(self): + arglist = [ + '--consumer-key', identity_fakes.consumer_id, + '--consumer-secret', identity_fakes.consumer_secret, + '--project-id', identity_fakes.project_id + ] + verifylist = [ + ('consumer_key', identity_fakes.consumer_id), + ('consumer_secret', identity_fakes.consumer_secret), + ('project_id', identity_fakes.project_id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.request_tokens_mock.create.assert_called_with( + identity_fakes.consumer_id, + identity_fakes.consumer_secret, + identity_fakes.project_id) + + collist = ('expires', 'id', 'key', 'secret') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.request_token_expires, + identity_fakes.request_token_id, + identity_fakes.request_token_id, + identity_fakes.request_token_secret + ) + self.assertEqual(data, datalist) + + +class TestRequestTokenAuthorize(TestOAuth1): + + def setUp(self): + super(TestRequestTokenAuthorize, self).setUp() + + self.request_tokens_mock.authorize.return_value = \ + fakes.FakeResource( + None, copy.deepcopy(identity_fakes.OAUTH_VERIFIER), + loaded=True) + + self.cmd = token.AuthorizeRequestToken(self.app, None) + + def test_authorize_request_tokens(self): + arglist = [ + '--request-key', identity_fakes.request_token_id, + '--role-ids', identity_fakes.role_id + ] + verifylist = [ + ('request_key', identity_fakes.request_token_id), + ('role_ids', identity_fakes.role_id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.request_tokens_mock.authorize.assert_called_with( + identity_fakes.request_token_id, + [identity_fakes.role_id]) + + collist = ('oauth_verifier',) + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.oauth_verifier_pin, + ) + self.assertEqual(data, datalist) + + +class TestAccessTokenCreate(TestOAuth1): + + def setUp(self): + super(TestAccessTokenCreate, self).setUp() + + self.access_tokens_mock.create.return_value = fakes.FakeResource( + None, copy.deepcopy(identity_fakes.OAUTH_ACCESS_TOKEN), + loaded=True) + + self.cmd = token.CreateAccessToken(self.app, None) + + def test_create_access_tokens(self): + arglist = [ + '--consumer-key', identity_fakes.consumer_id, + '--consumer-secret', identity_fakes.consumer_secret, + '--request-key', identity_fakes.request_token_id, + '--request-secret', identity_fakes.request_token_secret, + '--verifier', identity_fakes.oauth_verifier_pin + ] + verifylist = [ + ('consumer_key', identity_fakes.consumer_id), + ('consumer_secret', identity_fakes.consumer_secret), + ('request_key', identity_fakes.request_token_id), + ('request_secret', identity_fakes.request_token_secret), + ('verifier', identity_fakes.oauth_verifier_pin) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.access_tokens_mock.create.assert_called_with( + identity_fakes.consumer_id, + identity_fakes.consumer_secret, + identity_fakes.request_token_id, + identity_fakes.request_token_secret, + identity_fakes.oauth_verifier_pin) + + collist = ('expires', 'id', 'key', 'secret') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.access_token_expires, + identity_fakes.access_token_id, + identity_fakes.access_token_id, + identity_fakes.access_token_secret + ) + self.assertEqual(data, datalist) diff --git a/setup.cfg b/setup.cfg index 406c940e94..150c40f716 100644 --- a/setup.cfg +++ b/setup.cfg @@ -163,10 +163,7 @@ openstack.identity.v2_0 = user_show = openstackclient.identity.v2_0.user:ShowUser openstack.identity.v3 = - access_token_authenticate = openstackclient.identity.v3.token:AuthenticateAccessToken access_token_create = openstackclient.identity.v3.token:CreateAccessToken - access_token_delete = openstackclient.identity.v3.token:DeleteAccessToken - access_token_list = openstackclient.identity.v3.token:ListAccessToken consumer_create = openstackclient.identity.v3.consumer:CreateConsumer consumer_delete = openstackclient.identity.v3.consumer:DeleteConsumer From 6380b8b95918a42cee63c21b90f7d8d55854d0c8 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 8 May 2014 11:09:22 -0500 Subject: [PATCH 0109/3494] Image create and set command updates and tests Refactor image create and set commands to properly handle properties. This is particularly tricky with exclusive booleans as in this case leaving both choices off the command line should NOT assume a default value but leave the existing value unchanged. Properties were not being updated correctly in the 'image set' command. Refactor it to use the same pattern as in other SetXxx commands. Add tests for arg handling. Change-Id: I123a64c9b4feecab25a3e2013cc047f55b1c9967 --- openstackclient/image/v1/image.py | 322 ++++++++------- openstackclient/tests/image/v1/fakes.py | 28 +- openstackclient/tests/image/v1/test_image.py | 401 ++++++++++++++++--- 3 files changed, 569 insertions(+), 182 deletions(-) diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index a78f7baa79..92d0995344 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -35,6 +35,10 @@ from openstackclient.common import utils +DEFAULT_CONTAINER_FORMAT = 'bare' +DEFAULT_DISK_FORMAT = 'raw' + + class CreateImage(show.ShowOne): """Create/upload an image""" @@ -45,176 +49,208 @@ def get_parser(self, prog_name): parser.add_argument( "name", metavar="", - help="Name of image", - ) - parser.add_argument( - "--disk-format", - default="raw", - metavar="", - help="Disk format of image", + help="New image name", ) parser.add_argument( "--id", metavar="", - help="ID of image to reserve", + help="Image ID to reserve", ) parser.add_argument( "--store", metavar="", - help="Store to upload image to", + help="Upload image to this store", ) parser.add_argument( "--container-format", - default="bare", + default=DEFAULT_CONTAINER_FORMAT, metavar="", - help="Container format of image", + help="Image container format " + "(default: %s)" % DEFAULT_CONTAINER_FORMAT, + ) + parser.add_argument( + "--disk-format", + default=DEFAULT_DISK_FORMAT, + metavar="", + help="Image disk format " + "(default: %s)" % DEFAULT_DISK_FORMAT, ) parser.add_argument( "--owner", metavar="", - help="Image owner (project name or ID)", + help="Image owner project name or ID", ) parser.add_argument( "--size", metavar="", - help="Size of image in bytes. Only used with --location and" - " --copy-from", + help="Image size, in bytes (only used with --location and" + " --copy-from)", ) parser.add_argument( "--min-disk", metavar="", - help="Minimum size of disk needed to boot image in gigabytes", + type=int, + help="Minimum disk size needed to boot image, in gigabytes", ) parser.add_argument( "--min-ram", - metavar="", - help="Minimum amount of ram needed to boot image in megabytes", + metavar="", + type=int, + help="Minimum RAM size needed to boot image, in megabytes", ) parser.add_argument( "--location", metavar="", - help="URL where the data for this image already resides", - ) - parser.add_argument( - "--file", - metavar="", - help="Local file that contains disk image", - ) - parser.add_argument( - "--checksum", - metavar="", - help="Hash of image data used for verification", + help="Download image from an existing URL", ) parser.add_argument( "--copy-from", metavar="", - help="Similar to --location, but this indicates that the image" - " should immediately be copied from the data store", + help="Copy image from the data store (similar to --location)", + ) + parser.add_argument( + "--file", + metavar="", + help="Upload image from local file", ) parser.add_argument( "--volume", metavar="", - help="Create the image from the specified volume", + help="Create image from a volume", ) parser.add_argument( "--force", dest='force', action='store_true', default=False, - help="If the image is created from a volume, force creation of the" - " image even if volume is in use.", + help="Force image creation if volume is in use " + "(only meaningful with --volume)", ) parser.add_argument( - "--property", - dest="properties", - metavar="", - action=parseractions.KeyValueAction, - help="Set property on this image " - '(repeat option to set multiple properties)', + "--checksum", + metavar="", + help="Image hash used for verification", ) protected_group = parser.add_mutually_exclusive_group() protected_group.add_argument( "--protected", - dest="protected", action="store_true", - help="Prevent image from being deleted (default: False)", + help="Prevent image from being deleted", ) protected_group.add_argument( "--unprotected", - dest="protected", - action="store_false", - default=False, - help="Allow images to be deleted (default: True)", + action="store_true", + help="Allow image to be deleted (default)", ) public_group = parser.add_mutually_exclusive_group() public_group.add_argument( "--public", - dest="is_public", action="store_true", - default=True, - help="Image is accessible to the public (default)", + help="Image is accessible to the public", ) public_group.add_argument( "--private", - dest="is_public", - action="store_false", - help="Image is inaccessible to the public", + action="store_true", + help="Image is inaccessible to the public (default)", + ) + parser.add_argument( + "--property", + dest="properties", + metavar="", + action=parseractions.KeyValueAction, + help="Set an image property " + "(repeat option to set multiple properties)", ) return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) + image_client = self.app.client_manager.image - # NOTE(jk0): Since create() takes kwargs, it's easiest to just make a - # copy of parsed_args and remove what we don't need. - args = vars(parsed_args) - args = dict(filter(lambda x: x[1] is not None, args.items())) - args.pop("columns") - args.pop("formatter") - args.pop("prefix") - args.pop("variables") - - if "location" not in args and "copy_from" not in args: - if "volume" in args: - pass - elif "file" in args: - args["data"] = open(args.pop("file"), "rb") + # Build an attribute dict from the parsed args, only include + # attributes that were actually set on the command line + kwargs = {} + copy_attrs = ('name', 'id', 'store', 'container_format', + 'disk_format', 'owner', 'size', 'min_disk', 'min_ram', + 'localtion', 'copy_from', 'volume', 'force', + 'checksum', 'properties') + for attr in copy_attrs: + if attr in parsed_args: + val = getattr(parsed_args, attr, None) + if val: + # Only include a value in kwargs for attributes that are + # actually present on the command line + kwargs[attr] = val + # Handle exclusive booleans with care + # Avoid including attributes in kwargs if an option is not + # present on the command line. These exclusive booleans are not + # a single value for the pair of options because the default must be + # to do nothing when no options are present as opposed to always + # setting a default. + if parsed_args.protected: + kwargs['protected'] = True + if parsed_args.unprotected: + kwargs['protected'] = False + if parsed_args.public: + kwargs['is_public'] = True + if parsed_args.private: + kwargs['is_public'] = False + + if not parsed_args.location and not parsed_args.copy_from: + if parsed_args.volume: + volume_client = self.app.client_manager.volume + source_volume = utils.find_resource( + volume_client.volumes, + parsed_args.volume, + ) + response, body = volume_client.volumes.upload_to_image( + source_volume.id, + parsed_args.force, + parsed_args.name, + parsed_args.container_format, + parsed_args.disk_format, + ) + info = body['os-volume_upload_image'] + elif parsed_args.file: + # Send an open file handle to glanceclient so it will + # do a chunked transfer + kwargs["data"] = open(parsed_args.file, "rb") else: - args["data"] = None + # Read file from stdin + kwargs["data"] = None if sys.stdin.isatty() is not True: if msvcrt: msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) - args["data"] = sys.stdin - - if "volume" in args: - volume_client = self.app.client_manager.volume - source_volume = utils.find_resource(volume_client.volumes, - parsed_args.volume) - response, body = volume_client.volumes.upload_to_image( - source_volume, - parsed_args.force, - parsed_args.name, - parsed_args.container_format, - parsed_args.disk_format) - info = body['os-volume_upload_image'] - else: - image_client = self.app.client_manager.image - try: - image = utils.find_resource( - image_client.images, - parsed_args.name, - ) - except exceptions.CommandError: + # Send an open file handle to glanceclient so it will + # do a chunked transfer + kwargs["data"] = sys.stdin + + try: + image = utils.find_resource( + image_client.images, + parsed_args.name, + ) + + # Preserve previous properties if any are being set now + if image.properties: + if parsed_args.properties: + image.properties.update(kwargs['properties']) + kwargs['properties'] = image.properties + + except exceptions.CommandError: + if not parsed_args.volume: # This is normal for a create or reserve (create w/o an image) - image = image_client.images.create(**args) - else: - # It must be an update - # If an image is specified via --file, --location or - # --copy-from let the API handle it - image = image_client.images.update(image, **args) + # But skip for create from volume + image = image_client.images.create(**kwargs) + else: + # Update an existing reservation - info = {} - info.update(image._info) + # If an image is specified via --file, --location or + # --copy-from let the API handle it + image = image_client.images.update(image.id, **kwargs) + + info = {} + info.update(image._info) return zip(*sorted(six.iteritems(info))) @@ -314,88 +350,104 @@ def get_parser(self, prog_name): parser.add_argument( "image", metavar="", - help="Name or ID of image to change", + help="Image name or ID to change", ) parser.add_argument( "--name", metavar="", - help="Name of image", + help="New image name", ) parser.add_argument( "--owner", metavar="", - help="Image owner (project name or ID)", + help="New image owner project name or ID", ) parser.add_argument( "--min-disk", metavar="", - help="Minimum size of disk needed to boot image in gigabytes", + type=int, + help="Minimum disk size needed to boot image, in gigabytes", ) parser.add_argument( "--min-ram", metavar="", - help="Minimum amount of ram needed to boot image in megabytes", - ) - parser.add_argument( - "--property", - dest="properties", - metavar="", - default={}, - action=parseractions.KeyValueAction, - help="Set property on this image " - '(repeat option to set multiple properties)', + type=int, + help="Minimum RAM size needed to boot image, in megabytes", ) protected_group = parser.add_mutually_exclusive_group() protected_group.add_argument( "--protected", - dest="protected", action="store_true", - help="Prevent image from being deleted (default: False)", + help="Prevent image from being deleted", ) protected_group.add_argument( "--unprotected", - dest="protected", - action="store_false", - default=False, - help="Allow images to be deleted (default: True)", + action="store_true", + help="Allow image to be deleted (default)", ) public_group = parser.add_mutually_exclusive_group() public_group.add_argument( "--public", - dest="is_public", action="store_true", - default=True, - help="Image is accessible to the public (default)", + help="Image is accessible to the public", ) public_group.add_argument( "--private", - dest="is_public", - action="store_false", - help="Image is inaccessible to the public", + action="store_true", + help="Image is inaccessible to the public (default)", + ) + parser.add_argument( + "--property", + dest="properties", + metavar="", + action=parseractions.KeyValueAction, + help="Set an image property " + "(repeat option to set multiple properties)", ) return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) + image_client = self.app.client_manager.image - # NOTE(jk0): Since create() takes kwargs, it's easiest to just make a - # copy of parsed_args and remove what we don't need. - args = vars(parsed_args) - args = dict(filter(lambda x: x[1] is not None, args.items())) - args.pop("columns") - args.pop("formatter") - args.pop("prefix") - args.pop("variables") - image_arg = args.pop("image") + kwargs = {} + copy_attrs = ('name', 'owner', 'min_disk', 'min_ram', 'properties') + for attr in copy_attrs: + if attr in parsed_args: + val = getattr(parsed_args, attr, None) + if val: + # Only include a value in kwargs for attributes that are + # actually present on the command line + kwargs[attr] = val + # Handle exclusive booleans with care + # Avoid including attributes in kwargs if an option is not + # present on the command line. These exclusive booleans are not + # a single value for the pair of options because the default must be + # to do nothing when no options are present as opposed to always + # setting a default. + if parsed_args.protected: + kwargs['protected'] = True + if parsed_args.unprotected: + kwargs['protected'] = False + if parsed_args.public: + kwargs['is_public'] = True + if parsed_args.private: + kwargs['is_public'] = False + + if not kwargs: + self.log.warning('no arguments specified') + return {}, {} - image_client = self.app.client_manager.image image = utils.find_resource( image_client.images, - image_arg, + parsed_args.image, ) - # Merge properties - args["properties"].update(image.properties) - image = image_client.images.update(image, **args) + + if image.properties and parsed_args.properties: + image.properties.update(kwargs['properties']) + kwargs['properties'] = image.properties + + image = image_client.images.update(image.id, **kwargs) info = {} info.update(image._info) diff --git a/openstackclient/tests/image/v1/fakes.py b/openstackclient/tests/image/v1/fakes.py index ea2af84ccd..972e641589 100644 --- a/openstackclient/tests/image/v1/fakes.py +++ b/openstackclient/tests/image/v1/fakes.py @@ -17,16 +17,38 @@ from openstackclient.tests import fakes from openstackclient.tests import utils +from openstackclient.tests.volume.v1 import fakes as volume_fakes image_id = 'im1' image_name = 'graven' +image_owner = 'baal' +image_protected = False +image_public = True +image_properties = { + 'Alpha': 'a', + 'Beta': 'b', + 'Gamma': 'g', +} +image_properties_str = "{'Alpha': 'a', 'Beta': 'b', 'Gamma': 'g'}" +image_data = 'line 1\nline 2\n' IMAGE = { 'id': image_id, - 'name': image_name + 'name': image_name, + 'container_format': '', + 'disk_format': '', + 'owner': image_owner, + 'min_disk': 0, + 'min_ram': 0, + 'is_public': image_public, + 'protected': image_protected, + 'properties': image_properties, } +IMAGE_columns = tuple(sorted(IMAGE)) +IMAGE_data = tuple((IMAGE[x] for x in sorted(IMAGE))) + class FakeImagev1Client(object): def __init__(self, **kwargs): @@ -44,3 +66,7 @@ def setUp(self): endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN, ) + self.app.client_manager.volume = volume_fakes.FakeVolumev1Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) diff --git a/openstackclient/tests/image/v1/test_image.py b/openstackclient/tests/image/v1/test_image.py index d7547f7630..b746a5382a 100644 --- a/openstackclient/tests/image/v1/test_image.py +++ b/openstackclient/tests/image/v1/test_image.py @@ -16,6 +16,7 @@ import copy import mock +from openstackclient.common import exceptions from openstackclient.image.v1 import image from openstackclient.tests import fakes from openstackclient.tests.image.v1 import fakes as image_fakes @@ -35,75 +36,228 @@ class TestImageCreate(TestImage): def setUp(self): super(TestImageCreate, self).setUp() + + self.images_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(image_fakes.IMAGE), + loaded=True, + ) + # This is the return value for utils.find_resource() self.images_mock.get.return_value = fakes.FakeResource( None, copy.deepcopy(image_fakes.IMAGE), loaded=True, ) + self.images_mock.update.return_value = fakes.FakeResource( + None, + copy.deepcopy(image_fakes.IMAGE), + loaded=True, + ) + + # Get the command object to test self.cmd = image.CreateImage(self.app, None) - def test_create_volume(self): + def test_image_reserve_no_options(self): + mock_exception = { + 'find.side_effect': exceptions.CommandError('x'), + 'get.side_effect': exceptions.CommandError('x'), + } + self.images_mock.configure_mock(**mock_exception) arglist = [ - '--volume', 'volly', image_fakes.image_name, ] verifylist = [ - ('volume', 'volly'), + ('container_format', image.DEFAULT_CONTAINER_FORMAT), + ('disk_format', image.DEFAULT_DISK_FORMAT), ('name', image_fakes.image_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.app.client_manager.volume = mock.Mock() - self.app.client_manager.volume.volumes = mock.Mock() - volumes = self.app.client_manager.volume.volumes - volumes.upload_to_image = mock.Mock() - response = {"id": 'volume_id', - "updated_at": 'updated_at', - "status": 'uploading', - "display_description": 'desc', - "size": 'size', - "volume_type": 'volume_type', - "image_id": 'image1', - "container_format": parsed_args.container_format, - "disk_format": parsed_args.disk_format, - "image_name": parsed_args.name} - full_response = {"os-volume_upload_image": response} - volumes.upload_to_image.return_value = (201, full_response) - volume_resource = fakes.FakeResource( + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # ImageManager.create(name=, **) + self.images_mock.create.assert_called_with( + name=image_fakes.image_name, + container_format=image.DEFAULT_CONTAINER_FORMAT, + disk_format=image.DEFAULT_DISK_FORMAT, + data=mock.ANY, + ) + + # Verify update() was not called, if it was show the args + self.assertEqual(self.images_mock.update.call_args_list, []) + + self.assertEqual(image_fakes.IMAGE_columns, columns) + self.assertEqual(image_fakes.IMAGE_data, data) + + def test_image_reserve_options(self): + mock_exception = { + 'find.side_effect': exceptions.CommandError('x'), + 'get.side_effect': exceptions.CommandError('x'), + } + self.images_mock.configure_mock(**mock_exception) + arglist = [ + '--container-format', 'ovf', + '--disk-format', 'fs', + '--min-disk', '10', + '--min-ram', '4', + '--protected', + '--private', + image_fakes.image_name, + ] + verifylist = [ + ('container_format', 'ovf'), + ('disk_format', 'fs'), + ('min_disk', 10), + ('min_ram', 4), + ('protected', True), + ('unprotected', False), + ('public', False), + ('private', True), + ('name', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # ImageManager.create(name=, **) + self.images_mock.create.assert_called_with( + name=image_fakes.image_name, + container_format='ovf', + disk_format='fs', + min_disk=10, + min_ram=4, + protected=True, + is_public=False, + data=mock.ANY, + ) + + # Verify update() was not called, if it was show the args + self.assertEqual(self.images_mock.update.call_args_list, []) + + self.assertEqual(image_fakes.IMAGE_columns, columns) + self.assertEqual(image_fakes.IMAGE_data, data) + + @mock.patch('__builtin__.open') + def test_image_create_file(self, open_mock): + mock_exception = { + 'find.side_effect': exceptions.CommandError('x'), + 'get.side_effect': exceptions.CommandError('x'), + } + self.images_mock.configure_mock(**mock_exception) + open_mock.return_value = image_fakes.image_data + arglist = [ + '--file', 'filer', + '--unprotected', + '--public', + '--property', 'Alpha=1', + '--property', 'Beta=2', + image_fakes.image_name, + ] + verifylist = [ + ('file', 'filer'), + ('protected', False), + ('unprotected', True), + ('public', True), + ('private', False), + ('properties', {'Alpha': '1', 'Beta': '2'}), + ('name', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + open_mock.assert_called_with('filer', 'rb') + + # ImageManager.get(name) + self.images_mock.get.assert_called_with(image_fakes.image_name) + + # ImageManager.create(name=, **) + self.images_mock.create.assert_called_with( + name=image_fakes.image_name, + container_format=image.DEFAULT_CONTAINER_FORMAT, + disk_format=image.DEFAULT_DISK_FORMAT, + protected=False, + is_public=True, + properties={ + 'Alpha': '1', + 'Beta': '2', + }, + data=image_fakes.image_data, + ) + + # Verify update() was not called, if it was show the args + self.assertEqual(self.images_mock.update.call_args_list, []) + + self.assertEqual(image_fakes.IMAGE_columns, columns) + self.assertEqual(image_fakes.IMAGE_data, data) + + def test_image_create_volume(self): + # Set up VolumeManager Mock + volumes_mock = self.app.client_manager.volume.volumes + volumes_mock.reset_mock() + volumes_mock.get.return_value = fakes.FakeResource( None, copy.deepcopy({'id': 'vol1', 'name': 'volly'}), loaded=True, ) - volumes.get.return_value = volume_resource - results = self.cmd.take_action(parsed_args) - volumes.upload_to_image.assert_called_with( - volume_resource, + response = { + "id": 'volume_id', + "updated_at": 'updated_at', + "status": 'uploading', + "display_description": 'desc', + "size": 'size', + "volume_type": 'volume_type', + "image_id": 'image1', + "container_format": image.DEFAULT_CONTAINER_FORMAT, + "disk_format": image.DEFAULT_DISK_FORMAT, + "image_name": image_fakes.image_name, + } + full_response = {"os-volume_upload_image": response} + volumes_mock.upload_to_image.return_value = (201, full_response) + + arglist = [ + '--volume', 'volly', + image_fakes.image_name, + ] + verifylist = [ + ('private', False), + ('protected', False), + ('public', False), + ('unprotected', False), + ('volume', 'volly'), + ('force', False), + ('name', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # VolumeManager.upload_to_image(volume, force, image_name, + # container_format, disk_format) + volumes_mock.upload_to_image.assert_called_with( + 'vol1', False, image_fakes.image_name, 'bare', 'raw', ) - expects = [('container_format', - 'disk_format', - 'display_description', - 'id', - 'image_id', - 'image_name', - 'size', - 'status', - 'updated_at', - 'volume_type'), - ('bare', - 'raw', - 'desc', - 'volume_id', - 'image1', - 'graven', - 'size', - 'uploading', - 'updated_at', - 'volume_type')] - for expected, result in zip(expects, results): - self.assertEqual(expected, result) + + # ImageManager.update(image_id, remove_props=, **) + self.images_mock.update.assert_called_with( + image_fakes.image_id, + name=image_fakes.image_name, + container_format=image.DEFAULT_CONTAINER_FORMAT, + disk_format=image.DEFAULT_DISK_FORMAT, + properties=image_fakes.image_properties, + volume='volly', + ) + + self.assertEqual(image_fakes.IMAGE_columns, columns) + self.assertEqual(image_fakes.IMAGE_data, data) class TestImageDelete(TestImage): @@ -137,3 +291,158 @@ def test_image_delete_no_options(self): self.images_mock.delete.assert_called_with( image_fakes.image_id, ) + + +class TestImageSet(TestImage): + + def setUp(self): + super(TestImageSet, self).setUp() + + # This is the return value for utils.find_resource() + self.images_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(image_fakes.IMAGE), + loaded=True, + ) + self.images_mock.update.return_value = fakes.FakeResource( + None, + copy.deepcopy(image_fakes.IMAGE), + loaded=True, + ) + + # Get the command object to test + self.cmd = image.SetImage(self.app, None) + + def test_image_set_no_options(self): + arglist = [ + image_fakes.image_name, + ] + verifylist = [ + ('image', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + # Verify update() was not called, if it was show the args + self.assertEqual(self.images_mock.update.call_args_list, []) + + def test_image_set_options(self): + arglist = [ + '--name', 'new-name', + '--owner', 'new-owner', + '--min-disk', '2', + '--min-ram', '4', + image_fakes.image_name, + ] + verifylist = [ + ('name', 'new-name'), + ('owner', 'new-owner'), + ('min_disk', 2), + ('min_ram', 4), + ('image', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + kwargs = { + 'name': 'new-name', + 'owner': 'new-owner', + 'min_disk': 2, + 'min_ram': 4, + } + # ImageManager.update(image, **kwargs) + self.images_mock.update.assert_called_with( + image_fakes.image_id, + **kwargs + ) + + self.assertEqual(image_fakes.IMAGE_columns, columns) + self.assertEqual(image_fakes.IMAGE_data, data) + + def test_image_set_bools1(self): + arglist = [ + '--protected', + '--private', + image_fakes.image_name, + ] + verifylist = [ + ('protected', True), + ('unprotected', False), + ('public', False), + ('private', True), + ('image', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + kwargs = { + 'protected': True, + 'is_public': False, + } + # ImageManager.update(image, **kwargs) + self.images_mock.update.assert_called_with( + image_fakes.image_id, + **kwargs + ) + + def test_image_set_bools2(self): + arglist = [ + '--unprotected', + '--public', + image_fakes.image_name, + ] + verifylist = [ + ('protected', False), + ('unprotected', True), + ('public', True), + ('private', False), + ('image', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + kwargs = { + 'protected': False, + 'is_public': True, + } + # ImageManager.update(image, **kwargs) + self.images_mock.update.assert_called_with( + image_fakes.image_id, + **kwargs + ) + + def test_image_set_properties(self): + arglist = [ + '--property', 'Alpha=1', + '--property', 'Beta=2', + image_fakes.image_name, + ] + verifylist = [ + ('properties', {'Alpha': '1', 'Beta': '2'}), + ('image', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + kwargs = { + 'properties': { + 'Alpha': '1', + 'Beta': '2', + 'Gamma': 'g', + }, + } + # ImageManager.update(image, **kwargs) + self.images_mock.update.assert_called_with( + image_fakes.image_id, + **kwargs + ) From 0b2987fef389603b95b2ba7b788492b8baa56745 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Thu, 22 May 2014 17:38:41 -0600 Subject: [PATCH 0110/3494] Fix find_resource for keystone and cinder The find_resource method had two hacks in in to support cinder and keystone and I have removed those in favor of a monkey patch for cinder. The find_resource method used to attempt to UUID parse the id, but it would do a manager.get anyway. I changed it to skip the UUID parsing. This will make things run minorly faster and it supports LDAP for keystone. The find_resource used to attempt to use display_name=name_or_id when finding. This was a hack for cinder support, but it breaks keystone because keystone totally messes up with the bogus filter and keystone refuses to fix it. Change-Id: I66e45a6341f704900f1d5321a0e70eac3d051665 Closes-Bug: #1306699 --- openstackclient/common/utils.py | 13 ---- openstackclient/tests/common/test_utils.py | 4 +- .../tests/volume/test_find_resource.py | 71 +++++++++++++++++++ openstackclient/volume/client.py | 5 ++ 4 files changed, 78 insertions(+), 15 deletions(-) create mode 100644 openstackclient/tests/volume/test_find_resource.py diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py index bc9ed26412..a420dd513d 100644 --- a/openstackclient/common/utils.py +++ b/openstackclient/common/utils.py @@ -21,7 +21,6 @@ import six import sys import time -import uuid from openstackclient.common import exceptions from openstackclient.openstack.common import strutils @@ -37,13 +36,6 @@ def find_resource(manager, name_or_id): except exceptions.NotFound: pass - # Try to get entity as uuid - try: - uuid.UUID(str(name_or_id)) - return manager.get(name_or_id) - except (ValueError, exceptions.NotFound): - pass - # Try directly using the passed value try: return manager.get(name_or_id) @@ -65,11 +57,6 @@ def find_resource(manager, name_or_id): # Eventually this should be pulled from a common set # of client exceptions. except Exception as ex: - try: - return manager.find(display_name=name_or_id) - except Exception: - pass - if type(ex).__name__ == 'NotFound': msg = "No %s with a name or ID of '%s' exists." % \ (manager.resource_class.__name__.lower(), name_or_id) diff --git a/openstackclient/tests/common/test_utils.py b/openstackclient/tests/common/test_utils.py index fbc1a926eb..6d75a9b5c9 100644 --- a/openstackclient/tests/common/test_utils.py +++ b/openstackclient/tests/common/test_utils.py @@ -117,7 +117,7 @@ def test_find_resource_find_not_found(self): self.assertEqual("No lego with a name or ID of 'legos' exists.", str(result)) self.manager.get.assert_called_with(self.name) - self.manager.find.assert_called_with(display_name=self.name) + self.manager.find.assert_called_with(name=self.name) def test_find_resource_find_no_unique(self): self.manager.get = mock.Mock(side_effect=Exception('Boom!')) @@ -129,4 +129,4 @@ def test_find_resource_find_no_unique(self): self.assertEqual("More than one lego exists with the name 'legos'.", str(result)) self.manager.get.assert_called_with(self.name) - self.manager.find.assert_called_with(display_name=self.name) + self.manager.find.assert_called_with(name=self.name) diff --git a/openstackclient/tests/volume/test_find_resource.py b/openstackclient/tests/volume/test_find_resource.py new file mode 100644 index 0000000000..8539070f14 --- /dev/null +++ b/openstackclient/tests/volume/test_find_resource.py @@ -0,0 +1,71 @@ +# Copyright 2013 Nebula Inc. +# +# 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 mock + +from cinderclient.v1 import volume_snapshots +from cinderclient.v1 import volumes + +from openstackclient.common import exceptions +from openstackclient.common import utils +from openstackclient.tests import utils as test_utils + + +ID = '1after909' +NAME = 'PhilSpector' + + +class TestFindResourceVolumes(test_utils.TestCase): + + def setUp(self): + super(TestFindResourceVolumes, self).setUp() + api = mock.Mock() + api.client = mock.Mock() + api.client.get = mock.Mock() + resp = mock.Mock() + body = {"volumes": [{"id": ID, 'display_name': NAME}]} + api.client.get.side_effect = [Exception("Not found"), (resp, body)] + self.manager = volumes.VolumeManager(api) + + def test_find(self): + result = utils.find_resource(self.manager, NAME) + self.assertEqual(ID, result.id) + self.assertEqual(NAME, result.display_name) + + def test_not_find(self): + self.assertRaises(exceptions.CommandError, utils.find_resource, + self.manager, 'GeorgeMartin') + + +class TestFindResourceVolumeSnapshots(test_utils.TestCase): + + def setUp(self): + super(TestFindResourceVolumeSnapshots, self).setUp() + api = mock.Mock() + api.client = mock.Mock() + api.client.get = mock.Mock() + resp = mock.Mock() + body = {"snapshots": [{"id": ID, 'display_name': NAME}]} + api.client.get.side_effect = [Exception("Not found"), (resp, body)] + self.manager = volume_snapshots.SnapshotManager(api) + + def test_find(self): + result = utils.find_resource(self.manager, NAME) + self.assertEqual(ID, result.id) + self.assertEqual(NAME, result.display_name) + + def test_not_find(self): + self.assertRaises(exceptions.CommandError, utils.find_resource, + self.manager, 'GeorgeMartin') diff --git a/openstackclient/volume/client.py b/openstackclient/volume/client.py index 7cf828b440..9b37b8f53c 100644 --- a/openstackclient/volume/client.py +++ b/openstackclient/volume/client.py @@ -15,8 +15,13 @@ import logging +from cinderclient.v1 import volume_snapshots +from cinderclient.v1 import volumes from openstackclient.common import utils +# Monkey patch for v1 cinderclient +volumes.Volume.NAME_ATTR = 'display_name' +volume_snapshots.Snapshot.NAME_ATTR = 'display_name' LOG = logging.getLogger(__name__) From 89cbdd1ae1f0ed2e55ffdc43fbed07e417751455 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 17 Jun 2014 23:24:55 -0400 Subject: [PATCH 0111/3494] Add a docs job to tox.ini A noticed that there wasn't a docs option when running tox. Thought it would be a good idea to add one. Partial-Bug: #1331304 Change-Id: I5af9ff5d5986ad546146c0fa73eb98256fd00c5f --- tox.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tox.ini b/tox.ini index cb95893591..c646457ff7 100644 --- a/tox.ini +++ b/tox.ini @@ -23,6 +23,10 @@ commands = python setup.py test --coverage --testr-args='{posargs}' [tox:jenkins] downloadcache = ~/cache/pip +[testenv:docs] +commands= + python setup.py build_sphinx + [flake8] ignore = E126,E202,W602,H302,H402,E265,H305,H307,H405,H904 show-source = True From deaff7274e4d0782a7ddedb5c7d371fe5a9bb141 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 17 Jun 2014 23:42:25 -0400 Subject: [PATCH 0112/3494] Update docs template To make things more consistent across all openstack projects, the developer docs should be upgraded to the newer template used by keystone and keystoneclient (and other projects). I dropped in the necessary static files and themes, and updated the config file to make the changes at build time. Change-Id: I5a268cff3cd5af29ad712705d540b9d1d6667d56 Partial-Bug: #1331304 --- doc/source/_static/basic.css | 416 ++++++++++++++++++++++++++ doc/source/_static/default.css | 230 ++++++++++++++ doc/source/_static/header-line.gif | Bin 0 -> 48 bytes doc/source/_static/header_bg.jpg | Bin 0 -> 3738 bytes doc/source/_static/jquery.tweet.js | 154 ++++++++++ doc/source/_static/nature.css | 245 +++++++++++++++ doc/source/_static/openstack_logo.png | Bin 0 -> 3670 bytes doc/source/_static/tweaks.css | 94 ++++++ doc/source/_theme/layout.html | 83 +++++ doc/source/_theme/theme.conf | 4 + doc/source/conf.py | 3 +- 11 files changed, 1228 insertions(+), 1 deletion(-) create mode 100644 doc/source/_static/basic.css create mode 100644 doc/source/_static/default.css create mode 100644 doc/source/_static/header-line.gif create mode 100644 doc/source/_static/header_bg.jpg create mode 100644 doc/source/_static/jquery.tweet.js create mode 100644 doc/source/_static/nature.css create mode 100644 doc/source/_static/openstack_logo.png create mode 100644 doc/source/_static/tweaks.css create mode 100644 doc/source/_theme/layout.html create mode 100644 doc/source/_theme/theme.conf diff --git a/doc/source/_static/basic.css b/doc/source/_static/basic.css new file mode 100644 index 0000000000..d909ce37c7 --- /dev/null +++ b/doc/source/_static/basic.css @@ -0,0 +1,416 @@ +/** + * Sphinx stylesheet -- basic theme + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +img { + border: 0; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li div.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable dl, table.indextable dd { + margin-top: 0; + margin-bottom: 0; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +/* -- general body styles --------------------------------------------------- */ + +a.headerlink { + visibility: hidden; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.field-list ul { + padding-left: 1em; +} + +.first { +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px 7px 0 7px; + background-color: #ffe; + width: 40%; + float: right; +} + +p.sidebar-title { + font-weight: bold; +} + +/* -- topics ---------------------------------------------------------------- */ + +div.topic { + border: 1px solid #ccc; + padding: 7px 7px 0 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +div.admonition dl { + margin-bottom: 0; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + border: 0; + border-collapse: collapse; +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 0; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +table.field-list td, table.field-list th { + border: 0 !important; +} + +table.footnote td, table.footnote th { + border: 0 !important; +} + +th { + text-align: left; + padding-right: 5px; +} + +/* -- other body styles ----------------------------------------------------- */ + +dl { + margin-bottom: 15px; +} + +dd p { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +dt:target, .highlight { + background-color: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.refcount { + color: #060; +} + +.optional { + font-size: 1.3em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; +} + +td.linenos pre { + padding: 5px 0px; + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + margin-left: 0.5em; +} + +table.highlighttable td { + padding: 0 0.5em 0 0.5em; +} + +tt.descname { + background-color: transparent; + font-weight: bold; + font-size: 1.2em; +} + +tt.descclassname { + background-color: transparent; +} + +tt.xref, a tt { + background-color: transparent; + font-weight: bold; +} + +h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { + background-color: transparent; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} diff --git a/doc/source/_static/default.css b/doc/source/_static/default.css new file mode 100644 index 0000000000..c8091ecb4d --- /dev/null +++ b/doc/source/_static/default.css @@ -0,0 +1,230 @@ +/** + * Sphinx stylesheet -- default theme + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + */ + +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: sans-serif; + font-size: 100%; + background-color: #11303d; + color: #000; + margin: 0; + padding: 0; +} + +div.document { + background-color: #1c4e63; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 230px; +} + +div.body { + background-color: #ffffff; + color: #000000; + padding: 0 20px 30px 20px; +} + +div.footer { + color: #ffffff; + width: 100%; + padding: 9px 0 9px 0; + text-align: center; + font-size: 75%; +} + +div.footer a { + color: #ffffff; + text-decoration: underline; +} + +div.related { + background-color: #133f52; + line-height: 30px; + color: #ffffff; +} + +div.related a { + color: #ffffff; +} + +div.sphinxsidebar { +} + +div.sphinxsidebar h3 { + font-family: 'Trebuchet MS', sans-serif; + color: #ffffff; + font-size: 1.4em; + font-weight: normal; + margin: 0; + padding: 0; +} + +div.sphinxsidebar h3 a { + color: #ffffff; +} + +div.sphinxsidebar h4 { + font-family: 'Trebuchet MS', sans-serif; + color: #ffffff; + font-size: 1.3em; + font-weight: normal; + margin: 5px 0 0 0; + padding: 0; +} + +div.sphinxsidebar p { + color: #ffffff; +} + +div.sphinxsidebar p.topless { + margin: 5px 10px 10px 10px; +} + +div.sphinxsidebar ul { + margin: 10px; + padding: 0; + color: #ffffff; +} + +div.sphinxsidebar a { + color: #98dbcc; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +/* -- body styles ----------------------------------------------------------- */ + +a { + color: #355f7c; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +div.body p, div.body dd, div.body li { + text-align: left; + line-height: 130%; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: 'Trebuchet MS', sans-serif; + background-color: #f2f2f2; + font-weight: normal; + color: #20435c; + border-bottom: 1px solid #ccc; + margin: 20px -20px 10px -20px; + padding: 3px 0 3px 10px; +} + +div.body h1 { margin-top: 0; font-size: 200%; } +div.body h2 { font-size: 160%; } +div.body h3 { font-size: 140%; } +div.body h4 { font-size: 120%; } +div.body h5 { font-size: 110%; } +div.body h6 { font-size: 100%; } + +a.headerlink { + color: #c60f0f; + font-size: 0.8em; + padding: 0 4px 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + background-color: #c60f0f; + color: white; +} + +div.body p, div.body dd, div.body li { + text-align: left; + line-height: 130%; +} + +div.admonition p.admonition-title + p { + display: inline; +} + +div.admonition p { + margin-bottom: 5px; +} + +div.admonition pre { + margin-bottom: 5px; +} + +div.admonition ul, div.admonition ol { + margin-bottom: 5px; +} + +div.note { + background-color: #eee; + border: 1px solid #ccc; +} + +div.seealso { + background-color: #ffc; + border: 1px solid #ff6; +} + +div.topic { + background-color: #eee; +} + +div.warning { + background-color: #ffe4e4; + border: 1px solid #f66; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre { + padding: 5px; + background-color: #eeffcc; + color: #333333; + line-height: 120%; + border: 1px solid #ac9; + border-left: none; + border-right: none; +} + +tt { + background-color: #ecf0f3; + padding: 0 1px 0 1px; + font-size: 0.95em; +} + +.warning tt { + background: #efc2c2; +} + +.note tt { + background: #d6d6d6; +} diff --git a/doc/source/_static/header-line.gif b/doc/source/_static/header-line.gif new file mode 100644 index 0000000000000000000000000000000000000000..3601730e03488b7b5f92dc992d23ad753357c167 GIT binary patch literal 48 zcmZ?wbhEHbWMg1uXkcVG`smgF|Nj+#vM@3*Ff!;c00Bsbfr-7RpY8O^Kn4bD08FwB Aga7~l literal 0 HcmV?d00001 diff --git a/doc/source/_static/header_bg.jpg b/doc/source/_static/header_bg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f788c41c26481728fa4329c17c87bde36001adc1 GIT binary patch literal 3738 zcmd5-YdDna8vedHnM0NtYi6>>At7O=uyTsZup5R_40A9)aXQa}U(l^=gSg=J*&3mKp$aM0r>UIFDe9Zy(vs} zWf)kqO2Y_n0$>ZQ0D&hY4tWjpY?Ii5?V)h*kc0fz?%ZIj3|{;F8E5l%d0)&*Hx~ulvc_*73u8%R zsVMV~ne!JY);&pWott~QIZYJFTXliYc2};JEU{X7W6;ZPfz;)U;U4#mEuK@K*=SC3BR-m&x9(Nna@>b@%FS34|P^jtsXRb5>z9gtPp;_MI2F3o*k z>csA-?CX4b;~4P-*L$+Mmb|51F)eD*wCc`Jt(9}C${Zo=!Uin=u_yMC^;`X!x$##4 z+~}dkT`NF@Uhw0r+6g_)?e!h8IX+OE^C96>UOsv0GPMD6(kr#ljhXRnA=O>Qj@%iT zqBF7aQ*}BG)h@6r0%#azk!r9yrN6>9dq~>KadV$~cGG?Hjk>~it^5rd#zS4KE*p+4 z;;B)%oBK8PNTs=A)a-z`n?3zJ%+h{`=>ijk4sYKr*>`eN1H`~Lo|Tm!o6qN{S* zeNl=NcpGzD55)XnLC|>g)~w={=c#4*x^;mk4Zo_FOFlffP@!?1`c+TogTVR4kp9-q z`d5cMBzNxk6qjPRK9*WY3uHS=bnm_QJvSMBBS_A#3i=ywsg6^|9rfruW0MhdGwHDO z?1gJRMQVecKE^gV{%uo(b)zl^Hd&vmnwFh88h*-?FJ;y=Hdqvt!K|s<$>xlzR=G4{ zZgGOCF43IXS?62B)w*N&dXt%U8X^Bjx}^%Yf>VFpFoKSGP%k?ems;&&J)|Dx(qtQD zu2tS)<_Qz4#LhBKYkl@Og}G)^5+F4P($Fk>)}{uMVv|;Sz2i4$XJ_WTw*;n>3N805rnXhbC52SC={E3rXRlrs|I6f;o|Cn%eje59{axu9sivy4oYmg=j|fLt3<3 zFce84aNb8GbK;y>RbBu71YBcYKL3@M3N25yoE%BtG z^K!`WTQ|fb-Ysa7T)mEw&4_b)PWYgc!)3W)H+neR9o^f|AXdgY1`gN+pvgzbbk`M z*Ts6${7M`2)9XIPy^MoXTiiP2GTp_OtgWMshnH)M&ZSO0)cet!oWo_0_&hV(0?Qdb zdo(sw{I#{hI`SWPM`N=U^#+MgN-*rZ#J7Cm7Jj89`5ehd_{z&9->Jc7$F(X4)&|`K z5rEgd;@dhi-IzJnSVpMd!Gf_G-QW+ zjVMrIas1)g%)GJ;(=oaK};O^)NYdS1`XR?K_;I7qj zhii5}x^he{U3M+GF+WpYws#=Pt#S9xB_X5QE7W+_rQdwMhukJnQj}5cnCz_sIJ#r0 zJa5drkRPI$X(4YdpCswJe#5aN4Jjw3V3Nzt&`lcKBI~#;!>jq7j8y# zvHrFg_#P376A45^hp-KU*P=R;DVdPK*w7D@Gw+`XsSpm^L-VkCooZF61sPAnnjsT# zND4C{>G#P10F_&txEoE!rX%Iy*L}Kna=Q%fDLJ_rF*LujRITZ)$g!?UYLkCXOoz-S z_p`Hny*Rh--l)aYQC&-2dd%;%VKGC1<1DJm_n~`nk4^yS`}&P zM}5bOypW0hwtvrwnE>}g1Mq+B>09qPp1b$hn6kC_iqF`tX#G-t7D$n}Ky9t}sUqiI zOe@odQ?JueZ+sg`-zoQ}J4if6vv1c9x{BDme+F6z{8esU^Kio zK_oPy9}@nlGywSOZy9`^- zzBg>C9|rgWF{pcCogEV@;d}VHrgeBl=5Dr*th4V!1`Z9Zrz9le1zHC#sM3{j#G2R?WMhl6b_yyoEAxX>Zixl$16`+^d$ihNtuIBUafyiCEv#oksNL<4= z*oDXsc7-(ww^9-b-6_|bITySG1N2C-7p0L4+V@R%j=4@ygc=89bmSNy38$S=ZiDyP z0SrqrVA;zi8kYBZ2@Mx(2Lx~-*bc@d1#4R($RJv$9ZTfx_t7Kc|HIHnd&@I386P?& z?d6Vd(48n${cTNFFCoSIUj#O{mmt%M&xCIFmR9Y3f{2UnF4e9@uFZOaYiY|CLdbDa z%xS9x4SHi7Fr-1?CnDqRK?)n&$TTBW5J?O&o{TnNCnLw*{QmT7{c}flSbp9&xi*zF z1TdUn&_!$_WxQbMKGkgsl}B%+N5ZV%Hy6_zJ>dejD89yCBMw9(d}z2fWjYH_nV6!F zqe_rI2H5Pi0^~S6)jjnu%lqZN*eQq6!||a24+edpSH_{C8Ew^g8dw2qdrH!@*E7K* z)00Bb8uUsai%v6Oa^L@3E02r|EG%EdV>q;=#2Q9Wjv3l?dAur$4bzyOl3M6 z1hf%&o*#2R&xnS1z4&R`Uq%`Ut0_P{BOwt;FuDb$1")); + }); + return $(returning); + }, + linkUser: function() { + var returning = []; + var regexp = /[\@]+([A-Za-z0-9-_]+)/gi; + this.each(function() { + returning.push(this.replace(regexp,"@$1")); + }); + return $(returning); + }, + linkHash: function() { + var returning = []; + var regexp = / [\#]+([A-Za-z0-9-_]+)/gi; + this.each(function() { + returning.push(this.replace(regexp, ' #$1')); + }); + return $(returning); + }, + capAwesome: function() { + var returning = []; + this.each(function() { + returning.push(this.replace(/\b(awesome)\b/gi, '$1')); + }); + return $(returning); + }, + capEpic: function() { + var returning = []; + this.each(function() { + returning.push(this.replace(/\b(epic)\b/gi, '$1')); + }); + return $(returning); + }, + makeHeart: function() { + var returning = []; + this.each(function() { + returning.push(this.replace(/(<)+[3]/gi, "")); + }); + return $(returning); + } + }); + + function relative_time(time_value) { + var parsed_date = Date.parse(time_value); + var relative_to = (arguments.length > 1) ? arguments[1] : new Date(); + var delta = parseInt((relative_to.getTime() - parsed_date) / 1000); + var pluralize = function (singular, n) { + return '' + n + ' ' + singular + (n == 1 ? '' : 's'); + }; + if(delta < 60) { + return 'less than a minute ago'; + } else if(delta < (45*60)) { + return 'about ' + pluralize("minute", parseInt(delta / 60)) + ' ago'; + } else if(delta < (24*60*60)) { + return 'about ' + pluralize("hour", parseInt(delta / 3600)) + ' ago'; + } else { + return 'about ' + pluralize("day", parseInt(delta / 86400)) + ' ago'; + } + } + + function build_url() { + var proto = ('https:' == document.location.protocol ? 'https:' : 'http:'); + if (s.list) { + return proto+"//api.twitter.com/1/"+s.username[0]+"/lists/"+s.list+"/statuses.json?per_page="+s.count+"&callback=?"; + } else if (s.query == null && s.username.length == 1) { + return proto+'//twitter.com/status/user_timeline/'+s.username[0]+'.json?count='+s.count+'&callback=?'; + } else { + var query = (s.query || 'from:'+s.username.join('%20OR%20from:')); + return proto+'//search.twitter.com/search.json?&q='+query+'&rpp='+s.count+'&callback=?'; + } + } + + return this.each(function(){ + var list = $('
    ').appendTo(this); + var intro = '

    '+s.intro_text+'

    '; + var outro = '

    '+s.outro_text+'

    '; + var loading = $('

    '+s.loading_text+'

    '); + + if(typeof(s.username) == "string"){ + s.username = [s.username]; + } + + if (s.loading_text) $(this).append(loading); + $.getJSON(build_url(), function(data){ + if (s.loading_text) loading.remove(); + if (s.intro_text) list.before(intro); + $.each((data.results || data), function(i,item){ + // auto join text based on verb tense and content + if (s.join_text == "auto") { + if (item.text.match(/^(@([A-Za-z0-9-_]+)) .*/i)) { + var join_text = s.auto_join_text_reply; + } else if (item.text.match(/(^\w+:\/\/[A-Za-z0-9-_]+\.[A-Za-z0-9-_:%&\?\/.=]+) .*/i)) { + var join_text = s.auto_join_text_url; + } else if (item.text.match(/^((\w+ed)|just) .*/im)) { + var join_text = s.auto_join_text_ed; + } else if (item.text.match(/^(\w*ing) .*/i)) { + var join_text = s.auto_join_text_ing; + } else { + var join_text = s.auto_join_text_default; + } + } else { + var join_text = s.join_text; + }; + + var from_user = item.from_user || item.user.screen_name; + var profile_image_url = item.profile_image_url || item.user.profile_image_url; + var join_template = ' '+join_text+' '; + var join = ((s.join_text) ? join_template : ' '); + var avatar_template = ''+from_user+'\'s avatar'; + var avatar = (s.avatar_size ? avatar_template : ''); + var date = ''+relative_time(item.created_at)+''; + var text = '' +$([item.text]).linkUrl().linkUser().linkHash().makeHeart().capAwesome().capEpic()[0]+ ''; + + // until we create a template option, arrange the items below to alter a tweet's display. + list.append('
  • ' + avatar + date + join + text + '
  • '); + + list.children('li:first').addClass('tweet_first'); + list.children('li:odd').addClass('tweet_even'); + list.children('li:even').addClass('tweet_odd'); + }); + if (s.outro_text) list.after(outro); + }); + + }); + }; +})(jQuery); \ No newline at end of file diff --git a/doc/source/_static/nature.css b/doc/source/_static/nature.css new file mode 100644 index 0000000000..a98bd4209d --- /dev/null +++ b/doc/source/_static/nature.css @@ -0,0 +1,245 @@ +/* + * nature.css_t + * ~~~~~~~~~~~~ + * + * Sphinx stylesheet -- nature theme. + * + * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: Arial, sans-serif; + font-size: 100%; + background-color: #111; + color: #555; + margin: 0; + padding: 0; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 {{ theme_sidebarwidth|toint }}px; +} + +hr { + border: 1px solid #B1B4B6; +} + +div.document { + background-color: #eee; +} + +div.body { + background-color: #ffffff; + color: #3E4349; + padding: 0 30px 30px 30px; + font-size: 0.9em; +} + +div.footer { + color: #555; + width: 100%; + padding: 13px 0; + text-align: center; + font-size: 75%; +} + +div.footer a { + color: #444; + text-decoration: underline; +} + +div.related { + background-color: #6BA81E; + line-height: 32px; + color: #fff; + text-shadow: 0px 1px 0 #444; + font-size: 0.9em; +} + +div.related a { + color: #E2F3CC; +} + +div.sphinxsidebar { + font-size: 0.75em; + line-height: 1.5em; +} + +div.sphinxsidebarwrapper{ + padding: 20px 0; +} + +div.sphinxsidebar h3, +div.sphinxsidebar h4 { + font-family: Arial, sans-serif; + color: #222; + font-size: 1.2em; + font-weight: normal; + margin: 0; + padding: 5px 10px; + background-color: #ddd; + text-shadow: 1px 1px 0 white +} + +div.sphinxsidebar h4{ + font-size: 1.1em; +} + +div.sphinxsidebar h3 a { + color: #444; +} + + +div.sphinxsidebar p { + color: #888; + padding: 5px 20px; +} + +div.sphinxsidebar p.topless { +} + +div.sphinxsidebar ul { + margin: 10px 20px; + padding: 0; + color: #000; +} + +div.sphinxsidebar a { + color: #444; +} + +div.sphinxsidebar input { + border: 1px solid #ccc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar input[type=text]{ + margin-left: 20px; +} + +/* -- body styles ----------------------------------------------------------- */ + +a { + color: #005B81; + text-decoration: none; +} + +a:hover { + color: #E32E00; + text-decoration: underline; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: Arial, sans-serif; + background-color: #BED4EB; + font-weight: normal; + color: #212224; + margin: 30px 0px 10px 0px; + padding: 5px 0 5px 10px; + text-shadow: 0px 1px 0 white +} + +div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; } +div.body h2 { font-size: 150%; background-color: #C8D5E3; } +div.body h3 { font-size: 120%; background-color: #D8DEE3; } +div.body h4 { font-size: 110%; background-color: #D8DEE3; } +div.body h5 { font-size: 100%; background-color: #D8DEE3; } +div.body h6 { font-size: 100%; background-color: #D8DEE3; } + +a.headerlink { + color: #c60f0f; + font-size: 0.8em; + padding: 0 4px 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + background-color: #c60f0f; + color: white; +} + +div.body p, div.body dd, div.body li { + line-height: 1.5em; +} + +div.admonition p.admonition-title + p { + display: inline; +} + +div.highlight{ + background-color: white; +} + +div.note { + background-color: #eee; + border: 1px solid #ccc; +} + +div.seealso { + background-color: #ffc; + border: 1px solid #ff6; +} + +div.topic { + background-color: #eee; +} + +div.warning { + background-color: #ffe4e4; + border: 1px solid #f66; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre { + padding: 10px; + background-color: White; + color: #222; + line-height: 1.2em; + border: 1px solid #C6C9CB; + font-size: 1.1em; + margin: 1.5em 0 1.5em 0; + -webkit-box-shadow: 1px 1px 1px #d8d8d8; + -moz-box-shadow: 1px 1px 1px #d8d8d8; +} + +tt { + background-color: #ecf0f3; + color: #222; + /* padding: 1px 2px; */ + font-size: 1.1em; + font-family: monospace; +} + +.viewcode-back { + font-family: Arial, sans-serif; +} + +div.viewcode-block:target { + background-color: #f4debf; + border-top: 1px solid #ac9; + border-bottom: 1px solid #ac9; +} diff --git a/doc/source/_static/openstack_logo.png b/doc/source/_static/openstack_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..146faec5cfe3773824f4caf39e4480e4974d10df GIT binary patch literal 3670 zcmV-c4yo~pP)CW75Qp#l)U;+N6jaIz6Nf$t6dNV>^>ETzcpQ=%tMaf0k|rg72+IW`z$FyfE+D{1@tt$t5DmX)*;QV?c;%+5Z&egAgfXTQJq-mZkC z>pFAHu}U=Axde_?s!99ZfDg_+9TYzDa6N1R3adhx&2Mb7>9w`KpMNz!>U5t2XQ8lZ zu+!+H7(PRwF@jAkwvI;|8|=Z_dfzV`Kpi;I!e=|Ql+HAdEag?VZ^Ilw9XJj9N1#1a z?UFC!)X62`CRIe^9YCLKbJ` z&O@f0zt{Z1YDF1utg2$F+rzvrncys+g37Xsd8)idSW(=}t#~qF#qBo29*@^ZCs<$W zpa144=o4g0z63h_ttPfIpH-FyG^MAH+6B~r$(4qw+Uv{2d#h`$lq+i+#Tf%CAzDFUh!pzX(6nW{EASJAQkhm!+}aGpHc z;(+N`S*@tYmump1T37E}J;!$0#F>^M*mT_X1x~bvnp&qP9IHI#bj-0z8FR+=p+e#*w3ugV#wX``sR-CI1!YiQsfc@Om<;1MBw zlfqH9z4Q|m*C?URU1OG(`UYn>Q8<|I!mby#FlN5MMFE8;Pyh$skbR?ngFLt?%nWSkS-#W5umy>@^DyAERP~{E&`M%0(qi&((^ahqL}u^jT<2dcf)p< z%Fxc9J$nh_`>_oNYC?oy`rIDY46Yrw4si3Qn~oXV%dJ}IlUD-40>QipyGa_dV0Z%J ztcEXm5yxR0gySJ04{nnbm#vP=Hq&GI<8VxcZ34pRjt6m%pE2H|!+HBJQrdBdyKHJR z2O_}hp!5bXuwniQYTF>yI|=cjT+2l`9T3|H+l4%ryPxWQm(ODW#8Ctj_CplcO=)qj zD#d~V6BahR9NY1kE5rF)_j<|!Cqnpq0uOKhL%w z>y8OyeTM1?REXc{0|3b=#WPZneh80PxL=Ljau1~+CgtMgg-vccMDX-L z9^7An_;!lFAi`#G_1F*OdM|Z$EVQs0m0$?mY}(baOZ%Zpd62#Pyg!3Jd4d zD^8+lSir&T6Y9-p9L#Wz6$5nXLjdOl?7Lv!TeMr}F14ranauW9=L>ubu*x>Bcrgwp zjrT@{rL*2Fc}Ilwn07QvdJfMOO2=(1Px)6&ih7lg839!Bx&}lQER~T`^7_x@fXo({ zCZMeZYt*!VgMTg>PR)PBaIwubzRY%jjE`-s zG;B}>2!lD=QLOTfQOEZKIEz*;yTJ9(Af0zNv;IDq7#Fr#W{Ap+7Sq1N3TL21X|h2t z=Dk>^bGSsRX-u+cZ23mMB_Ioc0yNIfcfLWB>$hVU3W3>d&a?IM+bGRGt+t}aiv(eh z(D6Z9N>U2|Qxle(!UVTeEKE6W))3WI5z48Rs8d5v0GwmyC8iQiUJO8KS?QwHl2abL zNW+hadDdPc8z%MSOG$l&WR@!!&M{WLmrnS=-0G#&`a)chX>mN9W1>|yqve@lL8a`f zXRmn$B8P=dLxE!2rIi}a*gh%FI4j?C;b@L=WgypiTRf==n6DKr9mUExo6a@{wLM-I z9%V9{!;5G!<8fMYikfEbrGXRQN-9*24}kIIpP&dEg@fiLqAY5|jjv}$P3x0avZODU zdX`c|G>h`1f=3uEu)L9C)H5%frni#HZXcX`TD{iQ-e2qXxj_f%|WW;byDMc%7+uBy}Y?KLC?jp%yyyeBNkqQ-*osw2ex&97Q{#C7%CdSDMNIV zTdC(LEm?&qPcNOjM)h9Grs|M(gsuhV8@96?m4WkQ>j{bJIs)m^neL%ua!i+N8>Lh+ zKu#7rF~VOH@hb{zGXYwys!Um4Vkf+H8Hj6?^eI%kT%j+HA0K=6qdQ@nfR57Q`Jm9T zc)Yg9-`e~BRE!xoKZ z=mP|0Kihr}V1$5sHw$QekmoL)lQ;~@H$S)}s3xuwypiubB?1%OyBpwC08TH!=?BrQ zhOp`PTu;%u0}Q=XKGb7d$g8*;de8c1UI|Re2R;;Radh_D!FIZg+JP`oJg>5 z;&B7eVAomZe>j~hOOIVRO_Q7eSGz37hxmnsG!n%HX`C6gSqFcg(RLmikn%EPR*wel zrsc;>!vQ<>2ZW`lk`MbNLopFd#_9mh8iKPH;KbjC@xJU${pdxuTF{uO(eG#9t*>XP z_4Seh`r_#q$^xeiuy(=eSouv66cpS!t3n`|j`6xnmSs1q@;0!I)m<6eYHHGMRdB87 ziruozT=gn@yp`B9oGxD-b7PqhZum|oJCfLB38&8v51ijj-Pb`qvCr3FtJ0aFms2h3(n0-}3jJ~J$ zCzep7-MIZFbo$(m8zWm?SoRl__blLE+!fFBVVk1&XLg+vmVNcTk9O2+q?x#F0LZUN zu6oM~C)(7^0|az4nM}@aZf<@RkH0CR8<-Yn-fZe+Dbr#iJWSt#tnR4^h<@ePXWmeHIO4q^X zCbiy(=k3R1o1}0E+7x*OOe-qnIXG{#N_rqK*1NH}Qz6aumTR`YTgo5K=q=61;5@b- zrgUA_Qz=)(TPN!tCZE|{?B0*r9ov5Fcip6xQ2;Yqs*2_o7TFKGp0|~bcP@6+a(rz^ zXXmmyBfT}ucw_t(6s+f^t_)nc>RKW<-q_&J35vN+RPLsR?VAsQeHLyCR7AWvxFOVc zAg-xl=j*RipzaKWx3lAf?ei`PoM;bbAL>svH?JqQwjSulb9bghytRt%*5x-no>xlf zh7qj0LYRXVDU})?Btsy7^71*ujsEP_ACyd)P)*ULWBCXox@PUfwmQ#)Vl&oeIqpQY zHMgU+xe0EhQ)RmjdB3JHGdrsvJ9?A=WwOrn)J?BH{+D&O_@SKdrj2|8Z{hS1T(k>&Zlt;p=tqw*mVY1aLt=u^eAHkW>8cb#@q& z4-SLa@ii zCt7NGrLv)1Scy9ew-sOwwLYn2a6T#KzJgnbacm7Z20q6tcs~C!0DI+r(=$l+x{=W0A}~0&W)ll4*&oF07*qoM6N<$f~n6U7ytkO literal 0 HcmV?d00001 diff --git a/doc/source/_static/tweaks.css b/doc/source/_static/tweaks.css new file mode 100644 index 0000000000..3f3fb3f071 --- /dev/null +++ b/doc/source/_static/tweaks.css @@ -0,0 +1,94 @@ +body { + background: #fff url(../_static/header_bg.jpg) top left no-repeat; +} + +#header { + width: 950px; + margin: 0 auto; + height: 102px; +} + +#header h1#logo { + background: url(../_static/openstack_logo.png) top left no-repeat; + display: block; + float: left; + text-indent: -9999px; + width: 175px; + height: 55px; +} + +#navigation { + background: url(../_static/header-line.gif) repeat-x 0 bottom; + display: block; + float: left; + margin: 27px 0 0 25px; + padding: 0; +} + +#navigation li{ + float: left; + display: block; + margin-right: 25px; +} + +#navigation li a { + display: block; + font-weight: normal; + text-decoration: none; + background-position: 50% 0; + padding: 20px 0 5px; + color: #353535; + font-size: 14px; +} + +#navigation li a.current, #navigation li a.section { + border-bottom: 3px solid #cf2f19; + color: #cf2f19; +} + +div.related { + background-color: #cde2f8; + border: 1px solid #b0d3f8; +} + +div.related a { + color: #4078ba; + text-shadow: none; +} + +div.sphinxsidebarwrapper { + padding-top: 0; +} + +pre { + color: #555; +} + +div.documentwrapper h1, div.documentwrapper h2, div.documentwrapper h3, div.documentwrapper h4, div.documentwrapper h5, div.documentwrapper h6 { + font-family: 'PT Sans', sans-serif !important; + color: #264D69; + border-bottom: 1px dotted #C5E2EA; + padding: 0; + background: none; + padding-bottom: 5px; +} + +div.documentwrapper h3 { + color: #CF2F19; +} + +a.headerlink { + color: #fff !important; + margin-left: 5px; + background: #CF2F19 !important; +} + +div.body { + margin-top: -25px; + margin-left: 230px; +} + +div.document { + width: 960px; + margin: 0 auto; +} \ No newline at end of file diff --git a/doc/source/_theme/layout.html b/doc/source/_theme/layout.html new file mode 100644 index 0000000000..750b782211 --- /dev/null +++ b/doc/source/_theme/layout.html @@ -0,0 +1,83 @@ +{% extends "basic/layout.html" %} +{% set css_files = css_files + ['_static/tweaks.css'] %} +{% set script_files = script_files + ['_static/jquery.tweet.js'] %} + +{%- macro sidebar() %} + {%- if not embedded %}{% if not theme_nosidebar|tobool %} +
    +
    + {%- block sidebarlogo %} + {%- if logo %} + + {%- endif %} + {%- endblock %} + {%- block sidebartoc %} + {%- if display_toc %} +

    {{ _('Table Of Contents') }}

    + {{ toc }} + {%- endif %} + {%- endblock %} + {%- block sidebarrel %} + {%- if prev %} +

    {{ _('Previous topic') }}

    +

    {{ prev.title }}

    + {%- endif %} + {%- if next %} +

    {{ _('Next topic') }}

    +

    {{ next.title }}

    + {%- endif %} + {%- endblock %} + {%- block sidebarsourcelink %} + {%- if show_source and has_source and sourcename %} +

    {{ _('This Page') }}

    + + {%- endif %} + {%- endblock %} + {%- if customsidebar %} + {% include customsidebar %} + {%- endif %} + {%- block sidebarsearch %} + {%- if pagename != "search" %} + + + {%- endif %} + {%- endblock %} +
    +
    + {%- endif %}{% endif %} +{%- endmacro %} + +{% block relbar1 %}{% endblock relbar1 %} + +{% block header %} + +{% endblock %} \ No newline at end of file diff --git a/doc/source/_theme/theme.conf b/doc/source/_theme/theme.conf new file mode 100644 index 0000000000..1cc4004464 --- /dev/null +++ b/doc/source/_theme/theme.conf @@ -0,0 +1,4 @@ +[theme] +inherit = basic +stylesheet = nature.css +pygments_style = tango diff --git a/doc/source/conf.py b/doc/source/conf.py index 47025b6daa..fcce1daacc 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -101,7 +101,8 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme_path = ["."] +html_theme = '_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the From 25d6955bb477241af0926ca90969b9e0c1ec5647 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 18 Jun 2014 11:19:31 -0600 Subject: [PATCH 0113/3494] Change the token verb to issue/revoke Change the token verb to issue/revoke as documented in: https://wiki.openstack.org/wiki/OpenStackClient/Commands#token https://wiki.openstack.org/wiki/OpenStackClient/Commands#Actions Change-Id: I44f77f98ad3269c4f2149301c204804dcb75ac81 --- openstackclient/identity/v2_0/token.py | 4 ++-- openstackclient/identity/v3/token.py | 2 +- setup.cfg | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openstackclient/identity/v2_0/token.py b/openstackclient/identity/v2_0/token.py index 793354cf6f..c503ac57fa 100644 --- a/openstackclient/identity/v2_0/token.py +++ b/openstackclient/identity/v2_0/token.py @@ -23,7 +23,7 @@ class CreateToken(show.ShowOne): - """Create token command""" + """Issue token command""" log = logging.getLogger(__name__ + '.CreateToken') @@ -40,7 +40,7 @@ def take_action(self, parsed_args): class DeleteToken(command.Command): - """Delete token command""" + """Revoke token command""" log = logging.getLogger(__name__ + '.DeleteToken') diff --git a/openstackclient/identity/v3/token.py b/openstackclient/identity/v3/token.py index 3cc78cd7a8..452a7ae567 100644 --- a/openstackclient/identity/v3/token.py +++ b/openstackclient/identity/v3/token.py @@ -186,7 +186,7 @@ def take_action(self, parsed_args): class CreateToken(show.ShowOne): - """Create token command""" + """Issue token command""" log = logging.getLogger(__name__ + '.CreateToken') diff --git a/setup.cfg b/setup.cfg index a1252cb8fe..9970a4b2bd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -151,8 +151,8 @@ openstack.identity.v2_0 = service_list =openstackclient.identity.v2_0.service:ListService service_show =openstackclient.identity.v2_0.service:ShowService - token_create =openstackclient.identity.v2_0.token:CreateToken - token_delete =openstackclient.identity.v2_0.token:DeleteToken + token_issue =openstackclient.identity.v2_0.token:CreateToken + token_revoke =openstackclient.identity.v2_0.token:DeleteToken user_role_list = openstackclient.identity.v2_0.role:ListUserRole @@ -236,7 +236,7 @@ openstack.identity.v3 = service_show = openstackclient.identity.v3.service:ShowService service_set = openstackclient.identity.v3.service:SetService - token_create = openstackclient.identity.v3.token:CreateToken + token_issue = openstackclient.identity.v3.token:CreateToken user_create = openstackclient.identity.v3.user:CreateUser user_delete = openstackclient.identity.v3.user:DeleteUser From 9dd3a5326c0fe04df3b4224754fd02351a4623c9 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 19 Jun 2014 12:38:30 -0500 Subject: [PATCH 0114/3494] Complete Identity v3 list command filters Complete the 'group list' and 'user list' filter options following the refactor in https://review.openstack.org/69878 Change-Id: Ib4af417c56d4f7da4b88852f191af615cc7fa2ec --- openstackclient/identity/v3/group.py | 47 ++++- openstackclient/identity/v3/user.py | 50 +++-- .../tests/identity/v3/test_group.py | 193 ++++++++++++++++++ .../tests/identity/v3/test_user.py | 102 ++++++++- 4 files changed, 362 insertions(+), 30 deletions(-) create mode 100644 openstackclient/tests/identity/v3/test_group.py diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index c5a4401722..4eb144896d 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -164,17 +164,21 @@ def take_action(self, parsed_args): class ListGroup(lister.Lister): - """List groups and optionally roles assigned to groups""" + """List groups""" log = logging.getLogger(__name__ + '.ListGroup') def get_parser(self, prog_name): parser = super(ListGroup, self).get_parser(prog_name) parser.add_argument( - 'group', - metavar='', - nargs='?', - help='Name or ID of group to list [required with --role]', + '--domain', + metavar='', + help='Filter group list by ', + ) + parser.add_argument( + '--user', + metavar='', + help='List group memberships for (name or ID)', ) parser.add_argument( '--long', @@ -188,18 +192,39 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity + if parsed_args.domain: + domain = utils.find_resource( + identity_client.domains, + parsed_args.domain, + ).id + else: + domain = None + + if parsed_args.user: + user = utils.find_resource( + identity_client.users, + parsed_args.user, + ).id + else: + user = None + # List groups if parsed_args.long: columns = ('ID', 'Name', 'Domain ID', 'Description') else: columns = ('ID', 'Name') - data = identity_client.groups.list() + data = identity_client.groups.list( + domain=domain, + user=user, + ) - return (columns, - (utils.get_item_properties( - s, columns, - formatters={}, - ) for s in data)) + return ( + columns, + (utils.get_item_properties( + s, columns, + formatters={}, + ) for s in data) + ) class RemoveUserFromGroup(command.Command): diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index c4adb225ca..38c3497339 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -148,17 +148,21 @@ def take_action(self, parsed_args): class ListUser(lister.Lister): - """List users and optionally roles assigned to users""" + """List users""" log = logging.getLogger(__name__ + '.ListUser') def get_parser(self, prog_name): parser = super(ListUser, self).get_parser(prog_name) parser.add_argument( - 'user', - metavar='', - nargs='?', - help='Name or ID of user to list [required with --role]', + '--domain', + metavar='', + help='Filter group list by ', + ) + parser.add_argument( + '--group', + metavar='', + help='List memberships of (name or ID)', ) parser.add_argument( '--long', @@ -169,7 +173,24 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) + identity_client = self.app.client_manager.identity + + if parsed_args.domain: + domain = utils.find_resource( + identity_client.domains, + parsed_args.domain, + ).id + else: + domain = None + + if parsed_args.group: + group = utils.find_resource( + identity_client.groups, + parsed_args.group, + ).id + else: + group = None # List users if parsed_args.long: @@ -177,13 +198,18 @@ def take_action(self, parsed_args): 'Description', 'Email', 'Enabled') else: columns = ('ID', 'Name') - data = self.app.client_manager.identity.users.list() + data = identity_client.users.list( + domain=domain, + group=group, + ) - return (columns, - (utils.get_item_properties( - s, columns, - formatters={}, - ) for s in data)) + return ( + columns, + (utils.get_item_properties( + s, columns, + formatters={}, + ) for s in data) + ) class SetUser(command.Command): diff --git a/openstackclient/tests/identity/v3/test_group.py b/openstackclient/tests/identity/v3/test_group.py new file mode 100644 index 0000000000..dce5636281 --- /dev/null +++ b/openstackclient/tests/identity/v3/test_group.py @@ -0,0 +1,193 @@ +# 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 copy + +from openstackclient.identity.v3 import group +from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes + + +class TestGroup(identity_fakes.TestIdentityv3): + + def setUp(self): + super(TestGroup, self).setUp() + + # Get a shortcut to the DomainManager Mock + self.domains_mock = self.app.client_manager.identity.domains + self.domains_mock.reset_mock() + + # Get a shortcut to the GroupManager Mock + self.groups_mock = self.app.client_manager.identity.groups + self.groups_mock.reset_mock() + + # Get a shortcut to the UserManager Mock + self.users_mock = self.app.client_manager.identity.users + self.users_mock.reset_mock() + + +class TestGroupList(TestGroup): + + def setUp(self): + super(TestGroupList, self).setUp() + + self.groups_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.GROUP), + loaded=True, + ) + self.groups_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.GROUP), + loaded=True, + ), + ] + + self.domains_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.DOMAIN), + loaded=True, + ) + + self.users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + + # Get the command object to test + self.cmd = group.ListGroup(self.app, None) + + def test_group_list_no_options(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'domain': None, + 'user': None, + } + + self.groups_mock.list.assert_called_with( + **kwargs + ) + + collist = ('ID', 'Name') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.group_id, + identity_fakes.group_name, + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_group_list_domain(self): + arglist = [ + '--domain', identity_fakes.domain_id, + ] + verifylist = [ + ('domain', identity_fakes.domain_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'domain': identity_fakes.domain_id, + 'user': None, + } + + self.groups_mock.list.assert_called_with( + **kwargs + ) + + collist = ('ID', 'Name') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.group_id, + identity_fakes.group_name, + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_group_list_user(self): + arglist = [ + '--user', identity_fakes.user_name, + ] + verifylist = [ + ('user', identity_fakes.user_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'domain': None, + 'user': identity_fakes.user_id, + } + + self.groups_mock.list.assert_called_with( + **kwargs + ) + + collist = ('ID', 'Name') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.group_id, + identity_fakes.group_name, + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_group_list_long(self): + arglist = [ + '--long', + ] + verifylist = [ + ('long', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'domain': None, + 'user': None, + } + + self.groups_mock.list.assert_called_with( + **kwargs + ) + + collist = ( + 'ID', + 'Name', + 'Domain ID', + 'Description', + ) + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.group_id, + identity_fakes.group_name, + '', + '', + ), ) + self.assertEqual(datalist, tuple(data)) diff --git a/openstackclient/tests/identity/v3/test_user.py b/openstackclient/tests/identity/v3/test_user.py index b9c060d767..569d91401c 100644 --- a/openstackclient/tests/identity/v3/test_user.py +++ b/openstackclient/tests/identity/v3/test_user.py @@ -34,9 +34,9 @@ def setUp(self): self.projects_mock = self.app.client_manager.identity.projects self.projects_mock.reset_mock() - # Get a shortcut to the RoleManager Mock - self.roles_mock = self.app.client_manager.identity.roles - self.roles_mock.reset_mock() + # Get a shortcut to the GroupManager Mock + self.groups_mock = self.app.client_manager.identity.groups + self.groups_mock.reset_mock() # Get a shortcut to the UserManager Mock self.users_mock = self.app.client_manager.identity.users @@ -489,6 +489,18 @@ def setUp(self): ), ] + self.domains_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.DOMAIN), + loaded=True, + ) + + self.groups_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.GROUP), + loaded=True, + ) + # Get the command object to test self.cmd = user.ListUser(self.app, None) @@ -500,7 +512,75 @@ def test_user_list_no_options(self): # DisplayCommandBase.take_action() returns two tuples columns, data = self.cmd.take_action(parsed_args) - self.users_mock.list.assert_called_with() + # Set expected values + kwargs = { + 'domain': None, + 'group': None, + } + + self.users_mock.list.assert_called_with( + **kwargs + ) + + collist = ('ID', 'Name') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.user_id, + identity_fakes.user_name, + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_user_list_domain(self): + arglist = [ + '--domain', identity_fakes.domain_id, + ] + verifylist = [ + ('domain', identity_fakes.domain_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'domain': identity_fakes.domain_id, + 'group': None, + } + + self.users_mock.list.assert_called_with( + **kwargs + ) + + collist = ('ID', 'Name') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.user_id, + identity_fakes.user_name, + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_user_list_group(self): + arglist = [ + '--group', identity_fakes.group_name, + ] + verifylist = [ + ('group', identity_fakes.group_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'domain': None, + 'group': identity_fakes.group_id, + } + + self.users_mock.list.assert_called_with( + **kwargs + ) collist = ('ID', 'Name') self.assertEqual(columns, collist) @@ -508,7 +588,7 @@ def test_user_list_no_options(self): identity_fakes.user_id, identity_fakes.user_name, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_user_list_long(self): arglist = [ @@ -522,7 +602,15 @@ def test_user_list_long(self): # DisplayCommandBase.take_action() returns two tuples columns, data = self.cmd.take_action(parsed_args) - self.users_mock.list.assert_called_with() + # Set expected values + kwargs = { + 'domain': None, + 'group': None, + } + + self.users_mock.list.assert_called_with( + **kwargs + ) collist = ( 'ID', @@ -543,7 +631,7 @@ def test_user_list_long(self): identity_fakes.user_email, True, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) class TestUserSet(TestUser): From 3019f11032459c10efea152186bd630479867f37 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 20 Jun 2014 03:38:56 +0000 Subject: [PATCH 0115/3494] Updated from global requirements Change-Id: I708fe9d2f10e53d61e67130ff41b0f92cddef64d --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 422b4a015b..484c4548a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,4 @@ python-keystoneclient>=0.9.0 python-novaclient>=2.17.0 python-cinderclient>=1.0.6 requests>=1.1 -six>=1.6.0 +six>=1.7.0 From 3fa5fa5ba745cec0ee96f1031fadff0d5e7820f0 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 15 May 2014 09:05:37 -0500 Subject: [PATCH 0116/3494] Update docs and release notes for 0.4.0 Change-Id: Iad6cfe5dee63adb9e60a0ea9811217b3175eb99c --- doc/source/commands.rst | 76 ++++++++++++++++++++---------------- doc/source/index.rst | 8 ++-- doc/source/man/openstack.rst | 9 +++++ doc/source/plugins.rst | 4 +- doc/source/releases.rst | 37 ++++++++++++++++++ 5 files changed, 95 insertions(+), 39 deletions(-) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index e0cfee7a47..a9d10c96d3 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -12,9 +12,9 @@ Commands take the form:: openstack [] [] [] -* All long options names begin with two dashes ('--') and use a single dash - ('-') internally between words (--like-this). Underscores ('_') are not used - in option names. +* All long options names begin with two dashes (``--``) and use a single dash + (``-``) internally between words (``--like-this``). Underscores (``_``) are + not used in option names. Global Options @@ -25,11 +25,12 @@ invocation regardless of action to be performed. They include authentication credentials and API version selection. Most global options have a corresponding environment variable that may also be used to set the value. If both are present, the command-line option takes priority. The environment variable -names are derived from the option name by dropping the leading dashes ('--'), -converting each embedded dash ('-') to an underscore ('_'), and converting +names are derived from the option name by dropping the leading dashes (``--``), +converting each embedded dash (``-``) to an underscore (``_``), and converting to upper case. -For example, ``--os-username`` can be set from the environment via ``OS_USERNAME``. +For example, the default value of ``--os-username`` can be set by defining +the environment variable ``OS_USERNAME``. Command Object(s) and Action @@ -64,39 +65,46 @@ the command and any positional arguments the command requires. Actions ------- -The actions used by OpenStackClient are defined below to provide a consistent meaning to each action. Many of them have logical opposite actions. Those actions with an opposite action are noted in parens if applicable. - -* authorize - authorize a token (used in OAuth) -* add (remove) - add some object to a container object; the command is built in the order of "container add object" ( ), the positional arguments appear in the same order -* attach (detach) - deprecated; use add/remove -* create (delete) - create a new occurrence of the specified object -* delete (create) - delete a specific occurrence of the specified object -* detach (attach) - deprecated; use add/remove -* list - display summary information about multiple objects -* lock (unlock) -* migrate - move a server to a different host; --live performs a live migration if possible -* pause (unpause) - stop a server and leave it in memory -* reboot - forcibly reboot a server -* rebuild - rebuild a server using (most of) the same arguments as in the original create -* remove (add) - remove an object from a group of objects -* rescue (unrescue) - reboot a server in a special rescue mode allowing access to the original disks -* resize - change a server's flavor -* resume (suspend) - return a suspended server to running state -* save - download an object locally -* set (unset) - set a property on the object, formerly called metadata -* show - display detailed information about the specific object -* suspend (resume) - stop a server and save to disk freeing memory -* unlock (lock) -* unpause (pause) - return a paused server to running state -* unrescue (rescue) - return a server to normal boot mode -* unset (set) - remove an attribute of the object +The actions used by OpenStackClient are defined below to provide a consistent +meaning to each action. Many of them have logical opposite actions. +Those actions with an opposite action are noted in parens if applicable. + +* ``authorize`` - authorize a token (used in OAuth) +* ``add`` (``remove``) - add some object to a container object; the command + is built in the order of ``container add object ``, + the positional arguments appear in the same order +* ``create`` (``delete``) - create a new occurrence of the specified object +* ``delete`` (``create``) - delete a specific occurrence of the specified object +* ``issue`` (``revoke``) - issue a token +* ``list`` - display summary information about multiple objects +* ``lock`` (``unlock``) +* ``migrate`` - move a server to a different host; ``--live`` performs a + live migration if possible +* ``pause`` (``unpause``) - stop a server and leave it in memory +* ``reboot`` - forcibly reboot a server +* ``rebuild`` - rebuild a server using (most of) the same arguments as in the original create +* ``remove`` (``add``) - remove an object from a group of objects +* ``rescue`` (``unrescue``) - reboot a server in a special rescue mode allowing access to the original disks +* ``resize`` - change a server's flavor +* ``resume`` (``suspend``) - return a suspended server to running state +* ``revoke`` (``issue``) - revoke a token +* ``save`` - download an object locally +* ``set`` (``unset``) - set a property on the object, formerly called metadata +* ``show`` - display detailed information about the specific object +* ``suspend`` (``resume``) - stop a server and save to disk freeing memory +* ``unlock`` (``lock``) +* ``unpause`` (``pause``) - return a paused server to running state +* ``unrescue`` (``rescue``) - return a server to normal boot mode +* ``unset`` (``set``) - remove an attribute of the object + Implementation ============== The command structure is designed to support seamless addition of plugin command modules via ``setuptools`` entry points. The plugin commands must -be subclasses of Cliff's command.Command object. +be subclasses of Cliff's command.Command object. See :doc:`plugins` for +more information. Command Entry Points @@ -111,7 +119,7 @@ entry points have the form:: verb_object = fully.qualified.module.vXX.object:VerbObject -For example, the 'list user' command fir the Identity API is identified in +For example, the ``list user`` command for the Identity API is identified in ``setup.cfg`` with:: openstack.identity.v3 = diff --git a/doc/source/index.rst b/doc/source/index.rst index f42ea1b720..2bb8f3868b 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,7 +1,9 @@ -OpenStack Client -================ +=============== +OpenStackClient +=============== -This is a command-line client for OpenStack that unifies the operation +OpenStackClient (aka OSC) is a command-line client for OpenStack that +unifies the operation of python-keystoneclient, python-novaclient, python-glanceclient and python-cinderclient in a single shell with a uniform command structure. diff --git a/doc/source/man/openstack.rst b/doc/source/man/openstack.rst index 74db68154a..16f0bc4764 100644 --- a/doc/source/man/openstack.rst +++ b/doc/source/man/openstack.rst @@ -181,9 +181,18 @@ The following environment variables can be set to alter the behaviour of :progra :envvar:`OS_CACERT` CA certificate bundle file +:envvar:`OS_COMPUTE_API_VERISON` + Compute API version (Default: 2) + :envvar:`OS_IDENTITY_API_VERISON` Identity API version (Default: 2.0) +:envvar:`OS_IMAGE_API_VERISON` + Image API version (Default: 1) + +:envvar:`OS_VOLUME_API_VERISON` + Volume API version (Default: 1) + :envvar:`OS_XXXX_API_VERISON` Additional API version options will be available depending on the installed API libraries. diff --git a/doc/source/plugins.rst b/doc/source/plugins.rst index 5cea16cf87..690c1269c8 100644 --- a/doc/source/plugins.rst +++ b/doc/source/plugins.rst @@ -11,7 +11,7 @@ Implementation -------------- Plugins are discovered by enumerating the entry points -found under ``openstack.cli.extension`` and initializing the specified +found under :py:mod:`openstack.cli.extension` and initializing the specified client module. :: @@ -42,5 +42,5 @@ defined for the API version: plugin_list = oscplugin.v1.plugin:ListPlugin plugin_show = oscplugin.v1.plugin:ShowPlugin -Note that OSC defines the group name as ``openstack..v`` +Note that OSC defines the group name as :py:mod:`openstack..v` so the version should not contain the leading 'v' character. diff --git a/doc/source/releases.rst b/doc/source/releases.rst index 85a29ba9ff..9551db2ee0 100644 --- a/doc/source/releases.rst +++ b/doc/source/releases.rst @@ -2,6 +2,43 @@ Release Notes ============= +0.4.0 (20 Jun 2014) +=================== + +* Bug 1184012_: fix Identity v2 endpoint command name/id handling +* Bug 1207615_: add ``--volume`` and ``--force`` to ``image create`` command +* Bug 1220280_: add ``--property`` to project create and set commands +* Bug 1246310_: add ``role assignments list`` command +* Bug 1285800_: rename ``agent`` to ``compute agent`` +* Bug 1289513_: add ``--domain`` to project list +* Bug 1289594_: fix keypair show output +* Bug 1292337_: fix ec2 credentials project ID output +* Bug 1303978_: fix output of ``volume type create`` command +* Bug 1316870_: display all output when ``--lines`` omitted from ``console log show`` command +* add 'interface' and 'url' columns to endpoint list command +* add identity provider create/delete/list/set/show commands +* change ``volume create --volume-type`` option to ``--type`` +* fix ``server image create`` command output +* configure appropriate logging levels for ``--verbose``, ``--quiet`` and ``--debug`` +* properly handle properties in Image v1 ``create`` and ``set`` commands +* rename Identity v2 ``token create`` to ``token issue`` +* add Identity v2 ``token revoke`` command +* refactor the ``group|user|role list`` command filters so that each command + only lists rows of that type of object, ie ``user list`` always lists users, etc. +* add ``role assignment list`` command +* add ``extension list`` command + +.. _1184012: https://launchpad.net/bugs/1184012 +.. _1207615: https://launchpad.net/bugs/1207615 +.. _1220280: https://launchpad.net/bugs/1220280 +.. _1246310: https://launchpad.net/bugs/1246310 +.. _1285800: https://launchpad.net/bugs/1285800 +.. _1289513: https://launchpad.net/bugs/1289513 +.. _1289594: https://launchpad.net/bugs/1289594 +.. _1292337: https://launchpad.net/bugs/1292337 +.. _1303978: https://launchpad.net/bugs/1303978 +.. _1316870: https://launchpad.net/bugs/1316870 + 0.3.1 (28 Feb 2014) =================== From a36898370537cdb610f9fd012eea2439ba642f21 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 20 Jun 2014 16:13:06 -0400 Subject: [PATCH 0117/3494] sync oslo bits update gettextutils.py, strutils.py, install_venv_common.py remove iniparsers.py oslo-incubator commit 1223cf Change-Id: I23923d580f57ab6c12622f10d9f278c44c863feb --- openstack-common.conf | 1 - openstackclient/openstack/common/__init__.py | 17 ++ .../openstack/common/gettextutils.py | 232 ++++++++++-------- openstackclient/openstack/common/iniparser.py | 130 ---------- openstackclient/openstack/common/strutils.py | 25 +- tools/install_venv_common.py | 2 +- 6 files changed, 156 insertions(+), 251 deletions(-) delete mode 100644 openstackclient/openstack/common/iniparser.py diff --git a/openstack-common.conf b/openstack-common.conf index 2b26cf209b..91d6387b4b 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -1,7 +1,6 @@ [DEFAULT] # The list of modules to copy from openstack-common -module=iniparser module=install_venv_common module=strutils diff --git a/openstackclient/openstack/common/__init__.py b/openstackclient/openstack/common/__init__.py index e69de29bb2..d1223eaf76 100644 --- a/openstackclient/openstack/common/__init__.py +++ b/openstackclient/openstack/common/__init__.py @@ -0,0 +1,17 @@ +# +# 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 six + + +six.add_move(six.MovedModule('mox', 'mox', 'mox3.mox')) diff --git a/openstackclient/openstack/common/gettextutils.py b/openstackclient/openstack/common/gettextutils.py index 2b15854862..6f573a7f8d 100644 --- a/openstackclient/openstack/common/gettextutils.py +++ b/openstackclient/openstack/common/gettextutils.py @@ -28,70 +28,135 @@ import locale from logging import handlers import os -import re from babel import localedata import six -_localedir = os.environ.get('openstackclient'.upper() + '_LOCALEDIR') -_t = gettext.translation('openstackclient', localedir=_localedir, fallback=True) - -# We use separate translation catalogs for each log level, so set up a -# mapping between the log level name and the translator. The domain -# for the log level is project_name + "-log-" + log_level so messages -# for each level end up in their own catalog. -_t_log_levels = dict( - (level, gettext.translation('openstackclient' + '-log-' + level, - localedir=_localedir, - fallback=True)) - for level in ['info', 'warning', 'error', 'critical'] -) - _AVAILABLE_LANGUAGES = {} + +# FIXME(dhellmann): Remove this when moving to oslo.i18n. USE_LAZY = False -def enable_lazy(): - """Convenience function for configuring _() to use lazy gettext - - Call this at the start of execution to enable the gettextutils._ - function to use lazy gettext functionality. This is useful if - your project is importing _ directly instead of using the - gettextutils.install() way of importing the _ function. +class TranslatorFactory(object): + """Create translator functions """ - global USE_LAZY - USE_LAZY = True + def __init__(self, domain, lazy=False, localedir=None): + """Establish a set of translation functions for the domain. + + :param domain: Name of translation domain, + specifying a message catalog. + :type domain: str + :param lazy: Delays translation until a message is emitted. + Defaults to False. + :type lazy: Boolean + :param localedir: Directory with translation catalogs. + :type localedir: str + """ + self.domain = domain + self.lazy = lazy + if localedir is None: + localedir = os.environ.get(domain.upper() + '_LOCALEDIR') + self.localedir = localedir -def _(msg): - if USE_LAZY: - return Message(msg, domain='openstackclient') - else: - if six.PY3: - return _t.gettext(msg) - return _t.ugettext(msg) + def _make_translation_func(self, domain=None): + """Return a new translation function ready for use. + Takes into account whether or not lazy translation is being + done. -def _log_translation(msg, level): - """Build a single translation of a log message - """ - if USE_LAZY: - return Message(msg, domain='openstackclient' + '-log-' + level) - else: - translator = _t_log_levels[level] + The domain can be specified to override the default from the + factory, but the localedir from the factory is always used + because we assume the log-level translation catalogs are + installed in the same directory as the main application + catalog. + + """ + if domain is None: + domain = self.domain + if self.lazy: + return functools.partial(Message, domain=domain) + t = gettext.translation( + domain, + localedir=self.localedir, + fallback=True, + ) if six.PY3: - return translator.gettext(msg) - return translator.ugettext(msg) + return t.gettext + return t.ugettext + + @property + def primary(self): + "The default translation function." + return self._make_translation_func() + + def _make_log_translation_func(self, level): + return self._make_translation_func(self.domain + '-log-' + level) + + @property + def log_info(self): + "Translate info-level log messages." + return self._make_log_translation_func('info') + + @property + def log_warning(self): + "Translate warning-level log messages." + return self._make_log_translation_func('warning') + + @property + def log_error(self): + "Translate error-level log messages." + return self._make_log_translation_func('error') + + @property + def log_critical(self): + "Translate critical-level log messages." + return self._make_log_translation_func('critical') + + +# NOTE(dhellmann): When this module moves out of the incubator into +# oslo.i18n, these global variables can be moved to an integration +# module within each application. + +# Create the global translation functions. +_translators = TranslatorFactory('openstackclient') + +# The primary translation function using the well-known name "_" +_ = _translators.primary # Translators for log levels. # # The abbreviated names are meant to reflect the usual use of a short # name like '_'. The "L" is for "log" and the other letter comes from # the level. -_LI = functools.partial(_log_translation, level='info') -_LW = functools.partial(_log_translation, level='warning') -_LE = functools.partial(_log_translation, level='error') -_LC = functools.partial(_log_translation, level='critical') +_LI = _translators.log_info +_LW = _translators.log_warning +_LE = _translators.log_error +_LC = _translators.log_critical + +# NOTE(dhellmann): End of globals that will move to the application's +# integration module. + + +def enable_lazy(): + """Convenience function for configuring _() to use lazy gettext + + Call this at the start of execution to enable the gettextutils._ + function to use lazy gettext functionality. This is useful if + your project is importing _ directly instead of using the + gettextutils.install() way of importing the _ function. + """ + # FIXME(dhellmann): This function will be removed in oslo.i18n, + # because the TranslatorFactory makes it superfluous. + global _, _LI, _LW, _LE, _LC, USE_LAZY + tf = TranslatorFactory('openstackclient', lazy=True) + _ = tf.primary + _LI = tf.log_info + _LW = tf.log_warning + _LE = tf.log_error + _LC = tf.log_critical + USE_LAZY = True def install(domain, lazy=False): @@ -113,26 +178,9 @@ def install(domain, lazy=False): any available locale. """ if lazy: - # NOTE(mrodden): Lazy gettext functionality. - # - # The following introduces a deferred way to do translations on - # messages in OpenStack. We override the standard _() function - # and % (format string) operation to build Message objects that can - # later be translated when we have more information. - def _lazy_gettext(msg): - """Create and return a Message object. - - Lazy gettext function for a given domain, it is a factory method - for a project/module to get a lazy gettext function for its own - translation domain (i.e. nova, glance, cinder, etc.) - - Message encapsulates a string so that we can translate - it later when needed. - """ - return Message(msg, domain=domain) - from six import moves - moves.builtins.__dict__['_'] = _lazy_gettext + tf = TranslatorFactory(domain, lazy=True) + moves.builtins.__dict__['_'] = tf.primary else: localedir = '%s_LOCALEDIR' % domain.upper() if six.PY3: @@ -248,47 +296,22 @@ def _sanitize_mod_params(self, other): if other is None: params = (other,) elif isinstance(other, dict): - params = self._trim_dictionary_parameters(other) - else: - params = self._copy_param(other) - return params - - def _trim_dictionary_parameters(self, dict_param): - """Return a dict that only has matching entries in the msgid.""" - # NOTE(luisg): Here we trim down the dictionary passed as parameters - # to avoid carrying a lot of unnecessary weight around in the message - # object, for example if someone passes in Message() % locals() but - # only some params are used, and additionally we prevent errors for - # non-deepcopyable objects by unicoding() them. - - # Look for %(param) keys in msgid; - # Skip %% and deal with the case where % is first character on the line - keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', self.msgid) - - # If we don't find any %(param) keys but have a %s - if not keys and re.findall('(?:[^%]|^)%[a-z]', self.msgid): - # Apparently the full dictionary is the parameter - params = self._copy_param(dict_param) - else: + # Merge the dictionaries + # Copy each item in case one does not support deep copy. params = {} - # Save our existing parameters as defaults to protect - # ourselves from losing values if we are called through an - # (erroneous) chain that builds a valid Message with - # arguments, and then does something like "msg % kwds" - # where kwds is an empty dictionary. - src = {} if isinstance(self.params, dict): - src.update(self.params) - src.update(dict_param) - for key in keys: - params[key] = self._copy_param(src[key]) - + for key, val in self.params.items(): + params[key] = self._copy_param(val) + for key, val in other.items(): + params[key] = self._copy_param(val) + else: + params = self._copy_param(other) return params def _copy_param(self, param): try: return copy.deepcopy(param) - except TypeError: + except Exception: # Fallback to casting to unicode this will handle the # python code-like objects that can't be deep-copied return six.text_type(param) @@ -300,13 +323,14 @@ def __add__(self, other): def __radd__(self, other): return self.__add__(other) - def __str__(self): - # NOTE(luisg): Logging in python 2.6 tries to str() log records, - # and it expects specifically a UnicodeError in order to proceed. - msg = _('Message objects do not support str() because they may ' - 'contain non-ascii characters. ' - 'Please use unicode() or translate() instead.') - raise UnicodeError(msg) + if six.PY2: + def __str__(self): + # NOTE(luisg): Logging in python 2.6 tries to str() log records, + # and it expects specifically a UnicodeError in order to proceed. + msg = _('Message objects do not support str() because they may ' + 'contain non-ascii characters. ' + 'Please use unicode() or translate() instead.') + raise UnicodeError(msg) def get_available_languages(domain): diff --git a/openstackclient/openstack/common/iniparser.py b/openstackclient/openstack/common/iniparser.py deleted file mode 100644 index 9bf399f0c7..0000000000 --- a/openstackclient/openstack/common/iniparser.py +++ /dev/null @@ -1,130 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 OpenStack LLC. -# -# 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. - - -class ParseError(Exception): - def __init__(self, message, lineno, line): - self.msg = message - self.line = line - self.lineno = lineno - - def __str__(self): - return 'at line %d, %s: %r' % (self.lineno, self.msg, self.line) - - -class BaseParser(object): - lineno = 0 - parse_exc = ParseError - - def _assignment(self, key, value): - self.assignment(key, value) - return None, [] - - def _get_section(self, line): - if line[-1] != ']': - return self.error_no_section_end_bracket(line) - if len(line) <= 2: - return self.error_no_section_name(line) - - return line[1:-1] - - def _split_key_value(self, line): - colon = line.find(':') - equal = line.find('=') - if colon < 0 and equal < 0: - return self.error_invalid_assignment(line) - - if colon < 0 or (equal >= 0 and equal < colon): - key, value = line[:equal], line[equal + 1:] - else: - key, value = line[:colon], line[colon + 1:] - - value = value.strip() - if ((value and value[0] == value[-1]) and - (value[0] == "\"" or value[0] == "'")): - value = value[1:-1] - return key.strip(), [value] - - def parse(self, lineiter): - key = None - value = [] - - for line in lineiter: - self.lineno += 1 - - line = line.rstrip() - if not line: - # Blank line, ends multi-line values - if key: - key, value = self._assignment(key, value) - continue - elif line[0] in (' ', '\t'): - # Continuation of previous assignment - if key is None: - self.error_unexpected_continuation(line) - else: - value.append(line.lstrip()) - continue - - if key: - # Flush previous assignment, if any - key, value = self._assignment(key, value) - - if line[0] == '[': - # Section start - section = self._get_section(line) - if section: - self.new_section(section) - elif line[0] in '#;': - self.comment(line[1:].lstrip()) - else: - key, value = self._split_key_value(line) - if not key: - return self.error_empty_key(line) - - if key: - # Flush previous assignment, if any - self._assignment(key, value) - - def assignment(self, key, value): - """Called when a full assignment is parsed""" - raise NotImplementedError() - - def new_section(self, section): - """Called when a new section is started""" - raise NotImplementedError() - - def comment(self, comment): - """Called when a comment is parsed""" - pass - - def error_invalid_assignment(self, line): - raise self.parse_exc("No ':' or '=' found in assignment", - self.lineno, line) - - def error_empty_key(self, line): - raise self.parse_exc('Key cannot be empty', self.lineno, line) - - def error_unexpected_continuation(self, line): - raise self.parse_exc('Unexpected continuation line', - self.lineno, line) - - def error_no_section_end_bracket(self, line): - raise self.parse_exc('Invalid section (must end with ])', - self.lineno, line) - - def error_no_section_name(self, line): - raise self.parse_exc('Empty section name', self.lineno, line) diff --git a/openstackclient/openstack/common/strutils.py b/openstackclient/openstack/common/strutils.py index 33fca54b26..9d70264fd3 100644 --- a/openstackclient/openstack/common/strutils.py +++ b/openstackclient/openstack/common/strutils.py @@ -78,7 +78,7 @@ def bool_from_string(subject, strict=False, default=False): Strings yielding False are 'f', 'false', 'off', 'n', 'no', or '0'. """ if not isinstance(subject, six.string_types): - subject = str(subject) + subject = six.text_type(subject) lowered = subject.strip().lower() @@ -98,7 +98,8 @@ def bool_from_string(subject, strict=False, default=False): def safe_decode(text, incoming=None, errors='strict'): - """Decodes incoming str using `incoming` if they're not already unicode. + """Decodes incoming text/bytes string using `incoming` if they're not + already unicode. :param incoming: Text's current encoding :param errors: Errors handling policy. See here for valid @@ -107,7 +108,7 @@ def safe_decode(text, incoming=None, errors='strict'): representation of it. :raises TypeError: If text is not an instance of str """ - if not isinstance(text, six.string_types): + if not isinstance(text, (six.string_types, six.binary_type)): raise TypeError("%s can't be decoded" % type(text)) if isinstance(text, six.text_type): @@ -137,7 +138,7 @@ def safe_decode(text, incoming=None, errors='strict'): def safe_encode(text, incoming=None, encoding='utf-8', errors='strict'): - """Encodes incoming str/unicode using `encoding`. + """Encodes incoming text/bytes string using `encoding`. If incoming is not specified, text is expected to be encoded with current python's default encoding. (`sys.getdefaultencoding`) @@ -150,7 +151,7 @@ def safe_encode(text, incoming=None, representation of it. :raises TypeError: If text is not an instance of str """ - if not isinstance(text, six.string_types): + if not isinstance(text, (six.string_types, six.binary_type)): raise TypeError("%s can't be encoded" % type(text)) if not incoming: @@ -158,19 +159,13 @@ def safe_encode(text, incoming=None, sys.getdefaultencoding()) if isinstance(text, six.text_type): - if six.PY3: - return text.encode(encoding, errors).decode(incoming) - else: - return text.encode(encoding, errors) + return text.encode(encoding, errors) elif text and encoding != incoming: # Decode text before encoding it with `encoding` text = safe_decode(text, incoming, errors) - if six.PY3: - return text.encode(encoding, errors).decode(incoming) - else: - return text.encode(encoding, errors) - - return text + return text.encode(encoding, errors) + else: + return text def string_to_bytes(text, unit_system='IEC', return_int=False): diff --git a/tools/install_venv_common.py b/tools/install_venv_common.py index 46822e3293..e279159abb 100644 --- a/tools/install_venv_common.py +++ b/tools/install_venv_common.py @@ -125,7 +125,7 @@ def parse_args(self, argv): parser.add_option('-n', '--no-site-packages', action='store_true', help="Do not inherit packages from global Python " - "install") + "install.") return parser.parse_args(argv[1:])[0] From eeebd0db8ca47032c292222513a518a21a909847 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 23 Jun 2014 05:34:50 +0000 Subject: [PATCH 0118/3494] Updated from global requirements Change-Id: Ia54da66d13b2667375a8a85c97ced25fd97b6b25 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 484c4548a8..e0f4767769 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ pbr>=0.6,!=0.7,<1.0 -cliff>=1.4.3 +cliff>=1.6.0 keyring>=2.1 pycrypto>=2.6 python-glanceclient>=0.9.0 From 8f59524c3e1ff3f34474d0cfcd217bc9fe0cbfc1 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Thu, 13 Feb 2014 08:07:51 -0700 Subject: [PATCH 0119/3494] Network CRUD bp/neutron https://wiki.openstack.org/wiki/OpenStackClient/Commands#Network_2 Change-Id: I89ee083154afa544b03587e84becace36d9d522a --- openstackclient/common/utils.py | 10 + openstackclient/network/__init__.py | 0 openstackclient/network/client.py | 61 ++++ openstackclient/network/common.py | 43 +++ openstackclient/network/v2_0/__init__.py | 0 openstackclient/network/v2_0/network.py | 236 +++++++++++++ openstackclient/tests/fakes.py | 1 + openstackclient/tests/network/__init__.py | 0 openstackclient/tests/network/common.py | 52 +++ .../tests/network/v2_0/__init__.py | 0 .../tests/network/v2_0/test_network.py | 319 ++++++++++++++++++ openstackclient/tests/test_shell.py | 11 +- requirements.txt | 1 + setup.cfg | 8 + 14 files changed, 740 insertions(+), 2 deletions(-) create mode 100644 openstackclient/network/__init__.py create mode 100644 openstackclient/network/client.py create mode 100644 openstackclient/network/common.py create mode 100644 openstackclient/network/v2_0/__init__.py create mode 100644 openstackclient/network/v2_0/network.py create mode 100644 openstackclient/tests/network/__init__.py create mode 100644 openstackclient/tests/network/common.py create mode 100644 openstackclient/tests/network/v2_0/__init__.py create mode 100644 openstackclient/tests/network/v2_0/test_network.py diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py index a420dd513d..0258f93122 100644 --- a/openstackclient/common/utils.py +++ b/openstackclient/common/utils.py @@ -84,6 +84,16 @@ def format_dict(data): return output[:-2] +def format_list(data): + """Return a formatted strings + + :param data: a list of strings + :rtype: a string formatted to a,b,c + """ + + return ', '.join(data) + + def get_item_properties(item, fields, mixed_case_fields=[], formatters={}): """Return a tuple containing the item properties. diff --git a/openstackclient/network/__init__.py b/openstackclient/network/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstackclient/network/client.py b/openstackclient/network/client.py new file mode 100644 index 0000000000..3c87e135e1 --- /dev/null +++ b/openstackclient/network/client.py @@ -0,0 +1,61 @@ +# 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 logging + +from openstackclient.common import utils + + +LOG = logging.getLogger(__name__) + +DEFAULT_NETWORK_API_VERSION = '2.0' +API_VERSION_OPTION = 'os_network_api_version' +API_NAME = "network" +API_VERSIONS = { + "2.0": "neutronclient.v2_0.client.Client", +} + + +def make_client(instance): + """Returns an network service client.""" + network_client = utils.get_client_class( + API_NAME, + instance._api_version[API_NAME], + API_VERSIONS) + if not instance._url: + instance._url = instance.get_endpoint_for_service_type("network") + return network_client( + username=instance._username, + tenant_name=instance._project_name, + password=instance._password, + region_name=instance._region_name, + auth_url=instance._auth_url, + endpoint_url=instance._url, + token=instance._token, + insecure=instance._insecure, + ca_cert=instance._cacert, + ) + + +def build_option_parser(parser): + """Hook to add global options""" + parser.add_argument( + '--os-network-api-version', + metavar='', + default=utils.env( + 'OS_NETWORK_API_VERSION', + default=DEFAULT_NETWORK_API_VERSION), + help='Network API version, default=' + + DEFAULT_NETWORK_API_VERSION + + ' (Env: OS_NETWORK_API_VERSION)') + return parser diff --git a/openstackclient/network/common.py b/openstackclient/network/common.py new file mode 100644 index 0000000000..5ba44f7bfa --- /dev/null +++ b/openstackclient/network/common.py @@ -0,0 +1,43 @@ +# 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 openstackclient.common import exceptions + + +def find(client, resource, resources, name_or_id): + """Find a network resource + + :param client: network client + :param resource: name of the resource + :param resources: plural name of resource + :param name_or_id: name or id of resource user is looking for + + For example: + n = find(netclient, 'network', 'networks', 'matrix') + """ + list_method = getattr(client, "list_%s" % resources) + # Search for by name + data = list_method(name=name_or_id, fields='id') + info = data[resources] + if len(info) == 1: + return info[0]['id'] + if len(info) > 1: + msg = "More than one %s exists with the name '%s'." + raise exceptions.CommandError(msg % (resource, name_or_id)) + # Search for by id + data = list_method(id=name_or_id, fields='id') + info = data[resources] + if len(info) == 1: + return info[0]['id'] + msg = "No %s with a name or ID of '%s' exists." % (resource, name_or_id) + raise exceptions.CommandError(msg) diff --git a/openstackclient/network/v2_0/__init__.py b/openstackclient/network/v2_0/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstackclient/network/v2_0/network.py b/openstackclient/network/v2_0/network.py new file mode 100644 index 0000000000..c0c25e710d --- /dev/null +++ b/openstackclient/network/v2_0/network.py @@ -0,0 +1,236 @@ +# 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. +# + +"""Network action implementations""" + +import logging +import six + +from cliff import command +from cliff import lister +from cliff import show + +from openstackclient.common import exceptions +from openstackclient.common import utils +from openstackclient.network import common + + +def filters(data): + if 'subnets' in data: + data['subnets'] = utils.format_list(data['subnets']) + return data + + +class CreateNetwork(show.ShowOne): + """Create a network""" + + log = logging.getLogger(__name__ + '.CreateNetwork') + + def get_parser(self, prog_name): + parser = super(CreateNetwork, self).get_parser(prog_name) + parser.add_argument( + 'name', metavar='', + help='Name of network to create') + admin_group = parser.add_mutually_exclusive_group() + admin_group.add_argument( + '--admin-state-up', + dest='admin_state', action='store_true', + default=True, help='Set Admin State Up') + admin_group.add_argument( + '--admin-state-down', + dest='admin_state', action='store_false', + help='Set Admin State Down') + share_group = parser.add_mutually_exclusive_group() + share_group.add_argument( + '--share', + dest='shared', action='store_true', + default=None, + help='Share the network across tenants') + share_group.add_argument( + '--no-share', + dest='shared', action='store_false', + help='Do not share the network across tenants') + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + client = self.app.client_manager.network + body = self.get_body(parsed_args) + create_method = getattr(client, "create_network") + data = create_method(body)['network'] + if data: + data = filters(data) + else: + data = {'': ''} + return zip(*sorted(six.iteritems(data))) + + def get_body(self, parsed_args): + body = {'name': str(parsed_args.name), + 'admin_state_up': parsed_args.admin_state} + if parsed_args.shared is not None: + body['shared'] = parsed_args.shared + return {'network': body} + + +class DeleteNetwork(command.Command): + + log = logging.getLogger(__name__ + '.DeleteNetwork') + + def get_parser(self, prog_name): + parser = super(DeleteNetwork, self).get_parser(prog_name) + parser.add_argument( + 'identifier', + metavar="", + help=("Name or identifier of network to delete") + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + client = self.app.client_manager.network + _id = common.find(client, 'network', 'networks', + parsed_args.identifier) + delete_method = getattr(client, "delete_network") + delete_method(_id) + return + + +class ListNetwork(lister.Lister): + """List networks""" + + log = logging.getLogger(__name__ + '.ListNetwork') + + def get_parser(self, prog_name): + parser = super(ListNetwork, self).get_parser(prog_name) + parser.add_argument( + '--external', + action='store_true', + default=False, + help='List external networks', + ) + parser.add_argument( + '--dhcp', + help='ID of the DHCP agent') + parser.add_argument( + '--long', + action='store_true', + default=False, + help='Long listing', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + client = self.app.client_manager.network + if parsed_args.dhcp: + list_method = getattr(client, 'list_networks_on_dhcp_agent') + resources = 'networks_on_dhcp_agent' + report_filter = {'dhcp_agent': parsed_args.dhcp} + data = list_method(**report_filter)[resources] + else: + list_method = getattr(client, "list_networks") + report_filter = {} + if parsed_args.external: + report_filter = {'router:external': True} + data = list_method(**report_filter)['networks'] + columns = len(data) > 0 and sorted(data[0].keys()) or [] + if parsed_args.columns: + list_columns = parsed_args.columns + else: + list_columns = ['id', 'name', 'subnets'] + if not parsed_args.long and not parsed_args.dhcp: + columns = [x for x in list_columns if x in columns] + formatters = {'subnets': utils.format_list} + return (columns, + (utils.get_dict_properties(s, columns, formatters=formatters) + for s in data)) + + +class SetNetwork(command.Command): + + log = logging.getLogger(__name__ + '.SetNetwork') + + def get_parser(self, prog_name): + parser = super(SetNetwork, self).get_parser(prog_name) + parser.add_argument( + 'identifier', + metavar="", + help=("Name or identifier of network to set") + ) + admin_group = parser.add_mutually_exclusive_group() + admin_group.add_argument( + '--admin-state-up', + dest='admin_state', action='store_true', + default=None, + help='Set Admin State Up') + admin_group.add_argument( + '--admin-state-down', + dest='admin_state', action='store_false', + help='Set Admin State Down') + parser.add_argument( + '--name', + metavar='', + help='New name for the network') + share_group = parser.add_mutually_exclusive_group() + share_group.add_argument( + '--share', + dest='shared', action='store_true', + default=None, + help='Share the network across tenants') + share_group.add_argument( + '--no-share', + dest='shared', action='store_false', + help='Do not share the network across tenants') + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + client = self.app.client_manager.network + _id = common.find(client, 'network', 'networks', + parsed_args.identifier) + body = {} + if parsed_args.name is not None: + body['name'] = str(parsed_args.name) + if parsed_args.admin_state is not None: + body['admin_state_up'] = parsed_args.admin_state + if parsed_args.shared is not None: + body['shared'] = parsed_args.shared + if body == {}: + raise exceptions.CommandError("Nothing specified to be set") + update_method = getattr(client, "update_network") + update_method(_id, {'network': body}) + return + + +class ShowNetwork(show.ShowOne): + + log = logging.getLogger(__name__ + '.ShowNetwork') + + def get_parser(self, prog_name): + parser = super(ShowNetwork, self).get_parser(prog_name) + parser.add_argument( + 'identifier', + metavar="", + help=("Name or identifier of network to show") + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + client = self.app.client_manager.network + _id = common.find(client, 'network', 'networks', + parsed_args.identifier) + show_method = getattr(client, "show_network") + data = show_method(_id)['network'] + data = filters(data) + return zip(*sorted(six.iteritems(data))) diff --git a/openstackclient/tests/fakes.py b/openstackclient/tests/fakes.py index 4c50c0be88..fb27ef94ac 100644 --- a/openstackclient/tests/fakes.py +++ b/openstackclient/tests/fakes.py @@ -52,6 +52,7 @@ def __init__(self): self.image = None self.object = None self.volume = None + self.network = None self.auth_ref = None diff --git a/openstackclient/tests/network/__init__.py b/openstackclient/tests/network/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstackclient/tests/network/common.py b/openstackclient/tests/network/common.py new file mode 100644 index 0000000000..9c5a5ce932 --- /dev/null +++ b/openstackclient/tests/network/common.py @@ -0,0 +1,52 @@ +# 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 openstackclient.tests import utils + + +class TestNetworkBase(utils.TestCommand): + def setUp(self): + super(TestNetworkBase, self).setUp() + self.app = mock.Mock(name='app') + self.app.client_manager = mock.Mock(name='client_manager') + self.namespace = argparse.Namespace() + + given_show_options = [ + '-f', + 'shell', + '-c', + 'id', + '--prefix', + 'TST', + ] + then_show_options = [ + ('formatter', 'shell'), + ('columns', ['id']), + ('prefix', 'TST'), + ] + given_list_options = [ + '-f', + 'csv', + '-c', + 'id', + '--quote', + 'all', + ] + then_list_options = [ + ('formatter', 'csv'), + ('columns', ['id']), + ('quote_mode', 'all'), + ] diff --git a/openstackclient/tests/network/v2_0/__init__.py b/openstackclient/tests/network/v2_0/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstackclient/tests/network/v2_0/test_network.py b/openstackclient/tests/network/v2_0/test_network.py new file mode 100644 index 0000000000..ef7d24ee45 --- /dev/null +++ b/openstackclient/tests/network/v2_0/test_network.py @@ -0,0 +1,319 @@ +# 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 copy +import mock + +from openstackclient.common import exceptions +from openstackclient.network.v2_0 import network +from openstackclient.tests.network import common + +RESOURCE = 'network' +RESOURCES = 'networks' +FAKE_ID = 'iditty' +FAKE_NAME = 'noo' +RECORD = { + 'id': FAKE_ID, + 'name': FAKE_NAME, + 'router:external': True, + 'subnets': ['a', 'b'], +} +COLUMNS = ['id', 'name', 'subnets'] +RESPONSE = {RESOURCE: RECORD} +FILTERED = [('id', 'name', 'router:external', 'subnets'), + (FAKE_ID, FAKE_NAME, True, 'a, b')] + + +class TestCreateNetwork(common.TestNetworkBase): + def test_create_no_options(self): + arglist = [ + FAKE_NAME, + ] + verifylist = [ + ('name', FAKE_NAME), + ('admin_state', True), + ('shared', None), + ] + mocker = mock.Mock(return_value=copy.deepcopy(RESPONSE)) + self.app.client_manager.network.create_network = mocker + cmd = network.CreateNetwork(self.app, self.namespace) + + parsed_args = self.check_parser(cmd, arglist, verifylist) + result = cmd.take_action(parsed_args) + + mocker.assert_called_with({ + RESOURCE: { + 'admin_state_up': True, + 'name': FAKE_NAME, + } + }) + self.assertEqual(FILTERED, result) + + def test_create_all_options(self): + arglist = [ + "--admin-state-down", + "--share", + FAKE_NAME, + ] + self.given_show_options + verifylist = [ + ('admin_state', False), + ('shared', True), + ('name', FAKE_NAME), + ] + self.then_show_options + mocker = mock.Mock(return_value=copy.deepcopy(RESPONSE)) + self.app.client_manager.network.create_network = mocker + cmd = network.CreateNetwork(self.app, self.namespace) + + parsed_args = self.check_parser(cmd, arglist, verifylist) + result = cmd.take_action(parsed_args) + + mocker.assert_called_with({ + RESOURCE: { + 'admin_state_up': False, + 'name': FAKE_NAME, + 'shared': True, + } + }) + self.assertEqual(FILTERED, result) + + def test_create_other_options(self): + arglist = [ + "--admin-state-up", + "--no-share", + FAKE_NAME, + ] + verifylist = [ + ('admin_state', True), + ('shared', False), + ('name', FAKE_NAME), + ] + mocker = mock.Mock(return_value=copy.deepcopy(RESPONSE)) + self.app.client_manager.network.create_network = mocker + cmd = network.CreateNetwork(self.app, self.namespace) + + parsed_args = self.check_parser(cmd, arglist, verifylist) + result = cmd.take_action(parsed_args) + + mocker.assert_called_with({ + RESOURCE: { + 'admin_state_up': True, + 'name': FAKE_NAME, + 'shared': False, + } + }) + self.assertEqual(FILTERED, result) + + +class TestDeleteNetwork(common.TestNetworkBase): + def test_delete(self): + arglist = [ + FAKE_NAME, + ] + verifylist = [ + ('identifier', FAKE_NAME), + ] + lister = mock.Mock(return_value={RESOURCES: [RECORD]}) + self.app.client_manager.network.list_networks = lister + mocker = mock.Mock(return_value=None) + self.app.client_manager.network.delete_network = mocker + cmd = network.DeleteNetwork(self.app, self.namespace) + + parsed_args = self.check_parser(cmd, arglist, verifylist) + result = cmd.take_action(parsed_args) + + mocker.assert_called_with(FAKE_ID) + self.assertEqual(None, result) + + +class TestListNetwork(common.TestNetworkBase): + def test_list_no_options(self): + arglist = [] + verifylist = [ + ('long', False), + ('dhcp', None), + ('external', False), + ] + lister = mock.Mock(return_value={RESOURCES: [RECORD]}) + self.app.client_manager.network.list_networks = lister + cmd = network.ListNetwork(self.app, self.namespace) + + parsed_args = self.check_parser(cmd, arglist, verifylist) + result = cmd.take_action(parsed_args) + + lister.assert_called_with() + self.assertEqual(COLUMNS, result[0]) + self.assertEqual((FAKE_ID, FAKE_NAME, 'a, b'), next(result[1])) + self.assertRaises(StopIteration, next, result[1]) + + def test_list_long(self): + arglist = ['--long'] + verifylist = [ + ('long', True), + ('dhcp', None), + ('external', False), + ] + lister = mock.Mock(return_value={RESOURCES: [RECORD]}) + self.app.client_manager.network.list_networks = lister + cmd = network.ListNetwork(self.app, self.namespace) + + parsed_args = self.check_parser(cmd, arglist, verifylist) + result = cmd.take_action(parsed_args) + + lister.assert_called_with() + headings = ['id', 'name', 'router:external', 'subnets'] + self.assertEqual(headings, result[0]) + data = (FAKE_ID, FAKE_NAME, True, 'a, b') + self.assertEqual(data, next(result[1])) + self.assertRaises(StopIteration, next, result[1]) + + def test_list_dhcp(self): + arglist = [ + '--dhcp', + 'dhcpid', + ] + self.given_list_options + verifylist = [ + ('dhcp', 'dhcpid'), + ] + self.then_list_options + fake_dhcp_data = [{'id': '1'}, {'id': '2'}] + fake_dhcp_response = {'networks_on_dhcp_agent': fake_dhcp_data} + lister = mock.Mock(return_value=fake_dhcp_response) + netty = self.app.client_manager.network + netty.list_networks_on_dhcp_agent = lister + cmd = network.ListNetwork(self.app, self.namespace) + + parsed_args = self.check_parser(cmd, arglist, verifylist) + result = cmd.take_action(parsed_args) + + lister.assert_called_with(dhcp_agent='dhcpid') + self.assertEqual(['id'], result[0]) + self.assertEqual(('1',), next(result[1])) + self.assertEqual(('2',), next(result[1])) + self.assertRaises(StopIteration, next, result[1]) + + def test_list_external(self): + arglist = ['--external', '-c', 'id'] + verifylist = [('external', True)] + lister = mock.Mock(return_value={RESOURCES: [RECORD]}) + self.app.client_manager.network.list_networks = lister + cmd = network.ListNetwork(self.app, self.namespace) + + parsed_args = self.check_parser(cmd, arglist, verifylist) + result = cmd.take_action(parsed_args) + + lister.assert_called_with(**{'router:external': True}) + self.assertEqual(['id'], result[0]) + self.assertEqual((FAKE_ID,), next(result[1])) + self.assertRaises(StopIteration, next, result[1]) + + +class TestSetNetwork(common.TestNetworkBase): + def test_set_this(self): + arglist = [ + FAKE_NAME, + '--admin-state-up', + '--name', 'noob', + '--share', + ] + verifylist = [ + ('identifier', FAKE_NAME), + ('admin_state', True), + ('name', 'noob'), + ('shared', True), + ] + lister = mock.Mock(return_value={RESOURCES: [RECORD]}) + self.app.client_manager.network.list_networks = lister + mocker = mock.Mock(return_value=None) + self.app.client_manager.network.update_network = mocker + cmd = network.SetNetwork(self.app, self.namespace) + + parsed_args = self.check_parser(cmd, arglist, verifylist) + result = cmd.take_action(parsed_args) + + exp = {'admin_state_up': True, 'name': 'noob', 'shared': True} + exp_record = {RESOURCE: exp} + mocker.assert_called_with(FAKE_ID, exp_record) + self.assertEqual(None, result) + + def test_set_that(self): + arglist = [ + FAKE_NAME, + '--admin-state-down', + '--no-share', + ] + verifylist = [ + ('identifier', FAKE_NAME), + ('admin_state', False), + ('shared', False), + ] + lister = mock.Mock(return_value={RESOURCES: [RECORD]}) + self.app.client_manager.network.list_networks = lister + mocker = mock.Mock(return_value=None) + self.app.client_manager.network.update_network = mocker + cmd = network.SetNetwork(self.app, self.namespace) + + parsed_args = self.check_parser(cmd, arglist, verifylist) + result = cmd.take_action(parsed_args) + + exp = {'admin_state_up': False, 'shared': False} + exp_record = {RESOURCE: exp} + mocker.assert_called_with(FAKE_ID, exp_record) + self.assertEqual(None, result) + + def test_set_nothing(self): + arglist = [FAKE_NAME, ] + verifylist = [('identifier', FAKE_NAME), ] + lister = mock.Mock(return_value={RESOURCES: [RECORD]}) + self.app.client_manager.network.list_networks = lister + mocker = mock.Mock(return_value=None) + self.app.client_manager.network.update_network = mocker + cmd = network.SetNetwork(self.app, self.namespace) + + parsed_args = self.check_parser(cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, cmd.take_action, + parsed_args) + + +class TestShowNetwork(common.TestNetworkBase): + def test_show_no_options(self): + arglist = [ + FAKE_NAME, + ] + verifylist = [ + ('identifier', FAKE_NAME), + ] + lister = mock.Mock(return_value={RESOURCES: [RECORD]}) + self.app.client_manager.network.list_networks = lister + mocker = mock.Mock(return_value=copy.deepcopy(RESPONSE)) + self.app.client_manager.network.show_network = mocker + cmd = network.ShowNetwork(self.app, self.namespace) + + parsed_args = self.check_parser(cmd, arglist, verifylist) + result = cmd.take_action(parsed_args) + + mocker.assert_called_with(FAKE_ID) + self.assertEqual(FILTERED, result) + + def test_show_all_options(self): + arglist = [FAKE_NAME] + self.given_show_options + verifylist = [('identifier', FAKE_NAME)] + self.then_show_options + lister = mock.Mock(return_value={RESOURCES: [RECORD]}) + self.app.client_manager.network.list_networks = lister + mocker = mock.Mock(return_value=copy.deepcopy(RESPONSE)) + self.app.client_manager.network.show_network = mocker + cmd = network.ShowNetwork(self.app, self.namespace) + + parsed_args = self.check_parser(cmd, arglist, verifylist) + result = cmd.take_action(parsed_args) + + mocker.assert_called_with(FAKE_ID) + self.assertEqual(FILTERED, result) diff --git a/openstackclient/tests/test_shell.py b/openstackclient/tests/test_shell.py index 9253f701fe..2ee8503a97 100644 --- a/openstackclient/tests/test_shell.py +++ b/openstackclient/tests/test_shell.py @@ -39,11 +39,13 @@ DEFAULT_IDENTITY_API_VERSION = "2.0" DEFAULT_IMAGE_API_VERSION = "v2" DEFAULT_VOLUME_API_VERSION = "1" +DEFAULT_NETWORK_API_VERSION = "2.0" LIB_COMPUTE_API_VERSION = "2" LIB_IDENTITY_API_VERSION = "2.0" LIB_IMAGE_API_VERSION = "1" LIB_VOLUME_API_VERSION = "1" +LIB_NETWORK_API_VERSION = "2.0" def make_shell(): @@ -128,6 +130,8 @@ def _assert_cli(self, cmd_options, default_args): default_args["image_api_version"]) self.assertEqual(_shell.options.os_volume_api_version, default_args["volume_api_version"]) + self.assertEqual(_shell.options.os_network_api_version, + default_args["network_api_version"]) class TestShellHelp(TestShell): @@ -455,6 +459,7 @@ def setUp(self): "OS_IDENTITY_API_VERSION": DEFAULT_IDENTITY_API_VERSION, "OS_IMAGE_API_VERSION": DEFAULT_IMAGE_API_VERSION, "OS_VOLUME_API_VERSION": DEFAULT_VOLUME_API_VERSION, + "OS_NETWORK_API_VERSION": DEFAULT_NETWORK_API_VERSION, } self.orig_env, os.environ = os.environ, env.copy() @@ -475,7 +480,8 @@ def test_default_env(self): "compute_api_version": DEFAULT_COMPUTE_API_VERSION, "identity_api_version": DEFAULT_IDENTITY_API_VERSION, "image_api_version": DEFAULT_IMAGE_API_VERSION, - "volume_api_version": DEFAULT_VOLUME_API_VERSION + "volume_api_version": DEFAULT_VOLUME_API_VERSION, + "network_api_version": DEFAULT_NETWORK_API_VERSION, } self._assert_cli(flag, kwargs) @@ -486,6 +492,7 @@ def test_empty_env(self): "compute_api_version": LIB_COMPUTE_API_VERSION, "identity_api_version": LIB_IDENTITY_API_VERSION, "image_api_version": LIB_IMAGE_API_VERSION, - "volume_api_version": LIB_VOLUME_API_VERSION + "volume_api_version": LIB_VOLUME_API_VERSION, + "network_api_version": LIB_NETWORK_API_VERSION } self._assert_cli(flag, kwargs) diff --git a/requirements.txt b/requirements.txt index e0f4767769..8ab9089f18 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,5 +6,6 @@ python-glanceclient>=0.9.0 python-keystoneclient>=0.9.0 python-novaclient>=2.17.0 python-cinderclient>=1.0.6 +python-neutronclient>=2.3.4,<3 requests>=1.1 six>=1.7.0 diff --git a/setup.cfg b/setup.cfg index 571a41fbcd..8e4e414303 100644 --- a/setup.cfg +++ b/setup.cfg @@ -33,6 +33,7 @@ openstack.cli.extension = image = openstackclient.image.client object_store = openstackclient.object.client volume = openstackclient.volume.client + network = openstackclient.network.client openstack.common = limits_show = openstackclient.common.limits:ShowLimits @@ -289,6 +290,13 @@ openstack.volume.v1 = volume_type_set = openstackclient.volume.v1.type:SetVolumeType volume_type_unset = openstackclient.volume.v1.type:UnsetVolumeType +openstack.network.v2_0 = + network_create = openstackclient.network.v2_0.network:CreateNetwork + network_delete = openstackclient.network.v2_0.network:DeleteNetwork + network_list = openstackclient.network.v2_0.network:ListNetwork + network_set = openstackclient.network.v2_0.network:SetNetwork + network_show = openstackclient.network.v2_0.network:ShowNetwork + [build_sphinx] source-dir = doc/source build-dir = doc/build From 86e0cf70cf5c4429fc527377b8ec4f18831173be Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 26 Jun 2014 23:05:30 -0700 Subject: [PATCH 0120/3494] Fixed typos in the identity client Change-Id: I76042110f5a008d4c097862a572448448f92a504 --- openstackclient/identity/v3/role.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index 2aabc00cc2..d8de7c2291 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -209,12 +209,12 @@ def get_parser(self, prog_name): user_or_group.add_argument( '--user', metavar='', - help='Name or ID of user to list roles asssigned to', + help='Name or ID of user to list roles assigned to', ) user_or_group.add_argument( '--group', metavar='', - help='Name or ID of group to list roles asssigned to', + help='Name or ID of group to list roles assigned to', ) return parser From a78d75f290278501b5d8130dc58420952a13e1a1 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 26 Jun 2014 17:06:38 -0500 Subject: [PATCH 0121/3494] Fix PEP8 H405 errors Change-Id: Id9ea03e7d88148f84bffe1b18b5b4315e6123012 --- openstackclient/common/commandmanager.py | 9 ++++++--- openstackclient/common/parseractions.py | 15 ++++++++++++--- openstackclient/shell.py | 5 ++--- openstackclient/tests/utils.py | 5 ++--- tox.ini | 2 +- 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/openstackclient/common/commandmanager.py b/openstackclient/common/commandmanager.py index aa238a23f0..08710c7735 100644 --- a/openstackclient/common/commandmanager.py +++ b/openstackclient/common/commandmanager.py @@ -13,7 +13,7 @@ # under the License. # -"""Modify Cliff's CommandManager""" +"""Modify cliff.CommandManager""" import logging import pkg_resources @@ -25,9 +25,12 @@ class CommandManager(cliff.commandmanager.CommandManager): - """Alters Cliff's default CommandManager behaviour to load additional - command groups after initialization. + """Add additional functionality to cliff.CommandManager + + Load additional command groups after initialization + Add *_command_group() methods """ + def __init__(self, namespace, convert_underscores=True): self.group_list = [] super(CommandManager, self).__init__(namespace, convert_underscores) diff --git a/openstackclient/common/parseractions.py b/openstackclient/common/parseractions.py index 644472d88e..8f6008e26b 100644 --- a/openstackclient/common/parseractions.py +++ b/openstackclient/common/parseractions.py @@ -19,9 +19,11 @@ class KeyValueAction(argparse.Action): - """A custom action to parse arguments as key=value pairs. - Ensures that dest is a dict + """A custom action to parse arguments as key=value pairs + + Ensures that ``dest`` is a dict """ + def __call__(self, parser, namespace, values, option_string=None): # Make sure we have an empty dict rather than None if getattr(namespace, self.dest, None) is None: @@ -35,7 +37,14 @@ def __call__(self, parser, namespace, values, option_string=None): class RangeAction(argparse.Action): - """A custom action to parse a single value or a range of values.""" + """A custom action to parse a single value or a range of values + + Parses single integer values or a range of integer values delimited + by a colon and returns a tuple of integers: + '4' sets ``dest`` to (4, 4) + '6:9' sets ``dest`` to (6, 9) + """ + def __call__(self, parser, namespace, values, option_string=None): range = values.split(':') if len(range) == 0: diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 67eaca55e6..6aae1a682e 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -330,9 +330,8 @@ def build_option_parser(self, description, version): return parser def authenticate_user(self): - """Make sure the user has provided all of the authentication - info we need. - """ + """Verify the required authentication credentials are present""" + self.log.debug('validating authentication options') if self.options.os_token or self.options.os_url: # Token flow auth takes priority diff --git a/openstackclient/tests/utils.py b/openstackclient/tests/utils.py index ff7d8a336d..48385d138a 100644 --- a/openstackclient/tests/utils.py +++ b/openstackclient/tests/utils.py @@ -41,9 +41,8 @@ def setUp(self): if tuple(sys.version_info)[0:2] < (2, 7): def assertIsInstance(self, obj, cls, msg=None): - """Same as self.assertTrue(isinstance(obj, cls)), with a nicer - default message - """ + """self.assertTrue(isinstance(obj, cls)), with a nicer message""" + if not isinstance(obj, cls): standardMsg = '%s is not an instance of %r' % (obj, cls) self.fail(self._formatMessage(msg, standardMsg)) diff --git a/tox.ini b/tox.ini index c646457ff7..95f00cdf8d 100644 --- a/tox.ini +++ b/tox.ini @@ -28,6 +28,6 @@ commands= python setup.py build_sphinx [flake8] -ignore = E126,E202,W602,H302,H402,E265,H305,H307,H405,H904 +ignore = E126,E202,W602,H302,H402,E265,H305,H307,H904 show-source = True exclude = .venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,tools From 11f3654f6e29c8d66a0ef550963056b70bc100d3 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 26 Jun 2014 18:09:24 -0500 Subject: [PATCH 0122/3494] Fix PEP8 E265 errors Change-Id: Ieb9a9af1da27d3935d1a4d3cfb61b0ccb03d099a --- openstackclient/common/restapi.py | 10 ---------- openstackclient/compute/v2/server.py | 2 +- openstackclient/tests/common/test_restapi.py | 4 ---- openstackclient/tests/identity/v3/fakes.py | 3 ++- openstackclient/tests/volume/v1/test_volume.py | 2 -- tox.ini | 2 +- 6 files changed, 4 insertions(+), 19 deletions(-) diff --git a/openstackclient/common/restapi.py b/openstackclient/common/restapi.py index 1bb64fae03..f20ad23dd2 100644 --- a/openstackclient/common/restapi.py +++ b/openstackclient/common/restapi.py @@ -251,16 +251,6 @@ def list(self, url, data=None, response_key=None, **kwargs): else: return response.json() - ###hack this for keystone!!! - #data = body[response_key] - # NOTE(ja): keystone returns values as list as {'values': [ ... ]} - # unlike other services which just return the list... - #if isinstance(data, dict): - # try: - # data = data['values'] - # except KeyError: - # pass - def set(self, url, data=None, response_key=None, **kwargs): """Update an object via a PUT request diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index f75444f2f1..2dcc7ae9a0 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -693,7 +693,7 @@ def take_action(self, parsed_args): if utils.wait_for_status( compute_client.servers.get, server.id, - #callback=_show_progress, + callback=_show_progress, ): sys.stdout.write('Complete\n') else: diff --git a/openstackclient/tests/common/test_restapi.py b/openstackclient/tests/common/test_restapi.py index 291818c108..d4fe2d3db1 100644 --- a/openstackclient/tests/common/test_restapi.py +++ b/openstackclient/tests/common/test_restapi.py @@ -137,10 +137,6 @@ def test_request_get_auth(self, session_mock): user_agent=fake_user_agent, ) gopher = api.request('GET', fake_url) - #session_mock.return_value.headers.setdefault.assert_called_with( - # 'X-Auth-Token', - # fake_auth, - #) session_mock.return_value.request.assert_called_with( 'GET', fake_url, diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index 711a423d25..8143409df5 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -114,7 +114,8 @@ 'description': idp_description } -#Assignments +# Assignments + ASSIGNMENT_WITH_PROJECT_ID_AND_USER_ID = { 'scope': {'project': {'id': project_id}}, 'user': {'id': user_id}, diff --git a/openstackclient/tests/volume/v1/test_volume.py b/openstackclient/tests/volume/v1/test_volume.py index d881598af3..cb006b1086 100644 --- a/openstackclient/tests/volume/v1/test_volume.py +++ b/openstackclient/tests/volume/v1/test_volume.py @@ -220,7 +220,6 @@ def test_volume_create_user_project_id(self): None, None, volume_fakes.volume_name, - #volume_fakes.volume_description, None, None, identity_fakes.user_id, @@ -296,7 +295,6 @@ def test_volume_create_user_project_name(self): None, None, volume_fakes.volume_name, - #volume_fakes.volume_description, None, None, identity_fakes.user_id, diff --git a/tox.ini b/tox.ini index 95f00cdf8d..a8bb1be659 100644 --- a/tox.ini +++ b/tox.ini @@ -28,6 +28,6 @@ commands= python setup.py build_sphinx [flake8] -ignore = E126,E202,W602,H302,H402,E265,H305,H307,H904 +ignore = E126,E202,W602,H302,H402,H305,H307,H904 show-source = True exclude = .venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,tools From 4914a8d1072f4aede3b3d4ac57bfb80530536505 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 27 Jun 2014 00:23:42 -0500 Subject: [PATCH 0123/3494] Fix PEP8 E126 and E202 errors Do both as they are all in the same set of files that required major re-formatting. Change-Id: I6e8a8ce19a55105124a33c0e2487fc4b4e06d252 --- .../tests/identity/v3/test_consumer.py | 144 ++++---- .../identity/v3/test_identity_provider.py | 323 +++++++++--------- .../tests/identity/v3/test_oauth.py | 229 +++++++------ tox.ini | 2 +- 4 files changed, 369 insertions(+), 329 deletions(-) diff --git a/openstackclient/tests/identity/v3/test_consumer.py b/openstackclient/tests/identity/v3/test_consumer.py index a1095709d7..dd2f332761 100644 --- a/openstackclient/tests/identity/v3/test_consumer.py +++ b/openstackclient/tests/identity/v3/test_consumer.py @@ -19,76 +19,80 @@ class TestOAuth1(identity_fakes.TestOAuth1): - def setUp(self): - super(TestOAuth1, self).setUp() - identity_client = self.app.client_manager.identity - self.consumers_mock = identity_client.oauth1.consumers - self.consumers_mock.reset_mock() + def setUp(self): + super(TestOAuth1, self).setUp() + identity_client = self.app.client_manager.identity + self.consumers_mock = identity_client.oauth1.consumers + self.consumers_mock.reset_mock() class TestConsumerCreate(TestOAuth1): - def setUp(self): - super(TestConsumerCreate, self).setUp() + def setUp(self): + super(TestConsumerCreate, self).setUp() - self.consumers_mock.create.return_value = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.OAUTH_CONSUMER), - loaded=True) + self.consumers_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.OAUTH_CONSUMER), + loaded=True, + ) - self.cmd = consumer.CreateConsumer(self.app, None) + self.cmd = consumer.CreateConsumer(self.app, None) - def test_create_consumer(self): - arglist = [ - '--description', identity_fakes.consumer_description - ] - verifylist = [ - ('description', identity_fakes.consumer_description) - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + def test_create_consumer(self): + arglist = [ + '--description', identity_fakes.consumer_description, + ] + verifylist = [ + ('description', identity_fakes.consumer_description), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) - self.consumers_mock.create.assert_called_with( - identity_fakes.consumer_description) + self.consumers_mock.create.assert_called_with( + identity_fakes.consumer_description, + ) - collist = ('description', 'id', 'secret') - self.assertEqual(columns, collist) - datalist = ( - identity_fakes.consumer_description, - identity_fakes.consumer_id, - identity_fakes.consumer_secret - ) - self.assertEqual(data, datalist) + collist = ('description', 'id', 'secret') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.consumer_description, + identity_fakes.consumer_id, + identity_fakes.consumer_secret, + ) + self.assertEqual(data, datalist) class TestConsumerDelete(TestOAuth1): - def setUp(self): - super(TestConsumerDelete, self).setUp() + def setUp(self): + super(TestConsumerDelete, self).setUp() - # This is the return value for utils.find_resource() - self.consumers_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.OAUTH_CONSUMER), - loaded=True) + # This is the return value for utils.find_resource() + self.consumers_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.OAUTH_CONSUMER), + loaded=True, + ) - self.consumers_mock.delete.return_value = None - self.cmd = consumer.DeleteConsumer(self.app, None) + self.consumers_mock.delete.return_value = None + self.cmd = consumer.DeleteConsumer(self.app, None) - def test_delete_consumer(self): - arglist = [ - identity_fakes.consumer_id - ] - verifylist = [ - ('consumer', identity_fakes.consumer_id) - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) + def test_delete_consumer(self): + arglist = [ + identity_fakes.consumer_id, + ] + verifylist = [ + ('consumer', identity_fakes.consumer_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.run(parsed_args) - self.assertEqual(result, 0) + result = self.cmd.run(parsed_args) + self.assertEqual(result, 0) - self.consumers_mock.delete.assert_called_with( - identity_fakes.consumer_id, - ) + self.consumers_mock.delete.assert_called_with( + identity_fakes.consumer_id, + ) class TestConsumerList(TestOAuth1): @@ -125,7 +129,7 @@ def test_consumer_list(self): self.assertEqual(columns, collist) datalist = (( identity_fakes.consumer_id, - identity_fakes.consumer_description + identity_fakes.consumer_description, ), ) self.assertEqual(tuple(data), datalist) @@ -138,29 +142,33 @@ def setUp(self): consumer_no_secret = copy.deepcopy(identity_fakes.OAUTH_CONSUMER) del consumer_no_secret['secret'] self.consumers_mock.get.return_value = fakes.FakeResource( - None, consumer_no_secret, loaded=True) + None, + consumer_no_secret, + loaded=True, + ) # Get the command object to test self.cmd = consumer.ShowConsumer(self.app, None) def test_consumer_show(self): arglist = [ - identity_fakes.consumer_id + identity_fakes.consumer_id, ] verifylist = [ - ('consumer', identity_fakes.consumer_id) + ('consumer', identity_fakes.consumer_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.consumers_mock.get.assert_called_with( - identity_fakes.consumer_id) + identity_fakes.consumer_id, + ) - collist = ('description', 'id' ) + collist = ('description', 'id') self.assertEqual(columns, collist) datalist = ( identity_fakes.consumer_description, - identity_fakes.consumer_id + identity_fakes.consumer_id, ) self.assertEqual(data, datalist) @@ -171,12 +179,18 @@ def setUp(self): super(TestConsumerSet, self).setUp() self.consumers_mock.get.return_value = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.OAUTH_CONSUMER), loaded=True) + None, + copy.deepcopy(identity_fakes.OAUTH_CONSUMER), + loaded=True, + ) consumer_updated = copy.deepcopy(identity_fakes.OAUTH_CONSUMER) consumer_updated['description'] = "consumer new description" self.consumers_mock.update.return_value = fakes.FakeResource( - None, consumer_updated, loaded=True) + None, + consumer_updated, + loaded=True, + ) self.cmd = consumer.SetConsumer(self.app, None) @@ -185,11 +199,11 @@ def test_consumer_update(self): arglist = [ '--description', new_description, - identity_fakes.consumer_id + identity_fakes.consumer_id, ] verifylist = [ ('description', new_description), - ('consumer', identity_fakes.consumer_id) + ('consumer', identity_fakes.consumer_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.run(parsed_args) @@ -197,4 +211,6 @@ def test_consumer_update(self): kwargs = {'description': new_description} self.consumers_mock.update.assert_called_with( - identity_fakes.consumer_id, **kwargs) + identity_fakes.consumer_id, + **kwargs + ) diff --git a/openstackclient/tests/identity/v3/test_identity_provider.py b/openstackclient/tests/identity/v3/test_identity_provider.py index 41015b69ab..cac68f1d48 100644 --- a/openstackclient/tests/identity/v3/test_identity_provider.py +++ b/openstackclient/tests/identity/v3/test_identity_provider.py @@ -21,152 +21,165 @@ class TestIdentityProvider(identity_fakes.TestFederatedIdentity): - def setUp(self): - super(TestIdentityProvider, self).setUp() + def setUp(self): + super(TestIdentityProvider, self).setUp() - self.identity_providers_mock = self.app.client_manager.\ - identity.identity_providers + self.identity_providers_mock = self.app.client_manager.\ + identity.identity_providers - self.identity_providers_mock.reset_mock() + self.identity_providers_mock.reset_mock() class TestIdentityProviderCreate(TestIdentityProvider): - def setUp(self): - super(TestIdentityProviderCreate, self).setUp() - - self.identity_providers_mock.create.return_value = \ - fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.IDENTITY_PROVIDER), - loaded=True - ) - - self.cmd = identity_provider.CreateIdentityProvider( - self.app, None) - - def test_create_identity_provider_no_options(self): - arglist = [ - identity_fakes.idp_id - ] - verifylist = [ - ('identity_provider_id', identity_fakes.idp_id) - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) - - # Set expected values - kwargs = { - 'enabled': True, - 'description': None, - } - - self.identity_providers_mock.create.assert_called_with( - identity_fakes.idp_id, **kwargs) - - collist = ('description', 'enabled', 'id') - self.assertEqual(columns, collist) - datalist = ( - identity_fakes.idp_description, - True, - identity_fakes.idp_id, - ) - self.assertEqual(data, datalist) - - def test_create_identity_provider_description(self): - arglist = ['--description', identity_fakes.idp_description, - identity_fakes.idp_id] - verifylist = [ - ('identity_provider_id', identity_fakes.idp_id), - ('description', identity_fakes.idp_description) - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) - - # Set expected values - kwargs = { - 'description': identity_fakes.idp_description, - 'enabled': True, - } - - self.identity_providers_mock.create.assert_called_with( - identity_fakes.idp_id, **kwargs) - - collist = ('description', 'enabled', 'id') - self.assertEqual(columns, collist) - datalist = ( - identity_fakes.idp_description, True, identity_fakes.idp_id, - ) - self.assertEqual(data, datalist) - - def test_create_identity_provider_disabled(self): - - # Prepare FakeResource object - IDENTITY_PROVIDER = copy.deepcopy(identity_fakes.IDENTITY_PROVIDER) - IDENTITY_PROVIDER['enabled'] = False - IDENTITY_PROVIDER['description'] = None - - self.identity_providers_mock.create.return_value = \ - fakes.FakeResource( - None, - IDENTITY_PROVIDER, - loaded=True - ) - arglist = ['--disable', - identity_fakes.idp_id] - verifylist = [ - ('identity_provider_id', identity_fakes.idp_id), - - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) - - # Set expected values - kwargs = { - 'enabled': False, - 'description': None - } - - self.identity_providers_mock.create.assert_called_with( - identity_fakes.idp_id, **kwargs) - - collist = ('description', 'enabled', 'id') - self.assertEqual(columns, collist) - datalist = ( + def setUp(self): + super(TestIdentityProviderCreate, self).setUp() + + self.identity_providers_mock.create.return_value = \ + fakes.FakeResource( None, - False, - identity_fakes.idp_id, + copy.deepcopy(identity_fakes.IDENTITY_PROVIDER), + loaded=True, ) - self.assertEqual(data, datalist) + self.cmd = identity_provider.CreateIdentityProvider( + self.app, + None, + ) -class TestIdentityProviderDelete(TestIdentityProvider): + def test_create_identity_provider_no_options(self): + arglist = [ + identity_fakes.idp_id, + ] + verifylist = [ + ('identity_provider_id', identity_fakes.idp_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': True, + 'description': None, + } + + self.identity_providers_mock.create.assert_called_with( + identity_fakes.idp_id, + **kwargs + ) - def setUp(self): - super(TestIdentityProviderDelete, self).setUp() + collist = ('description', 'enabled', 'id') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.idp_description, + True, + identity_fakes.idp_id, + ) + self.assertEqual(data, datalist) - # This is the return value for utils.find_resource() - self.identity_providers_mock.get.return_value = fakes.FakeResource( + def test_create_identity_provider_description(self): + arglist = [ + '--description', identity_fakes.idp_description, + identity_fakes.idp_id, + ] + verifylist = [ + ('identity_provider_id', identity_fakes.idp_id), + ('description', identity_fakes.idp_description), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'description': identity_fakes.idp_description, + 'enabled': True, + } + + self.identity_providers_mock.create.assert_called_with( + identity_fakes.idp_id, + **kwargs + ) + + collist = ('description', 'enabled', 'id') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.idp_description, + True, + identity_fakes.idp_id, + ) + self.assertEqual(data, datalist) + + def test_create_identity_provider_disabled(self): + + # Prepare FakeResource object + IDENTITY_PROVIDER = copy.deepcopy(identity_fakes.IDENTITY_PROVIDER) + IDENTITY_PROVIDER['enabled'] = False + IDENTITY_PROVIDER['description'] = None + + self.identity_providers_mock.create.return_value = \ + fakes.FakeResource( None, - copy.deepcopy(identity_fakes.IDENTITY_PROVIDER), - loaded=True) - - self.identity_providers_mock.delete.return_value = None - self.cmd = identity_provider.DeleteIdentityProvider( - self.app, None) - - def test_delete_identity_provider(self): - arglist = [ - identity_fakes.idp_id - ] - verifylist = [ - ('identity_provider', identity_fakes.idp_id) - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) - self.identity_providers_mock.delete.assert_called_with( - identity_fakes.idp_id, + IDENTITY_PROVIDER, + loaded=True, ) + arglist = [ + '--disable', + identity_fakes.idp_id, + ] + verifylist = [ + ('identity_provider_id', identity_fakes.idp_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': False, + 'description': None, + } + + self.identity_providers_mock.create.assert_called_with( + identity_fakes.idp_id, + **kwargs + ) + + collist = ('description', 'enabled', 'id') + self.assertEqual(columns, collist) + datalist = ( + None, + False, + identity_fakes.idp_id, + ) + self.assertEqual(data, datalist) + + +class TestIdentityProviderDelete(TestIdentityProvider): + + def setUp(self): + super(TestIdentityProviderDelete, self).setUp() + + # This is the return value for utils.find_resource() + self.identity_providers_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.IDENTITY_PROVIDER), + loaded=True, + ) + + self.identity_providers_mock.delete.return_value = None + self.cmd = identity_provider.DeleteIdentityProvider(self.app, None) + + def test_delete_identity_provider(self): + arglist = [ + identity_fakes.idp_id, + ] + verifylist = [ + ('identity_provider', identity_fakes.idp_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.identity_providers_mock.delete.assert_called_with( + identity_fakes.idp_id, + ) class TestIdentityProviderList(TestIdentityProvider): @@ -205,8 +218,7 @@ def test_identity_provider_list_no_options(self): datalist = (( identity_fakes.idp_id, True, - identity_fakes.idp_description - + identity_fakes.idp_description, ), ) self.assertEqual(tuple(data), datalist) @@ -219,7 +231,7 @@ def setUp(self): self.identity_providers_mock.get.return_value = fakes.FakeResource( None, copy.deepcopy(identity_fakes.IDENTITY_PROVIDER), - loaded=True + loaded=True, ) # Get the command object to test @@ -227,24 +239,25 @@ def setUp(self): def test_identity_provider_show(self): arglist = [ - identity_fakes.idp_id + identity_fakes.idp_id, ] verifylist = [ - ('identity_provider', identity_fakes.idp_id) + ('identity_provider', identity_fakes.idp_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.identity_providers_mock.get.assert_called_with( - identity_fakes.idp_id) + identity_fakes.idp_id, + ) - collist = ('description', 'enabled', 'id' ) + collist = ('description', 'enabled', 'id') self.assertEqual(columns, collist) datalist = ( identity_fakes.idp_description, True, - identity_fakes.idp_id + identity_fakes.idp_id, ) self.assertEqual(data, datalist) @@ -259,7 +272,6 @@ def test_identity_provider_disable(self): """Disable Identity Provider Set Identity Provider's ``enabled`` attribute to False. - """ def prepare(self): """Prepare fake return objects before the test is executed""" @@ -274,23 +286,27 @@ def prepare(self): prepare(self) arglist = [ - '--disable', identity_fakes.idp_id + '--disable', identity_fakes.idp_id, ] verifylist = [ ('identity_provider', identity_fakes.idp_id), ('enable', False), - ('disable', True) + ('disable', True), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) self.identity_providers_mock.update.assert_called_with( - identity_fakes.idp_id, enabled=False) - collist = ('description', 'enabled', 'id' ) + identity_fakes.idp_id, + enabled=False, + ) + + collist = ('description', 'enabled', 'id') self.assertEqual(columns, collist) datalist = ( identity_fakes.idp_description, False, - identity_fakes.idp_id + identity_fakes.idp_id, ) self.assertEqual(datalist, data) @@ -298,7 +314,6 @@ def test_identity_provider_enable(self): """Enable Identity Provider. Set Identity Provider's ``enabled`` attribute to True. - """ def prepare(self): """Prepare fake return objects before the test is executed""" @@ -311,24 +326,24 @@ def prepare(self): prepare(self) arglist = [ - '--enable', identity_fakes.idp_id + '--enable', identity_fakes.idp_id, ] verifylist = [ ('identity_provider', identity_fakes.idp_id), ('enable', True), - ('disable', False) + ('disable', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.identity_providers_mock.update.assert_called_with( identity_fakes.idp_id, enabled=True) - collist = ('description', 'enabled', 'id' ) + collist = ('description', 'enabled', 'id') self.assertEqual(columns, collist) datalist = ( identity_fakes.idp_description, True, - identity_fakes.idp_id + identity_fakes.idp_id, ) self.assertEqual(data, datalist) @@ -345,18 +360,18 @@ def prepare(self): resources = fakes.FakeResource( None, copy.deepcopy(identity_fakes.IDENTITY_PROVIDER), - loaded=True + loaded=True, ) self.identity_providers_mock.update.return_value = resources prepare(self) arglist = [ - identity_fakes.idp_id + identity_fakes.idp_id, ] verifylist = [ ('identity_provider', identity_fakes.idp_id), ('enable', False), - ('disable', False) + ('disable', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/identity/v3/test_oauth.py b/openstackclient/tests/identity/v3/test_oauth.py index 5a52864584..f796d4766a 100644 --- a/openstackclient/tests/identity/v3/test_oauth.py +++ b/openstackclient/tests/identity/v3/test_oauth.py @@ -19,90 +19,96 @@ class TestOAuth1(identity_fakes.TestOAuth1): - def setUp(self): - super(TestOAuth1, self).setUp() - identity_client = self.app.client_manager.identity - self.access_tokens_mock = identity_client.oauth1.access_tokens - self.access_tokens_mock.reset_mock() - self.request_tokens_mock = identity_client.oauth1.request_tokens - self.request_tokens_mock.reset_mock() + def setUp(self): + super(TestOAuth1, self).setUp() + identity_client = self.app.client_manager.identity + self.access_tokens_mock = identity_client.oauth1.access_tokens + self.access_tokens_mock.reset_mock() + self.request_tokens_mock = identity_client.oauth1.request_tokens + self.request_tokens_mock.reset_mock() class TestRequestTokenCreate(TestOAuth1): - def setUp(self): - super(TestRequestTokenCreate, self).setUp() - - self.request_tokens_mock.create.return_value = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.OAUTH_REQUEST_TOKEN), - loaded=True) - - self.cmd = token.CreateRequestToken(self.app, None) - - def test_create_request_tokens(self): - arglist = [ - '--consumer-key', identity_fakes.consumer_id, - '--consumer-secret', identity_fakes.consumer_secret, - '--project-id', identity_fakes.project_id - ] - verifylist = [ - ('consumer_key', identity_fakes.consumer_id), - ('consumer_secret', identity_fakes.consumer_secret), - ('project_id', identity_fakes.project_id) - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) - - self.request_tokens_mock.create.assert_called_with( - identity_fakes.consumer_id, - identity_fakes.consumer_secret, - identity_fakes.project_id) - - collist = ('expires', 'id', 'key', 'secret') - self.assertEqual(columns, collist) - datalist = ( - identity_fakes.request_token_expires, - identity_fakes.request_token_id, - identity_fakes.request_token_id, - identity_fakes.request_token_secret - ) - self.assertEqual(data, datalist) + def setUp(self): + super(TestRequestTokenCreate, self).setUp() + + self.request_tokens_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.OAUTH_REQUEST_TOKEN), + loaded=True, + ) + + self.cmd = token.CreateRequestToken(self.app, None) + + def test_create_request_tokens(self): + arglist = [ + '--consumer-key', identity_fakes.consumer_id, + '--consumer-secret', identity_fakes.consumer_secret, + '--project-id', identity_fakes.project_id, + ] + verifylist = [ + ('consumer_key', identity_fakes.consumer_id), + ('consumer_secret', identity_fakes.consumer_secret), + ('project_id', identity_fakes.project_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.request_tokens_mock.create.assert_called_with( + identity_fakes.consumer_id, + identity_fakes.consumer_secret, + identity_fakes.project_id, + ) + + collist = ('expires', 'id', 'key', 'secret') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.request_token_expires, + identity_fakes.request_token_id, + identity_fakes.request_token_id, + identity_fakes.request_token_secret, + ) + self.assertEqual(data, datalist) class TestRequestTokenAuthorize(TestOAuth1): - def setUp(self): - super(TestRequestTokenAuthorize, self).setUp() - - self.request_tokens_mock.authorize.return_value = \ - fakes.FakeResource( - None, copy.deepcopy(identity_fakes.OAUTH_VERIFIER), - loaded=True) - - self.cmd = token.AuthorizeRequestToken(self.app, None) - - def test_authorize_request_tokens(self): - arglist = [ - '--request-key', identity_fakes.request_token_id, - '--role-ids', identity_fakes.role_id - ] - verifylist = [ - ('request_key', identity_fakes.request_token_id), - ('role_ids', identity_fakes.role_id) - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) - - self.request_tokens_mock.authorize.assert_called_with( - identity_fakes.request_token_id, - [identity_fakes.role_id]) - - collist = ('oauth_verifier',) - self.assertEqual(columns, collist) - datalist = ( - identity_fakes.oauth_verifier_pin, + def setUp(self): + super(TestRequestTokenAuthorize, self).setUp() + + self.request_tokens_mock.authorize.return_value = \ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.OAUTH_VERIFIER), + loaded=True, ) - self.assertEqual(data, datalist) + + self.cmd = token.AuthorizeRequestToken(self.app, None) + + def test_authorize_request_tokens(self): + arglist = [ + '--request-key', identity_fakes.request_token_id, + '--role-ids', identity_fakes.role_id, + ] + verifylist = [ + ('request_key', identity_fakes.request_token_id), + ('role_ids', identity_fakes.role_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.request_tokens_mock.authorize.assert_called_with( + identity_fakes.request_token_id, + [identity_fakes.role_id], + ) + + collist = ('oauth_verifier',) + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.oauth_verifier_pin, + ) + self.assertEqual(data, datalist) class TestAccessTokenCreate(TestOAuth1): @@ -111,42 +117,45 @@ def setUp(self): super(TestAccessTokenCreate, self).setUp() self.access_tokens_mock.create.return_value = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.OAUTH_ACCESS_TOKEN), - loaded=True) + None, + copy.deepcopy(identity_fakes.OAUTH_ACCESS_TOKEN), + loaded=True, + ) self.cmd = token.CreateAccessToken(self.app, None) def test_create_access_tokens(self): - arglist = [ - '--consumer-key', identity_fakes.consumer_id, - '--consumer-secret', identity_fakes.consumer_secret, - '--request-key', identity_fakes.request_token_id, - '--request-secret', identity_fakes.request_token_secret, - '--verifier', identity_fakes.oauth_verifier_pin - ] - verifylist = [ - ('consumer_key', identity_fakes.consumer_id), - ('consumer_secret', identity_fakes.consumer_secret), - ('request_key', identity_fakes.request_token_id), - ('request_secret', identity_fakes.request_token_secret), - ('verifier', identity_fakes.oauth_verifier_pin) - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) - - self.access_tokens_mock.create.assert_called_with( - identity_fakes.consumer_id, - identity_fakes.consumer_secret, - identity_fakes.request_token_id, - identity_fakes.request_token_secret, - identity_fakes.oauth_verifier_pin) - - collist = ('expires', 'id', 'key', 'secret') - self.assertEqual(columns, collist) - datalist = ( - identity_fakes.access_token_expires, - identity_fakes.access_token_id, - identity_fakes.access_token_id, - identity_fakes.access_token_secret - ) - self.assertEqual(data, datalist) + arglist = [ + '--consumer-key', identity_fakes.consumer_id, + '--consumer-secret', identity_fakes.consumer_secret, + '--request-key', identity_fakes.request_token_id, + '--request-secret', identity_fakes.request_token_secret, + '--verifier', identity_fakes.oauth_verifier_pin, + ] + verifylist = [ + ('consumer_key', identity_fakes.consumer_id), + ('consumer_secret', identity_fakes.consumer_secret), + ('request_key', identity_fakes.request_token_id), + ('request_secret', identity_fakes.request_token_secret), + ('verifier', identity_fakes.oauth_verifier_pin), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.access_tokens_mock.create.assert_called_with( + identity_fakes.consumer_id, + identity_fakes.consumer_secret, + identity_fakes.request_token_id, + identity_fakes.request_token_secret, + identity_fakes.oauth_verifier_pin, + ) + + collist = ('expires', 'id', 'key', 'secret') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.access_token_expires, + identity_fakes.access_token_id, + identity_fakes.access_token_id, + identity_fakes.access_token_secret, + ) + self.assertEqual(data, datalist) diff --git a/tox.ini b/tox.ini index a8bb1be659..520a85cd42 100644 --- a/tox.ini +++ b/tox.ini @@ -28,6 +28,6 @@ commands= python setup.py build_sphinx [flake8] -ignore = E126,E202,W602,H302,H402,H305,H307,H904 +ignore = H302,H305,H307,H402,H904 show-source = True exclude = .venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,tools From 1fca946890be22f51463975c1bf6449b8680c23a Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 26 Jun 2014 18:20:35 -0500 Subject: [PATCH 0124/3494] Rename token classes to match command The token create/delete commands were renamed but not the class names. Rename them to match. Change-Id: Icbf9c0a954ed0332fa4c99e4ee2612bb11f89e3a --- openstackclient/identity/v2_0/token.py | 16 +++++++++------- openstackclient/identity/v3/token.py | 15 ++++++++------- .../tests/identity/v2_0/test_token.py | 16 ++++++++-------- openstackclient/tests/identity/v3/test_token.py | 10 +++++----- setup.cfg | 6 +++--- 5 files changed, 33 insertions(+), 30 deletions(-) diff --git a/openstackclient/identity/v2_0/token.py b/openstackclient/identity/v2_0/token.py index fe1557a59b..01e1b3b26a 100644 --- a/openstackclient/identity/v2_0/token.py +++ b/openstackclient/identity/v2_0/token.py @@ -22,30 +22,31 @@ from cliff import show -class CreateToken(show.ShowOne): - """Issue token command""" +class IssueToken(show.ShowOne): + """Issue new token""" - log = logging.getLogger(__name__ + '.CreateToken') + log = logging.getLogger(__name__ + '.IssueToken') def get_parser(self, prog_name): - parser = super(CreateToken, self).get_parser(prog_name) + parser = super(IssueToken, self).get_parser(prog_name) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity + token = identity_client.service_catalog.get_token() token['project_id'] = token.pop('tenant_id') return zip(*sorted(six.iteritems(token))) -class DeleteToken(command.Command): +class RevokeToken(command.Command): """Revoke token command""" - log = logging.getLogger(__name__ + '.DeleteToken') + log = logging.getLogger(__name__ + '.RevokeToken') def get_parser(self, prog_name): - parser = super(DeleteToken, self).get_parser(prog_name) + parser = super(RevokeToken, self).get_parser(prog_name) parser.add_argument( 'token', metavar='', @@ -56,5 +57,6 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity + identity_client.tokens.delete(parsed_args.token) return diff --git a/openstackclient/identity/v3/token.py b/openstackclient/identity/v3/token.py index cd73d03a2f..52ed439f15 100644 --- a/openstackclient/identity/v3/token.py +++ b/openstackclient/identity/v3/token.py @@ -22,7 +22,7 @@ class AuthorizeRequestToken(show.ShowOne): - """Authorize request token command""" + """Authorize request token""" log = logging.getLogger(__name__ + '.AuthorizeRequestToken') @@ -59,7 +59,7 @@ def take_action(self, parsed_args): class CreateAccessToken(show.ShowOne): - """Create access token command""" + """Create access token""" log = logging.getLogger(__name__ + '.CreateAccessToken') @@ -110,7 +110,7 @@ def take_action(self, parsed_args): class CreateRequestToken(show.ShowOne): - """Create request token command""" + """Create request token""" log = logging.getLogger(__name__ + '.CreateRequestToken') @@ -148,18 +148,19 @@ def take_action(self, parsed_args): return zip(*sorted(six.iteritems(info))) -class CreateToken(show.ShowOne): - """Issue token command""" +class IssueToken(show.ShowOne): + """Issue new token""" - log = logging.getLogger(__name__ + '.CreateToken') + log = logging.getLogger(__name__ + '.IssueToken') def get_parser(self, prog_name): - parser = super(CreateToken, self).get_parser(prog_name) + parser = super(IssueToken, self).get_parser(prog_name) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity + token = identity_client.service_catalog.get_token() if 'tenant_id' in token: token['project_id'] = token.pop('tenant_id') diff --git a/openstackclient/tests/identity/v2_0/test_token.py b/openstackclient/tests/identity/v2_0/test_token.py index e1967537b2..e094ad4ab7 100644 --- a/openstackclient/tests/identity/v2_0/test_token.py +++ b/openstackclient/tests/identity/v2_0/test_token.py @@ -27,15 +27,15 @@ def setUp(self): self.sc_mock.reset_mock() -class TestTokenCreate(TestToken): +class TestTokenIssue(TestToken): def setUp(self): - super(TestTokenCreate, self).setUp() + super(TestTokenIssue, self).setUp() self.sc_mock.get_token.return_value = identity_fakes.TOKEN - self.cmd = token.CreateToken(self.app, None) + self.cmd = token.IssueToken(self.app, None) - def test_token_create(self): + def test_token_issue(self): arglist = [] verifylist = [] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -56,18 +56,18 @@ def test_token_create(self): self.assertEqual(data, datalist) -class TestTokenDelete(TestToken): +class TestTokenRevoke(TestToken): TOKEN = 'fob' def setUp(self): - super(TestTokenDelete, self).setUp() + super(TestTokenRevoke, self).setUp() self.tokens_mock = self.app.client_manager.identity.tokens self.tokens_mock.reset_mock() self.tokens_mock.delete.return_value = True - self.cmd = token.DeleteToken(self.app, None) + self.cmd = token.RevokeToken(self.app, None) - def test_token_create(self): + def test_token_revoke(self): arglist = [self.TOKEN] verifylist = [('token', self.TOKEN)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/identity/v3/test_token.py b/openstackclient/tests/identity/v3/test_token.py index 7e1d1669b9..8888b93186 100644 --- a/openstackclient/tests/identity/v3/test_token.py +++ b/openstackclient/tests/identity/v3/test_token.py @@ -27,14 +27,14 @@ def setUp(self): self.sc_mock.reset_mock() -class TestTokenCreate(TestToken): +class TestTokenIssue(TestToken): def setUp(self): - super(TestTokenCreate, self).setUp() + super(TestTokenIssue, self).setUp() - self.cmd = token.CreateToken(self.app, None) + self.cmd = token.IssueToken(self.app, None) - def test_token_create_with_project_id(self): + def test_token_issue_with_project_id(self): arglist = [] verifylist = [] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -56,7 +56,7 @@ def test_token_create_with_project_id(self): ) self.assertEqual(data, datalist) - def test_token_create_with_domain_id(self): + def test_token_issue_with_domain_id(self): arglist = [] verifylist = [] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/setup.cfg b/setup.cfg index 8e4e414303..4c56104734 100644 --- a/setup.cfg +++ b/setup.cfg @@ -153,8 +153,8 @@ openstack.identity.v2_0 = service_list =openstackclient.identity.v2_0.service:ListService service_show =openstackclient.identity.v2_0.service:ShowService - token_issue =openstackclient.identity.v2_0.token:CreateToken - token_revoke =openstackclient.identity.v2_0.token:DeleteToken + token_issue = openstackclient.identity.v2_0.token:IssueToken + token_revoke = openstackclient.identity.v2_0.token:RevokeToken user_role_list = openstackclient.identity.v2_0.role:ListUserRole @@ -236,7 +236,7 @@ openstack.identity.v3 = service_show = openstackclient.identity.v3.service:ShowService service_set = openstackclient.identity.v3.service:SetService - token_issue = openstackclient.identity.v3.token:CreateToken + token_issue = openstackclient.identity.v3.token:IssueToken user_create = openstackclient.identity.v3.user:CreateUser user_delete = openstackclient.identity.v3.user:DeleteUser From 1d2cd677cf7cd95885e5d2ce1e1c8efbd9050b3f Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 26 Jun 2014 18:26:12 -0500 Subject: [PATCH 0125/3494] Sort/clean setup.cfg Change-Id: I68a1073d7ef4e6610233961c4aba8c4378ee584b --- setup.cfg | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/setup.cfg b/setup.cfg index 4c56104734..da07d3edc5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,15 +31,15 @@ openstack.cli = openstack.cli.extension = compute = openstackclient.compute.client image = openstackclient.image.client + network = openstackclient.network.client object_store = openstackclient.object.client volume = openstackclient.volume.client - network = openstackclient.network.client openstack.common = + extension_list = openstackclient.common.extension:ListExtension limits_show = openstackclient.common.limits:ShowLimits quota_set = openstackclient.common.quota:SetQuota quota_show = openstackclient.common.quota:ShowQuota - extension_list = openstackclient.common.extension:ListExtension openstack.compute.v2 = compute_agent_create = openstackclient.compute.v2.agent:CreateAgent @@ -144,26 +144,26 @@ openstack.identity.v2_0 = role_add = openstackclient.identity.v2_0.role:AddRole role_create = openstackclient.identity.v2_0.role:CreateRole role_delete = openstackclient.identity.v2_0.role:DeleteRole - role_list =openstackclient.identity.v2_0.role:ListRole + role_list = openstackclient.identity.v2_0.role:ListRole role_remove = openstackclient.identity.v2_0.role:RemoveRole - role_show =openstackclient.identity.v2_0.role:ShowRole + role_show = openstackclient.identity.v2_0.role:ShowRole service_create = openstackclient.identity.v2_0.service:CreateService service_delete = openstackclient.identity.v2_0.service:DeleteService - service_list =openstackclient.identity.v2_0.service:ListService - service_show =openstackclient.identity.v2_0.service:ShowService + service_list = openstackclient.identity.v2_0.service:ListService + service_show = openstackclient.identity.v2_0.service:ShowService token_issue = openstackclient.identity.v2_0.token:IssueToken token_revoke = openstackclient.identity.v2_0.token:RevokeToken - user_role_list = openstackclient.identity.v2_0.role:ListUserRole - user_create = openstackclient.identity.v2_0.user:CreateUser user_delete = openstackclient.identity.v2_0.user:DeleteUser user_list = openstackclient.identity.v2_0.user:ListUser user_set = openstackclient.identity.v2_0.user:SetUser user_show = openstackclient.identity.v2_0.user:ShowUser + user_role_list = openstackclient.identity.v2_0.role:ListUserRole + openstack.identity.v3 = access_token_create = openstackclient.identity.v3.token:CreateAccessToken @@ -258,6 +258,13 @@ openstack.image.v2 = image_save = openstackclient.image.v2.image:SaveImage image_show = openstackclient.image.v2.image:ShowImage +openstack.network.v2_0 = + network_create = openstackclient.network.v2_0.network:CreateNetwork + network_delete = openstackclient.network.v2_0.network:DeleteNetwork + network_list = openstackclient.network.v2_0.network:ListNetwork + network_set = openstackclient.network.v2_0.network:SetNetwork + network_show = openstackclient.network.v2_0.network:ShowNetwork + openstack.object_store.v1 = container_list = openstackclient.object.v1.container:ListContainer container_show = openstackclient.object.v1.container:ShowContainer @@ -265,18 +272,18 @@ openstack.object_store.v1 = object_show = openstackclient.object.v1.object:ShowObject openstack.volume.v1 = - snapshot_create = openstackclient.volume.v1.snapshot:CreateSnapshot - snapshot_delete = openstackclient.volume.v1.snapshot:DeleteSnapshot - snapshot_list = openstackclient.volume.v1.snapshot:ListSnapshot - snapshot_set = openstackclient.volume.v1.snapshot:SetSnapshot - snapshot_show = openstackclient.volume.v1.snapshot:ShowSnapshot - backup_create = openstackclient.volume.v1.backup:CreateBackup backup_delete = openstackclient.volume.v1.backup:DeleteBackup backup_list = openstackclient.volume.v1.backup:ListBackup backup_restore = openstackclient.volume.v1.backup:RestoreBackup backup_show = openstackclient.volume.v1.backup:ShowBackup + snapshot_create = openstackclient.volume.v1.snapshot:CreateSnapshot + snapshot_delete = openstackclient.volume.v1.snapshot:DeleteSnapshot + snapshot_list = openstackclient.volume.v1.snapshot:ListSnapshot + snapshot_set = openstackclient.volume.v1.snapshot:SetSnapshot + snapshot_show = openstackclient.volume.v1.snapshot:ShowSnapshot + volume_create = openstackclient.volume.v1.volume:CreateVolume volume_delete = openstackclient.volume.v1.volume:DeleteVolume volume_list = openstackclient.volume.v1.volume:ListVolume @@ -290,13 +297,6 @@ openstack.volume.v1 = volume_type_set = openstackclient.volume.v1.type:SetVolumeType volume_type_unset = openstackclient.volume.v1.type:UnsetVolumeType -openstack.network.v2_0 = - network_create = openstackclient.network.v2_0.network:CreateNetwork - network_delete = openstackclient.network.v2_0.network:DeleteNetwork - network_list = openstackclient.network.v2_0.network:ListNetwork - network_set = openstackclient.network.v2_0.network:SetNetwork - network_show = openstackclient.network.v2_0.network:ShowNetwork - [build_sphinx] source-dir = doc/source build-dir = doc/build From 3bdfef827c4e5f957ab1edca8495375427e210cc Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Sat, 21 Jun 2014 00:38:18 -0400 Subject: [PATCH 0126/3494] Remove backslash usage from a few tests Noticed these in the code, figured we should stick to not using backslashes if possible. Change-Id: I55e5402683141e14df7c2b38883b1f3cc2a6bb6a --- .../identity/v3/test_identity_provider.py | 29 ++++++------------- .../tests/identity/v3/test_oauth.py | 10 ++----- 2 files changed, 12 insertions(+), 27 deletions(-) diff --git a/openstackclient/tests/identity/v3/test_identity_provider.py b/openstackclient/tests/identity/v3/test_identity_provider.py index cac68f1d48..280d922772 100644 --- a/openstackclient/tests/identity/v3/test_identity_provider.py +++ b/openstackclient/tests/identity/v3/test_identity_provider.py @@ -24,9 +24,8 @@ class TestIdentityProvider(identity_fakes.TestFederatedIdentity): def setUp(self): super(TestIdentityProvider, self).setUp() - self.identity_providers_mock = self.app.client_manager.\ - identity.identity_providers - + identity_lib = self.app.client_manager.identity + self.identity_providers_mock = identity_lib.identity_providers self.identity_providers_mock.reset_mock() @@ -35,17 +34,10 @@ class TestIdentityProviderCreate(TestIdentityProvider): def setUp(self): super(TestIdentityProviderCreate, self).setUp() - self.identity_providers_mock.create.return_value = \ - fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.IDENTITY_PROVIDER), - loaded=True, - ) - - self.cmd = identity_provider.CreateIdentityProvider( - self.app, - None, - ) + copied_idp = copy.deepcopy(identity_fakes.IDENTITY_PROVIDER) + resource = fakes.FakeResource(None, copied_idp, loaded=True) + self.identity_providers_mock.create.return_value = resource + self.cmd = identity_provider.CreateIdentityProvider(self.app, None) def test_create_identity_provider_no_options(self): arglist = [ @@ -116,12 +108,9 @@ def test_create_identity_provider_disabled(self): IDENTITY_PROVIDER['enabled'] = False IDENTITY_PROVIDER['description'] = None - self.identity_providers_mock.create.return_value = \ - fakes.FakeResource( - None, - IDENTITY_PROVIDER, - loaded=True, - ) + resource = fakes.FakeResource(None, IDENTITY_PROVIDER, loaded=True) + self.identity_providers_mock.create.return_value = resource + arglist = [ '--disable', identity_fakes.idp_id, diff --git a/openstackclient/tests/identity/v3/test_oauth.py b/openstackclient/tests/identity/v3/test_oauth.py index f796d4766a..15ba04e33f 100644 --- a/openstackclient/tests/identity/v3/test_oauth.py +++ b/openstackclient/tests/identity/v3/test_oauth.py @@ -77,13 +77,9 @@ class TestRequestTokenAuthorize(TestOAuth1): def setUp(self): super(TestRequestTokenAuthorize, self).setUp() - self.request_tokens_mock.authorize.return_value = \ - fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.OAUTH_VERIFIER), - loaded=True, - ) - + copied_verifier = copy.deepcopy(identity_fakes.OAUTH_VERIFIER) + resource = fakes.FakeResource(None, copied_verifier, loaded=True) + self.request_tokens_mock.authorize.return_value = resource self.cmd = token.AuthorizeRequestToken(self.app, None) def test_authorize_request_tokens(self): From 0ab179143943878d53ce37e8412734620d7b7583 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 2 Jul 2014 15:47:14 +0000 Subject: [PATCH 0127/3494] Updated from global requirements Change-Id: I7f7714625a6cfb6bd8e0d313d27e898e178a1efb --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8ab9089f18..0112cde285 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,10 +2,10 @@ pbr>=0.6,!=0.7,<1.0 cliff>=1.6.0 keyring>=2.1 pycrypto>=2.6 -python-glanceclient>=0.9.0 +python-glanceclient>=0.13.1 python-keystoneclient>=0.9.0 python-novaclient>=2.17.0 -python-cinderclient>=1.0.6 -python-neutronclient>=2.3.4,<3 +python-cinderclient>=1.0.7 +python-neutronclient>=2.3.5,<3 requests>=1.1 six>=1.7.0 From 169587ddbd5b67258fe91e6d0428f371b9cf312e Mon Sep 17 00:00:00 2001 From: Cyril Roelandt Date: Thu, 3 Jul 2014 00:27:35 +0200 Subject: [PATCH 0128/3494] Python 3: do not use __builtin__ Use six.moves.builtins instead, this works with both Python 2 and 3. Change-Id: I57e7257d4f06c805f26383e0778ad104d50ea139 --- openstackclient/tests/image/v1/test_image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstackclient/tests/image/v1/test_image.py b/openstackclient/tests/image/v1/test_image.py index b746a5382a..b014482a84 100644 --- a/openstackclient/tests/image/v1/test_image.py +++ b/openstackclient/tests/image/v1/test_image.py @@ -139,7 +139,7 @@ def test_image_reserve_options(self): self.assertEqual(image_fakes.IMAGE_columns, columns) self.assertEqual(image_fakes.IMAGE_data, data) - @mock.patch('__builtin__.open') + @mock.patch('six.moves.builtins.open') def test_image_create_file(self, open_mock): mock_exception = { 'find.side_effect': exceptions.CommandError('x'), From 5672c688d1cc7cdc064510daa6251d39edbf3033 Mon Sep 17 00:00:00 2001 From: Matthieu Huin Date: Mon, 30 Jun 2014 19:12:27 +0200 Subject: [PATCH 0129/3494] trust authentication This patch enables authenticating by using a trust. The trust ID must be set with the parameter --os-trust-id or the env variable OS_TRUST_ID. Trusts are available for the identity v3 API. Co-Authored-By: Florent Flament Change-Id: Iacc389b203bbadda53ca31a7f5a9b8b6e1a1f522 --- doc/source/man/openstack.rst | 14 ++++-- openstackclient/common/clientmanager.py | 4 +- openstackclient/identity/client.py | 2 + openstackclient/shell.py | 28 ++++++++++- openstackclient/tests/test_shell.py | 63 +++++++++++++++++++------ 5 files changed, 89 insertions(+), 22 deletions(-) diff --git a/doc/source/man/openstack.rst b/doc/source/man/openstack.rst index 16f0bc4764..1023c603c6 100644 --- a/doc/source/man/openstack.rst +++ b/doc/source/man/openstack.rst @@ -83,6 +83,8 @@ OPTIONS :option:`--os-XXXX-api-version` Additional API version options will be available depending on the installed API libraries. +:option:`--os-trust-id` + id of the trust to use as a trustee user COMMANDS ======== @@ -181,21 +183,23 @@ The following environment variables can be set to alter the behaviour of :progra :envvar:`OS_CACERT` CA certificate bundle file -:envvar:`OS_COMPUTE_API_VERISON` +:envvar:`OS_COMPUTE_API_VERSION` Compute API version (Default: 2) -:envvar:`OS_IDENTITY_API_VERISON` +:envvar:`OS_IDENTITY_API_VERSION` Identity API version (Default: 2.0) -:envvar:`OS_IMAGE_API_VERISON` +:envvar:`OS_IMAGE_API_VERSION` Image API version (Default: 1) -:envvar:`OS_VOLUME_API_VERISON` +:envvar:`OS_VOLUME_API_VERSION` Volume API version (Default: 1) -:envvar:`OS_XXXX_API_VERISON` +:envvar:`OS_XXXX_API_VERSION` Additional API version options will be available depending on the installed API libraries. +:envvar:`OS_TRUST_ID` + id of the trust to use as a trustee user BUGS ==== diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index 353a0a1988..b310f3acdc 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -48,7 +48,8 @@ def __init__(self, token=None, url=None, auth_url=None, username=None, password=None, user_domain_id=None, user_domain_name=None, project_domain_id=None, project_domain_name=None, - region_name=None, api_version=None, verify=True): + region_name=None, api_version=None, verify=True, + trust_id=None): self._token = token self._url = url self._auth_url = auth_url @@ -64,6 +65,7 @@ def __init__(self, token=None, url=None, auth_url=None, self._project_domain_name = project_domain_name self._region_name = region_name self._api_version = api_version + self._trust_id = trust_id self._service_catalog = None # verify is the Requests-compatible form diff --git a/openstackclient/identity/client.py b/openstackclient/identity/client.py index b58098e90a..32a2c23b79 100644 --- a/openstackclient/identity/client.py +++ b/openstackclient/identity/client.py @@ -43,6 +43,7 @@ def make_client(instance): token=instance._token, cacert=instance._cacert, insecure=instance._insecure, + trust_id=instance._trust_id, ) else: LOG.debug('instantiating identity client: password flow') @@ -61,6 +62,7 @@ def make_client(instance): region_name=instance._region_name, cacert=instance._cacert, insecure=instance._insecure, + trust_id=instance._trust_id, ) instance.auth_ref = client.auth_ref return client diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 6aae1a682e..b1a991848e 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -326,6 +326,13 @@ def build_option_parser(self, description, version): help='Identity API version, default=' + identity_client.DEFAULT_IDENTITY_API_VERSION + ' (Env: OS_IDENTITY_API_VERSION)') + parser.add_argument( + '--os-trust-id', + metavar='', + default=utils.env('OS_TRUST_ID'), + help='Trust ID to use when authenticating. ' + 'This can only be used with Keystone v3 API ' + '(Env: OS_TRUST_ID)') return parser @@ -373,19 +380,35 @@ def authenticate_user(self): if not ((self.options.os_project_id or self.options.os_project_name) or (self.options.os_domain_id - or self.options.os_domain_name)): + or self.options.os_domain_name) or + self.options.os_trust_id): raise exc.CommandError( "You must provide authentication scope as a project " "or a domain via --os-project-id or env[OS_PROJECT_ID], " "--os-project-name or env[OS_PROJECT_NAME], " "--os-domain-id or env[OS_DOMAIN_ID], or" - "--os-domain-name or env[OS_DOMAIN_NAME].") + "--os-domain-name or env[OS_DOMAIN_NAME], or " + "--os-trust-id or env[OS_TRUST_ID].") if not self.options.os_auth_url: raise exc.CommandError( "You must provide an auth url via" " either --os-auth-url or via env[OS_AUTH_URL]") + if (self.options.os_trust_id and + self.options.os_identity_api_version != '3'): + raise exc.CommandError( + "Trusts can only be used with Identity API v3") + + if (self.options.os_trust_id and + ((self.options.os_project_id + or self.options.os_project_name) or + (self.options.os_domain_id + or self.options.os_domain_name))): + raise exc.CommandError( + "Authentication cannot be scoped to multiple targets. " + "Pick one of project, domain or trust.") + self.client_manager = clientmanager.ClientManager( token=self.options.os_token, url=self.options.os_url, @@ -403,6 +426,7 @@ def authenticate_user(self): region_name=self.options.os_region_name, verify=self.verify, api_version=self.api_version, + trust_id=self.options.os_trust_id, ) return diff --git a/openstackclient/tests/test_shell.py b/openstackclient/tests/test_shell.py index 2ee8503a97..dfb8021a51 100644 --- a/openstackclient/tests/test_shell.py +++ b/openstackclient/tests/test_shell.py @@ -104,6 +104,8 @@ def _assert_password_auth(self, cmd_options, default_args): default_args["password"]) self.assertEqual(_shell.options.os_region_name, default_args["region_name"]) + self.assertEqual(_shell.options.os_trust_id, + default_args["trust_id"]) def _assert_token_auth(self, cmd_options, default_args): with mock.patch("openstackclient.shell.OpenStackShell.initialize_app", @@ -181,7 +183,8 @@ def test_only_url_flow(self): "project_domain_name": "", "username": "", "password": "", - "region_name": "" + "region_name": "", + "trust_id": "", } self._assert_password_auth(flag, kwargs) @@ -199,7 +202,8 @@ def test_only_project_id_flow(self): "project_domain_name": "", "username": "", "password": "", - "region_name": "" + "region_name": "", + "trust_id": "", } self._assert_password_auth(flag, kwargs) @@ -217,7 +221,8 @@ def test_only_project_name_flow(self): "project_domain_name": "", "username": "", "password": "", - "region_name": "" + "region_name": "", + "trust_id": "", } self._assert_password_auth(flag, kwargs) @@ -235,7 +240,8 @@ def test_only_tenant_id_flow(self): "project_domain_name": "", "username": "", "password": "", - "region_name": "" + "region_name": "", + "trust_id": "", } self._assert_password_auth(flag, kwargs) @@ -253,7 +259,8 @@ def test_only_tenant_name_flow(self): "project_domain_name": "", "username": "", "password": "", - "region_name": "" + "region_name": "", + "trust_id": "", } self._assert_password_auth(flag, kwargs) @@ -271,7 +278,8 @@ def test_only_domain_id_flow(self): "project_domain_name": "", "username": "", "password": "", - "region_name": "" + "region_name": "", + "trust_id": "", } self._assert_password_auth(flag, kwargs) @@ -289,7 +297,8 @@ def test_only_domain_name_flow(self): "project_domain_name": "", "username": "", "password": "", - "region_name": "" + "region_name": "", + "trust_id": "", } self._assert_password_auth(flag, kwargs) @@ -307,7 +316,8 @@ def test_only_user_domain_id_flow(self): "project_domain_name": "", "username": "", "password": "", - "region_name": "" + "region_name": "", + "trust_id": "", } self._assert_password_auth(flag, kwargs) @@ -325,7 +335,8 @@ def test_only_user_domain_name_flow(self): "project_domain_name": "", "username": "", "password": "", - "region_name": "" + "region_name": "", + "trust_id": "", } self._assert_password_auth(flag, kwargs) @@ -343,7 +354,8 @@ def test_only_project_domain_id_flow(self): "project_domain_name": "", "username": "", "password": "", - "region_name": "" + "region_name": "", + "trust_id": "", } self._assert_password_auth(flag, kwargs) @@ -361,7 +373,8 @@ def test_only_project_domain_name_flow(self): "project_domain_name": DEFAULT_PROJECT_DOMAIN_NAME, "username": "", "password": "", - "region_name": "" + "region_name": "", + "trust_id": "", } self._assert_password_auth(flag, kwargs) @@ -379,7 +392,8 @@ def test_only_username_flow(self): "project_domain_name": "", "username": DEFAULT_USERNAME, "password": "", - "region_name": "" + "region_name": "", + "trust_id": "", } self._assert_password_auth(flag, kwargs) @@ -397,7 +411,8 @@ def test_only_password_flow(self): "project_domain_name": "", "username": "", "password": DEFAULT_PASSWORD, - "region_name": "" + "region_name": "", + "trust_id": "", } self._assert_password_auth(flag, kwargs) @@ -415,7 +430,27 @@ def test_only_region_name_flow(self): "project_domain_name": "", "username": "", "password": "", - "region_name": DEFAULT_REGION_NAME + "region_name": DEFAULT_REGION_NAME, + "trust_id": "", + } + self._assert_password_auth(flag, kwargs) + + def test_only_trust_id_flow(self): + flag = "--os-trust-id " + "1234" + kwargs = { + "auth_url": "", + "project_id": "", + "project_name": "", + "domain_id": "", + "domain_name": "", + "user_domain_id": "", + "user_domain_name": "", + "project_domain_id": "", + "project_domain_name": "", + "username": "", + "password": "", + "region_name": "", + "trust_id": "1234", } self._assert_password_auth(flag, kwargs) From b8f534df011fd3b16a182d25f627876aeecfee07 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Wed, 2 Jul 2014 14:12:44 -0700 Subject: [PATCH 0130/3494] Remove keyring support from openstackclient * The encryption it purports to offer is completely insecure. * It also appears to be broken. Closes-Bug: #1319381 Change-Id: Id15ecfbbfd15f142b14c125bfd85afd5032699ac --- README.rst | 6 +-- doc/source/man/openstack.rst | 6 --- openstackclient/common/openstackkeyring.py | 60 ---------------------- openstackclient/shell.py | 43 ---------------- requirements.txt | 2 - 5 files changed, 1 insertion(+), 116 deletions(-) delete mode 100644 openstackclient/common/openstackkeyring.py diff --git a/README.rst b/README.rst index 6da3d6a592..c892df1c10 100644 --- a/README.rst +++ b/README.rst @@ -79,7 +79,6 @@ The 'password flow' variation is most commonly used:: export OS_PROJECT_NAME= export OS_USERNAME= export OS_PASSWORD= # (optional) - export OS_USE_KEYRING=true # (optional) The corresponding command-line options look very similar:: @@ -87,12 +86,9 @@ The corresponding command-line options look very similar:: --os-project-name --os-username [--os-password ] - [--os-use-keyring] If a password is not provided above (in plaintext), you will be interactively -prompted to provide one securely. If keyring is enabled, the password entered -in the prompt is stored in keyring. From next time, the password is read from -keyring, if it is not provided above (in plaintext). +prompted to provide one securely. The token flow variation for authentication uses an already-acquired token and a URL pointing directly to the service API that presumably was acquired diff --git a/doc/source/man/openstack.rst b/doc/source/man/openstack.rst index 16f0bc4764..6c49ba3874 100644 --- a/doc/source/man/openstack.rst +++ b/doc/source/man/openstack.rst @@ -68,9 +68,6 @@ OPTIONS :option:`--os-default-domain` Default domain ID (Default: 'default') -:option:`--os-use-keyring` - Use keyring to store password (default: False) - :option:`--os-cacert` CA certificate bundle file @@ -175,9 +172,6 @@ The following environment variables can be set to alter the behaviour of :progra :envvar:`OS_DEFAULT_DOMAIN` Default domain ID (Default: ‘default’) -:envvar:`OS_USE_KEYRING` - Use keyring to store password (default: False) - :envvar:`OS_CACERT` CA certificate bundle file diff --git a/openstackclient/common/openstackkeyring.py b/openstackclient/common/openstackkeyring.py deleted file mode 100644 index 30450e80c8..0000000000 --- a/openstackclient/common/openstackkeyring.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright 2011-2013 OpenStack, LLC. -# -# 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. -# - -"""Keyring backend for OpenStack, to store encrypted password in a file.""" - -from Crypto.Cipher import AES - -import keyring -import os - - -KEYRING_FILE = os.path.join(os.path.expanduser('~'), '.openstack-keyring.cfg') - - -class OpenStackKeyring(keyring.backends.file.BaseKeyring): - """OpenStack Keyring to store encrypted password.""" - filename = KEYRING_FILE - - def supported(self): - """Applicable for all platforms, but not recommend.""" - pass - - def _init_crypter(self): - """Initialize the crypter using the class name.""" - block_size = 32 - padding = '0' - - # init the cipher with the class name, up to block_size - password = __name__[block_size:] - password = password + (block_size - len(password) % - block_size) * padding - return AES.new(password, AES.MODE_CFB) - - def encrypt(self, password): - """Encrypt the given password.""" - crypter = self._init_crypter() - return crypter.encrypt(password) - - def decrypt(self, password_encrypted): - """Decrypt the given password.""" - crypter = self._init_crypter() - return crypter.decrypt(password_encrypted) - - -def os_keyring(): - """Initialize the openstack keyring.""" - ring = 'openstackclient.common.openstackkeyring.OpenStackKeyring' - return keyring.core.load_keyring(None, ring) diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 6aae1a682e..e4f3b92e6a 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -31,7 +31,6 @@ from openstackclient.common import clientmanager from openstackclient.common import commandmanager from openstackclient.common import exceptions as exc -from openstackclient.common import openstackkeyring from openstackclient.common import restapi from openstackclient.common import utils from openstackclient.identity import client as identity_client @@ -305,18 +304,6 @@ def build_option_parser(self, description, version): default=env('OS_URL'), help='Defaults to env[OS_URL]') - env_os_keyring = env('OS_USE_KEYRING', default=False) - if type(env_os_keyring) == str: - if env_os_keyring.lower() in ['true', '1']: - env_os_keyring = True - else: - env_os_keyring = False - parser.add_argument('--os-use-keyring', - default=env_os_keyring, - action='store_true', - help='Use keyring to store password, ' - 'default=False (Env: OS_USE_KEYRING)') - parser.add_argument( '--os-identity-api-version', metavar='', @@ -352,14 +339,12 @@ def authenticate_user(self): "You must provide a username via" " either --os-username or env[OS_USERNAME]") - self.get_password_from_keyring() if not self.options.os_password: # No password, if we've got a tty, try prompting for it if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty(): # Check for Ctl-D try: self.options.os_password = getpass.getpass() - self.set_password_in_keyring() except EOFError: pass # No password because we did't have a tty or the @@ -406,34 +391,6 @@ def authenticate_user(self): ) return - def init_keyring_backend(self): - """Initialize openstack backend to use for keyring""" - return openstackkeyring.os_keyring() - - def get_password_from_keyring(self): - """Get password from keyring, if it's set""" - if self.options.os_use_keyring: - service = KEYRING_SERVICE - backend = self.init_keyring_backend() - if not self.options.os_password: - password = backend.get_password(service, - self.options.os_username) - self.options.os_password = password - - def set_password_in_keyring(self): - """Set password in keyring for this user""" - if self.options.os_use_keyring: - service = KEYRING_SERVICE - backend = self.init_keyring_backend() - if self.options.os_password: - password = backend.get_password(service, - self.options.os_username) - # either password is not set in keyring, or it is different - if password != self.options.os_password: - backend.set_password(service, - self.options.os_username, - self.options.os_password) - def initialize_app(self, argv): """Global app init bits: diff --git a/requirements.txt b/requirements.txt index 0112cde285..51715d1d76 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,5 @@ pbr>=0.6,!=0.7,<1.0 cliff>=1.6.0 -keyring>=2.1 -pycrypto>=2.6 python-glanceclient>=0.13.1 python-keystoneclient>=0.9.0 python-novaclient>=2.17.0 From 270c7fe96727cedf81e7f4fe6361672c512fc150 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 3 Jul 2014 18:25:42 -0400 Subject: [PATCH 0131/3494] Add support to list compute extensions Since novaclient has support to list extensions, we should add some of the logic to our list extensions command. Closes-Bug: #1337684 Change-Id: I3074225780142df265a34add03e60c0f7c64c711 --- openstackclient/common/extension.py | 28 +++++++++++++++++++--------- openstackclient/compute/client.py | 7 +++++-- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/openstackclient/common/extension.py b/openstackclient/common/extension.py index a8b1a6b08b..a3f94c09dd 100644 --- a/openstackclient/common/extension.py +++ b/openstackclient/common/extension.py @@ -19,16 +19,14 @@ from cliff import lister -from openstackclient.common import exceptions as exc from openstackclient.common import utils class ListExtension(lister.Lister): """List extension command""" - # TODO(mfisch): add support for volume and compute - # when the underlying APIs support it. Add support - # for network when it's added to openstackclient. + # TODO(mfisch): add support for volume and network + # when the underlying APIs support it. log = logging.getLogger(__name__ + '.ListExtension') @@ -44,6 +42,11 @@ def get_parser(self, prog_name): action='store_true', default=False, help='List extensions for the Identity API') + parser.add_argument( + '--compute', + action='store_true', + default=False, + help='List extensions for the Compute API') return parser def take_action(self, parsed_args): @@ -59,17 +62,24 @@ def take_action(self, parsed_args): # by default we want to show everything, unless the # user specifies one or more of the APIs to show - # for now, only identity is supported - show_all = (not parsed_args.identity) + # for now, only identity and compute are supported. + show_all = (not parsed_args.identity and not parsed_args.compute) if parsed_args.identity or show_all: identity_client = self.app.client_manager.identity try: data += identity_client.extensions.list() except Exception: - raise exc.CommandError( - "Extensions list not supported by" - " identity API") + message = "Extensions list not supported by Identity API" + self.log.warning(message) + + if parsed_args.compute or show_all: + compute_client = self.app.client_manager.compute + try: + data += compute_client.list_extensions.show_all() + except Exception: + message = "Extensions list not supported by Compute API" + self.log.warning(message) return (columns, (utils.get_item_properties( diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py index 3dacee88cf..c87128095f 100644 --- a/openstackclient/compute/client.py +++ b/openstackclient/compute/client.py @@ -15,6 +15,9 @@ import logging +from novaclient import extension +from novaclient.v1_1.contrib import list_extensions + from openstackclient.common import utils LOG = logging.getLogger(__name__) @@ -39,6 +42,7 @@ def make_client(instance): # Set client http_log_debug to True if verbosity level is high enough http_log_debug = utils.get_effective_log_level() <= logging.DEBUG + extensions = [extension.Extension('list_extensions', list_extensions)] client = compute_client( username=instance._username, api_key=instance._password, @@ -49,8 +53,7 @@ def make_client(instance): region_name=instance._region_name, # FIXME(dhellmann): get endpoint_type from option? endpoint_type='publicURL', - # FIXME(dhellmann): add extension discovery - extensions=[], + extensions=extensions, service_type=API_NAME, # FIXME(dhellmann): what is service_name? service_name='', From 1fedd38de6c14fb71529a8ee52390b6edfa837d0 Mon Sep 17 00:00:00 2001 From: Cyril Roelandt Date: Fri, 4 Jul 2014 10:20:37 +0200 Subject: [PATCH 0132/3494] Python 3: remove a useless code to safe_encode() The safe_encode method returns bytes, so we cannot concatenate its output with text strings. This call does not seem needed after all, so let's just remove it. Change-Id: I6c18427559147d4c732ff7daa6d6006e7e5f6365 --- openstackclient/common/utils.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py index 0258f93122..51c3ed4b28 100644 --- a/openstackclient/common/utils.py +++ b/openstackclient/common/utils.py @@ -23,7 +23,6 @@ import time from openstackclient.common import exceptions -from openstackclient.openstack.common import strutils def find_resource(manager, name_or_id): @@ -79,8 +78,7 @@ def format_dict(data): output = "" for s in data: - output = output + s + "='" + \ - strutils.safe_encode(six.text_type(data[s])) + "', " + output = output + s + "='" + six.text_type(data[s]) + "', " return output[:-2] From 9b2e264ada9f98444d5c7f106dd320dde282946c Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 4 Jul 2014 14:27:16 -0400 Subject: [PATCH 0133/3494] Add support to list volume extensions Since cinderclient has support to list extensions, we should add some of the logic to our list extensions command. Change-Id: I7dc7ca325ea9b82194bba6d875e7b8dc1884d77e Closes-Bug: #1337687 --- openstackclient/common/extension.py | 16 +++++++++++++++- openstackclient/volume/client.py | 7 ++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/openstackclient/common/extension.py b/openstackclient/common/extension.py index a3f94c09dd..91ee228be5 100644 --- a/openstackclient/common/extension.py +++ b/openstackclient/common/extension.py @@ -47,6 +47,11 @@ def get_parser(self, prog_name): action='store_true', default=False, help='List extensions for the Compute API') + parser.add_argument( + '--volume', + action='store_true', + default=False, + help='List extensions for the Volume API') return parser def take_action(self, parsed_args): @@ -63,7 +68,8 @@ def take_action(self, parsed_args): # by default we want to show everything, unless the # user specifies one or more of the APIs to show # for now, only identity and compute are supported. - show_all = (not parsed_args.identity and not parsed_args.compute) + show_all = (not parsed_args.identity and not parsed_args.compute + and not parsed_args.volume) if parsed_args.identity or show_all: identity_client = self.app.client_manager.identity @@ -81,6 +87,14 @@ def take_action(self, parsed_args): message = "Extensions list not supported by Compute API" self.log.warning(message) + if parsed_args.volume or show_all: + volume_client = self.app.client_manager.volume + try: + data += volume_client.list_extensions.show_all() + except Exception: + message = "Extensions list not supported by Volume API" + self.log.warning(message) + return (columns, (utils.get_item_properties( s, columns, diff --git a/openstackclient/volume/client.py b/openstackclient/volume/client.py index 9b37b8f53c..98e787d682 100644 --- a/openstackclient/volume/client.py +++ b/openstackclient/volume/client.py @@ -15,8 +15,11 @@ import logging +from cinderclient import extension +from cinderclient.v1.contrib import list_extensions from cinderclient.v1 import volume_snapshots from cinderclient.v1 import volumes + from openstackclient.common import utils # Monkey patch for v1 cinderclient @@ -46,6 +49,7 @@ def make_client(instance): # Set client http_log_debug to True if verbosity level is high enough http_log_debug = utils.get_effective_log_level() <= logging.DEBUG + extensions = [extension.Extension('list_extensions', list_extensions)] client = volume_client( username=instance._username, api_key=instance._password, @@ -54,7 +58,8 @@ def make_client(instance): cacert=instance._cacert, insecure=instance._insecure, region_name=instance._region_name, - http_log_debug=http_log_debug + extensions=extensions, + http_log_debug=http_log_debug, ) # Populate the Cinder client to skip another auth query to Identity From 79488377eb868460dff841d732ac003625505345 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Sun, 6 Jul 2014 10:02:51 -0600 Subject: [PATCH 0134/3494] Catch SystemExit for parse args If you have a test with parse args it fails with no error messages. This change throws an exception. Change-Id: I545aba346620a352fe570d394dbd4d6bd2daa995 --- openstackclient/tests/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openstackclient/tests/utils.py b/openstackclient/tests/utils.py index 48385d138a..307abd7b83 100644 --- a/openstackclient/tests/utils.py +++ b/openstackclient/tests/utils.py @@ -73,7 +73,10 @@ def setUp(self): def check_parser(self, cmd, args, verify_args): cmd_parser = cmd.get_parser('check_parser') - parsed_args = cmd_parser.parse_args(args) + try: + parsed_args = cmd_parser.parse_args(args) + except SystemExit: + raise Exception("Argument parse failed") for av in verify_args: attr, value = av if attr: From b157dc937e95b7334b0c1816b6b165d080fc3be6 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Mon, 7 Jul 2014 05:51:25 -0600 Subject: [PATCH 0135/3494] Move network stuff to v2 instead of v2_0 Rename network stuff v2 Change-Id: Ia9b8feda20dfd35b0f3712b8e2419d0bf5da0acd --- openstackclient/network/client.py | 4 ++-- openstackclient/network/{v2_0 => v2}/__init__.py | 0 openstackclient/network/{v2_0 => v2}/network.py | 0 .../tests/network/{v2_0 => v2}/__init__.py | 0 .../tests/network/{v2_0 => v2}/test_network.py | 2 +- openstackclient/tests/test_shell.py | 4 ++-- setup.cfg | 12 ++++++------ 7 files changed, 11 insertions(+), 11 deletions(-) rename openstackclient/network/{v2_0 => v2}/__init__.py (100%) rename openstackclient/network/{v2_0 => v2}/network.py (100%) rename openstackclient/tests/network/{v2_0 => v2}/__init__.py (100%) rename openstackclient/tests/network/{v2_0 => v2}/test_network.py (99%) diff --git a/openstackclient/network/client.py b/openstackclient/network/client.py index 3c87e135e1..db0b5279d8 100644 --- a/openstackclient/network/client.py +++ b/openstackclient/network/client.py @@ -18,11 +18,11 @@ LOG = logging.getLogger(__name__) -DEFAULT_NETWORK_API_VERSION = '2.0' +DEFAULT_NETWORK_API_VERSION = '2' API_VERSION_OPTION = 'os_network_api_version' API_NAME = "network" API_VERSIONS = { - "2.0": "neutronclient.v2_0.client.Client", + "2": "neutronclient.v2_0.client.Client", } diff --git a/openstackclient/network/v2_0/__init__.py b/openstackclient/network/v2/__init__.py similarity index 100% rename from openstackclient/network/v2_0/__init__.py rename to openstackclient/network/v2/__init__.py diff --git a/openstackclient/network/v2_0/network.py b/openstackclient/network/v2/network.py similarity index 100% rename from openstackclient/network/v2_0/network.py rename to openstackclient/network/v2/network.py diff --git a/openstackclient/tests/network/v2_0/__init__.py b/openstackclient/tests/network/v2/__init__.py similarity index 100% rename from openstackclient/tests/network/v2_0/__init__.py rename to openstackclient/tests/network/v2/__init__.py diff --git a/openstackclient/tests/network/v2_0/test_network.py b/openstackclient/tests/network/v2/test_network.py similarity index 99% rename from openstackclient/tests/network/v2_0/test_network.py rename to openstackclient/tests/network/v2/test_network.py index ef7d24ee45..b0c6af7002 100644 --- a/openstackclient/tests/network/v2_0/test_network.py +++ b/openstackclient/tests/network/v2/test_network.py @@ -15,7 +15,7 @@ import mock from openstackclient.common import exceptions -from openstackclient.network.v2_0 import network +from openstackclient.network.v2 import network from openstackclient.tests.network import common RESOURCE = 'network' diff --git a/openstackclient/tests/test_shell.py b/openstackclient/tests/test_shell.py index dfb8021a51..c180289e7d 100644 --- a/openstackclient/tests/test_shell.py +++ b/openstackclient/tests/test_shell.py @@ -39,13 +39,13 @@ DEFAULT_IDENTITY_API_VERSION = "2.0" DEFAULT_IMAGE_API_VERSION = "v2" DEFAULT_VOLUME_API_VERSION = "1" -DEFAULT_NETWORK_API_VERSION = "2.0" +DEFAULT_NETWORK_API_VERSION = "2" LIB_COMPUTE_API_VERSION = "2" LIB_IDENTITY_API_VERSION = "2.0" LIB_IMAGE_API_VERSION = "1" LIB_VOLUME_API_VERSION = "1" -LIB_NETWORK_API_VERSION = "2.0" +LIB_NETWORK_API_VERSION = "2" def make_shell(): diff --git a/setup.cfg b/setup.cfg index da07d3edc5..7023d4e585 100644 --- a/setup.cfg +++ b/setup.cfg @@ -258,12 +258,12 @@ openstack.image.v2 = image_save = openstackclient.image.v2.image:SaveImage image_show = openstackclient.image.v2.image:ShowImage -openstack.network.v2_0 = - network_create = openstackclient.network.v2_0.network:CreateNetwork - network_delete = openstackclient.network.v2_0.network:DeleteNetwork - network_list = openstackclient.network.v2_0.network:ListNetwork - network_set = openstackclient.network.v2_0.network:SetNetwork - network_show = openstackclient.network.v2_0.network:ShowNetwork +openstack.network.v2 = + network_create = openstackclient.network.v2.network:CreateNetwork + network_delete = openstackclient.network.v2.network:DeleteNetwork + network_list = openstackclient.network.v2.network:ListNetwork + network_set = openstackclient.network.v2.network:SetNetwork + network_show = openstackclient.network.v2.network:ShowNetwork openstack.object_store.v1 = container_list = openstackclient.object.v1.container:ListContainer From a065dd09e4e649186710435f93a1ad0734601476 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Mon, 7 Jul 2014 06:11:58 -0600 Subject: [PATCH 0136/3494] Allow network find to use alternate name Add the name_attr to the network find method so it can search for things like floating_ip_address for floating IP addresses rather than just id. Change-Id: I827e3745b06397a54555d1286e477bf2e05bf789 --- openstackclient/network/common.py | 6 +- openstackclient/tests/network/test_common.py | 72 ++++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 openstackclient/tests/network/test_common.py diff --git a/openstackclient/network/common.py b/openstackclient/network/common.py index 5ba44f7bfa..bd6203bd03 100644 --- a/openstackclient/network/common.py +++ b/openstackclient/network/common.py @@ -14,20 +14,22 @@ from openstackclient.common import exceptions -def find(client, resource, resources, name_or_id): +def find(client, resource, resources, name_or_id, name_attr='name'): """Find a network resource :param client: network client :param resource: name of the resource :param resources: plural name of resource :param name_or_id: name or id of resource user is looking for + :param name_attr: key to the name attribute for the resource For example: n = find(netclient, 'network', 'networks', 'matrix') """ list_method = getattr(client, "list_%s" % resources) # Search for by name - data = list_method(name=name_or_id, fields='id') + kwargs = {name_attr: name_or_id, 'fields': 'id'} + data = list_method(**kwargs) info = data[resources] if len(info) == 1: return info[0]['id'] diff --git a/openstackclient/tests/network/test_common.py b/openstackclient/tests/network/test_common.py new file mode 100644 index 0000000000..b30fdfcb35 --- /dev/null +++ b/openstackclient/tests/network/test_common.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. +# + +import mock + +from openstackclient.common import exceptions +from openstackclient.network import common +from openstackclient.tests import utils + +RESOURCE = 'resource' +RESOURCES = 'resources' +NAME = 'matrix' +ID = 'Fishburne' + + +class TestFind(utils.TestCase): + def setUp(self): + super(TestFind, self).setUp() + self.mock_client = mock.Mock() + self.list_resources = mock.Mock() + self.mock_client.list_resources = self.list_resources + self.matrix = {'id': ID} + + def test_name(self): + self.list_resources.return_value = {RESOURCES: [self.matrix]} + + result = common.find(self.mock_client, RESOURCE, RESOURCES, NAME) + + self.assertEqual(ID, result) + self.list_resources.assert_called_with(fields='id', name=NAME) + + def test_id(self): + self.list_resources.side_effect = [{RESOURCES: []}, + {RESOURCES: [self.matrix]}] + + result = common.find(self.mock_client, RESOURCE, RESOURCES, NAME) + + self.assertEqual(ID, result) + self.list_resources.assert_called_with(fields='id', id=NAME) + + def test_nameo(self): + self.list_resources.return_value = {RESOURCES: [self.matrix]} + + result = common.find(self.mock_client, RESOURCE, RESOURCES, NAME, + name_attr='nameo') + + self.assertEqual(ID, result) + self.list_resources.assert_called_with(fields='id', nameo=NAME) + + def test_dups(self): + dup = {'id': 'Larry'} + self.list_resources.return_value = {RESOURCES: [self.matrix, dup]} + + self.assertRaises(exceptions.CommandError, common.find, + self.mock_client, RESOURCE, RESOURCES, NAME) + + def test_nada(self): + self.list_resources.side_effect = [{RESOURCES: []}, + {RESOURCES: []}] + + self.assertRaises(exceptions.CommandError, common.find, + self.mock_client, RESOURCE, RESOURCES, NAME) From fba951568c5fcf765075134d8548a21de4f93a59 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Mon, 7 Jul 2014 09:11:57 -0600 Subject: [PATCH 0137/3494] Python 3: do not compare a list to a zip object In Python 3, zip() returns a zip object, not a list. Change-Id: I1a472bec3e12b5ae3c3555cf690b99a57579ce83 --- openstackclient/tests/network/v2/test_network.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openstackclient/tests/network/v2/test_network.py b/openstackclient/tests/network/v2/test_network.py index b0c6af7002..08b61a0a2c 100644 --- a/openstackclient/tests/network/v2/test_network.py +++ b/openstackclient/tests/network/v2/test_network.py @@ -49,7 +49,7 @@ def test_create_no_options(self): cmd = network.CreateNetwork(self.app, self.namespace) parsed_args = self.check_parser(cmd, arglist, verifylist) - result = cmd.take_action(parsed_args) + result = list(cmd.take_action(parsed_args)) mocker.assert_called_with({ RESOURCE: { @@ -75,7 +75,7 @@ def test_create_all_options(self): cmd = network.CreateNetwork(self.app, self.namespace) parsed_args = self.check_parser(cmd, arglist, verifylist) - result = cmd.take_action(parsed_args) + result = list(cmd.take_action(parsed_args)) mocker.assert_called_with({ RESOURCE: { @@ -102,7 +102,7 @@ def test_create_other_options(self): cmd = network.CreateNetwork(self.app, self.namespace) parsed_args = self.check_parser(cmd, arglist, verifylist) - result = cmd.take_action(parsed_args) + result = list(cmd.take_action(parsed_args)) mocker.assert_called_with({ RESOURCE: { @@ -298,7 +298,7 @@ def test_show_no_options(self): cmd = network.ShowNetwork(self.app, self.namespace) parsed_args = self.check_parser(cmd, arglist, verifylist) - result = cmd.take_action(parsed_args) + result = list(cmd.take_action(parsed_args)) mocker.assert_called_with(FAKE_ID) self.assertEqual(FILTERED, result) @@ -313,7 +313,7 @@ def test_show_all_options(self): cmd = network.ShowNetwork(self.app, self.namespace) parsed_args = self.check_parser(cmd, arglist, verifylist) - result = cmd.take_action(parsed_args) + result = list(cmd.take_action(parsed_args)) mocker.assert_called_with(FAKE_ID) self.assertEqual(FILTERED, result) From dad5b1051057b7adf1bffb43563491e93d7adf95 Mon Sep 17 00:00:00 2001 From: Christian Berendt Date: Mon, 7 Jul 2014 19:52:30 +0200 Subject: [PATCH 0138/3494] Replaced some UTF-8 characters with ASCII characters Change-Id: Ic4e53b742f8691dd2dafe1a8d7fa45e4340a3c94 --- doc/source/man/openstack.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/man/openstack.rst b/doc/source/man/openstack.rst index a4afb92648..9af1c42513 100644 --- a/doc/source/man/openstack.rst +++ b/doc/source/man/openstack.rst @@ -172,7 +172,7 @@ The following environment variables can be set to alter the behaviour of :progra Authentication region name :envvar:`OS_DEFAULT_DOMAIN` - Default domain ID (Default: ‘default’) + Default domain ID (Default: 'default') :envvar:`OS_CACERT` CA certificate bundle file From b6384886973c652c0161a9caeac6f31066edace1 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Fri, 30 May 2014 10:38:20 -0600 Subject: [PATCH 0139/3494] Domain administrator cannot do project operations Domain administrator cannot do project operations because the require access to the domain API (which they don't have). When attempting to find a domain for project operations, ignore errors because the API returns nothing without indicating there is a problem. The domain administrators will have to use a domain id, but they will still be able to do project operations. If the user does not have permission to read the domain table, they cannot use domain names. Change-Id: Ieed5d420022a407c8296a0bb3569d9469c89d752 Closes-Bug: #1317478 Closes-Bug: #1317485 --- openstackclient/identity/common.py | 21 ++++++ openstackclient/identity/v3/project.py | 18 ++---- .../tests/identity/v3/test_project.py | 64 +++++++++++++++++++ 3 files changed, 91 insertions(+), 12 deletions(-) diff --git a/openstackclient/identity/common.py b/openstackclient/identity/common.py index 6aeaa3c387..48dc0c89b8 100644 --- a/openstackclient/identity/common.py +++ b/openstackclient/identity/common.py @@ -16,6 +16,7 @@ """Common identity code""" from keystoneclient import exceptions as identity_exc +from keystoneclient.v3 import domains from openstackclient.common import exceptions from openstackclient.common import utils @@ -36,3 +37,23 @@ def find_service(identity_client, name_type_or_id): msg = ("No service with a type, name or ID of '%s' exists." % name_type_or_id) raise exceptions.CommandError(msg) + + +def find_domain(identity_client, name_or_id): + """Find a domain. + + If the user does not have permssions to access the v3 domain API, + assume that domain given is the id rather than the name. This + method is used by the project list command, so errors access the + domain will be ignored and if the user has access to the project + API, everything will work fine. + + Closes bugs #1317478 and #1317485. + """ + try: + dom = utils.find_resource(identity_client.domains, name_or_id) + if dom is not None: + return dom + except identity_exc.Forbidden: + pass + return domains.Domain(None, {'id': name_or_id}) diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 00a98d19a6..fa935f0bf5 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -24,6 +24,7 @@ from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.identity import common class CreateProject(show.ShowOne): @@ -73,10 +74,7 @@ def take_action(self, parsed_args): identity_client = self.app.client_manager.identity if parsed_args.domain: - domain = utils.find_resource( - identity_client.domains, - parsed_args.domain, - ).id + domain = common.find_domain(identity_client, parsed_args.domain).id else: domain = None @@ -156,10 +154,8 @@ def take_action(self, parsed_args): columns = ('ID', 'Name') kwargs = {} if parsed_args.domain: - kwargs['domain'] = utils.find_resource( - identity_client.domains, - parsed_args.domain, - ).id + domain = common.find_domain(identity_client, parsed_args.domain) + kwargs['domain'] = domain.id data = identity_client.projects.list(**kwargs) return (columns, (utils.get_item_properties( @@ -236,10 +232,8 @@ def take_action(self, parsed_args): if parsed_args.name: kwargs['name'] = parsed_args.name if parsed_args.domain: - kwargs['domain'] = utils.find_resource( - identity_client.domains, - parsed_args.domain, - ).id + domain = common.find_domain(identity_client, parsed_args.domain) + kwargs['domain'] = domain.id if parsed_args.description: kwargs['description'] = parsed_args.description if parsed_args.enable: diff --git a/openstackclient/tests/identity/v3/test_project.py b/openstackclient/tests/identity/v3/test_project.py index e0420a1e80..2e7bc54b5c 100644 --- a/openstackclient/tests/identity/v3/test_project.py +++ b/openstackclient/tests/identity/v3/test_project.py @@ -14,6 +14,7 @@ # import copy +import mock from openstackclient.identity.v3 import project from openstackclient.tests import fakes @@ -172,6 +173,45 @@ def test_project_create_domain(self): ) self.assertEqual(data, datalist) + def test_project_create_domain_no_perms(self): + arglist = [ + '--domain', identity_fakes.domain_id, + identity_fakes.project_name, + ] + verifylist = [ + ('domain', identity_fakes.domain_id), + ('enable', False), + ('disable', False), + ('name', identity_fakes.project_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + mocker = mock.Mock() + mocker.return_value = None + + with mock.patch("openstackclient.common.utils.find_resource", mocker): + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': identity_fakes.project_name, + 'domain': identity_fakes.domain_id, + 'description': None, + 'enabled': True, + } + self.projects_mock.create.assert_called_with( + **kwargs + ) + collist = ('description', 'domain_id', 'enabled', 'id', 'name') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.project_description, + identity_fakes.domain_id, + True, + identity_fakes.project_id, + identity_fakes.project_name, + ) + self.assertEqual(data, datalist) + def test_project_create_enable(self): arglist = [ '--enable', @@ -411,6 +451,30 @@ def test_project_list_domain(self): ), ) self.assertEqual(tuple(data), datalist) + def test_project_list_domain_no_perms(self): + arglist = [ + '--domain', identity_fakes.domain_id, + ] + verifylist = [ + ('domain', identity_fakes.domain_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + mocker = mock.Mock() + mocker.return_value = None + + with mock.patch("openstackclient.common.utils.find_resource", mocker): + columns, data = self.cmd.take_action(parsed_args) + + self.projects_mock.list.assert_called_with( + domain=identity_fakes.domain_id) + collist = ('ID', 'Name') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.project_id, + identity_fakes.project_name, + ), ) + self.assertEqual(tuple(data), datalist) + class TestProjectSet(TestProject): From 21bd4619ae61dfe849fea01211e6c45a86df77c5 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 8 Jul 2014 01:44:55 -0500 Subject: [PATCH 0140/3494] Clean up make_client() logging Change-Id: I0b6760a6401b50e3dfb891af75424ae89df42ebc --- openstackclient/compute/client.py | 2 +- openstackclient/identity/client.py | 6 ++++-- openstackclient/image/client.py | 1 + openstackclient/network/client.py | 2 ++ openstackclient/object/client.py | 3 ++- openstackclient/volume/client.py | 3 +-- 6 files changed, 11 insertions(+), 6 deletions(-) diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py index 3dacee88cf..6a7e3fe610 100644 --- a/openstackclient/compute/client.py +++ b/openstackclient/compute/client.py @@ -34,7 +34,7 @@ def make_client(instance): API_NAME, instance._api_version[API_NAME], API_VERSIONS) - LOG.debug('instantiating compute client: %s', compute_client) + LOG.debug('Instantiating compute client: %s', compute_client) # Set client http_log_debug to True if verbosity level is high enough http_log_debug = utils.get_effective_log_level() <= logging.DEBUG diff --git a/openstackclient/identity/client.py b/openstackclient/identity/client.py index 32a2c23b79..72e8bfaec0 100644 --- a/openstackclient/identity/client.py +++ b/openstackclient/identity/client.py @@ -36,8 +36,10 @@ def make_client(instance): API_NAME, instance._api_version[API_NAME], API_VERSIONS) + LOG.debug('Instantiating identity client: %s' % identity_client) + if instance._url: - LOG.debug('instantiating identity client: token flow') + LOG.debug('Using token auth') client = identity_client( endpoint=instance._url, token=instance._token, @@ -46,7 +48,7 @@ def make_client(instance): trust_id=instance._trust_id, ) else: - LOG.debug('instantiating identity client: password flow') + LOG.debug('Using password auth') client = identity_client( username=instance._username, password=instance._password, diff --git a/openstackclient/image/client.py b/openstackclient/image/client.py index ba48a0e967..a23d349e01 100644 --- a/openstackclient/image/client.py +++ b/openstackclient/image/client.py @@ -38,6 +38,7 @@ def make_client(instance): API_NAME, instance._api_version[API_NAME], API_VERSIONS) + LOG.debug('Instantiating image client: %s', image_client) if not instance._url: instance._url = instance.get_endpoint_for_service_type(API_NAME) diff --git a/openstackclient/network/client.py b/openstackclient/network/client.py index db0b5279d8..d3102da1eb 100644 --- a/openstackclient/network/client.py +++ b/openstackclient/network/client.py @@ -32,6 +32,8 @@ def make_client(instance): API_NAME, instance._api_version[API_NAME], API_VERSIONS) + LOG.debug('Instantiating network client: %s', network_client) + if not instance._url: instance._url = instance.get_endpoint_for_service_type("network") return network_client( diff --git a/openstackclient/object/client.py b/openstackclient/object/client.py index 273bea6ee8..4fe59794aa 100644 --- a/openstackclient/object/client.py +++ b/openstackclient/object/client.py @@ -35,11 +35,12 @@ def make_client(instance): API_NAME, instance._api_version[API_NAME], API_VERSIONS) + LOG.debug('Instantiating object client: %s' % object_client) + if instance._url: endpoint = instance._url else: endpoint = instance.get_endpoint_for_service_type(API_NAME) - LOG.debug('instantiating object client') client = object_client( endpoint=endpoint, token=instance._token, diff --git a/openstackclient/volume/client.py b/openstackclient/volume/client.py index 9b37b8f53c..f630f9f58c 100644 --- a/openstackclient/volume/client.py +++ b/openstackclient/volume/client.py @@ -40,8 +40,7 @@ def make_client(instance): instance._api_version[API_NAME], API_VERSIONS ) - - LOG.debug('instantiating volume client') + LOG.debug('Instantiating volume client: %s', volume_client) # Set client http_log_debug to True if verbosity level is high enough http_log_debug = utils.get_effective_log_level() <= logging.DEBUG From 4844a257790deef231176557776754dde929f840 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 8 May 2014 22:42:21 -0500 Subject: [PATCH 0141/3494] Add basic timing support Add support for --timing options. Use cliff via a pseudo-command 'Timing' to support multiple outputformats. If an output format other than the default 'table' is selected use CSV since the timing data is in list form. Will pick up timing data for any client object that has a method similar to novaclient's get_timings(). TODO: * Stop instantiating all of the clientmanager client objects just to check for timing data. Descriptor magic required? Change-Id: I7f1076b7a250fba6a8b24b2ae9353a7f51b792b2 --- openstackclient/common/clientmanager.py | 5 +- openstackclient/common/timing.py | 44 +++++++++++ openstackclient/compute/client.py | 4 +- openstackclient/shell.py | 33 ++++++++ openstackclient/tests/common/test_timing.py | 87 +++++++++++++++++++++ 5 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 openstackclient/common/timing.py create mode 100644 openstackclient/tests/common/test_timing.py diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index b310f3acdc..a2f85aff08 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -49,7 +49,7 @@ def __init__(self, token=None, url=None, auth_url=None, user_domain_id=None, user_domain_name=None, project_domain_id=None, project_domain_name=None, region_name=None, api_version=None, verify=True, - trust_id=None): + trust_id=None, timing=None): self._token = token self._url = url self._auth_url = auth_url @@ -67,6 +67,7 @@ def __init__(self, token=None, url=None, auth_url=None, self._api_version = api_version self._trust_id = trust_id self._service_catalog = None + self.timing = timing # verify is the Requests-compatible form self._verify = verify @@ -116,7 +117,7 @@ def get_extension_modules(group): setattr( ClientManager, - ep.name, + module.API_NAME, ClientCache( getattr(sys.modules[ep.module_name], 'make_client', None) ), diff --git a/openstackclient/common/timing.py b/openstackclient/common/timing.py new file mode 100644 index 0000000000..1c94682c84 --- /dev/null +++ b/openstackclient/common/timing.py @@ -0,0 +1,44 @@ +# 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. +# + +"""Timing Implementation""" + +import logging + +from cliff import lister + + +class Timing(lister.Lister): + """Show timing data""" + + log = logging.getLogger(__name__ + '.Timing') + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + + column_headers = ( + 'URL', + 'Seconds', + ) + + results = [] + total = 0.0 + for url, start, end in self.app.timing_data: + seconds = end - start + total += seconds + results.append((url, seconds)) + results.append(('Total', total)) + return ( + column_headers, + results, + ) diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py index 3dacee88cf..8d5cda132a 100644 --- a/openstackclient/compute/client.py +++ b/openstackclient/compute/client.py @@ -54,7 +54,9 @@ def make_client(instance): service_type=API_NAME, # FIXME(dhellmann): what is service_name? service_name='', - http_log_debug=http_log_debug) + http_log_debug=http_log_debug, + timings=instance.timing, + ) # Populate the Nova client to skip another auth query to Identity if instance._url: diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 1d0c577148..287243432f 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -32,6 +32,7 @@ from openstackclient.common import commandmanager from openstackclient.common import exceptions as exc from openstackclient.common import restapi +from openstackclient.common import timing from openstackclient.common import utils from openstackclient.identity import client as identity_client @@ -60,6 +61,7 @@ class OpenStackShell(app.App): CONSOLE_MESSAGE_FORMAT = '%(levelname)s: %(name)s %(message)s' log = logging.getLogger(__name__) + timing_data = [] def __init__(self): # Patch command.Command to add a default auth_required = True @@ -303,6 +305,12 @@ def build_option_parser(self, description, version): metavar='', default=env('OS_URL'), help='Defaults to env[OS_URL]') + parser.add_argument( + '--timing', + default=False, + action='store_true', + help="Print API call timing info", + ) parser.add_argument( '--os-identity-api-version', @@ -410,6 +418,7 @@ def authenticate_user(self): password=self.options.os_password, region_name=self.options.os_region_name, verify=self.verify, + timing=self.options.timing, api_version=self.api_version, trust_id=self.options.os_trust_id, ) @@ -499,9 +508,33 @@ def prepare_to_run_command(self, cmd): def clean_up(self, cmd, result, err): self.log.debug('clean_up %s', cmd.__class__.__name__) + if err: self.log.debug('got an error: %s', err) + # Process collected timing data + if self.options.timing: + # Loop through extensions + for mod in self.ext_modules: + client = getattr(self.client_manager, mod.API_NAME) + if hasattr(client, 'get_timings'): + self.timing_data.extend(client.get_timings()) + + # Use the Timing pseudo-command to generate the output + tcmd = timing.Timing(self, self.options) + tparser = tcmd.get_parser('Timing') + + # If anything other than prettytable is specified, force csv + format = 'table' + # Check the formatter used in the actual command + if hasattr(cmd, 'formatter') \ + and cmd.formatter != cmd._formatter_plugins['table'].obj: + format = 'csv' + + sys.stdout.write('\n') + targs = tparser.parse_args(['-f', format]) + tcmd.run(targs) + def interact(self): # NOTE(dtroyer): Maintain the old behaviour for interactive use as # this path does not call prepare_to_run_command() diff --git a/openstackclient/tests/common/test_timing.py b/openstackclient/tests/common/test_timing.py new file mode 100644 index 0000000000..aa910b91e5 --- /dev/null +++ b/openstackclient/tests/common/test_timing.py @@ -0,0 +1,87 @@ +# 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. +# + +"""Test Timing pseudo-command""" + +from openstackclient.common import timing +from openstackclient.tests import fakes +from openstackclient.tests import utils + + +timing_url = 'GET http://localhost:5000' +timing_start = 1404802774.872809 +timing_end = 1404802775.724802 + + +class FakeGenericClient(object): + + def __init__(self, **kwargs): + self.auth_token = kwargs['token'] + self.management_url = kwargs['endpoint'] + + +class TestTiming(utils.TestCommand): + + def setUp(self): + super(TestTiming, self).setUp() + + self.app.timing_data = [] + + self.app.client_manager.compute = FakeGenericClient( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) + + self.app.client_manager.volume = FakeGenericClient( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) + + # Get the command object to test + self.cmd = timing.Timing(self.app, None) + + def test_timing_list_no_data(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + collist = ('URL', 'Seconds') + self.assertEqual(collist, columns) + datalist = [ + ('Total', 0.0,) + ] + self.assertEqual(datalist, data) + + def test_timing_list(self): + self.app.timing_data = [ + (timing_url, timing_start, timing_end), + ] + + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + collist = ('URL', 'Seconds') + self.assertEqual(collist, columns) + timing_sec = timing_end - timing_start + datalist = [ + (timing_url, timing_sec), + ('Total', timing_sec) + ] + self.assertEqual(datalist, data) From ea2ac77a46b9bed87bbff902c093c1e8a43693af Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 7 Jul 2014 11:42:37 -0500 Subject: [PATCH 0142/3494] Fix server resize So apparently we've never resized a server??? Fixed command args and add some tests. Change-Id: I6c3f6fec22390e9d269b7117a42a190d2b4b80ba --- openstackclient/compute/v2/server.py | 19 ++- openstackclient/tests/compute/v2/fakes.py | 14 ++ .../tests/compute/v2/test_server.py | 135 ++++++++++++++++++ openstackclient/tests/utils.py | 8 ++ 4 files changed, 169 insertions(+), 7 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 2dcc7ae9a0..a4ed6fa47a 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -938,27 +938,32 @@ def take_action(self, parsed_args): class ResizeServer(command.Command): - """Convert server to a new flavor""" + """Scale server to a new flavor""" log = logging.getLogger(__name__ + '.ResizeServer') def get_parser(self, prog_name): parser = super(ResizeServer, self).get_parser(prog_name) phase_group = parser.add_mutually_exclusive_group() + parser.add_argument( + 'server', + metavar='', + help='Server (name or ID)', + ) phase_group.add_argument( '--flavor', metavar='', - help='Resize server to this flavor', + help='Resize server to specified flavor', ) phase_group.add_argument( '--verify', action="store_true", - help='Verify previous server resize', + help='Verify server resize is complete', ) phase_group.add_argument( '--revert', action="store_true", - help='Restore server before resize', + help='Restore server state before resize', ) parser.add_argument( '--wait', @@ -980,7 +985,7 @@ def take_action(self, parsed_args): compute_client.flavors, parsed_args.flavor, ) - server.resize(flavor) + compute_client.servers.resize(server, flavor) if parsed_args.wait: if utils.wait_for_status( compute_client.servers.get, @@ -993,9 +998,9 @@ def take_action(self, parsed_args): sys.stdout.write('\nError resizing server') raise SystemExit elif parsed_args.verify: - server.confirm_resize() + compute_client.servers.confirm_resize(server) elif parsed_args.revert: - server.revert_resize() + compute_client.servers.revert_resize(server) class ResumeServer(command.Command): diff --git a/openstackclient/tests/compute/v2/fakes.py b/openstackclient/tests/compute/v2/fakes.py index cef5ee9052..9a7964db0d 100644 --- a/openstackclient/tests/compute/v2/fakes.py +++ b/openstackclient/tests/compute/v2/fakes.py @@ -47,6 +47,18 @@ 'links': extension_links, } +flavor_id = 'm1.large' +flavor_name = 'Large' +flavor_ram = 8192 +flavor_vcpus = 4 + +FLAVOR = { + 'id': flavor_id, + 'name': flavor_name, + 'ram': flavor_ram, + 'vcpus': flavor_vcpus, +} + class FakeComputev2Client(object): def __init__(self, **kwargs): @@ -56,6 +68,8 @@ def __init__(self, **kwargs): self.servers.resource_class = fakes.FakeResource(None, {}) self.extensions = mock.Mock() self.extensions.resource_class = fakes.FakeResource(None, {}) + self.flavors = mock.Mock() + self.flavors.resource_class = fakes.FakeResource(None, {}) self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] diff --git a/openstackclient/tests/compute/v2/test_server.py b/openstackclient/tests/compute/v2/test_server.py index efe4c58b98..a98cd15690 100644 --- a/openstackclient/tests/compute/v2/test_server.py +++ b/openstackclient/tests/compute/v2/test_server.py @@ -30,6 +30,10 @@ def setUp(self): self.servers_mock = self.app.client_manager.compute.servers self.servers_mock.reset_mock() + # Get a shortcut to the FlavorManager Mock + self.flavors_mock = self.app.client_manager.compute.flavors + self.flavors_mock.reset_mock() + # Get a shortcut to the ImageManager Mock self.images_mock = self.app.client_manager.image.images self.images_mock.reset_mock() @@ -148,3 +152,134 @@ def test_server_image_create_name(self): image_fakes.image_owner, ) self.assertEqual(data, datalist) + + +class TestServerResize(TestServer): + + def setUp(self): + super(TestServerResize, self).setUp() + + # This is the return value for utils.find_resource() + self.servers_get_return_value = fakes.FakeResource( + None, + copy.deepcopy(compute_fakes.SERVER), + loaded=True, + ) + self.servers_mock.get.return_value = self.servers_get_return_value + + self.servers_mock.resize.return_value = None + self.servers_mock.confirm_resize.return_value = None + self.servers_mock.revert_resize.return_value = None + + # This is the return value for utils.find_resource() + self.flavors_get_return_value = fakes.FakeResource( + None, + copy.deepcopy(compute_fakes.FLAVOR), + loaded=True, + ) + self.flavors_mock.get.return_value = self.flavors_get_return_value + + # Get the command object to test + self.cmd = server.ResizeServer(self.app, None) + + def test_server_resize_no_options(self): + arglist = [ + compute_fakes.server_id, + ] + verifylist = [ + ('verify', False), + ('revert', False), + ('server', compute_fakes.server_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with( + compute_fakes.server_id, + ) + + self.assertNotCalled(self.servers_mock.resize) + self.assertNotCalled(self.servers_mock.confirm_resize) + self.assertNotCalled(self.servers_mock.revert_resize) + + def test_server_resize(self): + arglist = [ + '--flavor', compute_fakes.flavor_id, + compute_fakes.server_id, + ] + verifylist = [ + ('flavor', compute_fakes.flavor_id), + ('verify', False), + ('revert', False), + ('server', compute_fakes.server_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with( + compute_fakes.server_id, + ) + self.flavors_mock.get.assert_called_with( + compute_fakes.flavor_id, + ) + + self.servers_mock.resize.assert_called_with( + self.servers_get_return_value, + self.flavors_get_return_value, + ) + self.assertNotCalled(self.servers_mock.confirm_resize) + self.assertNotCalled(self.servers_mock.revert_resize) + + def test_server_resize_confirm(self): + arglist = [ + '--verify', + compute_fakes.server_id, + ] + verifylist = [ + ('verify', True), + ('revert', False), + ('server', compute_fakes.server_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with( + compute_fakes.server_id, + ) + + self.assertNotCalled(self.servers_mock.resize) + self.servers_mock.confirm_resize.assert_called_with( + self.servers_get_return_value, + ) + self.assertNotCalled(self.servers_mock.revert_resize) + + def test_server_resize_revert(self): + arglist = [ + '--revert', + compute_fakes.server_id, + ] + verifylist = [ + ('verify', False), + ('revert', True), + ('server', compute_fakes.server_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_with( + compute_fakes.server_id, + ) + + self.assertNotCalled(self.servers_mock.resize) + self.assertNotCalled(self.servers_mock.confirm_resize) + self.servers_mock.revert_resize.assert_called_with( + self.servers_get_return_value, + ) diff --git a/openstackclient/tests/utils.py b/openstackclient/tests/utils.py index 48385d138a..3755fa2647 100644 --- a/openstackclient/tests/utils.py +++ b/openstackclient/tests/utils.py @@ -37,6 +37,14 @@ def setUp(self): stderr = self.useFixture(fixtures.StringStream("stderr")).stream self.useFixture(fixtures.MonkeyPatch("sys.stderr", stderr)) + def assertNotCalled(self, m, msg=None): + """Assert a function was not called""" + + if m.called: + if not msg: + msg = 'method %s should not have been called' % m + self.fail(msg) + # 2.6 doesn't have the assert dict equals so make sure that it exists if tuple(sys.version_info)[0:2] < (2, 7): From 3cfb97e5ae930767f09c22402c3499c9bbb9eebd Mon Sep 17 00:00:00 2001 From: Cyril Roelandt Date: Tue, 15 Jul 2014 10:26:16 +0200 Subject: [PATCH 0143/3494] Add Python 3 support Change-Id: I65fabfc4788230b7280808ec912601c327095db0 --- setup.cfg | 2 ++ tox.ini | 1 + 2 files changed, 3 insertions(+) diff --git a/setup.cfg b/setup.cfg index 7023d4e585..00c6904fc0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,6 +16,8 @@ classifier = Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 2.6 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.3 [files] packages = diff --git a/tox.ini b/tox.ini index 520a85cd42..bdb98bec69 100644 --- a/tox.ini +++ b/tox.ini @@ -7,6 +7,7 @@ skipdist = True usedevelop = True install_command = pip install -U {opts} {packages} setenv = VIRTUAL_ENV={envdir} + PYTHONHASHSEED=0 deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = python setup.py testr --testr-args='{posargs}' From 498ddf95c67f15787f5083bf7aa9ea58668537da Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 17 Jul 2014 19:17:07 -0400 Subject: [PATCH 0144/3494] Change object API_NAME to 'object_store' Previously the API_NAME was 'object-store' which caused all sorts of failures when running swift commands Change-Id: I448ca10f7d173024313722246e63cf23fd71117c Closes-Bug: #1343658 --- openstackclient/object/client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openstackclient/object/client.py b/openstackclient/object/client.py index 4fe59794aa..006d54c517 100644 --- a/openstackclient/object/client.py +++ b/openstackclient/object/client.py @@ -23,7 +23,7 @@ DEFAULT_OBJECT_API_VERSION = '1' API_VERSION_OPTION = 'os_object_api_version' -API_NAME = 'object-store' +API_NAME = 'object_store' API_VERSIONS = { '1': 'openstackclient.object.client.ObjectClientv1', } @@ -31,6 +31,7 @@ def make_client(instance): """Returns an object service client.""" + object_client = utils.get_client_class( API_NAME, instance._api_version[API_NAME], @@ -40,7 +41,7 @@ def make_client(instance): if instance._url: endpoint = instance._url else: - endpoint = instance.get_endpoint_for_service_type(API_NAME) + endpoint = instance.get_endpoint_for_service_type("object-store") client = object_client( endpoint=endpoint, token=instance._token, From fc044c8847fbe985e2d9195bca6ab08a375b488d Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 17 Jul 2014 19:20:53 -0400 Subject: [PATCH 0145/3494] Update help text for some network commands Add help text for network delete, network set and network show Change-Id: I80ad5eae35f0eba2bfe19e06786c9b8ed1522046 Closes-Bug: #1343659 --- openstackclient/network/v2/network.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py index c0c25e710d..24d7197619 100644 --- a/openstackclient/network/v2/network.py +++ b/openstackclient/network/v2/network.py @@ -83,6 +83,7 @@ def get_body(self, parsed_args): class DeleteNetwork(command.Command): + """Delete a network""" log = logging.getLogger(__name__ + '.DeleteNetwork') @@ -157,6 +158,7 @@ def take_action(self, parsed_args): class SetNetwork(command.Command): + """Set network properties""" log = logging.getLogger(__name__ + '.SetNetwork') @@ -213,6 +215,7 @@ def take_action(self, parsed_args): class ShowNetwork(show.ShowOne): + """Show network details""" log = logging.getLogger(__name__ + '.ShowNetwork') From 5e7e94d59e3368b31c52649a0e5cc5018aac27b8 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Sun, 20 Jul 2014 13:15:02 +1000 Subject: [PATCH 0146/3494] Fix IDP commands identity_client.identity_providers doesn't exist as a manager. These are located at identity_client.federation.identity_providers. Fix the routes. Also fix passing id to .create() as a positional argument. This is not allowed from keystoneclient it should be passed as a keyword argument. Change-Id: I912c27fcee58b0723e27e9147def2cbd1c62c288 --- openstackclient/identity/v3/identity_provider.py | 15 ++++++++------- openstackclient/tests/identity/v3/fakes.py | 10 +++++++--- .../tests/identity/v3/test_identity_provider.py | 14 +++++++------- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/openstackclient/identity/v3/identity_provider.py b/openstackclient/identity/v3/identity_provider.py index b60678b51f..b9648a1d4b 100644 --- a/openstackclient/identity/v3/identity_provider.py +++ b/openstackclient/identity/v3/identity_provider.py @@ -61,8 +61,8 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - idp = identity_client.identity_providers.create( - parsed_args.identity_provider_id, + idp = identity_client.federation.identity_providers.create( + id=parsed_args.identity_provider_id, description=parsed_args.description, enabled=parsed_args.enabled) info = {} @@ -87,7 +87,7 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - identity_client.identity_providers.delete( + identity_client.federation.identity_providers.delete( parsed_args.identity_provider) return @@ -100,7 +100,8 @@ class ListIdentityProvider(lister.Lister): def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) columns = ('ID', 'Enabled', 'Description') - data = self.app.client_manager.identity.identity_providers.list() + identity_client = self.app.client_manager.identity + data = identity_client.federation.identity_providers.list() return (columns, (utils.get_item_properties( s, columns, @@ -136,7 +137,7 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) - identity_client = self.app.client_manager.identity + federation_client = self.app.client_manager.identity.federation if parsed_args.enable is True: enabled = True @@ -147,7 +148,7 @@ def take_action(self, parsed_args): "no arguments present") return (None, None) - identity_provider = identity_client.identity_providers.update( + identity_provider = federation_client.identity_providers.update( parsed_args.identity_provider, enabled=enabled) info = {} info.update(identity_provider._info) @@ -172,7 +173,7 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity identity_provider = utils.find_resource( - identity_client.identity_providers, + identity_client.federation.identity_providers, parsed_args.identity_provider) info = {} diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index 8143409df5..604171570d 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -201,14 +201,18 @@ def __init__(self, **kwargs): self.management_url = kwargs['endpoint'] -class FakeFederatedClient(FakeIdentityv3Client): +class FakeFederationManager(object): def __init__(self, **kwargs): - super(FakeFederatedClient, self).__init__(**kwargs) - self.identity_providers = mock.Mock() self.identity_providers.resource_class = fakes.FakeResource(None, {}) +class FakeFederatedClient(FakeIdentityv3Client): + def __init__(self, **kwargs): + super(FakeFederatedClient, self).__init__(**kwargs) + self.federation = FakeFederationManager() + + class FakeOAuth1Client(FakeIdentityv3Client): def __init__(self, **kwargs): super(FakeOAuth1Client, self).__init__(**kwargs) diff --git a/openstackclient/tests/identity/v3/test_identity_provider.py b/openstackclient/tests/identity/v3/test_identity_provider.py index 280d922772..c74bce8e7c 100644 --- a/openstackclient/tests/identity/v3/test_identity_provider.py +++ b/openstackclient/tests/identity/v3/test_identity_provider.py @@ -24,8 +24,8 @@ class TestIdentityProvider(identity_fakes.TestFederatedIdentity): def setUp(self): super(TestIdentityProvider, self).setUp() - identity_lib = self.app.client_manager.identity - self.identity_providers_mock = identity_lib.identity_providers + federation_lib = self.app.client_manager.identity.federation + self.identity_providers_mock = federation_lib.identity_providers self.identity_providers_mock.reset_mock() @@ -56,7 +56,7 @@ def test_create_identity_provider_no_options(self): } self.identity_providers_mock.create.assert_called_with( - identity_fakes.idp_id, + id=identity_fakes.idp_id, **kwargs ) @@ -88,7 +88,7 @@ def test_create_identity_provider_description(self): } self.identity_providers_mock.create.assert_called_with( - identity_fakes.idp_id, + id=identity_fakes.idp_id, **kwargs ) @@ -128,7 +128,7 @@ def test_create_identity_provider_disabled(self): } self.identity_providers_mock.create.assert_called_with( - identity_fakes.idp_id, + id=identity_fakes.idp_id, **kwargs ) @@ -217,12 +217,12 @@ class TestIdentityProviderShow(TestIdentityProvider): def setUp(self): super(TestIdentityProviderShow, self).setUp() - self.identity_providers_mock.get.return_value = fakes.FakeResource( + ret = fakes.FakeResource( None, copy.deepcopy(identity_fakes.IDENTITY_PROVIDER), loaded=True, ) - + self.identity_providers_mock.get.return_value = ret # Get the command object to test self.cmd = identity_provider.ShowIdentityProvider(self.app, None) From f3dbab4a6c90541257c10b4b9389455b3e6cec13 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 27 Jun 2014 09:10:28 -0500 Subject: [PATCH 0147/3494] Fix PEP8 E302 errors Also add remaining skipped checks to HACKING Change-Id: I0c4333ce29597e0a8a233af17c15bed2b4d0711f --- HACKING.rst | 11 +++++++++++ openstackclient/common/restapi.py | 4 ++-- openstackclient/object/v1/lib/container.py | 4 ++-- openstackclient/object/v1/lib/object.py | 4 ++-- tox.ini | 2 +- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/HACKING.rst b/HACKING.rst index 8744a93ac4..27c5e4851b 100644 --- a/HACKING.rst +++ b/HACKING.rst @@ -5,6 +5,17 @@ OpenStack Style Commandments http://docs.openstack.org/developer/hacking/ - Step 2: Read on +Deviations from Global Hacking +------------------------------ + +The following checks are specifically skipped in OpenStackClient:: + +* H305 - py2/py3 compatibility problems +* H307 - py2/py3 compatibility problems +* H402 - one line docstring ends in period +* H904 - backslash continuation line + + General ------- - thou shalt not violate causality in our time cone, or else diff --git a/openstackclient/common/restapi.py b/openstackclient/common/restapi.py index f20ad23dd2..a4822a1028 100644 --- a/openstackclient/common/restapi.py +++ b/openstackclient/common/restapi.py @@ -20,9 +20,9 @@ import requests try: - from urllib.parse import urlencode + from urllib.parse import urlencode # noqa except ImportError: - from urllib import urlencode + from urllib import urlencode # noqa USER_AGENT = 'RAPI' diff --git a/openstackclient/object/v1/lib/container.py b/openstackclient/object/v1/lib/container.py index 0bae23493f..72e97d4e1d 100644 --- a/openstackclient/object/v1/lib/container.py +++ b/openstackclient/object/v1/lib/container.py @@ -17,9 +17,9 @@ """Object v1 API library""" try: - from urllib.parse import urlparse + from urllib.parse import urlparse # noqa except ImportError: - from urlparse import urlparse + from urlparse import urlparse # noqa def list_containers( diff --git a/openstackclient/object/v1/lib/object.py b/openstackclient/object/v1/lib/object.py index 646737bdbd..ffc7e9b8ed 100644 --- a/openstackclient/object/v1/lib/object.py +++ b/openstackclient/object/v1/lib/object.py @@ -19,9 +19,9 @@ import six try: - from urllib.parse import urlparse + from urllib.parse import urlparse # noqa except ImportError: - from urlparse import urlparse + from urlparse import urlparse # noqa def list_objects( diff --git a/tox.ini b/tox.ini index bdb98bec69..b6935c2fb4 100644 --- a/tox.ini +++ b/tox.ini @@ -29,6 +29,6 @@ commands= python setup.py build_sphinx [flake8] -ignore = H302,H305,H307,H402,H904 +ignore = H305,H307,H402,H904 show-source = True exclude = .venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,tools From 5bb6c72ef72b2d83f5ddeaf4b3c09a89b76ba0a1 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 20 Jun 2014 10:06:05 -0500 Subject: [PATCH 0148/3494] Normalize more help strings Change-Id: I2b21bc904e35c1cc50da369d148e607fe3e8cf90 --- openstackclient/compute/v2/console.py | 8 +++---- openstackclient/compute/v2/flavor.py | 23 +++++++++++-------- openstackclient/compute/v2/floatingip.py | 14 +++++------ openstackclient/compute/v2/floatingippool.py | 2 +- openstackclient/compute/v2/hypervisor.py | 10 ++++---- openstackclient/compute/v2/keypair.py | 8 +++---- openstackclient/identity/v2_0/token.py | 2 +- .../identity/v3/identity_provider.py | 22 +++++++++--------- openstackclient/volume/v1/volume.py | 22 +++++++++--------- 9 files changed, 58 insertions(+), 53 deletions(-) diff --git a/openstackclient/compute/v2/console.py b/openstackclient/compute/v2/console.py index e1f84e2326..8206f3024b 100644 --- a/openstackclient/compute/v2/console.py +++ b/openstackclient/compute/v2/console.py @@ -26,7 +26,7 @@ class ShowConsoleLog(command.Command): - """Show console-log command""" + """Show server's console output""" log = logging.getLogger(__name__ + '.ShowConsoleLog') @@ -35,7 +35,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Name or ID of server to display console log', + help='Server (name or ID)', ) parser.add_argument( '--lines', @@ -67,7 +67,7 @@ def take_action(self, parsed_args): class ShowConsoleURL(show.ShowOne): - """Show console-url command""" + """Show server's remote console URL""" log = logging.getLogger(__name__ + '.ShowConsoleURL') @@ -76,7 +76,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Name or ID of server to display console log', + help='Server (name or ID)', ) type_group = parser.add_mutually_exclusive_group() type_group.add_argument( diff --git a/openstackclient/compute/v2/flavor.py b/openstackclient/compute/v2/flavor.py index 7342979628..6dd00b1b9e 100644 --- a/openstackclient/compute/v2/flavor.py +++ b/openstackclient/compute/v2/flavor.py @@ -26,7 +26,7 @@ class CreateFlavor(show.ShowOne): - """Create flavor command""" + """Create new flavor""" log = logging.getLogger(__name__ + ".CreateFlavor") @@ -35,7 +35,8 @@ def get_parser(self, prog_name): parser.add_argument( "name", metavar="", - help="Name of the new flavor") + help="New flavor name", + ) parser.add_argument( "--id", metavar="", @@ -84,12 +85,14 @@ def get_parser(self, prog_name): dest="public", action="store_true", default=True, - help="Flavor is accessible to the public (default)") + help="Flavor is accessible to other projects (default)", + ) public_group.add_argument( "--private", dest="public", action="store_false", - help="Flavor is inaccessible to the public") + help="Flavor is inaccessible to other projects", + ) return parser def take_action(self, parsed_args): @@ -115,7 +118,7 @@ def take_action(self, parsed_args): class DeleteFlavor(command.Command): - """Delete flavor command""" + """Delete a flavor""" log = logging.getLogger(__name__ + ".DeleteFlavor") @@ -124,7 +127,8 @@ def get_parser(self, prog_name): parser.add_argument( "flavor", metavar="", - help="Name or ID of flavor to delete") + help="Flavor to delete (name or ID)", + ) return parser def take_action(self, parsed_args): @@ -137,7 +141,7 @@ def take_action(self, parsed_args): class ListFlavor(lister.Lister): - """List flavor command""" + """List flavors""" log = logging.getLogger(__name__ + ".ListFlavor") @@ -164,7 +168,7 @@ def take_action(self, parsed_args): class ShowFlavor(show.ShowOne): - """Show flavor command""" + """Show flavor details""" log = logging.getLogger(__name__ + ".ShowFlavor") @@ -173,7 +177,8 @@ def get_parser(self, prog_name): parser.add_argument( "flavor", metavar="", - help="Name or ID of flavor to display") + help="Flavor to display (name or ID)", + ) return parser def take_action(self, parsed_args): diff --git a/openstackclient/compute/v2/floatingip.py b/openstackclient/compute/v2/floatingip.py index 72b19c6cf5..658f0d5abb 100644 --- a/openstackclient/compute/v2/floatingip.py +++ b/openstackclient/compute/v2/floatingip.py @@ -26,7 +26,7 @@ class AddFloatingIP(command.Command): - """Add floating-ip command""" + """Add floating-ip to server""" log = logging.getLogger(__name__ + ".AddFloatingIP") @@ -40,7 +40,7 @@ def get_parser(self, prog_name): parser.add_argument( "server", metavar="", - help="Name of the server to receive the IP address", + help="Server to receive the IP address (name or ID)", ) return parser @@ -56,7 +56,7 @@ def take_action(self, parsed_args): class CreateFloatingIP(show.ShowOne): - """Create floating-ip command""" + """Create new floating-ip""" log = logging.getLogger(__name__ + '.CreateFloatingIP') @@ -80,7 +80,7 @@ def take_action(self, parsed_args): class DeleteFloatingIP(command.Command): - """Delete floating-ip command""" + """Delete a floating-ip""" log = logging.getLogger(__name__ + '.DeleteFloatingIP') @@ -107,7 +107,7 @@ def take_action(self, parsed_args): class ListFloatingIP(lister.Lister): - """List floating-ip command""" + """List floating-ips""" log = logging.getLogger(__name__ + '.ListFloatingIP') @@ -127,7 +127,7 @@ def take_action(self, parsed_args): class RemoveFloatingIP(command.Command): - """Remove floating-ip command""" + """Remove floating-ip from server""" log = logging.getLogger(__name__ + ".RemoveFloatingIP") @@ -141,7 +141,7 @@ def get_parser(self, prog_name): parser.add_argument( "server", metavar="", - help="Name of the server to remove the IP address from", + help="Server to remove the IP address from (name or ID)", ) return parser diff --git a/openstackclient/compute/v2/floatingippool.py b/openstackclient/compute/v2/floatingippool.py index d5e8d0dd62..db1c9f0f79 100644 --- a/openstackclient/compute/v2/floatingippool.py +++ b/openstackclient/compute/v2/floatingippool.py @@ -23,7 +23,7 @@ class ListFloatingIPPool(lister.Lister): - """List floating-ip-pool command""" + """List floating-ip-pools""" log = logging.getLogger(__name__ + '.ListFloatingIPPool') diff --git a/openstackclient/compute/v2/hypervisor.py b/openstackclient/compute/v2/hypervisor.py index 334987e289..e01258d155 100644 --- a/openstackclient/compute/v2/hypervisor.py +++ b/openstackclient/compute/v2/hypervisor.py @@ -25,7 +25,7 @@ class ListHypervisor(lister.Lister): - """List hypervisor command""" + """List hypervisors""" log = logging.getLogger(__name__ + ".ListHypervisor") @@ -33,9 +33,9 @@ def get_parser(self, prog_name): parser = super(ListHypervisor, self).get_parser(prog_name) parser.add_argument( "--matching", - metavar="", - help="List hypervisors with hostnames matching the given" - " substring") + metavar="", + help="Filter hypervisors using substring", + ) return parser def take_action(self, parsed_args): @@ -58,7 +58,7 @@ def take_action(self, parsed_args): class ShowHypervisor(show.ShowOne): - """Show hypervisor command""" + """Show hypervisor details""" log = logging.getLogger(__name__ + ".ShowHypervisor") diff --git a/openstackclient/compute/v2/keypair.py b/openstackclient/compute/v2/keypair.py index 972443a43c..74cf4372f5 100644 --- a/openstackclient/compute/v2/keypair.py +++ b/openstackclient/compute/v2/keypair.py @@ -29,7 +29,7 @@ class CreateKeypair(show.ShowOne): - """Create keypair command""" + """Create new keypair""" log = logging.getLogger(__name__ + '.CreateKeypair') @@ -79,7 +79,7 @@ def take_action(self, parsed_args): class DeleteKeypair(command.Command): - """Delete keypair command""" + """Delete a keypair""" log = logging.getLogger(__name__ + '.DeleteKeypair') @@ -100,7 +100,7 @@ def take_action(self, parsed_args): class ListKeypair(lister.Lister): - """List keypair command""" + """List keypairs""" log = logging.getLogger(__name__ + ".ListKeypair") @@ -120,7 +120,7 @@ def take_action(self, parsed_args): class ShowKeypair(show.ShowOne): - """Show keypair command""" + """Show keypair details""" log = logging.getLogger(__name__ + '.ShowKeypair') diff --git a/openstackclient/identity/v2_0/token.py b/openstackclient/identity/v2_0/token.py index 01e1b3b26a..cc2c8a7fd4 100644 --- a/openstackclient/identity/v2_0/token.py +++ b/openstackclient/identity/v2_0/token.py @@ -41,7 +41,7 @@ def take_action(self, parsed_args): class RevokeToken(command.Command): - """Revoke token command""" + """Revoke existing token""" log = logging.getLogger(__name__ + '.RevokeToken') diff --git a/openstackclient/identity/v3/identity_provider.py b/openstackclient/identity/v3/identity_provider.py index b60678b51f..2463f00257 100644 --- a/openstackclient/identity/v3/identity_provider.py +++ b/openstackclient/identity/v3/identity_provider.py @@ -25,7 +25,7 @@ class CreateIdentityProvider(show.ShowOne): - """Create identity_provider command""" + """Create new identity provider""" log = logging.getLogger(__name__ + '.CreateIdentityProvider') @@ -33,7 +33,7 @@ def get_parser(self, prog_name): parser = super(CreateIdentityProvider, self).get_parser(prog_name) parser.add_argument( 'identity_provider_id', - metavar='', + metavar='', help='New identity provider ID (must be unique)' ) parser.add_argument( @@ -71,7 +71,7 @@ def take_action(self, parsed_args): class DeleteIdentityProvider(command.Command): - """Delete identity provider""" + """Delete an identity provider""" log = logging.getLogger(__name__ + '.DeleteIdentityProvider') @@ -79,8 +79,8 @@ def get_parser(self, prog_name): parser = super(DeleteIdentityProvider, self).get_parser(prog_name) parser.add_argument( 'identity_provider', - metavar='', - help='ID of the identity provider to be deleted', + metavar='', + help='Identity provider ID to delete', ) return parser @@ -109,7 +109,7 @@ def take_action(self, parsed_args): class SetIdentityProvider(command.Command): - """Set identity provider""" + """Set identity provider properties""" log = logging.getLogger(__name__ + '.SetIdentityProvider') @@ -117,8 +117,8 @@ def get_parser(self, prog_name): parser = super(SetIdentityProvider, self).get_parser(prog_name) parser.add_argument( 'identity_provider', - metavar='', - help='ID of the identity provider to be changed', + metavar='', + help='Identity provider ID to change', ) enable_identity_provider = parser.add_mutually_exclusive_group() @@ -155,7 +155,7 @@ def take_action(self, parsed_args): class ShowIdentityProvider(show.ShowOne): - """Show identity provider""" + """Show identity provider details""" log = logging.getLogger(__name__ + '.ShowIdentityProvider') @@ -163,8 +163,8 @@ def get_parser(self, prog_name): parser = super(ShowIdentityProvider, self).get_parser(prog_name) parser.add_argument( 'identity_provider', - metavar='', - help='ID of the identity provider to be displayed', + metavar='', + help='Identity provider ID to show', ) return parser diff --git a/openstackclient/volume/v1/volume.py b/openstackclient/volume/v1/volume.py index 3e4af56ce3..99abac52f7 100644 --- a/openstackclient/volume/v1/volume.py +++ b/openstackclient/volume/v1/volume.py @@ -36,7 +36,7 @@ def get_parser(self, prog_name): parser.add_argument( 'name', metavar='', - help='Name of the volume', + help='Name of the new volume', ) parser.add_argument( '--size', @@ -48,7 +48,7 @@ def get_parser(self, prog_name): parser.add_argument( '--snapshot-id', metavar='', - help='ID of the snapshot', + help='Use as source of new volume', ) parser.add_argument( '--description', @@ -73,7 +73,7 @@ def get_parser(self, prog_name): parser.add_argument( '--availability-zone', metavar='', - help='Availability zone to use', + help='Create new volume in ', ) parser.add_argument( '--property', @@ -85,12 +85,12 @@ def get_parser(self, prog_name): parser.add_argument( '--image', metavar='', - help='Reference to a stored image', + help='Use as source of new volume', ) parser.add_argument( '--source', metavar='', - help='Source for volume clone', + help='Volume to clone (name or ID)', ) return parser @@ -143,7 +143,7 @@ def take_action(self, parsed_args): class DeleteVolume(command.Command): - """Delete volume""" + """Delete a volume""" log = logging.getLogger(__name__ + '.DeleteVolume') @@ -152,7 +152,7 @@ def get_parser(self, prog_name): parser.add_argument( 'volume', metavar='', - help='Name or ID of volume to delete', + help='Volume to delete (name or ID)', ) parser.add_argument( '--force', @@ -271,7 +271,7 @@ def get_parser(self, prog_name): parser.add_argument( 'volume', metavar='', - help='Name or ID of volume to change', + help='Volume to change (name or ID)', ) parser.add_argument( '--name', @@ -315,7 +315,7 @@ def take_action(self, parsed_args): class ShowVolume(show.ShowOne): - """Show specific volume""" + """Show volume details""" log = logging.getLogger(__name__ + '.ShowVolume') @@ -324,7 +324,7 @@ def get_parser(self, prog_name): parser.add_argument( 'volume', metavar='', - help='Name or ID of volume to display', + help='Volume to display (name or ID)', ) return parser @@ -357,7 +357,7 @@ def get_parser(self, prog_name): parser.add_argument( 'volume', metavar='', - help='Name or ID of volume to change', + help='Volume to change (name or ID)', ) parser.add_argument( '--property', From e1c32b8224624fd690fd8eb273e4859d5702b0c2 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 25 Jul 2014 01:09:59 -0400 Subject: [PATCH 0149/3494] Add more columns to image list output Add disk_format, container_format, size and status to image list command. Added tests as well. Change-Id: I8e3822c6d46d0020fc706955c026549f6c635587 Closes-Bug: #1348475 --- openstackclient/image/v1/image.py | 12 +++++- openstackclient/image/v2/image.py | 12 +++++- openstackclient/tests/image/v1/test_image.py | 45 ++++++++++++++++++++ openstackclient/tests/image/v2/test_image.py | 45 ++++++++++++++++++++ 4 files changed, 112 insertions(+), 2 deletions(-) diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index 92d0995344..cd746cf53e 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -291,6 +291,12 @@ def get_parser(self, prog_name): metavar="", help="Number of images to request in each paginated request", ) + parser.add_argument( + '--long', + action='store_true', + default=False, + help='List additional fields in output', + ) return parser def take_action(self, parsed_args): @@ -303,7 +309,11 @@ def take_action(self, parsed_args): kwargs["page_size"] = parsed_args.page_size data = image_client.images.list(**kwargs) - columns = ["ID", "Name"] + if parsed_args.long: + columns = ('ID', 'Name', 'Disk Format', 'Container Format', + 'Size', 'Status') + else: + columns = ("ID", "Name") return (columns, (utils.get_item_properties(s, columns) for s in data)) diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 08897b2bbf..275e562cff 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -63,6 +63,12 @@ def get_parser(self, prog_name): metavar="", help="Number of images to request in each paginated request", ) + parser.add_argument( + '--long', + action='store_true', + default=False, + help='List additional fields in output', + ) return parser def take_action(self, parsed_args): @@ -75,7 +81,11 @@ def take_action(self, parsed_args): kwargs["page_size"] = parsed_args.page_size data = image_client.images.list(**kwargs) - columns = ["ID", "Name"] + if parsed_args.long: + columns = ('ID', 'Name', 'Disk Format', 'Container Format', + 'Size', 'Status') + else: + columns = ("ID", "Name") return (columns, (utils.get_item_properties(s, columns) for s in data)) diff --git a/openstackclient/tests/image/v1/test_image.py b/openstackclient/tests/image/v1/test_image.py index b014482a84..3f97b151c2 100644 --- a/openstackclient/tests/image/v1/test_image.py +++ b/openstackclient/tests/image/v1/test_image.py @@ -446,3 +446,48 @@ def test_image_set_properties(self): image_fakes.image_id, **kwargs ) + + +class TestImageList(TestImage): + + def setUp(self): + super(TestImageList, self).setUp() + + # This is the return value for utils.find_resource() + self.images_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(image_fakes.IMAGE), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = image.ListImage(self.app, None) + + def test_image_list_long_option(self): + arglist = [ + '--long', + ] + verifylist = [ + ('long', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + self.images_mock.list.assert_called_with() + + collist = ('ID', 'Name', 'Disk Format', 'Container Format', + 'Size', 'Status') + + self.assertEqual(columns, collist) + datalist = (( + image_fakes.image_id, + image_fakes.image_name, + '', + '', + '', + '', + ), ) + self.assertEqual(datalist, tuple(data)) diff --git a/openstackclient/tests/image/v2/test_image.py b/openstackclient/tests/image/v2/test_image.py index ef84e2c04e..81c9023fb5 100644 --- a/openstackclient/tests/image/v2/test_image.py +++ b/openstackclient/tests/image/v2/test_image.py @@ -61,3 +61,48 @@ def test_image_delete_no_options(self): self.images_mock.delete.assert_called_with( image_fakes.image_id, ) + + +class TestImageList(TestImage): + + def setUp(self): + super(TestImageList, self).setUp() + + # This is the return value for utils.find_resource() + self.images_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(image_fakes.IMAGE), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = image.ListImage(self.app, None) + + def test_image_list_long_option(self): + arglist = [ + '--long', + ] + verifylist = [ + ('long', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + self.images_mock.list.assert_called_with() + + collist = ('ID', 'Name', 'Disk Format', 'Container Format', + 'Size', 'Status') + + self.assertEqual(columns, collist) + datalist = (( + image_fakes.image_id, + image_fakes.image_name, + '', + '', + '', + '', + ), ) + self.assertEqual(datalist, tuple(data)) From 6e1fa8b27df4f0697f3f44f710469792358e6c50 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 25 Jul 2014 01:16:01 -0400 Subject: [PATCH 0150/3494] Change V2 image tests to actually run V2 image code The current tests for image do not run v2 image code, changing that portion also made it's only test fail. I opted to change the image delete code and not the test, since passing the object ID is more in line with the rest of the project code. Change-Id: I62e13c063a5d68279dbbf31e59266db6285d73bf --- openstackclient/image/v2/image.py | 2 +- openstackclient/tests/image/v2/test_image.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 08897b2bbf..67cb16b697 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -48,7 +48,7 @@ def take_action(self, parsed_args): image_client.images, parsed_args.image, ) - image_client.images.delete(image) + image_client.images.delete(image.id) class ListImage(lister.Lister): diff --git a/openstackclient/tests/image/v2/test_image.py b/openstackclient/tests/image/v2/test_image.py index ef84e2c04e..9dbdad5cbc 100644 --- a/openstackclient/tests/image/v2/test_image.py +++ b/openstackclient/tests/image/v2/test_image.py @@ -15,7 +15,7 @@ import copy -from openstackclient.image.v1 import image +from openstackclient.image.v2 import image from openstackclient.tests import fakes from openstackclient.tests.image.v2 import fakes as image_fakes From b96d9d374c524e44edfb9656525bbbcc32d3a605 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 8 Jul 2014 01:44:55 -0500 Subject: [PATCH 0151/3494] More make_client() logging cleanup Change-Id: I5af4b9c52c69d6e31e6ca5f90d5880c097880a71 --- openstackclient/identity/client.py | 2 +- openstackclient/object/client.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openstackclient/identity/client.py b/openstackclient/identity/client.py index 72e8bfaec0..7f5390c86b 100644 --- a/openstackclient/identity/client.py +++ b/openstackclient/identity/client.py @@ -36,7 +36,7 @@ def make_client(instance): API_NAME, instance._api_version[API_NAME], API_VERSIONS) - LOG.debug('Instantiating identity client: %s' % identity_client) + LOG.debug('Instantiating identity client: %s', identity_client) if instance._url: LOG.debug('Using token auth') diff --git a/openstackclient/object/client.py b/openstackclient/object/client.py index 006d54c517..b81ffaaf44 100644 --- a/openstackclient/object/client.py +++ b/openstackclient/object/client.py @@ -36,7 +36,7 @@ def make_client(instance): API_NAME, instance._api_version[API_NAME], API_VERSIONS) - LOG.debug('Instantiating object client: %s' % object_client) + LOG.debug('Instantiating object client: %s', object_client) if instance._url: endpoint = instance._url From 25e0d2ab2794dc68cccec0d1e7b463b7d7f7e65f Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Fri, 11 Jul 2014 09:05:37 -0600 Subject: [PATCH 0152/3494] Add network extension list Network extension list support Change-Id: I013f68ef2c3329c8db59e2441dd8d4ffafd4470e Closes-Bug: #1337685 --- openstackclient/common/extension.py | 54 ++++++++++++++----- .../tests/common/test_extension.py | 52 +++++++++++++++++- openstackclient/tests/network/v2/fakes.py | 35 ++++++++++++ 3 files changed, 126 insertions(+), 15 deletions(-) create mode 100644 openstackclient/tests/network/v2/fakes.py diff --git a/openstackclient/common/extension.py b/openstackclient/common/extension.py index 91ee228be5..be7426daa0 100644 --- a/openstackclient/common/extension.py +++ b/openstackclient/common/extension.py @@ -15,6 +15,7 @@ """Extension action implementations""" +import itertools import logging from cliff import lister @@ -25,28 +26,30 @@ class ListExtension(lister.Lister): """List extension command""" - # TODO(mfisch): add support for volume and network - # when the underlying APIs support it. - log = logging.getLogger(__name__ + '.ListExtension') def get_parser(self, prog_name): parser = super(ListExtension, self).get_parser(prog_name) parser.add_argument( - '--long', + '--compute', action='store_true', default=False, - help='List additional fields in output') + help='List extensions for the Compute API') parser.add_argument( '--identity', action='store_true', default=False, help='List extensions for the Identity API') parser.add_argument( - '--compute', + '--long', action='store_true', default=False, - help='List extensions for the Compute API') + help='List additional fields in output') + parser.add_argument( + '--network', + action='store_true', + default=False, + help='List extensions for the Network API') parser.add_argument( '--volume', action='store_true', @@ -69,7 +72,7 @@ def take_action(self, parsed_args): # user specifies one or more of the APIs to show # for now, only identity and compute are supported. show_all = (not parsed_args.identity and not parsed_args.compute - and not parsed_args.volume) + and not parsed_args.volume and not parsed_args.network) if parsed_args.identity or show_all: identity_client = self.app.client_manager.identity @@ -95,8 +98,33 @@ def take_action(self, parsed_args): message = "Extensions list not supported by Volume API" self.log.warning(message) - return (columns, - (utils.get_item_properties( - s, columns, - formatters={}, - ) for s in data)) + # Resource classes for the above + extension_tuples = ( + utils.get_item_properties( + s, + columns, + formatters={}, + ) for s in data + ) + + # Dictionaries for the below + if parsed_args.network or show_all: + network_client = self.app.client_manager.network + try: + data = network_client.list_extensions()['extensions'] + dict_tuples = ( + utils.get_dict_properties( + s, + columns, + formatters={}, + ) for s in data + ) + extension_tuples = itertools.chain( + extension_tuples, + dict_tuples + ) + except Exception: + message = "Extensions list not supported by Network API" + self.log.warning(message) + + return (columns, extension_tuples) diff --git a/openstackclient/tests/common/test_extension.py b/openstackclient/tests/common/test_extension.py index 2e6e7050f8..5561345b50 100644 --- a/openstackclient/tests/common/test_extension.py +++ b/openstackclient/tests/common/test_extension.py @@ -18,6 +18,7 @@ from openstackclient.tests import utils from openstackclient.tests.identity.v2_0 import fakes as identity_fakes +from openstackclient.tests.network.v2 import fakes as network_fakes class TestExtension(utils.TestCommand): @@ -29,12 +30,15 @@ def setUp(self): endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN, ) - - # Get shortcuts to the ExtensionManager Mocks self.identity_extensions_mock = ( self.app.client_manager.identity.extensions) self.identity_extensions_mock.reset_mock() + network = network_fakes.FakeNetworkV2Client() + self.app.client_manager.network = network + self.network_extensions_mock = network.list_extensions + self.network_extensions_mock.reset_mock() + class TestExtensionList(TestExtension): @@ -48,6 +52,13 @@ def setUp(self): loaded=True, ), ] + self.network_extensions_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.EXTENSION), + loaded=True, + ), + ] # Get the command object to test self.cmd = extension.ListExtension(self.app, None) @@ -71,6 +82,11 @@ def test_extension_list_no_options(self): identity_fakes.extension_alias, identity_fakes.extension_description, ), + ( + network_fakes.extension_name, + network_fakes.extension_alias, + network_fakes.extension_description, + ), ) self.assertEqual(tuple(data), datalist) @@ -101,6 +117,14 @@ def test_extension_list_long(self): identity_fakes.extension_updated, identity_fakes.extension_links, ), + ( + network_fakes.extension_name, + network_fakes.extension_namespace, + network_fakes.extension_description, + network_fakes.extension_alias, + network_fakes.extension_updated, + network_fakes.extension_links, + ), ) self.assertEqual(tuple(data), datalist) @@ -126,3 +150,27 @@ def test_extension_list_identity(self): identity_fakes.extension_description, ), ) self.assertEqual(tuple(data), datalist) + + def test_extension_list_network(self): + arglist = [ + '--network', + ] + verifylist = [ + ('network', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_extensions_mock.assert_called_with() + + collist = ('Name', 'Alias', 'Description') + self.assertEqual(columns, collist) + datalist = ( + ( + network_fakes.extension_name, + network_fakes.extension_alias, + network_fakes.extension_description, + ), + ) + self.assertEqual(tuple(data), datalist) diff --git a/openstackclient/tests/network/v2/fakes.py b/openstackclient/tests/network/v2/fakes.py new file mode 100644 index 0000000000..ea191c8e8c --- /dev/null +++ b/openstackclient/tests/network/v2/fakes.py @@ -0,0 +1,35 @@ +# 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 mock + +extension_name = 'Matrix' +extension_namespace = 'http://docs.openstack.org/network/' +extension_description = 'Simulated reality' +extension_updated = '2013-07-09T12:00:0-00:00' +extension_alias = 'Dystopian' +extension_links = '[{"href":''"https://github.com/os/network", "type"}]' + +NETEXT = { + 'name': extension_name, + 'namespace': extension_namespace, + 'description': extension_description, + 'updated': extension_updated, + 'alias': extension_alias, + 'links': extension_links, +} + + +class FakeNetworkV2Client(object): + def __init__(self, **kwargs): + self.list_extensions = mock.Mock(return_value={'extensions': [NETEXT]}) From 81d11799c6fe6f09d0493755f2533d223ae13433 Mon Sep 17 00:00:00 2001 From: wanghong Date: Tue, 29 Jul 2014 15:33:18 +0800 Subject: [PATCH 0153/3494] fix typo in identity/v3/endpoint.py Change-Id: Idf57a6a988f5c0f20f3b0b19ab896642ce10d70b --- openstackclient/identity/v3/endpoint.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openstackclient/identity/v3/endpoint.py b/openstackclient/identity/v3/endpoint.py index 93d77be339..fa1b862847 100644 --- a/openstackclient/identity/v3/endpoint.py +++ b/openstackclient/identity/v3/endpoint.py @@ -57,13 +57,13 @@ def get_parser(self, prog_name): dest='enabled', action='store_true', default=True, - help='Enable user', + help='Enable endpoint', ) enable_group.add_argument( '--disable', dest='enabled', action='store_false', - help='Disable user', + help='Disable endpoint', ) return parser @@ -166,13 +166,13 @@ def get_parser(self, prog_name): dest='enabled', action='store_true', default=True, - help='Enable user', + help='Enable endpoint', ) enable_group.add_argument( '--disable', dest='enabled', action='store_false', - help='Disable user', + help='Disable endpoint', ) return parser From 75e8490e54bf442b36534ea9c8b53c203b6a9938 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 31 Jul 2014 00:53:36 -0400 Subject: [PATCH 0154/3494] Cleanup README.rst There are two harmless typos in the README.rst 'OpenStackclient' => 'OpenStack Client' 'python-*client' => the '*' is being interpreted as a link Change-Id: Ie813e220c3c150f46edb2c93f94e8bb78bdb0013 Closes-Bug: #1350518 --- README.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index c892df1c10..7c2ec90468 100644 --- a/README.rst +++ b/README.rst @@ -2,9 +2,9 @@ OpenStack Client ================ -OpenStackclient (aka ``python-openstackclient``) is a command-line client for +OpenStack Client (aka ``python-openstackclient``) is a command-line client for the OpenStack APIs. -It is primarily a wrapper to the stock python-*client modules that implement the +It is primarily a wrapper to the stock python-\*client modules that implement the actual REST API client actions. This is an implementation of the design goals shown in @@ -15,13 +15,14 @@ operations in OpenStack. The master repository is on GitHub_. .. _OpenStack Client Wiki: https://wiki.openstack.org/wiki/OpenStackClient .. _GitHub: https://github.com/openstack/python-openstackclient -OpenStackclient has a plugin mechanism to add support for API extensions. +OpenStack Client has a plugin mechanism to add support for API extensions. * `Release management`_ * `Blueprints and feature specifications`_ * `Issue tracking`_ * `PyPi`_ * `Developer Docs`_ + .. _release management: https://launchpad.net/python-openstackclient .. _Blueprints and feature specifications: https://blueprints.launchpad.net/python-openstackclient .. _Issue tracking: https://bugs.launchpad.net/python-openstackclient @@ -39,7 +40,7 @@ or output. We do not, however, expect any major changes at this point. Getting Started =============== -OpenStackclient can be installed from PyPI using pip:: +OpenStack Client can be installed from PyPI using pip:: pip install python-openstackclient From be83ae763ffbcd3208ba1df9fe8b22cfe3fa6fa2 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Sun, 3 Aug 2014 02:19:29 -0400 Subject: [PATCH 0155/3494] Add container create and delete support Add basic container create and delete support to OSC. Change-Id: Ia104db9d7e580d33097ea33a5690998f817995d1 implements: bp swift-client --- openstackclient/common/restapi.py | 18 ++++++-- openstackclient/object/v1/container.py | 51 ++++++++++++++++++++++ openstackclient/object/v1/lib/container.py | 39 +++++++++++++++++ setup.cfg | 2 + 4 files changed, 107 insertions(+), 3 deletions(-) diff --git a/openstackclient/common/restapi.py b/openstackclient/common/restapi.py index a4822a1028..a646acb364 100644 --- a/openstackclient/common/restapi.py +++ b/openstackclient/common/restapi.py @@ -189,7 +189,11 @@ def patch(self, url, data=None, json=None, **kwargs): :param \*\*kwargs: Optional arguments passed to ``request`` """ - return self.request('PATCH', url, data=data, json=json, **kwargs) + if json: + kwargs['json'] = json + if data: + kwargs['data'] = data + return self.request('PATCH', url, **kwargs) def post(self, url, data=None, json=None, **kwargs): """Send a POST request. Returns :class:`requests.Response` object. @@ -201,7 +205,11 @@ def post(self, url, data=None, json=None, **kwargs): :param \*\*kwargs: Optional arguments passed to ``request`` """ - return self.request('POST', url, data=data, json=json, **kwargs) + if json: + kwargs['json'] = json + if data: + kwargs['data'] = data + return self.request('POST', url, **kwargs) def put(self, url, data=None, json=None, **kwargs): """Send a PUT request. Returns :class:`requests.Response` object. @@ -213,7 +221,11 @@ def put(self, url, data=None, json=None, **kwargs): :param \*\*kwargs: Optional arguments passed to ``request`` """ - return self.request('PUT', url, data=data, json=json, **kwargs) + if json: + kwargs['json'] = json + if data: + kwargs['data'] = data + return self.request('PUT', url, **kwargs) # Command verb methods diff --git a/openstackclient/object/v1/container.py b/openstackclient/object/v1/container.py index 1e252aaf4f..ae4013fc0c 100644 --- a/openstackclient/object/v1/container.py +++ b/openstackclient/object/v1/container.py @@ -19,6 +19,7 @@ import logging import six +from cliff import command from cliff import lister from cliff import show @@ -26,6 +27,56 @@ from openstackclient.object.v1.lib import container as lib_container +class CreateContainer(show.ShowOne): + """Create a container""" + + log = logging.getLogger(__name__ + '.CreateContainer') + + def get_parser(self, prog_name): + parser = super(CreateContainer, self).get_parser(prog_name) + parser.add_argument( + 'container', + metavar='', + help='New container name', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + data = lib_container.create_container( + self.app.restapi, + self.app.client_manager.object_store.endpoint, + parsed_args.container, + ) + + return zip(*sorted(six.iteritems(data))) + + +class DeleteContainer(command.Command): + """Delete a container""" + + log = logging.getLogger(__name__ + '.DeleteContainer') + + def get_parser(self, prog_name): + parser = super(DeleteContainer, self).get_parser(prog_name) + parser.add_argument( + 'container', + metavar='', + help='Container name to delete', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + lib_container.delete_container( + self.app.restapi, + self.app.client_manager.object_store.endpoint, + parsed_args.container, + ) + + class ListContainer(lister.Lister): """List containers""" diff --git a/openstackclient/object/v1/lib/container.py b/openstackclient/object/v1/lib/container.py index 72e97d4e1d..bd50955514 100644 --- a/openstackclient/object/v1/lib/container.py +++ b/openstackclient/object/v1/lib/container.py @@ -22,6 +22,45 @@ from urlparse import urlparse # noqa +def create_container( + api, + url, + container, +): + """Create a container + + :param api: a restapi object + :param url: endpoint + :param container: name of container to create + :returns: dict of returned headers + """ + + response = api.put("%s/%s" % (url, container)) + url_parts = urlparse(url) + data = { + 'account': url_parts.path.split('/')[-1], + 'container': container, + } + data['x-trans-id'] = response.headers.get('x-trans-id', None) + + return data + + +def delete_container( + api, + url, + container, +): + """Delete a container + + :param api: a restapi object + :param url: endpoint + :param container: name of container to delete + """ + + api.delete("%s/%s" % (url, container)) + + def list_containers( api, url, diff --git a/setup.cfg b/setup.cfg index 00c6904fc0..7f07286c0d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -268,6 +268,8 @@ openstack.network.v2 = network_show = openstackclient.network.v2.network:ShowNetwork openstack.object_store.v1 = + container_create = openstackclient.object.v1.container:CreateContainer + container_delete = openstackclient.object.v1.container:DeleteContainer container_list = openstackclient.object.v1.container:ListContainer container_show = openstackclient.object.v1.container:ShowContainer object_list = openstackclient.object.v1.object:ListObject From a9fb5fa102560d389a8a9f76ed572f1c4fc9944b Mon Sep 17 00:00:00 2001 From: wanghong Date: Tue, 29 Jul 2014 15:33:18 +0800 Subject: [PATCH 0156/3494] v3 endpoint set shouldn't always need service option Change-Id: I71aab1ee4f467dc963e7afa7fc1c82b4255ea822 Closes-Bug: #1351121 --- openstackclient/identity/v3/endpoint.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openstackclient/identity/v3/endpoint.py b/openstackclient/identity/v3/endpoint.py index fa1b862847..4ea44e7a72 100644 --- a/openstackclient/identity/v3/endpoint.py +++ b/openstackclient/identity/v3/endpoint.py @@ -181,16 +181,20 @@ def take_action(self, parsed_args): identity_client = self.app.client_manager.identity endpoint = utils.find_resource(identity_client.endpoints, parsed_args.endpoint) - service = common.find_service(identity_client, parsed_args.service) if (not parsed_args.interface and not parsed_args.url and not parsed_args.service and not parsed_args.region): sys.stdout.write("Endpoint not updated, no arguments present") return + service_id = None + if parsed_args.service: + service = common.find_service(identity_client, parsed_args.service) + service_id = service.id + identity_client.endpoints.update( endpoint.id, - service=service.id, + service=service_id, url=parsed_args.url, interface=parsed_args.interface, region=parsed_args.region, From 40013f3c0214e7724e574c2c598bce1f0ccbe714 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Mon, 4 Aug 2014 03:28:17 +0000 Subject: [PATCH 0157/3494] Updated from global requirements Change-Id: If62daf2539ff69323c905c12c19e041f83ef8eb2 --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 51715d1d76..b1a122c497 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ pbr>=0.6,!=0.7,<1.0 cliff>=1.6.0 python-glanceclient>=0.13.1 -python-keystoneclient>=0.9.0 +python-keystoneclient>=0.10.0 python-novaclient>=2.17.0 python-cinderclient>=1.0.7 -python-neutronclient>=2.3.5,<3 +python-neutronclient>=2.3.6,<3 requests>=1.1 six>=1.7.0 From e2ebeb7fdcb63576db2b59b9c59f782b2a5e7d75 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 4 Aug 2014 00:04:13 -0400 Subject: [PATCH 0158/3494] user create v2.0 depends on tenantId in response User create for v2.0 no longer always contains a tenantId in the response. Add a guard to check for tenantId first before pop'ing it. Change-Id: I428dbc26520bb86efad33768ce04f584217ad168 Closes-Bug: #1352119 --- openstackclient/identity/v2_0/user.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index 60af6ddb98..b291c88258 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -99,9 +99,10 @@ def take_action(self, parsed_args): # NOTE(dtroyer): The users.create() method wants 'tenant_id' but # the returned resource has 'tenantId'. Sigh. # We're using project_id now inside OSC so there. - user._info.update( - {'project_id': user._info.pop('tenantId')} - ) + if 'tenantId' in user._info: + user._info.update( + {'project_id': user._info.pop('tenantId')} + ) info = {} info.update(user._info) From b5001e4b213ef92f31e418203f7e7deeb5e305c0 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 5 Aug 2014 01:31:20 -0400 Subject: [PATCH 0159/3494] Use oslosphinx to generate documentation Rather than host different and possibly out of date versions of static and theme files, use oslosphinx to generate the docs. Change-Id: I7eadc8e40aa10cc26cfd6aece6efa5d13fee70b0 --- doc/source/_static/basic.css | 416 -------------------------- doc/source/_static/default.css | 230 -------------- doc/source/_static/header-line.gif | Bin 48 -> 0 bytes doc/source/_static/header_bg.jpg | Bin 3738 -> 0 bytes doc/source/_static/jquery.tweet.js | 154 ---------- doc/source/_static/nature.css | 245 --------------- doc/source/_static/openstack_logo.png | Bin 3670 -> 0 bytes doc/source/_static/tweaks.css | 94 ------ doc/source/_theme/layout.html | 83 ----- doc/source/_theme/theme.conf | 4 - doc/source/conf.py | 11 +- test-requirements.txt | 1 + 12 files changed, 7 insertions(+), 1231 deletions(-) delete mode 100644 doc/source/_static/basic.css delete mode 100644 doc/source/_static/default.css delete mode 100644 doc/source/_static/header-line.gif delete mode 100644 doc/source/_static/header_bg.jpg delete mode 100644 doc/source/_static/jquery.tweet.js delete mode 100644 doc/source/_static/nature.css delete mode 100644 doc/source/_static/openstack_logo.png delete mode 100644 doc/source/_static/tweaks.css delete mode 100644 doc/source/_theme/layout.html delete mode 100644 doc/source/_theme/theme.conf diff --git a/doc/source/_static/basic.css b/doc/source/_static/basic.css deleted file mode 100644 index d909ce37c7..0000000000 --- a/doc/source/_static/basic.css +++ /dev/null @@ -1,416 +0,0 @@ -/** - * Sphinx stylesheet -- basic theme - * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - */ - -/* -- main layout ----------------------------------------------------------- */ - -div.clearer { - clear: both; -} - -/* -- relbar ---------------------------------------------------------------- */ - -div.related { - width: 100%; - font-size: 90%; -} - -div.related h3 { - display: none; -} - -div.related ul { - margin: 0; - padding: 0 0 0 10px; - list-style: none; -} - -div.related li { - display: inline; -} - -div.related li.right { - float: right; - margin-right: 5px; -} - -/* -- sidebar --------------------------------------------------------------- */ - -div.sphinxsidebarwrapper { - padding: 10px 5px 0 10px; -} - -div.sphinxsidebar { - float: left; - width: 230px; - margin-left: -100%; - font-size: 90%; -} - -div.sphinxsidebar ul { - list-style: none; -} - -div.sphinxsidebar ul ul, -div.sphinxsidebar ul.want-points { - margin-left: 20px; - list-style: square; -} - -div.sphinxsidebar ul ul { - margin-top: 0; - margin-bottom: 0; -} - -div.sphinxsidebar form { - margin-top: 10px; -} - -div.sphinxsidebar input { - border: 1px solid #98dbcc; - font-family: sans-serif; - font-size: 1em; -} - -img { - border: 0; -} - -/* -- search page ----------------------------------------------------------- */ - -ul.search { - margin: 10px 0 0 20px; - padding: 0; -} - -ul.search li { - padding: 5px 0 5px 20px; - background-image: url(file.png); - background-repeat: no-repeat; - background-position: 0 7px; -} - -ul.search li a { - font-weight: bold; -} - -ul.search li div.context { - color: #888; - margin: 2px 0 0 30px; - text-align: left; -} - -ul.keywordmatches li.goodmatch a { - font-weight: bold; -} - -/* -- index page ------------------------------------------------------------ */ - -table.contentstable { - width: 90%; -} - -table.contentstable p.biglink { - line-height: 150%; -} - -a.biglink { - font-size: 1.3em; -} - -span.linkdescr { - font-style: italic; - padding-top: 5px; - font-size: 90%; -} - -/* -- general index --------------------------------------------------------- */ - -table.indextable td { - text-align: left; - vertical-align: top; -} - -table.indextable dl, table.indextable dd { - margin-top: 0; - margin-bottom: 0; -} - -table.indextable tr.pcap { - height: 10px; -} - -table.indextable tr.cap { - margin-top: 10px; - background-color: #f2f2f2; -} - -img.toggler { - margin-right: 3px; - margin-top: 3px; - cursor: pointer; -} - -/* -- general body styles --------------------------------------------------- */ - -a.headerlink { - visibility: hidden; -} - -h1:hover > a.headerlink, -h2:hover > a.headerlink, -h3:hover > a.headerlink, -h4:hover > a.headerlink, -h5:hover > a.headerlink, -h6:hover > a.headerlink, -dt:hover > a.headerlink { - visibility: visible; -} - -div.body p.caption { - text-align: inherit; -} - -div.body td { - text-align: left; -} - -.field-list ul { - padding-left: 1em; -} - -.first { -} - -p.rubric { - margin-top: 30px; - font-weight: bold; -} - -/* -- sidebars -------------------------------------------------------------- */ - -div.sidebar { - margin: 0 0 0.5em 1em; - border: 1px solid #ddb; - padding: 7px 7px 0 7px; - background-color: #ffe; - width: 40%; - float: right; -} - -p.sidebar-title { - font-weight: bold; -} - -/* -- topics ---------------------------------------------------------------- */ - -div.topic { - border: 1px solid #ccc; - padding: 7px 7px 0 7px; - margin: 10px 0 10px 0; -} - -p.topic-title { - font-size: 1.1em; - font-weight: bold; - margin-top: 10px; -} - -/* -- admonitions ----------------------------------------------------------- */ - -div.admonition { - margin-top: 10px; - margin-bottom: 10px; - padding: 7px; -} - -div.admonition dt { - font-weight: bold; -} - -div.admonition dl { - margin-bottom: 0; -} - -p.admonition-title { - margin: 0px 10px 5px 0px; - font-weight: bold; -} - -div.body p.centered { - text-align: center; - margin-top: 25px; -} - -/* -- tables ---------------------------------------------------------------- */ - -table.docutils { - border: 0; - border-collapse: collapse; -} - -table.docutils td, table.docutils th { - padding: 1px 8px 1px 0; - border-top: 0; - border-left: 0; - border-right: 0; - border-bottom: 1px solid #aaa; -} - -table.field-list td, table.field-list th { - border: 0 !important; -} - -table.footnote td, table.footnote th { - border: 0 !important; -} - -th { - text-align: left; - padding-right: 5px; -} - -/* -- other body styles ----------------------------------------------------- */ - -dl { - margin-bottom: 15px; -} - -dd p { - margin-top: 0px; -} - -dd ul, dd table { - margin-bottom: 10px; -} - -dd { - margin-top: 3px; - margin-bottom: 10px; - margin-left: 30px; -} - -dt:target, .highlight { - background-color: #fbe54e; -} - -dl.glossary dt { - font-weight: bold; - font-size: 1.1em; -} - -.field-list ul { - margin: 0; - padding-left: 1em; -} - -.field-list p { - margin: 0; -} - -.refcount { - color: #060; -} - -.optional { - font-size: 1.3em; -} - -.versionmodified { - font-style: italic; -} - -.system-message { - background-color: #fda; - padding: 5px; - border: 3px solid red; -} - -.footnote:target { - background-color: #ffa -} - -.line-block { - display: block; - margin-top: 1em; - margin-bottom: 1em; -} - -.line-block .line-block { - margin-top: 0; - margin-bottom: 0; - margin-left: 1.5em; -} - -/* -- code displays --------------------------------------------------------- */ - -pre { - overflow: auto; -} - -td.linenos pre { - padding: 5px 0px; - border: 0; - background-color: transparent; - color: #aaa; -} - -table.highlighttable { - margin-left: 0.5em; -} - -table.highlighttable td { - padding: 0 0.5em 0 0.5em; -} - -tt.descname { - background-color: transparent; - font-weight: bold; - font-size: 1.2em; -} - -tt.descclassname { - background-color: transparent; -} - -tt.xref, a tt { - background-color: transparent; - font-weight: bold; -} - -h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { - background-color: transparent; -} - -/* -- math display ---------------------------------------------------------- */ - -img.math { - vertical-align: middle; -} - -div.body div.math p { - text-align: center; -} - -span.eqno { - float: right; -} - -/* -- printout stylesheet --------------------------------------------------- */ - -@media print { - div.document, - div.documentwrapper, - div.bodywrapper { - margin: 0 !important; - width: 100%; - } - - div.sphinxsidebar, - div.related, - div.footer, - #top-link { - display: none; - } -} diff --git a/doc/source/_static/default.css b/doc/source/_static/default.css deleted file mode 100644 index c8091ecb4d..0000000000 --- a/doc/source/_static/default.css +++ /dev/null @@ -1,230 +0,0 @@ -/** - * Sphinx stylesheet -- default theme - * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - */ - -@import url("basic.css"); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: sans-serif; - font-size: 100%; - background-color: #11303d; - color: #000; - margin: 0; - padding: 0; -} - -div.document { - background-color: #1c4e63; -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - margin: 0 0 0 230px; -} - -div.body { - background-color: #ffffff; - color: #000000; - padding: 0 20px 30px 20px; -} - -div.footer { - color: #ffffff; - width: 100%; - padding: 9px 0 9px 0; - text-align: center; - font-size: 75%; -} - -div.footer a { - color: #ffffff; - text-decoration: underline; -} - -div.related { - background-color: #133f52; - line-height: 30px; - color: #ffffff; -} - -div.related a { - color: #ffffff; -} - -div.sphinxsidebar { -} - -div.sphinxsidebar h3 { - font-family: 'Trebuchet MS', sans-serif; - color: #ffffff; - font-size: 1.4em; - font-weight: normal; - margin: 0; - padding: 0; -} - -div.sphinxsidebar h3 a { - color: #ffffff; -} - -div.sphinxsidebar h4 { - font-family: 'Trebuchet MS', sans-serif; - color: #ffffff; - font-size: 1.3em; - font-weight: normal; - margin: 5px 0 0 0; - padding: 0; -} - -div.sphinxsidebar p { - color: #ffffff; -} - -div.sphinxsidebar p.topless { - margin: 5px 10px 10px 10px; -} - -div.sphinxsidebar ul { - margin: 10px; - padding: 0; - color: #ffffff; -} - -div.sphinxsidebar a { - color: #98dbcc; -} - -div.sphinxsidebar input { - border: 1px solid #98dbcc; - font-family: sans-serif; - font-size: 1em; -} - -/* -- body styles ----------------------------------------------------------- */ - -a { - color: #355f7c; - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -div.body p, div.body dd, div.body li { - text-align: left; - line-height: 130%; -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: 'Trebuchet MS', sans-serif; - background-color: #f2f2f2; - font-weight: normal; - color: #20435c; - border-bottom: 1px solid #ccc; - margin: 20px -20px 10px -20px; - padding: 3px 0 3px 10px; -} - -div.body h1 { margin-top: 0; font-size: 200%; } -div.body h2 { font-size: 160%; } -div.body h3 { font-size: 140%; } -div.body h4 { font-size: 120%; } -div.body h5 { font-size: 110%; } -div.body h6 { font-size: 100%; } - -a.headerlink { - color: #c60f0f; - font-size: 0.8em; - padding: 0 4px 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - background-color: #c60f0f; - color: white; -} - -div.body p, div.body dd, div.body li { - text-align: left; - line-height: 130%; -} - -div.admonition p.admonition-title + p { - display: inline; -} - -div.admonition p { - margin-bottom: 5px; -} - -div.admonition pre { - margin-bottom: 5px; -} - -div.admonition ul, div.admonition ol { - margin-bottom: 5px; -} - -div.note { - background-color: #eee; - border: 1px solid #ccc; -} - -div.seealso { - background-color: #ffc; - border: 1px solid #ff6; -} - -div.topic { - background-color: #eee; -} - -div.warning { - background-color: #ffe4e4; - border: 1px solid #f66; -} - -p.admonition-title { - display: inline; -} - -p.admonition-title:after { - content: ":"; -} - -pre { - padding: 5px; - background-color: #eeffcc; - color: #333333; - line-height: 120%; - border: 1px solid #ac9; - border-left: none; - border-right: none; -} - -tt { - background-color: #ecf0f3; - padding: 0 1px 0 1px; - font-size: 0.95em; -} - -.warning tt { - background: #efc2c2; -} - -.note tt { - background: #d6d6d6; -} diff --git a/doc/source/_static/header-line.gif b/doc/source/_static/header-line.gif deleted file mode 100644 index 3601730e03488b7b5f92dc992d23ad753357c167..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48 zcmZ?wbhEHbWMg1uXkcVG`smgF|Nj+#vM@3*Ff!;c00Bsbfr-7RpY8O^Kn4bD08FwB Aga7~l diff --git a/doc/source/_static/header_bg.jpg b/doc/source/_static/header_bg.jpg deleted file mode 100644 index f788c41c26481728fa4329c17c87bde36001adc1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3738 zcmd5-YdDna8vedHnM0NtYi6>>At7O=uyTsZup5R_40A9)aXQa}U(l^=gSg=J*&3mKp$aM0r>UIFDe9Zy(vs} zWf)kqO2Y_n0$>ZQ0D&hY4tWjpY?Ii5?V)h*kc0fz?%ZIj3|{;F8E5l%d0)&*Hx~ulvc_*73u8%R zsVMV~ne!JY);&pWott~QIZYJFTXliYc2};JEU{X7W6;ZPfz;)U;U4#mEuK@K*=SC3BR-m&x9(Nna@>b@%FS34|P^jtsXRb5>z9gtPp;_MI2F3o*k z>csA-?CX4b;~4P-*L$+Mmb|51F)eD*wCc`Jt(9}C${Zo=!Uin=u_yMC^;`X!x$##4 z+~}dkT`NF@Uhw0r+6g_)?e!h8IX+OE^C96>UOsv0GPMD6(kr#ljhXRnA=O>Qj@%iT zqBF7aQ*}BG)h@6r0%#azk!r9yrN6>9dq~>KadV$~cGG?Hjk>~it^5rd#zS4KE*p+4 z;;B)%oBK8PNTs=A)a-z`n?3zJ%+h{`=>ijk4sYKr*>`eN1H`~Lo|Tm!o6qN{S* zeNl=NcpGzD55)XnLC|>g)~w={=c#4*x^;mk4Zo_FOFlffP@!?1`c+TogTVR4kp9-q z`d5cMBzNxk6qjPRK9*WY3uHS=bnm_QJvSMBBS_A#3i=ywsg6^|9rfruW0MhdGwHDO z?1gJRMQVecKE^gV{%uo(b)zl^Hd&vmnwFh88h*-?FJ;y=Hdqvt!K|s<$>xlzR=G4{ zZgGOCF43IXS?62B)w*N&dXt%U8X^Bjx}^%Yf>VFpFoKSGP%k?ems;&&J)|Dx(qtQD zu2tS)<_Qz4#LhBKYkl@Og}G)^5+F4P($Fk>)}{uMVv|;Sz2i4$XJ_WTw*;n>3N805rnXhbC52SC={E3rXRlrs|I6f;o|Cn%eje59{axu9sivy4oYmg=j|fLt3<3 zFce84aNb8GbK;y>RbBu71YBcYKL3@M3N25yoE%BtG z^K!`WTQ|fb-Ysa7T)mEw&4_b)PWYgc!)3W)H+neR9o^f|AXdgY1`gN+pvgzbbk`M z*Ts6${7M`2)9XIPy^MoXTiiP2GTp_OtgWMshnH)M&ZSO0)cet!oWo_0_&hV(0?Qdb zdo(sw{I#{hI`SWPM`N=U^#+MgN-*rZ#J7Cm7Jj89`5ehd_{z&9->Jc7$F(X4)&|`K z5rEgd;@dhi-IzJnSVpMd!Gf_G-QW+ zjVMrIas1)g%)GJ;(=oaK};O^)NYdS1`XR?K_;I7qj zhii5}x^he{U3M+GF+WpYws#=Pt#S9xB_X5QE7W+_rQdwMhukJnQj}5cnCz_sIJ#r0 zJa5drkRPI$X(4YdpCswJe#5aN4Jjw3V3Nzt&`lcKBI~#;!>jq7j8y# zvHrFg_#P376A45^hp-KU*P=R;DVdPK*w7D@Gw+`XsSpm^L-VkCooZF61sPAnnjsT# zND4C{>G#P10F_&txEoE!rX%Iy*L}Kna=Q%fDLJ_rF*LujRITZ)$g!?UYLkCXOoz-S z_p`Hny*Rh--l)aYQC&-2dd%;%VKGC1<1DJm_n~`nk4^yS`}&P zM}5bOypW0hwtvrwnE>}g1Mq+B>09qPp1b$hn6kC_iqF`tX#G-t7D$n}Ky9t}sUqiI zOe@odQ?JueZ+sg`-zoQ}J4if6vv1c9x{BDme+F6z{8esU^Kio zK_oPy9}@nlGywSOZy9`^- zzBg>C9|rgWF{pcCogEV@;d}VHrgeBl=5Dr*th4V!1`Z9Zrz9le1zHC#sM3{j#G2R?WMhl6b_yyoEAxX>Zixl$16`+^d$ihNtuIBUafyiCEv#oksNL<4= z*oDXsc7-(ww^9-b-6_|bITySG1N2C-7p0L4+V@R%j=4@ygc=89bmSNy38$S=ZiDyP z0SrqrVA;zi8kYBZ2@Mx(2Lx~-*bc@d1#4R($RJv$9ZTfx_t7Kc|HIHnd&@I386P?& z?d6Vd(48n${cTNFFCoSIUj#O{mmt%M&xCIFmR9Y3f{2UnF4e9@uFZOaYiY|CLdbDa z%xS9x4SHi7Fr-1?CnDqRK?)n&$TTBW5J?O&o{TnNCnLw*{QmT7{c}flSbp9&xi*zF z1TdUn&_!$_WxQbMKGkgsl}B%+N5ZV%Hy6_zJ>dejD89yCBMw9(d}z2fWjYH_nV6!F zqe_rI2H5Pi0^~S6)jjnu%lqZN*eQq6!||a24+edpSH_{C8Ew^g8dw2qdrH!@*E7K* z)00Bb8uUsai%v6Oa^L@3E02r|EG%EdV>q;=#2Q9Wjv3l?dAur$4bzyOl3M6 z1hf%&o*#2R&xnS1z4&R`Uq%`Ut0_P{BOwt;FuDb$1")); - }); - return $(returning); - }, - linkUser: function() { - var returning = []; - var regexp = /[\@]+([A-Za-z0-9-_]+)/gi; - this.each(function() { - returning.push(this.replace(regexp,"@$1")); - }); - return $(returning); - }, - linkHash: function() { - var returning = []; - var regexp = / [\#]+([A-Za-z0-9-_]+)/gi; - this.each(function() { - returning.push(this.replace(regexp, ' #$1')); - }); - return $(returning); - }, - capAwesome: function() { - var returning = []; - this.each(function() { - returning.push(this.replace(/\b(awesome)\b/gi, '$1')); - }); - return $(returning); - }, - capEpic: function() { - var returning = []; - this.each(function() { - returning.push(this.replace(/\b(epic)\b/gi, '$1')); - }); - return $(returning); - }, - makeHeart: function() { - var returning = []; - this.each(function() { - returning.push(this.replace(/(<)+[3]/gi, "")); - }); - return $(returning); - } - }); - - function relative_time(time_value) { - var parsed_date = Date.parse(time_value); - var relative_to = (arguments.length > 1) ? arguments[1] : new Date(); - var delta = parseInt((relative_to.getTime() - parsed_date) / 1000); - var pluralize = function (singular, n) { - return '' + n + ' ' + singular + (n == 1 ? '' : 's'); - }; - if(delta < 60) { - return 'less than a minute ago'; - } else if(delta < (45*60)) { - return 'about ' + pluralize("minute", parseInt(delta / 60)) + ' ago'; - } else if(delta < (24*60*60)) { - return 'about ' + pluralize("hour", parseInt(delta / 3600)) + ' ago'; - } else { - return 'about ' + pluralize("day", parseInt(delta / 86400)) + ' ago'; - } - } - - function build_url() { - var proto = ('https:' == document.location.protocol ? 'https:' : 'http:'); - if (s.list) { - return proto+"//api.twitter.com/1/"+s.username[0]+"/lists/"+s.list+"/statuses.json?per_page="+s.count+"&callback=?"; - } else if (s.query == null && s.username.length == 1) { - return proto+'//twitter.com/status/user_timeline/'+s.username[0]+'.json?count='+s.count+'&callback=?'; - } else { - var query = (s.query || 'from:'+s.username.join('%20OR%20from:')); - return proto+'//search.twitter.com/search.json?&q='+query+'&rpp='+s.count+'&callback=?'; - } - } - - return this.each(function(){ - var list = $('
      ').appendTo(this); - var intro = '

      '+s.intro_text+'

      '; - var outro = '

      '+s.outro_text+'

      '; - var loading = $('

      '+s.loading_text+'

      '); - - if(typeof(s.username) == "string"){ - s.username = [s.username]; - } - - if (s.loading_text) $(this).append(loading); - $.getJSON(build_url(), function(data){ - if (s.loading_text) loading.remove(); - if (s.intro_text) list.before(intro); - $.each((data.results || data), function(i,item){ - // auto join text based on verb tense and content - if (s.join_text == "auto") { - if (item.text.match(/^(@([A-Za-z0-9-_]+)) .*/i)) { - var join_text = s.auto_join_text_reply; - } else if (item.text.match(/(^\w+:\/\/[A-Za-z0-9-_]+\.[A-Za-z0-9-_:%&\?\/.=]+) .*/i)) { - var join_text = s.auto_join_text_url; - } else if (item.text.match(/^((\w+ed)|just) .*/im)) { - var join_text = s.auto_join_text_ed; - } else if (item.text.match(/^(\w*ing) .*/i)) { - var join_text = s.auto_join_text_ing; - } else { - var join_text = s.auto_join_text_default; - } - } else { - var join_text = s.join_text; - }; - - var from_user = item.from_user || item.user.screen_name; - var profile_image_url = item.profile_image_url || item.user.profile_image_url; - var join_template = ' '+join_text+' '; - var join = ((s.join_text) ? join_template : ' '); - var avatar_template = ''+from_user+'\'s avatar'; - var avatar = (s.avatar_size ? avatar_template : ''); - var date = ''+relative_time(item.created_at)+''; - var text = '' +$([item.text]).linkUrl().linkUser().linkHash().makeHeart().capAwesome().capEpic()[0]+ ''; - - // until we create a template option, arrange the items below to alter a tweet's display. - list.append('
    • ' + avatar + date + join + text + '
    • '); - - list.children('li:first').addClass('tweet_first'); - list.children('li:odd').addClass('tweet_even'); - list.children('li:even').addClass('tweet_odd'); - }); - if (s.outro_text) list.after(outro); - }); - - }); - }; -})(jQuery); \ No newline at end of file diff --git a/doc/source/_static/nature.css b/doc/source/_static/nature.css deleted file mode 100644 index a98bd4209d..0000000000 --- a/doc/source/_static/nature.css +++ /dev/null @@ -1,245 +0,0 @@ -/* - * nature.css_t - * ~~~~~~~~~~~~ - * - * Sphinx stylesheet -- nature theme. - * - * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -@import url("basic.css"); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: Arial, sans-serif; - font-size: 100%; - background-color: #111; - color: #555; - margin: 0; - padding: 0; -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - margin: 0 0 0 {{ theme_sidebarwidth|toint }}px; -} - -hr { - border: 1px solid #B1B4B6; -} - -div.document { - background-color: #eee; -} - -div.body { - background-color: #ffffff; - color: #3E4349; - padding: 0 30px 30px 30px; - font-size: 0.9em; -} - -div.footer { - color: #555; - width: 100%; - padding: 13px 0; - text-align: center; - font-size: 75%; -} - -div.footer a { - color: #444; - text-decoration: underline; -} - -div.related { - background-color: #6BA81E; - line-height: 32px; - color: #fff; - text-shadow: 0px 1px 0 #444; - font-size: 0.9em; -} - -div.related a { - color: #E2F3CC; -} - -div.sphinxsidebar { - font-size: 0.75em; - line-height: 1.5em; -} - -div.sphinxsidebarwrapper{ - padding: 20px 0; -} - -div.sphinxsidebar h3, -div.sphinxsidebar h4 { - font-family: Arial, sans-serif; - color: #222; - font-size: 1.2em; - font-weight: normal; - margin: 0; - padding: 5px 10px; - background-color: #ddd; - text-shadow: 1px 1px 0 white -} - -div.sphinxsidebar h4{ - font-size: 1.1em; -} - -div.sphinxsidebar h3 a { - color: #444; -} - - -div.sphinxsidebar p { - color: #888; - padding: 5px 20px; -} - -div.sphinxsidebar p.topless { -} - -div.sphinxsidebar ul { - margin: 10px 20px; - padding: 0; - color: #000; -} - -div.sphinxsidebar a { - color: #444; -} - -div.sphinxsidebar input { - border: 1px solid #ccc; - font-family: sans-serif; - font-size: 1em; -} - -div.sphinxsidebar input[type=text]{ - margin-left: 20px; -} - -/* -- body styles ----------------------------------------------------------- */ - -a { - color: #005B81; - text-decoration: none; -} - -a:hover { - color: #E32E00; - text-decoration: underline; -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: Arial, sans-serif; - background-color: #BED4EB; - font-weight: normal; - color: #212224; - margin: 30px 0px 10px 0px; - padding: 5px 0 5px 10px; - text-shadow: 0px 1px 0 white -} - -div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; } -div.body h2 { font-size: 150%; background-color: #C8D5E3; } -div.body h3 { font-size: 120%; background-color: #D8DEE3; } -div.body h4 { font-size: 110%; background-color: #D8DEE3; } -div.body h5 { font-size: 100%; background-color: #D8DEE3; } -div.body h6 { font-size: 100%; background-color: #D8DEE3; } - -a.headerlink { - color: #c60f0f; - font-size: 0.8em; - padding: 0 4px 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - background-color: #c60f0f; - color: white; -} - -div.body p, div.body dd, div.body li { - line-height: 1.5em; -} - -div.admonition p.admonition-title + p { - display: inline; -} - -div.highlight{ - background-color: white; -} - -div.note { - background-color: #eee; - border: 1px solid #ccc; -} - -div.seealso { - background-color: #ffc; - border: 1px solid #ff6; -} - -div.topic { - background-color: #eee; -} - -div.warning { - background-color: #ffe4e4; - border: 1px solid #f66; -} - -p.admonition-title { - display: inline; -} - -p.admonition-title:after { - content: ":"; -} - -pre { - padding: 10px; - background-color: White; - color: #222; - line-height: 1.2em; - border: 1px solid #C6C9CB; - font-size: 1.1em; - margin: 1.5em 0 1.5em 0; - -webkit-box-shadow: 1px 1px 1px #d8d8d8; - -moz-box-shadow: 1px 1px 1px #d8d8d8; -} - -tt { - background-color: #ecf0f3; - color: #222; - /* padding: 1px 2px; */ - font-size: 1.1em; - font-family: monospace; -} - -.viewcode-back { - font-family: Arial, sans-serif; -} - -div.viewcode-block:target { - background-color: #f4debf; - border-top: 1px solid #ac9; - border-bottom: 1px solid #ac9; -} diff --git a/doc/source/_static/openstack_logo.png b/doc/source/_static/openstack_logo.png deleted file mode 100644 index 146faec5cfe3773824f4caf39e4480e4974d10df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3670 zcmV-c4yo~pP)CW75Qp#l)U;+N6jaIz6Nf$t6dNV>^>ETzcpQ=%tMaf0k|rg72+IW`z$FyfE+D{1@tt$t5DmX)*;QV?c;%+5Z&egAgfXTQJq-mZkC z>pFAHu}U=Axde_?s!99ZfDg_+9TYzDa6N1R3adhx&2Mb7>9w`KpMNz!>U5t2XQ8lZ zu+!+H7(PRwF@jAkwvI;|8|=Z_dfzV`Kpi;I!e=|Ql+HAdEag?VZ^Ilw9XJj9N1#1a z?UFC!)X62`CRIe^9YCLKbJ` z&O@f0zt{Z1YDF1utg2$F+rzvrncys+g37Xsd8)idSW(=}t#~qF#qBo29*@^ZCs<$W zpa144=o4g0z63h_ttPfIpH-FyG^MAH+6B~r$(4qw+Uv{2d#h`$lq+i+#Tf%CAzDFUh!pzX(6nW{EASJAQkhm!+}aGpHc z;(+N`S*@tYmump1T37E}J;!$0#F>^M*mT_X1x~bvnp&qP9IHI#bj-0z8FR+=p+e#*w3ugV#wX``sR-CI1!YiQsfc@Om<;1MBw zlfqH9z4Q|m*C?URU1OG(`UYn>Q8<|I!mby#FlN5MMFE8;Pyh$skbR?ngFLt?%nWSkS-#W5umy>@^DyAERP~{E&`M%0(qi&((^ahqL}u^jT<2dcf)p< z%Fxc9J$nh_`>_oNYC?oy`rIDY46Yrw4si3Qn~oXV%dJ}IlUD-40>QipyGa_dV0Z%J ztcEXm5yxR0gySJ04{nnbm#vP=Hq&GI<8VxcZ34pRjt6m%pE2H|!+HBJQrdBdyKHJR z2O_}hp!5bXuwniQYTF>yI|=cjT+2l`9T3|H+l4%ryPxWQm(ODW#8Ctj_CplcO=)qj zD#d~V6BahR9NY1kE5rF)_j<|!Cqnpq0uOKhL%w z>y8OyeTM1?REXc{0|3b=#WPZneh80PxL=Ljau1~+CgtMgg-vccMDX-L z9^7An_;!lFAi`#G_1F*OdM|Z$EVQs0m0$?mY}(baOZ%Zpd62#Pyg!3Jd4d zD^8+lSir&T6Y9-p9L#Wz6$5nXLjdOl?7Lv!TeMr}F14ranauW9=L>ubu*x>Bcrgwp zjrT@{rL*2Fc}Ilwn07QvdJfMOO2=(1Px)6&ih7lg839!Bx&}lQER~T`^7_x@fXo({ zCZMeZYt*!VgMTg>PR)PBaIwubzRY%jjE`-s zG;B}>2!lD=QLOTfQOEZKIEz*;yTJ9(Af0zNv;IDq7#Fr#W{Ap+7Sq1N3TL21X|h2t z=Dk>^bGSsRX-u+cZ23mMB_Ioc0yNIfcfLWB>$hVU3W3>d&a?IM+bGRGt+t}aiv(eh z(D6Z9N>U2|Qxle(!UVTeEKE6W))3WI5z48Rs8d5v0GwmyC8iQiUJO8KS?QwHl2abL zNW+hadDdPc8z%MSOG$l&WR@!!&M{WLmrnS=-0G#&`a)chX>mN9W1>|yqve@lL8a`f zXRmn$B8P=dLxE!2rIi}a*gh%FI4j?C;b@L=WgypiTRf==n6DKr9mUExo6a@{wLM-I z9%V9{!;5G!<8fMYikfEbrGXRQN-9*24}kIIpP&dEg@fiLqAY5|jjv}$P3x0avZODU zdX`c|G>h`1f=3uEu)L9C)H5%frni#HZXcX`TD{iQ-e2qXxj_f%|WW;byDMc%7+uBy}Y?KLC?jp%yyyeBNkqQ-*osw2ex&97Q{#C7%CdSDMNIV zTdC(LEm?&qPcNOjM)h9Grs|M(gsuhV8@96?m4WkQ>j{bJIs)m^neL%ua!i+N8>Lh+ zKu#7rF~VOH@hb{zGXYwys!Um4Vkf+H8Hj6?^eI%kT%j+HA0K=6qdQ@nfR57Q`Jm9T zc)Yg9-`e~BRE!xoKZ z=mP|0Kihr}V1$5sHw$QekmoL)lQ;~@H$S)}s3xuwypiubB?1%OyBpwC08TH!=?BrQ zhOp`PTu;%u0}Q=XKGb7d$g8*;de8c1UI|Re2R;;Radh_D!FIZg+JP`oJg>5 z;&B7eVAomZe>j~hOOIVRO_Q7eSGz37hxmnsG!n%HX`C6gSqFcg(RLmikn%EPR*wel zrsc;>!vQ<>2ZW`lk`MbNLopFd#_9mh8iKPH;KbjC@xJU${pdxuTF{uO(eG#9t*>XP z_4Seh`r_#q$^xeiuy(=eSouv66cpS!t3n`|j`6xnmSs1q@;0!I)m<6eYHHGMRdB87 ziruozT=gn@yp`B9oGxD-b7PqhZum|oJCfLB38&8v51ijj-Pb`qvCr3FtJ0aFms2h3(n0-}3jJ~J$ zCzep7-MIZFbo$(m8zWm?SoRl__blLE+!fFBVVk1&XLg+vmVNcTk9O2+q?x#F0LZUN zu6oM~C)(7^0|az4nM}@aZf<@RkH0CR8<-Yn-fZe+Dbr#iJWSt#tnR4^h<@ePXWmeHIO4q^X zCbiy(=k3R1o1}0E+7x*OOe-qnIXG{#N_rqK*1NH}Qz6aumTR`YTgo5K=q=61;5@b- zrgUA_Qz=)(TPN!tCZE|{?B0*r9ov5Fcip6xQ2;Yqs*2_o7TFKGp0|~bcP@6+a(rz^ zXXmmyBfT}ucw_t(6s+f^t_)nc>RKW<-q_&J35vN+RPLsR?VAsQeHLyCR7AWvxFOVc zAg-xl=j*RipzaKWx3lAf?ei`PoM;bbAL>svH?JqQwjSulb9bghytRt%*5x-no>xlf zh7qj0LYRXVDU})?Btsy7^71*ujsEP_ACyd)P)*ULWBCXox@PUfwmQ#)Vl&oeIqpQY zHMgU+xe0EhQ)RmjdB3JHGdrsvJ9?A=WwOrn)J?BH{+D&O_@SKdrj2|8Z{hS1T(k>&Zlt;p=tqw*mVY1aLt=u^eAHkW>8cb#@q& z4-SLa@ii zCt7NGrLv)1Scy9ew-sOwwLYn2a6T#KzJgnbacm7Z20q6tcs~C!0DI+r(=$l+x{=W0A}~0&W)ll4*&oF07*qoM6N<$f~n6U7ytkO diff --git a/doc/source/_static/tweaks.css b/doc/source/_static/tweaks.css deleted file mode 100644 index 3f3fb3f071..0000000000 --- a/doc/source/_static/tweaks.css +++ /dev/null @@ -1,94 +0,0 @@ -body { - background: #fff url(../_static/header_bg.jpg) top left no-repeat; -} - -#header { - width: 950px; - margin: 0 auto; - height: 102px; -} - -#header h1#logo { - background: url(../_static/openstack_logo.png) top left no-repeat; - display: block; - float: left; - text-indent: -9999px; - width: 175px; - height: 55px; -} - -#navigation { - background: url(../_static/header-line.gif) repeat-x 0 bottom; - display: block; - float: left; - margin: 27px 0 0 25px; - padding: 0; -} - -#navigation li{ - float: left; - display: block; - margin-right: 25px; -} - -#navigation li a { - display: block; - font-weight: normal; - text-decoration: none; - background-position: 50% 0; - padding: 20px 0 5px; - color: #353535; - font-size: 14px; -} - -#navigation li a.current, #navigation li a.section { - border-bottom: 3px solid #cf2f19; - color: #cf2f19; -} - -div.related { - background-color: #cde2f8; - border: 1px solid #b0d3f8; -} - -div.related a { - color: #4078ba; - text-shadow: none; -} - -div.sphinxsidebarwrapper { - padding-top: 0; -} - -pre { - color: #555; -} - -div.documentwrapper h1, div.documentwrapper h2, div.documentwrapper h3, div.documentwrapper h4, div.documentwrapper h5, div.documentwrapper h6 { - font-family: 'PT Sans', sans-serif !important; - color: #264D69; - border-bottom: 1px dotted #C5E2EA; - padding: 0; - background: none; - padding-bottom: 5px; -} - -div.documentwrapper h3 { - color: #CF2F19; -} - -a.headerlink { - color: #fff !important; - margin-left: 5px; - background: #CF2F19 !important; -} - -div.body { - margin-top: -25px; - margin-left: 230px; -} - -div.document { - width: 960px; - margin: 0 auto; -} \ No newline at end of file diff --git a/doc/source/_theme/layout.html b/doc/source/_theme/layout.html deleted file mode 100644 index 750b782211..0000000000 --- a/doc/source/_theme/layout.html +++ /dev/null @@ -1,83 +0,0 @@ -{% extends "basic/layout.html" %} -{% set css_files = css_files + ['_static/tweaks.css'] %} -{% set script_files = script_files + ['_static/jquery.tweet.js'] %} - -{%- macro sidebar() %} - {%- if not embedded %}{% if not theme_nosidebar|tobool %} -
      -
      - {%- block sidebarlogo %} - {%- if logo %} - - {%- endif %} - {%- endblock %} - {%- block sidebartoc %} - {%- if display_toc %} -

      {{ _('Table Of Contents') }}

      - {{ toc }} - {%- endif %} - {%- endblock %} - {%- block sidebarrel %} - {%- if prev %} -

      {{ _('Previous topic') }}

      -

      {{ prev.title }}

      - {%- endif %} - {%- if next %} -

      {{ _('Next topic') }}

      -

      {{ next.title }}

      - {%- endif %} - {%- endblock %} - {%- block sidebarsourcelink %} - {%- if show_source and has_source and sourcename %} -

      {{ _('This Page') }}

      - - {%- endif %} - {%- endblock %} - {%- if customsidebar %} - {% include customsidebar %} - {%- endif %} - {%- block sidebarsearch %} - {%- if pagename != "search" %} - - - {%- endif %} - {%- endblock %} -
      -
      - {%- endif %}{% endif %} -{%- endmacro %} - -{% block relbar1 %}{% endblock relbar1 %} - -{% block header %} - -{% endblock %} \ No newline at end of file diff --git a/doc/source/_theme/theme.conf b/doc/source/_theme/theme.conf deleted file mode 100644 index 1cc4004464..0000000000 --- a/doc/source/_theme/theme.conf +++ /dev/null @@ -1,4 +0,0 @@ -[theme] -inherit = basic -stylesheet = nature.css -pygments_style = tango diff --git a/doc/source/conf.py b/doc/source/conf.py index fcce1daacc..6777f13a5f 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -32,10 +32,11 @@ extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', - 'sphinx.ext.todo'] + 'sphinx.ext.todo', + 'oslosphinx'] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +#templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' @@ -101,8 +102,8 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme_path = ["."] -html_theme = '_theme' +#html_theme_path = ["."] +#html_theme = '_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -131,7 +132,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +#html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. diff --git a/test-requirements.txt b/test-requirements.txt index 1b872c3b30..0b1acd9b50 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,6 +4,7 @@ coverage>=3.6 discover fixtures>=0.3.14 mock>=1.0 +oslosphinx sphinx>=1.1.2,!=1.2.0,<1.3 testrepository>=0.0.18 testtools>=0.9.34 From ddb7e18974964abbe3fc50dab47ab55286a7328c Mon Sep 17 00:00:00 2001 From: wanghong Date: Thu, 7 Aug 2014 11:14:00 +0800 Subject: [PATCH 0160/3494] test_find_resource fails if run alone Currently, we set 'NAME_ATTR' attribute for Volume and Snapshot class in volume.client.py. When we test test_find_resource alone, the Volume and Snapshot class do not have 'NAME_ATTR' attribute since we do not import volume.client, which causes the tests to fail. Change-Id: I06f727ffa8d37afe1a1191c36574887fecc7a733 Closes-Bug: #1353788 --- openstackclient/tests/volume/test_find_resource.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openstackclient/tests/volume/test_find_resource.py b/openstackclient/tests/volume/test_find_resource.py index 8539070f14..56081966b7 100644 --- a/openstackclient/tests/volume/test_find_resource.py +++ b/openstackclient/tests/volume/test_find_resource.py @@ -21,6 +21,7 @@ from openstackclient.common import exceptions from openstackclient.common import utils from openstackclient.tests import utils as test_utils +from openstackclient.volume import client # noqa ID = '1after909' From 8af26a51c3a9ad5808d7c69a07d652121dab27c1 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Sun, 3 Aug 2014 03:47:10 -0400 Subject: [PATCH 0161/3494] Add commands for object upload and delete Add commands to upload an object to a container, and to delete an object from a container. Change-Id: I37c02315495bba5abe612733d1109a3d4ce256a1 implements: bp swift-client --- openstackclient/object/v1/lib/object.py | 46 ++++++++++++++++++ openstackclient/object/v1/object.py | 63 +++++++++++++++++++++++++ setup.cfg | 2 + 3 files changed, 111 insertions(+) diff --git a/openstackclient/object/v1/lib/object.py b/openstackclient/object/v1/lib/object.py index ffc7e9b8ed..2473caa39a 100644 --- a/openstackclient/object/v1/lib/object.py +++ b/openstackclient/object/v1/lib/object.py @@ -24,6 +24,52 @@ from urlparse import urlparse # noqa +def create_object( + api, + url, + container, + object, +): + """Create an object, upload it to a container + + :param api: a restapi object + :param url: endpoint + :param container: name of container to store object + :param object: local path to object + :returns: dict of returned headers + """ + + full_url = "%s/%s/%s" % (url, container, object) + response = api.put(full_url, data=open(object)) + url_parts = urlparse(url) + data = { + 'account': url_parts.path.split('/')[-1], + 'container': container, + 'object': object, + } + data['x-trans-id'] = response.headers.get('X-Trans-Id', None) + data['etag'] = response.headers.get('Etag', None) + + return data + + +def delete_object( + api, + url, + container, + object, +): + """Delete an object stored in a container + + :param api: a restapi object + :param url: endpoint + :param container: name of container that stores object + :param container: name of object to delete + """ + + api.delete("%s/%s/%s" % (url, container, object)) + + def list_objects( api, url, diff --git a/openstackclient/object/v1/object.py b/openstackclient/object/v1/object.py index ee30c84210..4a99a8f191 100644 --- a/openstackclient/object/v1/object.py +++ b/openstackclient/object/v1/object.py @@ -19,6 +19,7 @@ import logging import six +from cliff import command from cliff import lister from cliff import show @@ -26,6 +27,68 @@ from openstackclient.object.v1.lib import object as lib_object +class CreateObject(show.ShowOne): + """Upload an object to a container""" + + log = logging.getLogger(__name__ + '.CreateObject') + + def get_parser(self, prog_name): + parser = super(CreateObject, self).get_parser(prog_name) + parser.add_argument( + 'container', + metavar='', + help='Container to store new object', + ) + parser.add_argument( + 'object', + metavar='', + help='Local path of object to upload', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + data = lib_object.create_object( + self.app.restapi, + self.app.client_manager.object_store.endpoint, + parsed_args.container, + parsed_args.object, + ) + + return zip(*sorted(six.iteritems(data))) + + +class DeleteObject(command.Command): + """Delete an object within a container""" + + log = logging.getLogger(__name__ + '.DeleteObject') + + def get_parser(self, prog_name): + parser = super(DeleteObject, self).get_parser(prog_name) + parser.add_argument( + 'container', + metavar='', + help='Container that stores the object to delete', + ) + parser.add_argument( + 'object', + metavar='', + help='Object to delete', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + lib_object.delete_object( + self.app.restapi, + self.app.client_manager.object_store.endpoint, + parsed_args.container, + parsed_args.object, + ) + + class ListObject(lister.Lister): """List objects""" diff --git a/setup.cfg b/setup.cfg index 7f07286c0d..b1b80a06bd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -272,6 +272,8 @@ openstack.object_store.v1 = container_delete = openstackclient.object.v1.container:DeleteContainer container_list = openstackclient.object.v1.container:ListContainer container_show = openstackclient.object.v1.container:ShowContainer + object_create = openstackclient.object.v1.object:CreateObject + object_delete = openstackclient.object.v1.object:DeleteObject object_list = openstackclient.object.v1.object:ListObject object_show = openstackclient.object.v1.object:ShowObject From 19b8605224156c48107541580a264860131b57ab Mon Sep 17 00:00:00 2001 From: wanghong Date: Tue, 12 Aug 2014 19:36:57 +0800 Subject: [PATCH 0162/3494] a mistake in tests/identity/v3/test_role.py Change test_service_show to test_role_show. Change-Id: Ieef7fdeb9401b4dc28720c9ba14bf460ac171288 --- openstackclient/tests/identity/v3/test_role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstackclient/tests/identity/v3/test_role.py b/openstackclient/tests/identity/v3/test_role.py index fa02ecb9ca..3d2a402bc7 100644 --- a/openstackclient/tests/identity/v3/test_role.py +++ b/openstackclient/tests/identity/v3/test_role.py @@ -715,7 +715,7 @@ def setUp(self): # Get the command object to test self.cmd = role.ShowRole(self.app, None) - def test_service_show(self): + def test_role_show(self): arglist = [ identity_fakes.role_name, ] From 2dc060cff3b3f85582347d70eb594b1570e0dc7d Mon Sep 17 00:00:00 2001 From: wanghong Date: Tue, 12 Aug 2014 19:29:21 +0800 Subject: [PATCH 0163/3494] add tests for identity v3 domain Change-Id: I478215f62b51e6e73283f0304ea1b0736177d1b1 --- openstackclient/identity/v3/domain.py | 11 +- openstackclient/tests/identity/v3/fakes.py | 3 + .../tests/identity/v3/test_domain.py | 414 ++++++++++++++++++ 3 files changed, 423 insertions(+), 5 deletions(-) create mode 100644 openstackclient/tests/identity/v3/test_domain.py diff --git a/openstackclient/identity/v3/domain.py b/openstackclient/identity/v3/domain.py index f976384738..49397afc46 100644 --- a/openstackclient/identity/v3/domain.py +++ b/openstackclient/identity/v3/domain.py @@ -135,13 +135,12 @@ def get_parser(self, prog_name): '--enable', dest='enabled', action='store_true', - default=True, help='Enable domain (default)', ) enable_group.add_argument( '--disable', - dest='enabled', - action='store_false', + dest='disabled', + action='store_true', help='Disable domain', ) return parser @@ -156,8 +155,10 @@ def take_action(self, parsed_args): kwargs['name'] = parsed_args.name if parsed_args.description: kwargs['description'] = parsed_args.description - if 'enabled' in parsed_args: - kwargs['enabled'] = parsed_args.enabled + if parsed_args.enabled: + kwargs['enabled'] = True + if parsed_args.disabled: + kwargs['enabled'] = False if not kwargs: sys.stdout.write("Domain not updated, no arguments present") diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index 604171570d..4c43bacb81 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -21,10 +21,13 @@ domain_id = 'd1' domain_name = 'oftheking' +domain_description = 'domain description' DOMAIN = { 'id': domain_id, 'name': domain_name, + 'description': domain_description, + 'enabled': True, } group_id = 'gr-010' diff --git a/openstackclient/tests/identity/v3/test_domain.py b/openstackclient/tests/identity/v3/test_domain.py new file mode 100644 index 0000000000..8dad5bcc7a --- /dev/null +++ b/openstackclient/tests/identity/v3/test_domain.py @@ -0,0 +1,414 @@ +# 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 copy + +from openstackclient.identity.v3 import domain +from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes + + +class TestDomain(identity_fakes.TestIdentityv3): + + def setUp(self): + super(TestDomain, self).setUp() + + # Get a shortcut to the DomainManager Mock + self.domains_mock = self.app.client_manager.identity.domains + self.domains_mock.reset_mock() + + +class TestDomainCreate(TestDomain): + + def setUp(self): + super(TestDomainCreate, self).setUp() + + self.domains_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.DOMAIN), + loaded=True, + ) + + # Get the command object to test + self.cmd = domain.CreateDomain(self.app, None) + + def test_domain_create_no_options(self): + arglist = [ + identity_fakes.domain_name, + ] + verifylist = [ + ('enabled', True), + ('name', identity_fakes.domain_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': identity_fakes.domain_name, + 'description': None, + 'enabled': True, + } + self.domains_mock.create.assert_called_with( + **kwargs + ) + + collist = ('description', 'enabled', 'id', 'name') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.domain_description, + True, + identity_fakes.domain_id, + identity_fakes.domain_name, + ) + self.assertEqual(datalist, data) + + def test_domain_create_description(self): + arglist = [ + '--description', 'new desc', + identity_fakes.domain_name, + ] + verifylist = [ + ('description', 'new desc'), + ('enabled', True), + ('name', identity_fakes.domain_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': identity_fakes.domain_name, + 'description': 'new desc', + 'enabled': True, + } + self.domains_mock.create.assert_called_with( + **kwargs + ) + + collist = ('description', 'enabled', 'id', 'name') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.domain_description, + True, + identity_fakes.domain_id, + identity_fakes.domain_name, + ) + self.assertEqual(datalist, data) + + def test_domain_create_enable(self): + arglist = [ + '--enable', + identity_fakes.domain_name, + ] + verifylist = [ + ('enabled', True), + ('name', identity_fakes.domain_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': identity_fakes.domain_name, + 'description': None, + 'enabled': True, + } + self.domains_mock.create.assert_called_with( + **kwargs + ) + + collist = ('description', 'enabled', 'id', 'name') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.domain_description, + True, + identity_fakes.domain_id, + identity_fakes.domain_name, + ) + self.assertEqual(datalist, data) + + def test_domain_create_disable(self): + arglist = [ + '--disable', + identity_fakes.domain_name, + ] + verifylist = [ + ('enabled', False), + ('name', identity_fakes.domain_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': identity_fakes.domain_name, + 'description': None, + 'enabled': False, + } + self.domains_mock.create.assert_called_with( + **kwargs + ) + + collist = ('description', 'enabled', 'id', 'name') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.domain_description, + True, + identity_fakes.domain_id, + identity_fakes.domain_name, + ) + self.assertEqual(datalist, data) + + +class TestDomainDelete(TestDomain): + + def setUp(self): + super(TestDomainDelete, self).setUp() + + # This is the return value for utils.find_resource() + self.domains_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.DOMAIN), + loaded=True, + ) + self.domains_mock.delete.return_value = None + + # Get the command object to test + self.cmd = domain.DeleteDomain(self.app, None) + + def test_domain_delete(self): + arglist = [ + identity_fakes.domain_id, + ] + verifylist = [ + ('domain', identity_fakes.domain_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(0, result) + + self.domains_mock.delete.assert_called_with( + identity_fakes.domain_id, + ) + + +class TestDomainList(TestDomain): + + def setUp(self): + super(TestDomainList, self).setUp() + + self.domains_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.DOMAIN), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = domain.ListDomain(self.app, None) + + def test_domain_list_no_options(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + self.domains_mock.list.assert_called_with() + + collist = ('ID', 'Name', 'Enabled', 'Description') + self.assertEqual(collist, columns) + datalist = (( + identity_fakes.domain_id, + identity_fakes.domain_name, + True, + identity_fakes.domain_description, + ), ) + self.assertEqual(datalist, tuple(data)) + + +class TestDomainSet(TestDomain): + + def setUp(self): + super(TestDomainSet, self).setUp() + + self.domains_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.DOMAIN), + loaded=True, + ) + + self.domains_mock.update.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.DOMAIN), + loaded=True, + ) + + # Get the command object to test + self.cmd = domain.SetDomain(self.app, None) + + def test_domain_set_no_options(self): + arglist = [ + identity_fakes.domain_name, + ] + verifylist = [ + ('domain', identity_fakes.domain_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(0, result) + + self.assertNotCalled(self.domains_mock.update) + + def test_domain_set_name(self): + arglist = [ + '--name', 'qwerty', + identity_fakes.domain_id, + ] + verifylist = [ + ('name', 'qwerty'), + ('domain', identity_fakes.domain_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(0, result) + + # Set expected values + kwargs = { + 'name': 'qwerty', + } + self.domains_mock.update.assert_called_with( + identity_fakes.domain_id, + **kwargs + ) + + def test_domain_set_description(self): + arglist = [ + '--description', 'new desc', + identity_fakes.domain_id, + ] + verifylist = [ + ('description', 'new desc'), + ('domain', identity_fakes.domain_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(0, result) + + # Set expected values + kwargs = { + 'description': 'new desc', + } + self.domains_mock.update.assert_called_with( + identity_fakes.domain_id, + **kwargs + ) + + def test_domain_set_enable(self): + arglist = [ + '--enable', + identity_fakes.domain_id, + ] + verifylist = [ + ('enabled', True), + ('domain', identity_fakes.domain_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(0, result) + + # Set expected values + kwargs = { + 'enabled': True, + } + self.domains_mock.update.assert_called_with( + identity_fakes.domain_id, + **kwargs + ) + + def test_domain_set_disable(self): + arglist = [ + '--disable', + identity_fakes.domain_id, + ] + verifylist = [ + ('disabled', True), + ('domain', identity_fakes.domain_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(0, result) + + # Set expected values + kwargs = { + 'enabled': False, + } + self.domains_mock.update.assert_called_with( + identity_fakes.domain_id, + **kwargs + ) + + +class TestDomainShow(TestDomain): + + def setUp(self): + super(TestDomainShow, self).setUp() + + self.domains_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.DOMAIN), + loaded=True, + ) + + # Get the command object to test + self.cmd = domain.ShowDomain(self.app, None) + + def test_domain_show(self): + arglist = [ + identity_fakes.domain_id, + ] + verifylist = [ + ('domain', identity_fakes.domain_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + self.domains_mock.get.assert_called_with( + identity_fakes.domain_id, + ) + + collist = ('description', 'enabled', 'id', 'name') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.domain_description, + True, + identity_fakes.domain_id, + identity_fakes.domain_name, + ) + self.assertEqual(datalist, data) From 99ad9ef92e73d49bb6966a5a2a01ca6ccaf9d135 Mon Sep 17 00:00:00 2001 From: wanghong Date: Fri, 1 Aug 2014 11:02:15 +0800 Subject: [PATCH 0164/3494] add tests for identity v3 endpoint Change-Id: I1479460473656ea4e2a48a976808371e840b49c1 Closes-Bug: #1348867 --- openstackclient/identity/v3/endpoint.py | 16 +- openstackclient/tests/identity/v3/fakes.py | 16 + .../tests/identity/v3/test_endpoint.py | 575 ++++++++++++++++++ 3 files changed, 602 insertions(+), 5 deletions(-) create mode 100644 openstackclient/tests/identity/v3/test_endpoint.py diff --git a/openstackclient/identity/v3/endpoint.py b/openstackclient/identity/v3/endpoint.py index 4ea44e7a72..5ab5dac466 100644 --- a/openstackclient/identity/v3/endpoint.py +++ b/openstackclient/identity/v3/endpoint.py @@ -165,13 +165,12 @@ def get_parser(self, prog_name): '--enable', dest='enabled', action='store_true', - default=True, help='Enable endpoint', ) enable_group.add_argument( '--disable', - dest='enabled', - action='store_false', + dest='disabled', + action='store_true', help='Disable endpoint', ) return parser @@ -183,7 +182,8 @@ def take_action(self, parsed_args): parsed_args.endpoint) if (not parsed_args.interface and not parsed_args.url - and not parsed_args.service and not parsed_args.region): + and not parsed_args.service and not parsed_args.region + and not parsed_args.enabled and not parsed_args.disabled): sys.stdout.write("Endpoint not updated, no arguments present") return @@ -192,13 +192,19 @@ def take_action(self, parsed_args): service = common.find_service(identity_client, parsed_args.service) service_id = service.id + enabled = None + if parsed_args.enabled: + enabled = True + if parsed_args.disabled: + enabled = False + identity_client.endpoints.update( endpoint.id, service=service_id, url=parsed_args.url, interface=parsed_args.interface, region=parsed_args.region, - enabled=parsed_args.enabled + enabled=enabled ) return diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index 604171570d..fda452172d 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -74,6 +74,20 @@ 'enabled': True, } +endpoint_id = 'e-123' +endpoint_url = 'http://127.0.0.1:35357' +endpoint_region = 'RegionOne' +endpoint_interface = 'admin' + +ENDPOINT = { + 'id': endpoint_id, + 'url': endpoint_url, + 'region': endpoint_region, + 'interface': endpoint_interface, + 'service_id': service_id, + 'enabled': True, +} + user_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' user_name = 'paul' user_description = 'Sir Paul' @@ -182,6 +196,8 @@ class FakeIdentityv3Client(object): def __init__(self, **kwargs): self.domains = mock.Mock() self.domains.resource_class = fakes.FakeResource(None, {}) + self.endpoints = mock.Mock() + self.endpoints.resource_class = fakes.FakeResource(None, {}) self.groups = mock.Mock() self.groups.resource_class = fakes.FakeResource(None, {}) self.oauth1 = mock.Mock() diff --git a/openstackclient/tests/identity/v3/test_endpoint.py b/openstackclient/tests/identity/v3/test_endpoint.py new file mode 100644 index 0000000000..b90ba71904 --- /dev/null +++ b/openstackclient/tests/identity/v3/test_endpoint.py @@ -0,0 +1,575 @@ +# 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 copy + +from openstackclient.identity.v3 import endpoint +from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes + + +class TestEndpoint(identity_fakes.TestIdentityv3): + + def setUp(self): + super(TestEndpoint, self).setUp() + + # Get a shortcut to the EndpointManager Mock + self.endpoints_mock = self.app.client_manager.identity.endpoints + self.endpoints_mock.reset_mock() + + # Get a shortcut to the ServiceManager Mock + self.services_mock = self.app.client_manager.identity.services + self.services_mock.reset_mock() + + +class TestEndpointCreate(TestEndpoint): + + def setUp(self): + super(TestEndpointCreate, self).setUp() + + self.endpoints_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ENDPOINT), + loaded=True, + ) + + # This is the return value for common.find_resource(service) + self.services_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.SERVICE), + loaded=True, + ) + + # Get the command object to test + self.cmd = endpoint.CreateEndpoint(self.app, None) + + def test_endpoint_create_no_options(self): + arglist = [ + identity_fakes.service_id, + identity_fakes.endpoint_interface, + identity_fakes.endpoint_url, + ] + verifylist = [ + ('enabled', True), + ('service', identity_fakes.service_id), + ('interface', identity_fakes.endpoint_interface), + ('url', identity_fakes.endpoint_url), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'service': identity_fakes.service_id, + 'url': identity_fakes.endpoint_url, + 'interface': identity_fakes.endpoint_interface, + 'enabled': True, + 'region': None, + } + + self.endpoints_mock.create.assert_called_with( + **kwargs + ) + + collist = ('enabled', 'id', 'interface', 'region', 'service_id', + 'service_name', 'service_type', 'url') + self.assertEqual(collist, columns) + datalist = ( + True, + identity_fakes.endpoint_id, + identity_fakes.endpoint_interface, + identity_fakes.endpoint_region, + identity_fakes.service_id, + identity_fakes.service_name, + identity_fakes.service_type, + identity_fakes.endpoint_url, + ) + self.assertEqual(datalist, data) + + def test_endpoint_create_region(self): + arglist = [ + identity_fakes.service_id, + identity_fakes.endpoint_interface, + identity_fakes.endpoint_url, + '--region', identity_fakes.endpoint_region, + ] + verifylist = [ + ('enabled', True), + ('service', identity_fakes.service_id), + ('interface', identity_fakes.endpoint_interface), + ('url', identity_fakes.endpoint_url), + ('region', identity_fakes.endpoint_region), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'service': identity_fakes.service_id, + 'url': identity_fakes.endpoint_url, + 'interface': identity_fakes.endpoint_interface, + 'enabled': True, + 'region': identity_fakes.endpoint_region, + } + + self.endpoints_mock.create.assert_called_with( + **kwargs + ) + + collist = ('enabled', 'id', 'interface', 'region', 'service_id', + 'service_name', 'service_type', 'url') + self.assertEqual(collist, columns) + datalist = ( + True, + identity_fakes.endpoint_id, + identity_fakes.endpoint_interface, + identity_fakes.endpoint_region, + identity_fakes.service_id, + identity_fakes.service_name, + identity_fakes.service_type, + identity_fakes.endpoint_url, + ) + self.assertEqual(datalist, data) + + def test_endpoint_create_enable(self): + arglist = [ + identity_fakes.service_id, + identity_fakes.endpoint_interface, + identity_fakes.endpoint_url, + '--enable' + ] + verifylist = [ + ('enabled', True), + ('service', identity_fakes.service_id), + ('interface', identity_fakes.endpoint_interface), + ('url', identity_fakes.endpoint_url), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'service': identity_fakes.service_id, + 'url': identity_fakes.endpoint_url, + 'interface': identity_fakes.endpoint_interface, + 'enabled': True, + 'region': None, + } + + self.endpoints_mock.create.assert_called_with( + **kwargs + ) + + collist = ('enabled', 'id', 'interface', 'region', 'service_id', + 'service_name', 'service_type', 'url') + self.assertEqual(collist, columns) + datalist = ( + True, + identity_fakes.endpoint_id, + identity_fakes.endpoint_interface, + identity_fakes.endpoint_region, + identity_fakes.service_id, + identity_fakes.service_name, + identity_fakes.service_type, + identity_fakes.endpoint_url, + ) + self.assertEqual(datalist, data) + + def test_endpoint_create_disable(self): + arglist = [ + identity_fakes.service_id, + identity_fakes.endpoint_interface, + identity_fakes.endpoint_url, + '--disable', + ] + verifylist = [ + ('enabled', False), + ('service', identity_fakes.service_id), + ('interface', identity_fakes.endpoint_interface), + ('url', identity_fakes.endpoint_url), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'service': identity_fakes.service_id, + 'url': identity_fakes.endpoint_url, + 'interface': identity_fakes.endpoint_interface, + 'enabled': False, + 'region': None, + } + + self.endpoints_mock.create.assert_called_with( + **kwargs + ) + + collist = ('enabled', 'id', 'interface', 'region', 'service_id', + 'service_name', 'service_type', 'url') + self.assertEqual(collist, columns) + datalist = ( + True, + identity_fakes.endpoint_id, + identity_fakes.endpoint_interface, + identity_fakes.endpoint_region, + identity_fakes.service_id, + identity_fakes.service_name, + identity_fakes.service_type, + identity_fakes.endpoint_url, + ) + self.assertEqual(datalist, data) + + +class TestEndpointDelete(TestEndpoint): + + def setUp(self): + super(TestEndpointDelete, self).setUp() + + # This is the return value for utils.find_resource(endpoint) + self.endpoints_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ENDPOINT), + loaded=True, + ) + self.endpoints_mock.delete.return_value = None + + # Get the command object to test + self.cmd = endpoint.DeleteEndpoint(self.app, None) + + def test_endpoint_delete(self): + arglist = [ + identity_fakes.endpoint_id, + ] + verifylist = [ + ('endpoint', identity_fakes.endpoint_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(0, result) + + self.endpoints_mock.delete.assert_called_with( + identity_fakes.endpoint_id, + ) + + +class TestEndpointList(TestEndpoint): + + def setUp(self): + super(TestEndpointList, self).setUp() + + self.endpoints_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ENDPOINT), + loaded=True, + ), + ] + + # This is the return value for common.find_resource(service) + self.services_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.SERVICE), + loaded=True, + ) + + # Get the command object to test + self.cmd = endpoint.ListEndpoint(self.app, None) + + def test_endpoint_list_no_options(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + self.endpoints_mock.list.assert_called_with() + + collist = ('ID', 'Region', 'Service Name', 'Service Type', + 'Enabled', 'Interface', 'URL') + self.assertEqual(collist, columns) + datalist = (( + identity_fakes.endpoint_id, + identity_fakes.endpoint_region, + identity_fakes.service_name, + identity_fakes.service_type, + True, + identity_fakes.endpoint_interface, + identity_fakes.endpoint_url, + ),) + self.assertEqual(datalist, tuple(data)) + + +class TestEndpointSet(TestEndpoint): + + def setUp(self): + super(TestEndpointSet, self).setUp() + + # This is the return value for utils.find_resource(endpoint) + self.endpoints_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ENDPOINT), + loaded=True, + ) + + self.endpoints_mock.update.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ENDPOINT), + loaded=True, + ) + + # This is the return value for common.find_resource(service) + self.services_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.SERVICE), + loaded=True, + ) + + # Get the command object to test + self.cmd = endpoint.SetEndpoint(self.app, None) + + def test_endpoint_set_no_options(self): + arglist = [ + identity_fakes.endpoint_id, + ] + verifylist = [ + ('endpoint', identity_fakes.endpoint_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(0, result) + + self.assertNotCalled(self.endpoints_mock.update) + + def test_endpoint_set_interface(self): + arglist = [ + '--interface', 'public', + identity_fakes.endpoint_id + ] + verifylist = [ + ('interface', 'public'), + ('endpoint', identity_fakes.endpoint_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(0, result) + + # Set expected values + kwargs = { + 'enabled': None, + 'interface': 'public', + 'url': None, + 'region': None, + 'service': None, + } + self.endpoints_mock.update.assert_called_with( + identity_fakes.endpoint_id, + **kwargs + ) + + def test_endpoint_set_url(self): + arglist = [ + '--url', 'http://localhost:5000', + identity_fakes.endpoint_id + ] + verifylist = [ + ('url', 'http://localhost:5000'), + ('endpoint', identity_fakes.endpoint_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(0, result) + + # Set expected values + kwargs = { + 'enabled': None, + 'interface': None, + 'url': 'http://localhost:5000', + 'region': None, + 'service': None, + } + self.endpoints_mock.update.assert_called_with( + identity_fakes.endpoint_id, + **kwargs + ) + + def test_endpoint_set_service(self): + arglist = [ + '--service', identity_fakes.service_id, + identity_fakes.endpoint_id + ] + verifylist = [ + ('service', identity_fakes.service_id), + ('endpoint', identity_fakes.endpoint_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(0, result) + + # Set expected values + kwargs = { + 'enabled': None, + 'interface': None, + 'url': None, + 'region': None, + 'service': identity_fakes.service_id, + } + self.endpoints_mock.update.assert_called_with( + identity_fakes.endpoint_id, + **kwargs + ) + + def test_endpoint_set_region(self): + arglist = [ + '--region', 'e-rzzz', + identity_fakes.endpoint_id + ] + verifylist = [ + ('region', 'e-rzzz'), + ('endpoint', identity_fakes.endpoint_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(0, result) + + # Set expected values + kwargs = { + 'enabled': None, + 'interface': None, + 'url': None, + 'region': 'e-rzzz', + 'service': None, + } + self.endpoints_mock.update.assert_called_with( + identity_fakes.endpoint_id, + **kwargs + ) + + def test_endpoint_set_enable(self): + arglist = [ + '--enable', + identity_fakes.endpoint_id + ] + verifylist = [ + ('enabled', True), + ('endpoint', identity_fakes.endpoint_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(0, result) + + # Set expected values + kwargs = { + 'enabled': True, + 'interface': None, + 'url': None, + 'region': None, + 'service': None, + } + self.endpoints_mock.update.assert_called_with( + identity_fakes.endpoint_id, + **kwargs + ) + + def test_endpoint_set_disable(self): + arglist = [ + '--disable', + identity_fakes.endpoint_id + ] + verifylist = [ + ('disabled', True), + ('endpoint', identity_fakes.endpoint_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(0, result) + + # Set expected values + kwargs = { + 'enabled': False, + 'interface': None, + 'url': None, + 'region': None, + 'service': None, + } + self.endpoints_mock.update.assert_called_with( + identity_fakes.endpoint_id, + **kwargs + ) + + +class TestEndpointShow(TestEndpoint): + + def setUp(self): + super(TestEndpointShow, self).setUp() + + self.endpoints_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ENDPOINT), + loaded=True, + ) + + # This is the return value for common.find_resource(service) + self.services_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.SERVICE), + loaded=True, + ) + + # Get the command object to test + self.cmd = endpoint.ShowEndpoint(self.app, None) + + def test_endpoint_show(self): + arglist = [ + identity_fakes.endpoint_id, + ] + verifylist = [ + ('endpoint', identity_fakes.endpoint_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + self.endpoints_mock.get.assert_called_with( + identity_fakes.endpoint_id, + ) + + collist = ('enabled', 'id', 'interface', 'region', 'service_id', + 'service_name', 'service_type', 'url') + self.assertEqual(collist, columns) + datalist = ( + True, + identity_fakes.endpoint_id, + identity_fakes.endpoint_interface, + identity_fakes.endpoint_region, + identity_fakes.service_id, + identity_fakes.service_name, + identity_fakes.service_type, + identity_fakes.endpoint_url, + ) + self.assertEqual(datalist, data) From 181f16da8a2433809d319441d8261b908faf2dd9 Mon Sep 17 00:00:00 2001 From: wanghong Date: Mon, 18 Aug 2014 16:41:15 +0800 Subject: [PATCH 0165/3494] add service/interface/region filter for endpoint v3 Change-Id: I7eac5b2ff5f5a6f3f08b22dd3a48a5ae7e2c056b Closes-Bug: #1281888 --- openstackclient/identity/v3/endpoint.py | 28 +++++- .../tests/identity/v3/test_endpoint.py | 96 +++++++++++++++++++ 2 files changed, 123 insertions(+), 1 deletion(-) diff --git a/openstackclient/identity/v3/endpoint.py b/openstackclient/identity/v3/endpoint.py index 5ab5dac466..39798b2dd4 100644 --- a/openstackclient/identity/v3/endpoint.py +++ b/openstackclient/identity/v3/endpoint.py @@ -114,12 +114,38 @@ class ListEndpoint(lister.Lister): log = logging.getLogger(__name__ + '.ListEndpoint') + def get_parser(self, prog_name): + parser = super(ListEndpoint, self).get_parser(prog_name) + parser.add_argument( + '--service', + metavar='', + help='Filter by a specific service') + parser.add_argument( + '--interface', + metavar='', + choices=['admin', 'public', 'internal'], + help='Filter by a specific interface, must be admin, public or' + ' internal') + parser.add_argument( + '--region', + metavar='', + help='Filter by a specific region') + return parser + def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity columns = ('ID', 'Region', 'Service Name', 'Service Type', 'Enabled', 'Interface', 'URL') - data = identity_client.endpoints.list() + kwargs = {} + if parsed_args.service: + service = common.find_service(identity_client, parsed_args.service) + kwargs['service'] = service.id + if parsed_args.interface: + kwargs['interface'] = parsed_args.interface + if parsed_args.region: + kwargs['region'] = parsed_args.region + data = identity_client.endpoints.list(**kwargs) for ep in data: service = common.find_service(identity_client, ep.service_id) diff --git a/openstackclient/tests/identity/v3/test_endpoint.py b/openstackclient/tests/identity/v3/test_endpoint.py index b90ba71904..ea05326e20 100644 --- a/openstackclient/tests/identity/v3/test_endpoint.py +++ b/openstackclient/tests/identity/v3/test_endpoint.py @@ -317,6 +317,102 @@ def test_endpoint_list_no_options(self): ),) self.assertEqual(datalist, tuple(data)) + def test_endpoint_list_service(self): + arglist = [ + '--service', identity_fakes.service_name, + ] + verifylist = [ + ('service', identity_fakes.service_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'service': identity_fakes.service_id, + } + self.endpoints_mock.list.assert_called_with(**kwargs) + + collist = ('ID', 'Region', 'Service Name', 'Service Type', + 'Enabled', 'Interface', 'URL') + self.assertEqual(collist, columns) + datalist = (( + identity_fakes.endpoint_id, + identity_fakes.endpoint_region, + identity_fakes.service_name, + identity_fakes.service_type, + True, + identity_fakes.endpoint_interface, + identity_fakes.endpoint_url, + ),) + self.assertEqual(datalist, tuple(data)) + + def test_endpoint_list_interface(self): + arglist = [ + '--interface', identity_fakes.endpoint_interface, + ] + verifylist = [ + ('interface', identity_fakes.endpoint_interface), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'interface': identity_fakes.endpoint_interface, + } + self.endpoints_mock.list.assert_called_with(**kwargs) + + collist = ('ID', 'Region', 'Service Name', 'Service Type', + 'Enabled', 'Interface', 'URL') + self.assertEqual(collist, columns) + datalist = (( + identity_fakes.endpoint_id, + identity_fakes.endpoint_region, + identity_fakes.service_name, + identity_fakes.service_type, + True, + identity_fakes.endpoint_interface, + identity_fakes.endpoint_url, + ),) + self.assertEqual(datalist, tuple(data)) + + def test_endpoint_list_region(self): + arglist = [ + '--region', identity_fakes.endpoint_region, + ] + verifylist = [ + ('region', identity_fakes.endpoint_region), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'region': identity_fakes.endpoint_region, + } + self.endpoints_mock.list.assert_called_with(**kwargs) + + collist = ('ID', 'Region', 'Service Name', 'Service Type', + 'Enabled', 'Interface', 'URL') + self.assertEqual(collist, columns) + datalist = (( + identity_fakes.endpoint_id, + identity_fakes.endpoint_region, + identity_fakes.service_name, + identity_fakes.service_type, + True, + identity_fakes.endpoint_interface, + identity_fakes.endpoint_url, + ),) + self.assertEqual(datalist, tuple(data)) + class TestEndpointSet(TestEndpoint): From cecf1a77377b87ea6e3c36e30c9664f7b79dc309 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Mon, 18 Aug 2014 05:41:58 -0600 Subject: [PATCH 0166/3494] Network use enable/disable vs admin state up/down Use --enable and --disable vs --admin-state-up/--admin-state-down Change-Id: I90040b925cb537a8ba13d1dd609c51bb669cf149 --- openstackclient/network/v2/network.py | 29 +++++++++++-------- .../tests/network/v2/test_network.py | 8 ++--- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py index 24d7197619..63411d55d5 100644 --- a/openstackclient/network/v2/network.py +++ b/openstackclient/network/v2/network.py @@ -43,13 +43,16 @@ def get_parser(self, prog_name): help='Name of network to create') admin_group = parser.add_mutually_exclusive_group() admin_group.add_argument( - '--admin-state-up', - dest='admin_state', action='store_true', - default=True, help='Set Admin State Up') + '--enable', + dest='admin_state', + default=True, + action='store_true', + help='Set administrative state up') admin_group.add_argument( - '--admin-state-down', - dest='admin_state', action='store_false', - help='Set Admin State Down') + '--disable', + dest='admin_state', + action='store_false', + help='Set administrative state down') share_group = parser.add_mutually_exclusive_group() share_group.add_argument( '--share', @@ -171,14 +174,16 @@ def get_parser(self, prog_name): ) admin_group = parser.add_mutually_exclusive_group() admin_group.add_argument( - '--admin-state-up', - dest='admin_state', action='store_true', + '--enable', + dest='admin_state', default=None, - help='Set Admin State Up') + action='store_true', + help='Set administrative state up') admin_group.add_argument( - '--admin-state-down', - dest='admin_state', action='store_false', - help='Set Admin State Down') + '--disable', + dest='admin_state', + action='store_false', + help='Set administrative state down') parser.add_argument( '--name', metavar='', diff --git a/openstackclient/tests/network/v2/test_network.py b/openstackclient/tests/network/v2/test_network.py index 08b61a0a2c..468db5e039 100644 --- a/openstackclient/tests/network/v2/test_network.py +++ b/openstackclient/tests/network/v2/test_network.py @@ -61,7 +61,7 @@ def test_create_no_options(self): def test_create_all_options(self): arglist = [ - "--admin-state-down", + "--disable", "--share", FAKE_NAME, ] + self.given_show_options @@ -88,7 +88,7 @@ def test_create_all_options(self): def test_create_other_options(self): arglist = [ - "--admin-state-up", + "--enable", "--no-share", FAKE_NAME, ] @@ -220,7 +220,7 @@ class TestSetNetwork(common.TestNetworkBase): def test_set_this(self): arglist = [ FAKE_NAME, - '--admin-state-up', + '--enable', '--name', 'noob', '--share', ] @@ -247,7 +247,7 @@ def test_set_this(self): def test_set_that(self): arglist = [ FAKE_NAME, - '--admin-state-down', + '--disable', '--no-share', ] verifylist = [ From 7f8791ad48dddb5d8ec8f1a6059b6ee5fab182b9 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 22 Aug 2014 12:34:21 +0000 Subject: [PATCH 0167/3494] Updated from global requirements Change-Id: I067f2ff0c78547088500fa2831c1c5abb75864bc --- requirements.txt | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index b1a122c497..874bf6d466 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,5 +5,5 @@ python-keystoneclient>=0.10.0 python-novaclient>=2.17.0 python-cinderclient>=1.0.7 python-neutronclient>=2.3.6,<3 -requests>=1.1 +requests>=1.2.1 six>=1.7.0 diff --git a/test-requirements.txt b/test-requirements.txt index 0b1acd9b50..8c911b3df3 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,7 +4,7 @@ coverage>=3.6 discover fixtures>=0.3.14 mock>=1.0 -oslosphinx +oslosphinx>=2.2.0.0a2 sphinx>=1.1.2,!=1.2.0,<1.3 testrepository>=0.0.18 testtools>=0.9.34 From eb6b3027e61b9f62e161506f8a8b7d0efaad29c6 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Wed, 9 Jul 2014 11:54:39 -0400 Subject: [PATCH 0168/3494] Add oslo.i18n as a dependency Add i18n in requirements.txt implements bp add_i18n Change-Id: I84ecd16696593414739c52ee344b8a1c9868941a --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 51715d1d76..f616d6ac62 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ -pbr>=0.6,!=0.7,<1.0 cliff>=1.6.0 +oslo.i18n>=0.2.0 # Apache-2.0 +pbr>=0.6,!=0.7,<1.0 python-glanceclient>=0.13.1 python-keystoneclient>=0.9.0 python-novaclient>=2.17.0 From c2b0cec6e3cfa7b5ebd9762abca9179581d8722e Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Wed, 9 Jul 2014 13:40:12 -0400 Subject: [PATCH 0169/3494] Create message variables for exceptions Instead of inline messages, let's create variables instead, as it's easier to find strings, and mark them for translation. Change-Id: Ibbcfdbc59d12a0cb4af50f73043d3ff7f3c76f99 --- openstackclient/compute/v2/keypair.py | 5 +++-- openstackclient/compute/v2/server.py | 17 ++++++++++------- openstackclient/network/v2/network.py | 3 ++- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/openstackclient/compute/v2/keypair.py b/openstackclient/compute/v2/keypair.py index 972443a43c..6aedbfeeec 100644 --- a/openstackclient/compute/v2/keypair.py +++ b/openstackclient/compute/v2/keypair.py @@ -57,8 +57,9 @@ def take_action(self, parsed_args): with open(os.path.expanduser(parsed_args.public_key)) as p: public_key = p.read() except IOError as e: - raise exceptions.CommandError( - "Key file %s not found: %s" % (parsed_args.public_key, e)) + msg = "Key file %s not found: %s" + raise exceptions.CommandError(msg + % (parsed_args.public_key, e)) keypair = compute_client.keypairs.create( parsed_args.name, diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 2dcc7ae9a0..1f2097b979 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -300,19 +300,22 @@ def take_action(self, parsed_args): raise exceptions.CommandError("Can't open '%s': %s" % (src, e)) if parsed_args.min > parsed_args.max: - raise exceptions.CommandError("min instances should be <= " - "max instances") + msg = "min instances should be <= max instances" + raise exceptions.CommandError(msg) if parsed_args.min < 1: - raise exceptions.CommandError("min instances should be > 0") + msg = "min instances should be > 0" + raise exceptions.CommandError(msg) if parsed_args.max < 1: - raise exceptions.CommandError("max instances should be > 0") + msg = "max instances should be > 0" + raise exceptions.CommandError(msg) userdata = None if parsed_args.user_data: try: userdata = open(parsed_args.user_data) except IOError as e: - raise exceptions.CommandError("Can't open '%s': %s" % + msg = "Can't open '%s': %s" + raise exceptions.CommandError(msg % (parsed_args.user_data, e)) block_device_mapping = dict(v.split('=', 1) @@ -1077,8 +1080,8 @@ def take_action(self, parsed_args): if p1 == p2: server.change_password(p1) else: - raise exceptions.CommandError( - "Passwords do not match, password unchanged") + msg = "Passwords do not match, password unchanged" + raise exceptions.CommandError(msg) class ShowServer(show.ShowOne): diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py index c0c25e710d..f119b581b4 100644 --- a/openstackclient/network/v2/network.py +++ b/openstackclient/network/v2/network.py @@ -206,7 +206,8 @@ def take_action(self, parsed_args): if parsed_args.shared is not None: body['shared'] = parsed_args.shared if body == {}: - raise exceptions.CommandError("Nothing specified to be set") + msg = "Nothing specified to be set" + raise exceptions.CommandError(msg) update_method = getattr(client, "update_network") update_method(_id, {'network': body}) return From 258798c7a62155327901b6a3168a19da55face1d Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Wed, 9 Jul 2014 13:42:59 -0400 Subject: [PATCH 0170/3494] Add i18n module to openstackclient Based on the information available at: http://docs.openstack.org/developer/oslo.i18n/usage.html implements bp i18n Change-Id: Ie44f95dcbf192736991f88d92773f0dc2e20fa64 --- openstackclient/i18n.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 openstackclient/i18n.py diff --git a/openstackclient/i18n.py b/openstackclient/i18n.py new file mode 100644 index 0000000000..bd52d64838 --- /dev/null +++ b/openstackclient/i18n.py @@ -0,0 +1,31 @@ +# Copyright 2012-2013 OpenStack Foundation +# +# 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 oslo import i18n + +_translators = i18n.TranslatorFactory(domain='openstackclient') + +# The primary translation function using the well-known name "_" +_ = _translators.primary + +# Translators for log levels. +# +# The abbreviated names are meant to reflect the usual use of a short +# name like '_'. The "L" is for "log" and the other letter comes from +# the level. +_LI = _translators.log_info +_LW = _translators.log_warning +_LE = _translators.log_error +_LC = _translators.log_critical From 4bbd03210f82d8f9a89627e08a546ba9841ff7fb Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 8 Aug 2014 17:38:44 -0500 Subject: [PATCH 0171/3494] Change app.restapi to app.client_manager.session This is step 1 toward using Keystone client's session.Session as the primary session/requests interface in OSC. * Move the session create into ClientManager and rename 'restapi' attribute to 'session' * Set up ClientManager and session loggers * Fix container and object command references to restapi/api Change-Id: I013d81520b336c7a6422cd22c05d1d65655e64f8 --- openstackclient/common/clientmanager.py | 14 ++- openstackclient/identity/client.py | 10 ++ openstackclient/object/v1/container.py | 8 +- openstackclient/object/v1/lib/container.py | 49 +++++---- openstackclient/object/v1/lib/object.py | 46 ++++---- openstackclient/object/v1/object.py | 8 +- openstackclient/shell.py | 8 -- .../tests/common/test_clientmanager.py | 104 ++++++++++++++++-- openstackclient/tests/fakes.py | 17 ++- .../tests/object/v1/lib/test_container.py | 72 ++++++------ .../tests/object/v1/lib/test_object.py | 103 ++++++++--------- .../tests/object/v1/test_container.py | 16 +-- .../tests/object/v1/test_object.py | 18 +-- 13 files changed, 293 insertions(+), 180 deletions(-) diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index a2f85aff08..4dcec8e013 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -19,6 +19,7 @@ import pkg_resources import sys +from openstackclient.common import restapi from openstackclient.identity import client as identity_client @@ -77,7 +78,18 @@ def __init__(self, token=None, url=None, auth_url=None, self._insecure = not verify else: self._cacert = verify - self._insecure = True + self._insecure = False + + self.session = restapi.RESTApi( + verify=verify, + debug=True, + ) + + # Get logging from root logger + root_logger = logging.getLogger('') + LOG.setLevel(root_logger.getEffectiveLevel()) + restapi_logger = logging.getLogger('restapi') + restapi_logger.setLevel(root_logger.getEffectiveLevel()) self.auth_ref = None diff --git a/openstackclient/identity/client.py b/openstackclient/identity/client.py index 7f5390c86b..172910f592 100644 --- a/openstackclient/identity/client.py +++ b/openstackclient/identity/client.py @@ -66,7 +66,17 @@ def make_client(instance): insecure=instance._insecure, trust_id=instance._trust_id, ) + + # TODO(dtroyer): the identity v2 role commands use this yet, fix that + # so we can remove it instance.auth_ref = client.auth_ref + + # NOTE(dtroyer): this is hanging around until restapi is replace by + # ksc session + instance.session.set_auth( + client.auth_ref.auth_token, + ) + return client diff --git a/openstackclient/object/v1/container.py b/openstackclient/object/v1/container.py index ae4013fc0c..1ca07f3aa4 100644 --- a/openstackclient/object/v1/container.py +++ b/openstackclient/object/v1/container.py @@ -45,7 +45,7 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) data = lib_container.create_container( - self.app.restapi, + self.app.client_manager.session, self.app.client_manager.object_store.endpoint, parsed_args.container, ) @@ -71,7 +71,7 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) lib_container.delete_container( - self.app.restapi, + self.app.client_manager.session, self.app.client_manager.object_store.endpoint, parsed_args.container, ) @@ -140,7 +140,7 @@ def take_action(self, parsed_args): kwargs['full_listing'] = True data = lib_container.list_containers( - self.app.restapi, + self.app.client_manager.session, self.app.client_manager.object_store.endpoint, **kwargs ) @@ -170,7 +170,7 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) data = lib_container.show_container( - self.app.restapi, + self.app.client_manager.session, self.app.client_manager.object_store.endpoint, parsed_args.container, ) diff --git a/openstackclient/object/v1/lib/container.py b/openstackclient/object/v1/lib/container.py index bd50955514..65a9fe4d02 100644 --- a/openstackclient/object/v1/lib/container.py +++ b/openstackclient/object/v1/lib/container.py @@ -23,46 +23,46 @@ def create_container( - api, + session, url, container, ): """Create a container - :param api: a restapi object + :param session: a restapi object :param url: endpoint :param container: name of container to create :returns: dict of returned headers """ - response = api.put("%s/%s" % (url, container)) + response = session.put("%s/%s" % (url, container)) url_parts = urlparse(url) data = { 'account': url_parts.path.split('/')[-1], 'container': container, + 'x-trans-id': response.headers.get('x-trans-id', None), } - data['x-trans-id'] = response.headers.get('x-trans-id', None) return data def delete_container( - api, + session, url, container, ): """Delete a container - :param api: a restapi object + :param session: a restapi object :param url: endpoint :param container: name of container to delete """ - api.delete("%s/%s" % (url, container)) + session.delete("%s/%s" % (url, container)) def list_containers( - api, + session, url, marker=None, limit=None, @@ -72,7 +72,7 @@ def list_containers( ): """Get containers in an account - :param api: a restapi object + :param session: a restapi object :param url: endpoint :param marker: marker query :param limit: limit query @@ -85,7 +85,7 @@ def list_containers( if full_listing: data = listing = list_containers( - api, + session, url, marker, limit, @@ -95,7 +95,7 @@ def list_containers( while listing: marker = listing[-1]['name'] listing = list_containers( - api, + session, url, marker, limit, @@ -117,34 +117,35 @@ def list_containers( params['end_marker'] = end_marker if prefix: params['prefix'] = prefix - return api.list(url, params=params) + return session.get(url, params=params).json() def show_container( - api, + session, url, container, ): """Get container details - :param api: a restapi object + :param session: a restapi object :param url: endpoint :param container: name of container to show :returns: dict of returned headers """ - response = api.head("%s/%s" % (url, container)) - url_parts = urlparse(url) + response = session.head("%s/%s" % (url, container)) data = { - 'account': url_parts.path.split('/')[-1], + 'account': response.headers.get('x-container-meta-owner', None), 'container': container, + 'object_count': response.headers.get( + 'x-container-object-count', + None, + ), + 'bytes_used': response.headers.get('x-container-bytes-used', None), + 'read_acl': response.headers.get('x-container-read', None), + 'write_acl': response.headers.get('x-container-write', None), + 'sync_to': response.headers.get('x-container-sync-to', None), + 'sync_key': response.headers.get('x-container-sync-key', None), } - data['object_count'] = response.headers.get( - 'x-container-object-count', None) - data['bytes_used'] = response.headers.get('x-container-bytes-used', None) - data['read_acl'] = response.headers.get('x-container-read', None) - data['write_acl'] = response.headers.get('x-container-write', None) - data['sync_to'] = response.headers.get('x-container-sync-to', None) - data['sync_key'] = response.headers.get('x-container-sync-key', None) return data diff --git a/openstackclient/object/v1/lib/object.py b/openstackclient/object/v1/lib/object.py index 2473caa39a..0ded0dadbe 100644 --- a/openstackclient/object/v1/lib/object.py +++ b/openstackclient/object/v1/lib/object.py @@ -25,14 +25,14 @@ def create_object( - api, + session, url, container, object, ): """Create an object, upload it to a container - :param api: a restapi object + :param session: a restapi object :param url: endpoint :param container: name of container to store object :param object: local path to object @@ -40,38 +40,38 @@ def create_object( """ full_url = "%s/%s/%s" % (url, container, object) - response = api.put(full_url, data=open(object)) + response = session.put(full_url, data=open(object)) url_parts = urlparse(url) data = { 'account': url_parts.path.split('/')[-1], 'container': container, 'object': object, + 'x-trans-id': response.headers.get('X-Trans-Id', None), + 'etag': response.headers.get('Etag', None), } - data['x-trans-id'] = response.headers.get('X-Trans-Id', None) - data['etag'] = response.headers.get('Etag', None) return data def delete_object( - api, + session, url, container, object, ): """Delete an object stored in a container - :param api: a restapi object + :param session: a restapi object :param url: endpoint :param container: name of container that stores object :param container: name of object to delete """ - api.delete("%s/%s/%s" % (url, container, object)) + session.delete("%s/%s/%s" % (url, container, object)) def list_objects( - api, + session, url, container, marker=None, @@ -84,7 +84,7 @@ def list_objects( ): """Get objects in a container - :param api: a restapi object + :param session: a restapi object :param url: endpoint :param container: container name to get a listing for :param marker: marker query @@ -101,7 +101,7 @@ def list_objects( if full_listing: data = listing = list_objects( - api, + session, url, container, marker, @@ -117,7 +117,7 @@ def list_objects( else: marker = listing[-1]['name'] listing = list_objects( - api, + session, url, container, marker, @@ -131,7 +131,6 @@ def list_objects( data.extend(listing) return data - object_url = url params = { 'format': 'json', } @@ -147,32 +146,31 @@ def list_objects( params['prefix'] = prefix if path: params['path'] = path - url = "%s/%s" % (object_url, container) - return api.list(url, params=params) + requrl = "%s/%s" % (url, container) + return session.get(requrl, params=params).json() def show_object( - api, + session, url, container, obj, ): """Get object details - :param api: a restapi object + :param session: a restapi object :param url: endpoint :param container: container name to get a listing for :returns: dict of object properties """ - response = api.head("%s/%s/%s" % (url, container, obj)) - url_parts = urlparse(url) + response = session.head("%s/%s/%s" % (url, container, obj)) data = { - 'account': url_parts.path.split('/')[-1], + 'account': response.headers.get('x-container-meta-owner', None), 'container': container, 'object': obj, + 'content-type': response.headers.get('content-type', None), } - data['content-type'] = response.headers.get('content-type', None) if 'content-length' in response.headers: data['content-length'] = response.headers.get('content-length', None) if 'last-modified' in response.headers: @@ -184,10 +182,10 @@ def show_object( 'x-object-manifest', None) for key, value in six.iteritems(response.headers): if key.startswith('x-object-meta-'): - data[key[len('x-object-meta-'):].title()] = value + data[key[len('x-object-meta-'):].lower()] = value elif key not in ( 'content-type', 'content-length', 'last-modified', - 'etag', 'date', 'x-object-manifest'): - data[key.title()] = value + 'etag', 'date', 'x-object-manifest', 'x-container-meta-owner'): + data[key.lower()] = value return data diff --git a/openstackclient/object/v1/object.py b/openstackclient/object/v1/object.py index 4a99a8f191..812ad6e11f 100644 --- a/openstackclient/object/v1/object.py +++ b/openstackclient/object/v1/object.py @@ -50,7 +50,7 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) data = lib_object.create_object( - self.app.restapi, + self.app.client_manager.session, self.app.client_manager.object_store.endpoint, parsed_args.container, parsed_args.object, @@ -82,7 +82,7 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) lib_object.delete_object( - self.app.restapi, + self.app.client_manager.session, self.app.client_manager.object_store.endpoint, parsed_args.container, parsed_args.object, @@ -170,7 +170,7 @@ def take_action(self, parsed_args): kwargs['full_listing'] = True data = lib_object.list_objects( - self.app.restapi, + self.app.client_manager.session, self.app.client_manager.object_store.endpoint, parsed_args.container, **kwargs @@ -206,7 +206,7 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) data = lib_object.show_object( - self.app.restapi, + self.app.client_manager.session, self.app.client_manager.object_store.endpoint, parsed_args.container, parsed_args.object, diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 287243432f..a6f508e41c 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -31,7 +31,6 @@ from openstackclient.common import clientmanager from openstackclient.common import commandmanager from openstackclient.common import exceptions as exc -from openstackclient.common import restapi from openstackclient.common import timing from openstackclient.common import utils from openstackclient.identity import client as identity_client @@ -484,10 +483,6 @@ def initialize_app(self, argv): self.verify = self.options.os_cacert else: self.verify = not self.options.insecure - self.restapi = restapi.RESTApi( - verify=self.verify, - debug=self.options.debug, - ) def prepare_to_run_command(self, cmd): """Set up auth and API versions""" @@ -498,12 +493,10 @@ def prepare_to_run_command(self, cmd): if cmd.best_effort: try: self.authenticate_user() - self.restapi.set_auth(self.client_manager.identity.auth_token) except Exception: pass else: self.authenticate_user() - self.restapi.set_auth(self.client_manager.identity.auth_token) return def clean_up(self, cmd, result, err): @@ -539,7 +532,6 @@ def interact(self): # NOTE(dtroyer): Maintain the old behaviour for interactive use as # this path does not call prepare_to_run_command() self.authenticate_user() - self.restapi.set_auth(self.client_manager.identity.auth_token) super(OpenStackShell, self).interact() diff --git a/openstackclient/tests/common/test_clientmanager.py b/openstackclient/tests/common/test_clientmanager.py index 6aee711d3e..5a25fa2c9b 100644 --- a/openstackclient/tests/common/test_clientmanager.py +++ b/openstackclient/tests/common/test_clientmanager.py @@ -14,11 +14,26 @@ # from openstackclient.common import clientmanager +from openstackclient.common import restapi from openstackclient.tests import utils +AUTH_REF = {'a': 1} AUTH_TOKEN = "foobar" AUTH_URL = "http://0.0.0.0" +USERNAME = "itchy" +PASSWORD = "scratchy" +SERVICE_CATALOG = {'sc': '123'} + + +def FakeMakeClient(instance): + return FakeClient() + + +class FakeClient(object): + auth_ref = AUTH_REF + auth_token = AUTH_TOKEN + service_catalog = SERVICE_CATALOG class Container(object): @@ -28,31 +43,96 @@ def __init__(self): pass +class TestClientCache(utils.TestCase): + + def test_singleton(self): + # NOTE(dtroyer): Verify that the ClientCache descriptor only invokes + # the factory one time and always returns the same value after that. + c = Container() + self.assertEqual(c.attr, c.attr) + + class TestClientManager(utils.TestCase): def setUp(self): super(TestClientManager, self).setUp() - api_version = {"identity": "2.0"} + clientmanager.ClientManager.identity = \ + clientmanager.ClientCache(FakeMakeClient) - self.client_manager = clientmanager.ClientManager( + def test_client_manager_token(self): + + client_manager = clientmanager.ClientManager( token=AUTH_TOKEN, url=AUTH_URL, + verify=True, + ) + + self.assertEqual( + AUTH_TOKEN, + client_manager._token, + ) + self.assertEqual( + AUTH_URL, + client_manager._url, + ) + self.assertIsInstance( + client_manager.session, + restapi.RESTApi, + ) + self.assertFalse(client_manager._insecure) + self.assertTrue(client_manager._verify) + + def test_client_manager_password(self): + + client_manager = clientmanager.ClientManager( auth_url=AUTH_URL, - api_version=api_version, + username=USERNAME, + password=PASSWORD, + verify=False, ) - def test_singleton(self): - # NOTE(dtroyer): Verify that the ClientCache descriptor only invokes - # the factory one time and always returns the same value after that. - c = Container() - self.assertEqual(c.attr, c.attr) + self.assertEqual( + AUTH_URL, + client_manager._auth_url, + ) + self.assertEqual( + USERNAME, + client_manager._username, + ) + self.assertEqual( + PASSWORD, + client_manager._password, + ) + self.assertIsInstance( + client_manager.session, + restapi.RESTApi, + ) + self.assertTrue(client_manager._insecure) + self.assertFalse(client_manager._verify) - def test_make_client_identity_default(self): + # These need to stick around until the old-style clients are gone + self.assertEqual( + AUTH_REF, + client_manager.auth_ref, + ) self.assertEqual( - self.client_manager.identity.auth_token, AUTH_TOKEN, + client_manager._token, ) self.assertEqual( - self.client_manager.identity.management_url, - AUTH_URL, + SERVICE_CATALOG, + client_manager._service_catalog, ) + + def test_client_manager_password_verify_ca(self): + + client_manager = clientmanager.ClientManager( + auth_url=AUTH_URL, + username=USERNAME, + password=PASSWORD, + verify='cafile', + ) + + self.assertFalse(client_manager._insecure) + self.assertTrue(client_manager._verify) + self.assertEqual('cafile', client_manager._cacert) diff --git a/openstackclient/tests/fakes.py b/openstackclient/tests/fakes.py index fb27ef94ac..263640ee21 100644 --- a/openstackclient/tests/fakes.py +++ b/openstackclient/tests/fakes.py @@ -13,9 +13,12 @@ # under the License. # +import json import six import sys +import requests + AUTH_TOKEN = "foobar" AUTH_URL = "http://0.0.0.0" @@ -42,7 +45,6 @@ def __init__(self, _stdout): self.stdin = sys.stdin self.stdout = _stdout or sys.stdout self.stderr = sys.stderr - self.restapi = None class FakeClientManager(object): @@ -53,6 +55,7 @@ def __init__(self): self.object = None self.volume = None self.network = None + self.session = None self.auth_ref = None @@ -78,3 +81,15 @@ def __repr__(self): k != 'manager') info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) return "<%s %s>" % (self.__class__.__name__, info) + + +class FakeResponse(requests.Response): + def __init__(self, headers={}, status_code=200, data=None, encoding=None): + super(FakeResponse, self).__init__() + + self.status_code = status_code + + self.headers.update(headers) + self._content = json.dumps(data) + if not isinstance(self._content, six.binary_type): + self._content = self._content.encode() diff --git a/openstackclient/tests/object/v1/lib/test_container.py b/openstackclient/tests/object/v1/lib/test_container.py index f7355592dd..ce70b8356b 100644 --- a/openstackclient/tests/object/v1/lib/test_container.py +++ b/openstackclient/tests/object/v1/lib/test_container.py @@ -18,7 +18,7 @@ import mock from openstackclient.object.v1.lib import container as lib_container -from openstackclient.tests.common import test_restapi as restapi +from openstackclient.tests import fakes from openstackclient.tests.object.v1 import fakes as object_fakes @@ -39,156 +39,158 @@ class TestContainer(object_fakes.TestObjectv1): def setUp(self): super(TestContainer, self).setUp() - self.app.restapi = mock.MagicMock() + self.app.client_manager.session = mock.MagicMock() class TestContainerList(TestContainer): def test_container_list_no_options(self): resp = [{'name': 'is-name'}] - self.app.restapi.list.return_value = resp + self.app.client_manager.session.get().json.return_value = resp data = lib_container.list_containers( - self.app.restapi, + self.app.client_manager.session, fake_url, ) # Check expected values - self.app.restapi.list.assert_called_with( + self.app.client_manager.session.get.assert_called_with( fake_url, params={ 'format': 'json', } ) - self.assertEqual(data, resp) + self.assertEqual(resp, data) def test_container_list_marker(self): resp = [{'name': 'is-name'}] - self.app.restapi.list.return_value = resp + self.app.client_manager.session.get().json.return_value = resp data = lib_container.list_containers( - self.app.restapi, + self.app.client_manager.session, fake_url, marker='next', ) # Check expected values - self.app.restapi.list.assert_called_with( + self.app.client_manager.session.get.assert_called_with( fake_url, params={ 'format': 'json', 'marker': 'next', } ) - self.assertEqual(data, resp) + self.assertEqual(resp, data) def test_container_list_limit(self): resp = [{'name': 'is-name'}] - self.app.restapi.list.return_value = resp + self.app.client_manager.session.get().json.return_value = resp data = lib_container.list_containers( - self.app.restapi, + self.app.client_manager.session, fake_url, limit=5, ) # Check expected values - self.app.restapi.list.assert_called_with( + self.app.client_manager.session.get.assert_called_with( fake_url, params={ 'format': 'json', 'limit': 5, } ) - self.assertEqual(data, resp) + self.assertEqual(resp, data) def test_container_list_end_marker(self): resp = [{'name': 'is-name'}] - self.app.restapi.list.return_value = resp + self.app.client_manager.session.get().json.return_value = resp data = lib_container.list_containers( - self.app.restapi, + self.app.client_manager.session, fake_url, end_marker='last', ) # Check expected values - self.app.restapi.list.assert_called_with( + self.app.client_manager.session.get.assert_called_with( fake_url, params={ 'format': 'json', 'end_marker': 'last', } ) - self.assertEqual(data, resp) + self.assertEqual(resp, data) def test_container_list_prefix(self): resp = [{'name': 'is-name'}] - self.app.restapi.list.return_value = resp + self.app.client_manager.session.get().json.return_value = resp data = lib_container.list_containers( - self.app.restapi, + self.app.client_manager.session, fake_url, prefix='foo/', ) # Check expected values - self.app.restapi.list.assert_called_with( + self.app.client_manager.session.get.assert_called_with( fake_url, params={ 'format': 'json', 'prefix': 'foo/', } ) - self.assertEqual(data, resp) + self.assertEqual(resp, data) def test_container_list_full_listing(self): + sess = self.app.client_manager.session def side_effect(*args, **kwargs): - rv = self.app.restapi.list.return_value - self.app.restapi.list.return_value = [] - self.app.restapi.list.side_effect = None + rv = sess.get().json.return_value + sess.get().json.return_value = [] + sess.get().json.side_effect = None return rv resp = [{'name': 'is-name'}] - self.app.restapi.list.return_value = resp - self.app.restapi.list.side_effect = side_effect + sess.get().json.return_value = resp + sess.get().json.side_effect = side_effect data = lib_container.list_containers( - self.app.restapi, + self.app.client_manager.session, fake_url, full_listing=True, ) # Check expected values - self.app.restapi.list.assert_called_with( + sess.get.assert_called_with( fake_url, params={ 'format': 'json', 'marker': 'is-name', } ) - self.assertEqual(data, resp) + self.assertEqual(resp, data) class TestContainerShow(TestContainer): def test_container_show_no_options(self): resp = { + 'X-Container-Meta-Owner': fake_account, 'x-container-object-count': 1, 'x-container-bytes-used': 577, } - self.app.restapi.head.return_value = \ - restapi.FakeResponse(headers=resp) + self.app.client_manager.session.head.return_value = \ + fakes.FakeResponse(headers=resp) data = lib_container.show_container( - self.app.restapi, + self.app.client_manager.session, fake_url, 'is-name', ) # Check expected values - self.app.restapi.head.assert_called_with( + self.app.client_manager.session.head.assert_called_with( fake_url + '/is-name', ) @@ -202,4 +204,4 @@ def test_container_show_no_options(self): 'sync_to': None, 'sync_key': None, } - self.assertEqual(data, data_expected) + self.assertEqual(data_expected, data) diff --git a/openstackclient/tests/object/v1/lib/test_object.py b/openstackclient/tests/object/v1/lib/test_object.py index 064efb53ed..f96732b468 100644 --- a/openstackclient/tests/object/v1/lib/test_object.py +++ b/openstackclient/tests/object/v1/lib/test_object.py @@ -18,7 +18,7 @@ import mock from openstackclient.object.v1.lib import object as lib_object -from openstackclient.tests.common import test_restapi as restapi +from openstackclient.tests import fakes from openstackclient.tests.object.v1 import fakes as object_fakes @@ -40,99 +40,99 @@ class TestObject(object_fakes.TestObjectv1): def setUp(self): super(TestObject, self).setUp() - self.app.restapi = mock.MagicMock() + self.app.client_manager.session = mock.MagicMock() class TestObjectListObjects(TestObject): def test_list_objects_no_options(self): resp = [{'name': 'is-name'}] - self.app.restapi.list.return_value = resp + self.app.client_manager.session.get().json.return_value = resp data = lib_object.list_objects( - self.app.restapi, + self.app.client_manager.session, fake_url, fake_container, ) # Check expected values - self.app.restapi.list.assert_called_with( + self.app.client_manager.session.get.assert_called_with( fake_url + '/' + fake_container, params={ 'format': 'json', } ) - self.assertEqual(data, resp) + self.assertEqual(resp, data) def test_list_objects_marker(self): resp = [{'name': 'is-name'}] - self.app.restapi.list.return_value = resp + self.app.client_manager.session.get().json.return_value = resp data = lib_object.list_objects( - self.app.restapi, + self.app.client_manager.session, fake_url, fake_container, marker='next', ) # Check expected values - self.app.restapi.list.assert_called_with( + self.app.client_manager.session.get.assert_called_with( fake_url + '/' + fake_container, params={ 'format': 'json', 'marker': 'next', } ) - self.assertEqual(data, resp) + self.assertEqual(resp, data) def test_list_objects_limit(self): resp = [{'name': 'is-name'}] - self.app.restapi.list.return_value = resp + self.app.client_manager.session.get().json.return_value = resp data = lib_object.list_objects( - self.app.restapi, + self.app.client_manager.session, fake_url, fake_container, limit=5, ) # Check expected values - self.app.restapi.list.assert_called_with( + self.app.client_manager.session.get.assert_called_with( fake_url + '/' + fake_container, params={ 'format': 'json', 'limit': 5, } ) - self.assertEqual(data, resp) + self.assertEqual(resp, data) def test_list_objects_end_marker(self): resp = [{'name': 'is-name'}] - self.app.restapi.list.return_value = resp + self.app.client_manager.session.get().json.return_value = resp data = lib_object.list_objects( - self.app.restapi, + self.app.client_manager.session, fake_url, fake_container, end_marker='last', ) # Check expected values - self.app.restapi.list.assert_called_with( + self.app.client_manager.session.get.assert_called_with( fake_url + '/' + fake_container, params={ 'format': 'json', 'end_marker': 'last', } ) - self.assertEqual(data, resp) + self.assertEqual(resp, data) def test_list_objects_delimiter(self): resp = [{'name': 'is-name'}] - self.app.restapi.list.return_value = resp + self.app.client_manager.session.get().json.return_value = resp data = lib_object.list_objects( - self.app.restapi, + self.app.client_manager.session, fake_url, fake_container, delimiter='|', @@ -142,85 +142,86 @@ def test_list_objects_delimiter(self): # NOTE(dtroyer): requests handles the URL encoding and we're # mocking that so use the otherwise-not-legal # pipe '|' char in the response. - self.app.restapi.list.assert_called_with( + self.app.client_manager.session.get.assert_called_with( fake_url + '/' + fake_container, params={ 'format': 'json', 'delimiter': '|', } ) - self.assertEqual(data, resp) + self.assertEqual(resp, data) def test_list_objects_prefix(self): resp = [{'name': 'is-name'}] - self.app.restapi.list.return_value = resp + self.app.client_manager.session.get().json.return_value = resp data = lib_object.list_objects( - self.app.restapi, + self.app.client_manager.session, fake_url, fake_container, prefix='foo/', ) # Check expected values - self.app.restapi.list.assert_called_with( + self.app.client_manager.session.get.assert_called_with( fake_url + '/' + fake_container, params={ 'format': 'json', 'prefix': 'foo/', } ) - self.assertEqual(data, resp) + self.assertEqual(resp, data) def test_list_objects_path(self): resp = [{'name': 'is-name'}] - self.app.restapi.list.return_value = resp + self.app.client_manager.session.get().json.return_value = resp data = lib_object.list_objects( - self.app.restapi, + self.app.client_manager.session, fake_url, fake_container, path='next', ) # Check expected values - self.app.restapi.list.assert_called_with( + self.app.client_manager.session.get.assert_called_with( fake_url + '/' + fake_container, params={ 'format': 'json', 'path': 'next', } ) - self.assertEqual(data, resp) + self.assertEqual(resp, data) def test_list_objects_full_listing(self): + sess = self.app.client_manager.session def side_effect(*args, **kwargs): - rv = self.app.restapi.list.return_value - self.app.restapi.list.return_value = [] - self.app.restapi.list.side_effect = None + rv = sess.get().json.return_value + sess.get().json.return_value = [] + sess.get().json.side_effect = None return rv resp = [{'name': 'is-name'}] - self.app.restapi.list.return_value = resp - self.app.restapi.list.side_effect = side_effect + sess.get().json.return_value = resp + sess.get().json.side_effect = side_effect data = lib_object.list_objects( - self.app.restapi, + sess, fake_url, fake_container, full_listing=True, ) # Check expected values - self.app.restapi.list.assert_called_with( + sess.get.assert_called_with( fake_url + '/' + fake_container, params={ 'format': 'json', 'marker': 'is-name', } ) - self.assertEqual(data, resp) + self.assertEqual(resp, data) class TestObjectShowObjects(TestObject): @@ -228,19 +229,20 @@ class TestObjectShowObjects(TestObject): def test_object_show_no_options(self): resp = { 'content-type': 'text/alpha', + 'x-container-meta-owner': fake_account, } - self.app.restapi.head.return_value = \ - restapi.FakeResponse(headers=resp) + self.app.client_manager.session.head.return_value = \ + fakes.FakeResponse(headers=resp) data = lib_object.show_object( - self.app.restapi, + self.app.client_manager.session, fake_url, fake_container, fake_object, ) # Check expected values - self.app.restapi.head.assert_called_with( + self.app.client_manager.session.head.assert_called_with( fake_url + '/%s/%s' % (fake_container, fake_object), ) @@ -250,7 +252,7 @@ def test_object_show_no_options(self): 'object': fake_object, 'content-type': 'text/alpha', } - self.assertEqual(data, data_expected) + self.assertEqual(data_expected, data) def test_object_show_all_options(self): resp = { @@ -258,22 +260,23 @@ def test_object_show_all_options(self): 'content-length': 577, 'last-modified': '20130101', 'etag': 'qaz', + 'x-container-meta-owner': fake_account, 'x-object-manifest': None, 'x-object-meta-wife': 'Wilma', 'x-tra-header': 'yabba-dabba-do', } - self.app.restapi.head.return_value = \ - restapi.FakeResponse(headers=resp) + self.app.client_manager.session.head.return_value = \ + fakes.FakeResponse(headers=resp) data = lib_object.show_object( - self.app.restapi, + self.app.client_manager.session, fake_url, fake_container, fake_object, ) # Check expected values - self.app.restapi.head.assert_called_with( + self.app.client_manager.session.head.assert_called_with( fake_url + '/%s/%s' % (fake_container, fake_object), ) @@ -286,7 +289,7 @@ def test_object_show_all_options(self): 'last-modified': '20130101', 'etag': 'qaz', 'x-object-manifest': None, - 'Wife': 'Wilma', - 'X-Tra-Header': 'yabba-dabba-do', + 'wife': 'Wilma', + 'x-tra-header': 'yabba-dabba-do', } - self.assertEqual(data, data_expected) + self.assertEqual(data_expected, data) diff --git a/openstackclient/tests/object/v1/test_container.py b/openstackclient/tests/object/v1/test_container.py index 4afb10063f..b72c79d653 100644 --- a/openstackclient/tests/object/v1/test_container.py +++ b/openstackclient/tests/object/v1/test_container.py @@ -77,7 +77,7 @@ def test_object_list_containers_no_options(self, c_mock): kwargs = { } c_mock.assert_called_with( - self.app.restapi, + self.app.client_manager.session, AUTH_URL, **kwargs ) @@ -113,7 +113,7 @@ def test_object_list_containers_prefix(self, c_mock): 'prefix': 'bit', } c_mock.assert_called_with( - self.app.restapi, + self.app.client_manager.session, AUTH_URL, **kwargs ) @@ -148,7 +148,7 @@ def test_object_list_containers_marker(self, c_mock): 'marker': object_fakes.container_name, } c_mock.assert_called_with( - self.app.restapi, + self.app.client_manager.session, AUTH_URL, **kwargs ) @@ -183,7 +183,7 @@ def test_object_list_containers_end_marker(self, c_mock): 'end_marker': object_fakes.container_name_3, } c_mock.assert_called_with( - self.app.restapi, + self.app.client_manager.session, AUTH_URL, **kwargs ) @@ -218,7 +218,7 @@ def test_object_list_containers_limit(self, c_mock): 'limit': 2, } c_mock.assert_called_with( - self.app.restapi, + self.app.client_manager.session, AUTH_URL, **kwargs ) @@ -252,7 +252,7 @@ def test_object_list_containers_long(self, c_mock): kwargs = { } c_mock.assert_called_with( - self.app.restapi, + self.app.client_manager.session, AUTH_URL, **kwargs ) @@ -296,7 +296,7 @@ def test_object_list_containers_all(self, c_mock): 'full_listing': True, } c_mock.assert_called_with( - self.app.restapi, + self.app.client_manager.session, AUTH_URL, **kwargs ) @@ -341,7 +341,7 @@ def test_container_show(self, c_mock): } # lib.container.show_container(api, url, container) c_mock.assert_called_with( - self.app.restapi, + self.app.client_manager.session, AUTH_URL, object_fakes.container_name, **kwargs diff --git a/openstackclient/tests/object/v1/test_object.py b/openstackclient/tests/object/v1/test_object.py index bea0d2708d..26d07b2ca7 100644 --- a/openstackclient/tests/object/v1/test_object.py +++ b/openstackclient/tests/object/v1/test_object.py @@ -71,7 +71,7 @@ def test_object_list_objects_no_options(self, o_mock): columns, data = self.cmd.take_action(parsed_args) o_mock.assert_called_with( - self.app.restapi, + self.app.client_manager.session, AUTH_URL, object_fakes.container_name, ) @@ -107,7 +107,7 @@ def test_object_list_objects_prefix(self, o_mock): 'prefix': 'floppy', } o_mock.assert_called_with( - self.app.restapi, + self.app.client_manager.session, AUTH_URL, object_fakes.container_name_2, **kwargs @@ -143,7 +143,7 @@ def test_object_list_objects_delimiter(self, o_mock): 'delimiter': '=', } o_mock.assert_called_with( - self.app.restapi, + self.app.client_manager.session, AUTH_URL, object_fakes.container_name_2, **kwargs @@ -179,7 +179,7 @@ def test_object_list_objects_marker(self, o_mock): 'marker': object_fakes.object_name_2, } o_mock.assert_called_with( - self.app.restapi, + self.app.client_manager.session, AUTH_URL, object_fakes.container_name_2, **kwargs @@ -215,7 +215,7 @@ def test_object_list_objects_end_marker(self, o_mock): 'end_marker': object_fakes.object_name_2, } o_mock.assert_called_with( - self.app.restapi, + self.app.client_manager.session, AUTH_URL, object_fakes.container_name_2, **kwargs @@ -251,7 +251,7 @@ def test_object_list_objects_limit(self, o_mock): 'limit': 2, } o_mock.assert_called_with( - self.app.restapi, + self.app.client_manager.session, AUTH_URL, object_fakes.container_name_2, **kwargs @@ -287,7 +287,7 @@ def test_object_list_objects_long(self, o_mock): kwargs = { } o_mock.assert_called_with( - self.app.restapi, + self.app.client_manager.session, AUTH_URL, object_fakes.container_name, **kwargs @@ -337,7 +337,7 @@ def test_object_list_objects_all(self, o_mock): 'full_listing': True, } o_mock.assert_called_with( - self.app.restapi, + self.app.client_manager.session, AUTH_URL, object_fakes.container_name, **kwargs @@ -384,7 +384,7 @@ def test_object_show(self, c_mock): } # lib.container.show_container(api, url, container) c_mock.assert_called_with( - self.app.restapi, + self.app.client_manager.session, AUTH_URL, object_fakes.container_name, object_fakes.object_name_1, From e19216e2824c04870f9fe44ab5b864814538694b Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 27 Aug 2014 17:19:49 -0500 Subject: [PATCH 0172/3494] Fix security group list for non-admin Non-admin users couldn't list security groups due to the project lookup failure. That shouldn't stop the listing. Change-Id: I27f6ff4975b35d1de1c852c8d4e830b83c7dec75 --- openstackclient/compute/v2/security_group.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/openstackclient/compute/v2/security_group.py b/openstackclient/compute/v2/security_group.py index 0ba55c9834..cd33085707 100644 --- a/openstackclient/compute/v2/security_group.py +++ b/openstackclient/compute/v2/security_group.py @@ -23,6 +23,7 @@ from cliff import lister from cliff import show +from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc from novaclient.v1_1 import security_group_rules from openstackclient.common import parseractions from openstackclient.common import utils @@ -150,10 +151,15 @@ def _get_project(project_id): search = {'all_tenants': parsed_args.all_projects} data = compute_client.security_groups.list(search_opts=search) - projects = self.app.client_manager.identity.projects.list() project_hash = {} - for project in projects: - project_hash[project.id] = project + try: + projects = self.app.client_manager.identity.projects.list() + except ksc_exc.Forbidden: + # This fails when the user is not an admin, just move along + pass + else: + for project in projects: + project_hash[project.id] = project return (column_headers, (utils.get_item_properties( From 22c544a8228fd41a29de2fd38d4c894818c32b78 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 27 Aug 2014 17:35:27 -0500 Subject: [PATCH 0173/3494] Fix server add security group The group resource was being passed when only the name is needed. Change-Id: Ia303804be4e336f9880205d931467cb831e812de --- openstackclient/compute/v2/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index a4ed6fa47a..3e4ffbb1a2 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -175,7 +175,7 @@ def take_action(self, parsed_args): parsed_args.group, ) - server.add_security_group(security_group) + server.add_security_group(security_group.name) return From 1ab38679b61290bcb204508dc34bed564e31cbcf Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 27 Aug 2014 23:25:44 -0500 Subject: [PATCH 0174/3494] Make Identity client load like the others This does a couple of things: * Loads the Identity client module in the same manner as the other 'base' clients (where 'base' == 'included in the OSC repo') * Changes the entry point group name for the base clients to 'openstack.cli.base'. The extension group name remains the same. * Loads the base modules first followed by the extension modules. This load order ensures that the extension module commands are all loaded _after_ the base commands, allowing extensions to now override the base commands. Change-Id: I4b9ca7f1df6eb8bbe8e3f663f3065c2ed80ce20b --- openstackclient/identity/client.py | 21 ++++++++++++ openstackclient/shell.py | 55 +++++++++++------------------- setup.cfg | 3 +- 3 files changed, 42 insertions(+), 37 deletions(-) diff --git a/openstackclient/identity/client.py b/openstackclient/identity/client.py index 7f5390c86b..a2bbb61dba 100644 --- a/openstackclient/identity/client.py +++ b/openstackclient/identity/client.py @@ -70,6 +70,27 @@ def make_client(instance): return client +def build_option_parser(parser): + """Hook to add global options""" + parser.add_argument( + '--os-identity-api-version', + metavar='', + default=utils.env( + 'OS_IDENTITY_API_VERSION', + default=DEFAULT_IDENTITY_API_VERSION), + help='Identity API version, default=' + + DEFAULT_IDENTITY_API_VERSION + + ' (Env: OS_IDENTITY_API_VERSION)') + parser.add_argument( + '--os-trust-id', + metavar='', + default=utils.env('OS_TRUST_ID'), + help='Trust ID to use when authenticating. ' + 'This can only be used with Keystone v3 API ' + '(Env: OS_TRUST_ID)') + return parser + + class IdentityClientv2_0(identity_client_v2_0.Client): """Tweak the earlier client class to deal with some changes""" def __getattr__(self, name): diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 287243432f..6fe5ba7d24 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -34,7 +34,6 @@ from openstackclient.common import restapi from openstackclient.common import timing from openstackclient.common import utils -from openstackclient.identity import client as identity_client KEYRING_SERVICE = 'openstack' @@ -76,6 +75,8 @@ def __init__(self): version=openstackclient.__version__, command_manager=commandmanager.CommandManager('openstack.cli')) + self.api_version = {} + # Until we have command line arguments parsed, dump any stack traces self.dump_stack_trace = True @@ -86,10 +87,14 @@ def __init__(self): # Assume TLS host certificate verification is enabled self.verify = True - # Get list of extension modules + # Get list of base modules self.ext_modules = clientmanager.get_extension_modules( - 'openstack.cli.extension', + 'openstack.cli.base', ) + # Append list of extension modules + self.ext_modules.extend(clientmanager.get_extension_modules( + 'openstack.cli.extension', + )) # Loop through extensions to get parser additions for mod in self.ext_modules: @@ -312,23 +317,6 @@ def build_option_parser(self, description, version): help="Print API call timing info", ) - parser.add_argument( - '--os-identity-api-version', - metavar='', - default=env( - 'OS_IDENTITY_API_VERSION', - default=identity_client.DEFAULT_IDENTITY_API_VERSION), - help='Identity API version, default=' + - identity_client.DEFAULT_IDENTITY_API_VERSION + - ' (Env: OS_IDENTITY_API_VERSION)') - parser.add_argument( - '--os-trust-id', - metavar='', - default=utils.env('OS_TRUST_ID'), - help='Trust ID to use when authenticating. ' - 'This can only be used with Keystone v3 API ' - '(Env: OS_TRUST_ID)') - return parser def authenticate_user(self): @@ -437,24 +425,19 @@ def initialize_app(self, argv): # Save default domain self.default_domain = self.options.os_default_domain - # Stash selected API versions for later - self.api_version = { - 'identity': self.options.os_identity_api_version, - } # Loop through extensions to get API versions for mod in self.ext_modules: - ver = getattr(self.options, mod.API_VERSION_OPTION, None) - if ver: - self.api_version[mod.API_NAME] = ver - self.log.debug('%(name)s API version %(version)s', - {'name': mod.API_NAME, 'version': ver}) - - # Add the API version-specific commands - for api in self.api_version.keys(): - version = '.v' + self.api_version[api].replace('.', '_') - cmd_group = 'openstack.' + api.replace('-', '_') + version - self.log.debug('command group %s', cmd_group) - self.command_manager.add_command_group(cmd_group) + version_opt = getattr(self.options, mod.API_VERSION_OPTION, None) + if version_opt: + api = mod.API_NAME + self.api_version[api] = version_opt + version = '.v' + version_opt.replace('.', '_') + cmd_group = 'openstack.' + api.replace('-', '_') + version + self.command_manager.add_command_group(cmd_group) + self.log.debug( + '%(name)s API version %(version)s, cmd group %(group)s', + {'name': api, 'version': version_opt, 'group': cmd_group} + ) # Commands that span multiple APIs self.command_manager.add_command_group( diff --git a/setup.cfg b/setup.cfg index b1b80a06bd..17bf62b206 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,8 +30,9 @@ console_scripts = openstack.cli = module_list = openstackclient.common.module:ListModule -openstack.cli.extension = +openstack.cli.base = compute = openstackclient.compute.client + identity = openstackclient.identity.client image = openstackclient.image.client network = openstackclient.network.client object_store = openstackclient.object.client From 181166f81bde6e96f42c2adc6dccb000312c68c0 Mon Sep 17 00:00:00 2001 From: Jeremy Stanley Date: Wed, 3 Sep 2014 19:05:47 +0000 Subject: [PATCH 0175/3494] Work toward Python 3.4 support and testing Change-Id: I70d8cf0971d18b9b2eb967e28cedecc897721f58 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index b6935c2fb4..42cf42417b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 1.6 -envlist = py26,py27,py33,pep8 +envlist = py26,py27,py33,py34,pep8 skipdist = True [testenv] From dc68d3f5cfb2df1008bfb342984bd7fe1298f917 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Thu, 4 Sep 2014 07:52:58 -0600 Subject: [PATCH 0176/3494] assertEquals order wrong Change-Id: I822b6ac5b8e8c3009d1ee2d647376eff84559c11 Partial-Bug: #1277104 --- .../tests/volume/v1/test_volume.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/openstackclient/tests/volume/v1/test_volume.py b/openstackclient/tests/volume/v1/test_volume.py index cb006b1086..f020791a4b 100644 --- a/openstackclient/tests/volume/v1/test_volume.py +++ b/openstackclient/tests/volume/v1/test_volume.py @@ -101,7 +101,7 @@ def test_volume_create_min_options(self): 'status', 'type', ) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( 'detached', volume_fakes.volume_zone, @@ -113,7 +113,7 @@ def test_volume_create_min_options(self): '', volume_fakes.volume_type, ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) def test_volume_create_options(self): arglist = [ @@ -165,7 +165,7 @@ def test_volume_create_options(self): 'status', 'type', ) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( 'detached', volume_fakes.volume_zone, @@ -177,7 +177,7 @@ def test_volume_create_options(self): '', volume_fakes.volume_type, ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) def test_volume_create_user_project_id(self): # Return a project @@ -240,7 +240,7 @@ def test_volume_create_user_project_id(self): 'status', 'type', ) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( 'detached', volume_fakes.volume_zone, @@ -252,7 +252,7 @@ def test_volume_create_user_project_id(self): '', volume_fakes.volume_type, ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) def test_volume_create_user_project_name(self): # Return a project @@ -315,7 +315,7 @@ def test_volume_create_user_project_name(self): 'status', 'type', ) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( 'detached', volume_fakes.volume_zone, @@ -327,7 +327,7 @@ def test_volume_create_user_project_name(self): '', volume_fakes.volume_type, ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) def test_volume_create_properties(self): arglist = [ @@ -376,7 +376,7 @@ def test_volume_create_properties(self): 'status', 'type', ) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( 'detached', volume_fakes.volume_zone, @@ -388,4 +388,4 @@ def test_volume_create_properties(self): '', volume_fakes.volume_type, ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) From b1663c96e6c9bc7d413c5bcf10ec370448c33d46 Mon Sep 17 00:00:00 2001 From: Aaron Rosen Date: Wed, 3 Sep 2014 23:29:46 -0700 Subject: [PATCH 0177/3494] Sync with oslo-incubator and add importutils From oslo-incubator commit: c4bfdb94c25b4488da61d77184d97f8784f21a11 Change-Id: I81d1113d113faa609ab7713a0e04667b11786247 --- openstack-common.conf | 2 + .../openstack/common/gettextutils.py | 67 ++++++----------- .../openstack/common/importutils.py | 73 +++++++++++++++++++ openstackclient/openstack/common/strutils.py | 72 ++++++++++++++++++ 4 files changed, 171 insertions(+), 43 deletions(-) create mode 100644 openstackclient/openstack/common/importutils.py diff --git a/openstack-common.conf b/openstack-common.conf index 91d6387b4b..ac923902b5 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -1,7 +1,9 @@ [DEFAULT] # The list of modules to copy from openstack-common +module=gettextutils module=install_venv_common +module=importutils module=strutils # The base module to hold the copy of openstack.common diff --git a/openstackclient/openstack/common/gettextutils.py b/openstackclient/openstack/common/gettextutils.py index 6f573a7f8d..0c82634b8f 100644 --- a/openstackclient/openstack/common/gettextutils.py +++ b/openstackclient/openstack/common/gettextutils.py @@ -23,7 +23,6 @@ """ import copy -import functools import gettext import locale from logging import handlers @@ -42,7 +41,7 @@ class TranslatorFactory(object): """Create translator functions """ - def __init__(self, domain, lazy=False, localedir=None): + def __init__(self, domain, localedir=None): """Establish a set of translation functions for the domain. :param domain: Name of translation domain, @@ -55,7 +54,6 @@ def __init__(self, domain, lazy=False, localedir=None): :type localedir: str """ self.domain = domain - self.lazy = lazy if localedir is None: localedir = os.environ.get(domain.upper() + '_LOCALEDIR') self.localedir = localedir @@ -75,16 +73,19 @@ def _make_translation_func(self, domain=None): """ if domain is None: domain = self.domain - if self.lazy: - return functools.partial(Message, domain=domain) - t = gettext.translation( - domain, - localedir=self.localedir, - fallback=True, - ) - if six.PY3: - return t.gettext - return t.ugettext + t = gettext.translation(domain, + localedir=self.localedir, + fallback=True) + # Use the appropriate method of the translation object based + # on the python version. + m = t.gettext if six.PY3 else t.ugettext + + def f(msg): + """oslo.i18n.gettextutils translation function.""" + if USE_LAZY: + return Message(msg, domain=domain) + return m(msg) + return f @property def primary(self): @@ -147,19 +148,11 @@ def enable_lazy(): your project is importing _ directly instead of using the gettextutils.install() way of importing the _ function. """ - # FIXME(dhellmann): This function will be removed in oslo.i18n, - # because the TranslatorFactory makes it superfluous. - global _, _LI, _LW, _LE, _LC, USE_LAZY - tf = TranslatorFactory('openstackclient', lazy=True) - _ = tf.primary - _LI = tf.log_info - _LW = tf.log_warning - _LE = tf.log_error - _LC = tf.log_critical + global USE_LAZY USE_LAZY = True -def install(domain, lazy=False): +def install(domain): """Install a _() function using the given translation domain. Given a translation domain, install a _() function using gettext's @@ -170,26 +163,14 @@ def install(domain, lazy=False): a translation-domain-specific environment variable (e.g. NOVA_LOCALEDIR). + Note that to enable lazy translation, enable_lazy must be + called. + :param domain: the translation domain - :param lazy: indicates whether or not to install the lazy _() function. - The lazy _() introduces a way to do deferred translation - of messages by installing a _ that builds Message objects, - instead of strings, which can then be lazily translated into - any available locale. """ - if lazy: - from six import moves - tf = TranslatorFactory(domain, lazy=True) - moves.builtins.__dict__['_'] = tf.primary - else: - localedir = '%s_LOCALEDIR' % domain.upper() - if six.PY3: - gettext.install(domain, - localedir=os.environ.get(localedir)) - else: - gettext.install(domain, - localedir=os.environ.get(localedir), - unicode=True) + from six import moves + tf = TranslatorFactory(domain) + moves.builtins.__dict__['_'] = tf.primary class Message(six.text_type): @@ -373,8 +354,8 @@ def get_available_languages(domain): 'zh_Hant_HK': 'zh_HK', 'zh_Hant': 'zh_TW', 'fil': 'tl_PH'} - for (locale, alias) in six.iteritems(aliases): - if locale in language_list and alias not in language_list: + for (locale_, alias) in six.iteritems(aliases): + if locale_ in language_list and alias not in language_list: language_list.append(alias) _AVAILABLE_LANGUAGES[domain] = language_list diff --git a/openstackclient/openstack/common/importutils.py b/openstackclient/openstack/common/importutils.py new file mode 100644 index 0000000000..69e8d8f121 --- /dev/null +++ b/openstackclient/openstack/common/importutils.py @@ -0,0 +1,73 @@ +# Copyright 2011 OpenStack Foundation. +# All Rights Reserved. +# +# 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 related utilities and helper functions. +""" + +import sys +import traceback + + +def import_class(import_str): + """Returns a class from a string including module and class.""" + mod_str, _sep, class_str = import_str.rpartition('.') + __import__(mod_str) + try: + return getattr(sys.modules[mod_str], class_str) + except AttributeError: + raise ImportError('Class %s cannot be found (%s)' % + (class_str, + traceback.format_exception(*sys.exc_info()))) + + +def import_object(import_str, *args, **kwargs): + """Import a class and return an instance of it.""" + return import_class(import_str)(*args, **kwargs) + + +def import_object_ns(name_space, import_str, *args, **kwargs): + """Tries to import object from default namespace. + + Imports a class and return an instance of it, first by trying + to find the class in a default namespace, then failing back to + a full path if not found in the default namespace. + """ + import_value = "%s.%s" % (name_space, import_str) + try: + return import_class(import_value)(*args, **kwargs) + except ImportError: + return import_class(import_str)(*args, **kwargs) + + +def import_module(import_str): + """Import a module.""" + __import__(import_str) + return sys.modules[import_str] + + +def import_versioned_module(version, submodule=None): + module = 'openstackclient.v%s' % version + if submodule: + module = '.'.join((module, submodule)) + return import_module(module) + + +def try_import(import_str, default=None): + """Try to import a module and if it fails return default.""" + try: + return import_module(import_str) + except ImportError: + return default diff --git a/openstackclient/openstack/common/strutils.py b/openstackclient/openstack/common/strutils.py index 9d70264fd3..ad3cb44c25 100644 --- a/openstackclient/openstack/common/strutils.py +++ b/openstackclient/openstack/common/strutils.py @@ -50,6 +50,39 @@ SLUGIFY_HYPHENATE_RE = re.compile(r"[-\s]+") +# NOTE(flaper87): The following globals are used by `mask_password` +_SANITIZE_KEYS = ['adminPass', 'admin_pass', 'password', 'admin_password'] + +# NOTE(ldbragst): Let's build a list of regex objects using the list of +# _SANITIZE_KEYS we already have. This way, we only have to add the new key +# to the list of _SANITIZE_KEYS and we can generate regular expressions +# for XML and JSON automatically. +_SANITIZE_PATTERNS_2 = [] +_SANITIZE_PATTERNS_1 = [] + +# NOTE(amrith): Some regular expressions have only one parameter, some +# have two parameters. Use different lists of patterns here. +_FORMAT_PATTERNS_1 = [r'(%(key)s\s*[=]\s*)[^\s^\'^\"]+'] +_FORMAT_PATTERNS_2 = [r'(%(key)s\s*[=]\s*[\"\']).*?([\"\'])', + r'(%(key)s\s+[\"\']).*?([\"\'])', + r'([-]{2}%(key)s\s+)[^\'^\"^=^\s]+([\s]*)', + r'(<%(key)s>).*?()', + r'([\"\']%(key)s[\"\']\s*:\s*[\"\']).*?([\"\'])', + r'([\'"].*?%(key)s[\'"]\s*:\s*u?[\'"]).*?([\'"])', + r'([\'"].*?%(key)s[\'"]\s*,\s*\'--?[A-z]+\'\s*,\s*u?' + '[\'"]).*?([\'"])', + r'(%(key)s\s*--?[A-z]+\s*)\S+(\s*)'] + +for key in _SANITIZE_KEYS: + for pattern in _FORMAT_PATTERNS_2: + reg_ex = re.compile(pattern % {'key': key}, re.DOTALL) + _SANITIZE_PATTERNS_2.append(reg_ex) + + for pattern in _FORMAT_PATTERNS_1: + reg_ex = re.compile(pattern % {'key': key}, re.DOTALL) + _SANITIZE_PATTERNS_1.append(reg_ex) + + def int_from_bool_as_string(subject): """Interpret a string as a boolean and return either 1 or 0. @@ -237,3 +270,42 @@ def to_slug(value, incoming=None, errors="strict"): "ascii", "ignore").decode("ascii") value = SLUGIFY_STRIP_RE.sub("", value).strip().lower() return SLUGIFY_HYPHENATE_RE.sub("-", value) + + +def mask_password(message, secret="***"): + """Replace password with 'secret' in message. + + :param message: The string which includes security information. + :param secret: value with which to replace passwords. + :returns: The unicode value of message with the password fields masked. + + For example: + + >>> mask_password("'adminPass' : 'aaaaa'") + "'adminPass' : '***'" + >>> mask_password("'admin_pass' : 'aaaaa'") + "'admin_pass' : '***'" + >>> mask_password('"password" : "aaaaa"') + '"password" : "***"' + >>> mask_password("'original_password' : 'aaaaa'") + "'original_password' : '***'" + >>> mask_password("u'original_password' : u'aaaaa'") + "u'original_password' : u'***'" + """ + message = six.text_type(message) + + # NOTE(ldbragst): Check to see if anything in message contains any key + # specified in _SANITIZE_KEYS, if not then just return the message since + # we don't have to mask any passwords. + if not any(key in message for key in _SANITIZE_KEYS): + return message + + substitute = r'\g<1>' + secret + r'\g<2>' + for pattern in _SANITIZE_PATTERNS_2: + message = re.sub(pattern, substitute, message) + + substitute = r'\g<1>' + secret + for pattern in _SANITIZE_PATTERNS_1: + message = re.sub(pattern, substitute, message) + + return message From b725b5017ad2bb6a1e52bb0c32e4c864e18d124b Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Thu, 14 Aug 2014 20:56:36 -0600 Subject: [PATCH 0178/3494] Multiple args for object and container commands Have object and container create and delete handle multiple arguments. Change-Id: I389358c13ac2d99655ca26e784e3d299286c0af3 --- openstackclient/object/v1/container.py | 43 ++++++++++++++--------- openstackclient/object/v1/object.py | 48 ++++++++++++++++---------- 2 files changed, 57 insertions(+), 34 deletions(-) diff --git a/openstackclient/object/v1/container.py b/openstackclient/object/v1/container.py index 1ca07f3aa4..b3184e8802 100644 --- a/openstackclient/object/v1/container.py +++ b/openstackclient/object/v1/container.py @@ -27,7 +27,7 @@ from openstackclient.object.v1.lib import container as lib_container -class CreateContainer(show.ShowOne): +class CreateContainer(lister.Lister): """Create a container""" log = logging.getLogger(__name__ + '.CreateContainer') @@ -35,22 +35,31 @@ class CreateContainer(show.ShowOne): def get_parser(self, prog_name): parser = super(CreateContainer, self).get_parser(prog_name) parser.add_argument( - 'container', + 'containers', metavar='', - help='New container name', + nargs="+", + help='Container name(s) to create', ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) - data = lib_container.create_container( - self.app.client_manager.session, - self.app.client_manager.object_store.endpoint, - parsed_args.container, - ) + results = [] + for container in parsed_args.containers: + data = lib_container.create_container( + self.app.client_manager.session, + self.app.client_manager.object_store.endpoint, + container, + ) + results.append(data) - return zip(*sorted(six.iteritems(data))) + columns = ("account", "container", "x-trans-id") + return (columns, + (utils.get_dict_properties( + s, columns, + formatters={}, + ) for s in results)) class DeleteContainer(command.Command): @@ -61,20 +70,22 @@ class DeleteContainer(command.Command): def get_parser(self, prog_name): parser = super(DeleteContainer, self).get_parser(prog_name) parser.add_argument( - 'container', + 'containers', metavar='', - help='Container name to delete', + nargs="+", + help='Container name(s) to delete', ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) - lib_container.delete_container( - self.app.client_manager.session, - self.app.client_manager.object_store.endpoint, - parsed_args.container, - ) + for container in parsed_args.containers: + lib_container.delete_container( + self.app.client_manager.session, + self.app.client_manager.object_store.endpoint, + container, + ) class ListContainer(lister.Lister): diff --git a/openstackclient/object/v1/object.py b/openstackclient/object/v1/object.py index 812ad6e11f..f1da6be45d 100644 --- a/openstackclient/object/v1/object.py +++ b/openstackclient/object/v1/object.py @@ -27,7 +27,7 @@ from openstackclient.object.v1.lib import object as lib_object -class CreateObject(show.ShowOne): +class CreateObject(lister.Lister): """Upload an object to a container""" log = logging.getLogger(__name__ + '.CreateObject') @@ -40,23 +40,32 @@ def get_parser(self, prog_name): help='Container to store new object', ) parser.add_argument( - 'object', + 'objects', metavar='', - help='Local path of object to upload', + nargs="+", + help='Local path of object(s) to upload', ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) - data = lib_object.create_object( - self.app.client_manager.session, - self.app.client_manager.object_store.endpoint, - parsed_args.container, - parsed_args.object, - ) + results = [] + for obj in parsed_args.objects: + data = lib_object.create_object( + self.app.client_manager.session, + self.app.client_manager.object_store.endpoint, + parsed_args.container, + obj, + ) + results.append(data) - return zip(*sorted(six.iteritems(data))) + columns = ("object", "container", "etag") + return (columns, + (utils.get_dict_properties( + s, columns, + formatters={}, + ) for s in results)) class DeleteObject(command.Command): @@ -72,21 +81,24 @@ def get_parser(self, prog_name): help='Container that stores the object to delete', ) parser.add_argument( - 'object', + 'objects', metavar='', - help='Object to delete', + nargs="+", + help='Object(s) to delete', ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) - lib_object.delete_object( - self.app.client_manager.session, - self.app.client_manager.object_store.endpoint, - parsed_args.container, - parsed_args.object, - ) + for obj in parsed_args.objects: + lib_object.delete_object( + self.app.restapi, + self.app.client_manager.session, + self.app.client_manager.object_store.endpoint, + parsed_args.container, + obj, + ) class ListObject(lister.Lister): From c43854048c308c76c04e1f43b8a366353cb13816 Mon Sep 17 00:00:00 2001 From: Aaron Rosen Date: Wed, 3 Sep 2014 22:21:59 -0700 Subject: [PATCH 0179/3494] Leverage openstack.common.importutils for import_class This patch drops the import_utils method from common.utils and leverages it from openstack.common.importutils instead. Change-Id: If7e7383aa742afe44f750f916c0d90d747793150 Closes-bug: 1365273 --- openstackclient/common/utils.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py index 51c3ed4b28..cef9dcee4b 100644 --- a/openstackclient/common/utils.py +++ b/openstackclient/common/utils.py @@ -19,10 +19,10 @@ import logging import os import six -import sys import time from openstackclient.common import exceptions +from openstackclient.openstack.common import importutils def find_resource(manager, name_or_id): @@ -157,17 +157,6 @@ def env(*vars, **kwargs): return kwargs.get('default', '') -def import_class(import_str): - """Returns a class from a string including module and class - - :param import_str: a string representation of the class name - :rtype: the requested class - """ - mod_str, _sep, class_str = import_str.rpartition('.') - __import__(mod_str) - return getattr(sys.modules[mod_str], class_str) - - def get_client_class(api_name, version, version_map): """Returns the client class for the requested API version @@ -183,7 +172,7 @@ def get_client_class(api_name, version, version_map): (api_name, version, ', '.join(version_map.keys()))) raise exceptions.UnsupportedVersion(msg) - return import_class(client_path) + return importutils.import_class(client_path) def wait_for_status(status_f, From 514ecc6e963bd0c05425f0da8aec79a69ed122c6 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Thu, 4 Sep 2014 08:06:03 -0600 Subject: [PATCH 0180/3494] Unordered dicts and lists causes variable results The unordered dict and lists causes variable results. The user may see different results and tests can fail. Might as well make this more consistent. Change-Id: I7045b40b44cbf3ee0f2ca79c6ea0d279b6d8cfe3 --- openstackclient/common/utils.py | 4 ++-- openstackclient/tests/common/test_utils.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py index 51c3ed4b28..eb7f1b0e1e 100644 --- a/openstackclient/common/utils.py +++ b/openstackclient/common/utils.py @@ -77,7 +77,7 @@ def format_dict(data): """ output = "" - for s in data: + for s in sorted(data): output = output + s + "='" + six.text_type(data[s]) + "', " return output[:-2] @@ -89,7 +89,7 @@ def format_list(data): :rtype: a string formatted to a,b,c """ - return ', '.join(data) + return ', '.join(sorted(data)) def get_item_properties(item, fields, mixed_case_fields=[], formatters={}): diff --git a/openstackclient/tests/common/test_utils.py b/openstackclient/tests/common/test_utils.py index 6d75a9b5c9..e782d410bf 100644 --- a/openstackclient/tests/common/test_utils.py +++ b/openstackclient/tests/common/test_utils.py @@ -130,3 +130,15 @@ def test_find_resource_find_no_unique(self): str(result)) self.manager.get.assert_called_with(self.name) self.manager.find.assert_called_with(name=self.name) + + def test_format_dict(self): + expected = "a='b', c='d', e='f'" + self.assertEqual(expected, + utils.format_dict({'a': 'b', 'c': 'd', 'e': 'f'})) + self.assertEqual(expected, + utils.format_dict({'e': 'f', 'c': 'd', 'a': 'b'})) + + def test_format_list(self): + expected = 'a, b, c' + self.assertEqual(expected, utils.format_list(['a', 'b', 'c'])) + self.assertEqual(expected, utils.format_list(['c', 'b', 'a'])) From 0069adef5ccec501c36b8da1d2de2821a97afe07 Mon Sep 17 00:00:00 2001 From: Mouad Benchchaoui Date: Thu, 10 Jul 2014 13:23:35 +0200 Subject: [PATCH 0181/3494] Add action 'user password set' for identiy v3 This new action will allow a user to change their own password by either providing the new password as an argument (--password) or by being prompted to enter the new password. In both cases user will be prompted to enter their current password as required by the v3 API. Closes-Bug: #1337245 Change-Id: I5e1e0fd2b46a4502318da57f7cce2b236fb2d93d --- openstackclient/common/utils.py | 9 ++-- openstackclient/identity/v3/user.py | 29 +++++++++++ .../tests/identity/v3/test_user.py | 48 +++++++++++++++++++ setup.cfg | 1 + 4 files changed, 84 insertions(+), 3 deletions(-) diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py index 0258f93122..54a06b049f 100644 --- a/openstackclient/common/utils.py +++ b/openstackclient/common/utils.py @@ -233,12 +233,15 @@ def get_effective_log_level(): return min_log_lvl -def get_password(stdin): +def get_password(stdin, prompt=None, confirm=True): + message = prompt or "User Password:" if hasattr(stdin, 'isatty') and stdin.isatty(): try: while True: - first_pass = getpass.getpass("User password: ") - second_pass = getpass.getpass("Repeat user password: ") + first_pass = getpass.getpass(message) + if not confirm: + return first_pass + second_pass = getpass.getpass("Repeat " + message) if first_pass == second_pass: return first_pass print("The passwords entered were not the same") diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 38c3497339..6ba54368a8 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -323,6 +323,35 @@ def take_action(self, parsed_args): return +class SetPasswordUser(command.Command): + """Change current user password""" + + log = logging.getLogger(__name__ + '.SetPasswordUser') + + def get_parser(self, prog_name): + parser = super(SetPasswordUser, self).get_parser(prog_name) + parser.add_argument( + '--password', + metavar='', + help='New user password' + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + identity_client = self.app.client_manager.identity + + current_password = utils.get_password( + self.app.stdin, prompt="Current Password:", confirm=False) + + password = parsed_args.password + if password is None: + password = utils.get_password( + self.app.stdin, prompt="New Password:") + + identity_client.users.update_password(current_password, password) + + class ShowUser(show.ShowOne): """Show user details""" diff --git a/openstackclient/tests/identity/v3/test_user.py b/openstackclient/tests/identity/v3/test_user.py index 569d91401c..42df57736e 100644 --- a/openstackclient/tests/identity/v3/test_user.py +++ b/openstackclient/tests/identity/v3/test_user.py @@ -13,7 +13,9 @@ # under the License. # +import contextlib import copy + import mock from openstackclient.identity.v3 import user @@ -944,6 +946,52 @@ def test_user_set_disable(self): ) +class TestUserSetPassword(TestUser): + + def setUp(self): + super(TestUserSetPassword, self).setUp() + self.cmd = user.SetPasswordUser(self.app, None) + + @staticmethod + @contextlib.contextmanager + def _mock_get_password(*passwords): + mocker = mock.Mock(side_effect=passwords) + with mock.patch("openstackclient.common.utils.get_password", mocker): + yield + + def test_user_password_change(self): + current_pass = 'old_pass' + new_pass = 'new_pass' + arglist = [ + '--password', new_pass, + ] + verifylist = [ + ('password', new_pass), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Mock getting user current password. + with self._mock_get_password(current_pass): + self.cmd.take_action(parsed_args) + + self.users_mock.update_password.assert_called_with( + current_pass, new_pass + ) + + def test_user_create_password_prompt(self): + current_pass = 'old_pass' + new_pass = 'new_pass' + parsed_args = self.check_parser(self.cmd, [], []) + + # Mock getting user current and new password. + with self._mock_get_password(current_pass, new_pass): + self.cmd.take_action(parsed_args) + + self.users_mock.update_password.assert_called_with( + current_pass, new_pass + ) + + class TestUserShow(TestUser): def setUp(self): diff --git a/setup.cfg b/setup.cfg index da07d3edc5..0106e7a02b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -242,6 +242,7 @@ openstack.identity.v3 = user_delete = openstackclient.identity.v3.user:DeleteUser user_list = openstackclient.identity.v3.user:ListUser user_set = openstackclient.identity.v3.user:SetUser + user_password_set = openstackclient.identity.v3.user:SetPasswordUser user_show = openstackclient.identity.v3.user:ShowUser openstack.image.v1 = From ae957b176e5918f41024c00cbc39ea371a0c37c6 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 22 Aug 2014 17:26:07 -0500 Subject: [PATCH 0182/3494] Use Keystone client session.Session This replaces the restapi requests wrapper with the one from Keystone client so we can take advantage of the auth plugins. As a first step only the v2 and v3 token and password plugins are supported. This maintainis no changes to the command options or environment variables. The next steps will include reworking the other API client interfaces to fully utilize the single auth session. Blueprint: ksc-session-auth Change-Id: I47ec63291e4c3cf36c8061299a4764f60b36ab89 --- openstackclient/common/clientmanager.py | 68 +++- openstackclient/common/restapi.py | 332 ----------------- openstackclient/identity/client.py | 36 +- openstackclient/identity/v2_0/token.py | 3 +- openstackclient/object/v1/lib/container.py | 8 +- openstackclient/object/v1/lib/object.py | 8 +- .../tests/common/test_clientmanager.py | 40 +- openstackclient/tests/common/test_restapi.py | 341 ------------------ openstackclient/tests/identity/v2_0/fakes.py | 1 - .../tests/identity/v2_0/test_token.py | 7 +- 10 files changed, 102 insertions(+), 742 deletions(-) delete mode 100644 openstackclient/common/restapi.py delete mode 100644 openstackclient/tests/common/test_restapi.py diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index 4dcec8e013..4206ad001c 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -19,7 +19,9 @@ import pkg_resources import sys -from openstackclient.common import restapi +from keystoneclient.auth.identity import v2 as v2_auth +from keystoneclient.auth.identity import v3 as v3_auth +from keystoneclient import session from openstackclient.identity import client as identity_client @@ -80,24 +82,68 @@ def __init__(self, token=None, url=None, auth_url=None, self._cacert = verify self._insecure = False - self.session = restapi.RESTApi( - verify=verify, - debug=True, - ) + ver_prefix = identity_client.AUTH_VERSIONS[ + self._api_version[identity_client.API_NAME] + ] # Get logging from root logger root_logger = logging.getLogger('') LOG.setLevel(root_logger.getEffectiveLevel()) - restapi_logger = logging.getLogger('restapi') - restapi_logger.setLevel(root_logger.getEffectiveLevel()) - self.auth_ref = None + # NOTE(dtroyer): These plugins are hard-coded for the first step + # in using the new Keystone auth plugins. + + if self._url: + LOG.debug('Using token auth %s', ver_prefix) + if ver_prefix == 'v2': + self.auth = v2_auth.Token( + auth_url=url, + token=token, + ) + else: + self.auth = v3_auth.Token( + auth_url=url, + token=token, + ) + else: + LOG.debug('Using password auth %s', ver_prefix) + if ver_prefix == 'v2': + self.auth = v2_auth.Password( + auth_url=auth_url, + username=username, + password=password, + trust_id=trust_id, + tenant_id=project_id, + tenant_name=project_name, + ) + else: + self.auth = v3_auth.Password( + auth_url=auth_url, + username=username, + password=password, + trust_id=trust_id, + user_domain_id=user_domain_id, + user_domain_name=user_domain_name, + domain_id=domain_id, + domain_name=domain_name, + project_id=project_id, + project_name=project_name, + project_domain_id=project_domain_id, + project_domain_name=project_domain_name, + ) + + self.session = session.Session( + auth=self.auth, + verify=verify, + ) + self.auth_ref = None if not self._url: + # Trigger the auth call + self.auth_ref = self.session.auth.get_auth_ref(self.session) # Populate other password flow attributes - self.auth_ref = self.identity.auth_ref - self._token = self.identity.auth_token - self._service_catalog = self.identity.service_catalog + self._token = self.session.auth.get_token(self.session) + self._service_catalog = self.auth_ref.service_catalog return diff --git a/openstackclient/common/restapi.py b/openstackclient/common/restapi.py deleted file mode 100644 index a646acb364..0000000000 --- a/openstackclient/common/restapi.py +++ /dev/null @@ -1,332 +0,0 @@ -# Copyright 2013 Nebula Inc. -# -# 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. -# - -"""REST API bits""" - -import json -import logging -import requests - -try: - from urllib.parse import urlencode # noqa -except ImportError: - from urllib import urlencode # noqa - - -USER_AGENT = 'RAPI' - -_logger = logging.getLogger(__name__) - - -class RESTApi(object): - """A REST API client that handles the interface from us to the server - - RESTApi is requests.Session wrapper that knows how to do: - * JSON serialization/deserialization - * log requests in 'curl' format - * basic API boilerplate for create/delete/list/set/show verbs - - * authentication is handled elsewhere and a token is passed in - - The expectation that there will be a RESTApi object per authentication - token in use, i.e. project/username/auth_endpoint - - On the other hand, a Client knows details about the specific REST Api that - it communicates with, such as the available endpoints, API versions, etc. - """ - - def __init__( - self, - session=None, - auth_header=None, - user_agent=USER_AGENT, - verify=True, - logger=None, - debug=None, - ): - """Construct a new REST client - - :param object session: A Session object to be used for - communicating with the identity service. - :param string auth_header: A token from an initialized auth_reference - to be used in the X-Auth-Token header - :param string user_agent: Set the User-Agent header in the requests - :param boolean/string verify: If ``True``, the SSL cert will be - verified. A CA_BUNDLE path can also be - provided. - :param logging.Logger logger: A logger to output to. (optional) - :param boolean debug: Enables debug logging of all request and - responses to identity service. - default False (optional) - """ - - self.set_auth(auth_header) - self.debug = debug - - if not session: - # We create a default session object - session = requests.Session() - self.session = session - self.session.verify = verify - self.session.user_agent = user_agent - - if logger: - self.logger = logger - else: - self.logger = _logger - - def set_auth(self, auth_header): - """Sets the current auth blob""" - self.auth_header = auth_header - - def set_header(self, header, content): - """Sets passed in headers into the session headers - - Replaces existing headers!! - """ - if content is None: - del self.session.headers[header] - else: - self.session.headers[header] = content - - def request(self, method, url, **kwargs): - """Make an authenticated (if token available) request - - :param method: Request HTTP method - :param url: Request URL - :param data: Request body - :param json: Request body to be encoded as JSON - Overwrites ``data`` argument if present - """ - - kwargs.setdefault('headers', {}) - if self.auth_header: - kwargs['headers']['X-Auth-Token'] = self.auth_header - - if 'json' in kwargs and isinstance(kwargs['json'], type({})): - kwargs['data'] = json.dumps(kwargs.pop('json')) - kwargs['headers']['Content-Type'] = 'application/json' - - kwargs.setdefault('allow_redirects', True) - - if self.debug: - self._log_request(method, url, **kwargs) - - response = self.session.request(method, url, **kwargs) - - if self.debug: - self._log_response(response) - - return self._error_handler(response) - - def _error_handler(self, response): - if response.status_code < 200 or response.status_code >= 300: - self.logger.debug( - "ERROR: %s", - response.text, - ) - response.raise_for_status() - return response - - # Convenience methods to mimic the ones provided by requests.Session - - def delete(self, url, **kwargs): - """Send a DELETE request. Returns :class:`requests.Response` object. - - :param url: Request URL - :param \*\*kwargs: Optional arguments passed to ``request`` - """ - - return self.request('DELETE', url, **kwargs) - - def get(self, url, **kwargs): - """Send a GET request. Returns :class:`requests.Response` object. - - :param url: Request URL - :param \*\*kwargs: Optional arguments passed to ``request`` - """ - - return self.request('GET', url, **kwargs) - - def head(self, url, **kwargs): - """Send a HEAD request. Returns :class:`requests.Response` object. - - :param url: Request URL - :param \*\*kwargs: Optional arguments passed to ``request`` - """ - - kwargs.setdefault('allow_redirects', False) - return self.request('HEAD', url, **kwargs) - - def options(self, url, **kwargs): - """Send an OPTIONS request. Returns :class:`requests.Response` object. - - :param url: Request URL - :param \*\*kwargs: Optional arguments passed to ``request`` - """ - - return self.request('OPTIONS', url, **kwargs) - - def patch(self, url, data=None, json=None, **kwargs): - """Send a PUT request. Returns :class:`requests.Response` object. - - :param url: Request URL - :param data: Request body - :param json: Request body to be encoded as JSON - Overwrites ``data`` argument if present - :param \*\*kwargs: Optional arguments passed to ``request`` - """ - - if json: - kwargs['json'] = json - if data: - kwargs['data'] = data - return self.request('PATCH', url, **kwargs) - - def post(self, url, data=None, json=None, **kwargs): - """Send a POST request. Returns :class:`requests.Response` object. - - :param url: Request URL - :param data: Request body - :param json: Request body to be encoded as JSON - Overwrites ``data`` argument if present - :param \*\*kwargs: Optional arguments passed to ``request`` - """ - - if json: - kwargs['json'] = json - if data: - kwargs['data'] = data - return self.request('POST', url, **kwargs) - - def put(self, url, data=None, json=None, **kwargs): - """Send a PUT request. Returns :class:`requests.Response` object. - - :param url: Request URL - :param data: Request body - :param json: Request body to be encoded as JSON - Overwrites ``data`` argument if present - :param \*\*kwargs: Optional arguments passed to ``request`` - """ - - if json: - kwargs['json'] = json - if data: - kwargs['data'] = data - return self.request('PUT', url, **kwargs) - - # Command verb methods - - def create(self, url, data=None, response_key=None, **kwargs): - """Create a new object via a POST request - - :param url: Request URL - :param data: Request body, wil be JSON encoded - :param response_key: Dict key in response body to extract - :param \*\*kwargs: Optional arguments passed to ``request`` - """ - - response = self.request('POST', url, json=data, **kwargs) - if response_key: - return response.json()[response_key] - else: - return response.json() - - def list(self, url, data=None, response_key=None, **kwargs): - """Retrieve a list of objects via a GET or POST request - - :param url: Request URL - :param data: Request body, will be JSON encoded - :param response_key: Dict key in response body to extract - :param \*\*kwargs: Optional arguments passed to ``request`` - """ - - if data: - response = self.request('POST', url, json=data, **kwargs) - else: - response = self.request('GET', url, **kwargs) - - if response_key: - return response.json()[response_key] - else: - return response.json() - - def set(self, url, data=None, response_key=None, **kwargs): - """Update an object via a PUT request - - :param url: Request URL - :param data: Request body - :param json: Request body to be encoded as JSON - Overwrites ``data`` argument if present - :param \*\*kwargs: Optional arguments passed to ``request`` - """ - - response = self.request('PUT', url, json=data) - if data: - if response_key: - return response.json()[response_key] - else: - return response.json() - else: - # Nothing to do here - return None - - def show(self, url, response_key=None, **kwargs): - """Retrieve a single object via a GET request - - :param url: Request URL - :param response_key: Dict key in response body to extract - :param \*\*kwargs: Optional arguments passed to ``request`` - """ - - response = self.request('GET', url, **kwargs) - if response_key: - return response.json()[response_key] - else: - return response.json() - - def _log_request(self, method, url, **kwargs): - if 'params' in kwargs and kwargs['params'] != {}: - url += '?' + urlencode(kwargs['params']) - - string_parts = [ - "curl -i", - "-X '%s'" % method, - "'%s'" % url, - ] - - for element in kwargs['headers']: - header = " -H '%s: %s'" % (element, kwargs['headers'][element]) - string_parts.append(header) - - self.logger.debug("REQ: %s" % " ".join(string_parts)) - if 'data' in kwargs: - self.logger.debug(" REQ BODY: %r\n" % (kwargs['data'])) - - def _log_response(self, response): - self.logger.debug( - "RESP: [%s] %r\n", - response.status_code, - response.headers, - ) - if response._content_consumed: - self.logger.debug( - " RESP BODY: %s\n", - response.text, - ) - self.logger.debug( - " encoding: %s", - response.encoding, - ) diff --git a/openstackclient/identity/client.py b/openstackclient/identity/client.py index 820d08cb2b..a43b50e373 100644 --- a/openstackclient/identity/client.py +++ b/openstackclient/identity/client.py @@ -29,6 +29,12 @@ '3': 'keystoneclient.v3.client.Client', } +# Translate our API version to auth plugin version prefix +AUTH_VERSIONS = { + '2.0': 'v2', + '3': 'v3', +} + def make_client(instance): """Returns an identity service client.""" @@ -38,6 +44,8 @@ def make_client(instance): API_VERSIONS) LOG.debug('Instantiating identity client: %s', identity_client) + # TODO(dtroyer): Something doesn't like the session.auth when using + # token auth, chase that down. if instance._url: LOG.debug('Using token auth') client = identity_client( @@ -50,32 +58,14 @@ def make_client(instance): else: LOG.debug('Using password auth') client = identity_client( - username=instance._username, - password=instance._password, - user_domain_id=instance._user_domain_id, - user_domain_name=instance._user_domain_name, - project_domain_id=instance._project_domain_id, - project_domain_name=instance._project_domain_name, - domain_id=instance._domain_id, - domain_name=instance._domain_name, - tenant_name=instance._project_name, - tenant_id=instance._project_id, - auth_url=instance._auth_url, - region_name=instance._region_name, + session=instance.session, cacert=instance._cacert, - insecure=instance._insecure, - trust_id=instance._trust_id, ) - # TODO(dtroyer): the identity v2 role commands use this yet, fix that - # so we can remove it - instance.auth_ref = client.auth_ref - - # NOTE(dtroyer): this is hanging around until restapi is replace by - # ksc session - instance.session.set_auth( - client.auth_ref.auth_token, - ) + # TODO(dtroyer): the identity v2 role commands use this yet, fix that + # so we can remove it + if not instance._url: + instance.auth_ref = instance.auth.get_auth_ref(instance.session) return client diff --git a/openstackclient/identity/v2_0/token.py b/openstackclient/identity/v2_0/token.py index cc2c8a7fd4..f3fedc0107 100644 --- a/openstackclient/identity/v2_0/token.py +++ b/openstackclient/identity/v2_0/token.py @@ -33,9 +33,8 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) - identity_client = self.app.client_manager.identity - token = identity_client.service_catalog.get_token() + token = self.app.client_manager.auth_ref.service_catalog.get_token() token['project_id'] = token.pop('tenant_id') return zip(*sorted(six.iteritems(token))) diff --git a/openstackclient/object/v1/lib/container.py b/openstackclient/object/v1/lib/container.py index 65a9fe4d02..63711838f4 100644 --- a/openstackclient/object/v1/lib/container.py +++ b/openstackclient/object/v1/lib/container.py @@ -29,7 +29,7 @@ def create_container( ): """Create a container - :param session: a restapi object + :param session: an authenticated keystoneclient.session.Session object :param url: endpoint :param container: name of container to create :returns: dict of returned headers @@ -53,7 +53,7 @@ def delete_container( ): """Delete a container - :param session: a restapi object + :param session: an authenticated keystoneclient.session.Session object :param url: endpoint :param container: name of container to delete """ @@ -72,7 +72,7 @@ def list_containers( ): """Get containers in an account - :param session: a restapi object + :param session: an authenticated keystoneclient.session.Session object :param url: endpoint :param marker: marker query :param limit: limit query @@ -127,7 +127,7 @@ def show_container( ): """Get container details - :param session: a restapi object + :param session: an authenticated keystoneclient.session.Session object :param url: endpoint :param container: name of container to show :returns: dict of returned headers diff --git a/openstackclient/object/v1/lib/object.py b/openstackclient/object/v1/lib/object.py index 0ded0dadbe..5000c79d08 100644 --- a/openstackclient/object/v1/lib/object.py +++ b/openstackclient/object/v1/lib/object.py @@ -32,7 +32,7 @@ def create_object( ): """Create an object, upload it to a container - :param session: a restapi object + :param session: an authenticated keystoneclient.session.Session object :param url: endpoint :param container: name of container to store object :param object: local path to object @@ -61,7 +61,7 @@ def delete_object( ): """Delete an object stored in a container - :param session: a restapi object + :param session: an authenticated keystoneclient.session.Session object :param url: endpoint :param container: name of container that stores object :param container: name of object to delete @@ -84,7 +84,7 @@ def list_objects( ): """Get objects in a container - :param session: a restapi object + :param session: an authenticated keystoneclient.session.Session object :param url: endpoint :param container: container name to get a listing for :param marker: marker query @@ -158,7 +158,7 @@ def show_object( ): """Get object details - :param session: a restapi object + :param session: an authenticated keystoneclient.session.Session object :param url: endpoint :param container: container name to get a listing for :returns: dict of object properties diff --git a/openstackclient/tests/common/test_clientmanager.py b/openstackclient/tests/common/test_clientmanager.py index 5a25fa2c9b..0bb657adb0 100644 --- a/openstackclient/tests/common/test_clientmanager.py +++ b/openstackclient/tests/common/test_clientmanager.py @@ -13,8 +13,10 @@ # under the License. # +import mock + +from keystoneclient.auth.identity import v2 as auth_v2 from openstackclient.common import clientmanager -from openstackclient.common import restapi from openstackclient.tests import utils @@ -25,6 +27,10 @@ PASSWORD = "scratchy" SERVICE_CATALOG = {'sc': '123'} +API_VERSION = { + 'identity': '2.0', +} + def FakeMakeClient(instance): return FakeClient() @@ -52,6 +58,7 @@ def test_singleton(self): self.assertEqual(c.attr, c.attr) +@mock.patch('keystoneclient.session.Session') class TestClientManager(utils.TestCase): def setUp(self): super(TestClientManager, self).setUp() @@ -59,12 +66,13 @@ def setUp(self): clientmanager.ClientManager.identity = \ clientmanager.ClientCache(FakeMakeClient) - def test_client_manager_token(self): + def test_client_manager_token(self, mock): client_manager = clientmanager.ClientManager( token=AUTH_TOKEN, url=AUTH_URL, verify=True, + api_version=API_VERSION, ) self.assertEqual( @@ -76,19 +84,20 @@ def test_client_manager_token(self): client_manager._url, ) self.assertIsInstance( - client_manager.session, - restapi.RESTApi, + client_manager.auth, + auth_v2.Token, ) self.assertFalse(client_manager._insecure) self.assertTrue(client_manager._verify) - def test_client_manager_password(self): + def test_client_manager_password(self, mock): client_manager = clientmanager.ClientManager( auth_url=AUTH_URL, username=USERNAME, password=PASSWORD, verify=False, + api_version=API_VERSION, ) self.assertEqual( @@ -104,33 +113,20 @@ def test_client_manager_password(self): client_manager._password, ) self.assertIsInstance( - client_manager.session, - restapi.RESTApi, + client_manager.auth, + auth_v2.Password, ) self.assertTrue(client_manager._insecure) self.assertFalse(client_manager._verify) - # These need to stick around until the old-style clients are gone - self.assertEqual( - AUTH_REF, - client_manager.auth_ref, - ) - self.assertEqual( - AUTH_TOKEN, - client_manager._token, - ) - self.assertEqual( - SERVICE_CATALOG, - client_manager._service_catalog, - ) - - def test_client_manager_password_verify_ca(self): + def test_client_manager_password_verify_ca(self, mock): client_manager = clientmanager.ClientManager( auth_url=AUTH_URL, username=USERNAME, password=PASSWORD, verify='cafile', + api_version=API_VERSION, ) self.assertFalse(client_manager._insecure) diff --git a/openstackclient/tests/common/test_restapi.py b/openstackclient/tests/common/test_restapi.py deleted file mode 100644 index d4fe2d3db1..0000000000 --- a/openstackclient/tests/common/test_restapi.py +++ /dev/null @@ -1,341 +0,0 @@ -# Copyright 2013 Nebula Inc. -# -# 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. -# - -"""Test rest module""" - -import json -import mock - -import requests -import six - -from openstackclient.common import restapi -from openstackclient.tests import utils - -fake_user_agent = 'test_rapi' - -fake_auth = '11223344556677889900' -fake_url = 'http://gopher.com' -fake_key = 'gopher' -fake_keys = 'gophers' -fake_gopher_mac = { - 'id': 'g1', - 'name': 'mac', - 'actor': 'Mel Blanc', -} -fake_gopher_tosh = { - 'id': 'g2', - 'name': 'tosh', - 'actor': 'Stan Freeberg', -} -fake_gopher_single = { - fake_key: fake_gopher_mac, -} -fake_gopher_list = { - fake_keys: - [ - fake_gopher_mac, - fake_gopher_tosh, - ] -} -fake_headers = { - 'User-Agent': fake_user_agent, -} - - -class FakeResponse(requests.Response): - def __init__(self, headers={}, status_code=200, data=None, encoding=None): - super(FakeResponse, self).__init__() - - self.status_code = status_code - - self.headers.update(headers) - self._content = json.dumps(data) - if not isinstance(self._content, six.binary_type): - self._content = self._content.encode() - - -@mock.patch('openstackclient.common.restapi.requests.Session') -class TestRESTApi(utils.TestCase): - - def test_request_get(self, session_mock): - resp = FakeResponse(status_code=200, data=fake_gopher_single) - session_mock.return_value = mock.MagicMock( - request=mock.MagicMock(return_value=resp), - ) - - api = restapi.RESTApi( - user_agent=fake_user_agent, - ) - gopher = api.request('GET', fake_url) - session_mock.return_value.request.assert_called_with( - 'GET', - fake_url, - headers={}, - allow_redirects=True, - ) - self.assertEqual(gopher.status_code, 200) - self.assertEqual(gopher.json(), fake_gopher_single) - - def test_request_get_return_300(self, session_mock): - resp = FakeResponse(status_code=300, data=fake_gopher_single) - session_mock.return_value = mock.MagicMock( - request=mock.MagicMock(return_value=resp), - ) - - api = restapi.RESTApi( - user_agent=fake_user_agent, - ) - gopher = api.request('GET', fake_url) - session_mock.return_value.request.assert_called_with( - 'GET', - fake_url, - headers={}, - allow_redirects=True, - ) - self.assertEqual(gopher.status_code, 300) - self.assertEqual(gopher.json(), fake_gopher_single) - - def test_request_get_fail_404(self, session_mock): - resp = FakeResponse(status_code=404, data=fake_gopher_single) - session_mock.return_value = mock.MagicMock( - request=mock.MagicMock(return_value=resp), - ) - - api = restapi.RESTApi( - user_agent=fake_user_agent, - ) - self.assertRaises(requests.HTTPError, api.request, 'GET', fake_url) - session_mock.return_value.request.assert_called_with( - 'GET', - fake_url, - headers={}, - allow_redirects=True, - ) - - def test_request_get_auth(self, session_mock): - resp = FakeResponse(data=fake_gopher_single) - session_mock.return_value = mock.MagicMock( - request=mock.MagicMock(return_value=resp), - headers=mock.MagicMock(return_value={}), - ) - - api = restapi.RESTApi( - auth_header=fake_auth, - user_agent=fake_user_agent, - ) - gopher = api.request('GET', fake_url) - session_mock.return_value.request.assert_called_with( - 'GET', - fake_url, - headers={ - 'X-Auth-Token': fake_auth, - }, - allow_redirects=True, - ) - self.assertEqual(gopher.json(), fake_gopher_single) - - def test_request_post(self, session_mock): - resp = FakeResponse(data=fake_gopher_single) - session_mock.return_value = mock.MagicMock( - request=mock.MagicMock(return_value=resp), - ) - - api = restapi.RESTApi( - user_agent=fake_user_agent, - ) - data = fake_gopher_tosh - gopher = api.request('POST', fake_url, json=data) - session_mock.return_value.request.assert_called_with( - 'POST', - fake_url, - headers={ - 'Content-Type': 'application/json', - }, - allow_redirects=True, - data=json.dumps(data), - ) - self.assertEqual(gopher.json(), fake_gopher_single) - - # Methods - # TODO(dtroyer): add the other method methods - - def test_delete(self, session_mock): - resp = FakeResponse(status_code=200, data=None) - session_mock.return_value = mock.MagicMock( - request=mock.MagicMock(return_value=resp), - ) - - api = restapi.RESTApi() - gopher = api.delete(fake_url) - session_mock.return_value.request.assert_called_with( - 'DELETE', - fake_url, - headers=mock.ANY, - allow_redirects=True, - ) - self.assertEqual(gopher.status_code, 200) - - # Commands - - def test_create(self, session_mock): - resp = FakeResponse(data=fake_gopher_single) - session_mock.return_value = mock.MagicMock( - request=mock.MagicMock(return_value=resp), - ) - - api = restapi.RESTApi() - data = fake_gopher_mac - - # Test no key - gopher = api.create(fake_url, data=data) - session_mock.return_value.request.assert_called_with( - 'POST', - fake_url, - headers=mock.ANY, - allow_redirects=True, - data=json.dumps(data), - ) - self.assertEqual(gopher, fake_gopher_single) - - # Test with key - gopher = api.create(fake_url, data=data, response_key=fake_key) - session_mock.return_value.request.assert_called_with( - 'POST', - fake_url, - headers=mock.ANY, - allow_redirects=True, - data=json.dumps(data), - ) - self.assertEqual(gopher, fake_gopher_mac) - - def test_list(self, session_mock): - resp = FakeResponse(data=fake_gopher_list) - session_mock.return_value = mock.MagicMock( - request=mock.MagicMock(return_value=resp), - ) - - # test base - api = restapi.RESTApi() - gopher = api.list(fake_url, response_key=fake_keys) - session_mock.return_value.request.assert_called_with( - 'GET', - fake_url, - headers=mock.ANY, - allow_redirects=True, - ) - self.assertEqual(gopher, [fake_gopher_mac, fake_gopher_tosh]) - - # test body - api = restapi.RESTApi() - data = {'qwerty': 1} - gopher = api.list(fake_url, response_key=fake_keys, data=data) - session_mock.return_value.request.assert_called_with( - 'POST', - fake_url, - headers=mock.ANY, - allow_redirects=True, - data=json.dumps(data), - ) - self.assertEqual(gopher, [fake_gopher_mac, fake_gopher_tosh]) - - # test query params - api = restapi.RESTApi() - params = {'qaz': '123'} - gophers = api.list(fake_url, response_key=fake_keys, params=params) - session_mock.return_value.request.assert_called_with( - 'GET', - fake_url, - headers=mock.ANY, - allow_redirects=True, - params=params, - ) - self.assertEqual(gophers, [fake_gopher_mac, fake_gopher_tosh]) - - def test_set(self, session_mock): - new_gopher = fake_gopher_single - new_gopher[fake_key]['name'] = 'Chip' - resp = FakeResponse(data=fake_gopher_single) - session_mock.return_value = mock.MagicMock( - request=mock.MagicMock(return_value=resp), - ) - - api = restapi.RESTApi() - data = fake_gopher_mac - data['name'] = 'Chip' - - # Test no data, no key - gopher = api.set(fake_url) - session_mock.return_value.request.assert_called_with( - 'PUT', - fake_url, - headers=mock.ANY, - allow_redirects=True, - json=None, - ) - self.assertEqual(gopher, None) - - # Test data, no key - gopher = api.set(fake_url, data=data) - session_mock.return_value.request.assert_called_with( - 'PUT', - fake_url, - headers=mock.ANY, - allow_redirects=True, - data=json.dumps(data), - ) - self.assertEqual(gopher, fake_gopher_single) - - # NOTE:(dtroyer): Key and no data is not tested as without data - # the response_key is moot - - # Test data and key - gopher = api.set(fake_url, data=data, response_key=fake_key) - session_mock.return_value.request.assert_called_with( - 'PUT', - fake_url, - headers=mock.ANY, - allow_redirects=True, - data=json.dumps(data), - ) - self.assertEqual(gopher, fake_gopher_mac) - - def test_show(self, session_mock): - resp = FakeResponse(data=fake_gopher_single) - session_mock.return_value = mock.MagicMock( - request=mock.MagicMock(return_value=resp), - ) - - api = restapi.RESTApi() - - # Test no key - gopher = api.show(fake_url) - session_mock.return_value.request.assert_called_with( - 'GET', - fake_url, - headers=mock.ANY, - allow_redirects=True, - ) - self.assertEqual(gopher, fake_gopher_single) - - # Test with key - gopher = api.show(fake_url, response_key=fake_key) - session_mock.return_value.request.assert_called_with( - 'GET', - fake_url, - headers=mock.ANY, - allow_redirects=True, - ) - self.assertEqual(gopher, fake_gopher_mac) diff --git a/openstackclient/tests/identity/v2_0/fakes.py b/openstackclient/tests/identity/v2_0/fakes.py index a8438e9663..b136f84165 100644 --- a/openstackclient/tests/identity/v2_0/fakes.py +++ b/openstackclient/tests/identity/v2_0/fakes.py @@ -125,7 +125,6 @@ class FakeIdentityv2Client(object): def __init__(self, **kwargs): self.roles = mock.Mock() self.roles.resource_class = fakes.FakeResource(None, {}) - self.service_catalog = mock.Mock() self.services = mock.Mock() self.services.resource_class = fakes.FakeResource(None, {}) self.tenants = mock.Mock() diff --git a/openstackclient/tests/identity/v2_0/test_token.py b/openstackclient/tests/identity/v2_0/test_token.py index e094ad4ab7..4184326c2b 100644 --- a/openstackclient/tests/identity/v2_0/test_token.py +++ b/openstackclient/tests/identity/v2_0/test_token.py @@ -13,6 +13,8 @@ # under the License. # +import mock + from openstackclient.identity.v2_0 import token from openstackclient.tests.identity.v2_0 import fakes as identity_fakes @@ -23,8 +25,9 @@ def setUp(self): super(TestToken, self).setUp() # Get a shortcut to the Service Catalog Mock - self.sc_mock = self.app.client_manager.identity.service_catalog - self.sc_mock.reset_mock() + self.sc_mock = mock.Mock() + self.app.client_manager.auth_ref = mock.Mock() + self.app.client_manager.auth_ref.service_catalog = self.sc_mock class TestTokenIssue(TestToken): From f7357b7fe78b7d7afe099a867d1dca14b19f8bc1 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Sat, 6 Sep 2014 14:32:47 -0500 Subject: [PATCH 0183/3494] Update docs and release notes for 0.4.1 release Closes-Bug: 1365505 Change-Id: I027d263ba9980715454b034a37733c5ff23fd169 --- doc/source/commands.rst | 52 ++++++++++++++++++++++++++-- doc/source/index.rst | 5 ++- doc/source/man/openstack.rst | 66 +++++++++++++++++++++++------------- doc/source/plugins.rst | 55 +++++++++++++++++++++++++++++- doc/source/releases.rst | 45 ++++++++++++++++++++++++ 5 files changed, 193 insertions(+), 30 deletions(-) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index a9d10c96d3..45e73a0594 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -62,6 +62,54 @@ They follow the same style as the global options and always appear between the command and any positional arguments the command requires. +Objects +------- + +The objects consist of one or more words to compose a unique name. +Occasionally when multiple APIs have a common name with common +overlapping purposes there will be options to select which object to use, or +the API resources will be merged, as in the ``quota`` object that has options +referring to both Compute and Volume quotas. + +* ``access token``: Identity - long-lived OAuth-based token +* ``aggregate``: Compute - a grouping of servers +* ``backup``: Volume - a volume copy +* ``console log``: Compute - a text dump of a server's console +* ``console url``: Compute - a URL to a server's remote console +* ``consumer``: Identity - OAuth-based delegatee +* ``container``: Object Store - a grouping of objects +* ``credential``: Identity - specific to identity providers +* ``domain``: Identity - a grouping of projects +* ``endpoint``: Identity - the base URL used to contact a specific service +* ``extension``: Compute, Identity, Volume - additional APIs available +* ``flavor``: Compute - pre-defined configurations of servers: ram, root disk, etc +* ``group``: Identity - a grouping of users +* ``host``: Compute - the physical computer running a hypervisor +* ``hypervisor``: Compute - the virtual machine manager +* ``identity provider``: Identity - a source of users and authentication +* ``image``: Image - a disk image +* ``ip fixed``: Compute, Network - an internal IP address assigned to a server +* ``ip floating``: Compute, Network - a public IP address that can be mapped to a server +* ``keypair``: Compute - an SSH public key +* ``limits``: Compute, Volume - resource usage limits +* ``module``: internal - installed Python modules in the OSC process +* ``network``: Network - a virtual network for connecting servers and other resources +* ``object``: Object Store - a single file in the Object Store +* ``policy``: Identity - determines authorization +* ``project``: Identity - the owner of a group of resources +* ``quota``: Compute, Volume - limit on resource usage +* ``request token``: Identity - temporary OAuth-based token +* ``role``: Identity - a policy object used to determine authorization +* ``security group``: Compute, Network - groups of network access rules +* ``security group rule``: Compute, Network - the individual rules that define protocol/IP/port access +* ``server``: Compute - a virtual machine instance +* ``service``: Identity - a cloud service +* ``snapshot``: Volume - a point-in-time copy of a volume +* ``token``: Identity - the magic text used to determine access +* ``user``: Identity - individuals using cloud resources +* ``volume``: Volume - block volumes +* ``volume type``: Volume - deployment-specific types of volumes available + Actions ------- @@ -103,7 +151,7 @@ Implementation The command structure is designed to support seamless addition of plugin command modules via ``setuptools`` entry points. The plugin commands must -be subclasses of Cliff's command.Command object. See :doc:`plugins` for +be subclasses of Cliff's ``command.Command`` object. See :doc:`plugins` for more information. @@ -117,7 +165,7 @@ supported. For example, to support Identity API v3 there is a group called ``openstack.identity.v3`` that contains the individual commands. The command entry points have the form:: - verb_object = fully.qualified.module.vXX.object:VerbObject + action_object = fully.qualified.module.vXX.object:ActionObject For example, the ``list user`` command for the Identity API is identified in ``setup.cfg`` with:: diff --git a/doc/source/index.rst b/doc/source/index.rst index 2bb8f3868b..569a5aa1b2 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -3,9 +3,8 @@ OpenStackClient =============== OpenStackClient (aka OSC) is a command-line client for OpenStack that -unifies the operation -of python-keystoneclient, python-novaclient, python-glanceclient and -python-cinderclient in a single shell with a uniform command structure. +brings the command set for Compute, Identity, Image, Object Store and Volume +APIs together in a single shell with a uniform command structure. Contents: diff --git a/doc/source/man/openstack.rst b/doc/source/man/openstack.rst index 9af1c42513..b8dcbd6b66 100644 --- a/doc/source/man/openstack.rst +++ b/doc/source/man/openstack.rst @@ -56,18 +56,24 @@ OPTIONS :option:`--os-username` Authentication username +:option:`--os-password` + Authentication password + :option:`--os-user-domain-name` | :option:`--os-user-domain-id` Domain name or id containing user -:option:`--os-password` - Authentication password +:option:`--os-user-domain-name` | :option:`--os-user-domain-id` + Domain name or ID containing user -:option:`--os-region-name` - Authentication region name +:option:`--os-trust-id` + id of the trust to use as a trustee user :option:`--os-default-domain` Default domain ID (Default: 'default') +:option:`--os-region-name` + Authentication region name + :option:`--os-cacert` CA certificate bundle file @@ -80,9 +86,6 @@ OPTIONS :option:`--os-XXXX-api-version` Additional API version options will be available depending on the installed API libraries. -:option:`--os-trust-id` - id of the trust to use as a trustee user - COMMANDS ======== @@ -94,6 +97,11 @@ To get a description of a specific command:: openstack help +Note that the set of commands shown will vary depending on the API versions +that are in effect at that time. For example, to force the display of the +Identity v3 commands: + + openstack --os-identity-api-version 3 --help :option:`complete` Print the bash completion functions for the current command set. @@ -101,6 +109,25 @@ To get a description of a specific command:: :option:`help ` Print help for an individual command +Additional information on the OpenStackClient command structure and arguments +is available in the `OpenStackClient Commands`_ wiki page. + +.. _`OpenStackClient Commands`: https://wiki.openstack.org/wiki/OpenStackClient/Commands + +Command Objects +--------------- + +The list of command objects is growing longer with the addition of OpenStack +project support. The object names may consist of multiple words to compose a +unique name. Occasionally when multiple APIs have a common name with common +overlapping purposes there will be options to select which object to use, or +the API resources will be merged, as in the ``quota`` object that has options +referring to both Compute and Volume quotas. + +Command Actions +--------------- + +The actions used by OpenStackClient are defined with specific meaning to provide a consistent behavior for each object. Some actions have logical opposite actions, and those pairs will always match for any object that uses them. NOTES ===== @@ -162,39 +189,30 @@ The following environment variables can be set to alter the behaviour of :progra :envvar:`OS_USERNAME` Authentication username -:envvar:`OS_USER_DOMAIN_NAME` - Domain name or id containing user - :envvar:`OS_PASSWORD` Authentication password -:envvar:`OS_REGION_NAME` - Authentication region name +:envvar:`OS_USER_DOMAIN_NAME` + Domain name or id containing user + +:envvar:`OS_TRUST_ID` + id of the trust to use as a trustee user :envvar:`OS_DEFAULT_DOMAIN` Default domain ID (Default: 'default') +:envvar:`OS_REGION_NAME` + Authentication region name + :envvar:`OS_CACERT` CA certificate bundle file -:envvar:`OS_COMPUTE_API_VERSION` - Compute API version (Default: 2) - :envvar:`OS_IDENTITY_API_VERSION` Identity API version (Default: 2.0) -:envvar:`OS_IMAGE_API_VERSION` - Image API version (Default: 1) - -:envvar:`OS_VOLUME_API_VERSION` - Volume API version (Default: 1) - :envvar:`OS_XXXX_API_VERSION` Additional API version options will be available depending on the installed API libraries. -:envvar:`OS_TRUST_ID` - id of the trust to use as a trustee user - BUGS ==== diff --git a/doc/source/plugins.rst b/doc/source/plugins.rst index 690c1269c8..3b35ec08e8 100644 --- a/doc/source/plugins.rst +++ b/doc/source/plugins.rst @@ -20,7 +20,7 @@ client module. openstack.cli.extension = oscplugin = oscplugin.client -The client module must implement the following interface functions: +The client module must define the following top-level variables: * ``API_NAME`` - A string containing the plugin API name; this is the name of the entry point declaring the plugin client module @@ -30,6 +30,9 @@ The client module must implement the following interface functions: version attribute; this must be a valid Python identifier and match the destination set in ``build_option_parser()``. * ``API_VERSIONS`` - A dict mapping a version string to the client class + +The client module must implement the following interface functions: + * ``build_option_parser(parser)`` - Hook to add global options to the parser * ``make_client(instance)`` - Hook to create the client object @@ -44,3 +47,53 @@ defined for the API version: Note that OSC defines the group name as :py:mod:`openstack..v` so the version should not contain the leading 'v' character. + +:: + + DEFAULT_OSCPLUGIN_API_VERSION = '1' + + # Required by the OSC plugin interface + API_NAME = 'oscplugin' + API_VERSION_OPTION = 'os_oscplugin_api_version' + API_VERSIONS = { + '1': 'oscplugin.v1.client.Client', + } + + # Required by the OSC plugin interface + def make_client(instance): + """Returns a client to the ClientManager + + Called to instantiate the requested client version. instance has + any available auth info that may be required to prepare the client. + + :param ClientManager instance: The ClientManager that owns the new client + """ + plugin_client = utils.get_client_class( + API_NAME, + instance._api_version[API_NAME], + API_VERSIONS) + + client = plugin_client() + return client + + # Required by the OSC plugin interface + def build_option_parser(parser): + """Hook to add global options + + Called from openstackclient.shell.OpenStackShell.__init__() + after the builtin parser has been initialized. This is + where a plugin can add global options such as an API version setting. + + :param argparse.ArgumentParser parser: The parser object that has been + initialized by OpenStackShell. + """ + parser.add_argument( + '--os-oscplugin-api-version', + metavar='', + default=utils.env( + 'OS_OSCPLUGIN_API_VERSION', + default=DEFAULT_OSCPLUGIN_API_VERSION), + help='OSC Plugin API version, default=' + + DEFAULT_OSCPLUGIN_API_VERSION + + ' (Env: OS_OSCPLUGIN_API_VERSION)') + return parser diff --git a/doc/source/releases.rst b/doc/source/releases.rst index 9551db2ee0..89b4ad111e 100644 --- a/doc/source/releases.rst +++ b/doc/source/releases.rst @@ -2,6 +2,51 @@ Release Notes ============= +0.4.1 (08 Sep 2014) +=================== + +* Bug 1319381_: remove insecure keyring support +* Bug 1317478_: fix ``project create`` for domain admin +* Bug 1317485_: fix ``project list`` for domain admins +* Bug 1281888_: add region filter to ``endpoint list`` command +* Bug 1337245_: add ``user password set`` command +* Bug 1337684_: add ``extension list --compute`` +* Bug 1337687_: add ``extension list --volume`` +* Bug 1343658_: fix ``container list`` command +* Bug 1343659_: add network command help text +* Bug 1348475_: add fields to ``image list`` output +* Bug 1351121_: v3 ``endpoint set`` should not require service option +* Bug 1352119_: v2 ``user create`` response error +* Bug 1353788_: test_file_resource() failure +* Bug 1364540_: load_keyring() exception fixed in bug 1319381_ +* Bug 1365505_: domain information not in help output +* fix ``security group list`` for non-admin +* fix ``server add security group`` +* add ``container create`` and ``container delete`` commands +* add ``object create`` and ``object delete`` commands +* add initial support for global ``--timing`` options (similar to nova CLI) +* complete Python 3 compatibility +* fix ``server resize` command +* add authentication via ``--os-trust-id`` for Identity v3 +* Add initial support for Network API, ``network create|delete|list|show`` + +.. _1319381: https://bugs.launchpad.net/bugs/1319381 +.. _1317478: https://bugs.launchpad.net/bugs/1317478 +.. _1317485: https://bugs.launchpad.net/bugs/1317485 +.. _1281888: https://bugs.launchpad.net/bugs/1281888 +.. _1337245: https://bugs.launchpad.net/bugs/1337245 +.. _1337684: https://bugs.launchpad.net/bugs/1337684 +.. _1337687: https://bugs.launchpad.net/bugs/1337687 +.. _1343658: https://bugs.launchpad.net/bugs/1343658 +.. _1343659: https://bugs.launchpad.net/bugs/1343659 +.. _1348475: https://bugs.launchpad.net/bugs/1348475 +.. _1351121: https://bugs.launchpad.net/bugs/1351121 +.. _1352119: https://bugs.launchpad.net/bugs/1352119 +.. _1353788: https://bugs.launchpad.net/bugs/1353788 +.. _1364540: https://bugs.launchpad.net/bugs/1364540 +.. _1365505: https://bugs.launchpad.net/bugs/1365505 + + 0.4.0 (20 Jun 2014) =================== From 9ecc59265613772fec9e0e3fdd9a871992ea6640 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 11 Sep 2014 17:20:35 +0000 Subject: [PATCH 0184/3494] Updated from global requirements Change-Id: I9bfbb802c5ec9be048ccecda8286b33fdfc6ef8c --- requirements.txt | 11 +++++++---- test-requirements.txt | 3 +++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index e205d51d9f..70eaf523a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,13 @@ +# The order of packages is significant, because pip processes them in the order +# of appearance. Changing the order has an impact on the overall integration +# process, which may cause wedges in the gate later. cliff>=1.6.0 -oslo.i18n>=0.2.0 # Apache-2.0 +oslo.i18n>=0.3.0 # Apache-2.0 pbr>=0.6,!=0.7,<1.0 -python-glanceclient>=0.13.1 +python-glanceclient>=0.14.0 python-keystoneclient>=0.10.0 -python-novaclient>=2.17.0 +python-novaclient>=2.18.0 python-cinderclient>=1.0.7 python-neutronclient>=2.3.6,<3 -requests>=1.2.1 +requests>=1.2.1,!=2.4.0 six>=1.7.0 diff --git a/test-requirements.txt b/test-requirements.txt index 8c911b3df3..2e22b3dfa9 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,6 @@ +# The order of packages is significant, because pip processes them in the order +# of appearance. Changing the order has an impact on the overall integration +# process, which may cause wedges in the gate later. hacking>=0.9.1,<0.10 coverage>=3.6 From c5df1826a6c331fb7ed07bc5260b5c35017b804e Mon Sep 17 00:00:00 2001 From: Andreas Jaeger Date: Sat, 13 Sep 2014 09:46:09 +0200 Subject: [PATCH 0185/3494] Stop using intersphinx Remove intersphinx from the docs build as it triggers network calls that occasionally fail, and we don't really use intersphinx (links other sphinx documents out on the internet) This also removes the requirement for internet access during docs build. This can cause docs jobs to fail if the project errors out on warnings. Change-Id: I71e941e2a639641a662a163c682eb86d51de42fb Related-Bug: #1368910 --- doc/source/conf.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 6777f13a5f..7c7a00b313 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -31,7 +31,6 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'oslosphinx'] @@ -261,7 +260,3 @@ # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' - - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'http://docs.python.org/': None} From e47787e12f4ed6f1568527bbc1218fe3edb412e0 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 5 Sep 2014 01:45:13 -0400 Subject: [PATCH 0186/3494] Add preliminary support for downloading objects Added command and library to download a single object from swift Change-Id: I3dc47b414ff37b526e6f633aa83ac3aa4b5be0ae implements: bp swift-client --- openstackclient/object/v1/lib/object.py | 26 ++++++++++++++++++ openstackclient/object/v1/object.py | 36 +++++++++++++++++++++++++ setup.cfg | 1 + 3 files changed, 63 insertions(+) diff --git a/openstackclient/object/v1/lib/object.py b/openstackclient/object/v1/lib/object.py index 5000c79d08..38b3c14ec7 100644 --- a/openstackclient/object/v1/lib/object.py +++ b/openstackclient/object/v1/lib/object.py @@ -150,6 +150,32 @@ def list_objects( return session.get(requrl, params=params).json() +def save_object( + session, + url, + container, + obj, + file=None +): + """Save an object stored in a container + + :param session: an authenticated keystoneclient.session.Session object + :param url: endpoint + :param container: name of container that stores object + :param object: name of object to save + :param file: local name of object + """ + + if not file: + file = obj + + response = session.get("%s/%s/%s" % (url, container, obj), stream=True) + if response.status_code == 200: + with open(file, 'wb') as f: + for chunk in response.iter_content(): + f.write(chunk) + + def show_object( session, url, diff --git a/openstackclient/object/v1/object.py b/openstackclient/object/v1/object.py index 812ad6e11f..d7ccf6c6e0 100644 --- a/openstackclient/object/v1/object.py +++ b/openstackclient/object/v1/object.py @@ -183,6 +183,42 @@ def take_action(self, parsed_args): ) for s in data)) +class SaveObject(command.Command): + """Save an object locally""" + + log = logging.getLogger(__name__ + ".SaveObject") + + def get_parser(self, prog_name): + parser = super(SaveObject, self).get_parser(prog_name) + parser.add_argument( + "--file", + metavar="", + help="Downloaded object filename [defaults to object name]", + ) + parser.add_argument( + 'container', + metavar='', + help='Container name that has the object', + ) + parser.add_argument( + "object", + metavar="", + help="Name of the object to save", + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + lib_object.save_object( + self.app.client_manager.session, + self.app.client_manager.object_store.endpoint, + parsed_args.container, + parsed_args.object, + parsed_args.file, + ) + + class ShowObject(show.ShowOne): """Show object information""" diff --git a/setup.cfg b/setup.cfg index 06523310bb..15f2147e87 100644 --- a/setup.cfg +++ b/setup.cfg @@ -277,6 +277,7 @@ openstack.object_store.v1 = object_create = openstackclient.object.v1.object:CreateObject object_delete = openstackclient.object.v1.object:DeleteObject object_list = openstackclient.object.v1.object:ListObject + object_save = openstackclient.object.v1.object:SaveObject object_show = openstackclient.object.v1.object:ShowObject openstack.volume.v1 = From 505c784bad14d806dd5911c953eb95fe964d188b Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 5 Sep 2014 12:56:21 -0400 Subject: [PATCH 0187/3494] Add preliminary save container support Save all objects from a container implements bp: swift-client Change-Id: I7f2437236574e212033e63d768929d813289ed05 --- openstackclient/object/v1/container.py | 24 ++++++++++++++++++++++ openstackclient/object/v1/lib/container.py | 19 +++++++++++++++++ setup.cfg | 1 + 3 files changed, 44 insertions(+) diff --git a/openstackclient/object/v1/container.py b/openstackclient/object/v1/container.py index 1ca07f3aa4..5a60a3e839 100644 --- a/openstackclient/object/v1/container.py +++ b/openstackclient/object/v1/container.py @@ -152,6 +152,30 @@ def take_action(self, parsed_args): ) for s in data)) +class SaveContainer(command.Command): + """Save the contents of a container locally""" + + log = logging.getLogger(__name__ + ".SaveContainer") + + def get_parser(self, prog_name): + parser = super(SaveContainer, self).get_parser(prog_name) + parser.add_argument( + 'container', + metavar='', + help='Container name to save', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + lib_container.save_container( + self.app.client_manager.session, + self.app.client_manager.object_store.endpoint, + parsed_args.container + ) + + class ShowContainer(show.ShowOne): """Show container information""" diff --git a/openstackclient/object/v1/lib/container.py b/openstackclient/object/v1/lib/container.py index 63711838f4..4293ff4a20 100644 --- a/openstackclient/object/v1/lib/container.py +++ b/openstackclient/object/v1/lib/container.py @@ -21,6 +21,8 @@ except ImportError: from urlparse import urlparse # noqa +from openstackclient.object.v1.lib import object as object_lib + def create_container( session, @@ -120,6 +122,23 @@ def list_containers( return session.get(url, params=params).json() +def save_container( + session, + url, + container +): + """Save all the content from a container + + :param session: an authenticated keystoneclient.session.Session object + :param url: endpoint + :param container: name of container to save + """ + + objects = object_lib.list_objects(session, url, container) + for object in objects: + object_lib.save_object(session, url, container, object['name']) + + def show_container( session, url, diff --git a/setup.cfg b/setup.cfg index 15f2147e87..8e632a677c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -273,6 +273,7 @@ openstack.object_store.v1 = container_create = openstackclient.object.v1.container:CreateContainer container_delete = openstackclient.object.v1.container:DeleteContainer container_list = openstackclient.object.v1.container:ListContainer + container_save = openstackclient.object.v1.container:SaveContainer container_show = openstackclient.object.v1.container:ShowContainer object_create = openstackclient.object.v1.object:CreateObject object_delete = openstackclient.object.v1.object:DeleteObject From 09a546891f49b17bf4a0129b0836107b165a83d3 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 9 Sep 2014 21:26:47 -0400 Subject: [PATCH 0188/3494] Add support for 'file' format objects Some objects can be saved as 'dirname/filename' which causes the existing support to fail. The correct behaviour should be to create the directories needed. Change-Id: I71c61bc3b0f76a3e6d2703bd45508f9d6483546e --- openstackclient/object/v1/lib/object.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openstackclient/object/v1/lib/object.py b/openstackclient/object/v1/lib/object.py index 38b3c14ec7..7a23fc7698 100644 --- a/openstackclient/object/v1/lib/object.py +++ b/openstackclient/object/v1/lib/object.py @@ -16,6 +16,8 @@ """Object v1 API library""" +import os + import six try: @@ -171,6 +173,8 @@ def save_object( response = session.get("%s/%s/%s" % (url, container, obj), stream=True) if response.status_code == 200: + if not os.path.exists(os.path.dirname(file)): + os.makedirs(os.path.dirname(file)) with open(file, 'wb') as f: for chunk in response.iter_content(): f.write(chunk) From 845de41635d7fefa1ae337f88a26ba11283a6552 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 12 Sep 2014 20:46:54 -0500 Subject: [PATCH 0189/3494] Return current user/project for user/project show commands If non-admin user attempts 'project show' or 'user show' on the currently authenticated project or user return the information that is already in the service catalog rather than throwing a Forbidden error. Change-Id: Ieeb6eacf71a471e410fbd3c09e7871740547e890 --- openstackclient/identity/v2_0/project.py | 28 +++++++++++++--- openstackclient/identity/v2_0/user.py | 42 +++++++++++++++++------- 2 files changed, 53 insertions(+), 17 deletions(-) diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index 7e19d5aec1..ebd65df7d6 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -22,6 +22,7 @@ from cliff import lister from cliff import show +from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc from openstackclient.common import parseractions from openstackclient.common import utils @@ -238,11 +239,28 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - project = utils.find_resource( - identity_client.tenants, - parsed_args.project, - ) info = {} - info.update(project._info) + try: + project = utils.find_resource( + identity_client.tenants, + parsed_args.project, + ) + info.update(project._info) + except ksc_exc.Forbidden as e: + auth_ref = self.app.client_manager.auth_ref + if ( + parsed_args.project == auth_ref.project_id or + parsed_args.project == auth_ref.project_name + ): + # Ask for currently auth'ed project so return it + info = { + 'id': auth_ref.project_id, + 'name': auth_ref.project_name, + # True because we don't get this far if it is disabled + 'enabled': True, + } + else: + raise e + return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index b291c88258..93ab94fe0e 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -22,6 +22,7 @@ from cliff import lister from cliff import show +from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc from openstackclient.common import utils @@ -347,20 +348,37 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - user = utils.find_resource( - identity_client.users, - parsed_args.user, - ) + info = {} + try: + user = utils.find_resource( + identity_client.users, + parsed_args.user, + ) + info.update(user._info) + except ksc_exc.Forbidden as e: + auth_ref = self.app.client_manager.auth_ref + if ( + parsed_args.user == auth_ref.user_id or + parsed_args.user == auth_ref.username + ): + # Ask for currently auth'ed project so return it + info = { + 'id': auth_ref.user_id, + 'name': auth_ref.username, + 'project_id': auth_ref.project_id, + # True because we don't get this far if it is disabled + 'enabled': True, + } + else: + raise e - if 'tenantId' in user._info: - user._info.update( - {'project_id': user._info.pop('tenantId')} + if 'tenantId' in info: + info.update( + {'project_id': info.pop('tenantId')} ) - if 'tenant_id' in user._info: - user._info.update( - {'project_id': user._info.pop('tenant_id')} + if 'tenant_id' in info: + info.update( + {'project_id': info.pop('tenant_id')} ) - info = {} - info.update(user._info) return zip(*sorted(six.iteritems(info))) From da45b34828bbc4000b1ede68f10bbcc1e4a47cc1 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Sun, 14 Sep 2014 18:16:32 -0500 Subject: [PATCH 0190/3494] Add service catalog commands 'catalog list' and 'catalog show' for Identity v2 Identity v2 only so far. Change-Id: I9df0dac3d5bb7c18f38a81bd7d29f8119462d3a5 --- openstackclient/identity/v2_0/catalog.py | 98 ++++++++++++++++ .../tests/identity/v2_0/test_catalog.py | 107 ++++++++++++++++++ setup.cfg | 3 + 3 files changed, 208 insertions(+) create mode 100644 openstackclient/identity/v2_0/catalog.py create mode 100644 openstackclient/tests/identity/v2_0/test_catalog.py diff --git a/openstackclient/identity/v2_0/catalog.py b/openstackclient/identity/v2_0/catalog.py new file mode 100644 index 0000000000..7bda1acb68 --- /dev/null +++ b/openstackclient/identity/v2_0/catalog.py @@ -0,0 +1,98 @@ +# 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. +# + +"""Identity v2 Service Catalog action implementations""" + +import logging +import six + +from cliff import lister +from cliff import show + +from openstackclient.common import utils + + +def _format_endpoints(eps=None): + if not eps: + return "" + for index, ep in enumerate(eps): + ret = eps[index]['region'] + '\n' + for url in ['publicURL', 'internalURL', 'adminURL']: + ret += " %s: %s\n" % (url, eps[index]['publicURL']) + return ret + + +class ListCatalog(lister.Lister): + """List services in the service catalog""" + + log = logging.getLogger(__name__ + '.ListCatalog') + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + # This is ugly because if auth hasn't happened yet we need + # to trigger it here. + sc = self.app.client_manager.session.auth.get_auth_ref( + self.app.client_manager.session, + ).service_catalog + + data = sc.get_data() + columns = ('Name', 'Type', 'Endpoints') + return (columns, + (utils.get_dict_properties( + s, columns, + formatters={ + 'Endpoints': _format_endpoints, + }, + ) for s in data)) + + +class ShowCatalog(show.ShowOne): + """Show service catalog details""" + + log = logging.getLogger(__name__ + '.ShowCatalog') + + def get_parser(self, prog_name): + parser = super(ShowCatalog, self).get_parser(prog_name) + parser.add_argument( + 'service', + metavar='', + help='Service to display (type, name or ID)', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + # This is ugly because if auth hasn't happened yet we need + # to trigger it here. + sc = self.app.client_manager.session.auth.get_auth_ref( + self.app.client_manager.session, + ).service_catalog + + data = None + for service in sc.get_data(): + if ( + 'name' in service and + service['name'] != parsed_args.service and + 'type' in service and + service['type'] != parsed_args.service + ): + continue + + data = service + data['endpoints'] = _format_endpoints(data['endpoints']) + if 'endpoints_links' in data: + data.pop('endpoints_links') + + return zip(*sorted(six.iteritems(data))) diff --git a/openstackclient/tests/identity/v2_0/test_catalog.py b/openstackclient/tests/identity/v2_0/test_catalog.py new file mode 100644 index 0000000000..5289cac439 --- /dev/null +++ b/openstackclient/tests/identity/v2_0/test_catalog.py @@ -0,0 +1,107 @@ +# 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 mock + +from openstackclient.identity.v2_0 import catalog +from openstackclient.tests import utils + + +class TestCatalog(utils.TestCommand): + + fake_service = { + 'id': 'qwertyuiop', + 'type': 'compute', + 'name': 'supernova', + 'endpoints': [{ + 'region': 'onlyone', + 'publicURL': 'https://public.example.com', + 'adminURL': 'https://admin.example.com', + }], + } + + def setUp(self): + super(TestCatalog, self).setUp() + + self.sc_mock = mock.MagicMock() + self.sc_mock.service_catalog.get_data.return_value = [ + self.fake_service, + ] + + self.auth_mock = mock.MagicMock() + self.app.client_manager.session = self.auth_mock + + self.auth_mock.auth.get_auth_ref.return_value = self.sc_mock + + +class TestCatalogList(TestCatalog): + + def setUp(self): + super(TestCatalogList, self).setUp() + + # Get the command object to test + self.cmd = catalog.ListCatalog(self.app, None) + + def test_catalog_list(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + self.sc_mock.service_catalog.get_data.assert_called_with() + + collist = ('Name', 'Type', 'Endpoints') + self.assertEqual(collist, columns) + datalist = (( + 'supernova', + 'compute', + 'onlyone\n publicURL: https://public.example.com\n ' + 'internalURL: https://public.example.com\n ' + 'adminURL: https://public.example.com\n', + ), ) + self.assertEqual(datalist, tuple(data)) + + +class TestCatalogShow(TestCatalog): + + def setUp(self): + super(TestCatalogShow, self).setUp() + + # Get the command object to test + self.cmd = catalog.ShowCatalog(self.app, None) + + def test_catalog_show(self): + arglist = [ + 'compute', + ] + verifylist = [ + ('service', 'compute'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + self.sc_mock.service_catalog.get_data.assert_called_with() + + collist = ('endpoints', 'id', 'name', 'type') + self.assertEqual(collist, columns) + datalist = ( + 'onlyone\n publicURL: https://public.example.com\n ' + 'internalURL: https://public.example.com\n ' + 'adminURL: https://public.example.com\n', + 'qwertyuiop', + 'supernova', + 'compute', + ) + self.assertEqual(datalist, data) diff --git a/setup.cfg b/setup.cfg index 06523310bb..daf98a3758 100644 --- a/setup.cfg +++ b/setup.cfg @@ -128,6 +128,9 @@ openstack.compute.v2 = server_unset = openstackclient.compute.v2.server:UnsetServer openstack.identity.v2_0 = + catalog_list = openstackclient.identity.v2_0.catalog:ListCatalog + catalog_show = openstackclient.identity.v2_0.catalog:ShowCatalog + ec2_credentials_create = openstackclient.identity.v2_0.ec2creds:CreateEC2Creds ec2_credentials_delete = openstackclient.identity.v2_0.ec2creds:DeleteEC2Creds ec2_credentials_list = openstackclient.identity.v2_0.ec2creds:ListEC2Creds From 92add18e311bc949ed7d7aee3386514f2017120b Mon Sep 17 00:00:00 2001 From: Anita Kuno Date: Thu, 18 Sep 2014 17:05:07 -0400 Subject: [PATCH 0191/3494] Acknowlege git.o.o as OpenStack's git server OpenStack's cannonical git server is at git.o.o this patch updates the index.rst to reflect that. Change-Id: I54eeea7fab2e0f64ca9f23f7c328e2ff19721a41 --- doc/source/index.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index 569a5aa1b2..0f92b3f018 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -21,7 +21,7 @@ Getting Started * Check out `the wiki`_ * Try `some commands`_ -* Read the source `on GitHub`_ +* Read the source `on OpenStack's Git server`_ .. _the wiki: https://wiki.openstack.org/OpenStackClient .. _some commands: https://wiki.openstack.org/OpenStackClient/Commands @@ -29,11 +29,11 @@ Getting Started Contributing ============ -Code is hosted `on GitHub`_. Submit bugs to the python-openstackclient project -on `Launchpad`_. Submit code to the openstack/python-openstackclient project -using `Gerrit`_. +Code is hosted `on OpenStack's Git server`_. Submit bugs to the +python-openstackclient project on `Launchpad`_. Submit code to +the openstack/python-openstackclient project using `Gerrit`_. -.. _on GitHub: https://github.com/openstack/python-openstackclient +.. _on OpenStack's Git server: https://git.openstack.org/cgit/openstack/python-openstackclient/tree .. _Launchpad: https://launchpad.net/python-openstackclient .. _Gerrit: http://wiki.openstack.org/GerritWorkflow From 2a2c8eec2668ca345d05779feb3ab3be3ebb656b Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 19 Sep 2014 08:51:38 +0000 Subject: [PATCH 0192/3494] Updated from global requirements Change-Id: I744a629cf685760ad96d60654d081fc495024ea8 --- requirements.txt | 4 ++-- test-requirements.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 70eaf523a8..a53e52c653 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ # The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. -cliff>=1.6.0 -oslo.i18n>=0.3.0 # Apache-2.0 +cliff>=1.7.0 # Apache-2.0 +oslo.i18n>=1.0.0 # Apache-2.0 pbr>=0.6,!=0.7,<1.0 python-glanceclient>=0.14.0 python-keystoneclient>=0.10.0 diff --git a/test-requirements.txt b/test-requirements.txt index 2e22b3dfa9..dd701c0053 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,7 +7,7 @@ coverage>=3.6 discover fixtures>=0.3.14 mock>=1.0 -oslosphinx>=2.2.0.0a2 +oslosphinx>=2.2.0 # Apache-2.0 sphinx>=1.1.2,!=1.2.0,<1.3 testrepository>=0.0.18 testtools>=0.9.34 From bfff44fc172d6e84180615fa777036bd5cba35ba Mon Sep 17 00:00:00 2001 From: Victor Silva Date: Fri, 19 Sep 2014 19:17:38 +0000 Subject: [PATCH 0193/3494] Fixing typo and improving docstring of find_domain This should make it easier to understand the purpose of find_domain - I believe the reason for which find_resource wasn't enough was not quite clear. Change-Id: I6a1cdfa86f52401d95c6da2cd38d7c95a140b4a1 --- openstackclient/identity/common.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openstackclient/identity/common.py b/openstackclient/identity/common.py index 48dc0c89b8..253729bddf 100644 --- a/openstackclient/identity/common.py +++ b/openstackclient/identity/common.py @@ -42,11 +42,11 @@ def find_service(identity_client, name_type_or_id): def find_domain(identity_client, name_or_id): """Find a domain. - If the user does not have permssions to access the v3 domain API, - assume that domain given is the id rather than the name. This - method is used by the project list command, so errors access the - domain will be ignored and if the user has access to the project - API, everything will work fine. + If the user does not have permissions to access the v3 domain API, e.g., + if the user is a project admin, assume that the domain given is the id + rather than the name. This method is used by the project list command, + so errors accessing the domain will be ignored and if the user has + access to the project API, everything will work fine. Closes bugs #1317478 and #1317485. """ From c8b3f2373387106b577fade2d00107677b7df619 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Sun, 21 Sep 2014 12:02:11 -0400 Subject: [PATCH 0194/3494] Change help text for image save command Change-Id: Ib2aecb68ffa06f9ac831131944c98c49cf99c75a Closes-Bug: #1372070 --- openstackclient/image/v1/image.py | 2 +- openstackclient/image/v2/image.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index cd746cf53e..465e9d7b15 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -333,7 +333,7 @@ def get_parser(self, prog_name): parser.add_argument( "image", metavar="", - help="Name or ID of image to delete", + help="Name or ID of image to save", ) return parser diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index ec023b6448..c12ff11a09 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -105,7 +105,7 @@ def get_parser(self, prog_name): parser.add_argument( "image", metavar="", - help="Name or ID of image to delete", + help="Name or ID of image to save", ) return parser From ffe976ce3edf2e4dc5dd50247a182ca690b4fdd9 Mon Sep 17 00:00:00 2001 From: Oleksii Chuprykov Date: Fri, 19 Sep 2014 14:24:53 +0300 Subject: [PATCH 0195/3494] Use oslo.utils Module `importutils` from common code was graduated to oslo.utils, so it would be great if we reuse this library. Remove unused strutils.py and gettextutils.py Change-Id: Iaae19fc5018d83103e5f15ff76d6da686bfdf5f8 --- openstack-common.conf | 3 - openstackclient/common/utils.py | 3 +- openstackclient/openstack/__init__.py | 0 openstackclient/openstack/common/__init__.py | 17 - .../openstack/common/gettextutils.py | 479 ------------------ .../openstack/common/importutils.py | 73 --- openstackclient/openstack/common/strutils.py | 311 ------------ requirements.txt | 1 + 8 files changed, 3 insertions(+), 884 deletions(-) delete mode 100644 openstackclient/openstack/__init__.py delete mode 100644 openstackclient/openstack/common/__init__.py delete mode 100644 openstackclient/openstack/common/gettextutils.py delete mode 100644 openstackclient/openstack/common/importutils.py delete mode 100644 openstackclient/openstack/common/strutils.py diff --git a/openstack-common.conf b/openstack-common.conf index ac923902b5..78b414c3f0 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -1,10 +1,7 @@ [DEFAULT] # The list of modules to copy from openstack-common -module=gettextutils module=install_venv_common -module=importutils -module=strutils # The base module to hold the copy of openstack.common base=openstackclient diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py index c013deee66..5c5466dfc6 100644 --- a/openstackclient/common/utils.py +++ b/openstackclient/common/utils.py @@ -21,8 +21,9 @@ import six import time +from oslo.utils import importutils + from openstackclient.common import exceptions -from openstackclient.openstack.common import importutils def find_resource(manager, name_or_id): diff --git a/openstackclient/openstack/__init__.py b/openstackclient/openstack/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openstackclient/openstack/common/__init__.py b/openstackclient/openstack/common/__init__.py deleted file mode 100644 index d1223eaf76..0000000000 --- a/openstackclient/openstack/common/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# -# 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 six - - -six.add_move(six.MovedModule('mox', 'mox', 'mox3.mox')) diff --git a/openstackclient/openstack/common/gettextutils.py b/openstackclient/openstack/common/gettextutils.py deleted file mode 100644 index 0c82634b8f..0000000000 --- a/openstackclient/openstack/common/gettextutils.py +++ /dev/null @@ -1,479 +0,0 @@ -# Copyright 2012 Red Hat, Inc. -# Copyright 2013 IBM Corp. -# All Rights Reserved. -# -# 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. - -""" -gettext for openstack-common modules. - -Usual usage in an openstack.common module: - - from openstackclient.openstack.common.gettextutils import _ -""" - -import copy -import gettext -import locale -from logging import handlers -import os - -from babel import localedata -import six - -_AVAILABLE_LANGUAGES = {} - -# FIXME(dhellmann): Remove this when moving to oslo.i18n. -USE_LAZY = False - - -class TranslatorFactory(object): - """Create translator functions - """ - - def __init__(self, domain, localedir=None): - """Establish a set of translation functions for the domain. - - :param domain: Name of translation domain, - specifying a message catalog. - :type domain: str - :param lazy: Delays translation until a message is emitted. - Defaults to False. - :type lazy: Boolean - :param localedir: Directory with translation catalogs. - :type localedir: str - """ - self.domain = domain - if localedir is None: - localedir = os.environ.get(domain.upper() + '_LOCALEDIR') - self.localedir = localedir - - def _make_translation_func(self, domain=None): - """Return a new translation function ready for use. - - Takes into account whether or not lazy translation is being - done. - - The domain can be specified to override the default from the - factory, but the localedir from the factory is always used - because we assume the log-level translation catalogs are - installed in the same directory as the main application - catalog. - - """ - if domain is None: - domain = self.domain - t = gettext.translation(domain, - localedir=self.localedir, - fallback=True) - # Use the appropriate method of the translation object based - # on the python version. - m = t.gettext if six.PY3 else t.ugettext - - def f(msg): - """oslo.i18n.gettextutils translation function.""" - if USE_LAZY: - return Message(msg, domain=domain) - return m(msg) - return f - - @property - def primary(self): - "The default translation function." - return self._make_translation_func() - - def _make_log_translation_func(self, level): - return self._make_translation_func(self.domain + '-log-' + level) - - @property - def log_info(self): - "Translate info-level log messages." - return self._make_log_translation_func('info') - - @property - def log_warning(self): - "Translate warning-level log messages." - return self._make_log_translation_func('warning') - - @property - def log_error(self): - "Translate error-level log messages." - return self._make_log_translation_func('error') - - @property - def log_critical(self): - "Translate critical-level log messages." - return self._make_log_translation_func('critical') - - -# NOTE(dhellmann): When this module moves out of the incubator into -# oslo.i18n, these global variables can be moved to an integration -# module within each application. - -# Create the global translation functions. -_translators = TranslatorFactory('openstackclient') - -# The primary translation function using the well-known name "_" -_ = _translators.primary - -# Translators for log levels. -# -# The abbreviated names are meant to reflect the usual use of a short -# name like '_'. The "L" is for "log" and the other letter comes from -# the level. -_LI = _translators.log_info -_LW = _translators.log_warning -_LE = _translators.log_error -_LC = _translators.log_critical - -# NOTE(dhellmann): End of globals that will move to the application's -# integration module. - - -def enable_lazy(): - """Convenience function for configuring _() to use lazy gettext - - Call this at the start of execution to enable the gettextutils._ - function to use lazy gettext functionality. This is useful if - your project is importing _ directly instead of using the - gettextutils.install() way of importing the _ function. - """ - global USE_LAZY - USE_LAZY = True - - -def install(domain): - """Install a _() function using the given translation domain. - - Given a translation domain, install a _() function using gettext's - install() function. - - The main difference from gettext.install() is that we allow - overriding the default localedir (e.g. /usr/share/locale) using - a translation-domain-specific environment variable (e.g. - NOVA_LOCALEDIR). - - Note that to enable lazy translation, enable_lazy must be - called. - - :param domain: the translation domain - """ - from six import moves - tf = TranslatorFactory(domain) - moves.builtins.__dict__['_'] = tf.primary - - -class Message(six.text_type): - """A Message object is a unicode object that can be translated. - - Translation of Message is done explicitly using the translate() method. - For all non-translation intents and purposes, a Message is simply unicode, - and can be treated as such. - """ - - def __new__(cls, msgid, msgtext=None, params=None, - domain='openstackclient', *args): - """Create a new Message object. - - In order for translation to work gettext requires a message ID, this - msgid will be used as the base unicode text. It is also possible - for the msgid and the base unicode text to be different by passing - the msgtext parameter. - """ - # If the base msgtext is not given, we use the default translation - # of the msgid (which is in English) just in case the system locale is - # not English, so that the base text will be in that locale by default. - if not msgtext: - msgtext = Message._translate_msgid(msgid, domain) - # We want to initialize the parent unicode with the actual object that - # would have been plain unicode if 'Message' was not enabled. - msg = super(Message, cls).__new__(cls, msgtext) - msg.msgid = msgid - msg.domain = domain - msg.params = params - return msg - - def translate(self, desired_locale=None): - """Translate this message to the desired locale. - - :param desired_locale: The desired locale to translate the message to, - if no locale is provided the message will be - translated to the system's default locale. - - :returns: the translated message in unicode - """ - - translated_message = Message._translate_msgid(self.msgid, - self.domain, - desired_locale) - if self.params is None: - # No need for more translation - return translated_message - - # This Message object may have been formatted with one or more - # Message objects as substitution arguments, given either as a single - # argument, part of a tuple, or as one or more values in a dictionary. - # When translating this Message we need to translate those Messages too - translated_params = _translate_args(self.params, desired_locale) - - translated_message = translated_message % translated_params - - return translated_message - - @staticmethod - def _translate_msgid(msgid, domain, desired_locale=None): - if not desired_locale: - system_locale = locale.getdefaultlocale() - # If the system locale is not available to the runtime use English - if not system_locale[0]: - desired_locale = 'en_US' - else: - desired_locale = system_locale[0] - - locale_dir = os.environ.get(domain.upper() + '_LOCALEDIR') - lang = gettext.translation(domain, - localedir=locale_dir, - languages=[desired_locale], - fallback=True) - if six.PY3: - translator = lang.gettext - else: - translator = lang.ugettext - - translated_message = translator(msgid) - return translated_message - - def __mod__(self, other): - # When we mod a Message we want the actual operation to be performed - # by the parent class (i.e. unicode()), the only thing we do here is - # save the original msgid and the parameters in case of a translation - params = self._sanitize_mod_params(other) - unicode_mod = super(Message, self).__mod__(params) - modded = Message(self.msgid, - msgtext=unicode_mod, - params=params, - domain=self.domain) - return modded - - def _sanitize_mod_params(self, other): - """Sanitize the object being modded with this Message. - - - Add support for modding 'None' so translation supports it - - Trim the modded object, which can be a large dictionary, to only - those keys that would actually be used in a translation - - Snapshot the object being modded, in case the message is - translated, it will be used as it was when the Message was created - """ - if other is None: - params = (other,) - elif isinstance(other, dict): - # Merge the dictionaries - # Copy each item in case one does not support deep copy. - params = {} - if isinstance(self.params, dict): - for key, val in self.params.items(): - params[key] = self._copy_param(val) - for key, val in other.items(): - params[key] = self._copy_param(val) - else: - params = self._copy_param(other) - return params - - def _copy_param(self, param): - try: - return copy.deepcopy(param) - except Exception: - # Fallback to casting to unicode this will handle the - # python code-like objects that can't be deep-copied - return six.text_type(param) - - def __add__(self, other): - msg = _('Message objects do not support addition.') - raise TypeError(msg) - - def __radd__(self, other): - return self.__add__(other) - - if six.PY2: - def __str__(self): - # NOTE(luisg): Logging in python 2.6 tries to str() log records, - # and it expects specifically a UnicodeError in order to proceed. - msg = _('Message objects do not support str() because they may ' - 'contain non-ascii characters. ' - 'Please use unicode() or translate() instead.') - raise UnicodeError(msg) - - -def get_available_languages(domain): - """Lists the available languages for the given translation domain. - - :param domain: the domain to get languages for - """ - if domain in _AVAILABLE_LANGUAGES: - return copy.copy(_AVAILABLE_LANGUAGES[domain]) - - localedir = '%s_LOCALEDIR' % domain.upper() - find = lambda x: gettext.find(domain, - localedir=os.environ.get(localedir), - languages=[x]) - - # NOTE(mrodden): en_US should always be available (and first in case - # order matters) since our in-line message strings are en_US - language_list = ['en_US'] - # NOTE(luisg): Babel <1.0 used a function called list(), which was - # renamed to locale_identifiers() in >=1.0, the requirements master list - # requires >=0.9.6, uncapped, so defensively work with both. We can remove - # this check when the master list updates to >=1.0, and update all projects - list_identifiers = (getattr(localedata, 'list', None) or - getattr(localedata, 'locale_identifiers')) - locale_identifiers = list_identifiers() - - for i in locale_identifiers: - if find(i) is not None: - language_list.append(i) - - # NOTE(luisg): Babel>=1.0,<1.3 has a bug where some OpenStack supported - # locales (e.g. 'zh_CN', and 'zh_TW') aren't supported even though they - # are perfectly legitimate locales: - # https://github.com/mitsuhiko/babel/issues/37 - # In Babel 1.3 they fixed the bug and they support these locales, but - # they are still not explicitly "listed" by locale_identifiers(). - # That is why we add the locales here explicitly if necessary so that - # they are listed as supported. - aliases = {'zh': 'zh_CN', - 'zh_Hant_HK': 'zh_HK', - 'zh_Hant': 'zh_TW', - 'fil': 'tl_PH'} - for (locale_, alias) in six.iteritems(aliases): - if locale_ in language_list and alias not in language_list: - language_list.append(alias) - - _AVAILABLE_LANGUAGES[domain] = language_list - return copy.copy(language_list) - - -def translate(obj, desired_locale=None): - """Gets the translated unicode representation of the given object. - - If the object is not translatable it is returned as-is. - If the locale is None the object is translated to the system locale. - - :param obj: the object to translate - :param desired_locale: the locale to translate the message to, if None the - default system locale will be used - :returns: the translated object in unicode, or the original object if - it could not be translated - """ - message = obj - if not isinstance(message, Message): - # If the object to translate is not already translatable, - # let's first get its unicode representation - message = six.text_type(obj) - if isinstance(message, Message): - # Even after unicoding() we still need to check if we are - # running with translatable unicode before translating - return message.translate(desired_locale) - return obj - - -def _translate_args(args, desired_locale=None): - """Translates all the translatable elements of the given arguments object. - - This method is used for translating the translatable values in method - arguments which include values of tuples or dictionaries. - If the object is not a tuple or a dictionary the object itself is - translated if it is translatable. - - If the locale is None the object is translated to the system locale. - - :param args: the args to translate - :param desired_locale: the locale to translate the args to, if None the - default system locale will be used - :returns: a new args object with the translated contents of the original - """ - if isinstance(args, tuple): - return tuple(translate(v, desired_locale) for v in args) - if isinstance(args, dict): - translated_dict = {} - for (k, v) in six.iteritems(args): - translated_v = translate(v, desired_locale) - translated_dict[k] = translated_v - return translated_dict - return translate(args, desired_locale) - - -class TranslationHandler(handlers.MemoryHandler): - """Handler that translates records before logging them. - - The TranslationHandler takes a locale and a target logging.Handler object - to forward LogRecord objects to after translating them. This handler - depends on Message objects being logged, instead of regular strings. - - The handler can be configured declaratively in the logging.conf as follows: - - [handlers] - keys = translatedlog, translator - - [handler_translatedlog] - class = handlers.WatchedFileHandler - args = ('/var/log/api-localized.log',) - formatter = context - - [handler_translator] - class = openstack.common.log.TranslationHandler - target = translatedlog - args = ('zh_CN',) - - If the specified locale is not available in the system, the handler will - log in the default locale. - """ - - def __init__(self, locale=None, target=None): - """Initialize a TranslationHandler - - :param locale: locale to use for translating messages - :param target: logging.Handler object to forward - LogRecord objects to after translation - """ - # NOTE(luisg): In order to allow this handler to be a wrapper for - # other handlers, such as a FileHandler, and still be able to - # configure it using logging.conf, this handler has to extend - # MemoryHandler because only the MemoryHandlers' logging.conf - # parsing is implemented such that it accepts a target handler. - handlers.MemoryHandler.__init__(self, capacity=0, target=target) - self.locale = locale - - def setFormatter(self, fmt): - self.target.setFormatter(fmt) - - def emit(self, record): - # We save the message from the original record to restore it - # after translation, so other handlers are not affected by this - original_msg = record.msg - original_args = record.args - - try: - self._translate_and_log_record(record) - finally: - record.msg = original_msg - record.args = original_args - - def _translate_and_log_record(self, record): - record.msg = translate(record.msg, self.locale) - - # In addition to translating the message, we also need to translate - # arguments that were passed to the log method that were not part - # of the main message e.g., log.info(_('Some message %s'), this_one)) - record.args = _translate_args(record.args, self.locale) - - self.target.emit(record) diff --git a/openstackclient/openstack/common/importutils.py b/openstackclient/openstack/common/importutils.py deleted file mode 100644 index 69e8d8f121..0000000000 --- a/openstackclient/openstack/common/importutils.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright 2011 OpenStack Foundation. -# All Rights Reserved. -# -# 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 related utilities and helper functions. -""" - -import sys -import traceback - - -def import_class(import_str): - """Returns a class from a string including module and class.""" - mod_str, _sep, class_str = import_str.rpartition('.') - __import__(mod_str) - try: - return getattr(sys.modules[mod_str], class_str) - except AttributeError: - raise ImportError('Class %s cannot be found (%s)' % - (class_str, - traceback.format_exception(*sys.exc_info()))) - - -def import_object(import_str, *args, **kwargs): - """Import a class and return an instance of it.""" - return import_class(import_str)(*args, **kwargs) - - -def import_object_ns(name_space, import_str, *args, **kwargs): - """Tries to import object from default namespace. - - Imports a class and return an instance of it, first by trying - to find the class in a default namespace, then failing back to - a full path if not found in the default namespace. - """ - import_value = "%s.%s" % (name_space, import_str) - try: - return import_class(import_value)(*args, **kwargs) - except ImportError: - return import_class(import_str)(*args, **kwargs) - - -def import_module(import_str): - """Import a module.""" - __import__(import_str) - return sys.modules[import_str] - - -def import_versioned_module(version, submodule=None): - module = 'openstackclient.v%s' % version - if submodule: - module = '.'.join((module, submodule)) - return import_module(module) - - -def try_import(import_str, default=None): - """Try to import a module and if it fails return default.""" - try: - return import_module(import_str) - except ImportError: - return default diff --git a/openstackclient/openstack/common/strutils.py b/openstackclient/openstack/common/strutils.py deleted file mode 100644 index ad3cb44c25..0000000000 --- a/openstackclient/openstack/common/strutils.py +++ /dev/null @@ -1,311 +0,0 @@ -# Copyright 2011 OpenStack Foundation. -# All Rights Reserved. -# -# 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. - -""" -System-level utilities and helper functions. -""" - -import math -import re -import sys -import unicodedata - -import six - -from openstackclient.openstack.common.gettextutils import _ - - -UNIT_PREFIX_EXPONENT = { - 'k': 1, - 'K': 1, - 'Ki': 1, - 'M': 2, - 'Mi': 2, - 'G': 3, - 'Gi': 3, - 'T': 4, - 'Ti': 4, -} -UNIT_SYSTEM_INFO = { - 'IEC': (1024, re.compile(r'(^[-+]?\d*\.?\d+)([KMGT]i?)?(b|bit|B)$')), - 'SI': (1000, re.compile(r'(^[-+]?\d*\.?\d+)([kMGT])?(b|bit|B)$')), -} - -TRUE_STRINGS = ('1', 't', 'true', 'on', 'y', 'yes') -FALSE_STRINGS = ('0', 'f', 'false', 'off', 'n', 'no') - -SLUGIFY_STRIP_RE = re.compile(r"[^\w\s-]") -SLUGIFY_HYPHENATE_RE = re.compile(r"[-\s]+") - - -# NOTE(flaper87): The following globals are used by `mask_password` -_SANITIZE_KEYS = ['adminPass', 'admin_pass', 'password', 'admin_password'] - -# NOTE(ldbragst): Let's build a list of regex objects using the list of -# _SANITIZE_KEYS we already have. This way, we only have to add the new key -# to the list of _SANITIZE_KEYS and we can generate regular expressions -# for XML and JSON automatically. -_SANITIZE_PATTERNS_2 = [] -_SANITIZE_PATTERNS_1 = [] - -# NOTE(amrith): Some regular expressions have only one parameter, some -# have two parameters. Use different lists of patterns here. -_FORMAT_PATTERNS_1 = [r'(%(key)s\s*[=]\s*)[^\s^\'^\"]+'] -_FORMAT_PATTERNS_2 = [r'(%(key)s\s*[=]\s*[\"\']).*?([\"\'])', - r'(%(key)s\s+[\"\']).*?([\"\'])', - r'([-]{2}%(key)s\s+)[^\'^\"^=^\s]+([\s]*)', - r'(<%(key)s>).*?()', - r'([\"\']%(key)s[\"\']\s*:\s*[\"\']).*?([\"\'])', - r'([\'"].*?%(key)s[\'"]\s*:\s*u?[\'"]).*?([\'"])', - r'([\'"].*?%(key)s[\'"]\s*,\s*\'--?[A-z]+\'\s*,\s*u?' - '[\'"]).*?([\'"])', - r'(%(key)s\s*--?[A-z]+\s*)\S+(\s*)'] - -for key in _SANITIZE_KEYS: - for pattern in _FORMAT_PATTERNS_2: - reg_ex = re.compile(pattern % {'key': key}, re.DOTALL) - _SANITIZE_PATTERNS_2.append(reg_ex) - - for pattern in _FORMAT_PATTERNS_1: - reg_ex = re.compile(pattern % {'key': key}, re.DOTALL) - _SANITIZE_PATTERNS_1.append(reg_ex) - - -def int_from_bool_as_string(subject): - """Interpret a string as a boolean and return either 1 or 0. - - Any string value in: - - ('True', 'true', 'On', 'on', '1') - - is interpreted as a boolean True. - - Useful for JSON-decoded stuff and config file parsing - """ - return bool_from_string(subject) and 1 or 0 - - -def bool_from_string(subject, strict=False, default=False): - """Interpret a string as a boolean. - - A case-insensitive match is performed such that strings matching 't', - 'true', 'on', 'y', 'yes', or '1' are considered True and, when - `strict=False`, anything else returns the value specified by 'default'. - - Useful for JSON-decoded stuff and config file parsing. - - If `strict=True`, unrecognized values, including None, will raise a - ValueError which is useful when parsing values passed in from an API call. - Strings yielding False are 'f', 'false', 'off', 'n', 'no', or '0'. - """ - if not isinstance(subject, six.string_types): - subject = six.text_type(subject) - - lowered = subject.strip().lower() - - if lowered in TRUE_STRINGS: - return True - elif lowered in FALSE_STRINGS: - return False - elif strict: - acceptable = ', '.join( - "'%s'" % s for s in sorted(TRUE_STRINGS + FALSE_STRINGS)) - msg = _("Unrecognized value '%(val)s', acceptable values are:" - " %(acceptable)s") % {'val': subject, - 'acceptable': acceptable} - raise ValueError(msg) - else: - return default - - -def safe_decode(text, incoming=None, errors='strict'): - """Decodes incoming text/bytes string using `incoming` if they're not - already unicode. - - :param incoming: Text's current encoding - :param errors: Errors handling policy. See here for valid - values http://docs.python.org/2/library/codecs.html - :returns: text or a unicode `incoming` encoded - representation of it. - :raises TypeError: If text is not an instance of str - """ - if not isinstance(text, (six.string_types, six.binary_type)): - raise TypeError("%s can't be decoded" % type(text)) - - if isinstance(text, six.text_type): - return text - - if not incoming: - incoming = (sys.stdin.encoding or - sys.getdefaultencoding()) - - try: - return text.decode(incoming, errors) - except UnicodeDecodeError: - # Note(flaper87) If we get here, it means that - # sys.stdin.encoding / sys.getdefaultencoding - # didn't return a suitable encoding to decode - # text. This happens mostly when global LANG - # var is not set correctly and there's no - # default encoding. In this case, most likely - # python will use ASCII or ANSI encoders as - # default encodings but they won't be capable - # of decoding non-ASCII characters. - # - # Also, UTF-8 is being used since it's an ASCII - # extension. - return text.decode('utf-8', errors) - - -def safe_encode(text, incoming=None, - encoding='utf-8', errors='strict'): - """Encodes incoming text/bytes string using `encoding`. - - If incoming is not specified, text is expected to be encoded with - current python's default encoding. (`sys.getdefaultencoding`) - - :param incoming: Text's current encoding - :param encoding: Expected encoding for text (Default UTF-8) - :param errors: Errors handling policy. See here for valid - values http://docs.python.org/2/library/codecs.html - :returns: text or a bytestring `encoding` encoded - representation of it. - :raises TypeError: If text is not an instance of str - """ - if not isinstance(text, (six.string_types, six.binary_type)): - raise TypeError("%s can't be encoded" % type(text)) - - if not incoming: - incoming = (sys.stdin.encoding or - sys.getdefaultencoding()) - - if isinstance(text, six.text_type): - return text.encode(encoding, errors) - elif text and encoding != incoming: - # Decode text before encoding it with `encoding` - text = safe_decode(text, incoming, errors) - return text.encode(encoding, errors) - else: - return text - - -def string_to_bytes(text, unit_system='IEC', return_int=False): - """Converts a string into an float representation of bytes. - - The units supported for IEC :: - - Kb(it), Kib(it), Mb(it), Mib(it), Gb(it), Gib(it), Tb(it), Tib(it) - KB, KiB, MB, MiB, GB, GiB, TB, TiB - - The units supported for SI :: - - kb(it), Mb(it), Gb(it), Tb(it) - kB, MB, GB, TB - - Note that the SI unit system does not support capital letter 'K' - - :param text: String input for bytes size conversion. - :param unit_system: Unit system for byte size conversion. - :param return_int: If True, returns integer representation of text - in bytes. (default: decimal) - :returns: Numerical representation of text in bytes. - :raises ValueError: If text has an invalid value. - - """ - try: - base, reg_ex = UNIT_SYSTEM_INFO[unit_system] - except KeyError: - msg = _('Invalid unit system: "%s"') % unit_system - raise ValueError(msg) - match = reg_ex.match(text) - if match: - magnitude = float(match.group(1)) - unit_prefix = match.group(2) - if match.group(3) in ['b', 'bit']: - magnitude /= 8 - else: - msg = _('Invalid string format: %s') % text - raise ValueError(msg) - if not unit_prefix: - res = magnitude - else: - res = magnitude * pow(base, UNIT_PREFIX_EXPONENT[unit_prefix]) - if return_int: - return int(math.ceil(res)) - return res - - -def to_slug(value, incoming=None, errors="strict"): - """Normalize string. - - Convert to lowercase, remove non-word characters, and convert spaces - to hyphens. - - Inspired by Django's `slugify` filter. - - :param value: Text to slugify - :param incoming: Text's current encoding - :param errors: Errors handling policy. See here for valid - values http://docs.python.org/2/library/codecs.html - :returns: slugified unicode representation of `value` - :raises TypeError: If text is not an instance of str - """ - value = safe_decode(value, incoming, errors) - # NOTE(aababilov): no need to use safe_(encode|decode) here: - # encodings are always "ascii", error handling is always "ignore" - # and types are always known (first: unicode; second: str) - value = unicodedata.normalize("NFKD", value).encode( - "ascii", "ignore").decode("ascii") - value = SLUGIFY_STRIP_RE.sub("", value).strip().lower() - return SLUGIFY_HYPHENATE_RE.sub("-", value) - - -def mask_password(message, secret="***"): - """Replace password with 'secret' in message. - - :param message: The string which includes security information. - :param secret: value with which to replace passwords. - :returns: The unicode value of message with the password fields masked. - - For example: - - >>> mask_password("'adminPass' : 'aaaaa'") - "'adminPass' : '***'" - >>> mask_password("'admin_pass' : 'aaaaa'") - "'admin_pass' : '***'" - >>> mask_password('"password" : "aaaaa"') - '"password" : "***"' - >>> mask_password("'original_password' : 'aaaaa'") - "'original_password' : '***'" - >>> mask_password("u'original_password' : u'aaaaa'") - "u'original_password' : u'***'" - """ - message = six.text_type(message) - - # NOTE(ldbragst): Check to see if anything in message contains any key - # specified in _SANITIZE_KEYS, if not then just return the message since - # we don't have to mask any passwords. - if not any(key in message for key in _SANITIZE_KEYS): - return message - - substitute = r'\g<1>' + secret + r'\g<2>' - for pattern in _SANITIZE_PATTERNS_2: - message = re.sub(pattern, substitute, message) - - substitute = r'\g<1>' + secret - for pattern in _SANITIZE_PATTERNS_1: - message = re.sub(pattern, substitute, message) - - return message diff --git a/requirements.txt b/requirements.txt index a53e52c653..4745c11252 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ # process, which may cause wedges in the gate later. cliff>=1.7.0 # Apache-2.0 oslo.i18n>=1.0.0 # Apache-2.0 +oslo.utils>=1.0.0 # Apache-2.0 pbr>=0.6,!=0.7,<1.0 python-glanceclient>=0.14.0 python-keystoneclient>=0.10.0 From 2d1225624c5115491288d53498906226ed53880f Mon Sep 17 00:00:00 2001 From: wanghong Date: Tue, 23 Sep 2014 14:52:44 +0800 Subject: [PATCH 0196/3494] v3 credential set always needs --user option Change-Id: Ieca76bb6ee2f328f4e33010623c25eb9c18e6952 Closes-Bug: #1372744 --- openstackclient/identity/v3/credential.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openstackclient/identity/v3/credential.py b/openstackclient/identity/v3/credential.py index 43d16c2962..f1e17b8502 100644 --- a/openstackclient/identity/v3/credential.py +++ b/openstackclient/identity/v3/credential.py @@ -151,11 +151,12 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - user_id = utils.find_resource(identity_client.users, - parsed_args.user).id kwargs = {} - if user_id: - kwargs['user'] = user_id + if parsed_args.user: + user_id = utils.find_resource(identity_client.users, + parsed_args.user).id + if user_id: + kwargs['user'] = user_id if parsed_args.type: kwargs['type'] = parsed_args.type if parsed_args.data: From 1212ddb431c8ecdebbc89dc54a5854ac7794ace5 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 23 Sep 2014 16:43:31 -0400 Subject: [PATCH 0197/3494] Remove unused reference to keyring There's a unnecessary reference that is not being used. Change-Id: I5ac85d2331385e4a31970b63fd17e650f82046ca --- openstackclient/shell.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 24804343db..0c91ab6ef1 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -35,8 +35,6 @@ from openstackclient.common import utils -KEYRING_SERVICE = 'openstack' - DEFAULT_DOMAIN = 'default' From 7029cf37e268a789a65bab3b9a16e4491854106e Mon Sep 17 00:00:00 2001 From: wanghong Date: Wed, 24 Sep 2014 11:04:41 +0800 Subject: [PATCH 0198/3494] utils.find_resource does not catch right exception Currently, utils.find_resource catch NotFound exception defined in openstackclient. However, different client libraries raise different exceptions defined in thire own library. Change-Id: Idc40428e30e59f71dbdbfa0555c0066fddc441c2 Closes-Bug: #1371924 --- openstackclient/common/utils.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py index 5c5466dfc6..818f8d4771 100644 --- a/openstackclient/common/utils.py +++ b/openstackclient/common/utils.py @@ -33,8 +33,15 @@ def find_resource(manager, name_or_id): try: if isinstance(name_or_id, int) or name_or_id.isdigit(): return manager.get(int(name_or_id)) - except exceptions.NotFound: - pass + # FIXME(dtroyer): The exception to catch here is dependent on which + # client library the manager passed in belongs to. + # Eventually this should be pulled from a common set + # of client exceptions. + except Exception as ex: + if type(ex).__name__ == 'NotFound': + pass + else: + raise # Try directly using the passed value try: From 3ddd4e2646c4db4172c3d74a0f51eb6d3e91b176 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Thu, 25 Sep 2014 19:08:05 +0000 Subject: [PATCH 0199/3494] Updated from global requirements Change-Id: I2a8250d0b01651563cfe74704ce5a9f97dd9fdf4 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4745c11252..04f448834f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ pbr>=0.6,!=0.7,<1.0 python-glanceclient>=0.14.0 python-keystoneclient>=0.10.0 python-novaclient>=2.18.0 -python-cinderclient>=1.0.7 +python-cinderclient>=1.1.0 python-neutronclient>=2.3.6,<3 requests>=1.2.1,!=2.4.0 six>=1.7.0 From 207c8cf3ef9237d21cde704eff767523b5f12f35 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 18 Sep 2014 22:10:03 -0500 Subject: [PATCH 0200/3494] Test top-to-bottom: object-store containers Replicate the object-store container command tests but use requests_mock to test the entire stack down to the requests module. These will be useful regressions tests when the existing object-store lib modules are moved to the low-level API object. Change-Id: Ibf25be46156eb1009f1b66f02f2073d3913b846d --- openstackclient/tests/fakes.py | 8 +- openstackclient/tests/object/v1/fakes.py | 11 +- .../tests/object/v1/test_container_all.py | 341 ++++++++++++++++++ test-requirements.txt | 1 + 4 files changed, 353 insertions(+), 8 deletions(-) create mode 100644 openstackclient/tests/object/v1/test_container_all.py diff --git a/openstackclient/tests/fakes.py b/openstackclient/tests/fakes.py index 263640ee21..5a1fc005e4 100644 --- a/openstackclient/tests/fakes.py +++ b/openstackclient/tests/fakes.py @@ -47,12 +47,18 @@ def __init__(self, _stdout): self.stderr = sys.stderr +class FakeClient(object): + def __init__(self, **kwargs): + self.endpoint = kwargs['endpoint'] + self.token = kwargs['token'] + + class FakeClientManager(object): def __init__(self): self.compute = None self.identity = None self.image = None - self.object = None + self.object_store = None self.volume = None self.network = None self.session = None diff --git a/openstackclient/tests/object/v1/fakes.py b/openstackclient/tests/object/v1/fakes.py index 87f6cab694..f10437b63f 100644 --- a/openstackclient/tests/object/v1/fakes.py +++ b/openstackclient/tests/object/v1/fakes.py @@ -17,6 +17,9 @@ from openstackclient.tests import utils +ACCOUNT_ID = 'tqbfjotld' +ENDPOINT = 'https://0.0.0.0:6482/v1/' + ACCOUNT_ID + container_name = 'bit-bucket' container_bytes = 1024 container_count = 1 @@ -71,17 +74,11 @@ } -class FakeObjectv1Client(object): - def __init__(self, **kwargs): - self.endpoint = kwargs['endpoint'] - self.token = kwargs['token'] - - class TestObjectv1(utils.TestCommand): def setUp(self): super(TestObjectv1, self).setUp() - self.app.client_manager.object_store = FakeObjectv1Client( + self.app.client_manager.object_store = fakes.FakeClient( endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN, ) diff --git a/openstackclient/tests/object/v1/test_container_all.py b/openstackclient/tests/object/v1/test_container_all.py new file mode 100644 index 0000000000..29d78454fa --- /dev/null +++ b/openstackclient/tests/object/v1/test_container_all.py @@ -0,0 +1,341 @@ +# 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 copy + +from requests_mock.contrib import fixture + +from keystoneclient import session +from openstackclient.object.v1 import container +from openstackclient.tests.object.v1 import fakes as object_fakes + + +class TestObjectAll(object_fakes.TestObjectv1): + + def setUp(self): + super(TestObjectAll, self).setUp() + + self.app.client_manager.session = session.Session() + self.requests_mock = self.useFixture(fixture.Fixture()) + + # TODO(dtroyer): move this to object_fakes.TestObjectv1 + self.app.client_manager.object_store.endpoint = object_fakes.ENDPOINT + + +class TestContainerCreate(TestObjectAll): + + def setUp(self): + super(TestContainerCreate, self).setUp() + + # Get the command object to test + self.cmd = container.CreateContainer(self.app, None) + + def test_object_create_container_single(self): + self.requests_mock.register_uri( + 'PUT', + object_fakes.ENDPOINT + '/ernie', + headers={'x-trans-id': '314159'}, + status_code=200, + ) + + arglist = [ + 'ernie', + ] + verifylist = [( + 'containers', ['ernie'], + )] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + collist = ('account', 'container', 'x-trans-id') + self.assertEqual(collist, columns) + datalist = [( + object_fakes.ACCOUNT_ID, + 'ernie', + '314159', + )] + self.assertEqual(datalist, list(data)) + + def test_object_create_container_more(self): + self.requests_mock.register_uri( + 'PUT', + object_fakes.ENDPOINT + '/ernie', + headers={'x-trans-id': '314159'}, + status_code=200, + ) + self.requests_mock.register_uri( + 'PUT', + object_fakes.ENDPOINT + '/bert', + headers={'x-trans-id': '42'}, + status_code=200, + ) + + arglist = [ + 'ernie', + 'bert', + ] + verifylist = [( + 'containers', ['ernie', 'bert'], + )] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + collist = ('account', 'container', 'x-trans-id') + self.assertEqual(collist, columns) + datalist = [ + ( + object_fakes.ACCOUNT_ID, + 'ernie', + '314159', + ), + ( + object_fakes.ACCOUNT_ID, + 'bert', + '42', + ), + ] + self.assertEqual(datalist, list(data)) + + +class TestContainerDelete(TestObjectAll): + + def setUp(self): + super(TestContainerDelete, self).setUp() + + # Get the command object to test + self.cmd = container.DeleteContainer(self.app, None) + + def test_object_delete_container_single(self): + self.requests_mock.register_uri( + 'DELETE', + object_fakes.ENDPOINT + '/ernie', + status_code=200, + ) + + arglist = [ + 'ernie', + ] + verifylist = [( + 'containers', ['ernie'], + )] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Command.take_action() returns None + ret = self.cmd.take_action(parsed_args) + self.assertIsNone(ret) + + def test_object_delete_container_more(self): + self.requests_mock.register_uri( + 'DELETE', + object_fakes.ENDPOINT + '/ernie', + status_code=200, + ) + self.requests_mock.register_uri( + 'DELETE', + object_fakes.ENDPOINT + '/bert', + status_code=200, + ) + + arglist = [ + 'ernie', + 'bert', + ] + verifylist = [( + 'containers', ['ernie', 'bert'], + )] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Command.take_action() returns None + ret = self.cmd.take_action(parsed_args) + self.assertIsNone(ret) + + +class TestContainerList(TestObjectAll): + + def setUp(self): + super(TestContainerList, self).setUp() + + # Get the command object to test + self.cmd = container.ListContainer(self.app, None) + + def test_object_list_containers_no_options(self): + return_body = [ + copy.deepcopy(object_fakes.CONTAINER), + copy.deepcopy(object_fakes.CONTAINER_3), + copy.deepcopy(object_fakes.CONTAINER_2), + ] + self.requests_mock.register_uri( + 'GET', + object_fakes.ENDPOINT + '?format=json', + json=return_body, + status_code=200, + ) + + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Lister.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + collist = ('Name',) + self.assertEqual(collist, columns) + datalist = [ + (object_fakes.container_name, ), + (object_fakes.container_name_3, ), + (object_fakes.container_name_2, ), + ] + self.assertEqual(datalist, list(data)) + + def test_object_list_containers_prefix(self): + return_body = [ + copy.deepcopy(object_fakes.CONTAINER), + copy.deepcopy(object_fakes.CONTAINER_3), + ] + self.requests_mock.register_uri( + 'GET', + object_fakes.ENDPOINT + '?format=json&prefix=bit', + json=return_body, + status_code=200, + ) + + arglist = [ + '--prefix', 'bit', + ] + verifylist = [ + ('prefix', 'bit'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Lister.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + collist = ('Name',) + self.assertEqual(collist, columns) + datalist = [ + (object_fakes.container_name, ), + (object_fakes.container_name_3, ), + ] + self.assertEqual(datalist, list(data)) + + +class TestContainerSave(TestObjectAll): + + def setUp(self): + super(TestContainerSave, self).setUp() + + # Get the command object to test + self.cmd = container.SaveContainer(self.app, None) + +# TODO(dtroyer): need to mock out object_lib.save_object() to test this +# def test_object_save_container(self): +# return_body = [ +# copy.deepcopy(object_fakes.OBJECT), +# copy.deepcopy(object_fakes.OBJECT_2), +# ] +# # Initial container list request +# self.requests_mock.register_uri( +# 'GET', +# object_fakes.ENDPOINT + '/oscar?format=json', +# json=return_body, +# status_code=200, +# ) +# # Individual object save requests +# self.requests_mock.register_uri( +# 'GET', +# object_fakes.ENDPOINT + '/oscar/' + object_fakes.object_name_1, +# json=object_fakes.OBJECT, +# status_code=200, +# ) +# self.requests_mock.register_uri( +# 'GET', +# object_fakes.ENDPOINT + '/oscar/' + object_fakes.object_name_2, +# json=object_fakes.OBJECT_2, +# status_code=200, +# ) +# +# arglist = [ +# 'oscar', +# ] +# verifylist = [( +# 'container', 'oscar', +# )] +# parsed_args = self.check_parser(self.cmd, arglist, verifylist) +# +# # Command.take_action() returns None +# ret = self.cmd.take_action(parsed_args) +# self.assertIsNone(ret) + + +class TestContainerShow(TestObjectAll): + + def setUp(self): + super(TestContainerShow, self).setUp() + + # Get the command object to test + self.cmd = container.ShowContainer(self.app, None) + + def test_object_show_container(self): + headers = { + 'x-container-meta-owner': object_fakes.ACCOUNT_ID, + 'x-container-object-count': '42', + 'x-container-bytes-used': '123', + 'x-container-read': 'qaz', + 'x-container-write': 'wsx', + 'x-container-sync-to': 'edc', + 'x-container-sync-key': 'rfv', + } + self.requests_mock.register_uri( + 'HEAD', + object_fakes.ENDPOINT + '/ernie', + headers=headers, + status_code=200, + ) + + arglist = [ + 'ernie', + ] + verifylist = [( + 'container', 'ernie', + )] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + collist = ( + 'account', + 'bytes_used', + 'container', + 'object_count', + 'read_acl', + 'sync_key', + 'sync_to', + 'write_acl', + ) + self.assertEqual(collist, columns) + datalist = [ + object_fakes.ACCOUNT_ID, + '123', + 'ernie', + '42', + 'qaz', + 'rfv', + 'edc', + 'wsx', + ] + self.assertEqual(datalist, list(data)) diff --git a/test-requirements.txt b/test-requirements.txt index dd701c0053..3b95cf7db3 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,6 +8,7 @@ discover fixtures>=0.3.14 mock>=1.0 oslosphinx>=2.2.0 # Apache-2.0 +requests-mock>=0.4.0 # Apache-2.0 sphinx>=1.1.2,!=1.2.0,<1.3 testrepository>=0.0.18 testtools>=0.9.34 From e3b9b9658805f274283a498ed82014dce3833fe3 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 18 Sep 2014 00:52:02 -0500 Subject: [PATCH 0201/3494] Add low-level API base class Adds the foundation of a low-level REST API client. This is the final prep stage in the conversion of the object-store commands from the old restapi interface to the keystoneclient.session-based API. * api.api.BaseAPI holds the common operations Change-Id: I8fba980e3eb2d787344f766507a9d0dae49dcadf --- openstackclient/api/__init__.py | 0 openstackclient/api/api.py | 349 +++++++++++++++++++++++++ openstackclient/tests/api/__init__.py | 0 openstackclient/tests/api/test_api.py | 362 ++++++++++++++++++++++++++ 4 files changed, 711 insertions(+) create mode 100644 openstackclient/api/__init__.py create mode 100644 openstackclient/api/api.py create mode 100644 openstackclient/tests/api/__init__.py create mode 100644 openstackclient/tests/api/test_api.py diff --git a/openstackclient/api/__init__.py b/openstackclient/api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstackclient/api/api.py b/openstackclient/api/api.py new file mode 100644 index 0000000000..72a66e1cb4 --- /dev/null +++ b/openstackclient/api/api.py @@ -0,0 +1,349 @@ +# 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. +# + +"""Base API Library""" + +import simplejson as json + +from keystoneclient.openstack.common.apiclient \ + import exceptions as ksc_exceptions +from keystoneclient import session as ksc_session +from openstackclient.common import exceptions + + +class KeystoneSession(object): + """Wrapper for the Keystone Session + + Restore some requests.session.Session compatibility; + keystoneclient.session.Session.request() has the method and url + arguments swapped from the rest of the requests-using world. + + """ + + def __init__( + self, + session=None, + endpoint=None, + **kwargs + ): + """Base object that contains some common API objects and methods + + :param Session session: + The default session to be used for making the HTTP API calls. + :param string endpoint: + The URL from the Service Catalog to be used as the base for API + requests on this API. + """ + + super(KeystoneSession, self).__init__() + + # a requests.Session-style interface + self.session = session + self.endpoint = endpoint + + def _request(self, method, url, session=None, **kwargs): + """Perform call into session + + All API calls are funneled through this method to provide a common + place to finalize the passed URL and other things. + + :param string method: + The HTTP method name, i.e. ``GET``, ``PUT``, etc + :param string url: + The API-specific portion of the URL path + :param Session session: + HTTP client session + :param kwargs: + keyword arguments passed to requests.request(). + :return: the requests.Response object + """ + + if not session: + session = self.session + if not session: + session = ksc_session.Session() + + if self.endpoint: + if url: + url = '/'.join([self.endpoint.rstrip('/'), url.lstrip('/')]) + else: + url = self.endpoint.rstrip('/') + + # Why is ksc session backwards??? + return session.request(url, method, **kwargs) + + +class BaseAPI(KeystoneSession): + """Base API""" + + def __init__( + self, + session=None, + service_type=None, + endpoint=None, + **kwargs + ): + """Base object that contains some common API objects and methods + + :param Session session: + The default session to be used for making the HTTP API calls. + :param string service_type: + API name, i.e. ``identity`` or ``compute`` + :param string endpoint: + The URL from the Service Catalog to be used as the base for API + requests on this API. + """ + + super(BaseAPI, self).__init__(session=session, endpoint=endpoint) + + self.service_type = service_type + + # The basic action methods all take a Session and return dict/lists + + def create( + self, + url, + session=None, + method=None, + **params + ): + """Create a new resource + + :param string url: + The API-specific portion of the URL path + :param Session session: + HTTP client session + :param string method: + HTTP method (default POST) + """ + + if not method: + method = 'POST' + ret = self._request(method, url, session=session, **params) + # Should this move into _requests()? + try: + return ret.json() + except json.JSONDecodeError: + return ret + + def delete( + self, + url, + session=None, + **params + ): + """Delete a resource + + :param string url: + The API-specific portion of the URL path + :param Session session: + HTTP client session + """ + + return self._request('DELETE', url, **params) + + def list( + self, + path, + session=None, + body=None, + detailed=False, + **params + ): + """Return a list of resources + + GET ${ENDPOINT}/${PATH} + + path is often the object's plural resource type + + :param string path: + The API-specific portion of the URL path + :param Session session: + HTTP client session + :param body: data that will be encoded as JSON and passed in POST + request (GET will be sent by default) + :param bool detailed: + Adds '/details' to path for some APIs to return extended attributes + :returns: + JSON-decoded response, could be a list or a dict-wrapped-list + """ + + if detailed: + path = '/'.join([path.rstrip('/'), 'details']) + + if body: + ret = self._request( + 'POST', + path, + # service=self.service_type, + json=body, + params=params, + ) + else: + ret = self._request( + 'GET', + path, + # service=self.service_type, + params=params, + ) + try: + return ret.json() + except json.JSONDecodeError: + return ret + + # Layered actions built on top of the basic action methods do not + # explicitly take a Session but one may still be passed in kwargs + + def find_attr( + self, + path, + value=None, + attr=None, + resource=None, + ): + """Find a resource via attribute or ID + + Most APIs return a list wrapped by a dict with the resource + name as key. Some APIs (Identity) return a dict when a query + string is present and there is one return value. Take steps to + unwrap these bodies and return a single dict without any resource + wrappers. + + :param string path: + The API-specific portion of the URL path + :param string value: + value to search for + :param string attr: + attribute to use for resource search + :param string resource: + plural of the object resource name; defaults to path + For example: + n = find(netclient, 'network', 'networks', 'matrix') + """ + + # Default attr is 'name' + if attr is None: + attr = 'name' + + # Default resource is path - in many APIs they are the same + if resource is None: + resource = path + + def getlist(kw): + """Do list call, unwrap resource dict if present""" + ret = self.list(path, **kw) + if type(ret) == dict and resource in ret: + ret = ret[resource] + return ret + + # Search by attribute + kwargs = {attr: value} + data = getlist(kwargs) + if type(data) == dict: + return data + if len(data) == 1: + return data[0] + if len(data) > 1: + msg = "Multiple %s exist with %s='%s'" + raise ksc_exceptions.CommandError( + msg % (resource, attr, value), + ) + + # Search by id + kwargs = {'id': value} + data = getlist(kwargs) + if len(data) == 1: + return data[0] + msg = "No %s with a %s or ID of '%s' found" + raise exceptions.CommandError(msg % (resource, attr, value)) + + def find_bulk( + self, + path, + **kwargs + ): + """Bulk load and filter locally + + :param string path: + The API-specific portion of the URL path + :param kwargs: + A dict of AVPs to match - logical AND + :returns: list of resource dicts + """ + + items = self.list(path) + if type(items) == dict: + # strip off the enclosing dict + key = list(items.keys())[0] + items = items[key] + + ret = [] + for o in items: + try: + if all(o[attr] == kwargs[attr] for attr in kwargs.keys()): + ret.append(o) + except KeyError: + continue + + return ret + + def find_one( + self, + path, + **kwargs + ): + """Find a resource by name or ID + + :param string path: + The API-specific portion of the URL path + :returns: + resource dict + """ + + bulk_list = self.find_bulk(path, **kwargs) + num_bulk = len(bulk_list) + if num_bulk == 0: + msg = "none found" + raise ksc_exceptions.NotFound(msg) + elif num_bulk > 1: + msg = "many found" + raise RuntimeError(msg) + return bulk_list[0] + + def find( + self, + path, + value=None, + attr=None, + ): + """Find a single resource by name or ID + + :param string path: + The API-specific portion of the URL path + :param string search: + search expression + :param string attr: + name of attribute for secondary search + """ + + try: + ret = self._request('GET', "/%s/%s" % (path, value)).json() + except ksc_exceptions.NotFound: + kwargs = {attr: value} + try: + ret = self.find_one("/%s/detail" % (path), **kwargs) + except ksc_exceptions.NotFound: + msg = "%s not found" % value + raise ksc_exceptions.NotFound(msg) + + return ret diff --git a/openstackclient/tests/api/__init__.py b/openstackclient/tests/api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstackclient/tests/api/test_api.py b/openstackclient/tests/api/test_api.py new file mode 100644 index 0000000000..32042e4f3b --- /dev/null +++ b/openstackclient/tests/api/test_api.py @@ -0,0 +1,362 @@ +# 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. +# + +"""Base API Library Tests""" + +from requests_mock.contrib import fixture + +from keystoneclient import session +from openstackclient.api import api +from openstackclient.common import exceptions +from openstackclient.tests import utils + + +RESP_ITEM_1 = { + 'id': '1', + 'name': 'alpha', + 'status': 'UP', +} +RESP_ITEM_2 = { + 'id': '2', + 'name': 'beta', + 'status': 'DOWN', +} +RESP_ITEM_3 = { + 'id': '3', + 'name': 'delta', + 'status': 'UP', +} + +LIST_RESP = [RESP_ITEM_1, RESP_ITEM_2] + +LIST_BODY = { + 'p1': 'xxx', + 'p2': 'yyy', +} + + +class TestSession(utils.TestCase): + + BASE_URL = 'https://api.example.com:1234/vX' + + def setUp(self): + super(TestSession, self).setUp() + self.sess = session.Session() + self.requests_mock = self.useFixture(fixture.Fixture()) + + +class TestKeystoneSession(TestSession): + + def setUp(self): + super(TestKeystoneSession, self).setUp() + self.api = api.KeystoneSession( + session=self.sess, + endpoint=self.BASE_URL, + ) + + def test_session_request(self): + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz', + json=RESP_ITEM_1, + status_code=200, + ) + ret = self.api._request('GET', '/qaz') + self.assertEqual(RESP_ITEM_1, ret.json()) + + +class TestBaseAPI(TestSession): + + def setUp(self): + super(TestBaseAPI, self).setUp() + self.api = api.BaseAPI( + session=self.sess, + endpoint=self.BASE_URL, + ) + + def test_create_post(self): + self.requests_mock.register_uri( + 'POST', + self.BASE_URL + '/qaz', + json=RESP_ITEM_1, + status_code=202, + ) + ret = self.api.create('qaz') + self.assertEqual(RESP_ITEM_1, ret) + + def test_create_put(self): + self.requests_mock.register_uri( + 'PUT', + self.BASE_URL + '/qaz', + json=RESP_ITEM_1, + status_code=202, + ) + ret = self.api.create('qaz', method='PUT') + self.assertEqual(RESP_ITEM_1, ret) + + def test_delete(self): + self.requests_mock.register_uri( + 'DELETE', + self.BASE_URL + '/qaz', + status_code=204, + ) + ret = self.api.delete('qaz') + self.assertEqual(204, ret.status_code) + + # find tests + + def test_find_attr_by_id(self): + + # All first requests (by name) will fail in this test + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz?name=1', + json={'qaz': []}, + status_code=200, + ) + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz?id=1', + json={'qaz': [RESP_ITEM_1]}, + status_code=200, + ) + ret = self.api.find_attr('qaz', '1') + self.assertEqual(RESP_ITEM_1, ret) + + # value not found + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz?name=0', + json={'qaz': []}, + status_code=200, + ) + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz?id=0', + json={'qaz': []}, + status_code=200, + ) + self.assertRaises( + exceptions.CommandError, + self.api.find_attr, + 'qaz', + '0', + ) + + # Attribute other than 'name' + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz?status=UP', + json={'qaz': [RESP_ITEM_1]}, + status_code=200, + ) + ret = self.api.find_attr('qaz', 'UP', attr='status') + self.assertEqual(RESP_ITEM_1, ret) + ret = self.api.find_attr('qaz', value='UP', attr='status') + self.assertEqual(RESP_ITEM_1, ret) + + def test_find_attr_by_name(self): + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz?name=alpha', + json={'qaz': [RESP_ITEM_1]}, + status_code=200, + ) + ret = self.api.find_attr('qaz', 'alpha') + self.assertEqual(RESP_ITEM_1, ret) + + # value not found + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz?name=0', + json={'qaz': []}, + status_code=200, + ) + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz?id=0', + json={'qaz': []}, + status_code=200, + ) + self.assertRaises( + exceptions.CommandError, + self.api.find_attr, + 'qaz', + '0', + ) + + # Attribute other than 'name' + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz?status=UP', + json={'qaz': [RESP_ITEM_1]}, + status_code=200, + ) + ret = self.api.find_attr('qaz', 'UP', attr='status') + self.assertEqual(RESP_ITEM_1, ret) + ret = self.api.find_attr('qaz', value='UP', attr='status') + self.assertEqual(RESP_ITEM_1, ret) + + def test_find_attr_path_resource(self): + + # Test resource different than path + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/wsx?name=1', + json={'qaz': []}, + status_code=200, + ) + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/wsx?id=1', + json={'qaz': [RESP_ITEM_1]}, + status_code=200, + ) + ret = self.api.find_attr('wsx', '1', resource='qaz') + self.assertEqual(RESP_ITEM_1, ret) + + def test_find_bulk_none(self): + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz', + json=LIST_RESP, + status_code=200, + ) + ret = self.api.find_bulk('qaz') + self.assertEqual(LIST_RESP, ret) + + def test_find_bulk_one(self): + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz', + json=LIST_RESP, + status_code=200, + ) + ret = self.api.find_bulk('qaz', id='1') + self.assertEqual([LIST_RESP[0]], ret) + + ret = self.api.find_bulk('qaz', id='0') + self.assertEqual([], ret) + + ret = self.api.find_bulk('qaz', name='beta') + self.assertEqual([LIST_RESP[1]], ret) + + ret = self.api.find_bulk('qaz', error='bogus') + self.assertEqual([], ret) + + def test_find_bulk_two(self): + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz', + json=LIST_RESP, + status_code=200, + ) + ret = self.api.find_bulk('qaz', id='1', name='alpha') + self.assertEqual([LIST_RESP[0]], ret) + + ret = self.api.find_bulk('qaz', id='1', name='beta') + self.assertEqual([], ret) + + ret = self.api.find_bulk('qaz', id='1', error='beta') + self.assertEqual([], ret) + + def test_find_bulk_dict(self): + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz', + json={'qaz': LIST_RESP}, + status_code=200, + ) + ret = self.api.find_bulk('qaz', id='1') + self.assertEqual([LIST_RESP[0]], ret) + + # list tests + + def test_list_no_body(self): + self.requests_mock.register_uri( + 'GET', + self.BASE_URL, + json=LIST_RESP, + status_code=200, + ) + ret = self.api.list('') + self.assertEqual(LIST_RESP, ret) + + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz', + json=LIST_RESP, + status_code=200, + ) + ret = self.api.list('qaz') + self.assertEqual(LIST_RESP, ret) + + def test_list_params(self): + params = {'format': 'json'} + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '?format=json', + json=LIST_RESP, + status_code=200, + ) + ret = self.api.list('', **params) + self.assertEqual(LIST_RESP, ret) + + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz?format=json', + json=LIST_RESP, + status_code=200, + ) + ret = self.api.list('qaz', **params) + self.assertEqual(LIST_RESP, ret) + + def test_list_body(self): + self.requests_mock.register_uri( + 'POST', + self.BASE_URL + '/qaz', + json=LIST_RESP, + status_code=200, + ) + ret = self.api.list('qaz', body=LIST_BODY) + self.assertEqual(LIST_RESP, ret) + + def test_list_detailed(self): + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz/details', + json=LIST_RESP, + status_code=200, + ) + ret = self.api.list('qaz', detailed=True) + self.assertEqual(LIST_RESP, ret) + + def test_list_filtered(self): + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz?attr=value', + json=LIST_RESP, + status_code=200, + ) + ret = self.api.list('qaz', attr='value') + self.assertEqual(LIST_RESP, ret) + + def test_list_wrapped(self): + self.requests_mock.register_uri( + 'GET', + self.BASE_URL + '/qaz?attr=value', + json={'responses': LIST_RESP}, + status_code=200, + ) + ret = self.api.list('qaz', attr='value') + self.assertEqual({'responses': LIST_RESP}, ret) From 31018bf7c2c57c530d55ed1dd90b9b65d489d557 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 18 Sep 2014 00:54:52 -0500 Subject: [PATCH 0202/3494] Move object-store commands to low-level API api.object_store.APIv1 now contains the formerly top-level functions implementing the object-store REST client. This replaces the old-style ObjectClientv1 that is no longer necessary. Change-Id: I7d8fea326b214481e7d6b24119bd41777c6aa968 --- openstackclient/api/object_store_v1.py | 381 ++++++++++++++++++ openstackclient/object/client.py | 22 +- openstackclient/object/v1/container.py | 29 +- openstackclient/object/v1/lib/container.py | 170 -------- openstackclient/object/v1/lib/object.py | 221 ---------- openstackclient/object/v1/object.py | 42 +- .../tests/api/test_object_store_v1.py | 326 +++++++++++++++ .../tests/object/v1/lib/__init__.py | 0 .../tests/object/v1/lib/test_container.py | 207 ---------- .../tests/object/v1/lib/test_object.py | 295 -------------- .../tests/object/v1/test_container.py | 81 +--- .../tests/object/v1/test_container_all.py | 6 +- .../tests/object/v1/test_object.py | 59 +-- 13 files changed, 773 insertions(+), 1066 deletions(-) create mode 100644 openstackclient/api/object_store_v1.py delete mode 100644 openstackclient/object/v1/lib/container.py delete mode 100644 openstackclient/object/v1/lib/object.py create mode 100644 openstackclient/tests/api/test_object_store_v1.py delete mode 100644 openstackclient/tests/object/v1/lib/__init__.py delete mode 100644 openstackclient/tests/object/v1/lib/test_container.py delete mode 100644 openstackclient/tests/object/v1/lib/test_object.py diff --git a/openstackclient/api/object_store_v1.py b/openstackclient/api/object_store_v1.py new file mode 100644 index 0000000000..f938b55ed5 --- /dev/null +++ b/openstackclient/api/object_store_v1.py @@ -0,0 +1,381 @@ +# 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. +# + +"""Object Store v1 API Library""" + +import os +import six + +try: + from urllib.parse import urlparse # noqa +except ImportError: + from urlparse import urlparse # noqa + +from openstackclient.api import api + + +class APIv1(api.BaseAPI): + """Object Store v1 API""" + + def __init__(self, **kwargs): + super(APIv1, self).__init__(**kwargs) + + def container_create( + self, + container=None, + ): + """Create a container + + :param string container: + name of container to create + :returns: + dict of returned headers + """ + + response = self.create(container, method='PUT') + url_parts = urlparse(self.endpoint) + data = { + 'account': url_parts.path.split('/')[-1], + 'container': container, + 'x-trans-id': response.headers.get('x-trans-id', None), + } + + return data + + def container_delete( + self, + container=None, + ): + """Delete a container + + :param string container: + name of container to delete + """ + + if container: + self.delete(container) + + def container_list( + self, + all_data=False, + limit=None, + marker=None, + end_marker=None, + prefix=None, + **params + ): + """Get containers in an account + + :param boolean all_data: + if True, return a full listing, else returns a max of + 10000 listings + :param integer limit: + query return count limit + :param string marker: + query marker + :param string end_marker: + query end_marker + :param string prefix: + query prefix + :returns: + list of container names + """ + + params['format'] = 'json' + + if all_data: + data = listing = self.container_list( + limit=limit, + marker=marker, + end_marker=end_marker, + prefix=prefix, + **params + ) + while listing: + marker = listing[-1]['name'] + listing = self.container_list( + limit=limit, + marker=marker, + end_marker=end_marker, + prefix=prefix, + **params + ) + if listing: + data.extend(listing) + return data + + if limit: + params['limit'] = limit + if marker: + params['marker'] = marker + if end_marker: + params['end_marker'] = end_marker + if prefix: + params['prefix'] = prefix + + return self.list('', **params) + + def container_save( + self, + container=None, + ): + """Save all the content from a container + + :param string container: + name of container to save + """ + + objects = self.object_list(container=container) + for object in objects: + self.object_save(container=container, object=object['name']) + + def container_show( + self, + container=None, + ): + """Get container details + + :param string container: + name of container to show + :returns: + dict of returned headers + """ + + response = self._request('HEAD', container) + data = { + 'account': response.headers.get('x-container-meta-owner', None), + 'container': container, + 'object_count': response.headers.get( + 'x-container-object-count', + None, + ), + 'bytes_used': response.headers.get('x-container-bytes-used', None), + 'read_acl': response.headers.get('x-container-read', None), + 'write_acl': response.headers.get('x-container-write', None), + 'sync_to': response.headers.get('x-container-sync-to', None), + 'sync_key': response.headers.get('x-container-sync-key', None), + } + return data + + def object_create( + self, + container=None, + object=None, + ): + """Create an object inside a container + + :param string container: + name of container to store object + :param string object: + local path to object + :returns: + dict of returned headers + """ + + if container is None or object is None: + # TODO(dtroyer): What exception to raise here? + return {} + + full_url = "%s/%s" % (container, object) + response = self.create(full_url, method='PUT', data=open(object)) + url_parts = urlparse(self.endpoint) + data = { + 'account': url_parts.path.split('/')[-1], + 'container': container, + 'object': object, + 'x-trans-id': response.headers.get('X-Trans-Id', None), + 'etag': response.headers.get('Etag', None), + } + + return data + + def object_delete( + self, + container=None, + object=None, + ): + """Delete an object from a container + + :param string container: + name of container that stores object + :param string object: + name of object to delete + """ + + if container is None or object is None: + return + + self.delete("%s/%s" % (container, object)) + + def object_list( + self, + container=None, + all_data=False, + limit=None, + marker=None, + end_marker=None, + delimiter=None, + prefix=None, + **params + ): + """List objects in a container + + :param string container: + container name to get a listing for + :param boolean all_data: + if True, return a full listing, else returns a max of + 10000 listings + :param integer limit: + query return count limit + :param string marker: + query marker + :param string end_marker: + query end_marker + :param string prefix: + query prefix + :param string delimiter: + string to delimit the queries on + :returns: a tuple of (response headers, a list of objects) The response + headers will be a dict and all header names will be lowercase. + """ + + if container is None or object is None: + return None + + if all_data: + data = listing = self.object_list( + container=container, + limit=limit, + marker=marker, + end_marker=end_marker, + prefix=prefix, + delimiter=delimiter, + **params + ) + while listing: + if delimiter: + marker = listing[-1].get('name', listing[-1].get('subdir')) + else: + marker = listing[-1]['name'] + listing = self.object_list( + container=container, + limit=limit, + marker=marker, + end_marker=end_marker, + prefix=prefix, + delimiter=delimiter, + **params + ) + if listing: + data.extend(listing) + return data + + params = {} + if limit: + params['limit'] = limit + if marker: + params['marker'] = marker + if end_marker: + params['end_marker'] = end_marker + if prefix: + params['prefix'] = prefix + if delimiter: + params['delimiter'] = delimiter + + return self.list(container, **params) + + def object_save( + self, + container=None, + object=None, + file=None, + ): + """Save an object stored in a container + + :param string container: + name of container that stores object + :param string object: + name of object to save + :param string file: + local name of object + """ + + if not file: + file = object + + response = self._request( + 'GET', + "%s/%s" % (container, object), + stream=True, + ) + if response.status_code == 200: + if not os.path.exists(os.path.dirname(file)): + os.makedirs(os.path.dirname(file)) + with open(file, 'wb') as f: + for chunk in response.iter_content(): + f.write(chunk) + + def object_show( + self, + container=None, + object=None, + ): + """Get object details + + :param string container: + container name for object to get + :param string object: + name of object to get + :returns: + dict of object properties + """ + + if container is None or object is None: + return {} + + response = self._request('HEAD', "%s/%s" % (container, object)) + data = { + 'account': response.headers.get('x-container-meta-owner', None), + 'container': container, + 'object': object, + 'content-type': response.headers.get('content-type', None), + } + if 'content-length' in response.headers: + data['content-length'] = response.headers.get( + 'content-length', + None, + ) + if 'last-modified' in response.headers: + data['last-modified'] = response.headers.get('last-modified', None) + if 'etag' in response.headers: + data['etag'] = response.headers.get('etag', None) + if 'x-object-manifest' in response.headers: + data['x-object-manifest'] = response.headers.get( + 'x-object-manifest', + None, + ) + for key, value in six.iteritems(response.headers): + if key.startswith('x-object-meta-'): + data[key[len('x-object-meta-'):].lower()] = value + elif key not in ( + 'content-type', + 'content-length', + 'last-modified', + 'etag', + 'date', + 'x-object-manifest', + 'x-container-meta-owner', + ): + data[key.lower()] = value + + return data diff --git a/openstackclient/object/client.py b/openstackclient/object/client.py index b81ffaaf44..887aa85b8e 100644 --- a/openstackclient/object/client.py +++ b/openstackclient/object/client.py @@ -17,6 +17,7 @@ import logging +from openstackclient.api import object_store_v1 from openstackclient.common import utils LOG = logging.getLogger(__name__) @@ -30,7 +31,7 @@ def make_client(instance): - """Returns an object service client.""" + """Returns an object-store API client.""" object_client = utils.get_client_class( API_NAME, @@ -42,9 +43,11 @@ def make_client(instance): endpoint = instance._url else: endpoint = instance.get_endpoint_for_service_type("object-store") - client = object_client( + + client = object_store_v1.APIv1( + session=instance.session, + service_type='object-store', endpoint=endpoint, - token=instance._token, ) return client @@ -61,16 +64,3 @@ def build_option_parser(parser): DEFAULT_OBJECT_API_VERSION + ' (Env: OS_OBJECT_API_VERSION)') return parser - - -class ObjectClientv1(object): - - def __init__( - self, - endpoint_type='publicURL', - endpoint=None, - token=None, - ): - self.endpoint_type = endpoint_type - self.endpoint = endpoint - self.token = token diff --git a/openstackclient/object/v1/container.py b/openstackclient/object/v1/container.py index 9d55381cbe..ead3df45e9 100644 --- a/openstackclient/object/v1/container.py +++ b/openstackclient/object/v1/container.py @@ -24,7 +24,6 @@ from cliff import show from openstackclient.common import utils -from openstackclient.object.v1.lib import container as lib_container class CreateContainer(lister.Lister): @@ -47,10 +46,8 @@ def take_action(self, parsed_args): results = [] for container in parsed_args.containers: - data = lib_container.create_container( - self.app.client_manager.session, - self.app.client_manager.object_store.endpoint, - container, + data = self.app.client_manager.object_store.container_create( + container=container, ) results.append(data) @@ -81,10 +78,8 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) for container in parsed_args.containers: - lib_container.delete_container( - self.app.client_manager.session, - self.app.client_manager.object_store.endpoint, - container, + self.app.client_manager.object_store.container_delete( + container=container, ) @@ -150,9 +145,7 @@ def take_action(self, parsed_args): if parsed_args.all: kwargs['full_listing'] = True - data = lib_container.list_containers( - self.app.client_manager.session, - self.app.client_manager.object_store.endpoint, + data = self.app.client_manager.object_store.container_list( **kwargs ) @@ -180,10 +173,8 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) - lib_container.save_container( - self.app.client_manager.session, - self.app.client_manager.object_store.endpoint, - parsed_args.container + self.app.client_manager.object_store.container_save( + container=parsed_args.container, ) @@ -204,10 +195,8 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) - data = lib_container.show_container( - self.app.client_manager.session, - self.app.client_manager.object_store.endpoint, - parsed_args.container, + data = self.app.client_manager.object_store.container_show( + container=parsed_args.container, ) return zip(*sorted(six.iteritems(data))) diff --git a/openstackclient/object/v1/lib/container.py b/openstackclient/object/v1/lib/container.py deleted file mode 100644 index 4293ff4a20..0000000000 --- a/openstackclient/object/v1/lib/container.py +++ /dev/null @@ -1,170 +0,0 @@ -# Copyright 2010-2012 OpenStack Foundation -# Copyright 2013 Nebula Inc. -# -# 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. -# - -"""Object v1 API library""" - -try: - from urllib.parse import urlparse # noqa -except ImportError: - from urlparse import urlparse # noqa - -from openstackclient.object.v1.lib import object as object_lib - - -def create_container( - session, - url, - container, -): - """Create a container - - :param session: an authenticated keystoneclient.session.Session object - :param url: endpoint - :param container: name of container to create - :returns: dict of returned headers - """ - - response = session.put("%s/%s" % (url, container)) - url_parts = urlparse(url) - data = { - 'account': url_parts.path.split('/')[-1], - 'container': container, - 'x-trans-id': response.headers.get('x-trans-id', None), - } - - return data - - -def delete_container( - session, - url, - container, -): - """Delete a container - - :param session: an authenticated keystoneclient.session.Session object - :param url: endpoint - :param container: name of container to delete - """ - - session.delete("%s/%s" % (url, container)) - - -def list_containers( - session, - url, - marker=None, - limit=None, - end_marker=None, - prefix=None, - full_listing=False, -): - """Get containers in an account - - :param session: an authenticated keystoneclient.session.Session object - :param url: endpoint - :param marker: marker query - :param limit: limit query - :param end_marker: end_marker query - :param prefix: prefix query - :param full_listing: if True, return a full listing, else returns a max - of 10000 listings - :returns: list of containers - """ - - if full_listing: - data = listing = list_containers( - session, - url, - marker, - limit, - end_marker, - prefix, - ) - while listing: - marker = listing[-1]['name'] - listing = list_containers( - session, - url, - marker, - limit, - end_marker, - prefix, - ) - if listing: - data.extend(listing) - return data - - params = { - 'format': 'json', - } - if marker: - params['marker'] = marker - if limit: - params['limit'] = limit - if end_marker: - params['end_marker'] = end_marker - if prefix: - params['prefix'] = prefix - return session.get(url, params=params).json() - - -def save_container( - session, - url, - container -): - """Save all the content from a container - - :param session: an authenticated keystoneclient.session.Session object - :param url: endpoint - :param container: name of container to save - """ - - objects = object_lib.list_objects(session, url, container) - for object in objects: - object_lib.save_object(session, url, container, object['name']) - - -def show_container( - session, - url, - container, -): - """Get container details - - :param session: an authenticated keystoneclient.session.Session object - :param url: endpoint - :param container: name of container to show - :returns: dict of returned headers - """ - - response = session.head("%s/%s" % (url, container)) - data = { - 'account': response.headers.get('x-container-meta-owner', None), - 'container': container, - 'object_count': response.headers.get( - 'x-container-object-count', - None, - ), - 'bytes_used': response.headers.get('x-container-bytes-used', None), - 'read_acl': response.headers.get('x-container-read', None), - 'write_acl': response.headers.get('x-container-write', None), - 'sync_to': response.headers.get('x-container-sync-to', None), - 'sync_key': response.headers.get('x-container-sync-key', None), - } - - return data diff --git a/openstackclient/object/v1/lib/object.py b/openstackclient/object/v1/lib/object.py deleted file mode 100644 index 7a23fc7698..0000000000 --- a/openstackclient/object/v1/lib/object.py +++ /dev/null @@ -1,221 +0,0 @@ -# Copyright 2010-2012 OpenStack Foundation -# Copyright 2013 Nebula Inc. -# -# 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. -# - -"""Object v1 API library""" - -import os - -import six - -try: - from urllib.parse import urlparse # noqa -except ImportError: - from urlparse import urlparse # noqa - - -def create_object( - session, - url, - container, - object, -): - """Create an object, upload it to a container - - :param session: an authenticated keystoneclient.session.Session object - :param url: endpoint - :param container: name of container to store object - :param object: local path to object - :returns: dict of returned headers - """ - - full_url = "%s/%s/%s" % (url, container, object) - response = session.put(full_url, data=open(object)) - url_parts = urlparse(url) - data = { - 'account': url_parts.path.split('/')[-1], - 'container': container, - 'object': object, - 'x-trans-id': response.headers.get('X-Trans-Id', None), - 'etag': response.headers.get('Etag', None), - } - - return data - - -def delete_object( - session, - url, - container, - object, -): - """Delete an object stored in a container - - :param session: an authenticated keystoneclient.session.Session object - :param url: endpoint - :param container: name of container that stores object - :param container: name of object to delete - """ - - session.delete("%s/%s/%s" % (url, container, object)) - - -def list_objects( - session, - url, - container, - marker=None, - limit=None, - end_marker=None, - delimiter=None, - prefix=None, - path=None, - full_listing=False, -): - """Get objects in a container - - :param session: an authenticated keystoneclient.session.Session object - :param url: endpoint - :param container: container name to get a listing for - :param marker: marker query - :param limit: limit query - :param end_marker: marker query - :param delimiter: string to delimit the queries on - :param prefix: prefix query - :param path: path query (equivalent: "delimiter=/" and "prefix=path/") - :param full_listing: if True, return a full listing, else returns a max - of 10000 listings - :returns: a tuple of (response headers, a list of objects) The response - headers will be a dict and all header names will be lowercase. - """ - - if full_listing: - data = listing = list_objects( - session, - url, - container, - marker, - limit, - end_marker, - delimiter, - prefix, - path, - ) - while listing: - if delimiter: - marker = listing[-1].get('name', listing[-1].get('subdir')) - else: - marker = listing[-1]['name'] - listing = list_objects( - session, - url, - container, - marker, - limit, - end_marker, - delimiter, - prefix, - path, - ) - if listing: - data.extend(listing) - return data - - params = { - 'format': 'json', - } - if marker: - params['marker'] = marker - if limit: - params['limit'] = limit - if end_marker: - params['end_marker'] = end_marker - if delimiter: - params['delimiter'] = delimiter - if prefix: - params['prefix'] = prefix - if path: - params['path'] = path - requrl = "%s/%s" % (url, container) - return session.get(requrl, params=params).json() - - -def save_object( - session, - url, - container, - obj, - file=None -): - """Save an object stored in a container - - :param session: an authenticated keystoneclient.session.Session object - :param url: endpoint - :param container: name of container that stores object - :param object: name of object to save - :param file: local name of object - """ - - if not file: - file = obj - - response = session.get("%s/%s/%s" % (url, container, obj), stream=True) - if response.status_code == 200: - if not os.path.exists(os.path.dirname(file)): - os.makedirs(os.path.dirname(file)) - with open(file, 'wb') as f: - for chunk in response.iter_content(): - f.write(chunk) - - -def show_object( - session, - url, - container, - obj, -): - """Get object details - - :param session: an authenticated keystoneclient.session.Session object - :param url: endpoint - :param container: container name to get a listing for - :returns: dict of object properties - """ - - response = session.head("%s/%s/%s" % (url, container, obj)) - data = { - 'account': response.headers.get('x-container-meta-owner', None), - 'container': container, - 'object': obj, - 'content-type': response.headers.get('content-type', None), - } - if 'content-length' in response.headers: - data['content-length'] = response.headers.get('content-length', None) - if 'last-modified' in response.headers: - data['last-modified'] = response.headers.get('last-modified', None) - if 'etag' in response.headers: - data['etag'] = response.headers.get('etag', None) - if 'x-object-manifest' in response.headers: - data['x-object-manifest'] = response.headers.get( - 'x-object-manifest', None) - for key, value in six.iteritems(response.headers): - if key.startswith('x-object-meta-'): - data[key[len('x-object-meta-'):].lower()] = value - elif key not in ( - 'content-type', 'content-length', 'last-modified', - 'etag', 'date', 'x-object-manifest', 'x-container-meta-owner'): - data[key.lower()] = value - - return data diff --git a/openstackclient/object/v1/object.py b/openstackclient/object/v1/object.py index f0ea763300..cbe9da2fb4 100644 --- a/openstackclient/object/v1/object.py +++ b/openstackclient/object/v1/object.py @@ -24,7 +24,6 @@ from cliff import show from openstackclient.common import utils -from openstackclient.object.v1.lib import object as lib_object class CreateObject(lister.Lister): @@ -52,11 +51,9 @@ def take_action(self, parsed_args): results = [] for obj in parsed_args.objects: - data = lib_object.create_object( - self.app.client_manager.session, - self.app.client_manager.object_store.endpoint, - parsed_args.container, - obj, + data = self.app.client_manager.object_store.object_create( + container=parsed_args.container, + object=obj, ) results.append(data) @@ -92,12 +89,9 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) for obj in parsed_args.objects: - lib_object.delete_object( - self.app.restapi, - self.app.client_manager.session, - self.app.client_manager.object_store.endpoint, - parsed_args.container, - obj, + self.app.client_manager.object_store.object_delete( + container=parsed_args.container, + object=obj, ) @@ -181,10 +175,8 @@ def take_action(self, parsed_args): if parsed_args.all: kwargs['full_listing'] = True - data = lib_object.list_objects( - self.app.client_manager.session, - self.app.client_manager.object_store.endpoint, - parsed_args.container, + data = self.app.client_manager.object_store.object_list( + container=parsed_args.container, **kwargs ) @@ -222,12 +214,10 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) - lib_object.save_object( - self.app.client_manager.session, - self.app.client_manager.object_store.endpoint, - parsed_args.container, - parsed_args.object, - parsed_args.file, + self.app.client_manager.object_store.object_save( + container=parsed_args.container, + object=parsed_args.object, + file=parsed_args.file, ) @@ -253,11 +243,9 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) - data = lib_object.show_object( - self.app.client_manager.session, - self.app.client_manager.object_store.endpoint, - parsed_args.container, - parsed_args.object, + data = self.app.client_manager.object_store.object_show( + container=parsed_args.container, + object=parsed_args.object, ) return zip(*sorted(six.iteritems(data))) diff --git a/openstackclient/tests/api/test_object_store_v1.py b/openstackclient/tests/api/test_object_store_v1.py new file mode 100644 index 0000000000..5a376a454b --- /dev/null +++ b/openstackclient/tests/api/test_object_store_v1.py @@ -0,0 +1,326 @@ +# 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. +# + +"""Object Store v1 API Library Tests""" + +from requests_mock.contrib import fixture + +from keystoneclient import session +from openstackclient.api import object_store_v1 as object_store +from openstackclient.tests import utils + + +FAKE_ACCOUNT = 'q12we34r' +FAKE_AUTH = '11223344556677889900' +FAKE_URL = 'http://gopher.com/v1/' + FAKE_ACCOUNT + +FAKE_CONTAINER = 'rainbarrel' +FAKE_OBJECT = 'spigot' + +LIST_CONTAINER_RESP = [ + 'qaz', + 'fred', +] + +LIST_OBJECT_RESP = [ + {'name': 'fred', 'bytes': 1234, 'content_type': 'text'}, + {'name': 'wilma', 'bytes': 5678, 'content_type': 'text'}, +] + + +class TestObjectAPIv1(utils.TestCase): + + def setUp(self): + super(TestObjectAPIv1, self).setUp() + sess = session.Session() + self.api = object_store.APIv1(session=sess, endpoint=FAKE_URL) + self.requests_mock = self.useFixture(fixture.Fixture()) + + +class TestContainer(TestObjectAPIv1): + + def setUp(self): + super(TestContainer, self).setUp() + + def test_container_create(self): + headers = { + 'x-trans-id': '1qaz2wsx', + } + self.requests_mock.register_uri( + 'PUT', + FAKE_URL + '/qaz', + headers=headers, + status_code=201, + ) + ret = self.api.container_create(container='qaz') + data = { + 'account': FAKE_ACCOUNT, + 'container': 'qaz', + 'x-trans-id': '1qaz2wsx', + } + self.assertEqual(data, ret) + + def test_container_delete(self): + self.requests_mock.register_uri( + 'DELETE', + FAKE_URL + '/qaz', + status_code=204, + ) + ret = self.api.container_delete(container='qaz') + self.assertIsNone(ret) + + def test_container_list_no_options(self): + self.requests_mock.register_uri( + 'GET', + FAKE_URL, + json=LIST_CONTAINER_RESP, + status_code=200, + ) + ret = self.api.container_list() + self.assertEqual(LIST_CONTAINER_RESP, ret) + + def test_container_list_prefix(self): + self.requests_mock.register_uri( + 'GET', + FAKE_URL + '?prefix=foo%2f&format=json', + json=LIST_CONTAINER_RESP, + status_code=200, + ) + ret = self.api.container_list( + prefix='foo/', + ) + self.assertEqual(LIST_CONTAINER_RESP, ret) + + def test_container_list_marker_limit_end(self): + self.requests_mock.register_uri( + 'GET', + FAKE_URL + '?marker=next&limit=2&end_marker=stop&format=json', + json=LIST_CONTAINER_RESP, + status_code=200, + ) + ret = self.api.container_list( + marker='next', + limit=2, + end_marker='stop', + ) + self.assertEqual(LIST_CONTAINER_RESP, ret) + +# def test_container_list_full_listing(self): +# sess = self.app.client_manager.session +# +# def side_effect(*args, **kwargs): +# rv = sess.get().json.return_value +# sess.get().json.return_value = [] +# sess.get().json.side_effect = None +# return rv +# +# resp = [{'name': 'is-name'}] +# sess.get().json.return_value = resp +# sess.get().json.side_effect = side_effect +# +# data = lib_container.list_containers( +# self.app.client_manager.session, +# fake_url, +# full_listing=True, +# ) +# +# # Check expected values +# sess.get.assert_called_with( +# fake_url, +# params={ +# 'format': 'json', +# 'marker': 'is-name', +# } +# ) +# self.assertEqual(resp, data) + + def test_container_show(self): + headers = { + 'X-Container-Meta-Owner': FAKE_ACCOUNT, + 'x-container-object-count': '1', + 'x-container-bytes-used': '577', + } + resp = { + 'account': FAKE_ACCOUNT, + 'container': 'qaz', + 'object_count': '1', + 'bytes_used': '577', + 'read_acl': None, + 'write_acl': None, + 'sync_to': None, + 'sync_key': None, + } + self.requests_mock.register_uri( + 'HEAD', + FAKE_URL + '/qaz', + headers=headers, + status_code=204, + ) + ret = self.api.container_show(container='qaz') + self.assertEqual(resp, ret) + + +class TestObject(TestObjectAPIv1): + + def setUp(self): + super(TestObject, self).setUp() + + def test_object_create(self): + headers = { + 'etag': 'youreit', + 'x-trans-id': '1qaz2wsx', + } + self.requests_mock.register_uri( + 'PUT', + FAKE_URL + '/qaz/requirements.txt', + headers=headers, + status_code=201, + ) + ret = self.api.object_create( + container='qaz', + object='requirements.txt', + ) + data = { + 'account': FAKE_ACCOUNT, + 'container': 'qaz', + 'object': 'requirements.txt', + 'etag': 'youreit', + 'x-trans-id': '1qaz2wsx', + } + self.assertEqual(data, ret) + + def test_object_delete(self): + self.requests_mock.register_uri( + 'DELETE', + FAKE_URL + '/qaz/wsx', + status_code=204, + ) + ret = self.api.object_delete( + container='qaz', + object='wsx', + ) + self.assertIsNone(ret) + + def test_object_list_no_options(self): + self.requests_mock.register_uri( + 'GET', + FAKE_URL + '/qaz', + json=LIST_OBJECT_RESP, + status_code=200, + ) + ret = self.api.object_list(container='qaz') + self.assertEqual(LIST_OBJECT_RESP, ret) + + def test_object_list_delimiter(self): + self.requests_mock.register_uri( + 'GET', + FAKE_URL + '/qaz?delimiter=%7C', + json=LIST_OBJECT_RESP, + status_code=200, + ) + ret = self.api.object_list( + container='qaz', + delimiter='|', + ) + self.assertEqual(LIST_OBJECT_RESP, ret) + + def test_object_list_prefix(self): + self.requests_mock.register_uri( + 'GET', + FAKE_URL + '/qaz?prefix=foo%2f', + json=LIST_OBJECT_RESP, + status_code=200, + ) + ret = self.api.object_list( + container='qaz', + prefix='foo/', + ) + self.assertEqual(LIST_OBJECT_RESP, ret) + + def test_object_list_marker_limit_end(self): + self.requests_mock.register_uri( + 'GET', + FAKE_URL + '/qaz?marker=next&limit=2&end_marker=stop', + json=LIST_CONTAINER_RESP, + status_code=200, + ) + ret = self.api.object_list( + container='qaz', + marker='next', + limit=2, + end_marker='stop', + ) + self.assertEqual(LIST_CONTAINER_RESP, ret) + +# def test_list_objects_full_listing(self): +# sess = self.app.client_manager.session +# +# def side_effect(*args, **kwargs): +# rv = sess.get().json.return_value +# sess.get().json.return_value = [] +# sess.get().json.side_effect = None +# return rv +# +# resp = [{'name': 'is-name'}] +# sess.get().json.return_value = resp +# sess.get().json.side_effect = side_effect +# +# data = lib_object.list_objects( +# sess, +# fake_url, +# fake_container, +# full_listing=True, +# ) +# +# # Check expected values +# sess.get.assert_called_with( +# fake_url + '/' + fake_container, +# params={ +# 'format': 'json', +# 'marker': 'is-name', +# } +# ) +# self.assertEqual(resp, data) + + def test_object_show(self): + headers = { + 'content-type': 'text/alpha', + 'content-length': '577', + 'last-modified': '20130101', + 'etag': 'qaz', + 'x-container-meta-owner': FAKE_ACCOUNT, + 'x-object-meta-wife': 'Wilma', + 'x-tra-header': 'yabba-dabba-do', + } + resp = { + 'account': FAKE_ACCOUNT, + 'container': 'qaz', + 'object': FAKE_OBJECT, + 'content-type': 'text/alpha', + 'content-length': '577', + 'last-modified': '20130101', + 'etag': 'qaz', + 'wife': 'Wilma', + 'x-tra-header': 'yabba-dabba-do', + } + self.requests_mock.register_uri( + 'HEAD', + FAKE_URL + '/qaz/' + FAKE_OBJECT, + headers=headers, + status_code=204, + ) + ret = self.api.object_show( + container='qaz', + object=FAKE_OBJECT, + ) + self.assertEqual(resp, ret) diff --git a/openstackclient/tests/object/v1/lib/__init__.py b/openstackclient/tests/object/v1/lib/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openstackclient/tests/object/v1/lib/test_container.py b/openstackclient/tests/object/v1/lib/test_container.py deleted file mode 100644 index ce70b8356b..0000000000 --- a/openstackclient/tests/object/v1/lib/test_container.py +++ /dev/null @@ -1,207 +0,0 @@ -# Copyright 2013 Nebula Inc. -# -# 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. -# - -"""Test Object API library module""" - -import mock - -from openstackclient.object.v1.lib import container as lib_container -from openstackclient.tests import fakes -from openstackclient.tests.object.v1 import fakes as object_fakes - - -fake_account = 'q12we34r' -fake_auth = '11223344556677889900' -fake_url = 'http://gopher.com/v1/' + fake_account - -fake_container = 'rainbarrel' - - -class FakeClient(object): - def __init__(self, endpoint=None, **kwargs): - self.endpoint = fake_url - self.token = fake_auth - - -class TestContainer(object_fakes.TestObjectv1): - - def setUp(self): - super(TestContainer, self).setUp() - self.app.client_manager.session = mock.MagicMock() - - -class TestContainerList(TestContainer): - - def test_container_list_no_options(self): - resp = [{'name': 'is-name'}] - self.app.client_manager.session.get().json.return_value = resp - - data = lib_container.list_containers( - self.app.client_manager.session, - fake_url, - ) - - # Check expected values - self.app.client_manager.session.get.assert_called_with( - fake_url, - params={ - 'format': 'json', - } - ) - self.assertEqual(resp, data) - - def test_container_list_marker(self): - resp = [{'name': 'is-name'}] - self.app.client_manager.session.get().json.return_value = resp - - data = lib_container.list_containers( - self.app.client_manager.session, - fake_url, - marker='next', - ) - - # Check expected values - self.app.client_manager.session.get.assert_called_with( - fake_url, - params={ - 'format': 'json', - 'marker': 'next', - } - ) - self.assertEqual(resp, data) - - def test_container_list_limit(self): - resp = [{'name': 'is-name'}] - self.app.client_manager.session.get().json.return_value = resp - - data = lib_container.list_containers( - self.app.client_manager.session, - fake_url, - limit=5, - ) - - # Check expected values - self.app.client_manager.session.get.assert_called_with( - fake_url, - params={ - 'format': 'json', - 'limit': 5, - } - ) - self.assertEqual(resp, data) - - def test_container_list_end_marker(self): - resp = [{'name': 'is-name'}] - self.app.client_manager.session.get().json.return_value = resp - - data = lib_container.list_containers( - self.app.client_manager.session, - fake_url, - end_marker='last', - ) - - # Check expected values - self.app.client_manager.session.get.assert_called_with( - fake_url, - params={ - 'format': 'json', - 'end_marker': 'last', - } - ) - self.assertEqual(resp, data) - - def test_container_list_prefix(self): - resp = [{'name': 'is-name'}] - self.app.client_manager.session.get().json.return_value = resp - - data = lib_container.list_containers( - self.app.client_manager.session, - fake_url, - prefix='foo/', - ) - - # Check expected values - self.app.client_manager.session.get.assert_called_with( - fake_url, - params={ - 'format': 'json', - 'prefix': 'foo/', - } - ) - self.assertEqual(resp, data) - - def test_container_list_full_listing(self): - sess = self.app.client_manager.session - - def side_effect(*args, **kwargs): - rv = sess.get().json.return_value - sess.get().json.return_value = [] - sess.get().json.side_effect = None - return rv - - resp = [{'name': 'is-name'}] - sess.get().json.return_value = resp - sess.get().json.side_effect = side_effect - - data = lib_container.list_containers( - self.app.client_manager.session, - fake_url, - full_listing=True, - ) - - # Check expected values - sess.get.assert_called_with( - fake_url, - params={ - 'format': 'json', - 'marker': 'is-name', - } - ) - self.assertEqual(resp, data) - - -class TestContainerShow(TestContainer): - - def test_container_show_no_options(self): - resp = { - 'X-Container-Meta-Owner': fake_account, - 'x-container-object-count': 1, - 'x-container-bytes-used': 577, - } - self.app.client_manager.session.head.return_value = \ - fakes.FakeResponse(headers=resp) - - data = lib_container.show_container( - self.app.client_manager.session, - fake_url, - 'is-name', - ) - - # Check expected values - self.app.client_manager.session.head.assert_called_with( - fake_url + '/is-name', - ) - - data_expected = { - 'account': fake_account, - 'container': 'is-name', - 'object_count': 1, - 'bytes_used': 577, - 'read_acl': None, - 'write_acl': None, - 'sync_to': None, - 'sync_key': None, - } - self.assertEqual(data_expected, data) diff --git a/openstackclient/tests/object/v1/lib/test_object.py b/openstackclient/tests/object/v1/lib/test_object.py deleted file mode 100644 index f96732b468..0000000000 --- a/openstackclient/tests/object/v1/lib/test_object.py +++ /dev/null @@ -1,295 +0,0 @@ -# Copyright 2013 Nebula Inc. -# -# 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. -# - -"""Test Object API library module""" - -import mock - -from openstackclient.object.v1.lib import object as lib_object -from openstackclient.tests import fakes -from openstackclient.tests.object.v1 import fakes as object_fakes - - -fake_account = 'q12we34r' -fake_auth = '11223344556677889900' -fake_url = 'http://gopher.com/v1/' + fake_account - -fake_container = 'rainbarrel' -fake_object = 'raindrop' - - -class FakeClient(object): - def __init__(self, endpoint=None, **kwargs): - self.endpoint = fake_url - self.token = fake_auth - - -class TestObject(object_fakes.TestObjectv1): - - def setUp(self): - super(TestObject, self).setUp() - self.app.client_manager.session = mock.MagicMock() - - -class TestObjectListObjects(TestObject): - - def test_list_objects_no_options(self): - resp = [{'name': 'is-name'}] - self.app.client_manager.session.get().json.return_value = resp - - data = lib_object.list_objects( - self.app.client_manager.session, - fake_url, - fake_container, - ) - - # Check expected values - self.app.client_manager.session.get.assert_called_with( - fake_url + '/' + fake_container, - params={ - 'format': 'json', - } - ) - self.assertEqual(resp, data) - - def test_list_objects_marker(self): - resp = [{'name': 'is-name'}] - self.app.client_manager.session.get().json.return_value = resp - - data = lib_object.list_objects( - self.app.client_manager.session, - fake_url, - fake_container, - marker='next', - ) - - # Check expected values - self.app.client_manager.session.get.assert_called_with( - fake_url + '/' + fake_container, - params={ - 'format': 'json', - 'marker': 'next', - } - ) - self.assertEqual(resp, data) - - def test_list_objects_limit(self): - resp = [{'name': 'is-name'}] - self.app.client_manager.session.get().json.return_value = resp - - data = lib_object.list_objects( - self.app.client_manager.session, - fake_url, - fake_container, - limit=5, - ) - - # Check expected values - self.app.client_manager.session.get.assert_called_with( - fake_url + '/' + fake_container, - params={ - 'format': 'json', - 'limit': 5, - } - ) - self.assertEqual(resp, data) - - def test_list_objects_end_marker(self): - resp = [{'name': 'is-name'}] - self.app.client_manager.session.get().json.return_value = resp - - data = lib_object.list_objects( - self.app.client_manager.session, - fake_url, - fake_container, - end_marker='last', - ) - - # Check expected values - self.app.client_manager.session.get.assert_called_with( - fake_url + '/' + fake_container, - params={ - 'format': 'json', - 'end_marker': 'last', - } - ) - self.assertEqual(resp, data) - - def test_list_objects_delimiter(self): - resp = [{'name': 'is-name'}] - self.app.client_manager.session.get().json.return_value = resp - - data = lib_object.list_objects( - self.app.client_manager.session, - fake_url, - fake_container, - delimiter='|', - ) - - # Check expected values - # NOTE(dtroyer): requests handles the URL encoding and we're - # mocking that so use the otherwise-not-legal - # pipe '|' char in the response. - self.app.client_manager.session.get.assert_called_with( - fake_url + '/' + fake_container, - params={ - 'format': 'json', - 'delimiter': '|', - } - ) - self.assertEqual(resp, data) - - def test_list_objects_prefix(self): - resp = [{'name': 'is-name'}] - self.app.client_manager.session.get().json.return_value = resp - - data = lib_object.list_objects( - self.app.client_manager.session, - fake_url, - fake_container, - prefix='foo/', - ) - - # Check expected values - self.app.client_manager.session.get.assert_called_with( - fake_url + '/' + fake_container, - params={ - 'format': 'json', - 'prefix': 'foo/', - } - ) - self.assertEqual(resp, data) - - def test_list_objects_path(self): - resp = [{'name': 'is-name'}] - self.app.client_manager.session.get().json.return_value = resp - - data = lib_object.list_objects( - self.app.client_manager.session, - fake_url, - fake_container, - path='next', - ) - - # Check expected values - self.app.client_manager.session.get.assert_called_with( - fake_url + '/' + fake_container, - params={ - 'format': 'json', - 'path': 'next', - } - ) - self.assertEqual(resp, data) - - def test_list_objects_full_listing(self): - sess = self.app.client_manager.session - - def side_effect(*args, **kwargs): - rv = sess.get().json.return_value - sess.get().json.return_value = [] - sess.get().json.side_effect = None - return rv - - resp = [{'name': 'is-name'}] - sess.get().json.return_value = resp - sess.get().json.side_effect = side_effect - - data = lib_object.list_objects( - sess, - fake_url, - fake_container, - full_listing=True, - ) - - # Check expected values - sess.get.assert_called_with( - fake_url + '/' + fake_container, - params={ - 'format': 'json', - 'marker': 'is-name', - } - ) - self.assertEqual(resp, data) - - -class TestObjectShowObjects(TestObject): - - def test_object_show_no_options(self): - resp = { - 'content-type': 'text/alpha', - 'x-container-meta-owner': fake_account, - } - self.app.client_manager.session.head.return_value = \ - fakes.FakeResponse(headers=resp) - - data = lib_object.show_object( - self.app.client_manager.session, - fake_url, - fake_container, - fake_object, - ) - - # Check expected values - self.app.client_manager.session.head.assert_called_with( - fake_url + '/%s/%s' % (fake_container, fake_object), - ) - - data_expected = { - 'account': fake_account, - 'container': fake_container, - 'object': fake_object, - 'content-type': 'text/alpha', - } - self.assertEqual(data_expected, data) - - def test_object_show_all_options(self): - resp = { - 'content-type': 'text/alpha', - 'content-length': 577, - 'last-modified': '20130101', - 'etag': 'qaz', - 'x-container-meta-owner': fake_account, - 'x-object-manifest': None, - 'x-object-meta-wife': 'Wilma', - 'x-tra-header': 'yabba-dabba-do', - } - self.app.client_manager.session.head.return_value = \ - fakes.FakeResponse(headers=resp) - - data = lib_object.show_object( - self.app.client_manager.session, - fake_url, - fake_container, - fake_object, - ) - - # Check expected values - self.app.client_manager.session.head.assert_called_with( - fake_url + '/%s/%s' % (fake_container, fake_object), - ) - - data_expected = { - 'account': fake_account, - 'container': fake_container, - 'object': fake_object, - 'content-type': 'text/alpha', - 'content-length': 577, - 'last-modified': '20130101', - 'etag': 'qaz', - 'x-object-manifest': None, - 'wife': 'Wilma', - 'x-tra-header': 'yabba-dabba-do', - } - self.assertEqual(data_expected, data) diff --git a/openstackclient/tests/object/v1/test_container.py b/openstackclient/tests/object/v1/test_container.py index b72c79d653..70b88fc425 100644 --- a/openstackclient/tests/object/v1/test_container.py +++ b/openstackclient/tests/object/v1/test_container.py @@ -16,6 +16,7 @@ import copy import mock +from openstackclient.api import object_store_v1 as object_store from openstackclient.object.v1 import container from openstackclient.tests.object.v1 import fakes as object_fakes @@ -30,28 +31,20 @@ def __init__(self, endpoint=None, **kwargs): self.token = AUTH_TOKEN -class TestObject(object_fakes.TestObjectv1): +class TestContainer(object_fakes.TestObjectv1): def setUp(self): - super(TestObject, self).setUp() - - -class TestObjectClient(TestObject): - - def test_make_client(self): - self.assertEqual( - self.app.client_manager.object_store.endpoint, - AUTH_URL, - ) - self.assertEqual( - self.app.client_manager.object_store.token, - AUTH_TOKEN, + super(TestContainer, self).setUp() + self.app.client_manager.object_store = object_store.APIv1( + session=mock.Mock(), + service_type="object-store", ) + self.api = self.app.client_manager.object_store @mock.patch( - 'openstackclient.object.v1.container.lib_container.list_containers' + 'openstackclient.api.object_store_v1.APIv1.container_list' ) -class TestContainerList(TestObject): +class TestContainerList(TestContainer): def setUp(self): super(TestContainerList, self).setUp() @@ -77,8 +70,6 @@ def test_object_list_containers_no_options(self, c_mock): kwargs = { } c_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, **kwargs ) @@ -113,8 +104,6 @@ def test_object_list_containers_prefix(self, c_mock): 'prefix': 'bit', } c_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, **kwargs ) @@ -134,43 +123,10 @@ def test_object_list_containers_marker(self, c_mock): arglist = [ '--marker', object_fakes.container_name, - ] - verifylist = [ - ('marker', object_fakes.container_name), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # DisplayCommandBase.take_action() returns two tuples - columns, data = self.cmd.take_action(parsed_args) - - # Set expected values - kwargs = { - 'marker': object_fakes.container_name, - } - c_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, - **kwargs - ) - - collist = ('Name',) - self.assertEqual(columns, collist) - datalist = ( - (object_fakes.container_name, ), - (object_fakes.container_name_3, ), - ) - self.assertEqual(tuple(data), datalist) - - def test_object_list_containers_end_marker(self, c_mock): - c_mock.return_value = [ - copy.deepcopy(object_fakes.CONTAINER), - copy.deepcopy(object_fakes.CONTAINER_3), - ] - - arglist = [ '--end-marker', object_fakes.container_name_3, ] verifylist = [ + ('marker', object_fakes.container_name), ('end_marker', object_fakes.container_name_3), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -180,11 +136,10 @@ def test_object_list_containers_end_marker(self, c_mock): # Set expected values kwargs = { + 'marker': object_fakes.container_name, 'end_marker': object_fakes.container_name_3, } c_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, **kwargs ) @@ -218,8 +173,6 @@ def test_object_list_containers_limit(self, c_mock): 'limit': 2, } c_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, **kwargs ) @@ -252,8 +205,6 @@ def test_object_list_containers_long(self, c_mock): kwargs = { } c_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, **kwargs ) @@ -296,8 +247,6 @@ def test_object_list_containers_all(self, c_mock): 'full_listing': True, } c_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, **kwargs ) @@ -312,9 +261,9 @@ def test_object_list_containers_all(self, c_mock): @mock.patch( - 'openstackclient.object.v1.container.lib_container.show_container' + 'openstackclient.api.object_store_v1.APIv1.container_show' ) -class TestContainerShow(TestObject): +class TestContainerShow(TestContainer): def setUp(self): super(TestContainerShow, self).setUp() @@ -341,9 +290,7 @@ def test_container_show(self, c_mock): } # lib.container.show_container(api, url, container) c_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, - object_fakes.container_name, + container=object_fakes.container_name, **kwargs ) diff --git a/openstackclient/tests/object/v1/test_container_all.py b/openstackclient/tests/object/v1/test_container_all.py index 29d78454fa..53b60b9abb 100644 --- a/openstackclient/tests/object/v1/test_container_all.py +++ b/openstackclient/tests/object/v1/test_container_all.py @@ -16,6 +16,7 @@ from requests_mock.contrib import fixture from keystoneclient import session +from openstackclient.api import object_store_v1 as object_store from openstackclient.object.v1 import container from openstackclient.tests.object.v1 import fakes as object_fakes @@ -29,7 +30,10 @@ def setUp(self): self.requests_mock = self.useFixture(fixture.Fixture()) # TODO(dtroyer): move this to object_fakes.TestObjectv1 - self.app.client_manager.object_store.endpoint = object_fakes.ENDPOINT + self.app.client_manager.object_store = object_store.APIv1( + session=self.app.client_manager.session, + endpoint=object_fakes.ENDPOINT, + ) class TestContainerCreate(TestObjectAll): diff --git a/openstackclient/tests/object/v1/test_object.py b/openstackclient/tests/object/v1/test_object.py index 26d07b2ca7..22f06999e2 100644 --- a/openstackclient/tests/object/v1/test_object.py +++ b/openstackclient/tests/object/v1/test_object.py @@ -16,6 +16,7 @@ import copy import mock +from openstackclient.api import object_store_v1 as object_store from openstackclient.object.v1 import object as obj from openstackclient.tests.object.v1 import fakes as object_fakes @@ -27,23 +28,15 @@ class TestObject(object_fakes.TestObjectv1): def setUp(self): super(TestObject, self).setUp() - - -class TestObjectClient(TestObject): - - def test_make_client(self): - self.assertEqual( - self.app.client_manager.object_store.endpoint, - AUTH_URL, - ) - self.assertEqual( - self.app.client_manager.object_store.token, - AUTH_TOKEN, + self.app.client_manager.object_store = object_store.APIv1( + session=mock.Mock(), + service_type="object-store", ) + self.api = self.app.client_manager.object_store @mock.patch( - 'openstackclient.object.v1.object.lib_object.list_objects' + 'openstackclient.api.object_store_v1.APIv1.object_list' ) class TestObjectList(TestObject): @@ -71,9 +64,7 @@ def test_object_list_objects_no_options(self, o_mock): columns, data = self.cmd.take_action(parsed_args) o_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, - object_fakes.container_name, + container=object_fakes.container_name, ) collist = ('Name',) @@ -107,9 +98,7 @@ def test_object_list_objects_prefix(self, o_mock): 'prefix': 'floppy', } o_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, - object_fakes.container_name_2, + container=object_fakes.container_name_2, **kwargs ) @@ -143,9 +132,7 @@ def test_object_list_objects_delimiter(self, o_mock): 'delimiter': '=', } o_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, - object_fakes.container_name_2, + container=object_fakes.container_name_2, **kwargs ) @@ -179,9 +166,7 @@ def test_object_list_objects_marker(self, o_mock): 'marker': object_fakes.object_name_2, } o_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, - object_fakes.container_name_2, + container=object_fakes.container_name_2, **kwargs ) @@ -215,9 +200,7 @@ def test_object_list_objects_end_marker(self, o_mock): 'end_marker': object_fakes.object_name_2, } o_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, - object_fakes.container_name_2, + container=object_fakes.container_name_2, **kwargs ) @@ -251,9 +234,7 @@ def test_object_list_objects_limit(self, o_mock): 'limit': 2, } o_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, - object_fakes.container_name_2, + container=object_fakes.container_name_2, **kwargs ) @@ -287,9 +268,7 @@ def test_object_list_objects_long(self, o_mock): kwargs = { } o_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, - object_fakes.container_name, + container=object_fakes.container_name, **kwargs ) @@ -337,9 +316,7 @@ def test_object_list_objects_all(self, o_mock): 'full_listing': True, } o_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, - object_fakes.container_name, + container=object_fakes.container_name, **kwargs ) @@ -353,7 +330,7 @@ def test_object_list_objects_all(self, o_mock): @mock.patch( - 'openstackclient.object.v1.object.lib_object.show_object' + 'openstackclient.api.object_store_v1.APIv1.object_show' ) class TestObjectShow(TestObject): @@ -384,10 +361,8 @@ def test_object_show(self, c_mock): } # lib.container.show_container(api, url, container) c_mock.assert_called_with( - self.app.client_manager.session, - AUTH_URL, - object_fakes.container_name, - object_fakes.object_name_1, + container=object_fakes.container_name, + object=object_fakes.object_name_1, **kwargs ) From 742982af4bb94b73a78c06688732acf1c8127f8a Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 19 Sep 2014 02:42:55 +0000 Subject: [PATCH 0203/3494] Add functional tests to osc Create a script that kicks off function tests that exercise openstackclient commands against a cloud. If no keystone/openstack process is detected, a devstack instance is spun up and the tests are run against that. There is also a hook added to tox.ini so that we can run these tests easily from a gate job. Change-Id: I3cc8b2b800de7ca74af506d2c7e8ee481fa985f0 --- functional/__init__.py | 0 functional/common/__init__.py | 0 functional/common/exceptions.py | 26 ++++++ functional/common/test.py | 129 ++++++++++++++++++++++++++++++ functional/harpoon.sh | 30 +++++++ functional/harpoonrc | 14 ++++ functional/tests/__init__.py | 0 functional/tests/test_identity.py | 35 ++++++++ post_test_hook.sh | 15 ++++ tox.ini | 4 + 10 files changed, 253 insertions(+) create mode 100644 functional/__init__.py create mode 100644 functional/common/__init__.py create mode 100644 functional/common/exceptions.py create mode 100644 functional/common/test.py create mode 100755 functional/harpoon.sh create mode 100644 functional/harpoonrc create mode 100644 functional/tests/__init__.py create mode 100644 functional/tests/test_identity.py create mode 100755 post_test_hook.sh diff --git a/functional/__init__.py b/functional/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/functional/common/__init__.py b/functional/common/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/functional/common/exceptions.py b/functional/common/exceptions.py new file mode 100644 index 0000000000..47c6071e28 --- /dev/null +++ b/functional/common/exceptions.py @@ -0,0 +1,26 @@ +# 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. + + +class CommandFailed(Exception): + def __init__(self, returncode, cmd, output, stderr): + super(CommandFailed, self).__init__() + self.returncode = returncode + self.cmd = cmd + self.stdout = output + self.stderr = stderr + + def __str__(self): + return ("Command '%s' returned non-zero exit status %d.\n" + "stdout:\n%s\n" + "stderr:\n%s" % (self.cmd, self.returncode, + self.stdout, self.stderr)) diff --git a/functional/common/test.py b/functional/common/test.py new file mode 100644 index 0000000000..c1bb0b101a --- /dev/null +++ b/functional/common/test.py @@ -0,0 +1,129 @@ +# 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 re +import shlex +import subprocess +import testtools + +import six + +from functional.common import exceptions + + +def execute(cmd, action, flags='', params='', fail_ok=False, + merge_stderr=False): + """Executes specified command for the given action.""" + cmd = ' '.join([cmd, flags, action, params]) + cmd = shlex.split(cmd.encode('utf-8')) + result = '' + result_err = '' + stdout = subprocess.PIPE + stderr = subprocess.STDOUT if merge_stderr else subprocess.PIPE + proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr) + result, result_err = proc.communicate() + if not fail_ok and proc.returncode != 0: + raise exceptions.CommandFailed(proc.returncode, cmd, result, + result_err) + return result + + +class TestCase(testtools.TestCase): + + delimiter_line = re.compile('^\+\-[\+\-]+\-\+$') + + def openstack(self, action, flags='', params='', fail_ok=False): + """Executes openstackclient command for the given action.""" + return execute('openstack', action, flags, params, fail_ok) + + def assert_table_structure(self, items, field_names): + """Verify that all items have keys listed in field_names.""" + for item in items: + for field in field_names: + self.assertIn(field, item) + + def assert_show_fields(self, items, field_names): + """Verify that all items have keys listed in field_names.""" + for item in items: + for key in six.iterkeys(item): + self.assertIn(key, field_names) + + def parse_show(self, raw_output): + """Return list of dicts with item values parsed from cli output.""" + + items = [] + table_ = self.table(raw_output) + for row in table_['values']: + item = {} + item[row[0]] = row[1] + items.append(item) + return items + + def parse_listing(self, raw_output): + """Return list of dicts with basic item parsed from cli output.""" + + items = [] + table_ = self.table(raw_output) + for row in table_['values']: + item = {} + for col_idx, col_key in enumerate(table_['headers']): + item[col_key] = row[col_idx] + items.append(item) + return items + + def table(self, output_lines): + """Parse single table from cli output. + + Return dict with list of column names in 'headers' key and + rows in 'values' key. + """ + table_ = {'headers': [], 'values': []} + columns = None + + if not isinstance(output_lines, list): + output_lines = output_lines.split('\n') + + if not output_lines[-1]: + # skip last line if empty (just newline at the end) + output_lines = output_lines[:-1] + + for line in output_lines: + if self.delimiter_line.match(line): + columns = self._table_columns(line) + continue + if '|' not in line: + continue + row = [] + for col in columns: + row.append(line[col[0]:col[1]].strip()) + if table_['headers']: + table_['values'].append(row) + else: + table_['headers'] = row + + return table_ + + def _table_columns(self, first_table_row): + """Find column ranges in output line. + + Return list of tuples (start,end) for each column + detected by plus (+) characters in delimiter line. + """ + positions = [] + start = 1 # there is '+' at 0 + while start < len(first_table_row): + end = first_table_row.find('+', start) + if end == -1: + break + positions.append((start, end)) + start = end + 1 + return positions diff --git a/functional/harpoon.sh b/functional/harpoon.sh new file mode 100755 index 0000000000..76c10ffb95 --- /dev/null +++ b/functional/harpoon.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +FUNCTIONAL_TEST_DIR=$(cd $(dirname "$0") && pwd) +source $FUNCTIONAL_TEST_DIR/harpoonrc + +OPENSTACKCLIENT_DIR=$FUNCTIONAL_TEST_DIR/.. + +if [[ -z $DEVSTACK_DIR ]]; then + echo "guessing location of devstack" + DEVSTACK_DIR=$OPENSTACKCLIENT_DIR/../devstack +fi + +function setup_credentials { + RC_FILE=$DEVSTACK_DIR/accrc/$HARPOON_USER/$HARPOON_TENANT + source $RC_FILE + echo 'sourcing' $RC_FILE + echo 'running tests with' + env | grep OS +} + +function run_tests { + cd $FUNCTIONAL_TEST_DIR + python -m testtools.run discover + rvalue=$? + cd $OPENSTACKCLIENT_DIR + exit $rvalue +} + +setup_credentials +run_tests diff --git a/functional/harpoonrc b/functional/harpoonrc new file mode 100644 index 0000000000..ed9201ca1e --- /dev/null +++ b/functional/harpoonrc @@ -0,0 +1,14 @@ +# Global options +#RECLONE=yes + +# Devstack options +#ADMIN_PASSWORD=openstack +#MYSQL_PASSWORD=openstack +#RABBIT_PASSWORD=openstack +#SERVICE_TOKEN=openstack +#SERVICE_PASSWORD=openstack + +# Harpoon options +HARPOON_USER=admin +HARPOON_TENANT=admin +#DEVSTACK_DIR=/opt/stack/devstack diff --git a/functional/tests/__init__.py b/functional/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/functional/tests/test_identity.py b/functional/tests/test_identity.py new file mode 100644 index 0000000000..5f8b4cb09c --- /dev/null +++ b/functional/tests/test_identity.py @@ -0,0 +1,35 @@ +# 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 functional.common import exceptions +from functional.common import test + + +class IdentityV2Tests(test.TestCase): + """Functional tests for Identity V2 commands. """ + + def test_user_list(self): + field_names = ['ID', 'Name'] + raw_output = self.openstack('user list') + items = self.parse_listing(raw_output) + self.assert_table_structure(items, field_names) + + def test_user_get(self): + field_names = ['email', 'enabled', 'id', 'name', + 'project_id', 'username'] + raw_output = self.openstack('user show admin') + items = self.parse_show(raw_output) + self.assert_show_fields(items, field_names) + + def test_bad_user_command(self): + self.assertRaises(exceptions.CommandFailed, + self.openstack, 'user unlist') diff --git a/post_test_hook.sh b/post_test_hook.sh new file mode 100755 index 0000000000..b82c1e62a9 --- /dev/null +++ b/post_test_hook.sh @@ -0,0 +1,15 @@ +#!/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/ + +set -xe + +OPENSTACKCLIENT_DIR=$(cd $(dirname "$0") && pwd) + +cd $OPENSTACKCLIENT_DIR +echo "Running openstackclient functional test suite" +sudo -H -u stack tox -e functional diff --git a/tox.ini b/tox.ini index 42cf42417b..b9493bc598 100644 --- a/tox.ini +++ b/tox.ini @@ -11,10 +11,14 @@ setenv = VIRTUAL_ENV={envdir} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = python setup.py testr --testr-args='{posargs}' +whitelist_externals = bash [testenv:pep8] commands = flake8 +[testenv:functional] +commands = bash -x {toxinidir}/functional/harpoon.sh + [testenv:venv] commands = {posargs} From 3842960f7189903f1ea5ab432cedc3ca803390d4 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 30 Sep 2014 18:28:01 -0400 Subject: [PATCH 0204/3494] Create a whole slew of functional tests for identity Complete the remaining identity v2 and v3 functional tests Change-Id: I193fd95e58a38caeb66d37c17cde75b983c48ca0 --- functional/tests/test_identity.py | 118 ++++++++++++++++++++++++++++-- 1 file changed, 112 insertions(+), 6 deletions(-) diff --git a/functional/tests/test_identity.py b/functional/tests/test_identity.py index 5f8b4cb09c..cdb0ed341b 100644 --- a/functional/tests/test_identity.py +++ b/functional/tests/test_identity.py @@ -10,26 +10,132 @@ # License for the specific language governing permissions and limitations # under the License. +import os +import uuid + from functional.common import exceptions from functional.common import test +BASIC_LIST_HEADERS = ['ID', 'Name'] + class IdentityV2Tests(test.TestCase): """Functional tests for Identity V2 commands. """ + USER_FIELDS = ['email', 'enabled', 'id', 'name', 'project_id', 'username'] + PROJECT_FIELDS = ['enabled', 'id', 'name', 'description'] + def test_user_list(self): - field_names = ['ID', 'Name'] raw_output = self.openstack('user list') items = self.parse_listing(raw_output) - self.assert_table_structure(items, field_names) + self.assert_table_structure(items, BASIC_LIST_HEADERS) - def test_user_get(self): - field_names = ['email', 'enabled', 'id', 'name', - 'project_id', 'username'] + def test_user_show(self): raw_output = self.openstack('user show admin') items = self.parse_show(raw_output) - self.assert_show_fields(items, field_names) + self.assert_show_fields(items, self.USER_FIELDS) + + def test_user_create(self): + raw_output = self.openstack('user create mjordan --password bulls' + ' --email hoops@example.com') + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.USER_FIELDS) + + def test_user_delete(self): + self.openstack('user create dummy') + raw_output = self.openstack('user delete dummy') + self.assertEqual(0, len(raw_output)) def test_bad_user_command(self): self.assertRaises(exceptions.CommandFailed, self.openstack, 'user unlist') + + def test_project_list(self): + raw_output = self.openstack('project list') + items = self.parse_listing(raw_output) + self.assert_table_structure(items, BASIC_LIST_HEADERS) + + def test_project_show(self): + raw_output = self.openstack('project show admin') + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.PROJECT_FIELDS) + + def test_project_create(self): + raw_output = self.openstack('project create test-project') + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.PROJECT_FIELDS) + + def test_project_delete(self): + self.openstack('project create dummy-project') + raw_output = self.openstack('project delete dummy-project') + self.assertEqual(0, len(raw_output)) + + +class IdentityV3Tests(test.TestCase): + """Functional tests for Identity V3 commands. """ + + DOMAIN_FIELDS = ['description', 'enabled', 'id', 'name', 'links'] + GROUP_FIELDS = ['description', 'domain_id', 'id', 'name', 'links'] + + def _create_dummy_group(self): + name = uuid.uuid4().hex + self.openstack('group create ' + name) + return name + + def _create_dummy_domain(self): + name = uuid.uuid4().hex + self.openstack('domain create ' + name) + return name + + def setUp(self): + super(IdentityV3Tests, self).setUp() + auth_url = os.environ.get('OS_AUTH_URL') + auth_url = auth_url.replace('v2.0', 'v3') + os.environ['OS_AUTH_URL'] = auth_url + os.environ['OS_IDENTITY_API_VERSION'] = '3' + os.environ['OS_USER_DOMAIN_ID'] = 'default' + os.environ['OS_PROJECT_DOMAIN_ID'] = 'default' + + def test_group_create(self): + raw_output = self.openstack('group create ' + uuid.uuid4().hex) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.GROUP_FIELDS) + + def test_group_list(self): + self._create_dummy_group() + raw_output = self.openstack('group list') + items = self.parse_listing(raw_output) + self.assert_table_structure(items, BASIC_LIST_HEADERS) + + def test_group_delete(self): + name = self._create_dummy_group() + raw_output = self.openstack('group delete ' + name) + self.assertEqual(0, len(raw_output)) + + def test_group_show(self): + name = self._create_dummy_group() + raw_output = self.openstack('group show ' + name) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.GROUP_FIELDS) + + def test_domain_create(self): + raw_output = self.openstack('domain create ' + uuid.uuid4().hex) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.DOMAIN_FIELDS) + + def test_domain_list(self): + self._create_dummy_domain() + raw_output = self.openstack('domain list') + items = self.parse_listing(raw_output) + self.assert_table_structure(items, BASIC_LIST_HEADERS) + + def test_domain_delete(self): + name = self._create_dummy_domain() + self.assertRaises(exceptions.CommandFailed, + self.openstack, 'domain delete ' + name) + + def test_domain_show(self): + name = self._create_dummy_domain() + raw_output = self.openstack('domain show ' + name) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.DOMAIN_FIELDS) From d972b8364c891b8275434c231bcb76e56172f573 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 2 Oct 2014 13:12:41 -0400 Subject: [PATCH 0205/3494] Pass in domain and project as positional args, not kwargs The signature for users.set in keystoneclient dictates that domain and project be sent in, not domainId and projectId, which are being incorrectly sent in as 'extra' data. Closes-Bug: #1376833 Change-Id: I44df3e492f61eab2241f3758dee622417bb6f399 --- openstackclient/identity/v3/user.py | 4 ++-- openstackclient/tests/identity/v3/test_user.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 6ba54368a8..e4eb7526c2 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -308,11 +308,11 @@ def take_action(self, parsed_args): if parsed_args.project: project_id = utils.find_resource( identity_client.projects, parsed_args.project).id - kwargs['projectId'] = project_id + kwargs['project'] = project_id if parsed_args.domain: domain_id = utils.find_resource( identity_client.domains, parsed_args.domain).id - kwargs['domainId'] = domain_id + kwargs['domain'] = domain_id kwargs['enabled'] = user.enabled if parsed_args.enable: kwargs['enabled'] = True diff --git a/openstackclient/tests/identity/v3/test_user.py b/openstackclient/tests/identity/v3/test_user.py index 42df57736e..bb59ebe568 100644 --- a/openstackclient/tests/identity/v3/test_user.py +++ b/openstackclient/tests/identity/v3/test_user.py @@ -842,7 +842,7 @@ def test_user_set_domain(self): # Set expected values kwargs = { 'enabled': True, - 'domainId': identity_fakes.domain_id, + 'domain': identity_fakes.domain_id, } # UserManager.update(user, name=, domain=, project=, password=, # email=, description=, enabled=, default_project=) @@ -874,7 +874,7 @@ def test_user_set_project(self): # Set expected values kwargs = { 'enabled': True, - 'projectId': identity_fakes.project_id, + 'project': identity_fakes.project_id, } # UserManager.update(user, name=, domain=, project=, password=, # email=, description=, enabled=, default_project=) From 693687e4ffebd38089cc55a168c743bf48df5603 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 2 Oct 2014 23:09:34 -0400 Subject: [PATCH 0206/3494] Remove duplicate env function in shell.py There already exists an env() function in utils. Let's use that one since it's common. Change-Id: I661984394cf0c0543b2f35bf76e3929dead54d1d --- openstackclient/shell.py | 37 ++++++++++++------------------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 0c91ab6ef1..8db1656c85 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -18,7 +18,6 @@ import argparse import getpass import logging -import os import sys import traceback @@ -38,20 +37,6 @@ DEFAULT_DOMAIN = 'default' -def env(*vars, **kwargs): - """Search for the first defined of possibly many env vars - - Returns the first environment variable defined in vars, or - returns the default defined in kwargs. - - """ - for v in vars: - value = os.environ.get(v, None) - if value: - return value - return kwargs.get('default', '') - - class OpenStackShell(app.App): CONSOLE_MESSAGE_FORMAT = '%(levelname)s: %(name)s %(message)s' @@ -191,26 +176,27 @@ def build_option_parser(self, description, version): parser.add_argument( '--os-auth-url', metavar='', - default=env('OS_AUTH_URL'), + default=utils.env('OS_AUTH_URL'), help='Authentication URL (Env: OS_AUTH_URL)') parser.add_argument( '--os-domain-name', metavar='', - default=env('OS_DOMAIN_NAME'), + default=utils.env('OS_DOMAIN_NAME'), help='Domain name of the requested domain-level ' 'authorization scope (Env: OS_DOMAIN_NAME)', ) parser.add_argument( '--os-domain-id', metavar='', - default=env('OS_DOMAIN_ID'), + default=utils.env('OS_DOMAIN_ID'), help='Domain ID of the requested domain-level ' 'authorization scope (Env: OS_DOMAIN_ID)', ) parser.add_argument( '--os-project-name', metavar='', - default=env('OS_PROJECT_NAME', default=env('OS_TENANT_NAME')), + default=utils.env('OS_PROJECT_NAME', + default=utils.env('OS_TENANT_NAME')), help='Project name of the requested project-level ' 'authorization scope (Env: OS_PROJECT_NAME)', ) @@ -223,7 +209,8 @@ def build_option_parser(self, description, version): parser.add_argument( '--os-project-id', metavar='', - default=env('OS_PROJECT_ID', default=env('OS_TENANT_ID')), + default=utils.env('OS_PROJECT_ID', + default=utils.env('OS_TENANT_ID')), help='Project ID of the requested project-level ' 'authorization scope (Env: OS_PROJECT_ID)', ) @@ -270,12 +257,12 @@ def build_option_parser(self, description, version): parser.add_argument( '--os-region-name', metavar='', - default=env('OS_REGION_NAME'), + default=utils.env('OS_REGION_NAME'), help='Authentication region name (Env: OS_REGION_NAME)') parser.add_argument( '--os-cacert', metavar='', - default=env('OS_CACERT'), + default=utils.env('OS_CACERT'), help='CA certificate bundle file (Env: OS_CACERT)') verify_group = parser.add_mutually_exclusive_group() verify_group.add_argument( @@ -291,7 +278,7 @@ def build_option_parser(self, description, version): parser.add_argument( '--os-default-domain', metavar='', - default=env( + default=utils.env( 'OS_DEFAULT_DOMAIN', default=DEFAULT_DOMAIN), help='Default domain ID, default=' + @@ -300,12 +287,12 @@ def build_option_parser(self, description, version): parser.add_argument( '--os-token', metavar='', - default=env('OS_TOKEN'), + default=utils.env('OS_TOKEN'), help='Defaults to env[OS_TOKEN]') parser.add_argument( '--os-url', metavar='', - default=env('OS_URL'), + default=utils.env('OS_URL'), help='Defaults to env[OS_URL]') parser.add_argument( '--timing', From 1934b1b24347fbe528c02fd8eafcd4c569e96763 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 3 Oct 2014 00:09:59 -0400 Subject: [PATCH 0207/3494] Place the command to generate docs on one line Change-Id: I99d78208c940bc6646327ee967e71187c32a159f --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 42cf42417b..2c3fb69073 100644 --- a/tox.ini +++ b/tox.ini @@ -25,8 +25,7 @@ commands = python setup.py test --coverage --testr-args='{posargs}' downloadcache = ~/cache/pip [testenv:docs] -commands= - python setup.py build_sphinx +commands = python setup.py build_sphinx [flake8] ignore = H305,H307,H402,H904 From 89bb5b0b8528ac793b9fc531a24ba709f73a8716 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 3 Oct 2014 00:25:56 -0400 Subject: [PATCH 0208/3494] Add some code-blocks to the docs Add some basic highlighting for the docs Change-Id: Ifa740856f3ef636bdf0f60f3b7d082c68062fe9b --- doc/source/commands.rst | 8 +++++--- doc/source/plugins.rst | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 45e73a0594..8857f3d55d 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -47,11 +47,13 @@ objects. In badly formed English it is expressed as "(Take) object1 -Examples:: +Examples: - group add user +.. code-block:: bash - volume type list # 'volume type' is a two-word single object + $ group add user + + $ volume type list # 'volume type' is a two-word single object Command Arguments and Options diff --git a/doc/source/plugins.rst b/doc/source/plugins.rst index 3b35ec08e8..0635f29ebe 100644 --- a/doc/source/plugins.rst +++ b/doc/source/plugins.rst @@ -14,7 +14,7 @@ Plugins are discovered by enumerating the entry points found under :py:mod:`openstack.cli.extension` and initializing the specified client module. -:: +.. code-block:: ini [entry_points] openstack.cli.extension = @@ -39,7 +39,7 @@ The client module must implement the following interface functions: OSC enumerates the plugin commands from the entry points in the usual manner defined for the API version: -:: +.. code-block:: ini openstack.oscplugin.v1 = plugin_list = oscplugin.v1.plugin:ListPlugin @@ -48,7 +48,7 @@ defined for the API version: Note that OSC defines the group name as :py:mod:`openstack..v` so the version should not contain the leading 'v' character. -:: +.. code-block:: python DEFAULT_OSCPLUGIN_API_VERSION = '1' From 0cb204e59b20f25b7a054b411507d6dabbc699ac Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 3 Oct 2014 19:35:13 -0400 Subject: [PATCH 0209/3494] Update gitignore add .project and .pydevproject to gitignore Change-Id: Ic258ded80612d31bd3017fce65000b619026e844 --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index a8d9f3d69d..84079f2f9f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ build ChangeLog dist doc/build +# Development environment files +.project +.pydevproject From 388bbbac2ce6bf9baf2f9ceb6102b0b1f7072264 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 6 Oct 2014 03:37:46 -0400 Subject: [PATCH 0210/3494] Fix issues with object related commands 1) Can't create instance of swiftclient. Since we now create an API instance, creating a swiftclient instance won't work. Trying to do any object related command fails. 2) Listing objects in a container fails, we depend on the data returned in a specific way, during the API transition this must have slipped through. Needs regression/funcitonal tests to mame sure this doesn't happen again. Change-Id: I69079a0dc9f32b84e6f9307729d3dbbba549ac5e --- functional/tests/test_object.py | 91 ++++++++++++++++++++++++++ openstackclient/api/object_store_v1.py | 5 +- openstackclient/object/client.py | 6 -- 3 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 functional/tests/test_object.py diff --git a/functional/tests/test_object.py b/functional/tests/test_object.py new file mode 100644 index 0000000000..396f0cb7ac --- /dev/null +++ b/functional/tests/test_object.py @@ -0,0 +1,91 @@ +# 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 uuid + +from functional.common import test + +BASIC_LIST_HEADERS = ['Name'] +CONTAINER_FIELDS = ['account', 'container', 'x-trans-id'] +OBJECT_FIELDS = ['object', 'container', 'etag'] + + +class ObjectV1Tests(test.TestCase): + """Functional tests for Object V1 commands. """ + + CONTAINER_NAME = uuid.uuid4().hex + OBJECT_NAME = uuid.uuid4().hex + + # NOTE(stevemar): Not using setUp since we only want this to run once + with open(OBJECT_NAME, 'w') as f: + f.write('test content') + + def test_container_create(self): + raw_output = self.openstack('container create ' + self.CONTAINER_NAME) + items = self.parse_listing(raw_output) + self.assert_show_fields(items, CONTAINER_FIELDS) + + def test_container_delete(self): + container_tmp = uuid.uuid4().hex + self.openstack('container create ' + container_tmp) + raw_output = self.openstack('container delete ' + container_tmp) + self.assertEqual(0, len(raw_output)) + + def test_container_list(self): + raw_output = self.openstack('container list') + items = self.parse_listing(raw_output) + self.assert_table_structure(items, BASIC_LIST_HEADERS) + + def test_container_show(self): + self.openstack('container show ' + self.CONTAINER_NAME) + # TODO(stevemar): Assert returned fields + + def test_container_save(self): + self.openstack('container save ' + self.CONTAINER_NAME) + # TODO(stevemar): Assert returned fields + + def test_object_create(self): + raw_output = self.openstack('object create ' + self.CONTAINER_NAME + + ' ' + self.OBJECT_NAME) + items = self.parse_listing(raw_output) + self.assert_show_fields(items, OBJECT_FIELDS) + + def test_object_delete(self): + raw_output = self.openstack('object delete ' + self.CONTAINER_NAME + + ' ' + self.OBJECT_NAME) + self.assertEqual(0, len(raw_output)) + + def test_object_list(self): + raw_output = self.openstack('object list ' + self.CONTAINER_NAME) + items = self.parse_listing(raw_output) + self.assert_table_structure(items, BASIC_LIST_HEADERS) + + def test_object_save(self): + self.openstack('object create ' + self.CONTAINER_NAME + + ' ' + self.OBJECT_NAME) + self.openstack('object save ' + self.CONTAINER_NAME + + ' ' + self.OBJECT_NAME) + # TODO(stevemar): Assert returned fields + + def test_object_save_with_filename(self): + self.openstack('object create ' + self.CONTAINER_NAME + + ' ' + self.OBJECT_NAME) + self.openstack('object save ' + self.CONTAINER_NAME + + ' ' + self.OBJECT_NAME + ' --file tmp.txt') + # TODO(stevemar): Assert returned fields + + def test_object_show(self): + self.openstack('object create ' + self.CONTAINER_NAME + + ' ' + self.OBJECT_NAME) + self.openstack('object show ' + self.CONTAINER_NAME + + ' ' + self.OBJECT_NAME) + # TODO(stevemar): Assert returned fields diff --git a/openstackclient/api/object_store_v1.py b/openstackclient/api/object_store_v1.py index f938b55ed5..57db906398 100644 --- a/openstackclient/api/object_store_v1.py +++ b/openstackclient/api/object_store_v1.py @@ -252,6 +252,7 @@ def object_list( if container is None or object is None: return None + params['format'] = 'json' if all_data: data = listing = self.object_list( container=container, @@ -280,7 +281,6 @@ def object_list( data.extend(listing) return data - params = {} if limit: params['limit'] = limit if marker: @@ -320,7 +320,8 @@ def object_save( ) if response.status_code == 200: if not os.path.exists(os.path.dirname(file)): - os.makedirs(os.path.dirname(file)) + 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(): f.write(chunk) diff --git a/openstackclient/object/client.py b/openstackclient/object/client.py index 887aa85b8e..1ac905c33e 100644 --- a/openstackclient/object/client.py +++ b/openstackclient/object/client.py @@ -33,12 +33,6 @@ def make_client(instance): """Returns an object-store API client.""" - object_client = utils.get_client_class( - API_NAME, - instance._api_version[API_NAME], - API_VERSIONS) - LOG.debug('Instantiating object client: %s', object_client) - if instance._url: endpoint = instance._url else: From 30b0a41ce70d39c5f650b6a2889b9e46a0930dd3 Mon Sep 17 00:00:00 2001 From: Marek Denis Date: Thu, 10 Apr 2014 18:43:51 +0200 Subject: [PATCH 0211/3494] Implement CRUD operations for Mapping objects Change-Id: I4b8f2e77e741cf74f50aba98ab975af7321b02c6 Implements: bp/add-openstackclient-federation-crud --- openstackclient/identity/v3/mapping.py | 209 +++++++++++++++ openstackclient/tests/identity/v3/fakes.py | 61 +++++ .../tests/identity/v3/test_mappings.py | 239 ++++++++++++++++++ setup.cfg | 6 + 4 files changed, 515 insertions(+) create mode 100644 openstackclient/identity/v3/mapping.py create mode 100644 openstackclient/tests/identity/v3/test_mappings.py diff --git a/openstackclient/identity/v3/mapping.py b/openstackclient/identity/v3/mapping.py new file mode 100644 index 0000000000..ae5e03bd2d --- /dev/null +++ b/openstackclient/identity/v3/mapping.py @@ -0,0 +1,209 @@ +# Copyright 2014 CERN +# +# 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. +# + +"""Identity v3 federation mapping action implementations""" + +import json +import logging + +from cliff import command +from cliff import lister +from cliff import show +import six + +from openstackclient.common import exceptions +from openstackclient.common import utils + + +class _RulesReader(object): + """Helper class capable of reading rules from files""" + + def _read_rules(self, path): + """Read and parse rules from path + + Expect the file to contain a valid JSON structure. + + :param path: path to the file + :return: loaded and valid dictionary with rules + :raises exception.CommandError: In case the file cannot be + accessed or the content is not a valid JSON. + + Example of the content of the file: + [ + { + "local": [ + { + "group": { + "id": "85a868" + } + } + ], + "remote": [ + { + "type": "orgPersonType", + "any_one_of": [ + "Employee" + ] + }, + { + "type": "sn", + "any_one_of": [ + "Young" + ] + } + ] + } + ] + + """ + blob = utils.read_blob_file_contents(path) + try: + rules = json.loads(blob) + except ValueError as e: + raise exceptions.CommandError( + 'An error occurred when reading ' + 'rules from file %s: %s' % (path, e)) + else: + return rules + + +class CreateMapping(show.ShowOne, _RulesReader): + """Create new federation mapping""" + + log = logging.getLogger(__name__ + '.CreateMapping') + + def get_parser(self, prog_name): + parser = super(CreateMapping, self).get_parser(prog_name) + parser.add_argument( + 'mapping', + metavar='', + help='New mapping (must be unique)', + ) + parser.add_argument( + '--rules', + metavar='', required=True, + help='Filename with rules', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity + + rules = self._read_rules(parsed_args.rules) + mapping = identity_client.federation.mappings.create( + mapping_id=parsed_args.mapping, + rules=rules) + + info = {} + info.update(mapping._info) + return zip(*sorted(six.iteritems(info))) + + +class DeleteMapping(command.Command): + """Delete federation mapping""" + + log = logging.getLogger(__name__ + '.DeleteMapping') + + def get_parser(self, prog_name): + parser = super(DeleteMapping, self).get_parser(prog_name) + parser.add_argument( + 'mapping', + metavar='', + help='Mapping to delete', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity + + identity_client.federation.mappings.delete(parsed_args.mapping) + return + + +class ListMapping(lister.Lister): + """List federation mappings""" + log = logging.getLogger(__name__ + '.ListMapping') + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + # NOTE(marek-denis): Since rules can be long and tedious I have decided + # to only list ids of the mappings. If somebody wants to check the + # rules, (s)he should show specific ones. + identity_client = self.app.client_manager.identity + data = identity_client.federation.mappings.list() + columns = ('ID',) + items = [utils.get_item_properties(s, columns) for s in data] + return (columns, items) + + +class SetMapping(show.ShowOne, _RulesReader): + """Update federation mapping""" + + log = logging.getLogger(__name__ + '.SetMapping') + + def get_parser(self, prog_name): + parser = super(SetMapping, self).get_parser(prog_name) + parser.add_argument( + 'mapping', + metavar='', + help='Mapping to update.', + ) + parser.add_argument( + '--rules', + metavar='', required=True, + help='Filename with rules', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity + + rules = self._read_rules(parsed_args.rules) + + mapping = identity_client.federation.mappings.update( + mapping=parsed_args.mapping, + rules=rules) + + info = {} + info.update(mapping._info) + return zip(*sorted(six.iteritems(info))) + + +class ShowMapping(show.ShowOne): + """Show federation mapping details""" + + log = logging.getLogger(__name__ + '.ShowMapping') + + def get_parser(self, prog_name): + parser = super(ShowMapping, self).get_parser(prog_name) + parser.add_argument( + 'mapping', + metavar='', + help='Mapping to show', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)' % parsed_args) + identity_client = self.app.client_manager.identity + + mapping = identity_client.federation.mappings.get(parsed_args.mapping) + + info = {} + info.update(mapping._info) + return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index e9cda9ffbc..a88a905e6f 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -38,6 +38,65 @@ 'name': group_name, } +mapping_id = 'test_mapping' +mapping_rules_file_path = '/tmp/path/to/file' +# Copied from +# (https://github.com/openstack/keystone/blob\ +# master/keystone/tests/mapping_fixtures.py +EMPLOYEE_GROUP_ID = "0cd5e9" +DEVELOPER_GROUP_ID = "xyz" +MAPPING_RULES = [ + { + "local": [ + { + "group": { + "id": EMPLOYEE_GROUP_ID + } + } + ], + "remote": [ + { + "type": "orgPersonType", + "not_any_of": [ + "Contractor", + "Guest" + ] + } + ] + } +] + +MAPPING_RULES_2 = [ + { + "local": [ + { + "group": { + "id": DEVELOPER_GROUP_ID + } + } + ], + "remote": [ + { + "type": "orgPersonType", + "any_one_of": [ + "Contractor" + ] + } + ] + } +] + + +MAPPING_RESPONSE = { + "id": mapping_id, + "rules": MAPPING_RULES +} + +MAPPING_RESPONSE_2 = { + "id": mapping_id, + "rules": MAPPING_RULES_2 +} + project_id = '8-9-64' project_name = 'beatles' project_description = 'Fab Four' @@ -224,6 +283,8 @@ class FakeFederationManager(object): def __init__(self, **kwargs): self.identity_providers = mock.Mock() self.identity_providers.resource_class = fakes.FakeResource(None, {}) + self.mappings = mock.Mock() + self.mappings.resource_class = fakes.FakeResource(None, {}) class FakeFederatedClient(FakeIdentityv3Client): diff --git a/openstackclient/tests/identity/v3/test_mappings.py b/openstackclient/tests/identity/v3/test_mappings.py new file mode 100644 index 0000000000..f5c318fefd --- /dev/null +++ b/openstackclient/tests/identity/v3/test_mappings.py @@ -0,0 +1,239 @@ +# Copyright 2014 CERN. +# +# 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 copy + +import mock + +from openstackclient.common import exceptions +from openstackclient.identity.v3 import mapping +from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes + + +class TestMapping(identity_fakes.TestFederatedIdentity): + + def setUp(self): + super(TestMapping, self).setUp() + + federation_lib = self.app.client_manager.identity.federation + self.mapping_mock = federation_lib.mappings + self.mapping_mock.reset_mock() + + +class TestMappingCreate(TestMapping): + def setUp(self): + super(TestMappingCreate, self).setUp() + self.mapping_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.MAPPING_RESPONSE), + loaded=True + ) + self.cmd = mapping.CreateMapping(self.app, None) + + def test_create_mapping(self): + arglist = [ + '--rules', identity_fakes.mapping_rules_file_path, + identity_fakes.mapping_id + ] + verifylist = [ + ('mapping', identity_fakes.mapping_id), + ('rules', identity_fakes.mapping_rules_file_path) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + mocker = mock.Mock() + mocker.return_value = identity_fakes.MAPPING_RULES + with mock.patch("openstackclient.identity.v3.mapping." + "CreateMapping._read_rules", mocker): + columns, data = self.cmd.take_action(parsed_args) + + self.mapping_mock.create.assert_called_with( + mapping_id=identity_fakes.mapping_id, + rules=identity_fakes.MAPPING_RULES) + + collist = ('id', 'rules') + self.assertEqual(columns, collist) + + datalist = (identity_fakes.mapping_id, + identity_fakes.MAPPING_RULES) + self.assertEqual(datalist, data) + + +class TestMappingDelete(TestMapping): + def setUp(self): + super(TestMappingDelete, self).setUp() + self.mapping_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.MAPPING_RESPONSE), + loaded=True) + + self.mapping_mock.delete.return_value = None + self.cmd = mapping.DeleteMapping(self.app, None) + + def test_delete_mapping(self): + arglist = [ + identity_fakes.mapping_id + ] + verifylist = [ + ('mapping', identity_fakes.mapping_id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.mapping_mock.delete.assert_called_with( + identity_fakes.mapping_id) + + +class TestMappingList(TestMapping): + def setUp(self): + super(TestMappingList, self).setUp() + self.mapping_mock.get.return_value = fakes.FakeResource( + None, + {'id': identity_fakes.mapping_id}, + loaded=True) + # Pretend list command returns list of two mappings. + # NOTE(marek-denis): We are returning FakeResources with mapping id + # only as ShowMapping class is implemented in a way where rules will + # not be displayed, only mapping ids. + self.mapping_mock.list.return_value = [ + fakes.FakeResource( + None, + {'id': identity_fakes.mapping_id}, + loaded=True, + ), + fakes.FakeResource( + None, + {'id': 'extra_mapping'}, + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = mapping.ListMapping(self.app, None) + + def test_mapping_list(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.mapping_mock.list.assert_called_with() + + collist = ('ID',) + self.assertEqual(columns, collist) + + datalist = [(identity_fakes.mapping_id,), ('extra_mapping',)] + self.assertEqual(datalist, data) + + +class TestMappingShow(TestMapping): + def setUp(self): + super(TestMappingShow, self).setUp() + + self.mapping_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.MAPPING_RESPONSE), + loaded=True + ) + + self.cmd = mapping.ShowMapping(self.app, None) + + def test_mapping_show(self): + arglist = [ + identity_fakes.mapping_id + ] + verifylist = [ + ('mapping', identity_fakes.mapping_id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.mapping_mock.get.assert_called_with( + identity_fakes.mapping_id) + + collist = ('id', 'rules') + self.assertEqual(columns, collist) + + datalist = (identity_fakes.mapping_id, + identity_fakes.MAPPING_RULES) + self.assertEqual(datalist, data) + + +class TestMappingSet(TestMapping): + + def setUp(self): + super(TestMappingSet, self).setUp() + + self.mapping_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.MAPPING_RESPONSE), + loaded=True + ) + + self.mapping_mock.update.return_value = fakes.FakeResource( + None, + identity_fakes.MAPPING_RESPONSE_2, + loaded=True + ) + + # Get the command object to test + self.cmd = mapping.SetMapping(self.app, None) + + def test_set_new_rules(self): + arglist = [ + '--rules', identity_fakes.mapping_rules_file_path, + identity_fakes.mapping_id + ] + verifylist = [ + ('mapping', identity_fakes.mapping_id), + ('rules', identity_fakes.mapping_rules_file_path) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + mocker = mock.Mock() + mocker.return_value = identity_fakes.MAPPING_RULES_2 + with mock.patch("openstackclient.identity.v3.mapping." + "SetMapping._read_rules", mocker): + columns, data = self.cmd.take_action(parsed_args) + self.mapping_mock.update.assert_called_with( + mapping=identity_fakes.mapping_id, + rules=identity_fakes.MAPPING_RULES_2) + + collist = ('id', 'rules') + self.assertEqual(columns, collist) + datalist = (identity_fakes.mapping_id, + identity_fakes.MAPPING_RULES_2) + self.assertEqual(datalist, data) + + def test_set_rules_wrong_file_path(self): + arglist = [ + '--rules', identity_fakes.mapping_rules_file_path, + identity_fakes.mapping_id + ] + verifylist = [ + ('mapping', identity_fakes.mapping_id), + ('rules', identity_fakes.mapping_rules_file_path) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) diff --git a/setup.cfg b/setup.cfg index 3178fe4467..d601fdfa22 100644 --- a/setup.cfg +++ b/setup.cfg @@ -212,6 +212,12 @@ openstack.identity.v3 = identity_provider_set = openstackclient.identity.v3.identity_provider:SetIdentityProvider identity_provider_show = openstackclient.identity.v3.identity_provider:ShowIdentityProvider + mapping_create = openstackclient.identity.v3.mapping:CreateMapping + mapping_delete = openstackclient.identity.v3.mapping:DeleteMapping + mapping_list = openstackclient.identity.v3.mapping:ListMapping + mapping_set = openstackclient.identity.v3.mapping:SetMapping + mapping_show = openstackclient.identity.v3.mapping:ShowMapping + policy_create = openstackclient.identity.v3.policy:CreatePolicy policy_delete = openstackclient.identity.v3.policy:DeletePolicy policy_list = openstackclient.identity.v3.policy:ListPolicy From 111d43ad8fcdd744b48b91475972366cf295f22e Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Wed, 9 Jul 2014 13:49:10 -0400 Subject: [PATCH 0212/3494] Update compute server messages for translation Mark some of the messages from the server for translation implements bp use_i18n Change-Id: I503efcfb4ca3dec1c427b58ee4a85de9a241dacd --- openstackclient/compute/v2/server.py | 239 +++++++++++++-------------- 1 file changed, 118 insertions(+), 121 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index ec7f212ddb..355774c316 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -25,11 +25,12 @@ from cliff import command from cliff import lister from cliff import show - from novaclient.v1_1 import servers + from openstackclient.common import exceptions from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.i18n import _ # noqa def _format_servers_list_networks(networks): @@ -106,17 +107,17 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) parser.add_argument( 'volume', metavar='', - help='Volume to add (name or ID)', + help=_('Volume to add (name or ID)'), ) parser.add_argument( '--device', metavar='', - help='Server internal device name for volume', + help=_('Server internal device name for volume'), ) return parser @@ -152,12 +153,12 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Name or ID of server to use', + help=_('Name or ID of server to use'), ) parser.add_argument( 'group', metavar='', - help='Name or ID of security group to add to server', + help=_('Name or ID of security group to add to server'), ) return parser @@ -189,91 +190,91 @@ def get_parser(self, prog_name): parser.add_argument( 'server_name', metavar='', - help='New server name') + help=_('New server name')) parser.add_argument( '--image', metavar='', required=True, - help='Create server from this image') + help=_('Create server from this image')) parser.add_argument( '--flavor', metavar='', required=True, - help='Create server with this flavor') + help=_('Create server with this flavor')) parser.add_argument( '--security-group', metavar='', action='append', default=[], - help='Security group to assign to this server ' - '(repeat for multiple groups)') + help=_('Security group to assign to this server ' + '(repeat for multiple groups)')) parser.add_argument( '--key-name', metavar='', - help='Keypair to inject into this server (optional extension)') + help=_('Keypair to inject into this server (optional extension)')) parser.add_argument( '--property', metavar='', action=parseractions.KeyValueAction, - help='Set a property on this server ' - '(repeat for multiple values)') + help=_('Set a property on this server ' + '(repeat for multiple values)')) parser.add_argument( '--file', metavar='', action='append', default=[], - help='File to inject into image before boot ' - '(repeat for multiple files)') + help=_('File to inject into image before boot ' + '(repeat for multiple files)')) parser.add_argument( '--user-data', metavar='', - help='User data file to serve from the metadata server') + help=_('User data file to serve from the metadata server')) parser.add_argument( '--availability-zone', metavar='', - help='Select an availability zone for the server') + help=_('Select an availability zone for the server')) parser.add_argument( '--block-device-mapping', metavar='', action='append', default=[], - help='Map block devices; map is ' - '::: ' - '(optional extension)') + help=_('Map block devices; map is ' + '::: ' + '(optional extension)')) parser.add_argument( '--nic', metavar='', action='append', default=[], - help='Specify NIC configuration (optional extension)') + help=_('Specify NIC configuration (optional extension)')) parser.add_argument( '--hint', metavar='', action='append', default=[], - help='Hints for the scheduler (optional extension)') + help=_('Hints for the scheduler (optional extension)')) parser.add_argument( '--config-drive', metavar='|True', default=False, - help='Use specified volume as the config drive, ' - 'or \'True\' to use an ephemeral drive') + help=_('Use specified volume as the config drive, ' + 'or \'True\' to use an ephemeral drive')) parser.add_argument( '--min', metavar='', type=int, default=1, - help='Minimum number of servers to launch (default=1)') + help=_('Minimum number of servers to launch (default=1)')) parser.add_argument( '--max', metavar='', type=int, default=1, - help='Maximum number of servers to launch (default=1)') + help=_('Maximum number of servers to launch (default=1)')) parser.add_argument( '--wait', action='store_true', - help='Wait for build to complete', + help=_('Wait for build to complete'), ) return parser @@ -300,13 +301,13 @@ def take_action(self, parsed_args): raise exceptions.CommandError("Can't open '%s': %s" % (src, e)) if parsed_args.min > parsed_args.max: - msg = "min instances should be <= max instances" + msg = _("min instances should be <= max instances") raise exceptions.CommandError(msg) if parsed_args.min < 1: - msg = "min instances should be > 0" + msg = _("min instances should be > 0") raise exceptions.CommandError(msg) if parsed_args.max < 1: - msg = "max instances should be > 0" + msg = _("max instances should be > 0") raise exceptions.CommandError(msg) userdata = None @@ -315,8 +316,7 @@ def take_action(self, parsed_args): userdata = open(parsed_args.user_data) except IOError as e: msg = "Can't open '%s': %s" - raise exceptions.CommandError(msg % - (parsed_args.user_data, e)) + raise exceptions.CommandError(msg % (parsed_args.user_data, e)) block_device_mapping = dict(v.split('=', 1) for v in parsed_args.block_device_mapping) @@ -378,9 +378,9 @@ def take_action(self, parsed_args): ): sys.stdout.write('\n') else: - self.log.error('Error creating server: %s', + self.log.error(_('Error creating server: %s'), parsed_args.server_name) - sys.stdout.write('\nError creating server') + sys.stdout.write(_('\nError creating server')) raise SystemExit details = _prep_server_detail(compute_client, server) @@ -397,17 +397,17 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) parser.add_argument( '--name', metavar='', - help='Name of new image (default is server name)', + help=_('Name of new image (default is server name)'), ) parser.add_argument( '--wait', action='store_true', - help='Wait for image create to complete', + help=_('Wait for image create to complete'), ) return parser @@ -437,11 +437,9 @@ def take_action(self, parsed_args): ): sys.stdout.write('\n') else: - self.log.error( - 'Error creating server snapshot: %s', - parsed_args.image_name, - ) - sys.stdout.write('\nError creating server snapshot') + self.log.error(_('Error creating server snapshot: %s'), + parsed_args.image_name) + sys.stdout.write(_('\nError creating server snapshot')) raise SystemExit image = utils.find_resource( @@ -449,9 +447,7 @@ def take_action(self, parsed_args): image_id, ) - info = {} - info.update(image._info) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(six.iteritems(image._info))) class DeleteServer(command.Command): @@ -464,7 +460,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Name or ID of server to delete') + help=_('Name or ID of server to delete')) return parser def take_action(self, parsed_args): @@ -486,50 +482,50 @@ def get_parser(self, prog_name): parser.add_argument( '--reservation-id', metavar='', - help='Only return instances that match the reservation') + help=_('Only return instances that match the reservation')) parser.add_argument( '--ip', metavar='', - help='Regular expression to match IP addresses') + help=_('Regular expression to match IP addresses')) parser.add_argument( '--ip6', metavar='', - help='Regular expression to match IPv6 addresses') + help=_('Regular expression to match IPv6 addresses')) parser.add_argument( '--name', metavar='', - help='Regular expression to match names') + help=_('Regular expression to match names')) parser.add_argument( '--status', metavar='', # FIXME(dhellmann): Add choices? - help='Search by server status') + help=_('Search by server status')) parser.add_argument( '--flavor', metavar='', - help='Search by flavor ID') + help=_('Search by flavor ID')) parser.add_argument( '--image', metavar='', - help='Search by image ID') + help=_('Search by image ID')) parser.add_argument( '--host', metavar='', - help='Search by hostname') + help=_('Search by hostname')) parser.add_argument( '--instance-name', metavar='', - help='Regular expression to match instance name (admin only)') + help=_('Regular expression to match instance name (admin only)')) parser.add_argument( '--all-projects', action='store_true', default=bool(int(os.environ.get("ALL_PROJECTS", 0))), - help='Include all projects (admin only)') + help=_('Include all projects (admin only)')) parser.add_argument( '--long', action='store_true', default=False, - help='List additional fields in output') + help=_('List additional fields in output')) return parser def take_action(self, parsed_args): @@ -598,7 +594,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) return parser @@ -632,17 +628,17 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server to migrate (name or ID)', + help=_('Server to migrate (name or ID)'), ) parser.add_argument( '--wait', action='store_true', - help='Wait for resize to complete', + help=_('Wait for resize to complete'), ) parser.add_argument( '--live', metavar='', - help='Target hostname', + help=_('Target hostname'), ) migration_group = parser.add_mutually_exclusive_group() migration_group.add_argument( @@ -650,13 +646,13 @@ def get_parser(self, prog_name): dest='shared_migration', action='store_true', default=True, - help='Perform a shared live migration (default)', + help=_('Perform a shared live migration (default)'), ) migration_group.add_argument( '--block-migration', dest='shared_migration', action='store_false', - help='Perform a block live migration', + help=_('Perform a block live migration'), ) disk_group = parser.add_mutually_exclusive_group() disk_group.add_argument( @@ -664,13 +660,14 @@ def get_parser(self, prog_name): dest='disk_overcommit', action='store_false', default=False, - help='Do not over-commit disk on the destination host (default)', + help=_('Do not over-commit disk on the' + ' destination host (default)'), ) disk_group.add_argument( '--disk-overcommit', action='store_true', default=False, - help='Allow disk over-commit on the destination host', + help=_('Allow disk over-commit on the destination host'), ) return parser @@ -698,9 +695,9 @@ def take_action(self, parsed_args): server.id, callback=_show_progress, ): - sys.stdout.write('Complete\n') + sys.stdout.write(_('Complete\n')) else: - sys.stdout.write('\nError migrating server') + sys.stdout.write(_('\nError migrating server')) raise SystemExit @@ -714,7 +711,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) return parser @@ -738,7 +735,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) group = parser.add_mutually_exclusive_group() group.add_argument( @@ -747,7 +744,7 @@ def get_parser(self, prog_name): action='store_const', const=servers.REBOOT_HARD, default=servers.REBOOT_SOFT, - help='Perform a hard reboot', + help=_('Perform a hard reboot'), ) group.add_argument( '--soft', @@ -755,12 +752,12 @@ def get_parser(self, prog_name): action='store_const', const=servers.REBOOT_SOFT, default=servers.REBOOT_SOFT, - help='Perform a soft reboot', + help=_('Perform a soft reboot'), ) parser.add_argument( '--wait', action='store_true', - help='Wait for reboot to complete', + help=_('Wait for reboot to complete'), ) return parser @@ -777,9 +774,9 @@ def take_action(self, parsed_args): server.id, callback=_show_progress, ): - sys.stdout.write('\nReboot complete\n') + sys.stdout.write(_('\nReboot complete\n')) else: - sys.stdout.write('\nError rebooting server\n') + sys.stdout.write(_('\nError rebooting server\n')) raise SystemExit @@ -793,13 +790,13 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) parser.add_argument( '--image', metavar='', required=True, - help='Recreate server from this image', + help=_('Recreate server from this image'), ) parser.add_argument( '--password', @@ -809,7 +806,7 @@ def get_parser(self, prog_name): parser.add_argument( '--wait', action='store_true', - help='Wait for rebuild to complete', + help=_('Wait for rebuild to complete'), ) return parser @@ -830,9 +827,9 @@ def take_action(self, parsed_args): server.id, callback=_show_progress, ): - sys.stdout.write('\nComplete\n') + sys.stdout.write(_('\nComplete\n')) else: - sys.stdout.write('\nError rebuilding server') + sys.stdout.write(_('\nError rebuilding server')) raise SystemExit details = _prep_server_detail(compute_client, server) @@ -849,12 +846,12 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Name or ID of server to use', + help=_('Name or ID of server to use'), ) parser.add_argument( 'group', metavar='', - help='Name or ID of security group to remove from server', + help=_('Name or ID of security group to remove from server'), ) return parser @@ -885,12 +882,12 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) parser.add_argument( 'volume', metavar='', - help='Volume to remove (name or ID)', + help=_('Volume to remove (name or ID)'), ) return parser @@ -925,7 +922,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) return parser @@ -951,27 +948,27 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) phase_group.add_argument( '--flavor', metavar='', - help='Resize server to specified flavor', + help=_('Resize server to specified flavor'), ) phase_group.add_argument( '--verify', action="store_true", - help='Verify server resize is complete', + help=_('Verify server resize is complete'), ) phase_group.add_argument( '--revert', action="store_true", - help='Restore server state before resize', + help=_('Restore server state before resize'), ) parser.add_argument( '--wait', action='store_true', - help='Wait for resize to complete', + help=_('Wait for resize to complete'), ) return parser @@ -996,9 +993,9 @@ def take_action(self, parsed_args): success_status=['active', 'verify_resize'], callback=_show_progress, ): - sys.stdout.write('Complete\n') + sys.stdout.write(_('Complete\n')) else: - sys.stdout.write('\nError resizing server') + sys.stdout.write(_('\nError resizing server')) raise SystemExit elif parsed_args.verify: compute_client.servers.confirm_resize(server) @@ -1016,7 +1013,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) return parser @@ -1040,24 +1037,24 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) parser.add_argument( '--name', metavar='', - help='New server name', + help=_('New server name'), ) parser.add_argument( '--root-password', action="store_true", - help='Set new root password (interactive only)', + help=_('Set new root password (interactive only)'), ) parser.add_argument( "--property", metavar="", action=parseractions.KeyValueAction, - help='Property to add/change for this server ' - '(repeat option to set multiple properties)', + help=_('Property to add/change for this server ' + '(repeat option to set multiple properties)'), ) return parser @@ -1080,12 +1077,12 @@ def take_action(self, parsed_args): ) if parsed_args.root_password: - p1 = getpass.getpass('New password: ') - p2 = getpass.getpass('Retype new password: ') + p1 = getpass.getpass(_('New password: ')) + p2 = getpass.getpass(_('Retype new password: ')) if p1 == p2: server.change_password(p1) else: - msg = "Passwords do not match, password unchanged" + msg = _("Passwords do not match, password unchanged") raise exceptions.CommandError(msg) @@ -1099,13 +1096,13 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server to show (name or ID)', + help=_('Server to show (name or ID)'), ) parser.add_argument( '--diagnostics', action='store_true', default=False, - help='Display diagnostics information for a given server', + help=_('Display diagnostics information for a given server'), ) return parser @@ -1118,7 +1115,7 @@ def take_action(self, parsed_args): if parsed_args.diagnostics: (resp, data) = server.diagnostics() if not resp.status_code == 200: - sys.stderr.write("Error retrieving diagnostics data") + sys.stderr.write(_("Error retrieving diagnostics data")) return ({}, {}) else: data = _prep_server_detail(compute_client, server) @@ -1136,12 +1133,12 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) parser.add_argument( '--login', metavar='', - help='Login name (ssh -l option)', + help=_('Login name (ssh -l option)'), ) parser.add_argument( '-l', @@ -1152,7 +1149,7 @@ def get_parser(self, prog_name): '--port', metavar='', type=int, - help='Destination port (ssh -p option)', + help=_('Destination port (ssh -p option)'), ) parser.add_argument( '-p', @@ -1164,7 +1161,7 @@ def get_parser(self, prog_name): parser.add_argument( '--identity', metavar='', - help='Private key file (ssh -i option)', + help=_('Private key file (ssh -i option)'), ) parser.add_argument( '-i', @@ -1175,7 +1172,7 @@ def get_parser(self, prog_name): parser.add_argument( '--option', metavar='', - help='Options in ssh_config(5) format (ssh -o option)', + help=_('Options in ssh_config(5) format (ssh -o option)'), ) parser.add_argument( '-o', @@ -1189,14 +1186,14 @@ def get_parser(self, prog_name): dest='ipv4', action='store_true', default=False, - help='Use only IPv4 addresses', + help=_('Use only IPv4 addresses'), ) ip_group.add_argument( '-6', dest='ipv6', action='store_true', default=False, - help='Use only IPv6 addresses', + help=_('Use only IPv6 addresses'), ) type_group = parser.add_mutually_exclusive_group() type_group.add_argument( @@ -1205,7 +1202,7 @@ def get_parser(self, prog_name): action='store_const', const='public', default='public', - help='Use public IP address', + help=_('Use public IP address'), ) type_group.add_argument( '--private', @@ -1213,14 +1210,14 @@ def get_parser(self, prog_name): action='store_const', const='private', default='public', - help='Use private IP address', + help=_('Use private IP address'), ) type_group.add_argument( '--address-type', metavar='', dest='address_type', default='public', - help='Use other IP address (public, private, etc)', + help=_('Use other IP address (public, private, etc)'), ) parser.add_argument( '-v', @@ -1294,7 +1291,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) return parser @@ -1318,7 +1315,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) return parser @@ -1342,7 +1339,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) return parser @@ -1366,7 +1363,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) return parser @@ -1390,15 +1387,15 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help=_('Server (name or ID)'), ) parser.add_argument( '--property', metavar='', action='append', default=[], - help='Property key to remove from server ' - '(repeat to set multiple values)', + help=_('Property key to remove from server ' + '(repeat to set multiple values)'), ) return parser From 5b6c24fdb0154bbbf41f0b05211001d783b69635 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 26 Jun 2014 17:23:59 -0500 Subject: [PATCH 0213/3494] Update for cliff commandmanager >=1.6.1 Cliff 1.6.1 added CommandManager.load_commands() so we can adopt it rather than rolling our own. Also, that second group is Greek, not Latin. Jeez... Change-Id: I4a63c22f37bcfd0ef5d83c2dbd08b58fda0db35c --- openstackclient/common/commandmanager.py | 18 ++++-------------- .../tests/common/test_commandmanager.py | 16 ++++++++-------- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/openstackclient/common/commandmanager.py b/openstackclient/common/commandmanager.py index 08710c7735..9901ea2089 100644 --- a/openstackclient/common/commandmanager.py +++ b/openstackclient/common/commandmanager.py @@ -16,7 +16,6 @@ """Modify cliff.CommandManager""" import logging -import pkg_resources import cliff.commandmanager @@ -35,23 +34,14 @@ def __init__(self, namespace, convert_underscores=True): self.group_list = [] super(CommandManager, self).__init__(namespace, convert_underscores) - def _load_commands(self, group=None): - if not group: - group = self.namespace - self.group_list.append(group) - for ep in pkg_resources.iter_entry_points(group): - cmd_name = ( - ep.name.replace('_', ' ') - if self.convert_underscores - else ep.name - ) - self.commands[cmd_name] = ep - return + def load_commands(self, namespace): + self.group_list.append(namespace) + return super(CommandManager, self).load_commands(namespace) def add_command_group(self, group=None): """Adds another group of command entrypoints""" if group: - self._load_commands(group) + self.load_commands(group) def get_command_groups(self): """Returns a list of the loaded command groups""" diff --git a/openstackclient/tests/common/test_commandmanager.py b/openstackclient/tests/common/test_commandmanager.py index 088ea21ea8..ca9ee9a70e 100644 --- a/openstackclient/tests/common/test_commandmanager.py +++ b/openstackclient/tests/common/test_commandmanager.py @@ -36,15 +36,15 @@ def __init__(self): class FakeCommandManager(commandmanager.CommandManager): commands = {} - def _load_commands(self, group=None): - if not group: + def load_commands(self, namespace): + if namespace == 'test': self.commands['one'] = FAKE_CMD_ONE self.commands['two'] = FAKE_CMD_TWO - self.group_list.append(self.namespace) - else: + self.group_list.append(namespace) + elif namespace == 'greek': self.commands['alpha'] = FAKE_CMD_ALPHA self.commands['beta'] = FAKE_CMD_BETA - self.group_list.append(group) + self.group_list.append(namespace) class TestCommandManager(utils.TestCase): @@ -62,7 +62,7 @@ def test_add_command_group(self): self.assertEqual(cmd_one, FAKE_CMD_ONE) # Load another command group - mgr.add_command_group('latin') + mgr.add_command_group('greek') # Find a new command cmd_alpha, name, args = mgr.find_command(['alpha']) @@ -82,7 +82,7 @@ def test_get_command_groups(self): self.assertEqual(cmd_mock, mock_cmd_one) # Load another command group - mgr.add_command_group('latin') + mgr.add_command_group('greek') gl = mgr.get_command_groups() - self.assertEqual(['test', 'latin'], gl) + self.assertEqual(['test', 'greek'], gl) From 14c61a0ace85a7b47403d4fba6c50320f717d37b Mon Sep 17 00:00:00 2001 From: Marek Denis Date: Thu, 2 Oct 2014 09:36:13 +0200 Subject: [PATCH 0214/3494] CRUD operations for federated protocols Openstackclient needs to have a capability to manage federated protocols (like saml2, openid connect, abfab). This patch allows users to administrate such operations from the commandline. Change-Id: I59eef2acdda60c7ec795d1bfe31e8e960b4478a1 Implements: bp/add-openstackclient-federation-crud --- .../identity/v3/federation_protocol.py | 182 +++++++++++++++++ openstackclient/tests/identity/v3/fakes.py | 24 +++ .../tests/identity/v3/test_protocol.py | 185 ++++++++++++++++++ setup.cfg | 6 + 4 files changed, 397 insertions(+) create mode 100644 openstackclient/identity/v3/federation_protocol.py create mode 100644 openstackclient/tests/identity/v3/test_protocol.py diff --git a/openstackclient/identity/v3/federation_protocol.py b/openstackclient/identity/v3/federation_protocol.py new file mode 100644 index 0000000000..adc4a28b14 --- /dev/null +++ b/openstackclient/identity/v3/federation_protocol.py @@ -0,0 +1,182 @@ +# 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. +# + + +"""Identity v3 Protocols actions implementations""" + +import logging +import six + +from cliff import command +from cliff import lister +from cliff import show + +from openstackclient.common import utils + + +class CreateProtocol(show.ShowOne): + """Create new Federation Protocol tied to an Identity Provider""" + + log = logging.getLogger(__name__ + 'CreateProtocol') + + def get_parser(self, prog_name): + parser = super(CreateProtocol, self).get_parser(prog_name) + parser.add_argument( + 'federation_protocol', + metavar='', + help='Protocol (must be unique per Identity Provider') + parser.add_argument( + '--identity-provider', + metavar='', + help=('Identity Provider you want to add the Protocol to ' + '(must already exist)'), required=True) + parser.add_argument( + '--mapping', + metavar='', required=True, + help='Mapping you want to be used (must already exist)') + + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + identity_client = self.app.client_manager.identity + protocol = identity_client.federation.protocols.create( + protocol_id=parsed_args.federation_protocol, + identity_provider=parsed_args.identity_provider, + mapping=parsed_args.mapping) + info = dict(protocol._info) + # NOTE(marek-denis): Identity provider is not included in a response + # from Keystone, however it should be listed to the user. Add it + # manually to the output list, simply reusing value provided by the + # user. + info['identity_provider'] = parsed_args.identity_provider + info['mapping'] = info.pop('mapping_id') + return zip(*sorted(six.iteritems(info))) + + +class DeleteProtocol(command.Command): + """Delete Federation Protocol tied to a Identity Provider""" + + log = logging.getLogger(__name__ + '.DeleteProtocol') + + def get_parser(self, prog_name): + parser = super(DeleteProtocol, self).get_parser(prog_name) + parser.add_argument( + 'federation_protocol', + metavar='', + help='Protocol (must be unique per Identity Provider') + parser.add_argument( + '--identity-provider', + metavar='', required=True, + help='Identity Provider the Protocol is tied to') + + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + identity_client = self.app.client_manager.identity + identity_client.federation.protocols.delete( + parsed_args.identity_provider, parsed_args.federation_protocol) + return + + +class ListProtocols(lister.Lister): + """List Protocols tied to an Identity Provider""" + + log = logging.getLogger(__name__ + '.ListProtocols') + + def get_parser(self, prog_name): + parser = super(ListProtocols, self).get_parser(prog_name) + parser.add_argument( + '--identity-provider', + metavar='', required=True, + help='Identity Provider the Protocol is tied to') + + return parser + + def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity + + protocols = identity_client.federation.protocols.list( + parsed_args.identity_provider) + columns = ('id', 'mapping') + response_attributes = ('id', 'mapping_id') + items = [utils.get_item_properties(s, response_attributes) + for s in protocols] + return (columns, items) + + +class SetProtocol(command.Command): + """Set Protocol tied to an Identity Provider""" + + log = logging.getLogger(__name__ + '.SetProtocol') + + def get_parser(self, prog_name): + parser = super(SetProtocol, self).get_parser(prog_name) + parser.add_argument( + 'federation_protocol', + metavar='', + help='Protocol (must be unique per Identity Provider') + parser.add_argument( + '--identity-provider', + metavar='', required=True, + help=('Identity Provider you want to add the Protocol to ' + '(must already exist)')) + parser.add_argument( + '--mapping', + metavar='', required=True, + help='Mapping you want to be used (must already exist)') + return parser + + def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity + + protocol = identity_client.federation.protocols.update( + parsed_args.identity_provider, parsed_args.federation_protocol, + parsed_args.mapping) + info = dict(protocol._info) + # NOTE(marek-denis): Identity provider is not included in a response + # from Keystone, however it should be listed to the user. Add it + # manually to the output list, simply reusing value provided by the + # user. + info['identity_provider'] = parsed_args.identity_provider + info['mapping'] = info.pop('mapping_id') + return zip(*sorted(six.iteritems(info))) + + +class ShowProtocol(show.ShowOne): + """Show Protocol tied to an Identity Provider""" + + log = logging.getLogger(__name__ + '.ShowProtocol') + + def get_parser(self, prog_name): + parser = super(ShowProtocol, self).get_parser(prog_name) + parser.add_argument( + 'federation_protocol', + metavar='', + help='Protocol (must be unique per Identity Provider') + parser.add_argument( + '--identity-provider', + metavar='', required=True, + help=('Identity Provider you want to add the Protocol to ' + '(must already exist)')) + return parser + + def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity + + protocol = identity_client.federation.protocols.get( + parsed_args.identity_provider, parsed_args.federation_protocol) + info = dict(protocol._info) + info['mapping'] = info.pop('mapping_id') + return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index a88a905e6f..b0df16f043 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -190,6 +190,28 @@ 'description': idp_description } +protocol_id = 'protocol' + +mapping_id = 'test_mapping' +mapping_id_updated = 'prod_mapping' + +PROTOCOL_ID_MAPPING = { + 'id': protocol_id, + 'mapping': mapping_id +} + +PROTOCOL_OUTPUT = { + 'id': protocol_id, + 'mapping_id': mapping_id, + 'identity_provider': idp_id +} + +PROTOCOL_OUTPUT_UPDATED = { + 'id': protocol_id, + 'mapping_id': mapping_id_updated, + 'identity_provider': idp_id +} + # Assignments ASSIGNMENT_WITH_PROJECT_ID_AND_USER_ID = { @@ -285,6 +307,8 @@ def __init__(self, **kwargs): self.identity_providers.resource_class = fakes.FakeResource(None, {}) self.mappings = mock.Mock() self.mappings.resource_class = fakes.FakeResource(None, {}) + self.protocols = mock.Mock() + self.protocols.resource_class = fakes.FakeResource(None, {}) class FakeFederatedClient(FakeIdentityv3Client): diff --git a/openstackclient/tests/identity/v3/test_protocol.py b/openstackclient/tests/identity/v3/test_protocol.py new file mode 100644 index 0000000000..3c9c3f0a7d --- /dev/null +++ b/openstackclient/tests/identity/v3/test_protocol.py @@ -0,0 +1,185 @@ +# Copyright 2014 CERN. +# +# 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 copy + +from openstackclient.identity.v3 import federation_protocol +from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes + + +class TestProtocol(identity_fakes.TestFederatedIdentity): + + def setUp(self): + super(TestProtocol, self).setUp() + + federation_lib = self.app.client_manager.identity.federation + self.protocols_mock = federation_lib.protocols + self.protocols_mock.reset_mock() + + +class TestProtocolCreate(TestProtocol): + + def setUp(self): + super(TestProtocolCreate, self).setUp() + + proto = copy.deepcopy(identity_fakes.PROTOCOL_OUTPUT) + resource = fakes.FakeResource(None, proto, loaded=True) + self.protocols_mock.create.return_value = resource + self.cmd = federation_protocol.CreateProtocol(self.app, None) + + def test_create_protocol(self): + argslist = [ + identity_fakes.protocol_id, + '--identity-provider', identity_fakes.idp_id, + '--mapping', identity_fakes.mapping_id + ] + + verifylist = [ + ('federation_protocol', identity_fakes.protocol_id), + ('identity_provider', identity_fakes.idp_id), + ('mapping', identity_fakes.mapping_id) + ] + parsed_args = self.check_parser(self.cmd, argslist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.protocols_mock.create.assert_called_with( + protocol_id=identity_fakes.protocol_id, + identity_provider=identity_fakes.idp_id, + mapping=identity_fakes.mapping_id) + + collist = ('id', 'identity_provider', 'mapping') + self.assertEqual(collist, columns) + + datalist = (identity_fakes.protocol_id, + identity_fakes.idp_id, + identity_fakes.mapping_id) + self.assertEqual(datalist, data) + + +class TestProtocolDelete(TestProtocol): + + def setUp(self): + super(TestProtocolDelete, self).setUp() + + # This is the return value for utils.find_resource() + self.protocols_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROTOCOL_OUTPUT), + loaded=True, + ) + + self.protocols_mock.delete.return_value = None + self.cmd = federation_protocol.DeleteProtocol(self.app, None) + + def test_delete_identity_provider(self): + arglist = [ + '--identity-provider', identity_fakes.idp_id, + identity_fakes.protocol_id + ] + verifylist = [ + ('federation_protocol', identity_fakes.protocol_id), + ('identity_provider', identity_fakes.idp_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.protocols_mock.delete.assert_called_with( + identity_fakes.idp_id, identity_fakes.protocol_id) + + +class TestProtocolList(TestProtocol): + + def setUp(self): + super(TestProtocolList, self).setUp() + + self.protocols_mock.get.return_value = fakes.FakeResource( + None, identity_fakes.PROTOCOL_ID_MAPPING, loaded=True) + + self.protocols_mock.list.return_value = [fakes.FakeResource( + None, identity_fakes.PROTOCOL_ID_MAPPING, loaded=True)] + + self.cmd = federation_protocol.ListProtocols(self.app, None) + + def test_list_protocols(self): + arglist = ['--identity-provider', identity_fakes.idp_id] + verifylist = [('identity_provider', identity_fakes.idp_id)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.protocols_mock.list.assert_called_with(identity_fakes.idp_id) + + +class TestProtocolSet(TestProtocol): + + def setUp(self): + super(TestProtocolSet, self).setUp() + self.protocols_mock.get.return_value = fakes.FakeResource( + None, identity_fakes.PROTOCOL_OUTPUT, loaded=True) + self.protocols_mock.update.return_value = fakes.FakeResource( + None, identity_fakes.PROTOCOL_OUTPUT_UPDATED, loaded=True) + + self.cmd = federation_protocol.SetProtocol(self.app, None) + + def test_set_new_mapping(self): + arglist = [ + identity_fakes.protocol_id, + '--identity-provider', identity_fakes.idp_id, + '--mapping', identity_fakes.mapping_id + ] + verifylist = [('identity_provider', identity_fakes.idp_id), + ('federation_protocol', identity_fakes.protocol_id), + ('mapping', identity_fakes.mapping_id)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.protocols_mock.update.assert_called_with( + identity_fakes.idp_id, identity_fakes.protocol_id, + identity_fakes.mapping_id) + + collist = ('id', 'identity_provider', 'mapping') + self.assertEqual(collist, columns) + + datalist = (identity_fakes.protocol_id, identity_fakes.idp_id, + identity_fakes.mapping_id_updated) + self.assertEqual(datalist, data) + + +class TestProtocolShow(TestProtocol): + + def setUp(self): + super(TestProtocolShow, self).setUp() + self.protocols_mock.get.return_value = fakes.FakeResource( + None, identity_fakes.PROTOCOL_OUTPUT, loaded=False) + + self.cmd = federation_protocol.ShowProtocol(self.app, None) + + def test_show_protocol(self): + arglist = [identity_fakes.protocol_id, '--identity-provider', + identity_fakes.idp_id] + verifylist = [('federation_protocol', identity_fakes.protocol_id), + ('identity_provider', identity_fakes.idp_id)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.protocols_mock.get.assert_called_with(identity_fakes.idp_id, + identity_fakes.protocol_id) + + collist = ('id', 'identity_provider', 'mapping') + self.assertEqual(collist, columns) + + datalist = (identity_fakes.protocol_id, + identity_fakes.idp_id, + identity_fakes.mapping_id) + self.assertEqual(datalist, data) diff --git a/setup.cfg b/setup.cfg index d601fdfa22..dc85967f42 100644 --- a/setup.cfg +++ b/setup.cfg @@ -230,6 +230,12 @@ openstack.identity.v3 = project_set = openstackclient.identity.v3.project:SetProject project_show = openstackclient.identity.v3.project:ShowProject + federation_protocol_create = openstackclient.identity.v3.federation_protocol:CreateProtocol + federation_protocol_delete = openstackclient.identity.v3.federation_protocol:DeleteProtocol + federation_protocol_list = openstackclient.identity.v3.federation_protocol:ListProtocols + federation_protocol_set = openstackclient.identity.v3.federation_protocol:SetProtocol + federation_protocol_show = openstackclient.identity.v3.federation_protocol:ShowProtocol + request_token_authorize = openstackclient.identity.v3.token:AuthorizeRequestToken request_token_create = openstackclient.identity.v3.token:CreateRequestToken From d32185cb34495b0af4b4e646a93aedf4d7f86d25 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 27 Aug 2014 23:26:42 -0500 Subject: [PATCH 0215/3494] Add 'command list' command * Add method to CommandManager to retrieve command names by group * Add ListCommands To list command groups loaded by cliff Change-Id: I37fe2471aa2fafa8aa223159452d52b1981021d6 --- openstackclient/common/commandmanager.py | 15 +++++++++ openstackclient/common/module.py | 16 ++++++++++ .../tests/common/test_commandmanager.py | 17 ++++++++++ openstackclient/tests/common/test_module.py | 32 +++++++++++++++++++ setup.cfg | 1 + 5 files changed, 81 insertions(+) diff --git a/openstackclient/common/commandmanager.py b/openstackclient/common/commandmanager.py index 9901ea2089..2d9575d9d2 100644 --- a/openstackclient/common/commandmanager.py +++ b/openstackclient/common/commandmanager.py @@ -16,6 +16,7 @@ """Modify cliff.CommandManager""" import logging +import pkg_resources import cliff.commandmanager @@ -46,3 +47,17 @@ def add_command_group(self, group=None): def get_command_groups(self): """Returns a list of the loaded command groups""" return self.group_list + + def get_command_names(self, group=None): + """Returns a list of commands loaded for the specified group""" + group_list = [] + if group is not None: + for ep in pkg_resources.iter_entry_points(group): + cmd_name = ( + ep.name.replace('_', ' ') + if self.convert_underscores + else ep.name + ) + group_list.append(cmd_name) + return group_list + return self.commands.keys() diff --git a/openstackclient/common/module.py b/openstackclient/common/module.py index 7f9c52db22..356cdca3b0 100644 --- a/openstackclient/common/module.py +++ b/openstackclient/common/module.py @@ -19,9 +19,25 @@ import six import sys +from cliff import lister from cliff import show +class ListCommand(lister.Lister): + """List recognized commands by group""" + + auth_required = False + log = logging.getLogger(__name__ + '.ListCommand') + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + cm = self.app.command_manager + groups = cm.get_command_groups() + + columns = ('Command Group', 'Commands') + return (columns, ((c, cm.get_command_names(group=c)) for c in groups)) + + class ListModule(show.ShowOne): """List module versions""" diff --git a/openstackclient/tests/common/test_commandmanager.py b/openstackclient/tests/common/test_commandmanager.py index ca9ee9a70e..e7803a48e4 100644 --- a/openstackclient/tests/common/test_commandmanager.py +++ b/openstackclient/tests/common/test_commandmanager.py @@ -86,3 +86,20 @@ def test_get_command_groups(self): gl = mgr.get_command_groups() self.assertEqual(['test', 'greek'], gl) + + def test_get_command_names(self): + mock_cmd_one = mock.Mock() + mock_cmd_one.name = 'one' + mock_cmd_two = mock.Mock() + mock_cmd_two.name = 'cmd two' + mock_pkg_resources = mock.Mock( + return_value=[mock_cmd_one, mock_cmd_two], + ) + with mock.patch( + 'pkg_resources.iter_entry_points', + mock_pkg_resources, + ) as iter_entry_points: + mgr = commandmanager.CommandManager('test') + assert iter_entry_points.called_once_with('test') + cmds = mgr.get_command_names('test') + self.assertEqual(['one', 'cmd two'], cmds) diff --git a/openstackclient/tests/common/test_module.py b/openstackclient/tests/common/test_module.py index ce1592e41c..6918c1b419 100644 --- a/openstackclient/tests/common/test_module.py +++ b/openstackclient/tests/common/test_module.py @@ -42,6 +42,38 @@ } +class TestCommandList(utils.TestCommand): + + def setUp(self): + super(TestCommandList, self).setUp() + + self.app.command_manager = mock.Mock() + self.app.command_manager.get_command_groups.return_value = ['test'] + self.app.command_manager.get_command_names.return_value = [ + 'one', + 'cmd two', + ] + + # Get the command object to test + self.cmd = osc_module.ListCommand(self.app, None) + + def test_command_list_no_options(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + collist = ('Command Group', 'Commands') + self.assertEqual(collist, columns) + datalist = (( + 'test', + ['one', 'cmd two'], + ), ) + self.assertEqual(datalist, tuple(data)) + + @mock.patch.dict( 'openstackclient.common.module.sys.modules', values=MODULES, diff --git a/setup.cfg b/setup.cfg index 3178fe4467..267c9e330d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,6 +28,7 @@ console_scripts = openstack = openstackclient.shell:main openstack.cli = + command_list = openstackclient.common.module:ListCommand module_list = openstackclient.common.module:ListModule openstack.cli.base = From 0c77a9fe8baa4df9ea2d0055db9c700af3cae310 Mon Sep 17 00:00:00 2001 From: Matthieu Huin Date: Fri, 18 Jul 2014 19:18:25 +0200 Subject: [PATCH 0216/3494] Support for keystone auth plugins This patch allows the user to choose which authentication plugin to use with the CLI. The arguments needed by the auth plugins are automatically added to the argument parser. Some examples with the currently available authentication plugins:: OS_USERNAME=admin OS_PROJECT_NAME=admin OS_AUTH_URL=http://keystone:5000/v2.0 \ OS_PASSWORD=admin openstack user list OS_USERNAME=admin OS_PROJECT_DOMAIN_NAME=default OS_USER_DOMAIN_NAME=default \ OS_PROJECT_NAME=admin OS_AUTH_URL=http://keystone:5000/v3 OS_PASSWORD=admin \ OS_IDENTITY_API_VERSION=3 OS_AUTH_PLUGIN=v3password openstack project list OS_TOKEN=1234 OS_URL=http://service_url:35357/v2.0 \ OS_IDENTITY_API_VERSION=2.0 openstack user list The --os-auth-plugin option can be omitted; if so the CLI will attempt to guess which plugin to use from the other options. Change-Id: I330c20ddb8d96b3a4287c68b57c36c4a0f869669 Co-Authored-By: Florent Flament --- doc/source/man/openstack.rst | 34 ++++ openstackclient/api/auth.py | 180 ++++++++++++++++++ openstackclient/common/clientmanager.py | 125 +++++------- openstackclient/identity/client.py | 21 +- openstackclient/shell.py | 164 +++++----------- .../tests/common/test_clientmanager.py | 172 ++++++++++++----- openstackclient/tests/fakes.py | 136 +++++++++++++ openstackclient/tests/test_shell.py | 86 ++++----- requirements.txt | 1 + 9 files changed, 614 insertions(+), 305 deletions(-) create mode 100644 openstackclient/api/auth.py diff --git a/doc/source/man/openstack.rst b/doc/source/man/openstack.rst index b8dcbd6b66..de2bbe92fa 100644 --- a/doc/source/man/openstack.rst +++ b/doc/source/man/openstack.rst @@ -21,6 +21,10 @@ DESCRIPTION equivalent to the CLIs provided by the OpenStack project client libraries, but with a distinct and consistent command structure. + +AUTHENTICATION METHODS +====================== + :program:`openstack` uses a similar authentication scheme as the OpenStack project CLIs, with the credential information supplied either as environment variables or as options on the command line. The primary difference is the use of 'project' in the name of the options @@ -33,6 +37,15 @@ command line. The primary difference is the use of 'project' in the name of the export OS_USERNAME= export OS_PASSWORD= # (optional) +:program:`openstack` can use different types of authentication plugins provided by the keystoneclient library. The following default plugins are available: + +* ``token``: Authentication with a token +* ``password``: Authentication with a username and a password + +Refer to the keystoneclient library documentation for more details about these plugins and their options, and for a complete list of available plugins. +Please bear in mind that some plugins might not support all of the functionalities of :program:`openstack`; for example the v3unscopedsaml plugin can deliver only unscoped tokens, some commands might not be available through this authentication method. + +Additionally, it is possible to use Keystone's service token to authenticate, by setting the options :option:`--os-token` and :option:`--os-url` (or the environment variables :envvar:`OS_TOKEN` and :envvar:`OS_URL` respectively). This method takes precedence over authentication plugins. OPTIONS ======= @@ -41,9 +54,16 @@ OPTIONS :program:`openstack` recognizes the following global topions: +:option:`--os-auth-plugin` + The authentication plugin to use when connecting to the Identity service. If this option is not set, :program:`openstack` will attempt to guess the authentication method to use based on the other options. + If this option is set, its version must match :option:`--os-identity-api-version` + :option:`--os-auth-url` Authentication URL +:option:`--os-url` + Service URL, when using a service token for authentication + :option:`--os-domain-name` | :option:`--os-domain-id` Domain-level authorization scope (name or ID) @@ -59,6 +79,9 @@ OPTIONS :option:`--os-password` Authentication password +:option:`--os-token` + Authenticated token or service token + :option:`--os-user-domain-name` | :option:`--os-user-domain-id` Domain name or id containing user @@ -86,6 +109,7 @@ OPTIONS :option:`--os-XXXX-api-version` Additional API version options will be available depending on the installed API libraries. + COMMANDS ======== @@ -174,9 +198,15 @@ ENVIRONMENT VARIABLES The following environment variables can be set to alter the behaviour of :program:`openstack`. Most of them have corresponding command-line options that take precedence if set. +:envvar:`OS_AUTH_PLUGIN` + The authentication plugin to use when connecting to the Identity service, its version must match the Identity API version + :envvar:`OS_AUTH_URL` Authentication URL +:envvar:`OS_URL` + Service URL (when using the service token) + :envvar:`OS_DOMAIN_NAME` Domain-level authorization scope (name or ID) @@ -189,6 +219,9 @@ The following environment variables can be set to alter the behaviour of :progra :envvar:`OS_USERNAME` Authentication username +:envvar:`OS_TOKEN` + Authenticated or service token + :envvar:`OS_PASSWORD` Authentication password @@ -213,6 +246,7 @@ The following environment variables can be set to alter the behaviour of :progra :envvar:`OS_XXXX_API_VERSION` Additional API version options will be available depending on the installed API libraries. + BUGS ==== diff --git a/openstackclient/api/auth.py b/openstackclient/api/auth.py new file mode 100644 index 0000000000..2bd5271f7d --- /dev/null +++ b/openstackclient/api/auth.py @@ -0,0 +1,180 @@ +# 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. +# + +"""Authentication Library""" + +import argparse +import logging + +import stevedore + +from keystoneclient.auth import base + +from openstackclient.common import exceptions as exc +from openstackclient.common import utils + + +LOG = logging.getLogger(__name__) + + +# Initialize the list of Authentication plugins early in order +# to get the command-line options +PLUGIN_LIST = stevedore.ExtensionManager( + base.PLUGIN_NAMESPACE, + invoke_on_load=False, + propagate_map_exceptions=True, +) +# TODO(dtroyer): add some method to list the plugins for the +# --os_auth_plugin option + +# Get the command line options so the help action has them available +OPTIONS_LIST = {} +for plugin in PLUGIN_LIST: + for o in plugin.plugin.get_options(): + os_name = o.dest.lower().replace('_', '-') + os_env_name = 'OS_' + os_name.upper().replace('-', '_') + OPTIONS_LIST.setdefault(os_name, {'env': os_env_name, 'help': ''}) + # TODO(mhu) simplistic approach, would be better to only add + # help texts if they vary from one auth plugin to another + # also the text rendering is ugly in the CLI ... + OPTIONS_LIST[os_name]['help'] += 'With %s: %s\n' % ( + plugin.name, + o.help, + ) + + +def _guess_authentication_method(options): + """If no auth plugin was specified, pick one based on other options""" + + if options.os_url: + # service token authentication, do nothing + return + auth_plugin = None + if options.os_password: + if options.os_identity_api_version == '3': + auth_plugin = 'v3password' + elif options.os_identity_api_version == '2.0': + auth_plugin = 'v2password' + else: + # let keystoneclient figure it out itself + auth_plugin = 'password' + elif options.os_token: + if options.os_identity_api_version == '3': + auth_plugin = 'v3token' + elif options.os_identity_api_version == '2.0': + auth_plugin = 'v2token' + else: + # let keystoneclient figure it out itself + auth_plugin = 'token' + else: + raise exc.CommandError( + "Could not figure out which authentication method " + "to use, please set --os-auth-plugin" + ) + LOG.debug("No auth plugin selected, picking %s from other " + "options" % auth_plugin) + options.os_auth_plugin = auth_plugin + + +def build_auth_params(cmd_options): + auth_params = {} + if cmd_options.os_url: + return {'token': cmd_options.os_token} + if cmd_options.os_auth_plugin: + auth_plugin = base.get_plugin_class(cmd_options.os_auth_plugin) + plugin_options = auth_plugin.get_options() + for option in plugin_options: + option_name = 'os_' + option.dest + LOG.debug('fetching option %s' % option_name) + auth_params[option.dest] = getattr(cmd_options, option_name, None) + # grab tenant from project for v2.0 API compatibility + if cmd_options.os_auth_plugin.startswith("v2"): + auth_params['tenant_id'] = getattr( + cmd_options, + 'os_project_id', + None, + ) + auth_params['tenant_name'] = getattr( + cmd_options, + 'os_project_name', + None, + ) + else: + # delay the plugin choice, grab every option + plugin_options = set([o.replace('-', '_') for o in OPTIONS_LIST]) + for option in plugin_options: + option_name = 'os_' + option + LOG.debug('fetching option %s' % option_name) + auth_params[option] = getattr(cmd_options, option_name, None) + return auth_params + + +def build_auth_plugins_option_parser(parser): + """Auth plugins options builder + + Builds dynamically the list of options expected by each available + authentication plugin. + + """ + available_plugins = [plugin.name for plugin in PLUGIN_LIST] + parser.add_argument( + '--os-auth-plugin', + metavar='', + default=utils.env('OS_AUTH_PLUGIN'), + help='The authentication method to use. If this option is not set, ' + 'openstackclient will attempt to guess the authentication method ' + 'to use based on the other options. If this option is set, ' + 'the --os-identity-api-version argument must be consistent ' + 'with the version of the method.\nAvailable methods are ' + + ', '.join(available_plugins), + choices=available_plugins + ) + # make sur we catch old v2.0 env values + envs = { + 'OS_PROJECT_NAME': utils.env( + 'OS_PROJECT_NAME', + default=utils.env('OS_TENANT_NAME') + ), + 'OS_PROJECT_ID': utils.env( + 'OS_PROJECT_ID', + default=utils.env('OS_TENANT_ID') + ), + } + for o in OPTIONS_LIST: + # remove allusion to tenants from v2.0 API + if 'tenant' not in o: + parser.add_argument( + '--os-' + o, + metavar='' % o, + default=envs.get(OPTIONS_LIST[o]['env'], + utils.env(OPTIONS_LIST[o]['env'])), + help='%s\n(Env: %s)' % (OPTIONS_LIST[o]['help'], + OPTIONS_LIST[o]['env']), + ) + # add tenant-related options for compatibility + # this is deprecated but still used in some tempest tests... + parser.add_argument( + '--os-tenant-name', + metavar='', + dest='os_project_name', + default=utils.env('OS_TENANT_NAME'), + help=argparse.SUPPRESS, + ) + parser.add_argument( + '--os-tenant-id', + metavar='', + dest='os_project_id', + default=utils.env('OS_TENANT_ID'), + help=argparse.SUPPRESS, + ) + return parser diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index 4206ad001c..0542b47362 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -19,9 +19,11 @@ import pkg_resources import sys -from keystoneclient.auth.identity import v2 as v2_auth -from keystoneclient.auth.identity import v3 as v3_auth +from keystoneclient.auth import base from keystoneclient import session +import requests + +from openstackclient.api import auth from openstackclient.identity import client as identity_client @@ -45,105 +47,66 @@ class ClientManager(object): """Manages access to API clients, including authentication.""" identity = ClientCache(identity_client.make_client) - def __init__(self, token=None, url=None, auth_url=None, - domain_id=None, domain_name=None, - project_name=None, project_id=None, - username=None, password=None, - user_domain_id=None, user_domain_name=None, - project_domain_id=None, project_domain_name=None, - region_name=None, api_version=None, verify=True, - trust_id=None, timing=None): - self._token = token - self._url = url - self._auth_url = auth_url - self._domain_id = domain_id - self._domain_name = domain_name - self._project_name = project_name - self._project_id = project_id - self._username = username - self._password = password - self._user_domain_id = user_domain_id - self._user_domain_name = user_domain_name - self._project_domain_id = project_domain_id - self._project_domain_name = project_domain_name - self._region_name = region_name + def __getattr__(self, name): + # this is for the auth-related parameters. + if name in ['_' + o.replace('-', '_') + for o in auth.OPTIONS_LIST]: + return self._auth_params[name[1:]] + + def __init__(self, auth_options, api_version=None, verify=True): + + if not auth_options.os_auth_plugin: + auth._guess_authentication_method(auth_options) + + self._auth_plugin = auth_options.os_auth_plugin + self._url = auth_options.os_url + self._auth_params = auth.build_auth_params(auth_options) + self._region_name = auth_options.os_region_name self._api_version = api_version - self._trust_id = trust_id self._service_catalog = None - self.timing = timing + self.timing = auth_options.timing + + # For compatability until all clients can be updated + if 'project_name' in self._auth_params: + self._project_name = self._auth_params['project_name'] + elif 'tenant_name' in self._auth_params: + self._project_name = self._auth_params['tenant_name'] # verify is the Requests-compatible form self._verify = verify # also store in the form used by the legacy client libs self._cacert = None - if verify is True or verify is False: + if isinstance(verify, bool): self._insecure = not verify else: self._cacert = verify self._insecure = False - ver_prefix = identity_client.AUTH_VERSIONS[ - self._api_version[identity_client.API_NAME] - ] - # Get logging from root logger root_logger = logging.getLogger('') LOG.setLevel(root_logger.getEffectiveLevel()) - # NOTE(dtroyer): These plugins are hard-coded for the first step - # in using the new Keystone auth plugins. - - if self._url: - LOG.debug('Using token auth %s', ver_prefix) - if ver_prefix == 'v2': - self.auth = v2_auth.Token( - auth_url=url, - token=token, - ) - else: - self.auth = v3_auth.Token( - auth_url=url, - token=token, - ) - else: - LOG.debug('Using password auth %s', ver_prefix) - if ver_prefix == 'v2': - self.auth = v2_auth.Password( - auth_url=auth_url, - username=username, - password=password, - trust_id=trust_id, - tenant_id=project_id, - tenant_name=project_name, - ) - else: - self.auth = v3_auth.Password( - auth_url=auth_url, - username=username, - password=password, - trust_id=trust_id, - user_domain_id=user_domain_id, - user_domain_name=user_domain_name, - domain_id=domain_id, - domain_name=domain_name, - project_id=project_id, - project_name=project_name, - project_domain_id=project_domain_id, - project_domain_name=project_domain_name, - ) - - self.session = session.Session( - auth=self.auth, - verify=verify, - ) + self.session = None + if not self._url: + LOG.debug('Using auth plugin: %s' % self._auth_plugin) + auth_plugin = base.get_plugin_class(self._auth_plugin) + self.auth = auth_plugin.load_from_options(**self._auth_params) + # needed by SAML authentication + request_session = requests.session() + self.session = session.Session( + auth=self.auth, + session=request_session, + verify=verify, + ) self.auth_ref = None - if not self._url: - # Trigger the auth call + if not self._auth_plugin.endswith("token") and not self._url: + LOG.debug("Populate other password flow attributes") self.auth_ref = self.session.auth.get_auth_ref(self.session) - # Populate other password flow attributes self._token = self.session.auth.get_token(self.session) self._service_catalog = self.auth_ref.service_catalog + else: + self._token = self._auth_params.get('token') return @@ -156,7 +119,7 @@ def get_endpoint_for_service_type(self, service_type): service_type=service_type) else: # Hope we were given the correct URL. - endpoint = self._url + endpoint = self._auth_url or self._url return endpoint diff --git a/openstackclient/identity/client.py b/openstackclient/identity/client.py index a43b50e373..bc10a6d237 100644 --- a/openstackclient/identity/client.py +++ b/openstackclient/identity/client.py @@ -16,9 +16,9 @@ import logging from keystoneclient.v2_0 import client as identity_client_v2_0 +from openstackclient.api import auth from openstackclient.common import utils - LOG = logging.getLogger(__name__) DEFAULT_IDENTITY_API_VERSION = '2.0' @@ -47,16 +47,15 @@ def make_client(instance): # TODO(dtroyer): Something doesn't like the session.auth when using # token auth, chase that down. if instance._url: - LOG.debug('Using token auth') + LOG.debug('Using service token auth') client = identity_client( endpoint=instance._url, - token=instance._token, + token=instance._auth_params['token'], cacert=instance._cacert, - insecure=instance._insecure, - trust_id=instance._trust_id, + insecure=instance._insecure ) else: - LOG.debug('Using password auth') + LOG.debug('Using auth plugin: %s' % instance._auth_plugin) client = identity_client( session=instance.session, cacert=instance._cacert, @@ -66,7 +65,6 @@ def make_client(instance): # so we can remove it if not instance._url: instance.auth_ref = instance.auth.get_auth_ref(instance.session) - return client @@ -81,14 +79,7 @@ def build_option_parser(parser): help='Identity API version, default=' + DEFAULT_IDENTITY_API_VERSION + ' (Env: OS_IDENTITY_API_VERSION)') - parser.add_argument( - '--os-trust-id', - metavar='', - default=utils.env('OS_TRUST_ID'), - help='Trust ID to use when authenticating. ' - 'This can only be used with Keystone v3 API ' - '(Env: OS_TRUST_ID)') - return parser + return auth.build_auth_plugins_option_parser(parser) class IdentityClientv2_0(identity_client_v2_0.Client): diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 8db1656c85..626e3f7da5 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -15,7 +15,6 @@ """Command-line interface to the OpenStack APIs""" -import argparse import getpass import logging import sys @@ -171,89 +170,13 @@ def build_option_parser(self, description, version): parser = super(OpenStackShell, self).build_option_parser( description, version) - - # Global arguments - parser.add_argument( - '--os-auth-url', - metavar='', - default=utils.env('OS_AUTH_URL'), - help='Authentication URL (Env: OS_AUTH_URL)') - parser.add_argument( - '--os-domain-name', - metavar='', - default=utils.env('OS_DOMAIN_NAME'), - help='Domain name of the requested domain-level ' - 'authorization scope (Env: OS_DOMAIN_NAME)', - ) - parser.add_argument( - '--os-domain-id', - metavar='', - default=utils.env('OS_DOMAIN_ID'), - help='Domain ID of the requested domain-level ' - 'authorization scope (Env: OS_DOMAIN_ID)', - ) - parser.add_argument( - '--os-project-name', - metavar='', - default=utils.env('OS_PROJECT_NAME', - default=utils.env('OS_TENANT_NAME')), - help='Project name of the requested project-level ' - 'authorization scope (Env: OS_PROJECT_NAME)', - ) - parser.add_argument( - '--os-tenant-name', - metavar='', - dest='os_project_name', - help=argparse.SUPPRESS, - ) - parser.add_argument( - '--os-project-id', - metavar='', - default=utils.env('OS_PROJECT_ID', - default=utils.env('OS_TENANT_ID')), - help='Project ID of the requested project-level ' - 'authorization scope (Env: OS_PROJECT_ID)', - ) - parser.add_argument( - '--os-tenant-id', - metavar='', - dest='os_project_id', - help=argparse.SUPPRESS, - ) - parser.add_argument( - '--os-username', - metavar='', - default=utils.env('OS_USERNAME'), - help='Authentication username (Env: OS_USERNAME)') - parser.add_argument( - '--os-password', - metavar='', - default=utils.env('OS_PASSWORD'), - help='Authentication password (Env: OS_PASSWORD)') - parser.add_argument( - '--os-user-domain-name', - metavar='', - default=utils.env('OS_USER_DOMAIN_NAME'), - help='Domain name of the user (Env: OS_USER_DOMAIN_NAME)') + # service token auth argument parser.add_argument( - '--os-user-domain-id', - metavar='', - default=utils.env('OS_USER_DOMAIN_ID'), - help='Domain ID of the user (Env: OS_USER_DOMAIN_ID)') - parser.add_argument( - '--os-project-domain-name', - metavar='', - default=utils.env('OS_PROJECT_DOMAIN_NAME'), - help='Domain name of the project which is the requested ' - 'project-level authorization scope ' - '(Env: OS_PROJECT_DOMAIN_NAME)') - parser.add_argument( - '--os-project-domain-id', - metavar='', - default=utils.env('OS_PROJECT_DOMAIN_ID'), - help='Domain ID of the project which is the requested ' - 'project-level authorization scope ' - '(Env: OS_PROJECT_DOMAIN_ID)') + '--os-url', + metavar='', + default=utils.env('OS_URL'), + help='Defaults to env[OS_URL]') + # Global arguments parser.add_argument( '--os-region-name', metavar='', @@ -284,16 +207,6 @@ def build_option_parser(self, description, version): help='Default domain ID, default=' + DEFAULT_DOMAIN + ' (Env: OS_DEFAULT_DOMAIN)') - parser.add_argument( - '--os-token', - metavar='', - default=utils.env('OS_TOKEN'), - help='Defaults to env[OS_TOKEN]') - parser.add_argument( - '--os-url', - metavar='', - default=utils.env('OS_URL'), - help='Defaults to env[OS_URL]') parser.add_argument( '--timing', default=False, @@ -306,20 +219,42 @@ def build_option_parser(self, description, version): def authenticate_user(self): """Verify the required authentication credentials are present""" - self.log.debug('validating authentication options') - if self.options.os_token or self.options.os_url: + self.log.debug("validating authentication options") + + # Assuming all auth plugins will be named in the same fashion, + # ie vXpluginName + if (not self.options.os_url and + self.options.os_auth_plugin.startswith('v') and + self.options.os_auth_plugin[1] != + self.options.os_identity_api_version[0]): + raise exc.CommandError( + "Auth plugin %s not compatible" + " with requested API version" % self.options.os_auth_plugin + ) + # TODO(mhu) All these checks should be exposed at the plugin level + # or just dropped altogether, as the client instantiation will fail + # anyway + if self.options.os_url and not self.options.os_token: + # service token needed + raise exc.CommandError( + "You must provide a service token via" + " either --os-token or env[OS_TOKEN]") + + if (self.options.os_auth_plugin.endswith('token') and + (self.options.os_token or self.options.os_auth_url)): # Token flow auth takes priority if not self.options.os_token: raise exc.CommandError( "You must provide a token via" " either --os-token or env[OS_TOKEN]") - if not self.options.os_url: + if not self.options.os_auth_url: raise exc.CommandError( "You must provide a service URL via" - " either --os-url or env[OS_URL]") + " either --os-auth-url or env[OS_AUTH_URL]") - else: + if (not self.options.os_url and + not self.options.os_auth_plugin.endswith('token')): # Validate password flow auth if not self.options.os_username: raise exc.CommandError( @@ -347,13 +282,15 @@ def authenticate_user(self): (self.options.os_domain_id or self.options.os_domain_name) or self.options.os_trust_id): - raise exc.CommandError( - "You must provide authentication scope as a project " - "or a domain via --os-project-id or env[OS_PROJECT_ID], " - "--os-project-name or env[OS_PROJECT_NAME], " - "--os-domain-id or env[OS_DOMAIN_ID], or" - "--os-domain-name or env[OS_DOMAIN_NAME], or " - "--os-trust-id or env[OS_TRUST_ID].") + if self.options.os_auth_plugin.endswith('password'): + raise exc.CommandError( + "You must provide authentication scope as a project " + "or a domain via --os-project-id " + "or env[OS_PROJECT_ID], " + "--os-project-name or env[OS_PROJECT_NAME], " + "--os-domain-id or env[OS_DOMAIN_ID], or" + "--os-domain-name or env[OS_DOMAIN_NAME], or " + "--os-trust-id or env[OS_TRUST_ID].") if not self.options.os_auth_url: raise exc.CommandError( @@ -375,24 +312,9 @@ def authenticate_user(self): "Pick one of project, domain or trust.") self.client_manager = clientmanager.ClientManager( - token=self.options.os_token, - url=self.options.os_url, - auth_url=self.options.os_auth_url, - domain_id=self.options.os_domain_id, - domain_name=self.options.os_domain_name, - project_name=self.options.os_project_name, - project_id=self.options.os_project_id, - user_domain_id=self.options.os_user_domain_id, - user_domain_name=self.options.os_user_domain_name, - project_domain_id=self.options.os_project_domain_id, - project_domain_name=self.options.os_project_domain_name, - username=self.options.os_username, - password=self.options.os_password, - region_name=self.options.os_region_name, + auth_options=self.options, verify=self.verify, - timing=self.options.timing, api_version=self.api_version, - trust_id=self.options.os_trust_id, ) return diff --git a/openstackclient/tests/common/test_clientmanager.py b/openstackclient/tests/common/test_clientmanager.py index 0bb657adb0..18461fb7e8 100644 --- a/openstackclient/tests/common/test_clientmanager.py +++ b/openstackclient/tests/common/test_clientmanager.py @@ -12,34 +12,25 @@ # License for the specific language governing permissions and limitations # under the License. # - import mock +from requests_mock.contrib import fixture from keystoneclient.auth.identity import v2 as auth_v2 +from keystoneclient.openstack.common import jsonutils +from keystoneclient import service_catalog + +from openstackclient.api import auth from openstackclient.common import clientmanager +from openstackclient.common import exceptions as exc +from openstackclient.tests import fakes from openstackclient.tests import utils -AUTH_REF = {'a': 1} -AUTH_TOKEN = "foobar" -AUTH_URL = "http://0.0.0.0" -USERNAME = "itchy" -PASSWORD = "scratchy" -SERVICE_CATALOG = {'sc': '123'} - -API_VERSION = { - 'identity': '2.0', -} - +API_VERSION = {"identity": "2.0"} -def FakeMakeClient(instance): - return FakeClient() - - -class FakeClient(object): - auth_ref = AUTH_REF - auth_token = AUTH_TOKEN - service_catalog = SERVICE_CATALOG +AUTH_REF = {'version': 'v2.0'} +AUTH_REF.update(fakes.TEST_RESPONSE_DICT['access']) +SERVICE_CATALOG = service_catalog.ServiceCatalogV2(AUTH_REF) class Container(object): @@ -49,6 +40,18 @@ def __init__(self): pass +class FakeOptions(object): + def __init__(self, **kwargs): + for option in auth.OPTIONS_LIST: + setattr(self, 'os_' + option.replace('-', '_'), None) + self.os_auth_plugin = None + self.os_identity_api_version = '2.0' + self.timing = None + self.os_region_name = None + self.os_url = None + self.__dict__.update(kwargs) + + class TestClientCache(utils.TestCase): def test_singleton(self): @@ -58,30 +61,38 @@ def test_singleton(self): self.assertEqual(c.attr, c.attr) -@mock.patch('keystoneclient.session.Session') class TestClientManager(utils.TestCase): def setUp(self): super(TestClientManager, self).setUp() - - clientmanager.ClientManager.identity = \ - clientmanager.ClientCache(FakeMakeClient) - - def test_client_manager_token(self, mock): + self.mock = mock.Mock() + self.requests = self.useFixture(fixture.Fixture()) + # fake v2password token retrieval + self.stub_auth(json=fakes.TEST_RESPONSE_DICT) + # fake v3password token retrieval + self.stub_auth(json=fakes.TEST_RESPONSE_DICT_V3, + url='/'.join([fakes.AUTH_URL, 'auth/tokens'])) + # fake password version endpoint discovery + self.stub_auth(json=fakes.TEST_VERSIONS, + url=fakes.AUTH_URL, + verb='GET') + + def test_client_manager_token(self): client_manager = clientmanager.ClientManager( - token=AUTH_TOKEN, - url=AUTH_URL, - verify=True, + auth_options=FakeOptions(os_token=fakes.AUTH_TOKEN, + os_auth_url=fakes.AUTH_URL, + os_auth_plugin='v2token'), api_version=API_VERSION, + verify=True ) self.assertEqual( - AUTH_TOKEN, + fakes.AUTH_TOKEN, client_manager._token, ) self.assertEqual( - AUTH_URL, - client_manager._url, + fakes.AUTH_URL, + client_manager._auth_url, ) self.assertIsInstance( client_manager.auth, @@ -90,26 +101,26 @@ def test_client_manager_token(self, mock): self.assertFalse(client_manager._insecure) self.assertTrue(client_manager._verify) - def test_client_manager_password(self, mock): + def test_client_manager_password(self): client_manager = clientmanager.ClientManager( - auth_url=AUTH_URL, - username=USERNAME, - password=PASSWORD, - verify=False, + auth_options=FakeOptions(os_auth_url=fakes.AUTH_URL, + os_username=fakes.USERNAME, + os_password=fakes.PASSWORD), api_version=API_VERSION, + verify=False, ) self.assertEqual( - AUTH_URL, + fakes.AUTH_URL, client_manager._auth_url, ) self.assertEqual( - USERNAME, + fakes.USERNAME, client_manager._username, ) self.assertEqual( - PASSWORD, + fakes.PASSWORD, client_manager._password, ) self.assertIsInstance( @@ -119,16 +130,87 @@ def test_client_manager_password(self, mock): self.assertTrue(client_manager._insecure) self.assertFalse(client_manager._verify) - def test_client_manager_password_verify_ca(self, mock): + # These need to stick around until the old-style clients are gone + self.assertEqual( + AUTH_REF, + client_manager.auth_ref, + ) + self.assertEqual( + fakes.AUTH_TOKEN, + client_manager._token, + ) + self.assertEqual( + dir(SERVICE_CATALOG), + dir(client_manager._service_catalog), + ) + + def stub_auth(self, json=None, url=None, verb=None, **kwargs): + subject_token = fakes.AUTH_TOKEN + base_url = fakes.AUTH_URL + if json: + text = jsonutils.dumps(json) + headers = {'X-Subject-Token': subject_token, + 'Content-Type': 'application/json'} + if not url: + url = '/'.join([base_url, 'tokens']) + url = url.replace("/?", "?") + if not verb: + verb = 'POST' + self.requests.register_uri(verb, + url, + headers=headers, + text=text) + + def test_client_manager_password_verify_ca(self): client_manager = clientmanager.ClientManager( - auth_url=AUTH_URL, - username=USERNAME, - password=PASSWORD, - verify='cafile', + auth_options=FakeOptions(os_auth_url=fakes.AUTH_URL, + os_username=fakes.USERNAME, + os_password=fakes.PASSWORD, + os_auth_plugin='v2password'), api_version=API_VERSION, + verify='cafile', ) self.assertFalse(client_manager._insecure) self.assertTrue(client_manager._verify) self.assertEqual('cafile', client_manager._cacert) + + def _client_manager_guess_auth_plugin(self, auth_params, + api_version, auth_plugin): + auth_params['os_auth_plugin'] = auth_plugin + auth_params['os_identity_api_version'] = api_version + client_manager = clientmanager.ClientManager( + auth_options=FakeOptions(**auth_params), + api_version=API_VERSION, + verify=True + ) + self.assertEqual( + auth_plugin, + client_manager._auth_plugin, + ) + + def test_client_manager_guess_auth_plugin(self): + # test token auth + params = dict(os_token=fakes.AUTH_TOKEN, + os_auth_url=fakes.AUTH_URL) + self._client_manager_guess_auth_plugin(params, '2.0', 'v2token') + self._client_manager_guess_auth_plugin(params, '3', 'v3token') + self._client_manager_guess_auth_plugin(params, 'XXX', 'token') + # test service auth + params = dict(os_token=fakes.AUTH_TOKEN, os_url='test') + self._client_manager_guess_auth_plugin(params, 'XXX', '') + # test password auth + params = dict(os_auth_url=fakes.AUTH_URL, + os_username=fakes.USERNAME, + os_password=fakes.PASSWORD) + self._client_manager_guess_auth_plugin(params, '2.0', 'v2password') + self._client_manager_guess_auth_plugin(params, '3', 'v3password') + self._client_manager_guess_auth_plugin(params, 'XXX', 'password') + + def test_client_manager_guess_auth_plugin_failure(self): + self.assertRaises(exc.CommandError, + clientmanager.ClientManager, + auth_options=FakeOptions(os_auth_plugin=''), + api_version=API_VERSION, + verify=True) diff --git a/openstackclient/tests/fakes.py b/openstackclient/tests/fakes.py index 5a1fc005e4..f8b7bb6f39 100644 --- a/openstackclient/tests/fakes.py +++ b/openstackclient/tests/fakes.py @@ -22,6 +22,142 @@ AUTH_TOKEN = "foobar" AUTH_URL = "http://0.0.0.0" +USERNAME = "itchy" +PASSWORD = "scratchy" +TEST_RESPONSE_DICT = { + "access": { + "metadata": { + "is_admin": 0, + "roles": [ + "1234", + ] + }, + "serviceCatalog": [ + { + "endpoints": [ + { + "adminURL": AUTH_URL + "/v2.0", + "id": "1234", + "internalURL": AUTH_URL + "/v2.0", + "publicURL": AUTH_URL + "/v2.0", + "region": "RegionOne" + } + ], + "endpoints_links": [], + "name": "keystone", + "type": "identity" + } + ], + "token": { + "expires": "2035-01-01T00:00:01Z", + "id": AUTH_TOKEN, + "issued_at": "2013-01-01T00:00:01.692048", + "tenant": { + "description": None, + "enabled": True, + "id": "1234", + "name": "testtenant" + } + }, + "user": { + "id": "5678", + "name": USERNAME, + "roles": [ + { + "name": "testrole" + }, + ], + "roles_links": [], + "username": USERNAME + } + } +} +TEST_RESPONSE_DICT_V3 = { + "token": { + "audit_ids": [ + "a" + ], + "catalog": [ + ], + "expires_at": "2034-09-29T18:27:15.978064Z", + "extras": {}, + "issued_at": "2014-09-29T17:27:15.978097Z", + "methods": [ + "password" + ], + "project": { + "domain": { + "id": "default", + "name": "Default" + }, + "id": "bbb", + "name": "project" + }, + "roles": [ + ], + "user": { + "domain": { + "id": "default", + "name": "Default" + }, + "id": "aaa", + "name": USERNAME + } + } +} +TEST_VERSIONS = { + "versions": { + "values": [ + { + "id": "v3.0", + "links": [ + { + "href": AUTH_URL, + "rel": "self" + } + ], + "media-types": [ + { + "base": "application/json", + "type": "application/vnd.openstack.identity-v3+json" + }, + { + "base": "application/xml", + "type": "application/vnd.openstack.identity-v3+xml" + } + ], + "status": "stable", + "updated": "2013-03-06T00:00:00Z" + }, + { + "id": "v2.0", + "links": [ + { + "href": AUTH_URL, + "rel": "self" + }, + { + "href": "http://docs.openstack.org/", + "rel": "describedby", + "type": "text/html" + } + ], + "media-types": [ + { + "base": "application/json", + "type": "application/vnd.openstack.identity-v2.0+json" + }, + { + "base": "application/xml", + "type": "application/vnd.openstack.identity-v2.0+xml" + } + ], + "status": "stable", + "updated": "2014-04-17T00:00:00Z" + } + ] + } +} class FakeStdout: diff --git a/openstackclient/tests/test_shell.py b/openstackclient/tests/test_shell.py index c180289e7d..b0c1452ef9 100644 --- a/openstackclient/tests/test_shell.py +++ b/openstackclient/tests/test_shell.py @@ -34,6 +34,8 @@ DEFAULT_REGION_NAME = "ZZ9_Plural_Z_Alpha" DEFAULT_TOKEN = "token" DEFAULT_SERVICE_URL = "http://127.0.0.1:8771/v3.0/" +DEFAULT_AUTH_PLUGIN = "v2password" + DEFAULT_COMPUTE_API_VERSION = "2" DEFAULT_IDENTITY_API_VERSION = "2.0" @@ -106,6 +108,8 @@ def _assert_password_auth(self, cmd_options, default_args): default_args["region_name"]) self.assertEqual(_shell.options.os_trust_id, default_args["trust_id"]) + self.assertEqual(_shell.options.os_auth_plugin, + default_args['auth_plugin']) def _assert_token_auth(self, cmd_options, default_args): with mock.patch("openstackclient.shell.OpenStackShell.initialize_app", @@ -115,7 +119,8 @@ def _assert_token_auth(self, cmd_options, default_args): self.app.assert_called_with(["list", "role"]) self.assertEqual(_shell.options.os_token, default_args["os_token"]) - self.assertEqual(_shell.options.os_url, default_args["os_url"]) + self.assertEqual(_shell.options.os_auth_url, + default_args["os_auth_url"]) def _assert_cli(self, cmd_options, default_args): with mock.patch("openstackclient.shell.OpenStackShell.initialize_app", @@ -175,9 +180,9 @@ def test_only_url_flow(self): "auth_url": DEFAULT_AUTH_URL, "project_id": "", "project_name": "", + "user_domain_id": "", "domain_id": "", "domain_name": "", - "user_domain_id": "", "user_domain_name": "", "project_domain_id": "", "project_domain_name": "", @@ -185,6 +190,7 @@ def test_only_url_flow(self): "password": "", "region_name": "", "trust_id": "", + "auth_plugin": "", } self._assert_password_auth(flag, kwargs) @@ -204,6 +210,7 @@ def test_only_project_id_flow(self): "password": "", "region_name": "", "trust_id": "", + "auth_plugin": "", } self._assert_password_auth(flag, kwargs) @@ -223,44 +230,7 @@ def test_only_project_name_flow(self): "password": "", "region_name": "", "trust_id": "", - } - self._assert_password_auth(flag, kwargs) - - def test_only_tenant_id_flow(self): - flag = "--os-tenant-id " + DEFAULT_PROJECT_ID - kwargs = { - "auth_url": "", - "project_id": DEFAULT_PROJECT_ID, - "project_name": "", - "domain_id": "", - "domain_name": "", - "user_domain_id": "", - "user_domain_name": "", - "project_domain_id": "", - "project_domain_name": "", - "username": "", - "password": "", - "region_name": "", - "trust_id": "", - } - self._assert_password_auth(flag, kwargs) - - def test_only_tenant_name_flow(self): - flag = "--os-tenant-name " + DEFAULT_PROJECT_NAME - kwargs = { - "auth_url": "", - "project_id": "", - "project_name": DEFAULT_PROJECT_NAME, - "domain_id": "", - "domain_name": "", - "user_domain_id": "", - "user_domain_name": "", - "project_domain_id": "", - "project_domain_name": "", - "username": "", - "password": "", - "region_name": "", - "trust_id": "", + "auth_plugin": "", } self._assert_password_auth(flag, kwargs) @@ -280,6 +250,7 @@ def test_only_domain_id_flow(self): "password": "", "region_name": "", "trust_id": "", + "auth_plugin": "", } self._assert_password_auth(flag, kwargs) @@ -299,6 +270,7 @@ def test_only_domain_name_flow(self): "password": "", "region_name": "", "trust_id": "", + "auth_plugin": "", } self._assert_password_auth(flag, kwargs) @@ -318,6 +290,7 @@ def test_only_user_domain_id_flow(self): "password": "", "region_name": "", "trust_id": "", + "auth_plugin": "", } self._assert_password_auth(flag, kwargs) @@ -337,6 +310,7 @@ def test_only_user_domain_name_flow(self): "password": "", "region_name": "", "trust_id": "", + "auth_plugin": "", } self._assert_password_auth(flag, kwargs) @@ -356,6 +330,7 @@ def test_only_project_domain_id_flow(self): "password": "", "region_name": "", "trust_id": "", + "auth_plugin": "", } self._assert_password_auth(flag, kwargs) @@ -375,6 +350,7 @@ def test_only_project_domain_name_flow(self): "password": "", "region_name": "", "trust_id": "", + "auth_plugin": "", } self._assert_password_auth(flag, kwargs) @@ -394,6 +370,7 @@ def test_only_username_flow(self): "password": "", "region_name": "", "trust_id": "", + "auth_plugin": "", } self._assert_password_auth(flag, kwargs) @@ -413,6 +390,7 @@ def test_only_password_flow(self): "password": DEFAULT_PASSWORD, "region_name": "", "trust_id": "", + "auth_plugin": "", } self._assert_password_auth(flag, kwargs) @@ -432,6 +410,7 @@ def test_only_region_name_flow(self): "password": "", "region_name": DEFAULT_REGION_NAME, "trust_id": "", + "auth_plugin": "", } self._assert_password_auth(flag, kwargs) @@ -451,6 +430,27 @@ def test_only_trust_id_flow(self): "password": "", "region_name": "", "trust_id": "1234", + "auth_plugin": "", + } + self._assert_password_auth(flag, kwargs) + + def test_only_auth_plugin_flow(self): + flag = "--os-auth-plugin " + "v2password" + kwargs = { + "auth_url": "", + "project_id": "", + "project_name": "", + "domain_id": "", + "domain_name": "", + "user_domain_id": "", + "user_domain_name": "", + "project_domain_id": "", + "project_domain_name": "", + "username": "", + "password": "", + "region_name": "", + "trust_id": "", + "auth_plugin": DEFAULT_AUTH_PLUGIN } self._assert_password_auth(flag, kwargs) @@ -460,7 +460,7 @@ def setUp(self): super(TestShellTokenAuth, self).setUp() env = { "OS_TOKEN": DEFAULT_TOKEN, - "OS_URL": DEFAULT_SERVICE_URL, + "OS_AUTH_URL": DEFAULT_SERVICE_URL, } self.orig_env, os.environ = os.environ, env.copy() @@ -472,7 +472,7 @@ def test_default_auth(self): flag = "" kwargs = { "os_token": DEFAULT_TOKEN, - "os_url": DEFAULT_SERVICE_URL + "os_auth_url": DEFAULT_SERVICE_URL } self._assert_token_auth(flag, kwargs) @@ -481,7 +481,7 @@ def test_empty_auth(self): flag = "" kwargs = { "os_token": "", - "os_url": "" + "os_auth_url": "" } self._assert_token_auth(flag, kwargs) diff --git a/requirements.txt b/requirements.txt index 04f448834f..447b534a5e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,4 @@ python-cinderclient>=1.1.0 python-neutronclient>=2.3.6,<3 requests>=1.2.1,!=2.4.0 six>=1.7.0 +stevedore>=1.0.0 From bb71df9ced18f343911fb90e745b4d875672cf27 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 3 Oct 2014 22:54:42 -0400 Subject: [PATCH 0217/3494] Mark identity v2 resources for translation mark v2 catalog, ec2, endpoint, project, role, service and token Change-Id: I14a5852bfee4ca9e25130d001fdadd7778ad0996 --- openstackclient/identity/v2_0/catalog.py | 3 +- openstackclient/identity/v2_0/ec2creds.py | 15 +++++----- openstackclient/identity/v2_0/endpoint.py | 17 +++++------ openstackclient/identity/v2_0/project.py | 35 ++++++++++++----------- openstackclient/identity/v2_0/role.py | 31 +++++++++----------- openstackclient/identity/v2_0/service.py | 19 ++++++------ openstackclient/identity/v2_0/token.py | 4 ++- 7 files changed, 64 insertions(+), 60 deletions(-) diff --git a/openstackclient/identity/v2_0/catalog.py b/openstackclient/identity/v2_0/catalog.py index 7bda1acb68..1a96fdf60f 100644 --- a/openstackclient/identity/v2_0/catalog.py +++ b/openstackclient/identity/v2_0/catalog.py @@ -20,6 +20,7 @@ from cliff import show from openstackclient.common import utils +from openstackclient.i18n import _ # noqa def _format_endpoints(eps=None): @@ -67,7 +68,7 @@ def get_parser(self, prog_name): parser.add_argument( 'service', metavar='', - help='Service to display (type, name or ID)', + help=_('Service to display (type, name or ID)'), ) return parser diff --git a/openstackclient/identity/v2_0/ec2creds.py b/openstackclient/identity/v2_0/ec2creds.py index 74c9d5ebab..fd8eef8463 100644 --- a/openstackclient/identity/v2_0/ec2creds.py +++ b/openstackclient/identity/v2_0/ec2creds.py @@ -24,6 +24,7 @@ from cliff import show from openstackclient.common import utils +from openstackclient.i18n import _ # noqa class CreateEC2Creds(show.ShowOne): @@ -36,12 +37,12 @@ def get_parser(self, prog_name): parser.add_argument( '--project', metavar='', - help='Specify a project [admin only]', + help=_('Specify a project [admin only]'), ) parser.add_argument( '--user', metavar='', - help='Specify a user [admin only]', + help=_('Specify a user [admin only]'), ) return parser @@ -83,12 +84,12 @@ def get_parser(self, prog_name): parser.add_argument( 'access_key', metavar='', - help='Credentials access key', + help=_('Credentials access key'), ) parser.add_argument( '--user', metavar='', - help='Specify a user [admin only]', + help=_('Specify a user [admin only]'), ) return parser @@ -118,7 +119,7 @@ def get_parser(self, prog_name): parser.add_argument( '--user', metavar='', - help='Specify a user [admin only]', + help=_('Specify a user [admin only]'), ) return parser @@ -156,12 +157,12 @@ def get_parser(self, prog_name): parser.add_argument( 'access_key', metavar='', - help='Credentials access key', + help=_('Credentials access key'), ) parser.add_argument( '--user', metavar='', - help='Specify a user [admin only]', + help=_('Specify a user [admin only]'), ) return parser diff --git a/openstackclient/identity/v2_0/endpoint.py b/openstackclient/identity/v2_0/endpoint.py index 36f52cad4d..f7d7883109 100644 --- a/openstackclient/identity/v2_0/endpoint.py +++ b/openstackclient/identity/v2_0/endpoint.py @@ -23,6 +23,7 @@ from cliff import show from openstackclient.common import utils +from openstackclient.i18n import _ # noqa from openstackclient.identity import common @@ -36,24 +37,24 @@ def get_parser(self, prog_name): parser.add_argument( 'service', metavar='', - help='New endpoint service') + help=_('New endpoint service')) parser.add_argument( '--region', metavar='', - help='New endpoint region') + help=_('New endpoint region')) parser.add_argument( '--publicurl', metavar='', required=True, - help='New endpoint public URL') + help=_('New endpoint public URL')) parser.add_argument( '--adminurl', metavar='', - help='New endpoint admin URL') + help=_('New endpoint admin URL')) parser.add_argument( '--internalurl', metavar='', - help='New endpoint internal URL') + help=_('New endpoint internal URL')) return parser def take_action(self, parsed_args): @@ -84,7 +85,7 @@ def get_parser(self, prog_name): parser.add_argument( 'endpoint', metavar='', - help='ID of endpoint to delete') + help=_('ID of endpoint to delete')) return parser def take_action(self, parsed_args): @@ -105,7 +106,7 @@ def get_parser(self, prog_name): '--long', action='store_true', default=False, - help='List additional fields in output') + help=_('List additional fields in output')) return parser def take_action(self, parsed_args): @@ -139,7 +140,7 @@ def get_parser(self, prog_name): parser.add_argument( 'endpoint_or_service', metavar='', - help='Endpoint ID or name, type or ID of service to display') + help=_('Endpoint ID or name, type or ID of service to display')) return parser def take_action(self, parsed_args): diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index ebd65df7d6..7ead0890d4 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -21,10 +21,11 @@ from cliff import command from cliff import lister from cliff import show - from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc + from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.i18n import _ # noqa class CreateProject(show.ShowOne): @@ -37,30 +38,30 @@ def get_parser(self, prog_name): parser.add_argument( 'name', metavar='', - help='New project name', + help=_('New project name'), ) parser.add_argument( '--description', metavar='', - help='New project description', + help=_('New project description'), ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', action='store_true', - help='Enable project (default)', + help=_('Enable project (default)'), ) enable_group.add_argument( '--disable', action='store_true', - help='Disable project', + help=_('Disable project'), ) parser.add_argument( '--property', metavar='', action=parseractions.KeyValueAction, - help='Property to add for this project ' - '(repeat option to set multiple properties)', + help=_('Property to add for this project ' + '(repeat option to set multiple properties)'), ) return parser @@ -97,7 +98,7 @@ def get_parser(self, prog_name): parser.add_argument( 'project', metavar='', - help='Project to delete (name or ID)', + help=_('Project to delete (name or ID)'), ) return parser @@ -125,7 +126,7 @@ def get_parser(self, prog_name): '--long', action='store_true', default=False, - help='List additional fields in output', + help=_('List additional fields in output'), ) return parser @@ -153,35 +154,35 @@ def get_parser(self, prog_name): parser.add_argument( 'project', metavar='', - help='Project to change (name or ID)', + help=_('Project to change (name or ID)'), ) parser.add_argument( '--name', metavar='', - help='New project name', + help=_('New project name'), ) parser.add_argument( '--description', metavar='', - help='New project description', + help=_('New project description'), ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', action='store_true', - help='Enable project', + help=_('Enable project'), ) enable_group.add_argument( '--disable', action='store_true', - help='Disable project', + help=_('Disable project'), ) parser.add_argument( '--property', metavar='', action=parseractions.KeyValueAction, - help='Property to add for this project ' - '(repeat option to set multiple properties)', + help=_('Property to add for this project ' + '(repeat option to set multiple properties)'), ) return parser @@ -233,7 +234,7 @@ def get_parser(self, prog_name): parser.add_argument( 'project', metavar='', - help='Project to display (name or ID)') + help=_('Project to display (name or ID)')) return parser def take_action(self, parsed_args): diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index faf48ed95f..4c45004dae 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -24,6 +24,7 @@ from openstackclient.common import exceptions from openstackclient.common import utils +from openstackclient.i18n import _ # noqa class AddRole(show.ShowOne): @@ -36,18 +37,17 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help='Role name or ID to add to user') + help=_('Role name or ID to add to user')) parser.add_argument( '--project', metavar='', required=True, - help='Include project (name or ID)', - ) + help=_('Include project (name or ID)')) parser.add_argument( '--user', metavar='', required=True, - help='Name or ID of user to include') + help=_('Name or ID of user to include')) return parser def take_action(self, parsed_args): @@ -80,7 +80,7 @@ def get_parser(self, prog_name): parser.add_argument( 'role_name', metavar='', - help='New role name') + help=_('New role name')) return parser def take_action(self, parsed_args): @@ -103,8 +103,7 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help='Name or ID of role to delete', - ) + help=_('Name or ID of role to delete')) return parser def take_action(self, parsed_args): @@ -147,12 +146,11 @@ def get_parser(self, prog_name): 'user', metavar='', nargs='?', - help='Name or ID of user to include') + help=_('Name or ID of user to include')) parser.add_argument( '--project', metavar='', - help='Include project (name or ID)', - ) + help=_('Include project (name or ID)')) return parser def take_action(self, parsed_args): @@ -167,13 +165,13 @@ def take_action(self, parsed_args): if self.app.client_manager.auth_ref: parsed_args.project = auth_ref.project_id else: - msg = "Project must be specified" + msg = _("Project must be specified") raise exceptions.CommandError(msg) if not parsed_args.user: if self.app.client_manager.auth_ref: parsed_args.user = auth_ref.user_id else: - msg = "User must be specified" + msg = _("User must be specified") raise exceptions.CommandError(msg) project = utils.find_resource( @@ -213,18 +211,17 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help='Role name or ID to remove from user') + help=_('Role name or ID to remove from user')) parser.add_argument( '--project', metavar='', required=True, - help='Project to include (name or ID)', - ) + help=_('Project to include (name or ID)')) parser.add_argument( '--user', metavar='', required=True, - help='Name or ID of user') + help=_('Name or ID of user')) return parser def take_action(self, parsed_args): @@ -252,7 +249,7 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help='Name or ID of role to display') + help=_('Name or ID of role to display')) return parser def take_action(self, parsed_args): diff --git a/openstackclient/identity/v2_0/service.py b/openstackclient/identity/v2_0/service.py index 138ed3b097..458dce7cba 100644 --- a/openstackclient/identity/v2_0/service.py +++ b/openstackclient/identity/v2_0/service.py @@ -24,6 +24,7 @@ from openstackclient.common import exceptions from openstackclient.common import utils +from openstackclient.i18n import _ # noqa from openstackclient.identity import common @@ -37,18 +38,18 @@ def get_parser(self, prog_name): parser.add_argument( 'name', metavar='', - help='New service name', + help=_('New service name'), ) parser.add_argument( '--type', metavar='', required=True, - help='New service type (compute, image, identity, volume, etc)', + help=_('New service type (compute, image, identity, volume, etc)'), ) parser.add_argument( '--description', metavar='', - help='New service description', + help=_('New service description'), ) return parser @@ -76,7 +77,7 @@ def get_parser(self, prog_name): parser.add_argument( 'service', metavar='', - help='Service to delete (name or ID)', + help=_('Service to delete (name or ID)'), ) return parser @@ -99,7 +100,7 @@ def get_parser(self, prog_name): '--long', action='store_true', default=False, - help='List additional fields in output') + help=_('List additional fields in output')) return parser def take_action(self, parsed_args): @@ -127,13 +128,13 @@ def get_parser(self, prog_name): parser.add_argument( 'service', metavar='', - help='Service to display (type, name or ID)', + help=_('Service to display (type, name or ID)'), ) parser.add_argument( '--catalog', action='store_true', default=False, - help='Show service catalog information', + help=_('Show service catalog information'), ) return parser @@ -150,8 +151,8 @@ def take_action(self, parsed_args): info.update(service_endpoints[0]) return zip(*sorted(six.iteritems(info))) - msg = ("No service catalog with a type, name or ID of '%s' " - "exists." % (parsed_args.service)) + msg = _("No service catalog with a type, name or ID of '%s' " + "exists.") % (parsed_args.service) raise exceptions.CommandError(msg) else: service = common.find_service(identity_client, parsed_args.service) diff --git a/openstackclient/identity/v2_0/token.py b/openstackclient/identity/v2_0/token.py index f3fedc0107..c8b003ee02 100644 --- a/openstackclient/identity/v2_0/token.py +++ b/openstackclient/identity/v2_0/token.py @@ -21,6 +21,8 @@ from cliff import command from cliff import show +from openstackclient.i18n import _ # noqa + class IssueToken(show.ShowOne): """Issue new token""" @@ -49,7 +51,7 @@ def get_parser(self, prog_name): parser.add_argument( 'token', metavar='', - help='Token to be deleted', + help=_('Token to be deleted'), ) return parser From 364071a90bfe9dcec1d02a349c33dc8422fc14f3 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 7 Oct 2014 02:15:15 -0400 Subject: [PATCH 0218/3494] Add domain parameters to user show for Identity V3 Update `user show` for Identity V3 to account for a domain argument, in doing so, also update `find resource` to be more flexible by allowing **kwargs. Also update `group show` and `project show` since they follow the same logic as a user within a group. Change-Id: Ib828e4dbeb0bd31164396069ce8a64c873179779 Closes-Bug: #1378165 --- openstackclient/common/utils.py | 28 +++++++++++++++++++++++--- openstackclient/identity/v3/group.py | 23 ++++++++++++++++----- openstackclient/identity/v3/project.py | 23 +++++++++++++++------ openstackclient/identity/v3/user.py | 22 +++++++++++++------- 4 files changed, 75 insertions(+), 21 deletions(-) diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py index 818f8d4771..9ad3823cc8 100644 --- a/openstackclient/common/utils.py +++ b/openstackclient/common/utils.py @@ -26,8 +26,27 @@ from openstackclient.common import exceptions -def find_resource(manager, name_or_id): - """Helper for the _find_* methods.""" +def find_resource(manager, name_or_id, **kwargs): + """Helper for the _find_* methods. + + :param manager: A client manager class + :param name_or_id: The resource we are trying to find + :param kwargs: To be used in calling .find() + :rtype: The found resource + + This method will attempt to find a resource in a variety of ways. + Primarily .get() methods will be called with `name_or_id` as an integer + value, and tried again as a string value. + + If both fail, then a .find() is attempted, which is essentially calling + a .list() function with a 'name' query parameter that is set to + `name_or_id`. + + Lastly, if any kwargs are passed in, they will be treated as additional + query parameters. This is particularly handy in the case of finding + resources in a domain. + + """ # Try to get entity as integer id try: @@ -49,7 +68,10 @@ def find_resource(manager, name_or_id): except Exception: pass - kwargs = {} + if len(kwargs) == 0: + kwargs = {} + + # Prepare the kwargs for calling find if 'NAME_ATTR' in manager.resource_class.__dict__: # novaclient does this for oddball resources kwargs[manager.resource_class.NAME_ATTR] = name_or_id diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index 4eb144896d..fdb9501ad6 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -24,6 +24,7 @@ from cliff import show from openstackclient.common import utils +from openstackclient.identity import common class AddUserToGroup(command.Command): @@ -321,14 +322,26 @@ def get_parser(self, prog_name): parser.add_argument( 'group', metavar='', - help='Name or ID of group to display') + help='Name or ID of group to display', + ) + parser.add_argument( + '--domain', + metavar='', + help='Domain where group resides (name or ID)', + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - group = utils.find_resource(identity_client.groups, parsed_args.group) - info = {} - info.update(group._info) - return zip(*sorted(six.iteritems(info))) + if parsed_args.domain: + domain = common.find_domain(identity_client, parsed_args.domain) + group = utils.find_resource(identity_client.groups, + parsed_args.group, + domain_id=domain.id) + else: + group = utils.find_resource(identity_client.groups, + parsed_args.group) + + return zip(*sorted(six.iteritems(group._info))) diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index fa935f0bf5..ec8e5a3b1a 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -264,15 +264,26 @@ def get_parser(self, prog_name): parser.add_argument( 'project', metavar='', - help='Name or ID of project to display') + help='Name or ID of project to display', + ) + parser.add_argument( + '--domain', + metavar='', + help='Domain where project resides (name or ID)', + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - project = utils.find_resource(identity_client.projects, - parsed_args.project) - info = {} - info.update(project._info) - return zip(*sorted(six.iteritems(info))) + if parsed_args.domain: + domain = common.find_domain(identity_client, parsed_args.domain) + project = utils.find_resource(identity_client.projects, + parsed_args.project, + domain_id=domain.id) + else: + project = utils.find_resource(identity_client.projects, + parsed_args.project) + + return zip(*sorted(six.iteritems(project._info))) diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index e4eb7526c2..73bb7f1308 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -23,6 +23,7 @@ from cliff import show from openstackclient.common import utils +from openstackclient.identity import common class CreateUser(show.ShowOne): @@ -364,17 +365,24 @@ def get_parser(self, prog_name): metavar='', help='User to display (name or ID)', ) + parser.add_argument( + '--domain', + metavar='', + help='Domain where user resides (name or ID)', + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - user = utils.find_resource( - identity_client.users, - parsed_args.user, - ) + if parsed_args.domain: + domain = common.find_domain(identity_client, parsed_args.domain) + user = utils.find_resource(identity_client.users, + parsed_args.user, + domain_id=domain.id) + else: + user = utils.find_resource(identity_client.users, + parsed_args.user) - info = {} - info.update(user._info) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(six.iteritems(user._info))) From b61db3eb725d19d991c9eac39546c2a6dbc6ffbd Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 3 Oct 2014 22:29:47 -0400 Subject: [PATCH 0219/3494] Add translation markers for user v2 actions implements bp use_i18n Change-Id: I86508a232c9cf88695b7982dad0b9b02eaf8b3a1 --- MANIFEST.in | 2 + babel.cfg | 1 + openstackclient/i18n.py | 2 +- openstackclient/identity/v2_0/user.py | 41 +++++----- .../locale/python-openstackclient.pot | 78 +++++++++++++++++++ requirements.txt | 1 + setup.cfg | 14 ++++ test-requirements.txt | 2 - 8 files changed, 118 insertions(+), 23 deletions(-) create mode 100644 babel.cfg create mode 100644 python-openstackclient/locale/python-openstackclient.pot diff --git a/MANIFEST.in b/MANIFEST.in index fd87b80fdf..aeabc0d31a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,10 +1,12 @@ include AUTHORS +include babel.cfg include ChangeLog include LICENSE include README.rst recursive-include doc * recursive-include tests * +recursive-include python-openstackclient *.po *.pot exclude .gitignore exclude .gitreview diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 0000000000..efceab818b --- /dev/null +++ b/babel.cfg @@ -0,0 +1 @@ +[python: **.py] diff --git a/openstackclient/i18n.py b/openstackclient/i18n.py index bd52d64838..3611b315c9 100644 --- a/openstackclient/i18n.py +++ b/openstackclient/i18n.py @@ -15,7 +15,7 @@ from oslo import i18n -_translators = i18n.TranslatorFactory(domain='openstackclient') +_translators = i18n.TranslatorFactory(domain='python-openstackclient') # The primary translation function using the well-known name "_" _ = _translators.primary diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index 93ab94fe0e..729c6ac81f 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -21,9 +21,10 @@ from cliff import command from cliff import lister from cliff import show - from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc + from openstackclient.common import utils +from openstackclient.i18n import _ # noqa class CreateUser(show.ShowOne): @@ -36,39 +37,39 @@ def get_parser(self, prog_name): parser.add_argument( 'name', metavar='', - help='New user name', + help=_('New user name'), ) parser.add_argument( '--password', metavar='', - help='New user password', + help=_('New user password'), ) parser.add_argument( '--password-prompt', dest="password_prompt", action="store_true", - help='Prompt interactively for password', + help=_('Prompt interactively for password'), ) parser.add_argument( '--email', metavar='', - help='New user email address', + help=_('New user email address'), ) parser.add_argument( '--project', metavar='', - help='Set default project (name or ID)', + help=_('Set default project (name or ID)'), ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', action='store_true', - help='Enable user (default)', + help=_('Enable user (default)'), ) enable_group.add_argument( '--disable', action='store_true', - help='Disable user', + help=_('Disable user'), ) return parser @@ -120,7 +121,7 @@ def get_parser(self, prog_name): parser.add_argument( 'user', metavar='', - help='User to delete (name or ID)', + help=_('User to delete (name or ID)'), ) return parser @@ -147,13 +148,13 @@ def get_parser(self, prog_name): parser.add_argument( '--project', metavar='', - help='Filter users by project (name or ID)', + help=_('Filter users by project (name or ID)'), ) parser.add_argument( '--long', action='store_true', default=False, - help='List additional fields in output') + help=_('List additional fields in output')) return parser def take_action(self, parsed_args): @@ -237,44 +238,44 @@ def get_parser(self, prog_name): parser.add_argument( 'user', metavar='', - help='User to change (name or ID)', + help=_('User to change (name or ID)'), ) parser.add_argument( '--name', metavar='', - help='New user name', + help=_('New user name'), ) parser.add_argument( '--password', metavar='', - help='New user password', + help=_('New user password'), ) parser.add_argument( '--password-prompt', dest="password_prompt", action="store_true", - help='Prompt interactively for password', + help=_('Prompt interactively for password'), ) parser.add_argument( '--email', metavar='', - help='New user email address', + help=_('New user email address'), ) parser.add_argument( '--project', metavar='', - help='New default project (name or ID)', + help=_('New default project (name or ID)'), ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', action='store_true', - help='Enable user (default)', + help=_('Enable user (default)'), ) enable_group.add_argument( '--disable', action='store_true', - help='Disable user', + help=_('Disable user'), ) return parser @@ -340,7 +341,7 @@ def get_parser(self, prog_name): parser.add_argument( 'user', metavar='', - help='User to display (name or ID)', + help=_('User to display (name or ID)'), ) return parser diff --git a/python-openstackclient/locale/python-openstackclient.pot b/python-openstackclient/locale/python-openstackclient.pot new file mode 100644 index 0000000000..5dcdac88b9 --- /dev/null +++ b/python-openstackclient/locale/python-openstackclient.pot @@ -0,0 +1,78 @@ +# Translations template for python-openstackclient. +# Copyright (C) 2014 ORGANIZATION +# This file is distributed under the same license as the +# python-openstackclient project. +# FIRST AUTHOR , 2014. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: python-openstackclient 0.4.2.dev43.gaf67658\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2014-10-09 13:11-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" + +#: openstackclient/identity/v2_0/user.py:40 +#: openstackclient/identity/v2_0/user.py:246 +msgid "New user name" +msgstr "" + +#: openstackclient/identity/v2_0/user.py:45 +#: openstackclient/identity/v2_0/user.py:251 +msgid "New user password" +msgstr "" + +#: openstackclient/identity/v2_0/user.py:51 +#: openstackclient/identity/v2_0/user.py:257 +msgid "Prompt interactively for password" +msgstr "" + +#: openstackclient/identity/v2_0/user.py:56 +#: openstackclient/identity/v2_0/user.py:262 +msgid "New user email address" +msgstr "" + +#: openstackclient/identity/v2_0/user.py:61 +msgid "Set default project (name or ID)" +msgstr "" + +#: openstackclient/identity/v2_0/user.py:67 +#: openstackclient/identity/v2_0/user.py:273 +msgid "Enable user (default)" +msgstr "" + +#: openstackclient/identity/v2_0/user.py:72 +#: openstackclient/identity/v2_0/user.py:278 +msgid "Disable user" +msgstr "" + +#: openstackclient/identity/v2_0/user.py:124 +msgid "User to delete (name or ID)" +msgstr "" + +#: openstackclient/identity/v2_0/user.py:151 +msgid "Filter users by project (name or ID)" +msgstr "" + +#: openstackclient/identity/v2_0/user.py:157 +msgid "List additional fields in output" +msgstr "" + +#: openstackclient/identity/v2_0/user.py:241 +msgid "User to change (name or ID)" +msgstr "" + +#: openstackclient/identity/v2_0/user.py:267 +msgid "New default project (name or ID)" +msgstr "" + +#: openstackclient/identity/v2_0/user.py:344 +msgid "User to display (name or ID)" +msgstr "" + diff --git a/requirements.txt b/requirements.txt index 04f448834f..f6d1c61c08 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ # The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. +Babel>=1.3 cliff>=1.7.0 # Apache-2.0 oslo.i18n>=1.0.0 # Apache-2.0 oslo.utils>=1.0.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index 3178fe4467..e97bbd2a6c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -320,3 +320,17 @@ upload-dir = doc/build/html [wheel] universal = 1 + +[extract_messages] +keywords = _ gettext ngettext l_ lazy_gettext +mapping_file = babel.cfg +output_file = python-openstackclient/locale/python-openstackclient.pot + +[update_catalog] +domain = python-openstackclient +output_dir = python-openstackclient/locale +input_file = python-openstackclient/locale/python-openstackclient.pot + +[compile_catalog] +directory = python-openstackclient/locale +domain = python-openstackclient diff --git a/test-requirements.txt b/test-requirements.txt index 3b95cf7db3..50ddc88e9c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -13,5 +13,3 @@ sphinx>=1.1.2,!=1.2.0,<1.3 testrepository>=0.0.18 testtools>=0.9.34 WebOb>=1.2.3 - -Babel>=1.3 From f0c57e17c9a4b5bbe2f072a4eacefce3bcf30d45 Mon Sep 17 00:00:00 2001 From: Nathan Kinder Date: Tue, 7 Oct 2014 16:30:56 -0700 Subject: [PATCH 0220/3494] Allow --domain to be used for identity commands without lookup Performing create, list, or set operations for users, groups, and projects with the --domain option attempts to look up the domain for name to ID conversion. In the case of an environment using Keystone domains, it is desired to allow a domain admin to perform these operations for objects in their domain without allowing them to list or show domains. The current behavior prevents the domain admin from performing these operations since they will be forbidden to perform the underlying list_domains operation. This patch makes the domain lookup error a soft failure, and falls back to using the passed in domain argument directly as a domain ID in the request that it sends to Keystone. Change-Id: I5139097f8cedc53693f6f71297518917ac72e50a Closes-Bug: #1378565 --- openstackclient/identity/v3/group.py | 18 +++++++----------- openstackclient/identity/v3/project.py | 13 +++++++------ openstackclient/identity/v3/user.py | 17 +++++++---------- 3 files changed, 21 insertions(+), 27 deletions(-) diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index fdb9501ad6..bdb4e02980 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -129,8 +129,8 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity if parsed_args.domain: - domain = utils.find_resource(identity_client.domains, - parsed_args.domain).id + domain = common.find_domain(identity_client, + parsed_args.domain).id else: domain = None group = identity_client.groups.create( @@ -174,7 +174,7 @@ def get_parser(self, prog_name): parser.add_argument( '--domain', metavar='', - help='Filter group list by ', + help='Filter group list by (name or ID)', ) parser.add_argument( '--user', @@ -194,10 +194,8 @@ def take_action(self, parsed_args): identity_client = self.app.client_manager.identity if parsed_args.domain: - domain = utils.find_resource( - identity_client.domains, - parsed_args.domain, - ).id + domain = common.find_domain(identity_client, + parsed_args.domain).id else: domain = None @@ -301,10 +299,8 @@ def take_action(self, parsed_args): if parsed_args.description: kwargs['description'] = parsed_args.description if parsed_args.domain: - domain = utils.find_resource( - identity_client.domains, parsed_args.domain).id - kwargs['domain'] = domain - + kwargs['domain'] = common.find_domain(identity_client, + parsed_args.domain).id if not len(kwargs): sys.stderr.write("Group not updated, no arguments present") return diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index ec8e5a3b1a..7b3c281cf9 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -74,7 +74,8 @@ def take_action(self, parsed_args): identity_client = self.app.client_manager.identity if parsed_args.domain: - domain = common.find_domain(identity_client, parsed_args.domain).id + domain = common.find_domain(identity_client, + parsed_args.domain).id else: domain = None @@ -141,7 +142,7 @@ def get_parser(self, prog_name): parser.add_argument( '--domain', metavar='', - help='Filter by a specific domain', + help='Filter by a specific domain (name or ID)', ) return parser @@ -154,8 +155,8 @@ def take_action(self, parsed_args): columns = ('ID', 'Name') kwargs = {} if parsed_args.domain: - domain = common.find_domain(identity_client, parsed_args.domain) - kwargs['domain'] = domain.id + kwargs['domain'] = common.find_domain(identity_client, + parsed_args.domain).id data = identity_client.projects.list(**kwargs) return (columns, (utils.get_item_properties( @@ -232,8 +233,8 @@ def take_action(self, parsed_args): if parsed_args.name: kwargs['name'] = parsed_args.name if parsed_args.domain: - domain = common.find_domain(identity_client, parsed_args.domain) - kwargs['domain'] = domain.id + kwargs['domain'] = common.find_domain(identity_client, + parsed_args.domain).id if parsed_args.description: kwargs['description'] = parsed_args.description if parsed_args.enable: diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 73bb7f1308..10921219d6 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -95,8 +95,8 @@ def take_action(self, parsed_args): project_id = None if parsed_args.domain: - domain_id = utils.find_resource( - identity_client.domains, parsed_args.domain).id + domain_id = common.find_domain(identity_client, + parsed_args.domain).id else: domain_id = None @@ -158,7 +158,7 @@ def get_parser(self, prog_name): parser.add_argument( '--domain', metavar='', - help='Filter group list by ', + help='Filter user list by (name or ID)', ) parser.add_argument( '--group', @@ -178,10 +178,8 @@ def take_action(self, parsed_args): identity_client = self.app.client_manager.identity if parsed_args.domain: - domain = utils.find_resource( - identity_client.domains, - parsed_args.domain, - ).id + domain = common.find_domain(identity_client, + parsed_args.domain).id else: domain = None @@ -311,9 +309,8 @@ def take_action(self, parsed_args): identity_client.projects, parsed_args.project).id kwargs['project'] = project_id if parsed_args.domain: - domain_id = utils.find_resource( - identity_client.domains, parsed_args.domain).id - kwargs['domain'] = domain_id + kwargs['domain'] = common.find_domain(identity_client, + parsed_args.domain).id kwargs['enabled'] = user.enabled if parsed_args.enable: kwargs['enabled'] = True From 3af547a1a6e597ea1b38fb273195ac1ef00d29dd Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 11 Oct 2014 14:25:50 -0700 Subject: [PATCH 0221/3494] Fix operation on clouds with availability-zones In a cloud with AZs, you can get multiple entries back from the service catalog - one for each AZ and then one that is AZ agnostic that's tied to the region. If the region_name is plumbed all the way through, this works as intended. Change-Id: I3b365ea306e8111fc80830672ae8080a5d1dc8e0 --- openstackclient/common/clientmanager.py | 4 ++-- openstackclient/compute/client.py | 2 +- openstackclient/network/client.py | 3 ++- openstackclient/volume/client.py | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index 0542b47362..387721a453 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -110,13 +110,13 @@ def __init__(self, auth_options, api_version=None, verify=True): return - def get_endpoint_for_service_type(self, service_type): + def get_endpoint_for_service_type(self, service_type, region_name=None): """Return the endpoint URL for the service type.""" # See if we are using password flow auth, i.e. we have a # service catalog to select endpoints from if self._service_catalog: endpoint = self._service_catalog.url_for( - service_type=service_type) + service_type=service_type, region_name=region_name) else: # Hope we were given the correct URL. endpoint = self._auth_url or self._url diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py index dc50507eb2..d473295b71 100644 --- a/openstackclient/compute/client.py +++ b/openstackclient/compute/client.py @@ -68,7 +68,7 @@ def make_client(instance): else: # password flow client.client.management_url = instance.get_endpoint_for_service_type( - API_NAME) + API_NAME, region_name=instance._region_name) client.client.service_catalog = instance._service_catalog client.client.auth_token = instance._token return client diff --git a/openstackclient/network/client.py b/openstackclient/network/client.py index d3102da1eb..870fdad1db 100644 --- a/openstackclient/network/client.py +++ b/openstackclient/network/client.py @@ -35,7 +35,8 @@ def make_client(instance): LOG.debug('Instantiating network client: %s', network_client) if not instance._url: - instance._url = instance.get_endpoint_for_service_type("network") + instance._url = instance.get_endpoint_for_service_type( + "network", region_name=instance._region_name) return network_client( username=instance._username, tenant_name=instance._project_name, diff --git a/openstackclient/volume/client.py b/openstackclient/volume/client.py index f71fbe8bcd..58cb267e53 100644 --- a/openstackclient/volume/client.py +++ b/openstackclient/volume/client.py @@ -68,7 +68,7 @@ def make_client(instance): else: # password flow client.client.management_url = instance.get_endpoint_for_service_type( - API_NAME) + API_NAME, region_name=instance._region_name) client.client.service_catalog = instance._service_catalog client.client.auth_token = instance._token From 7b046f951131a94cb3c345cd03a0bb5220720522 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 11 Oct 2014 22:37:59 +0000 Subject: [PATCH 0222/3494] Updated from global requirements Change-Id: I2ac5b9ac545c1bb6ec6279ecbe74e3301eb07a25 --- requirements.txt | 6 +++--- test-requirements.txt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 447b534a5e..8bb42eddf1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,10 +6,10 @@ oslo.i18n>=1.0.0 # Apache-2.0 oslo.utils>=1.0.0 # Apache-2.0 pbr>=0.6,!=0.7,<1.0 python-glanceclient>=0.14.0 -python-keystoneclient>=0.10.0 +python-keystoneclient>=0.11.1 python-novaclient>=2.18.0 python-cinderclient>=1.1.0 python-neutronclient>=2.3.6,<3 -requests>=1.2.1,!=2.4.0 +requests>=2.2.0,!=2.4.0 six>=1.7.0 -stevedore>=1.0.0 +stevedore>=1.0.0 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index 3b95cf7db3..dea07e8d05 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,7 +9,7 @@ fixtures>=0.3.14 mock>=1.0 oslosphinx>=2.2.0 # Apache-2.0 requests-mock>=0.4.0 # Apache-2.0 -sphinx>=1.1.2,!=1.2.0,<1.3 +sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 testrepository>=0.0.18 testtools>=0.9.34 WebOb>=1.2.3 From 1b3c7ec122504a41dc35aaf3f0693c9e20015df8 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 10 Oct 2014 14:17:11 -0400 Subject: [PATCH 0223/3494] Fix issue token for v3 Currently the code is broken as it references a part of keystoneclient that does not exist. Change-Id: I7fbc754537fbb4acffb166b5854840acfaef1fb8 Closes-Bug: #1379871 --- functional/tests/test_identity.py | 6 ++++++ openstackclient/identity/v3/token.py | 4 ++-- openstackclient/tests/identity/v3/fakes.py | 4 +++- openstackclient/tests/identity/v3/test_token.py | 3 ++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/functional/tests/test_identity.py b/functional/tests/test_identity.py index cdb0ed341b..c5779a206d 100644 --- a/functional/tests/test_identity.py +++ b/functional/tests/test_identity.py @@ -76,6 +76,7 @@ class IdentityV3Tests(test.TestCase): DOMAIN_FIELDS = ['description', 'enabled', 'id', 'name', 'links'] GROUP_FIELDS = ['description', 'domain_id', 'id', 'name', 'links'] + TOKEN_FIELDS = ['expires', 'id', 'project_id', 'user_id'] def _create_dummy_group(self): name = uuid.uuid4().hex @@ -139,3 +140,8 @@ def test_domain_show(self): raw_output = self.openstack('domain show ' + name) items = self.parse_show(raw_output) self.assert_show_fields(items, self.DOMAIN_FIELDS) + + def test_token_issue(self): + raw_output = self.openstack('token issue') + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.TOKEN_FIELDS) diff --git a/openstackclient/identity/v3/token.py b/openstackclient/identity/v3/token.py index 52ed439f15..aca5c66993 100644 --- a/openstackclient/identity/v3/token.py +++ b/openstackclient/identity/v3/token.py @@ -159,9 +159,9 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) - identity_client = self.app.client_manager.identity + session = self.app.client_manager.identity.session - token = identity_client.service_catalog.get_token() + token = session.auth.auth_ref.service_catalog.get_token() if 'tenant_id' in token: token['project_id'] = token.pop('tenant_id') return zip(*sorted(six.iteritems(token))) diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index b0df16f043..1ca1c55d5d 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -292,7 +292,9 @@ def __init__(self, **kwargs): self.roles.resource_class = fakes.FakeResource(None, {}) self.services = mock.Mock() self.services.resource_class = fakes.FakeResource(None, {}) - self.service_catalog = mock.Mock() + self.session = mock.Mock() + self.session.auth.auth_ref.service_catalog.resource_class = \ + fakes.FakeResource(None, {}) self.users = mock.Mock() self.users.resource_class = fakes.FakeResource(None, {}) self.role_assignments = mock.Mock() diff --git a/openstackclient/tests/identity/v3/test_token.py b/openstackclient/tests/identity/v3/test_token.py index 8888b93186..dbe855555c 100644 --- a/openstackclient/tests/identity/v3/test_token.py +++ b/openstackclient/tests/identity/v3/test_token.py @@ -23,7 +23,8 @@ def setUp(self): super(TestToken, self).setUp() # Get a shortcut to the Service Catalog Mock - self.sc_mock = self.app.client_manager.identity.service_catalog + session = self.app.client_manager.identity.session + self.sc_mock = session.auth.auth_ref.service_catalog self.sc_mock.reset_mock() From a8d4b0eebb563c8aad33300938b1b32347eb0050 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 3 Oct 2014 00:07:45 -0400 Subject: [PATCH 0224/3494] Remove 'links' section from several v3 Identity objects The links field in the returned objects from the v3 Identity API aren't really useful, so let's remove them. Managed to remove most of them from the core API. I'll likely remove the extension/contribution (oauth/federation) related ones in another patch. Also in this patch the code for setting services and projects was changed. Though not incorrect, it was not needed to copy the entire returned object, we should just need to pass in the fields we want to update. Change-Id: I164ca9ad8b28fa10b291e9115ef40753e387c547 --- openstackclient/identity/v3/credential.py | 2 ++ openstackclient/identity/v3/domain.py | 2 ++ openstackclient/identity/v3/endpoint.py | 2 ++ openstackclient/identity/v3/group.py | 6 +++--- openstackclient/identity/v3/policy.py | 2 ++ openstackclient/identity/v3/project.py | 15 ++++----------- openstackclient/identity/v3/role.py | 2 ++ openstackclient/identity/v3/service.py | 6 +++--- openstackclient/identity/v3/user.py | 6 +++--- openstackclient/tests/identity/v3/fakes.py | 9 +++++++++ .../tests/identity/v3/test_project.py | 16 ---------------- .../tests/identity/v3/test_service.py | 8 -------- 12 files changed, 32 insertions(+), 44 deletions(-) diff --git a/openstackclient/identity/v3/credential.py b/openstackclient/identity/v3/credential.py index f1e17b8502..4d6889548b 100644 --- a/openstackclient/identity/v3/credential.py +++ b/openstackclient/identity/v3/credential.py @@ -73,6 +73,7 @@ def take_action(self, parsed_args): blob=parsed_args.data, project=project) + credential._info.pop('links') return zip(*sorted(six.iteritems(credential._info))) @@ -193,4 +194,5 @@ def take_action(self, parsed_args): credential = utils.find_resource(identity_client.credentials, parsed_args.credential) + credential._info.pop('links') return zip(*sorted(six.iteritems(credential._info))) diff --git a/openstackclient/identity/v3/domain.py b/openstackclient/identity/v3/domain.py index 49397afc46..d14da48682 100644 --- a/openstackclient/identity/v3/domain.py +++ b/openstackclient/identity/v3/domain.py @@ -66,6 +66,7 @@ def take_action(self, parsed_args): enabled=parsed_args.enabled, ) + domain._info.pop('links') return zip(*sorted(six.iteritems(domain._info))) @@ -187,4 +188,5 @@ def take_action(self, parsed_args): domain = utils.find_resource(identity_client.domains, parsed_args.domain) + domain._info.pop('links') return zip(*sorted(six.iteritems(domain._info))) diff --git a/openstackclient/identity/v3/endpoint.py b/openstackclient/identity/v3/endpoint.py index 39798b2dd4..0c077c5a34 100644 --- a/openstackclient/identity/v3/endpoint.py +++ b/openstackclient/identity/v3/endpoint.py @@ -81,6 +81,7 @@ def take_action(self, parsed_args): ) info = {} + endpoint._info.pop('links') info.update(endpoint._info) info['service_name'] = service.name info['service_type'] = service.type @@ -258,6 +259,7 @@ def take_action(self, parsed_args): service = common.find_service(identity_client, endpoint.service_id) info = {} + endpoint._info.pop('links') info.update(endpoint._info) info['service_name'] = service.name info['service_type'] = service.type diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index bdb4e02980..d2ffca2733 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -138,9 +138,8 @@ def take_action(self, parsed_args): domain=domain, description=parsed_args.description) - info = {} - info.update(group._info) - return zip(*sorted(six.iteritems(info))) + group._info.pop('links') + return zip(*sorted(six.iteritems(group._info))) class DeleteGroup(command.Command): @@ -340,4 +339,5 @@ def take_action(self, parsed_args): group = utils.find_resource(identity_client.groups, parsed_args.group) + group._info.pop('links') return zip(*sorted(six.iteritems(group._info))) diff --git a/openstackclient/identity/v3/policy.py b/openstackclient/identity/v3/policy.py index 87f3cbe974..802880bfc2 100644 --- a/openstackclient/identity/v3/policy.py +++ b/openstackclient/identity/v3/policy.py @@ -55,6 +55,7 @@ def take_action(self, parsed_args): blob=blob, type=parsed_args.type ) + policy._info.pop('links') return zip(*sorted(six.iteritems(policy._info))) @@ -173,4 +174,5 @@ def take_action(self, parsed_args): policy = utils.find_resource(identity_client.policies, parsed_args.policy) + policy._info.pop('links') return zip(*sorted(six.iteritems(policy._info))) diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 7b3c281cf9..1cdeb15067 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -94,9 +94,8 @@ def take_action(self, parsed_args): **kwargs ) - info = {} - info.update(project._info) - return zip(*sorted(six.iteritems(info))) + project._info.pop('links') + return zip(*sorted(six.iteritems(project._info))) class DeleteProject(command.Command): @@ -229,7 +228,7 @@ def take_action(self, parsed_args): parsed_args.project, ) - kwargs = project._info + kwargs = {} if parsed_args.name: kwargs['name'] = parsed_args.name if parsed_args.domain: @@ -243,13 +242,6 @@ def take_action(self, parsed_args): kwargs['enabled'] = False if parsed_args.property: kwargs.update(parsed_args.property) - if 'id' in kwargs: - del kwargs['id'] - if 'domain_id' in kwargs: - # Hack around broken Identity API arg names - kwargs.update( - {'domain': kwargs.pop('domain_id')} - ) identity_client.projects.update(project.id, **kwargs) return @@ -287,4 +279,5 @@ def take_action(self, parsed_args): project = utils.find_resource(identity_client.projects, parsed_args.project) + project._info.pop('links') return zip(*sorted(six.iteritems(project._info))) diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index d8de7c2291..a3c24b7af6 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -157,6 +157,7 @@ def take_action(self, parsed_args): role = identity_client.roles.create(name=parsed_args.name) + role._info.pop('links') return zip(*sorted(six.iteritems(role._info))) @@ -472,4 +473,5 @@ def take_action(self, parsed_args): parsed_args.role, ) + role._info.pop('links') return zip(*sorted(six.iteritems(role._info))) diff --git a/openstackclient/identity/v3/service.py b/openstackclient/identity/v3/service.py index 88301edc05..4f62226968 100644 --- a/openstackclient/identity/v3/service.py +++ b/openstackclient/identity/v3/service.py @@ -70,6 +70,7 @@ def take_action(self, parsed_args): enabled=enabled, ) + service._info.pop('links') return zip(*sorted(six.iteritems(service._info))) @@ -161,7 +162,7 @@ def take_action(self, parsed_args): service = common.find_service(identity_client, parsed_args.service) - kwargs = service._info + kwargs = {} if parsed_args.type: kwargs['type'] = parsed_args.type if parsed_args.name: @@ -170,8 +171,6 @@ def take_action(self, parsed_args): kwargs['enabled'] = True if parsed_args.disable: kwargs['enabled'] = False - if 'id' in kwargs: - del kwargs['id'] identity_client.services.update( service.id, @@ -200,4 +199,5 @@ def take_action(self, parsed_args): service = common.find_service(identity_client, parsed_args.service) + service._info.pop('links') return zip(*sorted(six.iteritems(service._info))) diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 10921219d6..9a3e00b83a 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -116,9 +116,8 @@ def take_action(self, parsed_args): enabled=enabled ) - info = {} - info.update(user._info) - return zip(*sorted(six.iteritems(info))) + user._info.pop('links') + return zip(*sorted(six.iteritems(user._info))) class DeleteUser(command.Command): @@ -382,4 +381,5 @@ def take_action(self, parsed_args): user = utils.find_resource(identity_client.users, parsed_args.user) + user._info.pop('links') return zip(*sorted(six.iteritems(user._info))) diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index b0df16f043..8e8c8767ca 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -18,6 +18,7 @@ from openstackclient.tests import fakes from openstackclient.tests import utils +base_url = 'http://identity:5000/v3/' domain_id = 'd1' domain_name = 'oftheking' @@ -28,6 +29,7 @@ 'name': domain_name, 'description': domain_description, 'enabled': True, + 'links': base_url + 'domains/' + domain_id, } group_id = 'gr-010' @@ -36,6 +38,7 @@ GROUP = { 'id': group_id, 'name': group_name, + 'links': base_url + 'groups/' + group_id, } mapping_id = 'test_mapping' @@ -107,6 +110,7 @@ 'description': project_description, 'enabled': True, 'domain_id': domain_id, + 'links': base_url + 'projects/' + project_id, } PROJECT_2 = { @@ -115,6 +119,7 @@ 'description': project_description + 'plus four more', 'enabled': True, 'domain_id': domain_id, + 'links': base_url + 'projects/' + project_id, } role_id = 'r1' @@ -123,6 +128,7 @@ ROLE = { 'id': role_id, 'name': role_name, + 'links': base_url + 'roles/' + role_id, } service_id = 's-123' @@ -134,6 +140,7 @@ 'name': service_name, 'type': service_type, 'enabled': True, + 'links': base_url + 'services/' + service_id, } endpoint_id = 'e-123' @@ -148,6 +155,7 @@ 'interface': endpoint_interface, 'service_id': service_id, 'enabled': True, + 'links': base_url + 'endpoints/' + endpoint_id, } user_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' @@ -162,6 +170,7 @@ 'email': user_email, 'enabled': True, 'domain_id': domain_id, + 'links': base_url + 'users/' + user_id, } token_expires = '2014-01-01T00:00:00Z' diff --git a/openstackclient/tests/identity/v3/test_project.py b/openstackclient/tests/identity/v3/test_project.py index 2e7bc54b5c..1060a27795 100644 --- a/openstackclient/tests/identity/v3/test_project.py +++ b/openstackclient/tests/identity/v3/test_project.py @@ -533,9 +533,6 @@ def test_project_set_name(self): # Set expected values kwargs = { - 'description': identity_fakes.project_description, - 'domain': identity_fakes.domain_id, - 'enabled': True, 'name': 'qwerty', } # ProjectManager.update(project, name=, domain=, description=, @@ -564,9 +561,6 @@ def test_project_set_description(self): # Set expected values kwargs = { 'description': 'new desc', - 'domain': identity_fakes.domain_id, - 'enabled': True, - 'name': identity_fakes.project_name, } self.projects_mock.update.assert_called_with( identity_fakes.project_id, @@ -590,10 +584,7 @@ def test_project_set_enable(self): # Set expected values kwargs = { - 'description': identity_fakes.project_description, - 'domain': identity_fakes.domain_id, 'enabled': True, - 'name': identity_fakes.project_name, } self.projects_mock.update.assert_called_with( identity_fakes.project_id, @@ -617,10 +608,7 @@ def test_project_set_disable(self): # Set expected values kwargs = { - 'description': identity_fakes.project_description, - 'domain': identity_fakes.domain_id, 'enabled': False, - 'name': identity_fakes.project_name, } self.projects_mock.update.assert_called_with( identity_fakes.project_id, @@ -644,10 +632,6 @@ def test_project_set_property(self): # Set expected values kwargs = { - 'description': identity_fakes.project_description, - 'domain': identity_fakes.domain_id, - 'enabled': True, - 'name': identity_fakes.project_name, 'fee': 'fi', 'fo': 'fum', } diff --git a/openstackclient/tests/identity/v3/test_service.py b/openstackclient/tests/identity/v3/test_service.py index 6733f7faef..57db77b12a 100644 --- a/openstackclient/tests/identity/v3/test_service.py +++ b/openstackclient/tests/identity/v3/test_service.py @@ -267,9 +267,7 @@ def test_service_set_type(self): # Set expected values kwargs = { - 'name': identity_fakes.service_name, 'type': identity_fakes.service_type, - 'enabled': True, } # ServiceManager.update(service, name=, type=, enabled=, **kwargs) self.services_mock.update.assert_called_with( @@ -297,8 +295,6 @@ def test_service_set_name(self): # Set expected values kwargs = { 'name': identity_fakes.service_name, - 'type': identity_fakes.service_type, - 'enabled': True, } # ServiceManager.update(service, name=, type=, enabled=, **kwargs) self.services_mock.update.assert_called_with( @@ -325,8 +321,6 @@ def test_service_set_enable(self): # Set expected values kwargs = { - 'name': identity_fakes.service_name, - 'type': identity_fakes.service_type, 'enabled': True, } # ServiceManager.update(service, name=, type=, enabled=, **kwargs) @@ -354,8 +348,6 @@ def test_service_set_disable(self): # Set expected values kwargs = { - 'name': identity_fakes.service_name, - 'type': identity_fakes.service_type, 'enabled': False, } # ServiceManager.update(service, name=, type=, enabled=, **kwargs) From c3c6edbe8a083aef0fb6aea3cb461ff8e715fc59 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 9 Oct 2014 15:16:07 -0500 Subject: [PATCH 0225/3494] Add plugin to support token-endpoint auth The ksc auth plugins do not have support for the original token-endpoint (aka token flow) auth where the user supplies a token (possibly the Keystone admin_token) and an API endpoint. This is used for bootstrapping Keystone but also has other uses when a scoped user token is provided. The api.auth:TokenEndpoint class is required to provide the same interface methods so all of the special-case code branches to support token-endpoint can be removed. Some additional cleanups related to ClientManager and creating the Compute client also were done to streamline using sessions. Change-Id: I1a6059afa845a591eff92567ca346c09010a93af --- openstackclient/api/auth.py | 69 ++++++++++++++++--- openstackclient/common/clientmanager.py | 41 ++++++----- openstackclient/compute/client.py | 21 ++---- .../tests/common/test_clientmanager.py | 48 +++++++++---- setup.cfg | 3 + 5 files changed, 124 insertions(+), 58 deletions(-) diff --git a/openstackclient/api/auth.py b/openstackclient/api/auth.py index 2bd5271f7d..e33b72d575 100644 --- a/openstackclient/api/auth.py +++ b/openstackclient/api/auth.py @@ -18,6 +18,8 @@ import stevedore +from oslo.config import cfg + from keystoneclient.auth import base from openstackclient.common import exceptions as exc @@ -53,14 +55,14 @@ ) -def _guess_authentication_method(options): +def select_auth_plugin(options): """If no auth plugin was specified, pick one based on other options""" - if options.os_url: - # service token authentication, do nothing - return auth_plugin = None - if options.os_password: + if options.os_url and options.os_token: + # service token authentication + auth_plugin = 'token_endpoint' + elif options.os_password: if options.os_identity_api_version == '3': auth_plugin = 'v3password' elif options.os_identity_api_version == '2.0': @@ -83,14 +85,13 @@ def _guess_authentication_method(options): ) LOG.debug("No auth plugin selected, picking %s from other " "options" % auth_plugin) - options.os_auth_plugin = auth_plugin + return auth_plugin def build_auth_params(cmd_options): auth_params = {} - if cmd_options.os_url: - return {'token': cmd_options.os_token} if cmd_options.os_auth_plugin: + LOG.debug('auth_plugin: %s', cmd_options.os_auth_plugin) auth_plugin = base.get_plugin_class(cmd_options.os_auth_plugin) plugin_options = auth_plugin.get_options() for option in plugin_options: @@ -110,6 +111,7 @@ def build_auth_params(cmd_options): None, ) else: + LOG.debug('no auth_plugin') # delay the plugin choice, grab every option plugin_options = set([o.replace('-', '_') for o in OPTIONS_LIST]) for option in plugin_options: @@ -178,3 +180,54 @@ def build_auth_plugins_option_parser(parser): help=argparse.SUPPRESS, ) return parser + + +class TokenEndpoint(base.BaseAuthPlugin): + """Auth plugin to handle traditional token/endpoint usage + + Implements the methods required to handle token authentication + with a user-specified token and service endpoint; no Identity calls + are made for re-scoping, service catalog lookups or the like. + + The purpose of this plugin is to get rid of the special-case paths + in the code to handle this authentication format. Its primary use + is for bootstrapping the Keystone database. + """ + + def __init__(self, url, token, **kwargs): + """A plugin for static authentication with an existing token + + :param string url: Service endpoint + :param string token: Existing token + """ + super(TokenEndpoint, self).__init__() + self.endpoint = url + self.token = token + + def get_endpoint(self, session, **kwargs): + """Return the supplied endpoint""" + return self.endpoint + + def get_token(self, session): + """Return the supplied token""" + return self.token + + def get_auth_ref(self, session, **kwargs): + """Stub this method for compatibility""" + return None + + # Override this because it needs to be a class method... + @classmethod + def get_options(self): + options = super(TokenEndpoint, self).get_options() + + options.extend([ + # Maintain name 'url' for compatibility + cfg.StrOpt('url', + help='Specific service endpoint to use'), + cfg.StrOpt('token', + secret=True, + help='Authentication token to use'), + ]) + + return options diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index 0542b47362..bcb81990ad 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -54,9 +54,10 @@ def __getattr__(self, name): return self._auth_params[name[1:]] def __init__(self, auth_options, api_version=None, verify=True): - + # If no plugin is named by the user, select one based on + # the supplied options if not auth_options.os_auth_plugin: - auth._guess_authentication_method(auth_options) + auth_options.os_auth_plugin = auth.select_auth_plugin(auth_options) self._auth_plugin = auth_options.os_auth_plugin self._url = auth_options.os_url @@ -66,7 +67,7 @@ def __init__(self, auth_options, api_version=None, verify=True): self._service_catalog = None self.timing = auth_options.timing - # For compatability until all clients can be updated + # For compatibility until all clients can be updated if 'project_name' in self._auth_params: self._project_name = self._auth_params['project_name'] elif 'tenant_name' in self._auth_params: @@ -86,27 +87,25 @@ def __init__(self, auth_options, api_version=None, verify=True): root_logger = logging.getLogger('') LOG.setLevel(root_logger.getEffectiveLevel()) - self.session = None - if not self._url: - LOG.debug('Using auth plugin: %s' % self._auth_plugin) - auth_plugin = base.get_plugin_class(self._auth_plugin) - self.auth = auth_plugin.load_from_options(**self._auth_params) - # needed by SAML authentication - request_session = requests.session() - self.session = session.Session( - auth=self.auth, - session=request_session, - verify=verify, - ) + LOG.debug('Using auth plugin: %s' % self._auth_plugin) + auth_plugin = base.get_plugin_class(self._auth_plugin) + self.auth = auth_plugin.load_from_options(**self._auth_params) + # needed by SAML authentication + request_session = requests.session() + self.session = session.Session( + auth=self.auth, + session=request_session, + verify=verify, + ) self.auth_ref = None - if not self._auth_plugin.endswith("token") and not self._url: - LOG.debug("Populate other password flow attributes") - self.auth_ref = self.session.auth.get_auth_ref(self.session) - self._token = self.session.auth.get_token(self.session) + if 'token' not in self._auth_params: + LOG.debug("Get service catalog") + self.auth_ref = self.auth.get_auth_ref(self.session) self._service_catalog = self.auth_ref.service_catalog - else: - self._token = self._auth_params.get('token') + + # This begone when clients no longer need it... + self._token = self.auth.get_token(self.session) return diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py index dc50507eb2..6c03d24e3f 100644 --- a/openstackclient/compute/client.py +++ b/openstackclient/compute/client.py @@ -44,33 +44,20 @@ def make_client(instance): extensions = [extension.Extension('list_extensions', list_extensions)] client = compute_client( - username=instance._username, - api_key=instance._password, - project_id=instance._project_name, - auth_url=instance._auth_url, - cacert=instance._cacert, - insecure=instance._insecure, - region_name=instance._region_name, - # FIXME(dhellmann): get endpoint_type from option? - endpoint_type='publicURL', + session=instance.session, extensions=extensions, - service_type=API_NAME, - # FIXME(dhellmann): what is service_name? - service_name='', http_log_debug=http_log_debug, timings=instance.timing, ) # Populate the Nova client to skip another auth query to Identity - if instance._url: - # token flow - client.client.management_url = instance._url - else: + if 'token' not in instance._auth_params: # password flow client.client.management_url = instance.get_endpoint_for_service_type( API_NAME) client.client.service_catalog = instance._service_catalog - client.client.auth_token = instance._token + client.client.auth_token = instance.auth.get_token(instance.session) + return client diff --git a/openstackclient/tests/common/test_clientmanager.py b/openstackclient/tests/common/test_clientmanager.py index 18461fb7e8..5ec86d595d 100644 --- a/openstackclient/tests/common/test_clientmanager.py +++ b/openstackclient/tests/common/test_clientmanager.py @@ -76,6 +76,31 @@ def setUp(self): url=fakes.AUTH_URL, verb='GET') + def test_client_manager_token_endpoint(self): + + client_manager = clientmanager.ClientManager( + auth_options=FakeOptions(os_token=fakes.AUTH_TOKEN, + os_url=fakes.AUTH_URL, + os_auth_plugin='token_endpoint'), + api_version=API_VERSION, + verify=True + ) + self.assertEqual( + fakes.AUTH_URL, + client_manager._url, + ) + + self.assertEqual( + fakes.AUTH_TOKEN, + client_manager._token, + ) + self.assertIsInstance( + client_manager.auth, + auth.TokenEndpoint, + ) + self.assertFalse(client_manager._insecure) + self.assertTrue(client_manager._verify) + def test_client_manager_token(self): client_manager = clientmanager.ClientManager( @@ -176,8 +201,7 @@ def test_client_manager_password_verify_ca(self): self.assertTrue(client_manager._verify) self.assertEqual('cafile', client_manager._cacert) - def _client_manager_guess_auth_plugin(self, auth_params, - api_version, auth_plugin): + def _select_auth_plugin(self, auth_params, api_version, auth_plugin): auth_params['os_auth_plugin'] = auth_plugin auth_params['os_identity_api_version'] = api_version client_manager = clientmanager.ClientManager( @@ -190,25 +214,25 @@ def _client_manager_guess_auth_plugin(self, auth_params, client_manager._auth_plugin, ) - def test_client_manager_guess_auth_plugin(self): + def test_client_manager_select_auth_plugin(self): # test token auth params = dict(os_token=fakes.AUTH_TOKEN, os_auth_url=fakes.AUTH_URL) - self._client_manager_guess_auth_plugin(params, '2.0', 'v2token') - self._client_manager_guess_auth_plugin(params, '3', 'v3token') - self._client_manager_guess_auth_plugin(params, 'XXX', 'token') - # test service auth + self._select_auth_plugin(params, '2.0', 'v2token') + self._select_auth_plugin(params, '3', 'v3token') + self._select_auth_plugin(params, 'XXX', 'token') + # test token/endpoint auth params = dict(os_token=fakes.AUTH_TOKEN, os_url='test') - self._client_manager_guess_auth_plugin(params, 'XXX', '') + self._select_auth_plugin(params, 'XXX', 'token_endpoint') # test password auth params = dict(os_auth_url=fakes.AUTH_URL, os_username=fakes.USERNAME, os_password=fakes.PASSWORD) - self._client_manager_guess_auth_plugin(params, '2.0', 'v2password') - self._client_manager_guess_auth_plugin(params, '3', 'v3password') - self._client_manager_guess_auth_plugin(params, 'XXX', 'password') + self._select_auth_plugin(params, '2.0', 'v2password') + self._select_auth_plugin(params, '3', 'v3password') + self._select_auth_plugin(params, 'XXX', 'password') - def test_client_manager_guess_auth_plugin_failure(self): + def test_client_manager_select_auth_plugin_failure(self): self.assertRaises(exc.CommandError, clientmanager.ClientManager, auth_options=FakeOptions(os_auth_plugin=''), diff --git a/setup.cfg b/setup.cfg index dc85967f42..d9cced1523 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,6 +27,9 @@ packages = console_scripts = openstack = openstackclient.shell:main +keystoneclient.auth.plugin = + token_endpoint = openstackclient.api.auth:TokenEndpoint + openstack.cli = module_list = openstackclient.common.module:ListModule From a388ce40f444330c0f7d5665a74761a9f58baa44 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sun, 12 Oct 2014 09:16:38 -0700 Subject: [PATCH 0226/3494] Put pbr and six first in requirements list Each of them have scenarios where it's important that they install first. Change-Id: Ia9fff6e94651693f9fa74ca676771b19144bcd8b --- requirements.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2bbe09d8b1..3fcde32154 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,17 @@ # The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. +pbr>=0.6,!=0.7,<1.0 +six>=1.7.0 + Babel>=1.3 cliff>=1.7.0 # Apache-2.0 oslo.i18n>=1.0.0 # Apache-2.0 oslo.utils>=1.0.0 # Apache-2.0 -pbr>=0.6,!=0.7,<1.0 python-glanceclient>=0.14.0 python-keystoneclient>=0.11.1 python-novaclient>=2.18.0 python-cinderclient>=1.1.0 python-neutronclient>=2.3.6,<3 requests>=2.2.0,!=2.4.0 -six>=1.7.0 stevedore>=1.0.0 # Apache-2.0 From bcf4b3caece9edc6a04be3ce59697301e48ad34a Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 8 Oct 2014 23:22:24 -0500 Subject: [PATCH 0227/3494] Update use of open() in object API * Switch to use io.open() for py3 compatibility and simpler testing. * Open files in 'rb' mode to avoid translation on Windows Previously tests simply relied on files that were present in the repository to run tests using open(). Change the filenames to ensure that no longer happens. requests_mock doesn't have a way to match against the request body for PUT/POST; an attempt to add a new Matcher to do that worked but it needs to subclass the currently private adapter._Matcher class or duplicate most of its functionality. Change-Id: I8c30b41db20af8ecafe67e760e872fc08adec905 --- openstackclient/api/object_store_v1.py | 8 ++++++- .../tests/api/test_object_store_v1.py | 21 +++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/openstackclient/api/object_store_v1.py b/openstackclient/api/object_store_v1.py index 57db906398..c52eeb3ac9 100644 --- a/openstackclient/api/object_store_v1.py +++ b/openstackclient/api/object_store_v1.py @@ -13,6 +13,7 @@ """Object Store v1 API Library""" +import io import os import six @@ -187,7 +188,12 @@ def object_create( return {} full_url = "%s/%s" % (container, object) - response = self.create(full_url, method='PUT', data=open(object)) + with io.open(object, 'rb') as f: + response = self.create( + full_url, + method='PUT', + data=f, + ) url_parts = urlparse(self.endpoint) data = { 'account': url_parts.path.split('/')[-1], diff --git a/openstackclient/tests/api/test_object_store_v1.py b/openstackclient/tests/api/test_object_store_v1.py index 5a376a454b..b18a003db5 100644 --- a/openstackclient/tests/api/test_object_store_v1.py +++ b/openstackclient/tests/api/test_object_store_v1.py @@ -13,6 +13,8 @@ """Object Store v1 API Library Tests""" +import mock + from requests_mock.contrib import fixture from keystoneclient import session @@ -175,30 +177,41 @@ class TestObject(TestObjectAPIv1): def setUp(self): super(TestObject, self).setUp() - def test_object_create(self): + @mock.patch('openstackclient.api.object_store_v1.io.open') + def base_object_create(self, file_contents, mock_open): + mock_open.read.return_value = file_contents + headers = { 'etag': 'youreit', 'x-trans-id': '1qaz2wsx', } + # TODO(dtroyer): When requests_mock gains the ability to + # match against request.body add this check + # https://review.openstack.org/127316 self.requests_mock.register_uri( 'PUT', - FAKE_URL + '/qaz/requirements.txt', + FAKE_URL + '/qaz/counter.txt', headers=headers, + # body=file_contents, status_code=201, ) ret = self.api.object_create( container='qaz', - object='requirements.txt', + object='counter.txt', ) data = { 'account': FAKE_ACCOUNT, 'container': 'qaz', - 'object': 'requirements.txt', + 'object': 'counter.txt', 'etag': 'youreit', 'x-trans-id': '1qaz2wsx', } self.assertEqual(data, ret) + def test_object_create(self): + self.base_object_create('111\n222\n333\n') + self.base_object_create(bytes([0x31, 0x00, 0x0d, 0x0a, 0x7f, 0xff])) + def test_object_delete(self): self.requests_mock.register_uri( 'DELETE', From 897418edca52d9856ef7381a5822fce3bcf8a804 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 13 Oct 2014 11:13:48 -0500 Subject: [PATCH 0228/3494] Move plugin stuff to clientmanager The OSC plugins work by adding an object as an attribute to a ClientManager instance. The initialization and management of thos plugins belongs in clientmanager.py. At this point the only part not moved is the API version dict initialization bcause the timing and connection to the CommandManager initialization. It gets refactored anyway when API discovery becomes operational. Change-Id: If9cb9a0c45a3a577082a5cdbb793769211f20ebb --- openstackclient/common/clientmanager.py | 30 ++++++++++++++++++++++--- openstackclient/shell.py | 18 +++------------ 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index 0542b47362..09c5c25c7a 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -29,6 +29,8 @@ LOG = logging.getLogger(__name__) +PLUGIN_MODULES = [] + class ClientCache(object): """Descriptor class for caching created client handles.""" @@ -123,11 +125,13 @@ def get_endpoint_for_service_type(self, service_type): return endpoint -def get_extension_modules(group): - """Add extension clients""" +# Plugin Support + +def get_plugin_modules(group): + """Find plugin entry points""" mod_list = [] for ep in pkg_resources.iter_entry_points(group): - LOG.debug('found extension %r', ep.name) + LOG.debug('Found plugin %r', ep.name) __import__(ep.module_name) module = sys.modules[ep.module_name] @@ -136,6 +140,7 @@ def get_extension_modules(group): if init_func: init_func('x') + # Add the plugin to the ClientManager setattr( ClientManager, module.API_NAME, @@ -144,3 +149,22 @@ def get_extension_modules(group): ), ) return mod_list + + +def build_plugin_option_parser(parser): + """Add plugin options to the parser""" + + # Loop through extensions to get parser additions + for mod in PLUGIN_MODULES: + parser = mod.build_option_parser(parser) + return parser + + +# Get list of base plugin modules +PLUGIN_MODULES = get_plugin_modules( + 'openstack.cli.base', +) +# Append list of external plugin modules +PLUGIN_MODULES.extend(get_plugin_modules( + 'openstack.cli.extension', +)) diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 626e3f7da5..1f9eb32b53 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -68,19 +68,6 @@ def __init__(self): # Assume TLS host certificate verification is enabled self.verify = True - # Get list of base modules - self.ext_modules = clientmanager.get_extension_modules( - 'openstack.cli.base', - ) - # Append list of extension modules - self.ext_modules.extend(clientmanager.get_extension_modules( - 'openstack.cli.extension', - )) - - # Loop through extensions to get parser additions - for mod in self.ext_modules: - self.parser = mod.build_option_parser(self.parser) - # NOTE(dtroyer): This hack changes the help action that Cliff # automatically adds to the parser so we can defer # its execution until after the api-versioned commands @@ -170,6 +157,7 @@ def build_option_parser(self, description, version): parser = super(OpenStackShell, self).build_option_parser( description, version) + # service token auth argument parser.add_argument( '--os-url', @@ -214,7 +202,7 @@ def build_option_parser(self, description, version): help="Print API call timing info", ) - return parser + return clientmanager.build_plugin_option_parser(parser) def authenticate_user(self): """Verify the required authentication credentials are present""" @@ -332,7 +320,7 @@ def initialize_app(self, argv): self.default_domain = self.options.os_default_domain # Loop through extensions to get API versions - for mod in self.ext_modules: + for mod in clientmanager.PLUGIN_MODULES: version_opt = getattr(self.options, mod.API_VERSION_OPTION, None) if version_opt: api = mod.API_NAME From ca783f46595a7d90cea0ad8491b65aa5f9370a04 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 13 Oct 2014 22:40:11 -0500 Subject: [PATCH 0229/3494] Close files on image create The file opened for --file was never closed. Close it if it is a file object. Change-Id: I7bd120a2413de42339771d01e8fd1894d38c3011 --- openstackclient/image/v1/image.py | 53 +++++++++++--------- openstackclient/tests/image/v1/test_image.py | 17 +++++-- 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index 465e9d7b15..32dd388c0f 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -15,6 +15,7 @@ """Image V1 Action Implementations""" +import io import logging import os import six @@ -214,10 +215,9 @@ def take_action(self, parsed_args): elif parsed_args.file: # Send an open file handle to glanceclient so it will # do a chunked transfer - kwargs["data"] = open(parsed_args.file, "rb") + kwargs["data"] = io.open(parsed_args.file, "rb") else: # Read file from stdin - kwargs["data"] = None if sys.stdin.isatty() is not True: if msvcrt: msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) @@ -225,29 +225,36 @@ def take_action(self, parsed_args): # do a chunked transfer kwargs["data"] = sys.stdin + # Wrap the call to catch exceptions in order to close files try: - image = utils.find_resource( - image_client.images, - parsed_args.name, - ) - - # Preserve previous properties if any are being set now - if image.properties: - if parsed_args.properties: - image.properties.update(kwargs['properties']) - kwargs['properties'] = image.properties - - except exceptions.CommandError: - if not parsed_args.volume: - # This is normal for a create or reserve (create w/o an image) - # But skip for create from volume - image = image_client.images.create(**kwargs) - else: - # Update an existing reservation + try: + image = utils.find_resource( + image_client.images, + parsed_args.name, + ) - # If an image is specified via --file, --location or - # --copy-from let the API handle it - image = image_client.images.update(image.id, **kwargs) + # Preserve previous properties if any are being set now + if image.properties: + if parsed_args.properties: + image.properties.update(kwargs['properties']) + kwargs['properties'] = image.properties + + except exceptions.CommandError: + if not parsed_args.volume: + # This is normal for a create or reserve (create w/o + # an image), but skip for create from volume + image = image_client.images.create(**kwargs) + else: + # Update an existing reservation + + # If an image is specified via --file, --location or + # --copy-from let the API handle it + image = image_client.images.update(image.id, **kwargs) + finally: + # Clean up open files - make sure data isn't a string + if ('data' in kwargs and hasattr(kwargs['data'], 'close') and + kwargs['data'] != sys.stdin): + kwargs['data'].close() info = {} info.update(image._info) diff --git a/openstackclient/tests/image/v1/test_image.py b/openstackclient/tests/image/v1/test_image.py index 3f97b151c2..a05669300e 100644 --- a/openstackclient/tests/image/v1/test_image.py +++ b/openstackclient/tests/image/v1/test_image.py @@ -139,14 +139,17 @@ def test_image_reserve_options(self): self.assertEqual(image_fakes.IMAGE_columns, columns) self.assertEqual(image_fakes.IMAGE_data, data) - @mock.patch('six.moves.builtins.open') - def test_image_create_file(self, open_mock): + @mock.patch('openstackclient.image.v1.image.io.open', name='Open') + def test_image_create_file(self, mock_open): + mock_file = mock.MagicMock(name='File') + mock_open.return_value = mock_file + mock_open.read.return_value = image_fakes.image_data mock_exception = { 'find.side_effect': exceptions.CommandError('x'), 'get.side_effect': exceptions.CommandError('x'), } self.images_mock.configure_mock(**mock_exception) - open_mock.return_value = image_fakes.image_data + arglist = [ '--file', 'filer', '--unprotected', @@ -169,7 +172,11 @@ def test_image_create_file(self, open_mock): # DisplayCommandBase.take_action() returns two tuples columns, data = self.cmd.take_action(parsed_args) - open_mock.assert_called_with('filer', 'rb') + # Ensure input file is opened + mock_open.assert_called_with('filer', 'rb') + + # Ensure the input file is closed + mock_file.close.assert_called_with() # ImageManager.get(name) self.images_mock.get.assert_called_with(image_fakes.image_name) @@ -185,7 +192,7 @@ def test_image_create_file(self, open_mock): 'Alpha': '1', 'Beta': '2', }, - data=image_fakes.image_data, + data=mock_file, ) # Verify update() was not called, if it was show the args From 89217a6557e16872ac1af0e305ac09886a9e1255 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 13 Oct 2014 16:30:38 -0500 Subject: [PATCH 0230/3494] Close files on server create, add tests The files opened for the --files and --user-data options were never closed, potentially leaking memory in a long-running client. Close them if they are file objects. Add a couple of basic tests for server create. Change-Id: I1658b0caa2d6af17308149cb52196ee28266ddf2 --- openstackclient/compute/v2/server.py | 17 +- openstackclient/tests/compute/v2/fakes.py | 1 + .../tests/compute/v2/test_server.py | 174 ++++++++++++++++++ openstackclient/tests/utils.py | 6 +- 4 files changed, 194 insertions(+), 4 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 355774c316..a6d645b9e9 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -17,6 +17,7 @@ import argparse import getpass +import io import logging import os import six @@ -296,7 +297,7 @@ def take_action(self, parsed_args): for f in parsed_args.file: dst, src = f.split('=', 1) try: - files[dst] = open(src) + files[dst] = io.open(src, 'rb') except IOError as e: raise exceptions.CommandError("Can't open '%s': %s" % (src, e)) @@ -313,7 +314,7 @@ def take_action(self, parsed_args): userdata = None if parsed_args.user_data: try: - userdata = open(parsed_args.user_data) + userdata = io.open(parsed_args.user_data) except IOError as e: msg = "Can't open '%s': %s" raise exceptions.CommandError(msg % (parsed_args.user_data, e)) @@ -368,7 +369,17 @@ def take_action(self, parsed_args): self.log.debug('boot_args: %s', boot_args) self.log.debug('boot_kwargs: %s', boot_kwargs) - server = compute_client.servers.create(*boot_args, **boot_kwargs) + + # Wrap the call to catch exceptions in order to close files + try: + server = compute_client.servers.create(*boot_args, **boot_kwargs) + finally: + # Clean up open files - make sure they are not strings + for f in files: + if hasattr(f, 'close'): + f.close() + if hasattr(userdata, 'close'): + userdata.close() if parsed_args.wait: if utils.wait_for_status( diff --git a/openstackclient/tests/compute/v2/fakes.py b/openstackclient/tests/compute/v2/fakes.py index 9a7964db0d..e5ed5cbd7d 100644 --- a/openstackclient/tests/compute/v2/fakes.py +++ b/openstackclient/tests/compute/v2/fakes.py @@ -26,6 +26,7 @@ SERVER = { 'id': server_id, 'name': server_name, + 'metadata': {}, } extension_name = 'Multinic' diff --git a/openstackclient/tests/compute/v2/test_server.py b/openstackclient/tests/compute/v2/test_server.py index a98cd15690..50de5c6ab7 100644 --- a/openstackclient/tests/compute/v2/test_server.py +++ b/openstackclient/tests/compute/v2/test_server.py @@ -14,11 +14,13 @@ # import copy +import mock from openstackclient.compute.v2 import server from openstackclient.tests.compute.v2 import fakes as compute_fakes from openstackclient.tests import fakes from openstackclient.tests.image.v2 import fakes as image_fakes +from openstackclient.tests import utils class TestServer(compute_fakes.TestComputev2): @@ -30,6 +32,10 @@ def setUp(self): self.servers_mock = self.app.client_manager.compute.servers self.servers_mock.reset_mock() + # Get a shortcut to the ImageManager Mock + self.cimages_mock = self.app.client_manager.compute.images + self.cimages_mock.reset_mock() + # Get a shortcut to the FlavorManager Mock self.flavors_mock = self.app.client_manager.compute.flavors self.flavors_mock.reset_mock() @@ -39,6 +45,174 @@ def setUp(self): self.images_mock.reset_mock() +class TestServerCreate(TestServer): + + def setUp(self): + super(TestServerCreate, self).setUp() + + self.servers_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(compute_fakes.SERVER), + loaded=True, + ) + new_server = fakes.FakeResource( + None, + copy.deepcopy(compute_fakes.SERVER), + loaded=True, + ) + new_server.__dict__['networks'] = {} + self.servers_mock.get.return_value = new_server + + self.image = fakes.FakeResource( + None, + copy.deepcopy(image_fakes.IMAGE), + loaded=True, + ) + self.cimages_mock.get.return_value = self.image + + self.flavor = fakes.FakeResource( + None, + copy.deepcopy(compute_fakes.FLAVOR), + loaded=True, + ) + self.flavors_mock.get.return_value = self.flavor + + # Get the command object to test + self.cmd = server.CreateServer(self.app, None) + + def test_server_create_no_options(self): + arglist = [ + compute_fakes.server_id, + ] + verifylist = [ + ('server_name', compute_fakes.server_id), + ] + try: + # Missing required args should bail here + self.check_parser(self.cmd, arglist, verifylist) + except utils.ParserException: + pass + + def test_server_create_minimal(self): + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + compute_fakes.server_id, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('config_drive', False), + ('server_name', compute_fakes.server_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = dict( + meta=None, + files={}, + reservation_id=None, + min_count=1, + max_count=1, + security_groups=[], + userdata=None, + key_name=None, + availability_zone=None, + block_device_mapping={}, + nics=[], + scheduler_hints={}, + config_drive=None, + ) + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + compute_fakes.server_id, + self.image, + self.flavor, + **kwargs + ) + + collist = ('addresses', 'flavor', 'id', 'image', 'name', 'properties') + self.assertEqual(columns, collist) + datalist = ( + '', + 'Large ()', + compute_fakes.server_id, + 'graven ()', + compute_fakes.server_name, + '', + ) + self.assertEqual(data, datalist) + + @mock.patch('openstackclient.compute.v2.server.io.open') + def test_server_create_userdata(self, mock_open): + mock_file = mock.MagicMock(name='File') + mock_open.return_value = mock_file + mock_open.read.return_value = '#!/bin/sh' + + arglist = [ + '--image', 'image1', + '--flavor', 'flavor1', + '--user-data', 'userdata.sh', + compute_fakes.server_id, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', 'flavor1'), + ('user_data', 'userdata.sh'), + ('config_drive', False), + ('server_name', compute_fakes.server_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Ensure the userdata file is opened + mock_open.assert_called_with('userdata.sh') + + # Ensure the userdata file is closed + mock_file.close.assert_called() + + # Set expected values + kwargs = dict( + meta=None, + files={}, + reservation_id=None, + min_count=1, + max_count=1, + security_groups=[], + userdata=mock_file, + key_name=None, + availability_zone=None, + block_device_mapping={}, + nics=[], + scheduler_hints={}, + config_drive=None, + ) + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + compute_fakes.server_id, + self.image, + self.flavor, + **kwargs + ) + + collist = ('addresses', 'flavor', 'id', 'image', 'name', 'properties') + self.assertEqual(columns, collist) + datalist = ( + '', + 'Large ()', + compute_fakes.server_id, + 'graven ()', + compute_fakes.server_name, + '', + ) + self.assertEqual(data, datalist) + + class TestServerDelete(TestServer): def setUp(self): diff --git a/openstackclient/tests/utils.py b/openstackclient/tests/utils.py index 38d4725038..25f9852516 100644 --- a/openstackclient/tests/utils.py +++ b/openstackclient/tests/utils.py @@ -23,6 +23,10 @@ from openstackclient.tests import fakes +class ParserException(Exception): + pass + + class TestCase(testtools.TestCase): def setUp(self): testtools.TestCase.setUp(self) @@ -84,7 +88,7 @@ def check_parser(self, cmd, args, verify_args): try: parsed_args = cmd_parser.parse_args(args) except SystemExit: - raise Exception("Argument parse failed") + raise ParserException("Argument parse failed") for av in verify_args: attr, value = av if attr: From deda02331474632c47b88c166241cd65b2952269 Mon Sep 17 00:00:00 2001 From: wanghong Date: Fri, 17 Oct 2014 11:36:53 +0800 Subject: [PATCH 0231/3494] use jsonutils in oslo.serialization instead of keystoneclient keystoneclient/openstack/common/jsonutils.py is removed in this patch https://review.openstack.org/#/c/128454/ Now, we should use jsonutils in oslo.serialization package. Change-Id: I7c8e8e6d5dffa85244368fd578616c9b19f4fd21 --- openstackclient/tests/common/test_clientmanager.py | 2 +- requirements.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/openstackclient/tests/common/test_clientmanager.py b/openstackclient/tests/common/test_clientmanager.py index 18461fb7e8..9e9749c6a4 100644 --- a/openstackclient/tests/common/test_clientmanager.py +++ b/openstackclient/tests/common/test_clientmanager.py @@ -16,8 +16,8 @@ from requests_mock.contrib import fixture from keystoneclient.auth.identity import v2 as auth_v2 -from keystoneclient.openstack.common import jsonutils from keystoneclient import service_catalog +from oslo.serialization import jsonutils from openstackclient.api import auth from openstackclient.common import clientmanager diff --git a/requirements.txt b/requirements.txt index 3fcde32154..54ea795079 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,7 @@ Babel>=1.3 cliff>=1.7.0 # Apache-2.0 oslo.i18n>=1.0.0 # Apache-2.0 oslo.utils>=1.0.0 # Apache-2.0 +oslo.serialization>=1.0.0 # Apache-2.0 python-glanceclient>=0.14.0 python-keystoneclient>=0.11.1 python-novaclient>=2.18.0 From 0de67016c7daa1712b568cb2e49728fac3eb57ad Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 17 Oct 2014 22:26:57 -0500 Subject: [PATCH 0232/3494] Remove now-unnecessary client creation hacks Clients that can use ksc Session don't need the old junk to fake auth anymore: * compute * volume Clients that still need to be fed credentials can pick directly from the auth object in clientmanager. The _token attribute is removed, the token can be retrieved from the auth object: openstackclient/tests/common/test_clientmanager.py This change will break any plugin that relies on getting a token from instance._token. They should be updated to use the above, or preferable, to use keystoneclient.session.Session to create its HTTP interface object. Change-Id: I877a29de97a42f85f12a14c274fc003e6fba5135 --- openstackclient/common/clientmanager.py | 3 --- openstackclient/compute/client.py | 9 +-------- openstackclient/image/client.py | 2 +- openstackclient/network/client.py | 2 +- .../tests/common/test_clientmanager.py | 11 +--------- openstackclient/volume/client.py | 20 ++----------------- 6 files changed, 6 insertions(+), 41 deletions(-) diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index 928ab6eea6..336c0da0e9 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -106,9 +106,6 @@ def __init__(self, auth_options, api_version=None, verify=True): self.auth_ref = self.auth.get_auth_ref(self.session) self._service_catalog = self.auth_ref.service_catalog - # This begone when clients no longer need it... - self._token = self.auth.get_token(self.session) - return def get_endpoint_for_service_type(self, service_type, region_name=None): diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py index ff9ed88a0a..c87bbee700 100644 --- a/openstackclient/compute/client.py +++ b/openstackclient/compute/client.py @@ -43,6 +43,7 @@ def make_client(instance): http_log_debug = utils.get_effective_log_level() <= logging.DEBUG extensions = [extension.Extension('list_extensions', list_extensions)] + client = compute_client( session=instance.session, extensions=extensions, @@ -50,14 +51,6 @@ def make_client(instance): timings=instance.timing, ) - # Populate the Nova client to skip another auth query to Identity - if 'token' not in instance._auth_params: - # password flow - client.client.management_url = instance.get_endpoint_for_service_type( - API_NAME, region_name=instance._region_name) - client.client.service_catalog = instance._service_catalog - client.client.auth_token = instance.auth.get_token(instance.session) - return client diff --git a/openstackclient/image/client.py b/openstackclient/image/client.py index a23d349e01..84f5943754 100644 --- a/openstackclient/image/client.py +++ b/openstackclient/image/client.py @@ -45,7 +45,7 @@ def make_client(instance): return image_client( instance._url, - token=instance._token, + token=instance.auth.get_token(instance.session), cacert=instance._cacert, insecure=instance._insecure, ) diff --git a/openstackclient/network/client.py b/openstackclient/network/client.py index 870fdad1db..e4ce2f6a08 100644 --- a/openstackclient/network/client.py +++ b/openstackclient/network/client.py @@ -44,7 +44,7 @@ def make_client(instance): region_name=instance._region_name, auth_url=instance._auth_url, endpoint_url=instance._url, - token=instance._token, + token=instance.auth.get_token(instance.session), insecure=instance._insecure, ca_cert=instance._cacert, ) diff --git a/openstackclient/tests/common/test_clientmanager.py b/openstackclient/tests/common/test_clientmanager.py index d0738c79bf..24adfa0e0b 100644 --- a/openstackclient/tests/common/test_clientmanager.py +++ b/openstackclient/tests/common/test_clientmanager.py @@ -89,10 +89,9 @@ def test_client_manager_token_endpoint(self): fakes.AUTH_URL, client_manager._url, ) - self.assertEqual( fakes.AUTH_TOKEN, - client_manager._token, + client_manager.auth.get_token(None), ) self.assertIsInstance( client_manager.auth, @@ -111,10 +110,6 @@ def test_client_manager_token(self): verify=True ) - self.assertEqual( - fakes.AUTH_TOKEN, - client_manager._token, - ) self.assertEqual( fakes.AUTH_URL, client_manager._auth_url, @@ -160,10 +155,6 @@ def test_client_manager_password(self): AUTH_REF, client_manager.auth_ref, ) - self.assertEqual( - fakes.AUTH_TOKEN, - client_manager._token, - ) self.assertEqual( dir(SERVICE_CATALOG), dir(client_manager._service_catalog), diff --git a/openstackclient/volume/client.py b/openstackclient/volume/client.py index 58cb267e53..f4e2decb35 100644 --- a/openstackclient/volume/client.py +++ b/openstackclient/volume/client.py @@ -49,29 +49,13 @@ def make_client(instance): http_log_debug = utils.get_effective_log_level() <= logging.DEBUG extensions = [extension.Extension('list_extensions', list_extensions)] + client = volume_client( - username=instance._username, - api_key=instance._password, - project_id=instance._project_name, - auth_url=instance._auth_url, - cacert=instance._cacert, - insecure=instance._insecure, - region_name=instance._region_name, + session=instance.session, extensions=extensions, http_log_debug=http_log_debug, ) - # Populate the Cinder client to skip another auth query to Identity - if instance._url: - # token flow - client.client.management_url = instance._url - else: - # password flow - client.client.management_url = instance.get_endpoint_for_service_type( - API_NAME, region_name=instance._region_name) - client.client.service_catalog = instance._service_catalog - client.client.auth_token = instance._token - return client From 2166d7d3afbbdc1659e4cffdb7bcd890cd00ec19 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 17 Oct 2014 23:43:38 -0500 Subject: [PATCH 0233/3494] Remove ClientManager._service_catalog Anything that needs a service catalog can get it directly from auth_ref.service_catalog, no need to carry the extra attribute. ClientManager.get_endpoint_for_service_type() reamins the proper method to get an endpoint for clients that still need one directly. Change-Id: I809091c9c71d08f29606d7fd8b500898ff2cb8ae --- openstackclient/common/clientmanager.py | 14 +++++------ openstackclient/identity/client.py | 24 ++++--------------- openstackclient/identity/v2_0/service.py | 3 ++- openstackclient/image/client.py | 8 ++++--- openstackclient/network/client.py | 10 ++++---- openstackclient/object/client.py | 8 +++---- .../tests/common/test_clientmanager.py | 2 +- 7 files changed, 29 insertions(+), 40 deletions(-) diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index 336c0da0e9..febcedf4b1 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -66,7 +66,6 @@ def __init__(self, auth_options, api_version=None, verify=True): self._auth_params = auth.build_auth_params(auth_options) self._region_name = auth_options.os_region_name self._api_version = api_version - self._service_catalog = None self.timing = auth_options.timing # For compatibility until all clients can be updated @@ -104,7 +103,6 @@ def __init__(self, auth_options, api_version=None, verify=True): if 'token' not in self._auth_params: LOG.debug("Get service catalog") self.auth_ref = self.auth.get_auth_ref(self.session) - self._service_catalog = self.auth_ref.service_catalog return @@ -112,12 +110,14 @@ def get_endpoint_for_service_type(self, service_type, region_name=None): """Return the endpoint URL for the service type.""" # See if we are using password flow auth, i.e. we have a # service catalog to select endpoints from - if self._service_catalog: - endpoint = self._service_catalog.url_for( - service_type=service_type, region_name=region_name) + if self.auth_ref: + endpoint = self.auth_ref.service_catalog.url_for( + service_type=service_type, + region_name=region_name, + ) else: - # Hope we were given the correct URL. - endpoint = self._auth_url or self._url + # Get the passed endpoint directly from the auth plugin + endpoint = self.auth.get_endpoint(self.session) return endpoint diff --git a/openstackclient/identity/client.py b/openstackclient/identity/client.py index bc10a6d237..8050d12096 100644 --- a/openstackclient/identity/client.py +++ b/openstackclient/identity/client.py @@ -44,27 +44,11 @@ def make_client(instance): API_VERSIONS) LOG.debug('Instantiating identity client: %s', identity_client) - # TODO(dtroyer): Something doesn't like the session.auth when using - # token auth, chase that down. - if instance._url: - LOG.debug('Using service token auth') - client = identity_client( - endpoint=instance._url, - token=instance._auth_params['token'], - cacert=instance._cacert, - insecure=instance._insecure - ) - else: - LOG.debug('Using auth plugin: %s' % instance._auth_plugin) - client = identity_client( - session=instance.session, - cacert=instance._cacert, - ) + LOG.debug('Using auth plugin: %s' % instance._auth_plugin) + client = identity_client( + session=instance.session, + ) - # TODO(dtroyer): the identity v2 role commands use this yet, fix that - # so we can remove it - if not instance._url: - instance.auth_ref = instance.auth.get_auth_ref(instance.session) return client diff --git a/openstackclient/identity/v2_0/service.py b/openstackclient/identity/v2_0/service.py index 458dce7cba..e8848ddee2 100644 --- a/openstackclient/identity/v2_0/service.py +++ b/openstackclient/identity/v2_0/service.py @@ -141,9 +141,10 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity + auth_ref = self.app.client_manager.auth_ref if parsed_args.catalog: - endpoints = identity_client.service_catalog.get_endpoints( + endpoints = auth_ref.service_catalog.get_endpoints( service_type=parsed_args.service) for (service, service_endpoints) in six.iteritems(endpoints): if service_endpoints: diff --git a/openstackclient/image/client.py b/openstackclient/image/client.py index 84f5943754..c55ff85369 100644 --- a/openstackclient/image/client.py +++ b/openstackclient/image/client.py @@ -40,11 +40,13 @@ def make_client(instance): API_VERSIONS) LOG.debug('Instantiating image client: %s', image_client) - if not instance._url: - instance._url = instance.get_endpoint_for_service_type(API_NAME) + endpoint = instance.get_endpoint_for_service_type( + API_NAME, + region_name=instance._region_name, + ) return image_client( - instance._url, + endpoint, token=instance.auth.get_token(instance.session), cacert=instance._cacert, insecure=instance._insecure, diff --git a/openstackclient/network/client.py b/openstackclient/network/client.py index e4ce2f6a08..bb3e1b2397 100644 --- a/openstackclient/network/client.py +++ b/openstackclient/network/client.py @@ -34,16 +34,18 @@ def make_client(instance): API_VERSIONS) LOG.debug('Instantiating network client: %s', network_client) - if not instance._url: - instance._url = instance.get_endpoint_for_service_type( - "network", region_name=instance._region_name) + endpoint = instance.get_endpoint_for_service_type( + API_NAME, + region_name=instance._region_name, + ) + return network_client( username=instance._username, tenant_name=instance._project_name, password=instance._password, region_name=instance._region_name, auth_url=instance._auth_url, - endpoint_url=instance._url, + endpoint_url=endpoint, token=instance.auth.get_token(instance.session), insecure=instance._insecure, ca_cert=instance._cacert, diff --git a/openstackclient/object/client.py b/openstackclient/object/client.py index 1ac905c33e..beb7c04fc1 100644 --- a/openstackclient/object/client.py +++ b/openstackclient/object/client.py @@ -33,10 +33,10 @@ def make_client(instance): """Returns an object-store API client.""" - if instance._url: - endpoint = instance._url - else: - endpoint = instance.get_endpoint_for_service_type("object-store") + endpoint = instance.get_endpoint_for_service_type( + 'object-store', + region_name=instance._region_name, + ) client = object_store_v1.APIv1( session=instance.session, diff --git a/openstackclient/tests/common/test_clientmanager.py b/openstackclient/tests/common/test_clientmanager.py index 24adfa0e0b..a7b13c6c9c 100644 --- a/openstackclient/tests/common/test_clientmanager.py +++ b/openstackclient/tests/common/test_clientmanager.py @@ -157,7 +157,7 @@ def test_client_manager_password(self): ) self.assertEqual( dir(SERVICE_CATALOG), - dir(client_manager._service_catalog), + dir(client_manager.auth_ref.service_catalog), ) def stub_auth(self, json=None, url=None, verb=None, **kwargs): From f600c0eafbf9aff23b6efa1c7b94797a25873b99 Mon Sep 17 00:00:00 2001 From: wanghong Date: Mon, 20 Oct 2014 15:29:53 +0800 Subject: [PATCH 0234/3494] only generate one clientmanager instance in interactive mode Currently, we repeated to generate clientmanager instance when run command in interactive mode. This should be avoided. Change-Id: I0536a690bc173be38af08a2e4443115532041efd Closes-Bug: #1383083 --- openstackclient/shell.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 1f9eb32b53..668e48b5fc 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -68,6 +68,8 @@ def __init__(self): # Assume TLS host certificate verification is enabled self.verify = True + self.client_manager = None + # NOTE(dtroyer): This hack changes the help action that Cliff # automatically adds to the parser so we can defer # its execution until after the api-versioned commands @@ -204,8 +206,12 @@ def build_option_parser(self, description, version): return clientmanager.build_plugin_option_parser(parser) - def authenticate_user(self): - """Verify the required authentication credentials are present""" + def initialize_clientmanager(self): + """Validating authentication options and generate a clientmanager""" + + if self.client_manager: + self.log.debug('The clientmanager has been initialized already') + return self.log.debug("validating authentication options") @@ -370,11 +376,11 @@ def prepare_to_run_command(self, cmd): return if cmd.best_effort: try: - self.authenticate_user() + self.initialize_clientmanager() except Exception: pass else: - self.authenticate_user() + self.initialize_clientmanager() return def clean_up(self, cmd, result, err): @@ -409,7 +415,7 @@ def clean_up(self, cmd, result, err): def interact(self): # NOTE(dtroyer): Maintain the old behaviour for interactive use as # this path does not call prepare_to_run_command() - self.authenticate_user() + self.initialize_clientmanager() super(OpenStackShell, self).interact() From cd368bb81690af5b4e99c0fd71b35fb00c9e0786 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 20 Oct 2014 12:47:39 -0500 Subject: [PATCH 0235/3494] Fix token issue after auth changeup IssueToken.take_action() was missed in updating the structure of the ClientManager. Also, TOKEN_WITH_TENANT_ID in v3 is just wrong... Closes-Bug: #1383396 Change-Id: If2dd82a26af1d743ee9df73e0c1aebce497bf22e --- openstackclient/identity/v3/token.py | 4 +--- openstackclient/tests/identity/v3/fakes.py | 4 ++-- openstackclient/tests/identity/v3/test_token.py | 10 ++++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/openstackclient/identity/v3/token.py b/openstackclient/identity/v3/token.py index aca5c66993..5b09b69f61 100644 --- a/openstackclient/identity/v3/token.py +++ b/openstackclient/identity/v3/token.py @@ -159,9 +159,7 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) - session = self.app.client_manager.identity.session - - token = session.auth.auth_ref.service_catalog.get_token() + token = self.app.client_manager.auth_ref.service_catalog.get_token() if 'tenant_id' in token: token['project_id'] = token.pop('tenant_id') return zip(*sorted(six.iteritems(token))) diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index 69c2590563..5844d160b9 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -176,10 +176,10 @@ token_expires = '2014-01-01T00:00:00Z' token_id = 'tttttttt-tttt-tttt-tttt-tttttttttttt' -TOKEN_WITH_TENANT_ID = { +TOKEN_WITH_PROJECT_ID = { 'expires': token_expires, 'id': token_id, - 'tenant_id': project_id, + 'project_id': project_id, 'user_id': user_id, } diff --git a/openstackclient/tests/identity/v3/test_token.py b/openstackclient/tests/identity/v3/test_token.py index dbe855555c..f43b6f5f24 100644 --- a/openstackclient/tests/identity/v3/test_token.py +++ b/openstackclient/tests/identity/v3/test_token.py @@ -13,6 +13,8 @@ # under the License. # +import mock + from openstackclient.identity.v3 import token from openstackclient.tests.identity.v3 import fakes as identity_fakes @@ -23,9 +25,9 @@ def setUp(self): super(TestToken, self).setUp() # Get a shortcut to the Service Catalog Mock - session = self.app.client_manager.identity.session - self.sc_mock = session.auth.auth_ref.service_catalog - self.sc_mock.reset_mock() + self.sc_mock = mock.Mock() + self.app.client_manager.auth_ref = mock.Mock() + self.app.client_manager.auth_ref.service_catalog = self.sc_mock class TestTokenIssue(TestToken): @@ -40,7 +42,7 @@ def test_token_issue_with_project_id(self): verifylist = [] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.sc_mock.get_token.return_value = \ - identity_fakes.TOKEN_WITH_TENANT_ID + identity_fakes.TOKEN_WITH_PROJECT_ID # DisplayCommandBase.take_action() returns two tuples columns, data = self.cmd.take_action(parsed_args) From e063246b97a7f31a47aca0a5eb36d571f5df7236 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 20 Oct 2014 18:53:10 -0500 Subject: [PATCH 0236/3494] Clean up shell authentication * Remove the auth option checks as the auth plugins will validate their own options * Move the initialization of client_manager to the end of initialize_app() so it is always called. Note that no attempts to actually authenticate occur until the first use of one of the client attributes in client_manager. This leaves initialize_clientmanager() (formerly uathenticate_user()) empty so remove it. * Remove interact() as the client_manager has already been created And there is nothing left. * prepare_to_run_command() is reduced to trigger an authentication attempt for the best_effort auth commands, currently the only one is 'complete'. * Add prompt_for_password() to ask the user to enter a password when necessary. Passed to ClientManager in a new kward pw_func. Bug: 1355838 Change-Id: I9fdec9144c4c84f65aed1cf91ce41fe1895089b2 --- openstackclient/api/auth.py | 2 +- openstackclient/common/clientmanager.py | 46 +++++-- openstackclient/shell.py | 152 ++++++------------------ 3 files changed, 74 insertions(+), 126 deletions(-) diff --git a/openstackclient/api/auth.py b/openstackclient/api/auth.py index e33b72d575..f6e99cdcf0 100644 --- a/openstackclient/api/auth.py +++ b/openstackclient/api/auth.py @@ -62,7 +62,7 @@ def select_auth_plugin(options): if options.os_url and options.os_token: # service token authentication auth_plugin = 'token_endpoint' - elif options.os_password: + elif options.os_username: if options.os_identity_api_version == '3': auth_plugin = 'v3password' elif options.os_identity_api_version == '2.0': diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index febcedf4b1..ae38f16077 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -55,17 +55,46 @@ def __getattr__(self, name): for o in auth.OPTIONS_LIST]: return self._auth_params[name[1:]] - def __init__(self, auth_options, api_version=None, verify=True): + def __init__( + self, + auth_options, + api_version=None, + verify=True, + pw_func=None, + ): + """Set up a ClientManager + + :param auth_options: + Options collected from the command-line, environment, or wherever + :param api_version: + Dict of API versions: key is API name, value is the version + :param verify: + TLS certificate verification; may be a boolean to enable or disable + server certificate verification, or a filename of a CA certificate + bundle to be used in verification (implies True) + :param pw_func: + Callback function for asking the user for a password. The function + takes an optional string for the prompt ('Password: ' on None) and + returns a string containig the password + """ + # If no plugin is named by the user, select one based on # the supplied options if not auth_options.os_auth_plugin: auth_options.os_auth_plugin = auth.select_auth_plugin(auth_options) - self._auth_plugin = auth_options.os_auth_plugin + + # Horrible hack alert...must handle prompt for null password if + # password auth is requested. + if (self._auth_plugin.endswith('password') and + not auth_options.os_password): + auth_options.os_password = pw_func() + self._url = auth_options.os_url self._auth_params = auth.build_auth_params(auth_options) self._region_name = auth_options.os_region_name self._api_version = api_version + self._auth_ref = None self.timing = auth_options.timing # For compatibility until all clients can be updated @@ -99,13 +128,16 @@ def __init__(self, auth_options, api_version=None, verify=True): verify=verify, ) - self.auth_ref = None - if 'token' not in self._auth_params: - LOG.debug("Get service catalog") - self.auth_ref = self.auth.get_auth_ref(self.session) - return + @property + def auth_ref(self): + """Dereference will trigger an auth if it hasn't already""" + if not self._auth_ref: + LOG.debug("Get auth_ref") + self._auth_ref = self.auth.get_auth_ref(self.session) + return self._auth_ref + def get_endpoint_for_service_type(self, service_type, region_name=None): """Return the endpoint URL for the service type.""" # See if we are using password flow auth, i.e. we have a diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 668e48b5fc..e671ecc3dc 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -36,6 +36,30 @@ DEFAULT_DOMAIN = 'default' +def prompt_for_password(prompt=None): + """Prompt user for a password + + Propmpt for a password if stdin is a tty. + """ + + if not prompt: + prompt = 'Password: ' + pw = None + # If stdin is a tty, try prompting for the password + if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty(): + # Check for Ctl-D + try: + pw = getpass.getpass(prompt) + except EOFError: + pass + # No password because we did't have a tty or nothing was entered + if not pw: + raise exc.CommandError( + "No password entered, or found via --os-password or OS_PASSWORD", + ) + return pw + + class OpenStackShell(app.App): CONSOLE_MESSAGE_FORMAT = '%(levelname)s: %(name)s %(message)s' @@ -206,112 +230,6 @@ def build_option_parser(self, description, version): return clientmanager.build_plugin_option_parser(parser) - def initialize_clientmanager(self): - """Validating authentication options and generate a clientmanager""" - - if self.client_manager: - self.log.debug('The clientmanager has been initialized already') - return - - self.log.debug("validating authentication options") - - # Assuming all auth plugins will be named in the same fashion, - # ie vXpluginName - if (not self.options.os_url and - self.options.os_auth_plugin.startswith('v') and - self.options.os_auth_plugin[1] != - self.options.os_identity_api_version[0]): - raise exc.CommandError( - "Auth plugin %s not compatible" - " with requested API version" % self.options.os_auth_plugin - ) - # TODO(mhu) All these checks should be exposed at the plugin level - # or just dropped altogether, as the client instantiation will fail - # anyway - if self.options.os_url and not self.options.os_token: - # service token needed - raise exc.CommandError( - "You must provide a service token via" - " either --os-token or env[OS_TOKEN]") - - if (self.options.os_auth_plugin.endswith('token') and - (self.options.os_token or self.options.os_auth_url)): - # Token flow auth takes priority - if not self.options.os_token: - raise exc.CommandError( - "You must provide a token via" - " either --os-token or env[OS_TOKEN]") - - if not self.options.os_auth_url: - raise exc.CommandError( - "You must provide a service URL via" - " either --os-auth-url or env[OS_AUTH_URL]") - - if (not self.options.os_url and - not self.options.os_auth_plugin.endswith('token')): - # Validate password flow auth - if not self.options.os_username: - raise exc.CommandError( - "You must provide a username via" - " either --os-username or env[OS_USERNAME]") - - if not self.options.os_password: - # No password, if we've got a tty, try prompting for it - if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty(): - # Check for Ctl-D - try: - self.options.os_password = getpass.getpass() - except EOFError: - pass - # No password because we did't have a tty or the - # user Ctl-D when prompted? - if not self.options.os_password: - raise exc.CommandError( - "You must provide a password via" - " either --os-password, or env[OS_PASSWORD], " - " or prompted response") - - if not ((self.options.os_project_id - or self.options.os_project_name) or - (self.options.os_domain_id - or self.options.os_domain_name) or - self.options.os_trust_id): - if self.options.os_auth_plugin.endswith('password'): - raise exc.CommandError( - "You must provide authentication scope as a project " - "or a domain via --os-project-id " - "or env[OS_PROJECT_ID], " - "--os-project-name or env[OS_PROJECT_NAME], " - "--os-domain-id or env[OS_DOMAIN_ID], or" - "--os-domain-name or env[OS_DOMAIN_NAME], or " - "--os-trust-id or env[OS_TRUST_ID].") - - if not self.options.os_auth_url: - raise exc.CommandError( - "You must provide an auth url via" - " either --os-auth-url or via env[OS_AUTH_URL]") - - if (self.options.os_trust_id and - self.options.os_identity_api_version != '3'): - raise exc.CommandError( - "Trusts can only be used with Identity API v3") - - if (self.options.os_trust_id and - ((self.options.os_project_id - or self.options.os_project_name) or - (self.options.os_domain_id - or self.options.os_domain_name))): - raise exc.CommandError( - "Authentication cannot be scoped to multiple targets. " - "Pick one of project, domain or trust.") - - self.client_manager = clientmanager.ClientManager( - auth_options=self.options, - verify=self.verify, - api_version=self.api_version, - ) - return - def initialize_app(self, argv): """Global app init bits: @@ -368,19 +286,23 @@ def initialize_app(self, argv): else: self.verify = not self.options.insecure + self.client_manager = clientmanager.ClientManager( + auth_options=self.options, + verify=self.verify, + api_version=self.api_version, + pw_func=prompt_for_password, + ) + def prepare_to_run_command(self, cmd): """Set up auth and API versions""" self.log.debug('prepare_to_run_command %s', cmd.__class__.__name__) - if not cmd.auth_required: - return - if cmd.best_effort: + if cmd.auth_required and cmd.best_effort: try: - self.initialize_clientmanager() + # Trigger the Identity client to initialize + self.client_manager.auth_ref except Exception: pass - else: - self.initialize_clientmanager() return def clean_up(self, cmd, result, err): @@ -412,12 +334,6 @@ def clean_up(self, cmd, result, err): targs = tparser.parse_args(['-f', format]) tcmd.run(targs) - def interact(self): - # NOTE(dtroyer): Maintain the old behaviour for interactive use as - # this path does not call prepare_to_run_command() - self.initialize_clientmanager() - super(OpenStackShell, self).interact() - def main(argv=sys.argv[1:]): return OpenStackShell().run(argv) From e7bba3211a08a295a15997a552237b06e2238e3d Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 21 Oct 2014 18:00:13 -0400 Subject: [PATCH 0237/3494] Include support for using oslo debugger in tests Simply run `tox -e debug ` to get an interactive debugging prompt Change-Id: I09e5b844a33c2f0fd4230f01fbc6c0aa8d752545 --- test-requirements.txt | 1 + tox.ini | 3 +++ 2 files changed, 4 insertions(+) diff --git a/test-requirements.txt b/test-requirements.txt index 1bd4efb8a0..259ace266c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,6 +8,7 @@ discover fixtures>=0.3.14 mock>=1.0 oslosphinx>=2.2.0 # Apache-2.0 +oslotest>=1.2.0 # Apache-2.0 requests-mock>=0.4.0 # Apache-2.0 sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 testrepository>=0.0.18 diff --git a/tox.ini b/tox.ini index cac6f1169f..02dac23a7e 100644 --- a/tox.ini +++ b/tox.ini @@ -25,6 +25,9 @@ commands = {posargs} [testenv:cover] commands = python setup.py test --coverage --testr-args='{posargs}' +[testenv:debug] +commands = oslo_debug_helper -t openstackclient/tests {posargs} + [tox:jenkins] downloadcache = ~/cache/pip From c91d1ca66359fee1a1509f0ac6da4c32159bb59a Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 22 Oct 2014 16:04:11 -0500 Subject: [PATCH 0238/3494] Beef up object-store tests * Add object top-to-bottom tests * Move some fakes around * Clean up existing object tests Change-Id: If8406da611c11bbd2b1bf5153e45b720b0eea442 --- openstackclient/tests/object/v1/fakes.py | 10 +- .../tests/object/v1/test_container.py | 28 +-- .../tests/object/v1/test_container_all.py | 35 ++-- .../tests/object/v1/test_object.py | 36 ++-- .../tests/object/v1/test_object_all.py | 177 ++++++++++++++++++ 5 files changed, 228 insertions(+), 58 deletions(-) create mode 100644 openstackclient/tests/object/v1/test_object_all.py diff --git a/openstackclient/tests/object/v1/fakes.py b/openstackclient/tests/object/v1/fakes.py index f10437b63f..6aef05b1e8 100644 --- a/openstackclient/tests/object/v1/fakes.py +++ b/openstackclient/tests/object/v1/fakes.py @@ -13,7 +13,8 @@ # under the License. # -from openstackclient.tests import fakes +from keystoneclient import session +from openstackclient.api import object_store_v1 as object_store from openstackclient.tests import utils @@ -78,7 +79,8 @@ class TestObjectv1(utils.TestCommand): def setUp(self): super(TestObjectv1, self).setUp() - self.app.client_manager.object_store = fakes.FakeClient( - endpoint=fakes.AUTH_URL, - token=fakes.AUTH_TOKEN, + self.app.client_manager.session = session.Session() + self.app.client_manager.object_store = object_store.APIv1( + session=self.app.client_manager.session, + endpoint=ENDPOINT, ) diff --git a/openstackclient/tests/object/v1/test_container.py b/openstackclient/tests/object/v1/test_container.py index 70b88fc425..2ef2f7b52a 100644 --- a/openstackclient/tests/object/v1/test_container.py +++ b/openstackclient/tests/object/v1/test_container.py @@ -74,13 +74,13 @@ def test_object_list_containers_no_options(self, c_mock): ) collist = ('Name',) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( (object_fakes.container_name, ), (object_fakes.container_name_3, ), (object_fakes.container_name_2, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_object_list_containers_prefix(self, c_mock): c_mock.return_value = [ @@ -108,12 +108,12 @@ def test_object_list_containers_prefix(self, c_mock): ) collist = ('Name',) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( (object_fakes.container_name, ), (object_fakes.container_name_3, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_object_list_containers_marker(self, c_mock): c_mock.return_value = [ @@ -144,12 +144,12 @@ def test_object_list_containers_marker(self, c_mock): ) collist = ('Name',) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( (object_fakes.container_name, ), (object_fakes.container_name_3, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_object_list_containers_limit(self, c_mock): c_mock.return_value = [ @@ -177,12 +177,12 @@ def test_object_list_containers_limit(self, c_mock): ) collist = ('Name',) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( (object_fakes.container_name, ), (object_fakes.container_name_3, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_object_list_containers_long(self, c_mock): c_mock.return_value = [ @@ -209,7 +209,7 @@ def test_object_list_containers_long(self, c_mock): ) collist = ('Name', 'Bytes', 'Count') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( ( object_fakes.container_name, @@ -222,7 +222,7 @@ def test_object_list_containers_long(self, c_mock): object_fakes.container_count * 3, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_object_list_containers_all(self, c_mock): c_mock.return_value = [ @@ -251,13 +251,13 @@ def test_object_list_containers_all(self, c_mock): ) collist = ('Name',) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( (object_fakes.container_name, ), (object_fakes.container_name_2, ), (object_fakes.container_name_3, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) @mock.patch( @@ -295,10 +295,10 @@ def test_container_show(self, c_mock): ) collist = ('bytes', 'count', 'name') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( object_fakes.container_bytes, object_fakes.container_count, object_fakes.container_name, ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) diff --git a/openstackclient/tests/object/v1/test_container_all.py b/openstackclient/tests/object/v1/test_container_all.py index 53b60b9abb..8b200e09e3 100644 --- a/openstackclient/tests/object/v1/test_container_all.py +++ b/openstackclient/tests/object/v1/test_container_all.py @@ -15,34 +15,25 @@ from requests_mock.contrib import fixture -from keystoneclient import session -from openstackclient.api import object_store_v1 as object_store -from openstackclient.object.v1 import container +from openstackclient.object.v1 import container as container_cmds from openstackclient.tests.object.v1 import fakes as object_fakes -class TestObjectAll(object_fakes.TestObjectv1): +class TestContainerAll(object_fakes.TestObjectv1): def setUp(self): - super(TestObjectAll, self).setUp() + super(TestContainerAll, self).setUp() - self.app.client_manager.session = session.Session() self.requests_mock = self.useFixture(fixture.Fixture()) - # TODO(dtroyer): move this to object_fakes.TestObjectv1 - self.app.client_manager.object_store = object_store.APIv1( - session=self.app.client_manager.session, - endpoint=object_fakes.ENDPOINT, - ) - -class TestContainerCreate(TestObjectAll): +class TestContainerCreate(TestContainerAll): def setUp(self): super(TestContainerCreate, self).setUp() # Get the command object to test - self.cmd = container.CreateContainer(self.app, None) + self.cmd = container_cmds.CreateContainer(self.app, None) def test_object_create_container_single(self): self.requests_mock.register_uri( @@ -115,13 +106,13 @@ def test_object_create_container_more(self): self.assertEqual(datalist, list(data)) -class TestContainerDelete(TestObjectAll): +class TestContainerDelete(TestContainerAll): def setUp(self): super(TestContainerDelete, self).setUp() # Get the command object to test - self.cmd = container.DeleteContainer(self.app, None) + self.cmd = container_cmds.DeleteContainer(self.app, None) def test_object_delete_container_single(self): self.requests_mock.register_uri( @@ -168,13 +159,13 @@ def test_object_delete_container_more(self): self.assertIsNone(ret) -class TestContainerList(TestObjectAll): +class TestContainerList(TestContainerAll): def setUp(self): super(TestContainerList, self).setUp() # Get the command object to test - self.cmd = container.ListContainer(self.app, None) + self.cmd = container_cmds.ListContainer(self.app, None) def test_object_list_containers_no_options(self): return_body = [ @@ -237,13 +228,13 @@ def test_object_list_containers_prefix(self): self.assertEqual(datalist, list(data)) -class TestContainerSave(TestObjectAll): +class TestContainerSave(TestContainerAll): def setUp(self): super(TestContainerSave, self).setUp() # Get the command object to test - self.cmd = container.SaveContainer(self.app, None) + self.cmd = container_cmds.SaveContainer(self.app, None) # TODO(dtroyer): need to mock out object_lib.save_object() to test this # def test_object_save_container(self): @@ -285,13 +276,13 @@ def setUp(self): # self.assertIsNone(ret) -class TestContainerShow(TestObjectAll): +class TestContainerShow(TestContainerAll): def setUp(self): super(TestContainerShow, self).setUp() # Get the command object to test - self.cmd = container.ShowContainer(self.app, None) + self.cmd = container_cmds.ShowContainer(self.app, None) def test_object_show_container(self): headers = { diff --git a/openstackclient/tests/object/v1/test_object.py b/openstackclient/tests/object/v1/test_object.py index 22f06999e2..305fe8f83b 100644 --- a/openstackclient/tests/object/v1/test_object.py +++ b/openstackclient/tests/object/v1/test_object.py @@ -68,12 +68,12 @@ def test_object_list_objects_no_options(self, o_mock): ) collist = ('Name',) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( (object_fakes.object_name_1, ), (object_fakes.object_name_2, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_object_list_objects_prefix(self, o_mock): o_mock.return_value = [ @@ -103,11 +103,11 @@ def test_object_list_objects_prefix(self, o_mock): ) collist = ('Name',) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( (object_fakes.object_name_2, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_object_list_objects_delimiter(self, o_mock): o_mock.return_value = [ @@ -137,11 +137,11 @@ def test_object_list_objects_delimiter(self, o_mock): ) collist = ('Name',) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( (object_fakes.object_name_2, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_object_list_objects_marker(self, o_mock): o_mock.return_value = [ @@ -171,11 +171,11 @@ def test_object_list_objects_marker(self, o_mock): ) collist = ('Name',) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( (object_fakes.object_name_2, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_object_list_objects_end_marker(self, o_mock): o_mock.return_value = [ @@ -205,11 +205,11 @@ def test_object_list_objects_end_marker(self, o_mock): ) collist = ('Name',) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( (object_fakes.object_name_2, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_object_list_objects_limit(self, o_mock): o_mock.return_value = [ @@ -239,11 +239,11 @@ def test_object_list_objects_limit(self, o_mock): ) collist = ('Name',) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( (object_fakes.object_name_2, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_object_list_objects_long(self, o_mock): o_mock.return_value = [ @@ -273,7 +273,7 @@ def test_object_list_objects_long(self, o_mock): ) collist = ('Name', 'Bytes', 'Hash', 'Content Type', 'Last Modified') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( ( object_fakes.object_name_1, @@ -290,7 +290,7 @@ def test_object_list_objects_long(self, o_mock): object_fakes.object_modified_2, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_object_list_objects_all(self, o_mock): o_mock.return_value = [ @@ -321,12 +321,12 @@ def test_object_list_objects_all(self, o_mock): ) collist = ('Name',) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( (object_fakes.object_name_1, ), (object_fakes.object_name_2, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) @mock.patch( @@ -367,7 +367,7 @@ def test_object_show(self, c_mock): ) collist = ('bytes', 'content_type', 'hash', 'last_modified', 'name') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( object_fakes.object_bytes_1, object_fakes.object_content_type_1, @@ -375,4 +375,4 @@ def test_object_show(self, c_mock): object_fakes.object_modified_1, object_fakes.object_name_1, ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) diff --git a/openstackclient/tests/object/v1/test_object_all.py b/openstackclient/tests/object/v1/test_object_all.py new file mode 100644 index 0000000000..f2001e91d7 --- /dev/null +++ b/openstackclient/tests/object/v1/test_object_all.py @@ -0,0 +1,177 @@ +# 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 copy + +from requests_mock.contrib import fixture + +from openstackclient.object.v1 import object as object_cmds +from openstackclient.tests.object.v1 import fakes as object_fakes + + +class TestObjectAll(object_fakes.TestObjectv1): + def setUp(self): + super(TestObjectAll, self).setUp() + + self.requests_mock = self.useFixture(fixture.Fixture()) + + +class TestObjectCreate(TestObjectAll): + + def setUp(self): + super(TestObjectCreate, self).setUp() + + # Get the command object to test + self.cmd = object_cmds.CreateObject(self.app, None) + + +class TestObjectList(TestObjectAll): + + def setUp(self): + super(TestObjectList, self).setUp() + + # Get the command object to test + self.cmd = object_cmds.ListObject(self.app, None) + + def test_object_list_objects_no_options(self): + return_body = [ + copy.deepcopy(object_fakes.OBJECT), + copy.deepcopy(object_fakes.OBJECT_2), + ] + self.requests_mock.register_uri( + 'GET', + object_fakes.ENDPOINT + + '/' + + object_fakes.container_name + + '?format=json', + json=return_body, + status_code=200, + ) + + arglist = [ + object_fakes.container_name, + ] + verifylist = [ + ('container', object_fakes.container_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Lister.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + collist = ('Name',) + self.assertEqual(collist, columns) + datalist = [ + (object_fakes.object_name_1, ), + (object_fakes.object_name_2, ), + ] + self.assertEqual(datalist, list(data)) + + def test_object_list_objects_prefix(self): + return_body = [ + copy.deepcopy(object_fakes.OBJECT_2), + ] + self.requests_mock.register_uri( + 'GET', + object_fakes.ENDPOINT + + '/' + + object_fakes.container_name_2 + + '?prefix=floppy&format=json', + json=return_body, + status_code=200, + ) + + arglist = [ + '--prefix', 'floppy', + object_fakes.container_name_2, + ] + verifylist = [ + ('prefix', 'floppy'), + ('container', object_fakes.container_name_2), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + collist = ('Name',) + self.assertEqual(columns, collist) + datalist = ( + (object_fakes.object_name_2, ), + ) + self.assertEqual(tuple(data), datalist) + + +class TestObjectShow(TestObjectAll): + + def setUp(self): + super(TestObjectShow, self).setUp() + + # Get the command object to test + self.cmd = object_cmds.ShowObject(self.app, None) + + def test_object_show(self): + headers = { + 'content-type': 'text/plain', + 'content-length': '20', + 'last-modified': 'yesterday', + 'etag': '4c4e39a763d58392724bccf76a58783a', + 'x-container-meta-owner': object_fakes.ACCOUNT_ID, + 'x-object-manifest': 'manifest', + } + self.requests_mock.register_uri( + 'HEAD', + '/'.join([ + object_fakes.ENDPOINT, + object_fakes.container_name, + object_fakes.object_name_1, + ]), + headers=headers, + status_code=200, + ) + + arglist = [ + object_fakes.container_name, + object_fakes.object_name_1, + ] + verifylist = [ + ('container', object_fakes.container_name), + ('object', object_fakes.object_name_1), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + collist = ( + 'account', + 'container', + 'content-length', + 'content-type', + 'etag', + 'last-modified', + 'object', + 'x-object-manifest', + ) + self.assertEqual(collist, columns) + datalist = ( + object_fakes.ACCOUNT_ID, + object_fakes.container_name, + '20', + 'text/plain', + '4c4e39a763d58392724bccf76a58783a', + 'yesterday', + object_fakes.object_name_1, + 'manifest', + ) + self.assertEqual(datalist, data) From f079b5b9c4c030293b4ebfdf84d8b768b3aa3515 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 22 Oct 2014 11:12:47 -0500 Subject: [PATCH 0239/3494] Change --os-auth-plugin to --os-auth-type User's don't know what a plugin is. * Internally, os_auth_type and/or auth_type represents what the user supplied. * auth_plugin_name is the name of the selected plugin * auth_plugin is the actual plugin object Plugin selection process: * if --os-auth-type is supplied: * if it matches against an available plugin, done * (if it can map to an availble plugin type, done; TODO in a followup) * if --os-auth-type is not supplied: * if --os-url and --os-token are supplied, select 'token_endpoint' * if --os-username supplied, select identity_api_version + 'password' * if --os-token supplied, select identity_api_version + 'token' Change-Id: Ice4535214e311ebf924087cf77f6d84d76f5f3ee --- openstackclient/api/auth.py | 64 +++++++++---------- openstackclient/common/clientmanager.py | 18 +++--- .../tests/common/test_clientmanager.py | 16 ++--- openstackclient/tests/test_shell.py | 36 +++++------ 4 files changed, 66 insertions(+), 68 deletions(-) diff --git a/openstackclient/api/auth.py b/openstackclient/api/auth.py index f6e99cdcf0..e19c6b7931 100644 --- a/openstackclient/api/auth.py +++ b/openstackclient/api/auth.py @@ -36,8 +36,6 @@ invoke_on_load=False, propagate_map_exceptions=True, ) -# TODO(dtroyer): add some method to list the plugins for the -# --os_auth_plugin option # Get the command line options so the help action has them available OPTIONS_LIST = {} @@ -56,50 +54,53 @@ def select_auth_plugin(options): - """If no auth plugin was specified, pick one based on other options""" + """Pick an auth plugin based on --os-auth-type or other options""" + + auth_plugin_name = None + + if options.os_auth_type in [plugin.name for plugin in PLUGIN_LIST]: + # A direct plugin name was given, use it + return options.os_auth_type - auth_plugin = None if options.os_url and options.os_token: # service token authentication - auth_plugin = 'token_endpoint' + auth_plugin_name = 'token_endpoint' elif options.os_username: if options.os_identity_api_version == '3': - auth_plugin = 'v3password' + auth_plugin_name = 'v3password' elif options.os_identity_api_version == '2.0': - auth_plugin = 'v2password' + auth_plugin_name = 'v2password' else: # let keystoneclient figure it out itself - auth_plugin = 'password' + auth_plugin_name = 'password' elif options.os_token: if options.os_identity_api_version == '3': - auth_plugin = 'v3token' + auth_plugin_name = 'v3token' elif options.os_identity_api_version == '2.0': - auth_plugin = 'v2token' + auth_plugin_name = 'v2token' else: # let keystoneclient figure it out itself - auth_plugin = 'token' + auth_plugin_name = 'token' else: raise exc.CommandError( - "Could not figure out which authentication method " - "to use, please set --os-auth-plugin" + "Authentication type must be selected with --os-auth-type" ) - LOG.debug("No auth plugin selected, picking %s from other " - "options" % auth_plugin) - return auth_plugin + LOG.debug("Auth plugin %s selected" % auth_plugin_name) + return auth_plugin_name -def build_auth_params(cmd_options): +def build_auth_params(auth_plugin_name, cmd_options): auth_params = {} - if cmd_options.os_auth_plugin: - LOG.debug('auth_plugin: %s', cmd_options.os_auth_plugin) - auth_plugin = base.get_plugin_class(cmd_options.os_auth_plugin) - plugin_options = auth_plugin.get_options() + if auth_plugin_name: + LOG.debug('auth_type: %s', auth_plugin_name) + auth_plugin_class = base.get_plugin_class(auth_plugin_name) + plugin_options = auth_plugin_class.get_options() for option in plugin_options: option_name = 'os_' + option.dest LOG.debug('fetching option %s' % option_name) auth_params[option.dest] = getattr(cmd_options, option_name, None) # grab tenant from project for v2.0 API compatibility - if cmd_options.os_auth_plugin.startswith("v2"): + if auth_plugin_name.startswith("v2"): auth_params['tenant_id'] = getattr( cmd_options, 'os_project_id', @@ -111,14 +112,14 @@ def build_auth_params(cmd_options): None, ) else: - LOG.debug('no auth_plugin') + LOG.debug('no auth_type') # delay the plugin choice, grab every option plugin_options = set([o.replace('-', '_') for o in OPTIONS_LIST]) for option in plugin_options: option_name = 'os_' + option LOG.debug('fetching option %s' % option_name) auth_params[option] = getattr(cmd_options, option_name, None) - return auth_params + return (auth_plugin_class, auth_params) def build_auth_plugins_option_parser(parser): @@ -130,15 +131,12 @@ def build_auth_plugins_option_parser(parser): """ available_plugins = [plugin.name for plugin in PLUGIN_LIST] parser.add_argument( - '--os-auth-plugin', - metavar='', - default=utils.env('OS_AUTH_PLUGIN'), - help='The authentication method to use. If this option is not set, ' - 'openstackclient will attempt to guess the authentication method ' - 'to use based on the other options. If this option is set, ' - 'the --os-identity-api-version argument must be consistent ' - 'with the version of the method.\nAvailable methods are ' + - ', '.join(available_plugins), + '--os-auth-type', + metavar='', + default=utils.env('OS_AUTH_TYPE'), + help='Select an auhentication type. Available types: ' + + ', '.join(available_plugins) + + '. Default: selected based on --os-username/--os-token', choices=available_plugins ) # make sur we catch old v2.0 env values diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index ae38f16077..adec842f13 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -19,7 +19,6 @@ import pkg_resources import sys -from keystoneclient.auth import base from keystoneclient import session import requests @@ -78,20 +77,22 @@ def __init__( returns a string containig the password """ - # If no plugin is named by the user, select one based on + # If no auth type is named by the user, select one based on # the supplied options - if not auth_options.os_auth_plugin: - auth_options.os_auth_plugin = auth.select_auth_plugin(auth_options) - self._auth_plugin = auth_options.os_auth_plugin + self.auth_plugin_name = auth.select_auth_plugin(auth_options) # Horrible hack alert...must handle prompt for null password if # password auth is requested. - if (self._auth_plugin.endswith('password') and + if (self.auth_plugin_name.endswith('password') and not auth_options.os_password): auth_options.os_password = pw_func() + (auth_plugin, self._auth_params) = auth.build_auth_params( + self.auth_plugin_name, + auth_options, + ) + self._url = auth_options.os_url - self._auth_params = auth.build_auth_params(auth_options) self._region_name = auth_options.os_region_name self._api_version = api_version self._auth_ref = None @@ -117,8 +118,7 @@ def __init__( root_logger = logging.getLogger('') LOG.setLevel(root_logger.getEffectiveLevel()) - LOG.debug('Using auth plugin: %s' % self._auth_plugin) - auth_plugin = base.get_plugin_class(self._auth_plugin) + LOG.debug('Using auth plugin: %s' % self.auth_plugin_name) self.auth = auth_plugin.load_from_options(**self._auth_params) # needed by SAML authentication request_session = requests.session() diff --git a/openstackclient/tests/common/test_clientmanager.py b/openstackclient/tests/common/test_clientmanager.py index a7b13c6c9c..8c27e5621d 100644 --- a/openstackclient/tests/common/test_clientmanager.py +++ b/openstackclient/tests/common/test_clientmanager.py @@ -44,7 +44,7 @@ class FakeOptions(object): def __init__(self, **kwargs): for option in auth.OPTIONS_LIST: setattr(self, 'os_' + option.replace('-', '_'), None) - self.os_auth_plugin = None + self.os_auth_type = None self.os_identity_api_version = '2.0' self.timing = None self.os_region_name = None @@ -81,7 +81,7 @@ def test_client_manager_token_endpoint(self): client_manager = clientmanager.ClientManager( auth_options=FakeOptions(os_token=fakes.AUTH_TOKEN, os_url=fakes.AUTH_URL, - os_auth_plugin='token_endpoint'), + os_auth_type='token_endpoint'), api_version=API_VERSION, verify=True ) @@ -105,7 +105,7 @@ def test_client_manager_token(self): client_manager = clientmanager.ClientManager( auth_options=FakeOptions(os_token=fakes.AUTH_TOKEN, os_auth_url=fakes.AUTH_URL, - os_auth_plugin='v2token'), + os_auth_type='v2token'), api_version=API_VERSION, verify=True ) @@ -183,7 +183,7 @@ def test_client_manager_password_verify_ca(self): auth_options=FakeOptions(os_auth_url=fakes.AUTH_URL, os_username=fakes.USERNAME, os_password=fakes.PASSWORD, - os_auth_plugin='v2password'), + os_auth_type='v2password'), api_version=API_VERSION, verify='cafile', ) @@ -192,8 +192,8 @@ def test_client_manager_password_verify_ca(self): self.assertTrue(client_manager._verify) self.assertEqual('cafile', client_manager._cacert) - def _select_auth_plugin(self, auth_params, api_version, auth_plugin): - auth_params['os_auth_plugin'] = auth_plugin + def _select_auth_plugin(self, auth_params, api_version, auth_plugin_name): + auth_params['os_auth_type'] = auth_plugin_name auth_params['os_identity_api_version'] = api_version client_manager = clientmanager.ClientManager( auth_options=FakeOptions(**auth_params), @@ -201,8 +201,8 @@ def _select_auth_plugin(self, auth_params, api_version, auth_plugin): verify=True ) self.assertEqual( - auth_plugin, - client_manager._auth_plugin, + auth_plugin_name, + client_manager.auth_plugin_name, ) def test_client_manager_select_auth_plugin(self): diff --git a/openstackclient/tests/test_shell.py b/openstackclient/tests/test_shell.py index b0c1452ef9..837a48afd7 100644 --- a/openstackclient/tests/test_shell.py +++ b/openstackclient/tests/test_shell.py @@ -108,8 +108,8 @@ def _assert_password_auth(self, cmd_options, default_args): default_args["region_name"]) self.assertEqual(_shell.options.os_trust_id, default_args["trust_id"]) - self.assertEqual(_shell.options.os_auth_plugin, - default_args['auth_plugin']) + self.assertEqual(_shell.options.os_auth_type, + default_args['auth_type']) def _assert_token_auth(self, cmd_options, default_args): with mock.patch("openstackclient.shell.OpenStackShell.initialize_app", @@ -190,7 +190,7 @@ def test_only_url_flow(self): "password": "", "region_name": "", "trust_id": "", - "auth_plugin": "", + "auth_type": "", } self._assert_password_auth(flag, kwargs) @@ -210,7 +210,7 @@ def test_only_project_id_flow(self): "password": "", "region_name": "", "trust_id": "", - "auth_plugin": "", + "auth_type": "", } self._assert_password_auth(flag, kwargs) @@ -230,7 +230,7 @@ def test_only_project_name_flow(self): "password": "", "region_name": "", "trust_id": "", - "auth_plugin": "", + "auth_type": "", } self._assert_password_auth(flag, kwargs) @@ -250,7 +250,7 @@ def test_only_domain_id_flow(self): "password": "", "region_name": "", "trust_id": "", - "auth_plugin": "", + "auth_type": "", } self._assert_password_auth(flag, kwargs) @@ -270,7 +270,7 @@ def test_only_domain_name_flow(self): "password": "", "region_name": "", "trust_id": "", - "auth_plugin": "", + "auth_type": "", } self._assert_password_auth(flag, kwargs) @@ -290,7 +290,7 @@ def test_only_user_domain_id_flow(self): "password": "", "region_name": "", "trust_id": "", - "auth_plugin": "", + "auth_type": "", } self._assert_password_auth(flag, kwargs) @@ -310,7 +310,7 @@ def test_only_user_domain_name_flow(self): "password": "", "region_name": "", "trust_id": "", - "auth_plugin": "", + "auth_type": "", } self._assert_password_auth(flag, kwargs) @@ -330,7 +330,7 @@ def test_only_project_domain_id_flow(self): "password": "", "region_name": "", "trust_id": "", - "auth_plugin": "", + "auth_type": "", } self._assert_password_auth(flag, kwargs) @@ -350,7 +350,7 @@ def test_only_project_domain_name_flow(self): "password": "", "region_name": "", "trust_id": "", - "auth_plugin": "", + "auth_type": "", } self._assert_password_auth(flag, kwargs) @@ -370,7 +370,7 @@ def test_only_username_flow(self): "password": "", "region_name": "", "trust_id": "", - "auth_plugin": "", + "auth_type": "", } self._assert_password_auth(flag, kwargs) @@ -390,7 +390,7 @@ def test_only_password_flow(self): "password": DEFAULT_PASSWORD, "region_name": "", "trust_id": "", - "auth_plugin": "", + "auth_type": "", } self._assert_password_auth(flag, kwargs) @@ -410,7 +410,7 @@ def test_only_region_name_flow(self): "password": "", "region_name": DEFAULT_REGION_NAME, "trust_id": "", - "auth_plugin": "", + "auth_type": "", } self._assert_password_auth(flag, kwargs) @@ -430,12 +430,12 @@ def test_only_trust_id_flow(self): "password": "", "region_name": "", "trust_id": "1234", - "auth_plugin": "", + "auth_type": "", } self._assert_password_auth(flag, kwargs) - def test_only_auth_plugin_flow(self): - flag = "--os-auth-plugin " + "v2password" + def test_only_auth_type_flow(self): + flag = "--os-auth-type " + "v2password" kwargs = { "auth_url": "", "project_id": "", @@ -450,7 +450,7 @@ def test_only_auth_plugin_flow(self): "password": "", "region_name": "", "trust_id": "", - "auth_plugin": DEFAULT_AUTH_PLUGIN + "auth_type": DEFAULT_AUTH_PLUGIN } self._assert_password_auth(flag, kwargs) From 8ba74451ee9efe21a0554c184f28e380fe714313 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 21 Oct 2014 09:51:47 -0500 Subject: [PATCH 0240/3494] Adjust some logging levels * Promote select messages to INFO so lower logging levels can be useful * Help more modules not say so much all the time Change-Id: I814023c1489595998ae74efe40ef439b3522ee74 --- openstackclient/common/clientmanager.py | 2 +- openstackclient/shell.py | 37 +++++++++++++++---------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index adec842f13..0396e83dd2 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -118,7 +118,7 @@ def __init__( root_logger = logging.getLogger('') LOG.setLevel(root_logger.getEffectiveLevel()) - LOG.debug('Using auth plugin: %s' % self.auth_plugin_name) + LOG.info('Using auth plugin: %s' % self.auth_plugin_name) self.auth = auth_plugin.load_from_options(**self._auth_params) # needed by SAML authentication request_session = requests.session() diff --git a/openstackclient/shell.py b/openstackclient/shell.py index e671ecc3dc..1198bae18a 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -137,18 +137,7 @@ def configure_logging(self): super(OpenStackShell, self).configure_logging() root_logger = logging.getLogger('') - # Requests logs some stuff at INFO that we don't want - # unless we have DEBUG - requests_log = logging.getLogger("requests") - requests_log.setLevel(logging.ERROR) - - # Other modules we don't want DEBUG output for so - # don't reset them below - iso8601_log = logging.getLogger("iso8601") - iso8601_log.setLevel(logging.ERROR) - # Set logging to the requested level - self.dump_stack_trace = False if self.options.verbose_level == 0: # --quiet root_logger.setLevel(logging.ERROR) @@ -161,11 +150,28 @@ def configure_logging(self): elif self.options.verbose_level >= 3: # Two or more --verbose root_logger.setLevel(logging.DEBUG) - requests_log.setLevel(logging.DEBUG) + + # Requests logs some stuff at INFO that we don't want + # unless we have DEBUG + requests_log = logging.getLogger("requests") + + # Other modules we don't want DEBUG output for + cliff_log = logging.getLogger('cliff') + stevedore_log = logging.getLogger('stevedore') + iso8601_log = logging.getLogger("iso8601") if self.options.debug: # --debug forces traceback self.dump_stack_trace = True + requests_log.setLevel(logging.DEBUG) + cliff_log.setLevel(logging.DEBUG) + else: + self.dump_stack_trace = False + requests_log.setLevel(logging.ERROR) + cliff_log.setLevel(logging.ERROR) + + stevedore_log.setLevel(logging.ERROR) + iso8601_log.setLevel(logging.ERROR) def run(self, argv): try: @@ -295,8 +301,11 @@ def initialize_app(self, argv): def prepare_to_run_command(self, cmd): """Set up auth and API versions""" - self.log.debug('prepare_to_run_command %s', cmd.__class__.__name__) - + self.log.info( + 'command: %s.%s', + cmd.__class__.__module__, + cmd.__class__.__name__, + ) if cmd.auth_required and cmd.best_effort: try: # Trigger the Identity client to initialize From 2c9d263611190996d64e35bc74a8575aeb25ed3e Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 20 Oct 2014 11:43:29 -0500 Subject: [PATCH 0241/3494] Fix server create for boot-from-volume * server create required --image even when booting the server from a volume. Change options to require either --image or --volume to specify the server boot disk. Using --volume currently uses device 'vda' for the block mapping and ignores any other block mappings given in --block-device-mapping. * server create and server show are both affected by bug 1378842 where an excepion was thrown when no image ID was present in the returned server object, which is the case for a server booted from a volume. * Fix the remaining assertEqual() order problems in test_server.py Closes-Bug: 1378842 Closes-Bug: 1383338 Change-Id: I5daebf4e50a765d4920088dfead95b6295af6a4d --- openstackclient/compute/v2/server.py | 51 +++++++++++++++---- .../tests/compute/v2/test_server.py | 22 ++++---- 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index a6d645b9e9..d58df86c10 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -67,9 +67,10 @@ def _prep_server_detail(compute_client, server): # Convert the image blob to a name image_info = info.get('image', {}) - image_id = image_info.get('id', '') - image = utils.find_resource(compute_client.images, image_id) - info['image'] = "%s (%s)" % (image.name, image_id) + if image_info: + image_id = image_info.get('id', '') + image = utils.find_resource(compute_client.images, image_id) + info['image'] = "%s (%s)" % (image.name, image_id) # Convert the flavor blob to a name flavor_info = info.get('flavor', {}) @@ -192,11 +193,17 @@ def get_parser(self, prog_name): 'server_name', metavar='', help=_('New server name')) - parser.add_argument( + disk_group = parser.add_mutually_exclusive_group( + required=True, + ) + disk_group.add_argument( '--image', metavar='', - required=True, help=_('Create server from this image')) + disk_group.add_argument( + '--volume', + metavar='', + help=_('Create server from this volume')) parser.add_argument( '--flavor', metavar='', @@ -282,10 +289,23 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute + volume_client = self.app.client_manager.volume # Lookup parsed_args.image - image = utils.find_resource(compute_client.images, - parsed_args.image) + image = None + if parsed_args.image: + image = utils.find_resource( + compute_client.images, + parsed_args.image, + ) + + # Lookup parsed_args.volume + volume = None + if parsed_args.volume: + volume = utils.find_resource( + volume_client.volumes, + parsed_args.volume, + ).id # Lookup parsed_args.flavor flavor = utils.find_resource(compute_client.flavors, @@ -319,8 +339,21 @@ def take_action(self, parsed_args): msg = "Can't open '%s': %s" raise exceptions.CommandError(msg % (parsed_args.user_data, e)) - block_device_mapping = dict(v.split('=', 1) - for v in parsed_args.block_device_mapping) + block_device_mapping = {} + if volume: + # When booting from volume, for now assume no other mappings + # This device value is likely KVM-specific + block_device_mapping = {'vda': volume} + else: + for dev_map in parsed_args.block_device_mapping: + dev_key, dev_vol = dev_map.split('=', 1) + block_volume = None + if dev_vol: + block_volume = utils.find_resource( + volume_client.volumes, + dev_vol, + ).id + block_device_mapping.update({dev_key: block_volume}) nics = [] for nic_str in parsed_args.nic: diff --git a/openstackclient/tests/compute/v2/test_server.py b/openstackclient/tests/compute/v2/test_server.py index 50de5c6ab7..43aa7a70e6 100644 --- a/openstackclient/tests/compute/v2/test_server.py +++ b/openstackclient/tests/compute/v2/test_server.py @@ -134,17 +134,16 @@ def test_server_create_minimal(self): **kwargs ) - collist = ('addresses', 'flavor', 'id', 'image', 'name', 'properties') - self.assertEqual(columns, collist) + collist = ('addresses', 'flavor', 'id', 'name', 'properties') + self.assertEqual(collist, columns) datalist = ( '', 'Large ()', compute_fakes.server_id, - 'graven ()', compute_fakes.server_name, '', ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) @mock.patch('openstackclient.compute.v2.server.io.open') def test_server_create_userdata(self, mock_open): @@ -200,17 +199,16 @@ def test_server_create_userdata(self, mock_open): **kwargs ) - collist = ('addresses', 'flavor', 'id', 'image', 'name', 'properties') - self.assertEqual(columns, collist) + collist = ('addresses', 'flavor', 'id', 'name', 'properties') + self.assertEqual(collist, columns) datalist = ( '', 'Large ()', compute_fakes.server_id, - 'graven ()', compute_fakes.server_name, '', ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) class TestServerDelete(TestServer): @@ -288,14 +286,14 @@ def test_server_image_create_no_options(self): ) collist = ('id', 'is_public', 'name', 'owner') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( image_fakes.image_id, False, image_fakes.image_name, image_fakes.image_owner, ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) def test_server_image_create_name(self): arglist = [ @@ -318,14 +316,14 @@ def test_server_image_create_name(self): ) collist = ('id', 'is_public', 'name', 'owner') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( image_fakes.image_id, False, image_fakes.image_name, image_fakes.image_owner, ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) class TestServerResize(TestServer): From 631ed3c8026e6b4539e7a8bf4adb8d2a7239b36a Mon Sep 17 00:00:00 2001 From: Matthieu Huin Date: Thu, 25 Sep 2014 18:15:59 +0200 Subject: [PATCH 0242/3494] Unscoped federated user-specific commands A federated user can authenticate with the v3unscopedsaml plugin and list the domains and projects she is allowed to scope to. This patch introduces the new commands 'federation domain list' and 'federation project list'. Note that for these commands -and plugin- to be available, the lxml library must be installed. Change-Id: I2707b624befcfb0a01b40a094e12fd68a3ee7773 Co-Authored-By: Florent Flament --- doc/source/man/openstack.rst | 3 + openstackclient/identity/v3/unscoped_saml.py | 79 +++++++++++ openstackclient/tests/fakes.py | 1 + openstackclient/tests/identity/v3/fakes.py | 17 +++ .../tests/identity/v3/test_unscoped_saml.py | 128 ++++++++++++++++++ setup.cfg | 3 + 6 files changed, 231 insertions(+) create mode 100644 openstackclient/identity/v3/unscoped_saml.py create mode 100644 openstackclient/tests/identity/v3/test_unscoped_saml.py diff --git a/doc/source/man/openstack.rst b/doc/source/man/openstack.rst index de2bbe92fa..4a9df34a39 100644 --- a/doc/source/man/openstack.rst +++ b/doc/source/man/openstack.rst @@ -47,6 +47,9 @@ Please bear in mind that some plugins might not support all of the functionaliti Additionally, it is possible to use Keystone's service token to authenticate, by setting the options :option:`--os-token` and :option:`--os-url` (or the environment variables :envvar:`OS_TOKEN` and :envvar:`OS_URL` respectively). This method takes precedence over authentication plugins. +.. NOTE:: + To use the ``v3unscopedsaml`` method, the lxml package will need to be installed. + OPTIONS ======= diff --git a/openstackclient/identity/v3/unscoped_saml.py b/openstackclient/identity/v3/unscoped_saml.py new file mode 100644 index 0000000000..affbaf3a87 --- /dev/null +++ b/openstackclient/identity/v3/unscoped_saml.py @@ -0,0 +1,79 @@ +# 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. +# + +"""Identity v3 unscoped SAML auth action implementations. + +The first step of federated auth is to fetch an unscoped token. From there, +the user can list domains and projects they are allowed to access, and request +a scoped token.""" + +import logging + +from cliff import lister + +from openstackclient.common import exceptions +from openstackclient.common import utils + + +UNSCOPED_AUTH_PLUGINS = ['v3unscopedsaml', 'v3unscopedadfs'] + + +def auth_with_unscoped_saml(func): + """Check the unscoped federated context""" + def _decorated(self, parsed_args): + auth_plugin_name = self.app.client_manager.auth_plugin_name + if auth_plugin_name in UNSCOPED_AUTH_PLUGINS: + return func(self, parsed_args) + else: + msg = ('This command requires the use of an unscoped SAML ' + 'authentication plugin. Please use argument ' + '--os-auth-plugin with one of the following ' + 'plugins: ' + ', '.join(UNSCOPED_AUTH_PLUGINS)) + raise exceptions.CommandError(msg) + return _decorated + + +class ListAccessibleDomains(lister.Lister): + """List accessible domains""" + + log = logging.getLogger(__name__ + '.ListAccessibleDomains') + + @auth_with_unscoped_saml + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + columns = ('ID', 'Enabled', 'Name', 'Description') + identity_client = self.app.client_manager.identity + data = identity_client.federation.domains.list() + return (columns, + (utils.get_item_properties( + s, columns, + formatters={}, + ) for s in data)) + + +class ListAccessibleProjects(lister.Lister): + """List accessible projects""" + + log = logging.getLogger(__name__ + '.ListAccessibleProjects') + + @auth_with_unscoped_saml + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + columns = ('ID', 'Domain ID', 'Enabled', 'Name') + identity_client = self.app.client_manager.identity + data = identity_client.federation.projects.list() + return (columns, + (utils.get_item_properties( + s, columns, + formatters={}, + ) for s in data)) diff --git a/openstackclient/tests/fakes.py b/openstackclient/tests/fakes.py index f8b7bb6f39..abad4cffe6 100644 --- a/openstackclient/tests/fakes.py +++ b/openstackclient/tests/fakes.py @@ -199,6 +199,7 @@ def __init__(self): self.network = None self.session = None self.auth_ref = None + self.auth_plugin_name = None class FakeModule(object): diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index 5844d160b9..b195ed78b7 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -285,6 +285,19 @@ } +class FakeAuth(object): + def __init__(self, auth_method_class=None): + self._auth_method_class = auth_method_class + + def get_token(self, *args, **kwargs): + return token_id + + +class FakeSession(object): + def __init__(self, **kwargs): + self.auth = FakeAuth() + + class FakeIdentityv3Client(object): def __init__(self, **kwargs): self.domains = mock.Mock() @@ -320,6 +333,10 @@ def __init__(self, **kwargs): self.mappings.resource_class = fakes.FakeResource(None, {}) self.protocols = mock.Mock() self.protocols.resource_class = fakes.FakeResource(None, {}) + self.projects = mock.Mock() + self.projects.resource_class = fakes.FakeResource(None, {}) + self.domains = mock.Mock() + self.domains.resource_class = fakes.FakeResource(None, {}) class FakeFederatedClient(FakeIdentityv3Client): diff --git a/openstackclient/tests/identity/v3/test_unscoped_saml.py b/openstackclient/tests/identity/v3/test_unscoped_saml.py new file mode 100644 index 0000000000..6b2d3f5b1c --- /dev/null +++ b/openstackclient/tests/identity/v3/test_unscoped_saml.py @@ -0,0 +1,128 @@ +# 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 copy + +from openstackclient.common import exceptions +from openstackclient.identity.v3 import unscoped_saml +from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes + + +class TestUnscopedSAML(identity_fakes.TestFederatedIdentity): + + def setUp(self): + super(TestUnscopedSAML, self).setUp() + + federation_lib = self.app.client_manager.identity.federation + self.projects_mock = federation_lib.projects + self.projects_mock.reset_mock() + self.domains_mock = federation_lib.domains + self.domains_mock.reset_mock() + + +class TestProjectList(TestUnscopedSAML): + + def setUp(self): + super(TestProjectList, self).setUp() + + self.projects_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = unscoped_saml.ListAccessibleProjects(self.app, None) + + def test_accessible_projects_list(self): + self.app.client_manager.auth_plugin_name = 'v3unscopedsaml' + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.projects_mock.list.assert_called_with() + + collist = ('ID', 'Domain ID', 'Enabled', 'Name') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.project_id, + identity_fakes.domain_id, + True, + identity_fakes.project_name, + ), ) + self.assertEqual(tuple(data), datalist) + + def test_accessible_projects_list_wrong_auth(self): + auth = identity_fakes.FakeAuth("wrong auth") + self.app.client_manager.identity.session.auth = auth + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + +class TestDomainList(TestUnscopedSAML): + + def setUp(self): + super(TestDomainList, self).setUp() + + self.domains_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.DOMAIN), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = unscoped_saml.ListAccessibleDomains(self.app, None) + + def test_accessible_domains_list(self): + self.app.client_manager.auth_plugin_name = 'v3unscopedsaml' + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + self.domains_mock.list.assert_called_with() + + collist = ('ID', 'Enabled', 'Name', 'Description') + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.domain_id, + True, + identity_fakes.domain_name, + identity_fakes.domain_description, + ), ) + self.assertEqual(tuple(data), datalist) + + def test_accessible_domains_list_wrong_auth(self): + auth = identity_fakes.FakeAuth("wrong auth") + self.app.client_manager.identity.session.auth = auth + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, + parsed_args) diff --git a/setup.cfg b/setup.cfg index af601649de..c0519d11d6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -240,6 +240,9 @@ openstack.identity.v3 = federation_protocol_set = openstackclient.identity.v3.federation_protocol:SetProtocol federation_protocol_show = openstackclient.identity.v3.federation_protocol:ShowProtocol + federation_domain_list = openstackclient.identity.v3.unscoped_saml:ListAccessibleDomains + federation_project_list = openstackclient.identity.v3.unscoped_saml:ListAccessibleProjects + request_token_authorize = openstackclient.identity.v3.token:AuthorizeRequestToken request_token_create = openstackclient.identity.v3.token:CreateRequestToken From b19379363691e65a83f3109ba501d0262cad37f5 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Fri, 31 Oct 2014 15:15:54 +0100 Subject: [PATCH 0243/3494] Use fixtures from keystoneclient for static data We should use the fixture generation code from keystoneclient rather than keep our own copies of the token and discovery structure. Change-Id: I53c1d2935d1d65c39b8abea89427af2fc3edd181 --- openstackclient/tests/fakes.py | 145 +++------------------------------ 1 file changed, 11 insertions(+), 134 deletions(-) diff --git a/openstackclient/tests/fakes.py b/openstackclient/tests/fakes.py index f8b7bb6f39..e32181f1f9 100644 --- a/openstackclient/tests/fakes.py +++ b/openstackclient/tests/fakes.py @@ -17,6 +17,7 @@ import six import sys +from keystoneclient import fixture import requests @@ -24,140 +25,16 @@ AUTH_URL = "http://0.0.0.0" USERNAME = "itchy" PASSWORD = "scratchy" -TEST_RESPONSE_DICT = { - "access": { - "metadata": { - "is_admin": 0, - "roles": [ - "1234", - ] - }, - "serviceCatalog": [ - { - "endpoints": [ - { - "adminURL": AUTH_URL + "/v2.0", - "id": "1234", - "internalURL": AUTH_URL + "/v2.0", - "publicURL": AUTH_URL + "/v2.0", - "region": "RegionOne" - } - ], - "endpoints_links": [], - "name": "keystone", - "type": "identity" - } - ], - "token": { - "expires": "2035-01-01T00:00:01Z", - "id": AUTH_TOKEN, - "issued_at": "2013-01-01T00:00:01.692048", - "tenant": { - "description": None, - "enabled": True, - "id": "1234", - "name": "testtenant" - } - }, - "user": { - "id": "5678", - "name": USERNAME, - "roles": [ - { - "name": "testrole" - }, - ], - "roles_links": [], - "username": USERNAME - } - } -} -TEST_RESPONSE_DICT_V3 = { - "token": { - "audit_ids": [ - "a" - ], - "catalog": [ - ], - "expires_at": "2034-09-29T18:27:15.978064Z", - "extras": {}, - "issued_at": "2014-09-29T17:27:15.978097Z", - "methods": [ - "password" - ], - "project": { - "domain": { - "id": "default", - "name": "Default" - }, - "id": "bbb", - "name": "project" - }, - "roles": [ - ], - "user": { - "domain": { - "id": "default", - "name": "Default" - }, - "id": "aaa", - "name": USERNAME - } - } -} -TEST_VERSIONS = { - "versions": { - "values": [ - { - "id": "v3.0", - "links": [ - { - "href": AUTH_URL, - "rel": "self" - } - ], - "media-types": [ - { - "base": "application/json", - "type": "application/vnd.openstack.identity-v3+json" - }, - { - "base": "application/xml", - "type": "application/vnd.openstack.identity-v3+xml" - } - ], - "status": "stable", - "updated": "2013-03-06T00:00:00Z" - }, - { - "id": "v2.0", - "links": [ - { - "href": AUTH_URL, - "rel": "self" - }, - { - "href": "http://docs.openstack.org/", - "rel": "describedby", - "type": "text/html" - } - ], - "media-types": [ - { - "base": "application/json", - "type": "application/vnd.openstack.identity-v2.0+json" - }, - { - "base": "application/xml", - "type": "application/vnd.openstack.identity-v2.0+xml" - } - ], - "status": "stable", - "updated": "2014-04-17T00:00:00Z" - } - ] - } -} + +TEST_RESPONSE_DICT = fixture.V2Token(token_id=AUTH_TOKEN, + user_name=USERNAME) +_s = TEST_RESPONSE_DICT.add_service('identity', name='keystone') +_s.add_endpoint(AUTH_URL + '/v2.0') + +TEST_RESPONSE_DICT_V3 = fixture.V3Token(user_name=USERNAME) +TEST_RESPONSE_DICT_V3.set_project_scope() + +TEST_VERSIONS = fixture.DiscoveryList(href=AUTH_URL) class FakeStdout: From 59735bf10da67b1611ae6e6b34f282512b8b63bf Mon Sep 17 00:00:00 2001 From: Colleen Murphy Date: Fri, 7 Nov 2014 15:15:43 +0100 Subject: [PATCH 0244/3494] Add cliff-tablib to requirements cliff-tablib gives cliff the ability to format list and show output in html, json, or yaml (http://cliff-tablib.readthedocs.org/). This patch adds cliff-tablib to requirements.txt so that it can be installed along with cliff. Change-Id: I4daab97642482e6f40cd8209ff5edd9c680092c0 --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 54ea795079..287935a705 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ six>=1.7.0 Babel>=1.3 cliff>=1.7.0 # Apache-2.0 +cliff-tablib>=1.0 oslo.i18n>=1.0.0 # Apache-2.0 oslo.utils>=1.0.0 # Apache-2.0 oslo.serialization>=1.0.0 # Apache-2.0 From 42d0b20ebc5fd7393d250fb72d30d5ce630dc54f Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 7 Nov 2014 04:44:54 -0600 Subject: [PATCH 0245/3494] Add --or-show option to user create The --or-show option is added to create commands for the common case of needing to ensure an object exists and getting its properties if it does or creating a new one if it does not exist. Note that if the object exists, any additional options that would set values in a newly created object are ignored if the object exists. FakeResource needs the __name__ attribute to fall through utils.find_resource. Prove the concept on v2 user create then propogate once we're happy with it... Change-Id: I6268566514840c284e6a1d44b409a81d6699ef99 --- openstackclient/identity/v2_0/user.py | 30 +++++-- openstackclient/tests/fakes.py | 1 + .../tests/identity/v2_0/test_user.py | 79 +++++++++++++++++++ 3 files changed, 103 insertions(+), 7 deletions(-) diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index 729c6ac81f..2ebcba188e 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -71,6 +71,11 @@ def get_parser(self, prog_name): action='store_true', help=_('Disable user'), ) + parser.add_argument( + '--or-show', + action='store_true', + help=_('Return existing user'), + ) return parser def take_action(self, parsed_args): @@ -91,13 +96,24 @@ def take_action(self, parsed_args): if parsed_args.password_prompt: parsed_args.password = utils.get_password(self.app.stdin) - user = identity_client.users.create( - parsed_args.name, - parsed_args.password, - parsed_args.email, - tenant_id=project_id, - enabled=enabled, - ) + try: + user = identity_client.users.create( + parsed_args.name, + parsed_args.password, + parsed_args.email, + tenant_id=project_id, + enabled=enabled, + ) + except ksc_exc.Conflict as e: + if parsed_args.or_show: + user = utils.find_resource( + identity_client.users, + parsed_args.name, + ) + self.log.info('Returning existing user %s', user.name) + else: + raise e + # NOTE(dtroyer): The users.create() method wants 'tenant_id' but # the returned resource has 'tenantId'. Sigh. # We're using project_id now inside OSC so there. diff --git a/openstackclient/tests/fakes.py b/openstackclient/tests/fakes.py index abad4cffe6..3c0b060d17 100644 --- a/openstackclient/tests/fakes.py +++ b/openstackclient/tests/fakes.py @@ -210,6 +210,7 @@ def __init__(self, name, version): class FakeResource(object): def __init__(self, manager, info, loaded=False): + self.__name__ = type(self).__name__ self.manager = manager self._info = info self._add_details(info) diff --git a/openstackclient/tests/identity/v2_0/test_user.py b/openstackclient/tests/identity/v2_0/test_user.py index e191431c94..f0ff41c8db 100644 --- a/openstackclient/tests/identity/v2_0/test_user.py +++ b/openstackclient/tests/identity/v2_0/test_user.py @@ -16,6 +16,7 @@ import copy import mock +from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc from openstackclient.identity.v2_0 import user from openstackclient.tests import fakes from openstackclient.tests.identity.v2_0 import fakes as identity_fakes @@ -342,6 +343,84 @@ def test_user_create_disable(self): ) self.assertEqual(data, datalist) + def test_user_create_or_show_exists(self): + def _raise_conflict(*args, **kwargs): + raise ksc_exc.Conflict(None) + + # need to make this throw an exception... + self.users_mock.create.side_effect = _raise_conflict + + self.users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + + arglist = [ + '--or-show', + identity_fakes.user_name, + ] + verifylist = [ + ('name', identity_fakes.user_name), + ('or_show', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # UserManager.create(name, password, email, tenant_id=, enabled=) + self.users_mock.get.assert_called_with(identity_fakes.user_name) + + collist = ('email', 'enabled', 'id', 'name', 'project_id') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.user_email, + True, + identity_fakes.user_id, + identity_fakes.user_name, + identity_fakes.project_id, + ) + self.assertEqual(datalist, data) + + def test_user_create_or_show_not_exists(self): + arglist = [ + '--or-show', + identity_fakes.user_name, + ] + verifylist = [ + ('name', identity_fakes.user_name), + ('or_show', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'enabled': True, + 'tenant_id': None, + } + # UserManager.create(name, password, email, tenant_id=, enabled=) + self.users_mock.create.assert_called_with( + identity_fakes.user_name, + None, + None, + **kwargs + ) + + collist = ('email', 'enabled', 'id', 'name', 'project_id') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.user_email, + True, + identity_fakes.user_id, + identity_fakes.user_name, + identity_fakes.project_id, + ) + self.assertEqual(datalist, data) + class TestUserDelete(TestUser): From 46f6df5f23164836cc6a3601aa9d5b018192209f Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 7 Nov 2014 15:04:46 -0600 Subject: [PATCH 0246/3494] Swap remaining assertEqual arguments Change-Id: I1abdebb298b93074657a7ba65a7186d814969780 --- .../tests/identity/v2_0/test_user.py | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/openstackclient/tests/identity/v2_0/test_user.py b/openstackclient/tests/identity/v2_0/test_user.py index f0ff41c8db..a025dd7d1a 100644 --- a/openstackclient/tests/identity/v2_0/test_user.py +++ b/openstackclient/tests/identity/v2_0/test_user.py @@ -84,7 +84,7 @@ def test_user_create_no_options(self): ) collist = ('email', 'enabled', 'id', 'name', 'project_id') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( identity_fakes.user_email, True, @@ -92,7 +92,7 @@ def test_user_create_no_options(self): identity_fakes.user_name, identity_fakes.project_id, ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) def test_user_create_password(self): arglist = [ @@ -123,7 +123,7 @@ def test_user_create_password(self): ) collist = ('email', 'enabled', 'id', 'name', 'project_id') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( identity_fakes.user_email, True, @@ -131,7 +131,7 @@ def test_user_create_password(self): identity_fakes.user_name, identity_fakes.project_id, ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) def test_user_create_password_prompt(self): arglist = [ @@ -164,7 +164,7 @@ def test_user_create_password_prompt(self): ) collist = ('email', 'enabled', 'id', 'name', 'project_id') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( identity_fakes.user_email, True, @@ -172,7 +172,7 @@ def test_user_create_password_prompt(self): identity_fakes.user_name, identity_fakes.project_id, ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) def test_user_create_email(self): arglist = [ @@ -202,7 +202,7 @@ def test_user_create_email(self): ) collist = ('email', 'enabled', 'id', 'name', 'project_id') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( identity_fakes.user_email, True, @@ -210,7 +210,7 @@ def test_user_create_email(self): identity_fakes.user_name, identity_fakes.project_id, ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) def test_user_create_project(self): # Return the new project @@ -255,7 +255,7 @@ def test_user_create_project(self): ) collist = ('email', 'enabled', 'id', 'name', 'project_id') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( identity_fakes.user_email, True, @@ -263,7 +263,7 @@ def test_user_create_project(self): identity_fakes.user_name, identity_fakes.PROJECT_2['id'], ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) def test_user_create_enable(self): arglist = [ @@ -294,7 +294,7 @@ def test_user_create_enable(self): ) collist = ('email', 'enabled', 'id', 'name', 'project_id') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( identity_fakes.user_email, True, @@ -302,7 +302,7 @@ def test_user_create_enable(self): identity_fakes.user_name, identity_fakes.project_id, ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) def test_user_create_disable(self): arglist = [ @@ -333,7 +333,7 @@ def test_user_create_disable(self): ) collist = ('email', 'enabled', 'id', 'name', 'project_id') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( identity_fakes.user_email, True, @@ -341,7 +341,7 @@ def test_user_create_disable(self): identity_fakes.user_name, identity_fakes.project_id, ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) def test_user_create_or_show_exists(self): def _raise_conflict(*args, **kwargs): @@ -495,12 +495,12 @@ def test_user_list_no_options(self): self.users_mock.list.assert_called_with(tenant_id=None) collist = ('ID', 'Name') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = (( identity_fakes.user_id, identity_fakes.user_name, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_user_list_project(self): arglist = [ @@ -518,12 +518,12 @@ def test_user_list_project(self): self.users_mock.list.assert_called_with(tenant_id=project_id) collist = ('ID', 'Name') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = (( identity_fakes.user_id, identity_fakes.user_name, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) def test_user_list_long(self): arglist = [ @@ -540,7 +540,7 @@ def test_user_list_long(self): self.users_mock.list.assert_called_with(tenant_id=None) collist = ('ID', 'Name', 'Project', 'Email', 'Enabled') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = (( identity_fakes.user_id, identity_fakes.user_name, @@ -548,7 +548,7 @@ def test_user_list_long(self): identity_fakes.user_email, True, ), ) - self.assertEqual(tuple(data), datalist) + self.assertEqual(datalist, tuple(data)) class TestUserSet(TestUser): @@ -816,7 +816,7 @@ def test_user_show(self): self.users_mock.get.assert_called_with(identity_fakes.user_id) collist = ('email', 'enabled', 'id', 'name', 'project_id') - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( identity_fakes.user_email, True, @@ -824,4 +824,4 @@ def test_user_show(self): identity_fakes.user_name, identity_fakes.project_id, ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) From 951ca3a6f38655dfb169878160d57fe5bf062f4a Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Sat, 8 Nov 2014 14:25:05 +0000 Subject: [PATCH 0247/3494] Updated from global requirements Change-Id: I778a0c00da51cdc52cd67d1b273d52e84d68992b --- requirements.txt | 2 +- test-requirements.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 287935a705..82be8e39ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,4 +16,4 @@ python-novaclient>=2.18.0 python-cinderclient>=1.1.0 python-neutronclient>=2.3.6,<3 requests>=2.2.0,!=2.4.0 -stevedore>=1.0.0 # Apache-2.0 +stevedore>=1.1.0 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index 259ace266c..de5732cf24 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,8 +9,8 @@ fixtures>=0.3.14 mock>=1.0 oslosphinx>=2.2.0 # Apache-2.0 oslotest>=1.2.0 # Apache-2.0 -requests-mock>=0.4.0 # Apache-2.0 +requests-mock>=0.5.1 # Apache-2.0 sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 testrepository>=0.0.18 -testtools>=0.9.34 +testtools>=0.9.36 WebOb>=1.2.3 From ab89ef5876406c376621be178dd6407b325c435b Mon Sep 17 00:00:00 2001 From: Oleksii Chuprykov Date: Wed, 12 Nov 2014 15:51:55 +0200 Subject: [PATCH 0248/3494] Tests work fine with random PYTHONHASHSEED Change-Id: Iba6fc87bbff289ae2572a7eb132f5c946dfa0956 Related-Bug: #1348818 --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 02dac23a7e..6e03d48640 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,6 @@ skipdist = True usedevelop = True install_command = pip install -U {opts} {packages} setenv = VIRTUAL_ENV={envdir} - PYTHONHASHSEED=0 deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = python setup.py testr --testr-args='{posargs}' From 27b0ff5cdaf5e446d60ae6a6201a7ce0132a13a4 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Wed, 12 Nov 2014 12:07:43 -0500 Subject: [PATCH 0249/3494] cleanup files that are created for swift functional tests Currently this portion of code is also being run when running tox to debug local tests. Which is very annoying since a developer will end up with a bunch of uuid files. Rather than creating it once per run, we can have a setup/teardown that is handled safely. Change-Id: I49a0bb3d14f24c54da93458d1e3b9093a1120453 --- functional/tests/test_object.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/functional/tests/test_object.py b/functional/tests/test_object.py index 396f0cb7ac..4121524ace 100644 --- a/functional/tests/test_object.py +++ b/functional/tests/test_object.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import os import uuid from functional.common import test @@ -25,9 +26,11 @@ class ObjectV1Tests(test.TestCase): CONTAINER_NAME = uuid.uuid4().hex OBJECT_NAME = uuid.uuid4().hex - # NOTE(stevemar): Not using setUp since we only want this to run once - with open(OBJECT_NAME, 'w') as f: - f.write('test content') + def setUp(self): + super(ObjectV1Tests, self).setUp() + self.addCleanup(os.remove, self.OBJECT_NAME) + with open(self.OBJECT_NAME, 'w') as f: + f.write('test content') def test_container_create(self): raw_output = self.openstack('container create ' + self.CONTAINER_NAME) From 070fa5091d43ee8c1f8f23b83ba36ca9d960f617 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 13 Nov 2014 16:04:49 -0500 Subject: [PATCH 0250/3494] Remove links from federation related commands in identity v3 We should remove the 'links' portion from the returned object for the following commands: * create/show federation protocol * create/show mapping * create/show identity provider Change-Id: I55654cce1f89de8e532f9acd8092257be33efd85 --- openstackclient/identity/v3/federation_protocol.py | 2 ++ openstackclient/identity/v3/identity_provider.py | 11 +++++------ openstackclient/identity/v3/mapping.py | 10 ++++------ 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/openstackclient/identity/v3/federation_protocol.py b/openstackclient/identity/v3/federation_protocol.py index adc4a28b14..693ec94eae 100644 --- a/openstackclient/identity/v3/federation_protocol.py +++ b/openstackclient/identity/v3/federation_protocol.py @@ -61,6 +61,7 @@ def take_action(self, parsed_args): # user. info['identity_provider'] = parsed_args.identity_provider info['mapping'] = info.pop('mapping_id') + info.pop('links', None) return zip(*sorted(six.iteritems(info))) @@ -179,4 +180,5 @@ def take_action(self, parsed_args): parsed_args.identity_provider, parsed_args.federation_protocol) info = dict(protocol._info) info['mapping'] = info.pop('mapping_id') + info.pop('links', None) return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/identity/v3/identity_provider.py b/openstackclient/identity/v3/identity_provider.py index 5e8ee56689..8a1b22d0cf 100644 --- a/openstackclient/identity/v3/identity_provider.py +++ b/openstackclient/identity/v3/identity_provider.py @@ -65,9 +65,9 @@ def take_action(self, parsed_args): id=parsed_args.identity_provider_id, description=parsed_args.description, enabled=parsed_args.enabled) - info = {} - info.update(idp._info) - return zip(*sorted(six.iteritems(info))) + + idp._info.pop('links', None) + return zip(*sorted(six.iteritems(idp._info))) class DeleteIdentityProvider(command.Command): @@ -176,6 +176,5 @@ def take_action(self, parsed_args): identity_client.federation.identity_providers, parsed_args.identity_provider) - info = {} - info.update(identity_provider._info) - return zip(*sorted(six.iteritems(info))) + identity_provider._info.pop('links', None) + return zip(*sorted(six.iteritems(identity_provider._info))) diff --git a/openstackclient/identity/v3/mapping.py b/openstackclient/identity/v3/mapping.py index ae5e03bd2d..c530a40439 100644 --- a/openstackclient/identity/v3/mapping.py +++ b/openstackclient/identity/v3/mapping.py @@ -107,9 +107,8 @@ def take_action(self, parsed_args): mapping_id=parsed_args.mapping, rules=rules) - info = {} - info.update(mapping._info) - return zip(*sorted(six.iteritems(info))) + mapping._info.pop('links', None) + return zip(*sorted(six.iteritems(mapping._info))) class DeleteMapping(command.Command): @@ -204,6 +203,5 @@ def take_action(self, parsed_args): mapping = identity_client.federation.mappings.get(parsed_args.mapping) - info = {} - info.update(mapping._info) - return zip(*sorted(six.iteritems(info))) + mapping._info.pop('links', None) + return zip(*sorted(six.iteritems(mapping._info))) From 3e97e1775dccd12757588e667ba511b464f84234 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 13 Nov 2014 16:48:59 -0500 Subject: [PATCH 0251/3494] Remove links from oauth consumers This should be the last of the v3 identity objects that return a links section upon create or show. Change-Id: I45a3b43c303bfed73950095bec8860cbea7a559c --- openstackclient/identity/v3/consumer.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openstackclient/identity/v3/consumer.py b/openstackclient/identity/v3/consumer.py index 7f54603540..b7e57d8dd0 100644 --- a/openstackclient/identity/v3/consumer.py +++ b/openstackclient/identity/v3/consumer.py @@ -46,9 +46,8 @@ def take_action(self, parsed_args): consumer = identity_client.oauth1.consumers.create( parsed_args.description ) - info = {} - info.update(consumer._info) - return zip(*sorted(six.iteritems(info))) + consumer._info.pop('links', None) + return zip(*sorted(six.iteritems(consumer._info))) class DeleteConsumer(command.Command): @@ -147,6 +146,5 @@ def take_action(self, parsed_args): consumer = utils.find_resource( identity_client.oauth1.consumers, parsed_args.consumer) - info = {} - info.update(consumer._info) - return zip(*sorted(six.iteritems(info))) + consumer._info.pop('links', None) + return zip(*sorted(six.iteritems(consumer._info))) From 7242113a8f8bcb8c227b259376e34e28ef6c2b52 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Wed, 12 Nov 2014 10:48:47 -0500 Subject: [PATCH 0252/3494] Add additional support for --or-show Add --or-show for the following: * v2 roles * v2 projects Change-Id: Ibbae19cda668575b9527fbd259f1298c48b8265b --- openstackclient/identity/v2_0/project.py | 27 ++++-- openstackclient/identity/v2_0/role.py | 18 +++- .../tests/identity/v2_0/test_project.py | 85 +++++++++++++++++++ .../tests/identity/v2_0/test_role.py | 71 ++++++++++++++++ 4 files changed, 194 insertions(+), 7 deletions(-) diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index 7ead0890d4..2d66b400d4 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -63,6 +63,11 @@ def get_parser(self, prog_name): help=_('Property to add for this project ' '(repeat option to set multiple properties)'), ) + parser.add_argument( + '--or-show', + action='store_true', + help=_('Return existing project'), + ) return parser def take_action(self, parsed_args): @@ -76,12 +81,22 @@ def take_action(self, parsed_args): if parsed_args.property: kwargs = parsed_args.property.copy() - project = identity_client.tenants.create( - parsed_args.name, - description=parsed_args.description, - enabled=enabled, - **kwargs - ) + try: + project = identity_client.tenants.create( + parsed_args.name, + description=parsed_args.description, + enabled=enabled, + **kwargs + ) + except ksc_exc.Conflict as e: + if parsed_args.or_show: + project = utils.find_resource( + identity_client.tenants, + parsed_args.name, + ) + self.log.info('Returning existing project %s', project.name) + else: + raise e info = {} info.update(project._info) diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index 4c45004dae..df69e857ad 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -21,6 +21,7 @@ from cliff import command from cliff import lister from cliff import show +from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc from openstackclient.common import exceptions from openstackclient.common import utils @@ -81,12 +82,27 @@ def get_parser(self, prog_name): 'role_name', metavar='', help=_('New role name')) + parser.add_argument( + '--or-show', + action='store_true', + help=_('Return existing role'), + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - role = identity_client.roles.create(parsed_args.role_name) + try: + role = identity_client.roles.create(parsed_args.role_name) + except ksc_exc.Conflict as e: + if parsed_args.or_show: + role = utils.find_resource( + identity_client.roles, + parsed_args.role_name, + ) + self.log.info('Returning existing role %s', role.name) + else: + raise e info = {} info.update(role._info) diff --git a/openstackclient/tests/identity/v2_0/test_project.py b/openstackclient/tests/identity/v2_0/test_project.py index d046cd4796..833b5d84d8 100644 --- a/openstackclient/tests/identity/v2_0/test_project.py +++ b/openstackclient/tests/identity/v2_0/test_project.py @@ -15,6 +15,8 @@ import copy +from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc + from openstackclient.identity.v2_0 import project from openstackclient.tests import fakes from openstackclient.tests.identity.v2_0 import fakes as identity_fakes @@ -219,6 +221,89 @@ def test_project_create_property(self): ) self.assertEqual(data, datalist) + def test_project_create_or_show_exists(self): + def _raise_conflict(*args, **kwargs): + raise ksc_exc.Conflict(None) + + # need to make this throw an exception... + self.projects_mock.create.side_effect = _raise_conflict + + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + + arglist = [ + '--or-show', + identity_fakes.project_name, + ] + verifylist = [ + ('name', identity_fakes.project_name), + ('or_show', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # ProjectManager.create(name, description, enabled) + self.projects_mock.get.assert_called_with(identity_fakes.project_name) + + # Set expected values + kwargs = { + 'description': None, + 'enabled': True, + } + self.projects_mock.create.assert_called_with( + identity_fakes.project_name, + **kwargs + ) + + collist = ('description', 'enabled', 'id', 'name') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.project_description, + True, + identity_fakes.project_id, + identity_fakes.project_name, + ) + self.assertEqual(datalist, data) + + def test_project_create_or_show_not_exists(self): + arglist = [ + '--or-show', + identity_fakes.project_name, + ] + verifylist = [ + ('name', identity_fakes.project_name), + ('or_show', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'description': None, + 'enabled': True, + } + self.projects_mock.create.assert_called_with( + identity_fakes.project_name, + **kwargs + ) + + collist = ('description', 'enabled', 'id', 'name') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.project_description, + True, + identity_fakes.project_id, + identity_fakes.project_name, + ) + self.assertEqual(datalist, data) + class TestProjectDelete(TestProject): diff --git a/openstackclient/tests/identity/v2_0/test_role.py b/openstackclient/tests/identity/v2_0/test_role.py index d515bd7cdb..67467747f7 100644 --- a/openstackclient/tests/identity/v2_0/test_role.py +++ b/openstackclient/tests/identity/v2_0/test_role.py @@ -16,6 +16,8 @@ import copy import mock +from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc + from openstackclient.common import exceptions from openstackclient.identity.v2_0 import role from openstackclient.tests import fakes @@ -142,6 +144,75 @@ def test_role_create_no_options(self): ) self.assertEqual(data, datalist) + def test_role_create_or_show_exists(self): + def _raise_conflict(*args, **kwargs): + raise ksc_exc.Conflict(None) + + # need to make this throw an exception... + self.roles_mock.create.side_effect = _raise_conflict + + self.roles_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE), + loaded=True, + ) + + arglist = [ + '--or-show', + identity_fakes.role_name, + ] + verifylist = [ + ('role_name', identity_fakes.role_name), + ('or_show', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # RoleManager.get(name, description, enabled) + self.roles_mock.get.assert_called_with(identity_fakes.role_name) + + # RoleManager.create(name) + self.roles_mock.create.assert_called_with( + identity_fakes.role_name, + ) + + collist = ('id', 'name') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.role_id, + identity_fakes.role_name, + ) + self.assertEqual(datalist, data) + + def test_role_create_or_show_not_exists(self): + arglist = [ + '--or-show', + identity_fakes.role_name, + ] + verifylist = [ + ('role_name', identity_fakes.role_name), + ('or_show', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # RoleManager.create(name) + self.roles_mock.create.assert_called_with( + identity_fakes.role_name, + ) + + collist = ('id', 'name') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.role_id, + identity_fakes.role_name, + ) + self.assertEqual(datalist, data) + class TestRoleDelete(TestRole): From 936722d59f4e28e4f6c578074eeb75152458d821 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Sun, 11 May 2014 13:26:35 -0500 Subject: [PATCH 0253/3494] Add arg to 'server image create' tests The 'protected' column was not being checked. Also add it to image.fakes.IMAGE. Change-Id: Ie431e9871a7da78b5a3924bfbc51d5575d994d86 --- openstackclient/tests/compute/v2/test_server.py | 10 ++++++---- openstackclient/tests/image/v2/fakes.py | 5 ++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/openstackclient/tests/compute/v2/test_server.py b/openstackclient/tests/compute/v2/test_server.py index 43aa7a70e6..19c13440db 100644 --- a/openstackclient/tests/compute/v2/test_server.py +++ b/openstackclient/tests/compute/v2/test_server.py @@ -285,13 +285,14 @@ def test_server_image_create_no_options(self): compute_fakes.server_name, ) - collist = ('id', 'is_public', 'name', 'owner') + collist = ('id', 'is_public', 'name', 'owner', 'protected') self.assertEqual(collist, columns) datalist = ( image_fakes.image_id, - False, + image_fakes.image_public, image_fakes.image_name, image_fakes.image_owner, + image_fakes.image_protected, ) self.assertEqual(datalist, data) @@ -315,13 +316,14 @@ def test_server_image_create_name(self): 'img-nam', ) - collist = ('id', 'is_public', 'name', 'owner') + collist = ('id', 'is_public', 'name', 'owner', 'protected') self.assertEqual(collist, columns) datalist = ( image_fakes.image_id, - False, + image_fakes.image_public, image_fakes.image_name, image_fakes.image_owner, + image_fakes.image_protected, ) self.assertEqual(datalist, data) diff --git a/openstackclient/tests/image/v2/fakes.py b/openstackclient/tests/image/v2/fakes.py index 96255cd448..1b7edf08bf 100644 --- a/openstackclient/tests/image/v2/fakes.py +++ b/openstackclient/tests/image/v2/fakes.py @@ -22,12 +22,15 @@ image_id = 'im1' image_name = 'graven' image_owner = 'baal' +image_public = False +image_protected = False IMAGE = { 'id': image_id, 'name': image_name, - 'is_public': False, + 'is_public': image_public, 'owner': image_owner, + 'protected': image_protected, } From be3cbd22bdad19d610636c170f21c311ebe80dac Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 14 Nov 2014 17:01:49 -0600 Subject: [PATCH 0254/3494] Look harder to find DevStack Change-Id: Ice5cc560513c5ada1c7a525464cd2823d5979542 --- functional/harpoon.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/functional/harpoon.sh b/functional/harpoon.sh index 76c10ffb95..4e16391794 100755 --- a/functional/harpoon.sh +++ b/functional/harpoon.sh @@ -6,8 +6,15 @@ source $FUNCTIONAL_TEST_DIR/harpoonrc OPENSTACKCLIENT_DIR=$FUNCTIONAL_TEST_DIR/.. if [[ -z $DEVSTACK_DIR ]]; then - echo "guessing location of devstack" DEVSTACK_DIR=$OPENSTACKCLIENT_DIR/../devstack + if [[ ! -d $DEVSTACK_DIR ]]; then + DEVSTACK_DIR=$HOME/devstack + if [[ ! -d $DEVSTACK_DIR ]]; then + echo "Where did you hide DevStack? Set DEVSTACK_DIR and try again" + exit 1 + fi + fi + echo "Using DevStack found at $DEVSTACK_DIR" fi function setup_credentials { From 126b2c543617866e9e1ea45ef9c5770ce5f5dda9 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Wed, 12 Nov 2014 16:11:37 -0500 Subject: [PATCH 0255/3494] Add an API example base and functional test base Add examples/common.py, which is a basic common setup that mimics OSC's configuration options and logging without the rest of the CLI. Also add the functional test tooling for examples to prevent bit rot. Co-Authored-By: Dean Troyer Change-Id: Ie92b675eafd93482ddc9a8ce0b0588e23ed50c35 --- examples/common.py | 264 ++++++++++++++++++++++++++++++ functional/common/test.py | 6 + functional/tests/test_examples.py | 22 +++ 3 files changed, 292 insertions(+) create mode 100755 examples/common.py create mode 100644 functional/tests/test_examples.py diff --git a/examples/common.py b/examples/common.py new file mode 100755 index 0000000000..ad3a5e492e --- /dev/null +++ b/examples/common.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python +# common.py - Common bits for API examples + +# 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. + +""" +API Examples + +This is a collection of common functions used by the example scripts. +It may also be run directly as a script to do basic testing of itself. + +common.object_parser() provides the common set of command-line arguments +used in the library CLIs for setting up authentication. This should make +playing with the example scripts against a running OpenStack simpler. + +common.configure_logging() provides the same basic logging control as +the OSC shell. + +common.make_session() does the minimal loading of a Keystone authentication +plugin and creates a Keystone client Session. + +""" + +import argparse +import logging +import os +import sys +import traceback + +from keystoneclient import session as ksc_session + +from openstackclient.api import auth + + +CONSOLE_MESSAGE_FORMAT = '%(levelname)s: %(name)s %(message)s' +DEFAULT_VERBOSE_LEVEL = 1 +USER_AGENT = 'osc-examples' + +PARSER_DESCRIPTION = 'A demonstration framework' + +DEFAULT_IDENTITY_API_VERSION = '2.0' + +_logger = logging.getLogger(__name__) + +# --debug sets this True +dump_stack_trace = False + + +# Generally useful stuff often found in a utils module + +def env(*vars, **kwargs): + """Search for the first defined of possibly many env vars + + Returns the first environment variable defined in vars, or + returns the default defined in kwargs. + + """ + for v in vars: + value = os.environ.get(v, None) + if value: + return value + return kwargs.get('default', '') + + +# Common Example functions + +def base_parser(parser): + """Set up some of the common CLI options + + These are the basic options that match the library CLIs so + command-line/environment setups for those also work with these + demonstration programs. + + """ + + # Global arguments + parser.add_argument( + '--os-url', + metavar='', + default=env('OS_URL'), + help='Defaults to env[OS_URL]', + ) + parser.add_argument( + '--os-region-name', + metavar='', + default=env('OS_REGION_NAME'), + help='Authentication region name (Env: OS_REGION_NAME)', + ) + parser.add_argument( + '--os-cacert', + metavar='', + default=env('OS_CACERT'), + help='CA certificate bundle file (Env: OS_CACERT)', + ) + verify_group = parser.add_mutually_exclusive_group() + verify_group.add_argument( + '--verify', + action='store_true', + help='Verify server certificate (default)', + ) + verify_group.add_argument( + '--insecure', + action='store_true', + help='Disable server certificate verification', + ) + parser.add_argument( + '--timing', + default=False, + action='store_true', + help="Print API call timing info", + ) + parser.add_argument( + '-v', '--verbose', + action='count', + dest='verbose_level', + default=1, + help='Increase verbosity of output. Can be repeated.', + ) + parser.add_argument( + '--debug', + default=False, + action='store_true', + help='show tracebacks on errors', + ) + parser.add_argument( + 'rest', + nargs='*', + help='the rest of the args', + ) + return parser + + +def configure_logging(opts): + """Typical app logging setup + + Based on OSC/cliff + + """ + + global dump_stack_trace + + root_logger = logging.getLogger('') + + # Requests logs some stuff at INFO that we don't want + # unless we have DEBUG + requests_log = logging.getLogger("requests") + requests_log.setLevel(logging.ERROR) + + # Other modules we don't want DEBUG output for so + # don't reset them below + iso8601_log = logging.getLogger("iso8601") + iso8601_log.setLevel(logging.ERROR) + + # Always send higher-level messages to the console via stderr + console = logging.StreamHandler(sys.stderr) + formatter = logging.Formatter(CONSOLE_MESSAGE_FORMAT) + console.setFormatter(formatter) + root_logger.addHandler(console) + + # Set logging to the requested level + dump_stack_trace = False + if opts.verbose_level == 0: + # --quiet + root_logger.setLevel(logging.ERROR) + elif opts.verbose_level == 1: + # This is the default case, no --debug, --verbose or --quiet + root_logger.setLevel(logging.WARNING) + elif opts.verbose_level == 2: + # One --verbose + root_logger.setLevel(logging.INFO) + elif opts.verbose_level >= 3: + # Two or more --verbose + root_logger.setLevel(logging.DEBUG) + requests_log.setLevel(logging.DEBUG) + + if opts.debug: + # --debug forces traceback + dump_stack_trace = True + root_logger.setLevel(logging.DEBUG) + requests_log.setLevel(logging.DEBUG) + + return + + +def make_session(opts, **kwargs): + """Create our base session using simple auth from ksc plugins + + The arguments required in opts varies depending on the auth plugin + that is used. This example assumes Identity v2 will be used + and selects token auth if both os_url and os_token have been + provided, otherwise it uses password. + + :param Namespace opts: + A parser options Namespace containing the authentication + options to be used + :param dict kwargs: + Additional options passed directly to Session constructor + """ + + # If no auth type is named by the user, select one based on + # the supplied options + auth_plugin_name = auth.select_auth_plugin(opts) + + (auth_plugin, auth_params) = auth.build_auth_params( + auth_plugin_name, + opts, + ) + auth_p = auth_plugin.load_from_options(**auth_params) + + session = ksc_session.Session( + auth=auth_p, + **kwargs + ) + + return session + + +# Top-level functions + +def run(opts): + """Default run command""" + + # Do some basic testing here + sys.stdout.write("Default run command\n") + sys.stdout.write("Verbose level: %s\n" % opts.verbose_level) + sys.stdout.write("Debug: %s\n" % opts.debug) + sys.stdout.write("dump_stack_trace: %s\n" % dump_stack_trace) + + +def setup(): + """Parse command line and configure logging""" + opts = base_parser( + auth.build_auth_plugins_option_parser( + argparse.ArgumentParser(description='Object API Example') + ) + ).parse_args() + configure_logging(opts) + return opts + + +def main(opts, run): + try: + return run(opts) + except Exception as e: + if dump_stack_trace: + _logger.error(traceback.format_exc(e)) + else: + _logger.error('Exception raised: ' + str(e)) + return 1 + + +if __name__ == "__main__": + opts = setup() + sys.exit(main(opts, run)) diff --git a/functional/common/test.py b/functional/common/test.py index c1bb0b101a..464844fad1 100644 --- a/functional/common/test.py +++ b/functional/common/test.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import os import re import shlex import subprocess @@ -19,6 +20,11 @@ from functional.common import exceptions +COMMON_DIR = os.path.dirname(os.path.abspath(__file__)) +FUNCTIONAL_DIR = os.path.normpath(os.path.join(COMMON_DIR, '..')) +ROOT_DIR = os.path.normpath(os.path.join(FUNCTIONAL_DIR, '..')) +EXAMPLE_DIR = os.path.join(ROOT_DIR, 'examples') + def execute(cmd, action, flags='', params='', fail_ok=False, merge_stderr=False): diff --git a/functional/tests/test_examples.py b/functional/tests/test_examples.py new file mode 100644 index 0000000000..fdaa26b8d4 --- /dev/null +++ b/functional/tests/test_examples.py @@ -0,0 +1,22 @@ +# 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 functional.common import test + + +class ExampleTests(test.TestCase): + """Functional tests for running examples.""" + + def test_common(self): + # NOTE(stevemar): If an examples has a non-zero return + # code, then execute will raise an error by default. + test.execute('python', test.EXAMPLE_DIR + '/common.py --debug') From 01a5ff6d3234457fd0f8268be13fca487a1793c2 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Sun, 12 Oct 2014 22:55:41 -0500 Subject: [PATCH 0256/3494] Add more session/api examples * examples/object_api.py - Example of using the Object_Store API * examples/osc-lib.py - Minimal client to use ClientManager as a library Also add matching functional tests Change-Id: I4243a21141a821420951d4b6352d41029cdcccbc --- examples/object_api.py | 106 ++++++++++++++++++++++++++++++ examples/osc-lib.py | 102 ++++++++++++++++++++++++++++ functional/tests/test_examples.py | 6 ++ 3 files changed, 214 insertions(+) create mode 100755 examples/object_api.py create mode 100755 examples/osc-lib.py diff --git a/examples/object_api.py b/examples/object_api.py new file mode 100755 index 0000000000..5c6bd9f053 --- /dev/null +++ b/examples/object_api.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python +# object_api.py - Example object-store API usage + +# 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. + +""" +Object Store API Examples + +This script shows the basic use of the low-level Object Store API + +""" + +import argparse +import logging +import sys + +import common + + +from openstackclient.api import object_store_v1 as object_store +from openstackclient.identity import client as identity_client + + +LOG = logging.getLogger('') + + +def run(opts): + """Run the examples""" + + # Set up certificate verification and CA bundle + # NOTE(dtroyer): This converts from the usual OpenStack way to the single + # requests argument and is an app-specific thing because + # we want to be like OpenStackClient. + if opts.os_cacert: + verify = opts.os_cacert + else: + verify = not opts.insecure + + # get a session + # common.make_session() does all the ugly work of mapping + # CLI options/env vars to the required plugin arguments. + # The returned session will have a configured auth object + # based on the selected plugin's available options. + # So to do...oh, just go to api.auth.py and look at what it does. + session = common.make_session(opts, verify=verify) + + # Extract an endpoint + auth_ref = session.auth.get_auth_ref(session) + + if opts.os_url: + endpoint = opts.os_url + else: + endpoint = auth_ref.service_catalog.url_for( + service_type='object-store', + endpoint_type='public', + ) + + # At this point we have a working session with a configured authentication + # plugin. From here on it is the app making the decisions. Need to + # talk to two clouds? Go back and make another session but with opts + # set to different credentials. Or use a config file and load it + # directly into the plugin. This example doesn't show that (yet). + # Want to work ahead? Look into the plugin load_from_*() methods + # (start in keystoneclient/auth/base.py). + + # This example is for the Object Store API so make one + obj_api = object_store.APIv1( + session=session, + service_type='object-store', + endpoint=endpoint, + ) + + # Do useful things with it + + c_list = obj_api.container_list() + print("Name\tCount\tBytes") + for c in c_list: + print("%s\t%d\t%d" % (c['name'], c['count'], c['bytes'])) + + if len(c_list) > 0: + # See what is in the first container + o_list = obj_api.object_list(c_list[0]['name']) + print("\nObject") + for o in o_list: + print("%s" % o) + + +if __name__ == "__main__": + opts = common.base_parser( + identity_client.build_option_parser( + argparse.ArgumentParser(description='Object API Example') + ) + ).parse_args() + + common.configure_logging(opts) + sys.exit(common.main(opts, run)) diff --git a/examples/osc-lib.py b/examples/osc-lib.py new file mode 100755 index 0000000000..69fc5d9849 --- /dev/null +++ b/examples/osc-lib.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python +# osc-lib.py - Example using OSC as a library + +# 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. + +""" +OpenStackClient Library Examples + +This script shows the basic use of the OpenStackClient ClientManager +as a library. + +""" + +import argparse +import logging +import sys + +import common + +from openstackclient.common import clientmanager + + +LOG = logging.getLogger('') + + +def run(opts): + """Run the examples""" + + # Loop through extensions to get API versions + # Currently API versions are statically selected. Once discovery + # is working this can go away... + api_version = {} + for mod in clientmanager.PLUGIN_MODULES: + version_opt = getattr(opts, mod.API_VERSION_OPTION, None) + if version_opt: + api = mod.API_NAME + api_version[api] = version_opt + + # Set up certificate verification and CA bundle + # NOTE(dtroyer): This converts from the usual OpenStack way to the single + # requests argument and is an app-specific thing because + # we want to be like OpenStackClient. + if opts.os_cacert: + verify = opts.os_cacert + else: + verify = not opts.insecure + + # Get a ClientManager + # Collect the auth and config options together and give them to + # ClientManager and it will wrangle all of the goons into place. + client_manager = clientmanager.ClientManager( + auth_options=opts, + verify=verify, + api_version=api_version, + ) + + # At this point we have a working client manager with a configured + # session and authentication plugin. From here on it is the app + # making the decisions. Need to talk to two clouds? Make another + # client manager with different opts. Or use a config file and load it + # directly into the plugin. This example doesn't show that (yet). + + # Do useful things with it + + # Look in the object store + c_list = client_manager.object_store.container_list() + print("Name\tCount\tBytes") + for c in c_list: + print("%s\t%d\t%d" % (c['name'], c['count'], c['bytes'])) + + if len(c_list) > 0: + # See what is in the first container + o_list = client_manager.object_store.object_list(c_list[0]['name']) + print("\nObject") + for o in o_list: + print("%s" % o) + + # Look at the compute flavors + flavor_list = client_manager.compute.flavors.list() + print("\nFlavors:") + for f in flavor_list: + print("%s" % f) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='ClientManager Example') + opts = common.base_parser( + clientmanager.build_plugin_option_parser(parser), + ).parse_args() + + common.configure_logging(opts) + sys.exit(common.main(opts, run)) diff --git a/functional/tests/test_examples.py b/functional/tests/test_examples.py index fdaa26b8d4..6e0e586724 100644 --- a/functional/tests/test_examples.py +++ b/functional/tests/test_examples.py @@ -20,3 +20,9 @@ def test_common(self): # NOTE(stevemar): If an examples has a non-zero return # code, then execute will raise an error by default. test.execute('python', test.EXAMPLE_DIR + '/common.py --debug') + + def test_object_api(self): + test.execute('python', test.EXAMPLE_DIR + '/object_api.py --debug') + + def test_osc_lib(self): + test.execute('python', test.EXAMPLE_DIR + '/osc-lib.py --debug') From 2b02beaa5182e678d9da00402a7c4f137710f813 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 14 Nov 2014 17:27:58 -0600 Subject: [PATCH 0257/3494] Liberalize version matching a bit For class-loading purposes we can just use the major version, so accept that. Only Identity and Compute were affected; Compute is included just to be pedantically complete. For command groups we also just use the major version so fix Compute and the version option handling. Change the internal default for Identity to a simple '2' so it is also consistent with the rest of the world. Then comes microversioning... Closes-Bug: #1292638 Change-Id: Ibaf823b31caa288a83de38d2c258860b128b87d8 --- openstackclient/compute/client.py | 1 + openstackclient/identity/client.py | 10 ++++++---- openstackclient/shell.py | 3 ++- openstackclient/tests/test_shell.py | 6 +++--- setup.cfg | 2 +- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py index c87bbee700..3725350ac1 100644 --- a/openstackclient/compute/client.py +++ b/openstackclient/compute/client.py @@ -27,6 +27,7 @@ API_NAME = 'compute' API_VERSIONS = { '1.1': 'novaclient.v1_1.client.Client', + '1': 'novaclient.v1_1.client.Client', '2': 'novaclient.v1_1.client.Client', } diff --git a/openstackclient/identity/client.py b/openstackclient/identity/client.py index 8050d12096..daf24e12a5 100644 --- a/openstackclient/identity/client.py +++ b/openstackclient/identity/client.py @@ -15,23 +15,25 @@ import logging -from keystoneclient.v2_0 import client as identity_client_v2_0 +from keystoneclient.v2_0 import client as identity_client_v2 from openstackclient.api import auth from openstackclient.common import utils LOG = logging.getLogger(__name__) -DEFAULT_IDENTITY_API_VERSION = '2.0' +DEFAULT_IDENTITY_API_VERSION = '2' API_VERSION_OPTION = 'os_identity_api_version' API_NAME = 'identity' API_VERSIONS = { - '2.0': 'openstackclient.identity.client.IdentityClientv2_0', + '2.0': 'openstackclient.identity.client.IdentityClientv2', + '2': 'openstackclient.identity.client.IdentityClientv2', '3': 'keystoneclient.v3.client.Client', } # Translate our API version to auth plugin version prefix AUTH_VERSIONS = { '2.0': 'v2', + '2': 'v2', '3': 'v3', } @@ -66,7 +68,7 @@ def build_option_parser(parser): return auth.build_auth_plugins_option_parser(parser) -class IdentityClientv2_0(identity_client_v2_0.Client): +class IdentityClientv2(identity_client_v2.Client): """Tweak the earlier client class to deal with some changes""" def __getattr__(self, name): # Map v3 'projects' back to v2 'tenants' diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 1198bae18a..ac5556affa 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -255,7 +255,8 @@ def initialize_app(self, argv): if version_opt: api = mod.API_NAME self.api_version[api] = version_opt - version = '.v' + version_opt.replace('.', '_') + # Command groups deal only with major versions + version = '.v' + version_opt.replace('.', '_').split('_')[0] cmd_group = 'openstack.' + api.replace('-', '_') + version self.command_manager.add_command_group(cmd_group) self.log.debug( diff --git a/openstackclient/tests/test_shell.py b/openstackclient/tests/test_shell.py index 837a48afd7..8656d089fd 100644 --- a/openstackclient/tests/test_shell.py +++ b/openstackclient/tests/test_shell.py @@ -38,13 +38,13 @@ DEFAULT_COMPUTE_API_VERSION = "2" -DEFAULT_IDENTITY_API_VERSION = "2.0" -DEFAULT_IMAGE_API_VERSION = "v2" +DEFAULT_IDENTITY_API_VERSION = "2" +DEFAULT_IMAGE_API_VERSION = "2" DEFAULT_VOLUME_API_VERSION = "1" DEFAULT_NETWORK_API_VERSION = "2" LIB_COMPUTE_API_VERSION = "2" -LIB_IDENTITY_API_VERSION = "2.0" +LIB_IDENTITY_API_VERSION = "2" LIB_IMAGE_API_VERSION = "1" LIB_VOLUME_API_VERSION = "1" LIB_NETWORK_API_VERSION = "2" diff --git a/setup.cfg b/setup.cfg index c0519d11d6..8ce5e59c64 100644 --- a/setup.cfg +++ b/setup.cfg @@ -131,7 +131,7 @@ openstack.compute.v2 = server_unrescue = openstackclient.compute.v2.server:UnrescueServer server_unset = openstackclient.compute.v2.server:UnsetServer -openstack.identity.v2_0 = +openstack.identity.v2 = catalog_list = openstackclient.identity.v2_0.catalog:ListCatalog catalog_show = openstackclient.identity.v2_0.catalog:ShowCatalog From c1b376dc333398903007f3f26ebd81378071bdcb Mon Sep 17 00:00:00 2001 From: Marek Denis Date: Mon, 17 Nov 2014 10:27:50 +0100 Subject: [PATCH 0258/3494] Add environment variable in the os-auth-type help Help for option --os-auth-type doesn't specify what environment variable configures it. This patch fixes that. Change-Id: Id2e29e477d5ca56339bd777fb73b5af13788615b --- openstackclient/api/auth.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openstackclient/api/auth.py b/openstackclient/api/auth.py index e19c6b7931..bfb2f83a47 100644 --- a/openstackclient/api/auth.py +++ b/openstackclient/api/auth.py @@ -136,7 +136,8 @@ def build_auth_plugins_option_parser(parser): default=utils.env('OS_AUTH_TYPE'), help='Select an auhentication type. Available types: ' + ', '.join(available_plugins) + - '. Default: selected based on --os-username/--os-token', + '. Default: selected based on --os-username/--os-token' + + ' (Env: OS_AUTH_TYPE)', choices=available_plugins ) # make sur we catch old v2.0 env values From 0d56d0178bd78057b2e8c5a1a6360ffb453d3791 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 24 Oct 2014 10:34:41 -0500 Subject: [PATCH 0259/3494] Add authentication description doc This is represents the current operation Closes-Bug: #1337422 Change-Id: I8092e7723b563647e13b6e2f0b7901a16572b6c7 --- doc/source/authentication.rst | 86 +++++++++++++++++++++++++++++++++++ doc/source/index.rst | 1 + 2 files changed, 87 insertions(+) create mode 100644 doc/source/authentication.rst diff --git a/doc/source/authentication.rst b/doc/source/authentication.rst new file mode 100644 index 0000000000..5acfe33947 --- /dev/null +++ b/doc/source/authentication.rst @@ -0,0 +1,86 @@ +============== +Authentication +============== + +OpenStackClient leverages `python-keystoneclient`_ authentication +plugins to support a number of different authentication methods. + +.. _`python-keystoneclient`: http://docs.openstack.org/developer/python-keystoneclient/authentication-plugins.html + +Authentication Process +---------------------- + +The user provides some number of authentication credential options. +If an authentication type is not provided (``--os-auth-type``), the +authentication options are examined to determine if one of the default +types can be used. If no match is found an error is reported and OSC exits. + +Note that the authentication call to the Identity service has not yet +occurred. It is deferred until the last possible moment in order to +reduce the number of unnecessary queries to the server, such as when further +processing detects an invalid command. + +Authentication Plugins +---------------------- + +The Keystone client library implements the base set of plugins. Additional +plugins may be available from the Keystone project or other sources. + +There are at least three authentication types that are always available: + +* **Password**: A project, username and password are used to identify the + user. An optional domain may also be included. This is the most common + type and is the default any time a username is supplied. An authentication + URL for the Identity service is also required. [Required: ``--os-auth-url``, + ``--os-project-name``, ``--os-username``; Optional: ``--os-password``] +* **Token**: This is slightly different from the usual token authentication + (described below as token/endpoint) in that a token and an authentication + URL are supplied and the plugin retrieves a new token. + [Required: ``--os-auth-url``, ``--os-token``] +* **Token/Endpoint**: This is the original token authentication (known as 'token + flow' in the early CLI documentation in the OpenStack wiki). It requires + a token and a direct endpoint that is used in the API call. The difference + from the new Token type is this token is used as-is, no call is made + to the Identity service from the client. This type is most often used to + bootstrap a Keystone server where the token is the ``admin_token`` configured + in ``keystone.conf``. It will also work with other services and a regular + scoped token such as one obtained from a ``token issue`` command. + [Required: ``--os-url``, ``--os-token``] +* **Others**: Other authentication plugins such as SAML, Kerberos, and OAuth1.0 + are under development and also supported. To use them, they must be selected + by supplying the ``--os-auth-type`` option. + +Detailed Process +---------------- + +The authentication process in OpenStackClient is all contained in and handled +by the ``ClientManager`` object. + +* On import ``api.auth``: + + * obtains the list of installed Keystone authentication + plugins from the ``keystoneclient.auth.plugin`` entry point. + * builds a list of authentication options from the plugins. + +* A new ``ClientManager`` is created and supplied with the set of options from the + command line and/or environment: + + * If ``--os-auth-type`` is provided and is a valid and available plugin + it is used. + * If ``--os-auth-type`` is not provided an authentication plugin + is selected based on the existing options. This is a short-circuit + evaluation, the first match wins. + + * If ``--os-endpoint`` and ``--os-token`` are both present ``token_endpoint`` + is selected + * If ``--os-username`` is supplied ``password`` is selected + * If ``--os-token`` is supplied ``token`` is selected + * If no selection has been made by now exit with error + + * Load the selected plugin class. + +* When an operation that requires authentication is attempted ``ClientManager`` + makes the actual inital request to the Identity service. + + * if ``--os-auth-url`` is not supplied for any of the types except + Token/Endpoint, exit with an error. diff --git a/doc/source/index.rst b/doc/source/index.rst index 0f92b3f018..b6145a86b6 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -14,6 +14,7 @@ Contents: releases commands plugins + authentication man/openstack Getting Started From 79653afa7b7f4c3795f6fe3c972628729cb51b4d Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 14 Nov 2014 01:59:14 -0500 Subject: [PATCH 0260/3494] Add --or-show support for v3 identity resources Add --or-show for the following: * v3 roles * v3 projects * v3 domains * v3 users * v3 groups Closes-Bug: #1390389 Change-Id: Id4ef043e5fda6be49a515eb3fe138c813c393ec9 --- openstackclient/identity/v3/domain.py | 26 ++++++++++++++++---- openstackclient/identity/v3/group.py | 26 ++++++++++++++++---- openstackclient/identity/v3/project.py | 30 +++++++++++++++++------ openstackclient/identity/v3/role.py | 17 ++++++++++++- openstackclient/identity/v3/user.py | 34 +++++++++++++++++++------- 5 files changed, 106 insertions(+), 27 deletions(-) diff --git a/openstackclient/identity/v3/domain.py b/openstackclient/identity/v3/domain.py index d14da48682..1233fea5e9 100644 --- a/openstackclient/identity/v3/domain.py +++ b/openstackclient/identity/v3/domain.py @@ -22,8 +22,10 @@ from cliff import command from cliff import lister from cliff import show +from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc from openstackclient.common import utils +from openstackclient.i18n import _ # noqa class CreateDomain(show.ShowOne): @@ -55,16 +57,30 @@ def get_parser(self, prog_name): dest='enabled', action='store_false', help='Disable domain') + parser.add_argument( + '--or-show', + action='store_true', + help=_('Return existing domain'), + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - domain = identity_client.domains.create( - name=parsed_args.name, - description=parsed_args.description, - enabled=parsed_args.enabled, - ) + + try: + domain = identity_client.domains.create( + name=parsed_args.name, + description=parsed_args.description, + enabled=parsed_args.enabled, + ) + except ksc_exc.Conflict as e: + if parsed_args.or_show: + domain = utils.find_resource(identity_client.domains, + parsed_args.name) + self.log.info('Returning existing domain %s', domain.name) + else: + raise e domain._info.pop('links') return zip(*sorted(six.iteritems(domain._info))) diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index d2ffca2733..14838bba57 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -22,8 +22,10 @@ from cliff import command from cliff import lister from cliff import show +from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc from openstackclient.common import utils +from openstackclient.i18n import _ # noqa from openstackclient.identity import common @@ -122,7 +124,11 @@ def get_parser(self, prog_name): '--domain', metavar='', help='References the domain ID or name which owns the group') - + parser.add_argument( + '--or-show', + action='store_true', + help=_('Return existing group'), + ) return parser def take_action(self, parsed_args): @@ -133,10 +139,20 @@ def take_action(self, parsed_args): parsed_args.domain).id else: domain = None - group = identity_client.groups.create( - name=parsed_args.name, - domain=domain, - description=parsed_args.description) + + try: + group = identity_client.groups.create( + name=parsed_args.name, + domain=domain, + description=parsed_args.description) + except ksc_exc.Conflict as e: + if parsed_args.or_show: + group = utils.find_resource(identity_client.groups, + parsed_args.name, + domain_id=domain) + self.log.info('Returning existing group %s', group.name) + else: + raise e group._info.pop('links') return zip(*sorted(six.iteritems(group._info))) diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 1cdeb15067..4fcf81278f 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -21,9 +21,11 @@ from cliff import command from cliff import lister from cliff import show +from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.i18n import _ # noqa from openstackclient.identity import common @@ -67,6 +69,11 @@ def get_parser(self, prog_name): help='Property to add for this project ' '(repeat option to set multiple properties)', ) + parser.add_argument( + '--or-show', + action='store_true', + help=_('Return existing project'), + ) return parser def take_action(self, parsed_args): @@ -86,13 +93,22 @@ def take_action(self, parsed_args): if parsed_args.property: kwargs = parsed_args.property.copy() - project = identity_client.projects.create( - name=parsed_args.name, - domain=domain, - description=parsed_args.description, - enabled=enabled, - **kwargs - ) + try: + project = identity_client.projects.create( + name=parsed_args.name, + domain=domain, + description=parsed_args.description, + enabled=enabled, + **kwargs + ) + except ksc_exc.Conflict as e: + if parsed_args.or_show: + project = utils.find_resource(identity_client.projects, + parsed_args.name, + domain_id=domain) + self.log.info('Returning existing project %s', project.name) + else: + raise e project._info.pop('links') return zip(*sorted(six.iteritems(project._info))) diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index a3c24b7af6..7801ca6563 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -22,8 +22,10 @@ from cliff import command from cliff import lister from cliff import show +from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc from openstackclient.common import utils +from openstackclient.i18n import _ # noqa class AddRole(command.Command): @@ -149,13 +151,26 @@ def get_parser(self, prog_name): metavar='', help='New role name', ) + parser.add_argument( + '--or-show', + action='store_true', + help=_('Return existing role'), + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - role = identity_client.roles.create(name=parsed_args.name) + try: + role = identity_client.roles.create(name=parsed_args.name) + except ksc_exc.Conflict as e: + if parsed_args.or_show: + role = utils.find_resource(identity_client.roles, + parsed_args.name) + self.log.info('Returning existing role %s', role.name) + else: + raise e role._info.pop('links') return zip(*sorted(six.iteritems(role._info))) diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 9a3e00b83a..63dd3b4f58 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -21,8 +21,10 @@ from cliff import command from cliff import lister from cliff import show +from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc from openstackclient.common import utils +from openstackclient.i18n import _ # noqa from openstackclient.identity import common @@ -80,6 +82,11 @@ def get_parser(self, prog_name): action='store_true', help='Disable user', ) + parser.add_argument( + '--or-show', + action='store_true', + help=_('Return existing user'), + ) return parser def take_action(self, parsed_args): @@ -106,15 +113,24 @@ def take_action(self, parsed_args): if parsed_args.password_prompt: parsed_args.password = utils.get_password(self.app.stdin) - user = identity_client.users.create( - name=parsed_args.name, - domain=domain_id, - default_project=project_id, - password=parsed_args.password, - email=parsed_args.email, - description=parsed_args.description, - enabled=enabled - ) + try: + user = identity_client.users.create( + name=parsed_args.name, + domain=domain_id, + default_project=project_id, + password=parsed_args.password, + email=parsed_args.email, + description=parsed_args.description, + enabled=enabled + ) + except ksc_exc.Conflict as e: + if parsed_args.or_show: + user = utils.find_resource(identity_client.users, + parsed_args.name, + domain_id=domain_id) + self.log.info('Returning existing user %s', user.name) + else: + raise e user._info.pop('links') return zip(*sorted(six.iteritems(user._info))) From 25f1c8b98a197c1a8fc0820485b29996a70b1ca9 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 18 Nov 2014 11:37:03 +0000 Subject: [PATCH 0261/3494] Updated from global requirements Change-Id: Ifd9110cf94dfd2f62e59939a7be1a88e919beb36 --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index de5732cf24..4038b1bb0a 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -12,5 +12,5 @@ oslotest>=1.2.0 # Apache-2.0 requests-mock>=0.5.1 # Apache-2.0 sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 testrepository>=0.0.18 -testtools>=0.9.36 +testtools>=0.9.36,!=1.2.0,!=1.4.0 WebOb>=1.2.3 From 6edc9b89ed674b0c7514f450ddeba7e81666b87c Mon Sep 17 00:00:00 2001 From: wanghong Date: Mon, 3 Nov 2014 10:31:04 +0800 Subject: [PATCH 0262/3494] add keystone v3 region object Co-Authored-By: Steve Martinelli Change-Id: Ia6f607630dbf507681733c3ab3b9b7c55de30f49 Closes-Bug: #1387932 --- openstackclient/identity/v3/region.py | 205 +++++++++ openstackclient/tests/identity/v3/fakes.py | 15 + .../tests/identity/v3/test_region.py | 406 ++++++++++++++++++ setup.cfg | 6 + 4 files changed, 632 insertions(+) create mode 100644 openstackclient/identity/v3/region.py create mode 100644 openstackclient/tests/identity/v3/test_region.py diff --git a/openstackclient/identity/v3/region.py b/openstackclient/identity/v3/region.py new file mode 100644 index 0000000000..cce3417d5a --- /dev/null +++ b/openstackclient/identity/v3/region.py @@ -0,0 +1,205 @@ +# 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. +# + +"""Identity v3 Region action implementations""" + +import logging +import six + +from cliff import command +from cliff import lister +from cliff import show + +from openstackclient.common import utils +from openstackclient.i18n import _ # noqa + + +class CreateRegion(show.ShowOne): + """Create new region""" + + log = logging.getLogger(__name__ + '.CreateRegion') + + def get_parser(self, prog_name): + parser = super(CreateRegion, self).get_parser(prog_name) + # NOTE(stevemar): The API supports an optional region ID, but that + # seems like poor UX, we will only support user-defined IDs. + parser.add_argument( + 'region', + metavar='', + help=_('New region'), + ) + parser.add_argument( + '--parent-region', + metavar='', + help=_('The parent region of new region'), + ) + parser.add_argument( + '--description', + metavar='', + help=_('New region description'), + ) + parser.add_argument( + '--url', + metavar='', + help=_('New region url'), + ) + + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + identity_client = self.app.client_manager.identity + + region = identity_client.regions.create( + id=parsed_args.region, + url=parsed_args.url, + parent_region=parsed_args.parent_region, + description=parsed_args.description, + ) + + region._info['region'] = region._info.pop('id') + region._info['parent_region'] = region._info.pop('parent_region_id') + region._info.pop('links', None) + return zip(*sorted(six.iteritems(region._info))) + + +class DeleteRegion(command.Command): + """Delete region""" + + log = logging.getLogger(__name__ + '.DeleteRegion') + + def get_parser(self, prog_name): + parser = super(DeleteRegion, self).get_parser(prog_name) + parser.add_argument( + 'region', + metavar='', + help=_('Region to delete'), + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + identity_client = self.app.client_manager.identity + + identity_client.regions.delete(parsed_args.region) + return + + +class ListRegion(lister.Lister): + """List regions""" + + log = logging.getLogger(__name__ + '.ListRegion') + + def get_parser(self, prog_name): + parser = super(ListRegion, self).get_parser(prog_name) + parser.add_argument( + '--parent-region', + metavar='', + help=_('Filter by parent region'), + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + identity_client = self.app.client_manager.identity + + kwargs = {} + if parsed_args.parent_region: + kwargs['parent_region_id'] = parsed_args.parent_region + + columns_headers = ('Region', 'Parent Region', 'Description', 'URL') + columns = ('ID', 'Parent Region Id', 'Description', 'URL') + + data = identity_client.regions.list(**kwargs) + return (columns_headers, + (utils.get_item_properties( + s, columns, + formatters={}, + ) for s in data)) + + +class SetRegion(command.Command): + """Set region properties""" + + log = logging.getLogger(__name__ + '.SetRegion') + + def get_parser(self, prog_name): + parser = super(SetRegion, self).get_parser(prog_name) + parser.add_argument( + 'region', + metavar='', + help=_('Region to change'), + ) + parser.add_argument( + '--parent-region', + metavar='', + help=_('New parent region of the region'), + ) + parser.add_argument( + '--description', + metavar='', + help=_('New region description'), + ) + parser.add_argument( + '--url', + metavar='', + help=_('New region url'), + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + identity_client = self.app.client_manager.identity + + if (not parsed_args.url + and not parsed_args.parent_region + and not parsed_args.description): + return + + kwargs = {} + if parsed_args.url: + kwargs['url'] = parsed_args.url + if parsed_args.description: + kwargs['description'] = parsed_args.description + if parsed_args.parent_region: + kwargs['parent_region'] = parsed_args.parent_region + + identity_client.regions.update(parsed_args.region, **kwargs) + return + + +class ShowRegion(show.ShowOne): + """Show region""" + + log = logging.getLogger(__name__ + '.ShowRegion') + + def get_parser(self, prog_name): + parser = super(ShowRegion, self).get_parser(prog_name) + parser.add_argument( + 'region', + metavar='', + help=_('Region to display'), + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + identity_client = self.app.client_manager.identity + + region = utils.find_resource(identity_client.regions, + parsed_args.region) + + region._info['region'] = region._info.pop('id') + region._info['parent_region'] = region._info.pop('parent_region_id') + region._info.pop('links', None) + return zip(*sorted(six.iteritems(region._info))) diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index b195ed78b7..7acaa7f156 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -122,6 +122,19 @@ 'links': base_url + 'projects/' + project_id, } +region_id = 'region_one' +region_url = 'http://localhost:1111' +region_parent_region_id = 'region_two' +region_description = 'region one' + +REGION = { + 'id': region_id, + 'url': region_url, + 'description': region_description, + 'parent_region_id': region_parent_region_id, + 'links': base_url + 'regions/' + region_id, +} + role_id = 'r1' role_name = 'roller' @@ -310,6 +323,8 @@ def __init__(self, **kwargs): self.oauth1.resource_class = fakes.FakeResource(None, {}) self.projects = mock.Mock() self.projects.resource_class = fakes.FakeResource(None, {}) + self.regions = mock.Mock() + self.regions.resource_class = fakes.FakeResource(None, {}) self.roles = mock.Mock() self.roles.resource_class = fakes.FakeResource(None, {}) self.services = mock.Mock() diff --git a/openstackclient/tests/identity/v3/test_region.py b/openstackclient/tests/identity/v3/test_region.py new file mode 100644 index 0000000000..7f6ced9f2d --- /dev/null +++ b/openstackclient/tests/identity/v3/test_region.py @@ -0,0 +1,406 @@ +# 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 copy + +from openstackclient.identity.v3 import region +from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes + + +class TestRegion(identity_fakes.TestIdentityv3): + + def setUp(self): + super(TestRegion, self).setUp() + + # Get a shortcut to the RegionManager Mock + self.regions_mock = self.app.client_manager.identity.regions + self.regions_mock.reset_mock() + + +class TestRegionCreate(TestRegion): + + def setUp(self): + super(TestRegionCreate, self).setUp() + + self.regions_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.REGION), + loaded=True, + ) + + # Get the command object to test + self.cmd = region.CreateRegion(self.app, None) + + def test_region_create_description(self): + arglist = [ + identity_fakes.region_id, + '--description', identity_fakes.region_description, + ] + verifylist = [ + ('region', identity_fakes.region_id), + ('description', identity_fakes.region_description) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'description': identity_fakes.region_description, + 'id': identity_fakes.region_id, + 'parent_region': None, + 'url': None, + } + self.regions_mock.create.assert_called_with( + **kwargs + ) + + collist = ('description', 'parent_region', 'region', 'url') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.region_description, + identity_fakes.region_parent_region_id, + identity_fakes.region_id, + identity_fakes.region_url, + ) + self.assertEqual(datalist, data) + + def test_region_create_no_options(self): + arglist = [ + identity_fakes.region_id, + ] + verifylist = [ + ('region', identity_fakes.region_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'description': None, + 'id': identity_fakes.region_id, + 'parent_region': None, + 'url': None, + } + self.regions_mock.create.assert_called_with( + **kwargs + ) + + collist = ('description', 'parent_region', 'region', 'url') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.region_description, + identity_fakes.region_parent_region_id, + identity_fakes.region_id, + identity_fakes.region_url, + ) + self.assertEqual(datalist, data) + + def test_region_create_parent_region_id(self): + arglist = [ + identity_fakes.region_id, + '--parent-region', identity_fakes.region_parent_region_id, + ] + verifylist = [ + ('region', identity_fakes.region_id), + ('parent_region', identity_fakes.region_parent_region_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'description': None, + 'id': identity_fakes.region_id, + 'parent_region': identity_fakes.region_parent_region_id, + 'url': None, + } + self.regions_mock.create.assert_called_with( + **kwargs + ) + + collist = ('description', 'parent_region', 'region', 'url') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.region_description, + identity_fakes.region_parent_region_id, + identity_fakes.region_id, + identity_fakes.region_url, + ) + self.assertEqual(datalist, data) + + def test_region_create_url(self): + arglist = [ + identity_fakes.region_id, + '--url', identity_fakes.region_url, + ] + verifylist = [ + ('region', identity_fakes.region_id), + ('url', identity_fakes.region_url), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'description': None, + 'id': identity_fakes.region_id, + 'parent_region': None, + 'url': identity_fakes.region_url, + } + self.regions_mock.create.assert_called_with( + **kwargs + ) + + collist = ('description', 'parent_region', 'region', 'url') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.region_description, + identity_fakes.region_parent_region_id, + identity_fakes.region_id, + identity_fakes.region_url, + ) + self.assertEqual(datalist, data) + + +class TestRegionDelete(TestRegion): + + def setUp(self): + super(TestRegionDelete, self).setUp() + + self.regions_mock.delete.return_value = None + + # Get the command object to test + self.cmd = region.DeleteRegion(self.app, None) + + def test_region_delete_no_options(self): + arglist = [ + identity_fakes.region_id, + ] + verifylist = [ + ('region', identity_fakes.region_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(0, result) + + self.regions_mock.delete.assert_called_with( + identity_fakes.region_id, + ) + + +class TestRegionList(TestRegion): + + def setUp(self): + super(TestRegionList, self).setUp() + + self.regions_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.REGION), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = region.ListRegion(self.app, None) + + def test_region_list_no_options(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + self.regions_mock.list.assert_called_with() + + collist = ('Region', 'Parent Region', 'Description', 'URL') + self.assertEqual(collist, columns) + datalist = (( + identity_fakes.region_id, + identity_fakes.region_parent_region_id, + identity_fakes.region_description, + identity_fakes.region_url, + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_region_list_parent_region_id(self): + arglist = [ + '--parent-region', identity_fakes.region_parent_region_id, + ] + verifylist = [ + ('parent_region', identity_fakes.region_parent_region_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + self.regions_mock.list.assert_called_with( + parent_region_id=identity_fakes.region_parent_region_id) + + collist = ('Region', 'Parent Region', 'Description', 'URL') + self.assertEqual(collist, columns) + datalist = (( + identity_fakes.region_id, + identity_fakes.region_parent_region_id, + identity_fakes.region_description, + identity_fakes.region_url, + ), ) + self.assertEqual(datalist, tuple(data)) + + +class TestRegionSet(TestRegion): + + def setUp(self): + super(TestRegionSet, self).setUp() + + self.regions_mock.update.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.REGION), + loaded=True, + ) + + # Get the command object to test + self.cmd = region.SetRegion(self.app, None) + + def test_region_set_no_options(self): + arglist = [ + identity_fakes.region_id, + ] + verifylist = [ + ('region', identity_fakes.region_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(0, result) + + self.assertNotCalled(self.regions_mock.update) + + def test_region_set_description(self): + arglist = [ + '--description', 'qwerty', + identity_fakes.region_id, + ] + verifylist = [ + ('description', 'qwerty'), + ('region', identity_fakes.region_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(0, result) + + # Set expected values + kwargs = { + 'description': 'qwerty', + } + self.regions_mock.update.assert_called_with( + identity_fakes.region_id, + **kwargs + ) + + def test_region_set_url(self): + arglist = [ + '--url', 'new url', + identity_fakes.region_id, + ] + verifylist = [ + ('url', 'new url'), + ('region', identity_fakes.region_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(0, result) + + # Set expected values + kwargs = { + 'url': 'new url', + } + self.regions_mock.update.assert_called_with( + identity_fakes.region_id, + **kwargs + ) + + def test_region_set_parent_region_id(self): + arglist = [ + '--parent-region', 'new_parent', + identity_fakes.region_id, + ] + verifylist = [ + ('parent_region', 'new_parent'), + ('region', identity_fakes.region_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(0, result) + + # Set expected values + kwargs = { + 'parent_region': 'new_parent', + } + self.regions_mock.update.assert_called_with( + identity_fakes.region_id, + **kwargs + ) + + +class TestRegionShow(TestRegion): + + def setUp(self): + super(TestRegionShow, self).setUp() + + self.regions_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.REGION), + loaded=True, + ) + + # Get the command object to test + self.cmd = region.ShowRegion(self.app, None) + + def test_region_show(self): + arglist = [ + identity_fakes.region_id, + ] + verifylist = [ + ('region', identity_fakes.region_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + self.regions_mock.get.assert_called_with( + identity_fakes.region_id, + ) + + collist = ('description', 'parent_region', 'region', 'url') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.region_description, + identity_fakes.region_parent_region_id, + identity_fakes.region_id, + identity_fakes.region_url, + ) + self.assertEqual(datalist, data) diff --git a/setup.cfg b/setup.cfg index c0519d11d6..e0cf5d1dc5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -243,6 +243,12 @@ openstack.identity.v3 = federation_domain_list = openstackclient.identity.v3.unscoped_saml:ListAccessibleDomains federation_project_list = openstackclient.identity.v3.unscoped_saml:ListAccessibleProjects + region_create = openstackclient.identity.v3.region:CreateRegion + region_delete = openstackclient.identity.v3.region:DeleteRegion + region_list = openstackclient.identity.v3.region:ListRegion + region_set = openstackclient.identity.v3.region:SetRegion + region_show = openstackclient.identity.v3.region:ShowRegion + request_token_authorize = openstackclient.identity.v3.token:AuthorizeRequestToken request_token_create = openstackclient.identity.v3.token:CreateRequestToken From 39116bf594e780caa924c46465205a110a4c8023 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 18 Nov 2014 09:02:04 -0600 Subject: [PATCH 0263/3494] Fix volume create --image 'volume create --image' should allow an image name to be used. Closes-Bug: #1383333 Change-Id: I996d46db321eef2d75c3d19b480319f8a78c09b3 --- openstackclient/tests/volume/v1/fakes.py | 23 +++ .../tests/volume/v1/test_volume.py | 136 ++++++++++++++++++ openstackclient/volume/v1/volume.py | 18 ++- 3 files changed, 174 insertions(+), 3 deletions(-) diff --git a/openstackclient/tests/volume/v1/fakes.py b/openstackclient/tests/volume/v1/fakes.py index c0ffbd3409..3477819072 100644 --- a/openstackclient/tests/volume/v1/fakes.py +++ b/openstackclient/tests/volume/v1/fakes.py @@ -65,6 +65,24 @@ 'links': extension_links, } +# NOTE(dtroyer): duplicating here the minimum image info needed to test +# volume create --image until circular references can be +# avoided by refactoring the test fakes. + +image_id = 'im1' +image_name = 'graven' + + +IMAGE = { + 'id': image_id, + 'name': image_name, +} + + +class FakeImagev1Client(object): + def __init__(self, **kwargs): + self.images = mock.Mock() + class FakeVolumev1Client(object): def __init__(self, **kwargs): @@ -91,3 +109,8 @@ def setUp(self): endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN, ) + + self.app.client_manager.image = FakeImagev1Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) diff --git a/openstackclient/tests/volume/v1/test_volume.py b/openstackclient/tests/volume/v1/test_volume.py index f020791a4b..cc5aeff80b 100644 --- a/openstackclient/tests/volume/v1/test_volume.py +++ b/openstackclient/tests/volume/v1/test_volume.py @@ -38,6 +38,10 @@ def setUp(self): self.users_mock = self.app.client_manager.identity.users self.users_mock.reset_mock() + # Get a shortcut to the ImageManager Mock + self.images_mock = self.app.client_manager.image.images + self.images_mock.reset_mock() + # TODO(dtroyer): The volume create tests are incomplete, only the minimal # options and the options that require additional processing @@ -389,3 +393,135 @@ def test_volume_create_properties(self): volume_fakes.volume_type, ) self.assertEqual(datalist, data) + + def test_volume_create_image_id(self): + self.images_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.IMAGE), + loaded=True, + ) + + arglist = [ + '--image', volume_fakes.image_id, + '--size', str(volume_fakes.volume_size), + volume_fakes.volume_name, + ] + verifylist = [ + ('image', volume_fakes.image_id), + ('size', volume_fakes.volume_size), + ('name', volume_fakes.volume_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # VolumeManager.create(size, snapshot_id=, source_volid=, + # display_name=, display_description=, + # volume_type=, user_id=, + # project_id=, availability_zone=, + # metadata=, imageRef=) + self.volumes_mock.create.assert_called_with( + volume_fakes.volume_size, + None, + None, + volume_fakes.volume_name, + None, + None, + None, + None, + None, + None, + volume_fakes.image_id, + ) + + collist = ( + 'attach_status', + 'availability_zone', + 'display_description', + 'display_name', + 'id', + 'properties', + 'size', + 'status', + 'type', + ) + self.assertEqual(collist, columns) + datalist = ( + 'detached', + volume_fakes.volume_zone, + volume_fakes.volume_description, + volume_fakes.volume_name, + volume_fakes.volume_id, + volume_fakes.volume_metadata_str, + volume_fakes.volume_size, + '', + volume_fakes.volume_type, + ) + self.assertEqual(datalist, data) + + def test_volume_create_image_name(self): + self.images_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.IMAGE), + loaded=True, + ) + + arglist = [ + '--image', volume_fakes.image_name, + '--size', str(volume_fakes.volume_size), + volume_fakes.volume_name, + ] + verifylist = [ + ('image', volume_fakes.image_name), + ('size', volume_fakes.volume_size), + ('name', volume_fakes.volume_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # VolumeManager.create(size, snapshot_id=, source_volid=, + # display_name=, display_description=, + # volume_type=, user_id=, + # project_id=, availability_zone=, + # metadata=, imageRef=) + self.volumes_mock.create.assert_called_with( + volume_fakes.volume_size, + None, + None, + volume_fakes.volume_name, + None, + None, + None, + None, + None, + None, + volume_fakes.image_id, + ) + + collist = ( + 'attach_status', + 'availability_zone', + 'display_description', + 'display_name', + 'id', + 'properties', + 'size', + 'status', + 'type', + ) + self.assertEqual(collist, columns) + datalist = ( + 'detached', + volume_fakes.volume_zone, + volume_fakes.volume_description, + volume_fakes.volume_name, + volume_fakes.volume_id, + volume_fakes.volume_metadata_str, + volume_fakes.volume_size, + '', + volume_fakes.volume_type, + ) + self.assertEqual(datalist, data) diff --git a/openstackclient/volume/v1/volume.py b/openstackclient/volume/v1/volume.py index 99abac52f7..84c216d320 100644 --- a/openstackclient/volume/v1/volume.py +++ b/openstackclient/volume/v1/volume.py @@ -99,6 +99,7 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity + image_client = self.app.client_manager.image volume_client = self.app.client_manager.volume source_volume = None @@ -111,12 +112,23 @@ def take_action(self, parsed_args): project = None if parsed_args.project: project = utils.find_resource( - identity_client.tenants, parsed_args.project).id + identity_client.tenants, + parsed_args.project, + ).id user = None if parsed_args.user: user = utils.find_resource( - identity_client.users, parsed_args.user).id + identity_client.users, + parsed_args.user, + ).id + + image = None + if parsed_args.image: + image = utils.find_resource( + image_client.images, + parsed_args.image, + ).id volume = volume_client.volumes.create( parsed_args.size, @@ -129,7 +141,7 @@ def take_action(self, parsed_args): project, parsed_args.availability_zone, parsed_args.property, - parsed_args.image + image, ) # Map 'metadata' column to 'properties' volume._info.update( From 6dc128636e6161851272d534d47dfbd422f65161 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 18 Nov 2014 00:16:21 -0500 Subject: [PATCH 0264/3494] Enhance the theming for modules page Also fixes a few small docstring syntax errors Change-Id: I85eb968e32c1191cf5d60d02deff2ab7f3291074 --- .gitignore | 2 ++ doc/ext/__init__.py | 0 doc/ext/apidoc.py | 43 ++++++++++++++++++++++++ doc/source/conf.py | 8 ++++- doc/source/index.rst | 4 +-- openstackclient/api/api.py | 1 + openstackclient/common/commandmanager.py | 2 +- 7 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 doc/ext/__init__.py create mode 100644 doc/ext/apidoc.py diff --git a/.gitignore b/.gitignore index 84079f2f9f..5dc75afa47 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,9 @@ AUTHORS build ChangeLog dist +# Doc related doc/build +doc/source/api/ # Development environment files .project .pydevproject diff --git a/doc/ext/__init__.py b/doc/ext/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/doc/ext/apidoc.py b/doc/ext/apidoc.py new file mode 100644 index 0000000000..5e18385a6d --- /dev/null +++ b/doc/ext/apidoc.py @@ -0,0 +1,43 @@ +# Copyright 2014 OpenStack Foundation +# +# 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 os.path as path + +from sphinx import apidoc + + +# NOTE(blk-u): pbr will run Sphinx multiple times when it generates +# documentation. Once for each builder. To run this extension we use the +# 'builder-inited' hook that fires at the beginning of a Sphinx build. +# We use ``run_already`` to make sure apidocs are only generated once +# even if Sphinx is run multiple times. +run_already = False + + +def run_apidoc(app): + global run_already + if run_already: + return + run_already = True + + package_dir = path.abspath(path.join(app.srcdir, '..', '..', + 'openstackclient')) + source_dir = path.join(app.srcdir, 'api') + apidoc.main(['apidoc', package_dir, '-f', + '-H', 'openstackclient Modules', + '-o', source_dir]) + + +def setup(app): + app.connect('builder-inited', run_apidoc) diff --git a/doc/source/conf.py b/doc/source/conf.py index 7c7a00b313..e805a98767 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -22,6 +22,10 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) +# NOTE(blk-u): Path for our Sphinx extension, remove when +# https://launchpad.net/bugs/1260495 is fixed. +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + # -- General configuration ---------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. @@ -32,7 +36,9 @@ extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', - 'oslosphinx'] + 'oslosphinx', + 'ext.apidoc', + ] # Add any paths that contain templates here, relative to this directory. #templates_path = ['_templates'] diff --git a/doc/source/index.rst b/doc/source/index.rst index 0f92b3f018..a3c6516d30 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -37,8 +37,8 @@ the openstack/python-openstackclient project using `Gerrit`_. .. _Launchpad: https://launchpad.net/python-openstackclient .. _Gerrit: http://wiki.openstack.org/GerritWorkflow -Index -===== +Indices and Tables +================== * :ref:`genindex` * :ref:`modindex` diff --git a/openstackclient/api/api.py b/openstackclient/api/api.py index 72a66e1cb4..67386aaf0b 100644 --- a/openstackclient/api/api.py +++ b/openstackclient/api/api.py @@ -227,6 +227,7 @@ def find_attr( attribute to use for resource search :param string resource: plural of the object resource name; defaults to path + For example: n = find(netclient, 'network', 'networks', 'matrix') """ diff --git a/openstackclient/common/commandmanager.py b/openstackclient/common/commandmanager.py index 2d9575d9d2..b34bf7d6f3 100644 --- a/openstackclient/common/commandmanager.py +++ b/openstackclient/common/commandmanager.py @@ -28,7 +28,7 @@ class CommandManager(cliff.commandmanager.CommandManager): """Add additional functionality to cliff.CommandManager Load additional command groups after initialization - Add *_command_group() methods + Add _command_group() methods """ def __init__(self, namespace, convert_underscores=True): From 254910d3ce34c954551a0827aa8727d6367f48f3 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 17 Nov 2014 16:42:30 -0600 Subject: [PATCH 0265/3494] Begin copying wiki command list here * Sort by command objects * Drop the comparison to the project CLIs * Minor updates to command help to match docs Initially include the cross-API commands to establish the structure and format. Change-Id: I77a7b3c89e088b66aa62941e29ce0b65b532285b --- doc/source/command-list.rst | 9 ++ doc/source/command-objects/extension.rst | 41 ++++++ doc/source/command-objects/limits.rst | 28 ++++ doc/source/command-objects/quota.rst | 157 ++++++++++++++++++++++ doc/source/commands.rst | 14 +- doc/source/index.rst | 1 + doc/source/releases.rst | 2 +- openstackclient/common/extension.py | 12 +- openstackclient/common/quota.py | 2 +- openstackclient/identity/v2_0/endpoint.py | 2 +- 10 files changed, 250 insertions(+), 18 deletions(-) create mode 100644 doc/source/command-list.rst create mode 100644 doc/source/command-objects/extension.rst create mode 100644 doc/source/command-objects/limits.rst create mode 100644 doc/source/command-objects/quota.rst diff --git a/doc/source/command-list.rst b/doc/source/command-list.rst new file mode 100644 index 0000000000..c4045b0406 --- /dev/null +++ b/doc/source/command-list.rst @@ -0,0 +1,9 @@ +============ +Command List +============ + +.. toctree:: + :glob: + :maxdepth: 2 + + command-objects/* diff --git a/doc/source/command-objects/extension.rst b/doc/source/command-objects/extension.rst new file mode 100644 index 0000000000..8f39a62529 --- /dev/null +++ b/doc/source/command-objects/extension.rst @@ -0,0 +1,41 @@ +========= +extension +========= + +Many OpenStack server APIs include API extensions that enable +additional functionality. + +extension list +-------------- + +List API extensions + +.. program:: extension list +.. code:: bash + + os extension list + [--compute] + [--identity] + [--network] + [--volume] + [--long] + +.. option:: --compute + + List extensions for the Compute API + +.. option:: --identity + + List extensions for the Identity API + +.. option:: --network + + List extensions for the Network API + +.. option:: --volume + + List extensions for the Volume API + +.. option:: --long + + List additional fields in output diff --git a/doc/source/command-objects/limits.rst b/doc/source/command-objects/limits.rst new file mode 100644 index 0000000000..ac388e0f56 --- /dev/null +++ b/doc/source/command-objects/limits.rst @@ -0,0 +1,28 @@ +====== +limits +====== + +The Compute and Volume APIs have resource usage limits. + +limits show +----------- + +Show compute and volume limits + +.. program:: limits show +.. code:: bash + + os limits show + --absolute [--reserved] | --rate + +.. option:: --absolute + + Show absolute limits + +.. option:: --rate + + Show rate limits + +.. option:: --reserved + + Include reservations count [only valid with :option:`--absolute`] diff --git a/doc/source/command-objects/quota.rst b/doc/source/command-objects/quota.rst new file mode 100644 index 0000000000..ba6712c083 --- /dev/null +++ b/doc/source/command-objects/quota.rst @@ -0,0 +1,157 @@ +===== +quota +===== + +Resource quotas appear in multiple APIs, OpenStackClient presents them as a single object with multiple properties. + +quota set +--------- + +Set quotas for project + +.. program:: quota set +.. code:: bash + + os quota set + # Compute settings + [--cores ] + [--fixed-ips ] + [--floating-ips ] + [--injected-file-size ] + [--injected-files ] + [--instances ] + [--key-pairs ] + [--properties ] + [--ram ] + + # Volume settings + [--gigabytes ] + [--snapshots ] + [--volumes ] + + + +Set quotas for class + +.. code:: bash + + os quota set + --class + # Compute settings + [--cores ] + [--fixed-ips ] + [--floating-ips ] + [--injected-file-size ] + [--injected-files ] + [--instances ] + [--key-pairs ] + [--properties ] + [--ram ] + + # Volume settings + [--gigabytes ] + [--snapshots ] + [--volumes ] + + + +.. option:: --class + + Set quotas for ```` + +.. option:: --properties + + New value for the properties quota + +.. option:: --ram + + New value for the ram quota + +.. option:: --secgroup-rules + + New value for the secgroup-rules quota + +.. option:: --instances + + New value for the instances quota + +.. option:: --key-pairs + + New value for the key-pairs quota + +.. option:: --fixed-ips + + New value for the fixed-ips quota + +.. option:: --secgroups + + New value for the secgroups quota + +.. option:: --injected-file-size + + New value for the injected-file-size quota + +.. option:: --floating-ips + + New value for the floating-ips quota + +.. option:: --injected-files + + New value for the injected-files quota + +.. option:: --cores + + New value for the cores quota + +.. option:: --injected-path-size + + New value for the injected-path-size quota + +.. option:: --gigabytes + + New value for the gigabytes quota + +.. option:: --volumes + + New value for the volumes quota + +.. option:: --snapshots + + New value for the snapshots quota + +quota show +---------- + +Show quotas for project + +.. program:: quota show +.. code:: bash + + os quota show + [--default] + + + +.. option:: --default + + Show default quotas for :ref:`\ ` + +.. _quota_show-project: +.. describe:: + + Show quotas for class + +.. code:: bash + + os quota show + --class + + +.. option:: --class + + Show quotas for :ref:`\ ` + +.. _quota_show-class: +.. describe:: + + Class to show \ No newline at end of file diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 8857f3d55d..37c62e8ec3 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -1,8 +1,4 @@ -======== -Commands -======== - - +================= Command Structure ================= @@ -83,7 +79,7 @@ referring to both Compute and Volume quotas. * ``credential``: Identity - specific to identity providers * ``domain``: Identity - a grouping of projects * ``endpoint``: Identity - the base URL used to contact a specific service -* ``extension``: Compute, Identity, Volume - additional APIs available +* ``extension``: (**Compute**, **Identity**, **Volume**) OpenStack server API extensions * ``flavor``: Compute - pre-defined configurations of servers: ram, root disk, etc * ``group``: Identity - a grouping of users * ``host``: Compute - the physical computer running a hypervisor @@ -93,13 +89,13 @@ referring to both Compute and Volume quotas. * ``ip fixed``: Compute, Network - an internal IP address assigned to a server * ``ip floating``: Compute, Network - a public IP address that can be mapped to a server * ``keypair``: Compute - an SSH public key -* ``limits``: Compute, Volume - resource usage limits +* ``limits``: (**Compute**, **Volume**) resource usage limits * ``module``: internal - installed Python modules in the OSC process * ``network``: Network - a virtual network for connecting servers and other resources * ``object``: Object Store - a single file in the Object Store * ``policy``: Identity - determines authorization * ``project``: Identity - the owner of a group of resources -* ``quota``: Compute, Volume - limit on resource usage +* ``quota``: (**Compute**, **Volume**) resource usage restrictions * ``request token``: Identity - temporary OAuth-based token * ``role``: Identity - a policy object used to determine authorization * ``security group``: Compute, Network - groups of network access rules @@ -149,7 +145,7 @@ Those actions with an opposite action are noted in parens if applicable. Implementation -============== +-------------- The command structure is designed to support seamless addition of plugin command modules via ``setuptools`` entry points. The plugin commands must diff --git a/doc/source/index.rst b/doc/source/index.rst index b6145a86b6..2edbb35d57 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -12,6 +12,7 @@ Contents: :maxdepth: 1 releases + command-list commands plugins authentication diff --git a/doc/source/releases.rst b/doc/source/releases.rst index 89b4ad111e..909c362eaf 100644 --- a/doc/source/releases.rst +++ b/doc/source/releases.rst @@ -26,7 +26,7 @@ Release Notes * add ``object create`` and ``object delete`` commands * add initial support for global ``--timing`` options (similar to nova CLI) * complete Python 3 compatibility -* fix ``server resize` command +* fix ``server resize`` command * add authentication via ``--os-trust-id`` for Identity v3 * Add initial support for Network API, ``network create|delete|list|show`` diff --git a/openstackclient/common/extension.py b/openstackclient/common/extension.py index be7426daa0..dad7ed6285 100644 --- a/openstackclient/common/extension.py +++ b/openstackclient/common/extension.py @@ -24,7 +24,7 @@ class ListExtension(lister.Lister): - """List extension command""" + """List API extensions""" log = logging.getLogger(__name__ + '.ListExtension') @@ -40,11 +40,6 @@ def get_parser(self, prog_name): action='store_true', default=False, help='List extensions for the Identity API') - parser.add_argument( - '--long', - action='store_true', - default=False, - help='List additional fields in output') parser.add_argument( '--network', action='store_true', @@ -55,6 +50,11 @@ def get_parser(self, prog_name): action='store_true', default=False, help='List extensions for the Volume API') + parser.add_argument( + '--long', + action='store_true', + default=False, + help='List additional fields in output') return parser def take_action(self, parsed_args): diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index e011fd3619..edf4ffdb98 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -72,7 +72,7 @@ def get_parser(self, prog_name): COMPUTE_QUOTAS.items(), VOLUME_QUOTAS.items()): parser.add_argument( '--%s' % v, - metavar='' % v, + metavar='<%s>' % v, type=int, help='New value for the %s quota' % v, ) diff --git a/openstackclient/identity/v2_0/endpoint.py b/openstackclient/identity/v2_0/endpoint.py index f7d7883109..c5189b62e7 100644 --- a/openstackclient/identity/v2_0/endpoint.py +++ b/openstackclient/identity/v2_0/endpoint.py @@ -28,7 +28,7 @@ class CreateEndpoint(show.ShowOne): - """Create endpoint command""" + """Create endpoint""" log = logging.getLogger(__name__ + '.CreateEndpoint') From 9eb30efbf3ee9105520e857fbd16363523ae44ee Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 17 Nov 2014 17:52:37 -0600 Subject: [PATCH 0266/3494] Command object docs: aggregate, console *, keypair aggregate console log console url keypair Change-Id: Iec9b8404ed5febd061a5dfd674b76aaa8aba67bc --- doc/source/command-objects/aggregate.rst | 150 +++++++++++++++++++++ doc/source/command-objects/console-log.rst | 25 ++++ doc/source/command-objects/console-url.rst | 33 +++++ doc/source/command-objects/keypair.rst | 71 ++++++++++ doc/source/commands.rst | 8 +- openstackclient/compute/v2/aggregate.py | 22 +-- openstackclient/compute/v2/console.py | 4 +- openstackclient/compute/v2/keypair.py | 20 +-- 8 files changed, 306 insertions(+), 27 deletions(-) create mode 100644 doc/source/command-objects/aggregate.rst create mode 100644 doc/source/command-objects/console-log.rst create mode 100644 doc/source/command-objects/console-url.rst create mode 100644 doc/source/command-objects/keypair.rst diff --git a/doc/source/command-objects/aggregate.rst b/doc/source/command-objects/aggregate.rst new file mode 100644 index 0000000000..474d811f88 --- /dev/null +++ b/doc/source/command-objects/aggregate.rst @@ -0,0 +1,150 @@ +========= +aggregate +========= + +Server aggregates provide a mechanism to group servers according to certain +criteria. + +aggregate add host +------------------ + +Add host to aggregate + +.. program aggregate add host +.. code:: bash + + os aggregate add host + + + +.. _aggregate_add_host-aggregate: +.. describe:: + + Aggregate (name or ID) + +.. describe:: + + Host to add to :ref:`\ ` + +aggregate create +---------------- + +Create a new aggregate + +.. program aggregate create +.. code:: bash + + os aggregate create + [--zone ] + [--property ] + + +.. option:: --zone + + Availability zone name + +.. option:: --property + + Property to add to this aggregate (repeat option to set multiple properties) + +.. describe:: + + New aggregate name + +aggregate delete +---------------- + +Delete an existing aggregate + +.. program aggregate delete +.. code:: bash + + os aggregate delete + + +.. describe:: + + Aggregate to delete (name or ID) + +aggregate list +-------------- + +List all aggregates + +.. program aggregate list +.. code:: bash + + os aggregate list + [--long] + +.. option:: --long + + List additional fields in output + +aggregate remove host +--------------------- + +Remove host from aggregate + +.. program aggregate remove host +.. code:: bash + + os aggregate remove host + + + +.. _aggregate_remove_host-aggregate: +.. describe:: + + Aggregate (name or ID) + +.. option:: + + Host to remove from :ref:`\ ` + +aggregate set +------------- + +Set aggregate properties + +.. program aggregate set +.. code:: bash + + os aggregate set + [--name ] + [--zone ] + [--property ] + + +.. option:: --name + + Set aggregate name + +.. option:: --zone + + Set availability zone name + +.. option:: --property + + Property to set on :ref:`\ ` + (repeat option to set multiple properties) + +.. _aggregate_set-aggregate: +.. describe:: + + Aggregate to modify (name or ID) + +aggregate show +-------------- + +Show a specific aggregate + +.. program aggregate show +.. code:: bash + + os aggregate show + + +.. describe:: + + Aggregate to show (name or ID) diff --git a/doc/source/command-objects/console-log.rst b/doc/source/command-objects/console-log.rst new file mode 100644 index 0000000000..8e56a07382 --- /dev/null +++ b/doc/source/command-objects/console-log.rst @@ -0,0 +1,25 @@ +=========== +console log +=========== + +Server console text dump + +console log show +---------------- + +Show server's console output + +.. program:: console log show +.. code:: bash + + os console log show + [--lines ] + + +.. option:: --lines + + Number of lines to display from the end of the log (default=all) + +.. describe:: + + Server to show log console log (name or ID) diff --git a/doc/source/command-objects/console-url.rst b/doc/source/command-objects/console-url.rst new file mode 100644 index 0000000000..45a0a52717 --- /dev/null +++ b/doc/source/command-objects/console-url.rst @@ -0,0 +1,33 @@ +=========== +console url +=========== + +Server remote console URL + +console url show +---------------- + +Show server's remote console URL + +.. program:: console url show +.. code:: bash + + os console url show + [--novnc | --xvpvnc | --spice] + + +.. option:: --novnc + + Show noVNC console URL (default) + +.. option:: --xvpvnc + + Show xpvnc console URL + +.. option:: --spice + + Show SPICE console URL + +.. describe:: + + Server to show URL (name or ID) diff --git a/doc/source/command-objects/keypair.rst b/doc/source/command-objects/keypair.rst new file mode 100644 index 0000000000..9ba0ee8f04 --- /dev/null +++ b/doc/source/command-objects/keypair.rst @@ -0,0 +1,71 @@ +======= +keypair +======= + +The badly named keypair is really the public key of an OpenSSH key pair to be +used for access to created servers. + +keypair create +-------------- + +Create new public key + +.. program keypair create +.. code:: bash + + os keypair create + [--public-key ] + + +.. option:: --public-key + + Filename for public key to add + +.. describe:: + + New public key name + +keypair delete +-------------- + +Delete a public key + +.. program keypair delete +.. code:: bash + + os keypair delete + + +.. describe:: + + Public key to delete + +keypair list +------------ + +List public key fingerprints + +.. program keypair list +.. code:: bash + + os keypair list + +keypair show +------------ + +Show public key details + +.. program keypair show +.. code:: bash + + os keypair show + [--public-key] + + +.. option:: --public-key + + Show only bare public key + +.. describe:: + + Public key to show diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 37c62e8ec3..506fbfc28f 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -70,10 +70,10 @@ the API resources will be merged, as in the ``quota`` object that has options referring to both Compute and Volume quotas. * ``access token``: Identity - long-lived OAuth-based token -* ``aggregate``: Compute - a grouping of servers +* ``aggregate``: (**Compute**) a grouping of servers * ``backup``: Volume - a volume copy -* ``console log``: Compute - a text dump of a server's console -* ``console url``: Compute - a URL to a server's remote console +* ``console log``: (**Compute**) server console text dump +* ``console url``: (**Compute**) server remote console URL * ``consumer``: Identity - OAuth-based delegatee * ``container``: Object Store - a grouping of objects * ``credential``: Identity - specific to identity providers @@ -88,7 +88,7 @@ referring to both Compute and Volume quotas. * ``image``: Image - a disk image * ``ip fixed``: Compute, Network - an internal IP address assigned to a server * ``ip floating``: Compute, Network - a public IP address that can be mapped to a server -* ``keypair``: Compute - an SSH public key +* ``keypair``: (**Compute**) an SSH public key * ``limits``: (**Compute**, **Volume**) resource usage limits * ``module``: internal - installed Python modules in the OSC process * ``network``: Network - a virtual network for connecting servers and other resources diff --git a/openstackclient/compute/v2/aggregate.py b/openstackclient/compute/v2/aggregate.py index 8fff4e6fcf..bfc2b115aa 100644 --- a/openstackclient/compute/v2/aggregate.py +++ b/openstackclient/compute/v2/aggregate.py @@ -37,12 +37,12 @@ def get_parser(self, prog_name): parser.add_argument( 'aggregate', metavar='', - help='Name or ID of aggregate', + help='Aggregate (name or ID)', ) parser.add_argument( 'host', metavar='', - help='Host to add to aggregate', + help='Host to add to ', ) return parser @@ -119,7 +119,7 @@ def get_parser(self, prog_name): parser.add_argument( 'aggregate', metavar='', - help='Name or ID of aggregate to delete', + help='Aggregate to delete (name or ID)', ) return parser @@ -197,12 +197,12 @@ def get_parser(self, prog_name): parser.add_argument( 'aggregate', metavar='', - help='Name or ID of aggregate', + help='Aggregate (name or ID)', ) parser.add_argument( 'host', metavar='', - help='Host to remove from aggregate', + help='Host to remove from ', ) return parser @@ -235,23 +235,23 @@ def get_parser(self, prog_name): parser.add_argument( 'aggregate', metavar='', - help='Name or ID of aggregate to display', + help='Aggregate to modify (name or ID)', ) parser.add_argument( '--name', - metavar='', - help='New aggregate name', + metavar='', + help='Set aggregate name', ) parser.add_argument( "--zone", metavar="", - help="Availability zone name", + help="Set availability zone name", ) parser.add_argument( "--property", metavar="", action=parseractions.KeyValueAction, - help='Property to add/change for this aggregate ' + help='Property to set on ' '(repeat option to set multiple properties)', ) return parser @@ -299,7 +299,7 @@ def get_parser(self, prog_name): parser.add_argument( 'aggregate', metavar='', - help='Name or ID of aggregate to display', + help='Aggregate to show (name or ID)', ) return parser diff --git a/openstackclient/compute/v2/console.py b/openstackclient/compute/v2/console.py index 8206f3024b..082a3a0c0f 100644 --- a/openstackclient/compute/v2/console.py +++ b/openstackclient/compute/v2/console.py @@ -35,7 +35,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help='Server to show console log (name or ID)', ) parser.add_argument( '--lines', @@ -76,7 +76,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help='Server (name or ID)', + help='Server to show URL (name or ID)', ) type_group = parser.add_mutually_exclusive_group() type_group.add_argument( diff --git a/openstackclient/compute/v2/keypair.py b/openstackclient/compute/v2/keypair.py index 22c07ef7f3..6a158d86ff 100644 --- a/openstackclient/compute/v2/keypair.py +++ b/openstackclient/compute/v2/keypair.py @@ -29,7 +29,7 @@ class CreateKeypair(show.ShowOne): - """Create new keypair""" + """Create new public key""" log = logging.getLogger(__name__ + '.CreateKeypair') @@ -38,7 +38,7 @@ def get_parser(self, prog_name): parser.add_argument( 'name', metavar='', - help='New keypair name', + help='New public key name', ) parser.add_argument( '--public-key', @@ -80,7 +80,7 @@ def take_action(self, parsed_args): class DeleteKeypair(command.Command): - """Delete a keypair""" + """Delete a public key""" log = logging.getLogger(__name__ + '.DeleteKeypair') @@ -88,8 +88,8 @@ def get_parser(self, prog_name): parser = super(DeleteKeypair, self).get_parser(prog_name) parser.add_argument( 'name', - metavar='', - help='Name of keypair to delete', + metavar='', + help='Public key to delete', ) return parser @@ -101,7 +101,7 @@ def take_action(self, parsed_args): class ListKeypair(lister.Lister): - """List keypairs""" + """List public key fingerprints""" log = logging.getLogger(__name__ + ".ListKeypair") @@ -121,7 +121,7 @@ def take_action(self, parsed_args): class ShowKeypair(show.ShowOne): - """Show keypair details""" + """Show public key details""" log = logging.getLogger(__name__ + '.ShowKeypair') @@ -129,14 +129,14 @@ def get_parser(self, prog_name): parser = super(ShowKeypair, self).get_parser(prog_name) parser.add_argument( 'name', - metavar='', - help='Name of keypair to display', + metavar='', + help='Public key to show', ) parser.add_argument( '--public-key', action='store_true', default=False, - help='Include public key in output', + help='Show only bare public key', ) return parser From 3ad16897bb6c352790c8d928dfb016c2e8cb59c4 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 21 Nov 2014 18:33:29 +0000 Subject: [PATCH 0267/3494] Updated from global requirements Change-Id: I2ae7af05f2052d8a8878e6477c8746cfdd1b74fa --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 4038b1bb0a..ac3e715093 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -12,5 +12,5 @@ oslotest>=1.2.0 # Apache-2.0 requests-mock>=0.5.1 # Apache-2.0 sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 testrepository>=0.0.18 -testtools>=0.9.36,!=1.2.0,!=1.4.0 +testtools>=0.9.36,!=1.2.0 WebOb>=1.2.3 From 04d30c1855f1229d986c7ad3bdae43a2b2d38990 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 18 Nov 2014 15:11:32 -0600 Subject: [PATCH 0268/3494] Command object docs: project, role, user project role user user role Change-Id: I445e09a3ffb69114912ae562a9285963a636bfd1 --- doc/source/command-objects/project.rst | 155 +++++++++++++++++ doc/source/command-objects/role.rst | 180 ++++++++++++++++++++ doc/source/command-objects/user-role.rst | 25 +++ doc/source/command-objects/user.rst | 204 +++++++++++++++++++++++ doc/source/commands.rst | 7 +- openstackclient/identity/v2_0/project.py | 22 +-- openstackclient/identity/v2_0/role.py | 37 ++-- openstackclient/identity/v2_0/user.py | 40 ++--- openstackclient/identity/v3/project.py | 38 ++--- openstackclient/identity/v3/role.py | 80 ++++----- openstackclient/identity/v3/user.py | 72 ++++---- 11 files changed, 718 insertions(+), 142 deletions(-) create mode 100644 doc/source/command-objects/project.rst create mode 100644 doc/source/command-objects/role.rst create mode 100644 doc/source/command-objects/user-role.rst create mode 100644 doc/source/command-objects/user.rst diff --git a/doc/source/command-objects/project.rst b/doc/source/command-objects/project.rst new file mode 100644 index 0000000000..ba741d1dc1 --- /dev/null +++ b/doc/source/command-objects/project.rst @@ -0,0 +1,155 @@ +======= +project +======= + +Identity v2, v3 + +project create +-------------- + +Create new project + +.. program:: project create +.. code:: bash + + os project create + [--domain ] + [--description ] + [--enable | --disable] + [--property ] + + +.. option:: --domain + + Domain owning the project (name or ID) + + .. versionadded:: 3 + +.. option:: --description + + Project description + +.. option:: --enable + + Enable project (default) + +.. option:: --disable + + Disable project + +.. option:: --property + + Add a property to :ref:`\ ` + (repeat option to set multiple properties) + +.. _project_create-name: +.. describe:: + + New project name + +project delete +-------------- + +Delete an existing project + +.. program:: project delete +.. code:: bash + + os project delete + + +.. _project_delete-project: +.. describe:: + + Project to delete (name or ID) + +project list +------------ + +List projects + +.. program:: project list +.. code:: bash + + os project list + [--domain ] + [--long] + +.. option:: --domain + + Filter projects by :option:`\ <--domain>` (name or ID) + + .. versionadded:: 3 + +.. option:: --long + + List additional fields in output + +project set +----------- + +Set project properties + +.. program:: project set +.. code:: bash + + os project set + [--name ] + [--domain ] + [--description ] + [--enable | --disable] + [--property ] + + +.. option:: --name + + Set project name + +.. option:: --domain + + Set domain owning :ref:`\ ` (name or ID) + + .. versionadded:: 3 + +.. option:: --description + + Set project description + +.. option:: --enable + + Enable project (default) + +.. option:: --disable + + Disable project + +.. option:: --property + + Set a property on :ref:`\ ` + (repeat option to set multiple properties) + +.. _project_set-project: +.. describe:: + + Project to modify (name or ID) + +project show +------------ + +.. program:: project show +.. code:: bash + + os project show + [--domain ] + + +.. option:: --domain + + Domain owning :ref:`\ ` (name or ID) + + .. versionadded:: 3 + +.. _project_show-project: +.. describe:: + + Project to show (name or ID) diff --git a/doc/source/command-objects/role.rst b/doc/source/command-objects/role.rst new file mode 100644 index 0000000000..1cc80d7d98 --- /dev/null +++ b/doc/source/command-objects/role.rst @@ -0,0 +1,180 @@ +==== +role +==== + +Identity v2, v3 + +role add +-------- + +Add role to a user or group in a project or domain + +.. program:: role add +.. code:: bash + + os role add + --domain | --project + --user | --group + + +.. option:: --domain + + Include `` (name or ID) + + .. versionadded:: 3 + +.. option:: --project + + Include `` (name or ID) + +.. option:: --user + + Include `` (name or ID) + +.. option:: --group + + Include `` (name or ID) + + .. versionadded:: 3 + +.. describe:: + + Role to add to ``:`` (name or ID) + +role create +----------- + +Create new role + +.. program:: role create +.. code:: bash + + os role create + + +.. describe:: + + New role name + +role delete +----------- + +Delete an existing role + +.. program:: role delete +.. code:: bash + + os role delete + + +.. option:: + + Role to delete (name or ID) + +role list +--------- + +List roles + +.. program:: role list +.. code:: bash + + os role list + [--domain | --project | --group ] + +.. option:: --domain + + Filter roles by (name or ID) + + .. versionadded:: 3 + +.. option:: --project + + Filter roles by (name or ID) + + .. versionadded:: 3 + +.. option:: --user + + Filter roles by (name or ID) + + .. versionadded:: 3 + +.. option:: --group + + Filter roles by (name or ID) + + .. versionadded:: 3 + +role remove +----------- + +Remove role from domain/project : user/group + +.. program:: role remove +.. code:: bash + + os role remove + [--domain | --project | --group ] + + +.. option:: --domain + + Include `` (name or ID) + + .. versionadded:: 3 + +.. option:: --project + + Include `` (name or ID) + +.. option:: --user + + Include `` (name or ID) + +.. option:: --group + + Include `` (name or ID) + + .. versionadded:: 3 + +.. describe:: + + Role to remove from ``:`` (name or ID) + +role set +-------- + +Set role properties + +.. versionadded:: 3 + +.. program:: role set +.. code:: bash + + os role set + [--name ] + + +.. option:: --name + + Set role name + +.. describe:: + + Role to modify (name or ID) + +role show +--------- + +.. program:: role show +.. code:: bash + + os role show + + +.. describe:: + + Role to show (name or ID) diff --git a/doc/source/command-objects/user-role.rst b/doc/source/command-objects/user-role.rst new file mode 100644 index 0000000000..a25e90ff8f --- /dev/null +++ b/doc/source/command-objects/user-role.rst @@ -0,0 +1,25 @@ +========= +user role +========= + +user role list +-------------- + +List user-role assignments + +*Removed in version 3.* + +.. program:: user role list +.. code:: bash + + os user role list + [--project ] + [] + +.. option:: --project + + Filter users by `` (name or ID) + +.. describe:: + + User to list (name or ID) diff --git a/doc/source/command-objects/user.rst b/doc/source/command-objects/user.rst new file mode 100644 index 0000000000..53becf2799 --- /dev/null +++ b/doc/source/command-objects/user.rst @@ -0,0 +1,204 @@ +==== +user +==== + +Identity v2, v3 + +user create +----------- + +Create new user + +.. program:: user create +.. code:: bash + + os user create + [--domain ] + [--project ] + [--password ] + [--password-prompt] + [--email ] + [--description ] + [--enable | --disable] + [--or-show] + + +.. option:: --domain + + Default domain (name or ID) + + .. versionadded:: 3 + +.. option:: --project + + Default project (name or ID) + +.. option:: --password + + Set user password + +.. option:: --password-prompt + + Prompt interactively for password + +.. option:: --email + + Set user email address + +.. option:: --description + + User description + + .. versionadded:: 3 + +.. option:: --enable + + Enable user (default) + +.. option:: --disable + + Disable user + +.. option:: --or-show + + Return existing user + + If the username already exist return the existing user data and do not fail. + +.. describe:: + + New user name + +user delete +----------- + +Delete user + +.. program:: user delete +.. code:: bash + + os user delete + + +.. describe:: + + User to delete (name or ID) + +user list +--------- + +List users + +.. program:: user list +.. code:: bash + + os user list + [--domain ] + [--project ] + [--group ] + [--long] + +.. option:: --domain + + Filter users by `` (name or ID) + + .. versionadded:: 3 + +.. option:: --project + + Filter users by `` (name or ID) + + *Removed in version 3.* + +.. option:: --group + + Filter users by `` membership (name or ID) + + .. versionadded:: 3 + +.. option:: --long + + List additional fields in output + +user set +-------- + +Set user properties + +.. program:: user set +.. code:: bash + + os user set + [--name ] + [--domain ] + [--project ] + [--password ] + [--email ] + [--description ] + [--enable|--disable] + + +.. option:: --name + + Set user name + +.. option:: --domain + + Set default domain (name or ID) + + .. versionadded:: 3 + +.. option:: --project + + Set default project (name or ID) + +.. option:: --password + + Set user password + +.. option:: --password-prompt + + Prompt interactively for password + +.. option:: --email + + Set user email address + +.. option:: --description + + Set user description + + .. versionadded:: 3 + +.. option:: --enable + + Enable user (default) + +.. option:: --disable + + Disable user + +.. describe:: + + User to modify (name or ID) + +user show +--------- + +.. program:: user show +.. code:: bash + + os user show + [--domain ] + + +.. option:: --domain + + Domain owning :ref:`\ ` (name or ID) + + .. versionadded:: 3 + +.. _user_show-user: +.. describe:: + + User to show (name or ID) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 506fbfc28f..250a8039d8 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -94,7 +94,7 @@ referring to both Compute and Volume quotas. * ``network``: Network - a virtual network for connecting servers and other resources * ``object``: Object Store - a single file in the Object Store * ``policy``: Identity - determines authorization -* ``project``: Identity - the owner of a group of resources +* ``project``: (**Identity**) owns a group of resources * ``quota``: (**Compute**, **Volume**) resource usage restrictions * ``request token``: Identity - temporary OAuth-based token * ``role``: Identity - a policy object used to determine authorization @@ -103,8 +103,9 @@ referring to both Compute and Volume quotas. * ``server``: Compute - a virtual machine instance * ``service``: Identity - a cloud service * ``snapshot``: Volume - a point-in-time copy of a volume -* ``token``: Identity - the magic text used to determine access -* ``user``: Identity - individuals using cloud resources +* ``token``: (**Identity**) a bearer token managed by Identity service +* ``user``: (**Identity**) individual cloud resources users +* ``user role``: (**Identity**) roles assigned to a user * ``volume``: Volume - block volumes * ``volume type``: Volume - deployment-specific types of volumes available diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index 2d66b400d4..df759ce6a8 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -42,8 +42,8 @@ def get_parser(self, prog_name): ) parser.add_argument( '--description', - metavar='', - help=_('New project description'), + metavar='', + help=_('Project description'), ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( @@ -60,7 +60,7 @@ def get_parser(self, prog_name): '--property', metavar='', action=parseractions.KeyValueAction, - help=_('Property to add for this project ' + help=_('Add a property to ' '(repeat option to set multiple properties)'), ) parser.add_argument( @@ -104,7 +104,7 @@ def take_action(self, parsed_args): class DeleteProject(command.Command): - """Delete project""" + """Delete an existing project""" log = logging.getLogger(__name__ + '.DeleteProject') @@ -169,17 +169,17 @@ def get_parser(self, prog_name): parser.add_argument( 'project', metavar='', - help=_('Project to change (name or ID)'), + help=_('Project to modify (name or ID)'), ) parser.add_argument( '--name', - metavar='', - help=_('New project name'), + metavar='', + help=_('Set project name'), ) parser.add_argument( '--description', - metavar='', - help=_('New project description'), + metavar='', + help=_('Set project description'), ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( @@ -196,7 +196,7 @@ def get_parser(self, prog_name): '--property', metavar='', action=parseractions.KeyValueAction, - help=_('Property to add for this project ' + help=_('Set a project property ' '(repeat option to set multiple properties)'), ) return parser @@ -249,7 +249,7 @@ def get_parser(self, prog_name): parser.add_argument( 'project', metavar='', - help=_('Project to display (name or ID)')) + help=_('Project to show (name or ID)')) return parser def take_action(self, parsed_args): diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index df69e857ad..cec95095cc 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -38,17 +38,20 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help=_('Role name or ID to add to user')) + help=_('Role to add to : (name or ID)'), + ) parser.add_argument( '--project', metavar='', required=True, - help=_('Include project (name or ID)')) + help=_('Include (name or ID)'), + ) parser.add_argument( '--user', metavar='', required=True, - help=_('Name or ID of user to include')) + help=_('Include (name or ID)'), + ) return parser def take_action(self, parsed_args): @@ -80,8 +83,9 @@ def get_parser(self, prog_name): parser = super(CreateRole, self).get_parser(prog_name) parser.add_argument( 'role_name', - metavar='', - help=_('New role name')) + metavar='', + help=_('New role name'), + ) parser.add_argument( '--or-show', action='store_true', @@ -110,7 +114,7 @@ def take_action(self, parsed_args): class DeleteRole(command.Command): - """Delete existing role""" + """Delete an existing role""" log = logging.getLogger(__name__ + '.DeleteRole') @@ -119,7 +123,8 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help=_('Name or ID of role to delete')) + help=_('Role to delete (name or ID)'), + ) return parser def take_action(self, parsed_args): @@ -162,11 +167,13 @@ def get_parser(self, prog_name): 'user', metavar='', nargs='?', - help=_('Name or ID of user to include')) + help=_('User to list (name or ID)'), + ) parser.add_argument( '--project', metavar='', - help=_('Include project (name or ID)')) + help=_('Filter users by (name or ID)'), + ) return parser def take_action(self, parsed_args): @@ -227,17 +234,20 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help=_('Role name or ID to remove from user')) + help=_('Role to remove from : (name or ID)'), + ) parser.add_argument( '--project', metavar='', required=True, - help=_('Project to include (name or ID)')) + help=_('Include (name or ID)'), + ) parser.add_argument( '--user', metavar='', required=True, - help=_('Name or ID of user')) + help=_('Include (name or ID)'), + ) return parser def take_action(self, parsed_args): @@ -265,7 +275,8 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help=_('Name or ID of role to display')) + help=_('Role to show (name or ID)'), + ) return parser def take_action(self, parsed_args): diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index 2ebcba188e..955e19e770 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -36,13 +36,18 @@ def get_parser(self, prog_name): parser = super(CreateUser, self).get_parser(prog_name) parser.add_argument( 'name', - metavar='', + metavar='', help=_('New user name'), ) + parser.add_argument( + '--project', + metavar='', + help=_('Default project (name or ID)'), + ) parser.add_argument( '--password', - metavar='', - help=_('New user password'), + metavar='', + help=_('Set user password'), ) parser.add_argument( '--password-prompt', @@ -52,13 +57,8 @@ def get_parser(self, prog_name): ) parser.add_argument( '--email', - metavar='', - help=_('New user email address'), - ) - parser.add_argument( - '--project', - metavar='', - help=_('Set default project (name or ID)'), + metavar='', + help=_('Set user email address'), ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( @@ -258,13 +258,18 @@ def get_parser(self, prog_name): ) parser.add_argument( '--name', - metavar='', - help=_('New user name'), + metavar='', + help=_('Set user name'), + ) + parser.add_argument( + '--project', + metavar='', + help=_('Set default project (name or ID)'), ) parser.add_argument( '--password', metavar='', - help=_('New user password'), + help=_('Set user password'), ) parser.add_argument( '--password-prompt', @@ -274,13 +279,8 @@ def get_parser(self, prog_name): ) parser.add_argument( '--email', - metavar='', - help=_('New user email address'), - ) - parser.add_argument( - '--project', - metavar='', - help=_('New default project (name or ID)'), + metavar='', + help=_('Set user email address'), ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 4fcf81278f..3b0e92fd8c 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -43,13 +43,13 @@ def get_parser(self, prog_name): ) parser.add_argument( '--domain', - metavar='', + metavar='', help='Domain owning the project (name or ID)', ) parser.add_argument( '--description', - metavar='', - help='New project description', + metavar='', + help='Project description', ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( @@ -66,7 +66,7 @@ def get_parser(self, prog_name): '--property', metavar='', action=parseractions.KeyValueAction, - help='Property to add for this project ' + help='Add a property to ' '(repeat option to set multiple properties)', ) parser.add_argument( @@ -115,7 +115,7 @@ def take_action(self, parsed_args): class DeleteProject(command.Command): - """Delete project""" + """Delete an existing project""" log = logging.getLogger(__name__ + '.DeleteProject') @@ -148,17 +148,17 @@ class ListProject(lister.Lister): def get_parser(self, prog_name): parser = super(ListProject, self).get_parser(prog_name) + parser.add_argument( + '--domain', + metavar='', + help='Filter projects by (name or ID)', + ) parser.add_argument( '--long', action='store_true', default=False, help='List additional fields in output', ) - parser.add_argument( - '--domain', - metavar='', - help='Filter by a specific domain (name or ID)', - ) return parser def take_action(self, parsed_args): @@ -190,22 +190,22 @@ def get_parser(self, prog_name): parser.add_argument( 'project', metavar='', - help='Project to change (name or ID)', + help='Project to modify (name or ID)', ) parser.add_argument( '--name', - metavar='', - help='New project name', + metavar='', + help='Set project name', ) parser.add_argument( '--domain', - metavar='', - help='New domain owning the project (name or ID)', + metavar='', + help='Set domain owning (name or ID)', ) parser.add_argument( '--description', - metavar='', - help='New project description', + metavar='', + help='Set project description', ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( @@ -222,7 +222,7 @@ def get_parser(self, prog_name): '--property', metavar='', action=parseractions.KeyValueAction, - help='Property to add for this project ' + help='Set a property on ' '(repeat option to set multiple properties)', ) return parser @@ -278,7 +278,7 @@ def get_parser(self, prog_name): parser.add_argument( '--domain', metavar='', - help='Domain where project resides (name or ID)', + help='Domain owning (name or ID)', ) return parser diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index 7801ca6563..017ffd769f 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -38,29 +38,29 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help='Name or ID of role to add', - ) - user_or_group = parser.add_mutually_exclusive_group() - user_or_group.add_argument( - '--user', - metavar='', - help='Name or ID of user to add a role', - ) - user_or_group.add_argument( - '--group', - metavar='', - help='Name or ID of group to add a role', + help='Role to add to (name or ID)', ) domain_or_project = parser.add_mutually_exclusive_group() domain_or_project.add_argument( '--domain', metavar='', - help='Name or ID of domain associated with user or group', + help='Include (name or ID)', ) domain_or_project.add_argument( '--project', metavar='', - help='Name or ID of project associated with user or group', + help='Include `` (name or ID)', + ) + user_or_group = parser.add_mutually_exclusive_group() + user_or_group.add_argument( + '--user', + metavar='', + help='Include (name or ID)', + ) + user_or_group.add_argument( + '--group', + metavar='', + help='Include (name or ID)', ) return parser @@ -177,7 +177,7 @@ def take_action(self, parsed_args): class DeleteRole(command.Command): - """Delete existing role""" + """Delete an existing role""" log = logging.getLogger(__name__ + '.DeleteRole') @@ -186,7 +186,7 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help='Name or ID of role to delete', + help='Role to delete (name or ID)', ) return parser @@ -214,23 +214,23 @@ def get_parser(self, prog_name): domain_or_project.add_argument( '--domain', metavar='', - help='Filter role list by ', + help='Filter roles by (name or ID)', ) domain_or_project.add_argument( '--project', metavar='', - help='Filter role list by ', + help='Filter roles by (name or ID)', ) user_or_group = parser.add_mutually_exclusive_group() user_or_group.add_argument( '--user', metavar='', - help='Name or ID of user to list roles assigned to', + help='Filter roles by (name or ID)', ) user_or_group.add_argument( '--group', metavar='', - help='Name or ID of group to list roles assigned to', + help='Filter roles by (name or ID)', ) return parser @@ -320,7 +320,7 @@ def take_action(self, parsed_args): class RemoveRole(command.Command): - """Remove role command""" + """Remove role from domain/project : user/group""" log = logging.getLogger(__name__ + '.RemoveRole') @@ -329,29 +329,29 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help='Name or ID of role to remove', - ) - user_or_group = parser.add_mutually_exclusive_group() - user_or_group.add_argument( - '--user', - metavar='', - help='Name or ID of user to remove a role', - ) - user_or_group.add_argument( - '--group', - metavar='', - help='Name or ID of group to remove a role', + help='Role to remove (name or ID)', ) domain_or_project = parser.add_mutually_exclusive_group() domain_or_project.add_argument( '--domain', metavar='', - help='Name or ID of domain associated with user or group', + help='Include (name or ID)', ) domain_or_project.add_argument( '--project', metavar='', - help='Name or ID of project associated with user or group', + help='Include (name or ID)', + ) + user_or_group = parser.add_mutually_exclusive_group() + user_or_group.add_argument( + '--user', + metavar='', + help='Include (name or ID)', + ) + user_or_group.add_argument( + '--group', + metavar='', + help='Include (name or ID)', ) return parser @@ -431,7 +431,7 @@ def take_action(self, parsed_args): class SetRole(command.Command): - """Set role command""" + """Set role properties""" log = logging.getLogger(__name__ + '.SetRole') @@ -440,12 +440,12 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help='Name or ID of role to update', + help='Role to modify (name or ID)', ) parser.add_argument( '--name', - metavar='', - help='New role name', + metavar='', + help='Set role name', ) return parser @@ -475,7 +475,7 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help='Name or ID of role to display', + help='Role to show (name or ID)', ) return parser diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 63dd3b4f58..10ffce36b8 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -37,13 +37,23 @@ def get_parser(self, prog_name): parser = super(CreateUser, self).get_parser(prog_name) parser.add_argument( 'name', - metavar='', + metavar='', help='New user name', ) + parser.add_argument( + '--domain', + metavar='', + help='Default domain (name or ID)', + ) + parser.add_argument( + '--project', + metavar='', + help='Default project (name or ID)', + ) parser.add_argument( '--password', - metavar='', - help='New user password', + metavar='', + help='Set user password', ) parser.add_argument( '--password-prompt', @@ -53,23 +63,13 @@ def get_parser(self, prog_name): ) parser.add_argument( '--email', - metavar='', - help='New user email address', - ) - parser.add_argument( - '--project', - metavar='', - help='Set default project (name or ID)', - ) - parser.add_argument( - '--domain', - metavar='', - help='New default domain name or ID', + metavar='', + help='Set user email address', ) parser.add_argument( '--description', metavar='', - help='Description for new user', + help='User description', ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( @@ -173,12 +173,12 @@ def get_parser(self, prog_name): parser.add_argument( '--domain', metavar='', - help='Filter user list by (name or ID)', + help='Filter users by (name or ID)', ) parser.add_argument( '--group', metavar='', - help='List memberships of (name or ID)', + help='Filter users by membership (name or ID)', ) parser.add_argument( '--long', @@ -240,13 +240,23 @@ def get_parser(self, prog_name): ) parser.add_argument( '--name', - metavar='', - help='New user name', + metavar='', + help='Set user name', + ) + parser.add_argument( + '--domain', + metavar='', + help='Set default domain (name or ID)', + ) + parser.add_argument( + '--project', + metavar='', + help='Set default project (name or ID)', ) parser.add_argument( '--password', - metavar='', - help='New user password', + metavar='', + help='Set user password', ) parser.add_argument( '--password-prompt', @@ -256,23 +266,13 @@ def get_parser(self, prog_name): ) parser.add_argument( '--email', - metavar='', - help='New user email address', - ) - parser.add_argument( - '--domain', - metavar='', - help='New domain name or ID', - ) - parser.add_argument( - '--project', - metavar='', - help='New project name or ID', + metavar='', + help='Set user email address', ) parser.add_argument( '--description', metavar='', - help='New description', + help='Set user description', ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( @@ -380,7 +380,7 @@ def get_parser(self, prog_name): parser.add_argument( '--domain', metavar='', - help='Domain where user resides (name or ID)', + help='Domain owning (name or ID)', ) return parser From 4b239eea4290522a24ed4242d983dc69ff7e382e Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Wed, 19 Nov 2014 15:31:25 -0500 Subject: [PATCH 0269/3494] Add support for domains when deleting identity v3 resources Currently, only deleting via IDs is possible for groups, projects and users. We should have an optional --domain argument that allows for a name to be specified for the resource. (Since these are all namespaced by domains). Change-Id: I18ace3db85a3969f0b97678d432d6f8368baa9cd --- doc/source/command-objects/project.rst | 6 ++++++ doc/source/command-objects/user.rst | 8 ++++++++ openstackclient/identity/v3/group.py | 16 +++++++++++++++- openstackclient/identity/v3/project.py | 17 +++++++++++++---- openstackclient/identity/v3/user.py | 17 +++++++++++++---- 5 files changed, 55 insertions(+), 9 deletions(-) diff --git a/doc/source/command-objects/project.rst b/doc/source/command-objects/project.rst index ba741d1dc1..0be4e80680 100644 --- a/doc/source/command-objects/project.rst +++ b/doc/source/command-objects/project.rst @@ -58,6 +58,12 @@ Delete an existing project os project delete +.. option:: --domain + + Domain owning :ref:`\ <_project_delete-project>` (name or ID) + + .. versionadded:: 3 + .. _project_delete-project: .. describe:: diff --git a/doc/source/command-objects/user.rst b/doc/source/command-objects/user.rst index 53becf2799..24a2725b7d 100644 --- a/doc/source/command-objects/user.rst +++ b/doc/source/command-objects/user.rst @@ -80,6 +80,14 @@ Delete user os user delete +.. option:: --domain + + Domain owning :ref:`\ <_user_delete-user>` (name or ID) + + .. versionadded:: 3 + +.. _user_delete-user: + .. describe:: User to delete (name or ID) diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index 14838bba57..5d3cf6422a 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -169,12 +169,26 @@ def get_parser(self, prog_name): 'group', metavar='', help='Name or ID of group to delete') + parser.add_argument( + '--domain', + metavar='', + help='Domain where group resides (name or ID)', + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - group = utils.find_resource(identity_client.groups, parsed_args.group) + + if parsed_args.domain: + domain = common.find_domain(identity_client, parsed_args.domain) + group = utils.find_resource(identity_client.groups, + parsed_args.group, + domain_id=domain.id) + else: + group = utils.find_resource(identity_client.groups, + parsed_args.group) + identity_client.groups.delete(group.id) return diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 3b0e92fd8c..1e3977ba36 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -126,16 +126,25 @@ def get_parser(self, prog_name): metavar='', help='Project to delete (name or ID)', ) + parser.add_argument( + '--domain', + metavar='', + help='Domain owning (name or ID)', + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - project = utils.find_resource( - identity_client.projects, - parsed_args.project, - ) + if parsed_args.domain: + domain = common.find_domain(identity_client, parsed_args.domain) + project = utils.find_resource(identity_client.projects, + parsed_args.project, + domain_id=domain.id) + else: + project = utils.find_resource(identity_client.projects, + parsed_args.project) identity_client.projects.delete(project.id) return diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 10ffce36b8..665dd4bb16 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -148,16 +148,25 @@ def get_parser(self, prog_name): metavar='', help='User to delete (name or ID)', ) + parser.add_argument( + '--domain', + metavar='', + help='Domain owning (name or ID)', + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - user = utils.find_resource( - identity_client.users, - parsed_args.user, - ) + if parsed_args.domain: + domain = common.find_domain(identity_client, parsed_args.domain) + user = utils.find_resource(identity_client.users, + parsed_args.user, + domain_id=domain.id) + else: + user = utils.find_resource(identity_client.users, + parsed_args.user) identity_client.users.delete(user.id) return From 5bc768bbc2c0ffa592a2e419cecdb898b9b65a83 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 20 Nov 2014 18:42:00 -0500 Subject: [PATCH 0270/3494] Add the ability to list projects based on a user Essentially performing GET /users/{user_id}/projects Change-Id: Iae6ddfc86a856fa24fbe293ec4af52ea671390f8 Closes-Bug: #1394793 --- doc/source/command-objects/project.rst | 7 +++++++ openstackclient/identity/v3/project.py | 24 ++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/doc/source/command-objects/project.rst b/doc/source/command-objects/project.rst index 0be4e80680..305e611495 100644 --- a/doc/source/command-objects/project.rst +++ b/doc/source/command-objects/project.rst @@ -79,6 +79,7 @@ List projects os project list [--domain ] + [--user ] [--long] .. option:: --domain @@ -87,6 +88,12 @@ List projects .. versionadded:: 3 +.. option:: --user + + Filter projects by :option:`\ <--user>` (name or ID) + + .. versionadded:: 3 + .. option:: --long List additional fields in output diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 1e3977ba36..e9adfe348a 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -162,6 +162,11 @@ def get_parser(self, prog_name): metavar='', help='Filter projects by (name or ID)', ) + parser.add_argument( + '--user', + metavar='', + help='Filter projects by (name or ID)', + ) parser.add_argument( '--long', action='store_true', @@ -178,9 +183,24 @@ def take_action(self, parsed_args): else: columns = ('ID', 'Name') kwargs = {} + + domain_id = None if parsed_args.domain: - kwargs['domain'] = common.find_domain(identity_client, - parsed_args.domain).id + domain_id = common.find_domain(identity_client, + parsed_args.domain).id + kwargs['domain'] = domain_id + + if parsed_args.user: + if parsed_args.domain: + user_id = utils.find_resource(identity_client.users, + parsed_args.user, + domain_id=domain_id).id + else: + user_id = utils.find_resource(identity_client.users, + parsed_args.user).id + + kwargs['user'] = user_id + data = identity_client.projects.list(**kwargs) return (columns, (utils.get_item_properties( From ac4950b46e65e5b3e4a98a0b0ce2a2c80747b3e8 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 17 Nov 2014 21:56:18 -0600 Subject: [PATCH 0271/3494] Command object docs: server, server image server server image Some cosmetic changes in the command source, sorting classes, help strings, etc. Change-Id: I3f68dae77b9fe02bc6866684e05aeff943dd9cc3 --- doc/source/command-objects/server-image.rst | 27 + doc/source/command-objects/server.rst | 544 ++++++++++++++++++++ doc/source/commands.rst | 3 +- openstackclient/compute/v2/server.py | 186 ++++--- 4 files changed, 680 insertions(+), 80 deletions(-) create mode 100644 doc/source/command-objects/server-image.rst create mode 100644 doc/source/command-objects/server.rst diff --git a/doc/source/command-objects/server-image.rst b/doc/source/command-objects/server-image.rst new file mode 100644 index 0000000000..681f6e4f2c --- /dev/null +++ b/doc/source/command-objects/server-image.rst @@ -0,0 +1,27 @@ +============ +server image +============ + +A server image is a disk image created from a running server instance. The +image is created in the Image store. + +server image create +------------------- + +Create a new disk image from a running server + +.. code:: bash + + os server image create + [--name ] + [--wait] + + +:option:`--name` + Name of new image (default is server name) + +:option:`--wait` + Wait for image create to complete + +:option:`` + Server (name or ID) diff --git a/doc/source/command-objects/server.rst b/doc/source/command-objects/server.rst new file mode 100644 index 0000000000..4f07632957 --- /dev/null +++ b/doc/source/command-objects/server.rst @@ -0,0 +1,544 @@ +====== +server +====== + + +server add security group +------------------------- + +Add security group to server + +.. code:: bash + + os server add security group + + + +:option:`` + Server (name or ID) + +:option:`` + Security group to add (name or ID) + +server add volume +----------------- + +Add volume to server + +.. code:: bash + + os server add volume + [--device ] + + + +:option:`--device` + Server internal device name for volume + +:option:`` + Server (name or ID) + +:option:`` + Volume to add (name or ID) + +server create +------------- + +Create a new server + +.. code:: bash + + os server create + --image | --volume + --flavor + [--security-group [...] ] + [--key-name ] + [--property [...] ] + [--file ] [...] ] + [--user-data ] + [--availability-zone ] + [--block-device-mapping [...] ] + [--nic [...] ] + [--hint [...] ] + [--config-drive |True ] + [--min ] + [--max ] + [--wait] + + +:option:`--image` + Create server from this image + +:option:`--volume` + Create server from this volume + +:option:`--flavor` + Create server with this flavor + +:option:`--security-group` + Security group to assign to this server (repeat for multiple groups) + +:option:`--key-name` + Keypair to inject into this server (optional extension) + +:option:`--property` + Set a property on this server (repeat for multiple values) + +:option:`--file` + File to inject into image before boot (repeat for multiple files) + +:option:`--user-data` + User data file to serve from the metadata server + +:option:`--availability-zone` + Select an availability zone for the server + +:option:`--block-device-mapping` + Map block devices; map is ::: (optional extension) + +:option:`--nic` + Specify NIC configuration (optional extension) + +:option:`--hint` + Hints for the scheduler (optional extension) + +:option:`--config-drive` |True + Use specified volume as the config drive, or 'True' to use an ephemeral drive + +:option:`--min` + Minimum number of servers to launch (default=1) + +:option:`--max` + Maximum number of servers to launch (default=1) + +:option:`--wait` + Wait for build to complete + +:option:`` + New server name + +server delete +------------- + +Delete server command + +.. code:: bash + + os server delete + + +:option:`` + Server (name or ID) + +server list +----------- + +List servers + +.. code:: bash + + os server list + [--reservation-id ] + [--ip ] + [--ip6 ] + [--name ] + [--instance-name ] + [--status ] + [--flavor ] + [--image ] + [--host ] + [--all-projects] + [--long] + +:option:`--reservation-id` + Only return instances that match the reservation + +:option:`--ip` + Regular expression to match IP addresses + +:option:`--ip6` + Regular expression to match IPv6 addresses + +:option:`--name` + Regular expression to match names + +:option:`--instance-name` + Regular expression to match instance name (admin only) + +:option:`--status` + Search by server status + +:option:`--flavor` + Search by flavor ID + +:option:`--image` + Search by image ID + +:option:`--host` + Search by hostname + +:option:`--all-projects` + Include all projects (admin only) + +:option:`--long` + List additional fields in output + +server lock +----------- + +Lock server + +.. code:: bash + + os server lock + + +:option:`` + Server (name or ID) + +server migrate +-------------- + +Migrate server to different host + +.. code:: bash + + os server migrate + --live + [--shared-migration | --block-migration] + [--disk-overcommit | --no-disk-overcommit] + [--wait] + + +:option:`--wait` + Wait for resize to complete + +:option:`--live` + Target hostname + +:option:`--shared-migration` + Perform a shared live migration (default) + +:option:`--block-migration` + Perform a block live migration + +:option:`--disk-overcommit` + Allow disk over-commit on the destination host + +:option:`--no-disk-overcommit` + Do not over-commit disk on the destination host (default) + +:option:`` + Server to migrate (name or ID) + +server pause +------------ + +Pause server + +.. code:: bash + + os server pause + + +:option:`` + Server (name or ID) + +server reboot +------------- + +Perform a hard or soft server reboot + +.. code:: bash + + os server reboot + [--hard | --soft] + [--wait] + + +:option:`--hard` + Perform a hard reboot + +:option:`--soft` + Perform a soft reboot + +:option:`--wait` + Wait for reboot to complete + +:option:`` + Server (name or ID) + +server rebuild +-------------- + +Rebuild server + +.. code:: bash + + os server rebuild + --image + [--password ] + [--wait] + + +:option:`--image` + Recreate server from this image + +:option:`--password` + Set the password on the rebuilt instance + +:option:`--wait` + Wait for rebuild to complete + +:option:`` + Server (name or ID) + +server remove security group +---------------------------- + +Remove security group from server + +.. code:: bash + + os server remove security group + + + +:option:`` + Name or ID of server to use + +:option:`` + Name or ID of security group to remove from server + +server remove volume +-------------------- + +Remove volume from server + +.. code:: bash + + os server remove volume + + + +:option:`` + Server (name or ID) + +:option:`` + Volume to remove (name or ID) + +server rescue +------------- + +Put server in rescue mode + +.. code:: bash + + os server rescue + + +:option:`` + Server (name or ID) + +server resize +------------- + +Scale server to a new flavor + +.. code:: bash + + os server resize + --flavor + [--wait] + + + os server resize + --verify | --revert + + +:option:`--flavor` + Resize server to specified flavor + +:option:`--verify` + Verify server resize is complete + +:option:`--revert` + Restore server state before resize + +:option:`--wait` + Wait for resize to complete + +:option:`` + Server (name or ID) + +A resize operation is implemented by creating a new server and copying +the contents of the original disk into a new one. It is also a two-step +process for the user: the first is to perform the resize, the second is +to either confirm (verify) success and release the old server, or to declare +a revert to release the new server and restart the old one. + +server resume +------------- + +Resume server + +.. code:: bash + + os server resume + + +:option:`` + Server (name or ID) + +server set +---------- + +Set server properties + +.. code:: bash + + os server set + --name + --property + [--property ] ... + --root-password + + +:option:`--name` + New server name + +:option:`--root-password` + Set new root password (interactive only) + +:option:`--property` + Property to add/change for this server (repeat option to set + multiple properties) + +:option:`` + Server (name or ID) + +server show +----------- + +Show server details + +.. code:: bash + + os server show + [--diagnostics] + + +:option:`--diagnostics` + Display server diagnostics information + +:option:`` + Server (name or ID) + +server ssh +---------- + +Ssh to server + +.. code:: bash + + os server ssh + [--login ] + [--port ] + [--identity ] + [--option ] + [--public | --private | --address-type ] + + +:option:`--login` + Login name (ssh -l option) + +:option:`--port` + Destination port (ssh -p option) + +:option:`--identity` + Private key file (ssh -i option) + +:option:`--option` + Options in ssh_config(5) format (ssh -o option) + +:option:`--public` + Use public IP address + +:option:`--private` + Use private IP address + +:option:`--address-type` + Use other IP address (public, private, etc) + +:option:`` + Server (name or ID) + +server suspend +-------------- + +Suspend server + +.. code:: bash + + os server suspend + + +:option:`` + Server (name or ID) + +server unlock +------------- + +Unlock server + +.. code:: bash + + os server unlock + + +:option:`` + Server (name or ID) + +server unpause +-------------- + +Unpause server + +.. code:: bash + + os server unpause + + +:option:`` + Server (name or ID) + +server unrescue +--------------- + +Restore server from rescue mode + +.. code:: bash + + os server unrescue + + +:option:`` + Server (name or ID) + +server unset +------------ + +Unset server properties + +.. code:: bash + + os server unset + --property + [--property ] ... + + +:option:`--property` + Property key to remove from server (repeat to set multiple values) + +:option:`` + Server (name or ID) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 250a8039d8..7b38134e42 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -100,7 +100,8 @@ referring to both Compute and Volume quotas. * ``role``: Identity - a policy object used to determine authorization * ``security group``: Compute, Network - groups of network access rules * ``security group rule``: Compute, Network - the individual rules that define protocol/IP/port access -* ``server``: Compute - a virtual machine instance +* ``server``: (**Compute**) virtual machine instance +* ``server image``: (**Compute**) saved server disk image * ``service``: Identity - a cloud service * ``snapshot``: Volume - a point-in-time copy of a volume * ``token``: (**Identity**) a bearer token managed by Identity service diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index d58df86c10..5ab1d5f3ff 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -99,27 +99,22 @@ def _show_progress(progress): sys.stdout.flush() -class AddServerVolume(command.Command): - """Add volume to server""" +class AddServerSecurityGroup(command.Command): + """Add security group to server""" - log = logging.getLogger(__name__ + '.AddServerVolume') + log = logging.getLogger(__name__ + '.AddServerSecurityGroup') def get_parser(self, prog_name): - parser = super(AddServerVolume, self).get_parser(prog_name) + parser = super(AddServerSecurityGroup, self).get_parser(prog_name) parser.add_argument( 'server', metavar='', help=_('Server (name or ID)'), ) parser.add_argument( - 'volume', - metavar='', - help=_('Volume to add (name or ID)'), - ) - parser.add_argument( - '--device', - metavar='', - help=_('Server internal device name for volume'), + 'group', + metavar='', + help=_('Security group to add (name or ID)'), ) return parser @@ -127,40 +122,41 @@ def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute - volume_client = self.app.client_manager.volume server = utils.find_resource( compute_client.servers, parsed_args.server, ) - volume = utils.find_resource( - volume_client.volumes, - parsed_args.volume, + security_group = utils.find_resource( + compute_client.security_groups, + parsed_args.group, ) - compute_client.volumes.create_server_volume( - server.id, - volume.id, - parsed_args.device, - ) + server.add_security_group(security_group.name) + return -class AddServerSecurityGroup(command.Command): - """Add security group to server""" +class AddServerVolume(command.Command): + """Add volume to server""" - log = logging.getLogger(__name__ + '.AddServerSecurityGroup') + log = logging.getLogger(__name__ + '.AddServerVolume') def get_parser(self, prog_name): - parser = super(AddServerSecurityGroup, self).get_parser(prog_name) + parser = super(AddServerVolume, self).get_parser(prog_name) parser.add_argument( 'server', metavar='', - help=_('Name or ID of server to use'), + help=_('Server (name or ID)'), ) parser.add_argument( - 'group', - metavar='', - help=_('Name or ID of security group to add to server'), + 'volume', + metavar='', + help=_('Volume to add (name or ID)'), + ) + parser.add_argument( + '--device', + metavar='', + help=_('Server internal device name for volume'), ) return parser @@ -168,18 +164,22 @@ def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute + volume_client = self.app.client_manager.volume server = utils.find_resource( compute_client.servers, parsed_args.server, ) - security_group = utils.find_resource( - compute_client.security_groups, - parsed_args.group, + volume = utils.find_resource( + volume_client.volumes, + parsed_args.volume, ) - server.add_security_group(security_group.name) - return + compute_client.volumes.create_server_volume( + server.id, + volume.id, + parsed_args.device, + ) class CreateServer(show.ShowOne): @@ -192,55 +192,65 @@ def get_parser(self, prog_name): parser.add_argument( 'server_name', metavar='', - help=_('New server name')) + help=_('New server name'), + ) disk_group = parser.add_mutually_exclusive_group( required=True, ) disk_group.add_argument( '--image', metavar='', - help=_('Create server from this image')) + help=_('Create server from this image'), + ) disk_group.add_argument( '--volume', metavar='', - help=_('Create server from this volume')) + help=_('Create server from this volume'), + ) parser.add_argument( '--flavor', metavar='', required=True, - help=_('Create server with this flavor')) + help=_('Create server with this flavor'), + ) parser.add_argument( '--security-group', metavar='', action='append', default=[], help=_('Security group to assign to this server ' - '(repeat for multiple groups)')) + '(repeat for multiple groups)'), + ) parser.add_argument( '--key-name', metavar='', - help=_('Keypair to inject into this server (optional extension)')) + help=_('Keypair to inject into this server (optional extension)'), + ) parser.add_argument( '--property', metavar='', action=parseractions.KeyValueAction, help=_('Set a property on this server ' - '(repeat for multiple values)')) + '(repeat for multiple values)'), + ) parser.add_argument( '--file', metavar='', action='append', default=[], help=_('File to inject into image before boot ' - '(repeat for multiple files)')) + '(repeat for multiple files)'), + ) parser.add_argument( '--user-data', metavar='', - help=_('User data file to serve from the metadata server')) + help=_('User data file to serve from the metadata server'), + ) parser.add_argument( '--availability-zone', metavar='', - help=_('Select an availability zone for the server')) + help=_('Select an availability zone for the server'), + ) parser.add_argument( '--block-device-mapping', metavar='', @@ -248,37 +258,43 @@ def get_parser(self, prog_name): default=[], help=_('Map block devices; map is ' '::: ' - '(optional extension)')) + '(optional extension)'), + ) parser.add_argument( '--nic', metavar='', action='append', default=[], - help=_('Specify NIC configuration (optional extension)')) + help=_('Specify NIC configuration (optional extension)'), + ) parser.add_argument( '--hint', metavar='', action='append', default=[], - help=_('Hints for the scheduler (optional extension)')) + help=_('Hints for the scheduler (optional extension)'), + ) parser.add_argument( '--config-drive', metavar='|True', default=False, help=_('Use specified volume as the config drive, ' - 'or \'True\' to use an ephemeral drive')) + 'or \'True\' to use an ephemeral drive'), + ) parser.add_argument( '--min', metavar='', type=int, default=1, - help=_('Minimum number of servers to launch (default=1)')) + help=_('Minimum number of servers to launch (default=1)'), + ) parser.add_argument( '--max', metavar='', type=int, default=1, - help=_('Maximum number of servers to launch (default=1)')) + help=_('Maximum number of servers to launch (default=1)'), + ) parser.add_argument( '--wait', action='store_true', @@ -504,7 +520,8 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help=_('Name or ID of server to delete')) + help=_('Server (name or ID)'), + ) return parser def take_action(self, parsed_args): @@ -526,50 +543,61 @@ def get_parser(self, prog_name): parser.add_argument( '--reservation-id', metavar='', - help=_('Only return instances that match the reservation')) + help=_('Only return instances that match the reservation'), + ) parser.add_argument( '--ip', metavar='', - help=_('Regular expression to match IP addresses')) + help=_('Regular expression to match IP addresses'), + ) parser.add_argument( '--ip6', metavar='', - help=_('Regular expression to match IPv6 addresses')) + help=_('Regular expression to match IPv6 addresses'), + ) parser.add_argument( '--name', - metavar='', - help=_('Regular expression to match names')) + metavar='', + help=_('Regular expression to match names'), + ) + parser.add_argument( + '--instance-name', + metavar='', + help=_('Regular expression to match instance name (admin only)'), + ) parser.add_argument( '--status', metavar='', # FIXME(dhellmann): Add choices? - help=_('Search by server status')) + help=_('Search by server status'), + ) parser.add_argument( '--flavor', metavar='', - help=_('Search by flavor ID')) + help=_('Search by flavor'), + ) parser.add_argument( '--image', metavar='', - help=_('Search by image ID')) + help=_('Search by image'), + ) parser.add_argument( '--host', metavar='', - help=_('Search by hostname')) - parser.add_argument( - '--instance-name', - metavar='', - help=_('Regular expression to match instance name (admin only)')) + help=_('Search by hostname'), + ) parser.add_argument( '--all-projects', action='store_true', default=bool(int(os.environ.get("ALL_PROJECTS", 0))), - help=_('Include all projects (admin only)')) + help=_('Include all projects (admin only)'), + ) parser.add_argument( '--long', action='store_true', default=False, - help=_('List additional fields in output')) + help=_('List additional fields in output'), + ) return parser def take_action(self, parsed_args): @@ -672,12 +700,7 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help=_('Server to migrate (name or ID)'), - ) - parser.add_argument( - '--wait', - action='store_true', - help=_('Wait for resize to complete'), + help=_('Server (name or ID)'), ) parser.add_argument( '--live', @@ -699,6 +722,12 @@ def get_parser(self, prog_name): help=_('Perform a block live migration'), ) disk_group = parser.add_mutually_exclusive_group() + disk_group.add_argument( + '--disk-overcommit', + action='store_true', + default=False, + help=_('Allow disk over-commit on the destination host'), + ) disk_group.add_argument( '--no-disk-overcommit', dest='disk_overcommit', @@ -707,11 +736,10 @@ def get_parser(self, prog_name): help=_('Do not over-commit disk on the' ' destination host (default)'), ) - disk_group.add_argument( - '--disk-overcommit', + parser.add_argument( + '--wait', action='store_true', - default=False, - help=_('Allow disk over-commit on the destination host'), + help=_('Wait for resize to complete'), ) return parser @@ -1140,13 +1168,13 @@ def get_parser(self, prog_name): parser.add_argument( 'server', metavar='', - help=_('Server to show (name or ID)'), + help=_('Server (name or ID)'), ) parser.add_argument( '--diagnostics', action='store_true', default=False, - help=_('Display diagnostics information for a given server'), + help=_('Display server diagnostics information'), ) return parser @@ -1439,7 +1467,7 @@ def get_parser(self, prog_name): action='append', default=[], help=_('Property key to remove from server ' - '(repeat to set multiple values)'), + '(repeat to unset multiple values)'), ) return parser From f7024281587be4178e3cc702aee0bfc6f001d144 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 17 Nov 2014 23:54:43 -0500 Subject: [PATCH 0272/3494] 1.0.0 release notes Release notes for our next cut of osc. Change-Id: Ic3b0d557f2a380c4b5a05903ff7394be7b961b55 --- doc/source/releases.rst | 51 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/doc/source/releases.rst b/doc/source/releases.rst index 909c362eaf..ad9c59e191 100644 --- a/doc/source/releases.rst +++ b/doc/source/releases.rst @@ -2,6 +2,57 @@ Release Notes ============= +1.0.0 (04 Dec 2014) +=================== + +* Bug 1337422_: document different ways to authenticate +* Bug 1383333_: Creating volume from image required image ID +* Bug 1292638_: Perhaps API Versions should Match Easier +* Bug 1390389_: create with a soft fail (create or show) for keystone operations +* Bug 1387932_: add keystone v3 region object +* Bug 1378842_: OSC fails to show server details if booted from volume +* Bug 1383338_: server create problems in boot-from-volume +* Bug 1337685_: Add the ability to list networks extensions +* Bug 1355838_: Don't make calls to Keystone for authN if insufficient args are present +* Bug 1371924_: strings are being treated as numbers +* Bug 1372070_: help text in error on openstack image save +* Bug 1372744_: v3 credential set always needs --user option +* Bug 1376833_: odd behavior when editing the domain of a user through Keystone v3 API +* Bug 1378165_: Domains should be supported for 'user show' command +* Bug 1378565_: The '--domain' arg for identity commands should not require domain lookup +* Bug 1379871_: token issue for identity v3 is broken +* Bug 1383083_: repeated to generate clientmanager in interactive mode +* Added functional tests framework and identity/object tests +* Authentication Plugin Support +* Use keystoneclient.session as the base HTTP transport +* implement swift client commands +* clean up 'links' section in keystone v3 resources +* Add cliff-tablib to requirements +* Include support for using oslo debugger in tests +* Close file handlers that were left open +* Added framework for i18n support, and marked Identity v2.0 files for translation +* Add 'command list' command +* CRUD Support for ``OS-FEDERATION`` resources (protocol, mappings, identity providers) + +.. _1337422: https://bugs.launchpad.net/bugs/1337422 +.. _1383333: https://bugs.launchpad.net/bugs/1383333 +.. _1292638: https://bugs.launchpad.net/bugs/1292638 +.. _1390389: https://bugs.launchpad.net/bugs/1390389 +.. _1387932: https://bugs.launchpad.net/bugs/1387932 +.. _1378842: https://bugs.launchpad.net/bugs/1378842 +.. _1383338: https://bugs.launchpad.net/bugs/1383338 +.. _1337685: https://bugs.launchpad.net/bugs/1337685 +.. _1355838: https://bugs.launchpad.net/bugs/1355838 +.. _1371924: https://bugs.launchpad.net/bugs/1371924 +.. _1372070: https://bugs.launchpad.net/bugs/1372070 +.. _1372744: https://bugs.launchpad.net/bugs/1372744 +.. _1376833: https://bugs.launchpad.net/bugs/1376833 +.. _1378165: https://bugs.launchpad.net/bugs/1378165 +.. _1378565: https://bugs.launchpad.net/bugs/1378565 +.. _1379871: https://bugs.launchpad.net/bugs/1379871 +.. _1383083: https://bugs.launchpad.net/bugs/1383083 + + 0.4.1 (08 Sep 2014) =================== From 625a8ae42d5a4cded26ba4d7c76b4d4d88bf9f20 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 2 Dec 2014 16:57:22 -0600 Subject: [PATCH 0273/3494] Add documentation of interactive mode This is a light description with some examples. Change-Id: Iff9ad904a150f2bb7673bd4106cf26bcefec08b9 --- doc/source/index.rst | 1 + doc/source/interactive.rst | 111 +++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 doc/source/interactive.rst diff --git a/doc/source/index.rst b/doc/source/index.rst index 8806daa069..9d4989f22e 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -16,6 +16,7 @@ Contents: commands plugins authentication + interactive man/openstack Getting Started diff --git a/doc/source/interactive.rst b/doc/source/interactive.rst new file mode 100644 index 0000000000..4822d89f59 --- /dev/null +++ b/doc/source/interactive.rst @@ -0,0 +1,111 @@ +================ +Interactive Mode +================ + +OpenStackClient has an interactive mode, similar to the :program:`virsh(1)` or +:program:`lvm(8)` commands on Linux. This mode is useful for executing a +series of commands without having to reload the CLI, or more importantly, +without having to re-authenticate to the cloud. + +Enter interactive mode by issuing the :command:`openstack` command with no +subcommand. An :code:`(openstack)` prompt will be displayed. Interactive mode +is terminated with :command:`exit`. + +Authentication +============== + +Authentication happens exactly as before, using the same global command line +options and environment variables, except it only happens once. +The credentials are cached and re-used for subsequent commands. This means +that to work with multiple clouds interactive mode must be ended so a +authentication to the second cloud can occur. + +Scripting +========= + +Using interactive mode inside scripts sounds counter-intuitive, but the same +single-authentication benefit can be achieved by passing OSC commands to +the CLI via :code:`stdin`. + +Sample session: + +.. code-block:: bash + + # assume auth credentials are in the environment + $ openstack + (openstack) keypair list + +--------+-------------------------------------------------+ + | Name | Fingerprint | + +--------+-------------------------------------------------+ + | bunsen | a5:da:0c:52:e8:52:42:a3:4f:b8:22:62:7b:e4:e8:89 | + | beaker | 45:9c:50:56:7c:fc:3a:b6:b5:60:02:2f:41:fb:a9:4c | + +--------+-------------------------------------------------+ + (openstack) image list + +--------------------------------------+----------------+ + | ID | Name | + +--------------------------------------+----------------+ + | 78b23835-c800-4d95-9d2a-e4de59a553d8 | OpenWRT r42884 | + | 2e45d43a-7c25-45f1-b012-06ac313e2f6b | Fedora 20 | + | de3a8396-3bae-42de-84bd-f4e398b8c320 | CirrOS | + +--------------------------------------+----------------+ + (openstack) flavor list + +--------------------------------------+----------+--------+--------+-----------+------+-------+-------------+-----------+-------------+ + | ID | Name | RAM | Disk | Ephemeral | Swap | VCPUs | RXTX Factor | Is Public | Extra Specs | + +--------------------------------------+----------+--------+--------+-----------+------+-------+-------------+-----------+-------------+ + | 12594680-56f7-4da2-8322-7266681b3070 | m1.small | 2048 | 20 | 0 | | 1 | | True | | + | 9274f903-0cc7-4a95-9124-1968018e355d | m1.tiny | 512 | 5 | 0 | | 1 | | True | | + +--------------------------------------+----------+--------+--------+-----------+------+-------+-------------+-----------+-------------+ + (openstack) server create --image CirrOS --flavor m1.small --key-name beaker sample-server + +-----------------------------+-------------------------------------------------+ + | Field | Value | + +-----------------------------+-------------------------------------------------+ + | config_drive | | + | created | 2014-11-19T18:08:41Z | + | flavor | m1.small (12594680-56f7-4da2-8322-7266681b3070) | + | id | 3a9a7f82-e902-4948-9245-52b045c76a1d | + | image | CirrOS (de3a8396-3bae-42de-84bd-f4e398b8c320) | + | key_name | bunsen | + | name | sample-server | + | progress | 0 | + | properties | | + | security_groups | [{u'name': u'default'}] | + | status | BUILD | + | tenant_id | 53c93c7952594d9ba16bd7072a165ce8 | + | updated | 2014-11-19T18:08:42Z | + | user_id | 1e4eea54c7124688a8092bec6e2dbee6 | + +-----------------------------+-------------------------------------------------+ + +A similar session can be issued all at once: + +.. code-block:: bash + + $ openstack < keypair list + > flavor show m1.small + > EOF + (openstack) +--------+-------------------------------------------------+ + | Name | Fingerprint | + +--------+-------------------------------------------------+ + | bunsen | a5:da:0c:52:e8:52:42:a3:4f:b8:22:62:7b:e4:e8:89 | + | beaker | 45:9c:50:56:7c:fc:3a:b6:b5:60:02:2f:41:fb:a9:4c | + +--------+-------------------------------------------------+ + (openstack) +----------------------------+--------------------------------------+ + | Field | Value | + +----------------------------+--------------------------------------+ + | OS-FLV-DISABLED:disabled | False | + | OS-FLV-EXT-DATA:ephemeral | 0 | + | disk | 20 | + | id | 12594680-56f7-4da2-8322-7266681b3070 | + | name | m1.small | + | os-flavor-access:is_public | True | + | ram | 2048 | + | swap | | + | vcpus | 1 | + +----------------------------+--------------------------------------+ + +Limitations +=========== + +The obvious limitations to Interactive Mode is that it is not a Domain Specific +Language (DSL), just a simple command processor. That means there are no variables +or flow control. From 13672123fccdd76a62416b88443e78269a80343a Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 4 Dec 2014 15:34:02 -0500 Subject: [PATCH 0274/3494] Safely pop project parent id Since we don't support multitenancy yet, we should just pop the parent id of a project. When keystoneclient supports mulittenancy we should bring everything in at once (CRUD), and these changes should be removed. Change-Id: I82c7c825502124a24ccdbadf09ecb2748887ca5d --- openstackclient/identity/v2_0/project.py | 8 +++++--- openstackclient/identity/v3/project.py | 4 ++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index df759ce6a8..b2f99425c7 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -98,9 +98,9 @@ def take_action(self, parsed_args): else: raise e - info = {} - info.update(project._info) - return zip(*sorted(six.iteritems(info))) + # TODO(stevemar): Remove the line below when we support multitenancy + project._info.pop('parent_id', None) + return zip(*sorted(six.iteritems(project._info))) class DeleteProject(command.Command): @@ -279,4 +279,6 @@ def take_action(self, parsed_args): else: raise e + # TODO(stevemar): Remove the line below when we support multitenancy + info.pop('parent_id', None) return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index e9adfe348a..2c2d408ec3 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -111,6 +111,8 @@ def take_action(self, parsed_args): 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))) @@ -325,4 +327,6 @@ def take_action(self, parsed_args): parsed_args.project) 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))) From 6a61dbc86fd2b45ea3126ed20f9f863109b794fb Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 17 Nov 2014 22:59:57 -0600 Subject: [PATCH 0275/3494] Command object docs: catalog, credentials, endpoint, region, token catalog credentials endpoint region token Change-Id: Icd7ec7fd207488b2ceb0280722aa9a684aeeac28 --- doc/source/command-objects/catalog.rst | 20 +++++ doc/source/command-objects/credentials.rst | 25 ++++++ doc/source/command-objects/endpoint.rst | 45 +++++++++++ doc/source/command-objects/region.rst | 94 ++++++++++++++++++++++ doc/source/command-objects/token.rst | 19 +++++ doc/source/commands.rst | 6 +- 6 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 doc/source/command-objects/catalog.rst create mode 100644 doc/source/command-objects/credentials.rst create mode 100644 doc/source/command-objects/endpoint.rst create mode 100644 doc/source/command-objects/region.rst create mode 100644 doc/source/command-objects/token.rst diff --git a/doc/source/command-objects/catalog.rst b/doc/source/command-objects/catalog.rst new file mode 100644 index 0000000000..99746dd716 --- /dev/null +++ b/doc/source/command-objects/catalog.rst @@ -0,0 +1,20 @@ +======= +catalog +======= + +Identity v2 + +catalog list +------------ + +.. code:: bash + + os catalog list + +catalog show +------------ + +.. code:: bash + + os catalog show + diff --git a/doc/source/command-objects/credentials.rst b/doc/source/command-objects/credentials.rst new file mode 100644 index 0000000000..ea8fc08fff --- /dev/null +++ b/doc/source/command-objects/credentials.rst @@ -0,0 +1,25 @@ +=========== +credentials +=========== + +credentials create +------------------ + +.. ''[consider rolling the ec2 creds into this too]'' + +.. code:: bash + + os credentials create + --x509 + [] + [] + +credentials show +---------------- + +.. code:: bash + + os credentials show + [--token] + [--user] + [--x509 [--root]] diff --git a/doc/source/command-objects/endpoint.rst b/doc/source/command-objects/endpoint.rst new file mode 100644 index 0000000000..128ddfa021 --- /dev/null +++ b/doc/source/command-objects/endpoint.rst @@ -0,0 +1,45 @@ +======== +endpoint +======== + +Identity v2, v3 + +endpoint create +--------------- + +.. program:: endpoint create +.. code:: bash + + os endpoint create + --publicurl + [--adminurl ] + [--internalurl ] + [--region ] + + +endpoint delete +--------------- + +.. program:: endpoint delete +.. code:: bash + + os endpoint delete + + +endpoint list +------------- + +.. program:: endpoint list +.. code:: bash + + os endpoint list + [--long] + +endpoint show +------------- + +.. program:: endpoint show +.. code:: bash + + os endpoint show + diff --git a/doc/source/command-objects/region.rst b/doc/source/command-objects/region.rst new file mode 100644 index 0000000000..788ed6facb --- /dev/null +++ b/doc/source/command-objects/region.rst @@ -0,0 +1,94 @@ +====== +region +====== + +Identity v3 + +region create +------------- + +Create new region + +.. code:: bash + + os region create + [--parent-region ] + [--description ] + [--url ] + + +:option:`--parent-region` + Parent region + +:option:`--description` + New region description + +:option:`--url` + New region URL + +:option:`` + New region ID + +region delete +------------- + +Delete region + +.. code:: bash + + os region delete + + +:option:`` + Region to delete + +region list +----------- + +List regions + +.. code:: bash + + os region list + [--parent-region ] + +:option:`--parent-region` + Filter by a specific parent region + +region set +---------- + +Set region properties + +.. code:: bash + + os region set + [--parent-region ] + [--description ] + [--url ] + + +:option:`--parent-region` + New parent region + +:option:`--description` + New region description + +:option:`--url` + New region URL + +:option:`` + Region ID to modify + +region show +----------- + +Show region + +.. code:: bash + + os region show + + +:option:`` + Region ID to modify diff --git a/doc/source/command-objects/token.rst b/doc/source/command-objects/token.rst new file mode 100644 index 0000000000..aec87d2850 --- /dev/null +++ b/doc/source/command-objects/token.rst @@ -0,0 +1,19 @@ +===== +token +===== + +Identity v2, v3 + +token issue +----------- + +.. code:: bash + + os token issue + +token revoke +------------ + +.. code:: bash + + os token revoke diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 250a8039d8..e15e000925 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -72,13 +72,14 @@ referring to both Compute and Volume quotas. * ``access token``: Identity - long-lived OAuth-based token * ``aggregate``: (**Compute**) a grouping of servers * ``backup``: Volume - a volume copy +* ``catalog``: (**Identity**) service catalog * ``console log``: (**Compute**) server console text dump * ``console url``: (**Compute**) server remote console URL * ``consumer``: Identity - OAuth-based delegatee * ``container``: Object Store - a grouping of objects -* ``credential``: Identity - specific to identity providers +* ``credentials``: (**Identity**) specific to identity providers * ``domain``: Identity - a grouping of projects -* ``endpoint``: Identity - the base URL used to contact a specific service +* ``endpoint``: (**Identity**) the base URL used to contact a specific service * ``extension``: (**Compute**, **Identity**, **Volume**) OpenStack server API extensions * ``flavor``: Compute - pre-defined configurations of servers: ram, root disk, etc * ``group``: Identity - a grouping of users @@ -96,6 +97,7 @@ referring to both Compute and Volume quotas. * ``policy``: Identity - determines authorization * ``project``: (**Identity**) owns a group of resources * ``quota``: (**Compute**, **Volume**) resource usage restrictions +* ``region``: (**Identity**) * ``request token``: Identity - temporary OAuth-based token * ``role``: Identity - a policy object used to determine authorization * ``security group``: Compute, Network - groups of network access rules From 1dc0e2b5529dd144443e948ad2b8e4a128c2ab9c Mon Sep 17 00:00:00 2001 From: Jeremy Stanley Date: Fri, 5 Dec 2014 03:30:40 +0000 Subject: [PATCH 0276/3494] Workflow documentation is now in infra-manual Replace URLs for workflow documentation to appropriate parts of the OpenStack Project Infrastructure Manual. Change-Id: Id09c9bdf8804c1ed90e49606e76ffbff1d96a7c2 --- doc/source/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index 8806daa069..2c85a65eb9 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -37,7 +37,7 @@ the openstack/python-openstackclient project using `Gerrit`_. .. _on OpenStack's Git server: https://git.openstack.org/cgit/openstack/python-openstackclient/tree .. _Launchpad: https://launchpad.net/python-openstackclient -.. _Gerrit: http://wiki.openstack.org/GerritWorkflow +.. _Gerrit: http://docs.openstack.org/infra/manual/developers.html#development-workflow Indices and Tables ================== From 62a2083a785bcfac25b2b7e409e1c7e066f9edff Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 5 Dec 2014 12:54:19 -0600 Subject: [PATCH 0277/3494] Fix ec2 credentials commands for new auth These commands were not updated for the new authentication model. Closes-Bug: 1399757 Change-Id: I5d4beb9d1fa6914fef5e4c7b459cdd967e614b24 --- openstackclient/identity/v2_0/ec2creds.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openstackclient/identity/v2_0/ec2creds.py b/openstackclient/identity/v2_0/ec2creds.py index fd8eef8463..7a4dea6610 100644 --- a/openstackclient/identity/v2_0/ec2creds.py +++ b/openstackclient/identity/v2_0/ec2creds.py @@ -57,7 +57,7 @@ def take_action(self, parsed_args): ).id else: # Get the project from the current auth - project = identity_client.auth_tenant_id + project = self.app.client_manager.auth_ref.project_id if parsed_args.user: user = utils.find_resource( identity_client.users, @@ -65,7 +65,7 @@ def take_action(self, parsed_args): ).id else: # Get the user from the current auth - user = identity_client.auth_user_id + user = self.app.client_manager.auth_ref.user_id creds = identity_client.ec2.create(user, project) @@ -104,7 +104,7 @@ def take_action(self, parsed_args): ).id else: # Get the user from the current auth - user = identity_client.auth_user_id + user = self.app.client_manager.auth_ref.user_id identity_client.ec2.delete(user, parsed_args.access_key) @@ -134,7 +134,7 @@ def take_action(self, parsed_args): ).id else: # Get the user from the current auth - user = identity_client.auth_user_id + user = self.app.client_manager.auth_ref.user_id columns = ('access', 'secret', 'tenant_id', 'user_id') column_headers = ('Access', 'Secret', 'Project ID', 'User ID') @@ -177,7 +177,7 @@ def take_action(self, parsed_args): ).id else: # Get the user from the current auth - user = identity_client.auth_user_id + user = self.app.client_manager.auth_ref.user_id creds = identity_client.ec2.get(user, parsed_args.access_key) From 1a25cbaf8f2c1643181ef6233f72a57aaac5404d Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 5 Dec 2014 12:54:19 -0600 Subject: [PATCH 0278/3494] Followup for ec2 credentials command fix Add functional tests for 'ec2 credentials' commands. Also fix tenant_id in output for create and show. Change-Id: I6ba3249b67408571624709e17f8aa2ac6d80237d --- functional/tests/test_identity.py | 46 +++++++++++++++++++++++ openstackclient/identity/v2_0/ec2creds.py | 12 ++++++ 2 files changed, 58 insertions(+) diff --git a/functional/tests/test_identity.py b/functional/tests/test_identity.py index c5779a206d..b328115446 100644 --- a/functional/tests/test_identity.py +++ b/functional/tests/test_identity.py @@ -24,6 +24,19 @@ class IdentityV2Tests(test.TestCase): USER_FIELDS = ['email', 'enabled', 'id', 'name', 'project_id', 'username'] PROJECT_FIELDS = ['enabled', 'id', 'name', 'description'] + EC2_CREDENTIALS_FIELDS = [ + 'access', + 'project_id', + 'secret', + 'trust_id', + 'user_id', + ] + EC2_CREDENTIALS_LIST_HEADERS = [ + 'Access', + 'Secret', + 'Project ID', + 'User ID', + ] def test_user_list(self): raw_output = self.openstack('user list') @@ -70,6 +83,39 @@ def test_project_delete(self): raw_output = self.openstack('project delete dummy-project') self.assertEqual(0, len(raw_output)) + def test_ec2_credentials_create(self): + create_output = self.openstack('ec2 credentials create') + create_items = self.parse_show(create_output) + self.openstack( + 'ec2 credentials delete %s' % create_items[0]['access'], + ) + self.assert_show_fields(create_items, self.EC2_CREDENTIALS_FIELDS) + + def test_ec2_credentials_delete(self): + create_output = self.openstack('ec2 credentials create') + create_items = self.parse_show(create_output) + raw_output = self.openstack( + 'ec2 credentials delete %s' % create_items[0]['access'], + ) + self.assertEqual(0, len(raw_output)) + + def test_ec2_credentials_list(self): + raw_output = self.openstack('ec2 credentials list') + items = self.parse_listing(raw_output) + self.assert_table_structure(items, self.EC2_CREDENTIALS_LIST_HEADERS) + + def test_ec2_credentials_show(self): + create_output = self.openstack('ec2 credentials create') + create_items = self.parse_show(create_output) + show_output = self.openstack( + 'ec2 credentials show %s' % create_items[0]['access'], + ) + items = self.parse_show(show_output) + self.openstack( + 'ec2 credentials delete %s' % create_items[0]['access'], + ) + self.assert_show_fields(items, self.EC2_CREDENTIALS_FIELDS) + class IdentityV3Tests(test.TestCase): """Functional tests for Identity V3 commands. """ diff --git a/openstackclient/identity/v2_0/ec2creds.py b/openstackclient/identity/v2_0/ec2creds.py index 7a4dea6610..a20ffd4b57 100644 --- a/openstackclient/identity/v2_0/ec2creds.py +++ b/openstackclient/identity/v2_0/ec2creds.py @@ -71,6 +71,12 @@ def take_action(self, parsed_args): info = {} info.update(creds._info) + + if 'tenant_id' in info: + info.update( + {'project_id': info.pop('tenant_id')} + ) + return zip(*sorted(six.iteritems(info))) @@ -183,4 +189,10 @@ def take_action(self, parsed_args): info = {} info.update(creds._info) + + if 'tenant_id' in info: + info.update( + {'project_id': info.pop('tenant_id')} + ) + return zip(*sorted(six.iteritems(info))) From c7a5ead8c738c85016e49026b9e38c65018c3ba6 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Mon, 8 Dec 2014 12:40:22 -0600 Subject: [PATCH 0279/3494] Release 1.0.1 Fix 'ec2 credentials' regression Change-Id: Ieb22f6c535ff42a14162cafc88df6099486f9afe --- doc/source/releases.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/source/releases.rst b/doc/source/releases.rst index ad9c59e191..0f0f33f6e6 100644 --- a/doc/source/releases.rst +++ b/doc/source/releases.rst @@ -2,6 +2,14 @@ Release Notes ============= +1.0.1 (08 Dec 2014) +=================== + +* Bug 1399757_: EC2 credentials create fails + +.. _1399757: https://bugs.launchpad.net/bugs/1399757 + + 1.0.0 (04 Dec 2014) =================== From 52d22359f100c287c375edc73b6c4bd61be4c8da Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 4 Dec 2014 15:09:03 -0600 Subject: [PATCH 0280/3494] Tweaks after the fact Change-Id: Id96203de023b3b8bde1984a61c41dd9bc1711de4 --- doc/source/command-objects/server.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/source/command-objects/server.rst b/doc/source/command-objects/server.rst index 4f07632957..482602eefc 100644 --- a/doc/source/command-objects/server.rst +++ b/doc/source/command-objects/server.rst @@ -139,8 +139,8 @@ List servers os server list [--reservation-id ] - [--ip ] - [--ip6 ] + [--ip ] + [--ip6 ] [--name ] [--instance-name ] [--status ] @@ -210,9 +210,6 @@ Migrate server to different host [--wait] -:option:`--wait` - Wait for resize to complete - :option:`--live` Target hostname @@ -228,6 +225,9 @@ Migrate server to different host :option:`--no-disk-overcommit` Do not over-commit disk on the destination host (default) +:option:`--wait` + Wait for resize to complete + :option:`` Server to migrate (name or ID) From a4208a7201bac0d6f361864e6f37bfd6b3626978 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 12 Dec 2014 22:21:32 +0000 Subject: [PATCH 0281/3494] Updated from global requirements Change-Id: I3b1cd7aac5c9603dfaccbd4ae30d07cbf7c96da2 --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 82be8e39ab..35d2396d31 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,9 +8,9 @@ Babel>=1.3 cliff>=1.7.0 # Apache-2.0 cliff-tablib>=1.0 oslo.i18n>=1.0.0 # Apache-2.0 -oslo.utils>=1.0.0 # Apache-2.0 +oslo.utils>=1.1.0 # Apache-2.0 oslo.serialization>=1.0.0 # Apache-2.0 -python-glanceclient>=0.14.0 +python-glanceclient>=0.15.0 python-keystoneclient>=0.11.1 python-novaclient>=2.18.0 python-cinderclient>=1.1.0 From 381b47ff05559113ae99f61f3c230b806dcaa9ca Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 9 Dec 2014 14:47:51 -0500 Subject: [PATCH 0282/3494] list availability zones for compute Adds the command `os availability zone list` Change-Id: I77bf52a9b84a62c3771a4838c9ea0c3af03eedb2 Closes-Bug: #1400795 --- .../compute/v2/availability_zone.py | 102 ++++++++++++++++++ setup.cfg | 2 + 2 files changed, 104 insertions(+) create mode 100644 openstackclient/compute/v2/availability_zone.py diff --git a/openstackclient/compute/v2/availability_zone.py b/openstackclient/compute/v2/availability_zone.py new file mode 100644 index 0000000000..c7c9241491 --- /dev/null +++ b/openstackclient/compute/v2/availability_zone.py @@ -0,0 +1,102 @@ +# 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. +# + +"""Compute v2 Availability Zone action implementations""" + +import copy +import logging + +from cliff import lister +from novaclient import exceptions as nova_exceptions +import six + +from openstackclient.common import utils +from openstackclient.i18n import _ # noqa + + +def _xform_availability_zone(az, include_extra): + result = [] + zone_info = {} + if hasattr(az, 'zoneState'): + zone_info['zone_status'] = ('available' if az.zoneState['available'] + else 'not available') + if hasattr(az, 'zoneName'): + zone_info['zone_name'] = az.zoneName + + if not include_extra: + result.append(zone_info) + return result + + if hasattr(az, 'hosts') and az.hosts: + for host, services in six.iteritems(az.hosts): + host_info = copy.deepcopy(zone_info) + host_info['host_name'] = host + + for svc, state in six.iteritems(services): + info = copy.deepcopy(host_info) + info['service_name'] = svc + info['service_status'] = '%s %s %s' % ( + 'enabled' if state['active'] else 'disabled', + ':-)' if state['available'] else 'XXX', + state['updated_at']) + result.append(info) + else: + zone_info['host_name'] = '' + zone_info['service_name'] = '' + zone_info['service_status'] = '' + result.append(zone_info) + return result + + +class ListAvailabilityZone(lister.Lister): + """List Availability Zones and their status""" + + log = logging.getLogger(__name__ + '.ListAvailabilityZone') + + def get_parser(self, prog_name): + parser = super(ListAvailabilityZone, self).get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=_('List additional fields in output'), + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + + if parsed_args.long: + columns = ('Zone Name', 'Zone Status', + 'Host Name', 'Service Name', 'Service Status') + else: + columns = ('Zone Name', 'Zone Status') + + compute_client = self.app.client_manager.compute + try: + data = compute_client.availability_zones.list() + except nova_exceptions.Forbidden as e: # policy doesn't allow + try: + data = compute_client.availability_zones.list(detailed=False) + except Exception: + raise e + + # Argh, the availability zones are not iterable... + result = [] + for zone in data: + result += _xform_availability_zone(zone, parsed_args.long) + + return (columns, + (utils.get_dict_properties( + s, columns + ) for s in result)) diff --git a/setup.cfg b/setup.cfg index 02b7751d43..e1e5a1ec4f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -49,6 +49,8 @@ openstack.common = quota_show = openstackclient.common.quota:ShowQuota openstack.compute.v2 = + availability_zone_list = openstackclient.compute.v2.availability_zone:ListAvailabilityZone + compute_agent_create = openstackclient.compute.v2.agent:CreateAgent compute_agent_delete = openstackclient.compute.v2.agent:DeleteAgent compute_agent_list = openstackclient.compute.v2.agent:ListAgent From 25a7c1f27f1ec83edfe1e33e83116ca3eda3ba94 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Wed, 17 Dec 2014 18:17:56 +1000 Subject: [PATCH 0283/3494] Don't import form keystoneclient.openstack.common The keystoneclient.openstack.common directory is where we sync files from oslo incubator. It is not a public directory and should not be being consumed by openstackclient. Change-Id: I011bb95c2c824e2dbc4b822ca922ae77b8d9b955 --- openstackclient/api/api.py | 3 +-- openstackclient/compute/v2/security_group.py | 2 +- openstackclient/identity/v2_0/project.py | 2 +- openstackclient/identity/v2_0/role.py | 2 +- openstackclient/identity/v2_0/user.py | 2 +- openstackclient/identity/v3/domain.py | 2 +- openstackclient/identity/v3/group.py | 2 +- openstackclient/identity/v3/project.py | 2 +- openstackclient/identity/v3/role.py | 2 +- openstackclient/identity/v3/user.py | 2 +- openstackclient/tests/identity/v2_0/test_project.py | 2 +- openstackclient/tests/identity/v2_0/test_role.py | 2 +- openstackclient/tests/identity/v2_0/test_user.py | 2 +- 13 files changed, 13 insertions(+), 14 deletions(-) diff --git a/openstackclient/api/api.py b/openstackclient/api/api.py index 67386aaf0b..90b4e9c38f 100644 --- a/openstackclient/api/api.py +++ b/openstackclient/api/api.py @@ -15,8 +15,7 @@ import simplejson as json -from keystoneclient.openstack.common.apiclient \ - import exceptions as ksc_exceptions +from keystoneclient import exceptions as ksc_exceptions from keystoneclient import session as ksc_session from openstackclient.common import exceptions diff --git a/openstackclient/compute/v2/security_group.py b/openstackclient/compute/v2/security_group.py index cd33085707..f7ffb1d115 100644 --- a/openstackclient/compute/v2/security_group.py +++ b/openstackclient/compute/v2/security_group.py @@ -23,7 +23,7 @@ from cliff import lister from cliff import show -from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc +from keystoneclient import exceptions as ksc_exc from novaclient.v1_1 import security_group_rules from openstackclient.common import parseractions from openstackclient.common import utils diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index b2f99425c7..6450d814a7 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -21,7 +21,7 @@ from cliff import command from cliff import lister from cliff import show -from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc +from keystoneclient import exceptions as ksc_exc from openstackclient.common import parseractions from openstackclient.common import utils diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index cec95095cc..475baa2c5d 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -21,7 +21,7 @@ from cliff import command from cliff import lister from cliff import show -from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc +from keystoneclient import exceptions as ksc_exc from openstackclient.common import exceptions from openstackclient.common import utils diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index 955e19e770..7f1ea6da72 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -21,7 +21,7 @@ from cliff import command from cliff import lister from cliff import show -from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc +from keystoneclient import exceptions as ksc_exc from openstackclient.common import utils from openstackclient.i18n import _ # noqa diff --git a/openstackclient/identity/v3/domain.py b/openstackclient/identity/v3/domain.py index 1233fea5e9..9e50fe54c6 100644 --- a/openstackclient/identity/v3/domain.py +++ b/openstackclient/identity/v3/domain.py @@ -22,7 +22,7 @@ from cliff import command from cliff import lister from cliff import show -from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc +from keystoneclient import exceptions as ksc_exc from openstackclient.common import utils from openstackclient.i18n import _ # noqa diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index 5d3cf6422a..b617cd7693 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -22,7 +22,7 @@ from cliff import command from cliff import lister from cliff import show -from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc +from keystoneclient import exceptions as ksc_exc from openstackclient.common import utils from openstackclient.i18n import _ # noqa diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 2c2d408ec3..42412e410b 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -21,7 +21,7 @@ from cliff import command from cliff import lister from cliff import show -from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc +from keystoneclient import exceptions as ksc_exc from openstackclient.common import parseractions from openstackclient.common import utils diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index 017ffd769f..f86854bc4c 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -22,7 +22,7 @@ from cliff import command from cliff import lister from cliff import show -from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc +from keystoneclient import exceptions as ksc_exc from openstackclient.common import utils from openstackclient.i18n import _ # noqa diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 665dd4bb16..037af70e25 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -21,7 +21,7 @@ from cliff import command from cliff import lister from cliff import show -from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc +from keystoneclient import exceptions as ksc_exc from openstackclient.common import utils from openstackclient.i18n import _ # noqa diff --git a/openstackclient/tests/identity/v2_0/test_project.py b/openstackclient/tests/identity/v2_0/test_project.py index 833b5d84d8..bb69b99d24 100644 --- a/openstackclient/tests/identity/v2_0/test_project.py +++ b/openstackclient/tests/identity/v2_0/test_project.py @@ -15,7 +15,7 @@ import copy -from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc +from keystoneclient import exceptions as ksc_exc from openstackclient.identity.v2_0 import project from openstackclient.tests import fakes diff --git a/openstackclient/tests/identity/v2_0/test_role.py b/openstackclient/tests/identity/v2_0/test_role.py index 67467747f7..ee1f19cd8e 100644 --- a/openstackclient/tests/identity/v2_0/test_role.py +++ b/openstackclient/tests/identity/v2_0/test_role.py @@ -16,7 +16,7 @@ import copy import mock -from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc +from keystoneclient import exceptions as ksc_exc from openstackclient.common import exceptions from openstackclient.identity.v2_0 import role diff --git a/openstackclient/tests/identity/v2_0/test_user.py b/openstackclient/tests/identity/v2_0/test_user.py index a025dd7d1a..598e1d2b0f 100644 --- a/openstackclient/tests/identity/v2_0/test_user.py +++ b/openstackclient/tests/identity/v2_0/test_user.py @@ -16,7 +16,7 @@ import copy import mock -from keystoneclient.openstack.common.apiclient import exceptions as ksc_exc +from keystoneclient import exceptions as ksc_exc from openstackclient.identity.v2_0 import user from openstackclient.tests import fakes from openstackclient.tests.identity.v2_0 import fakes as identity_fakes From 71d9c8b5b343011bb0c9d90dab9cb75510d4c618 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 22 Dec 2014 13:20:41 -0500 Subject: [PATCH 0284/3494] Properly format 'attached to' column list when listing volumes Previously, no data was being returned for the 'attached to' field when listing volumes. Dig into the the returned array to format the column. Change-Id: Iebd79e5ddcb4a335703d9b2675aa7128995de918 Closes-Bug: #1404931 --- openstackclient/volume/v1/volume.py | 41 ++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/openstackclient/volume/v1/volume.py b/openstackclient/volume/v1/volume.py index 84c216d320..b78779bd71 100644 --- a/openstackclient/volume/v1/volume.py +++ b/openstackclient/volume/v1/volume.py @@ -221,6 +221,25 @@ def get_parser(self, prog_name): def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) + volume_client = self.app.client_manager.volume + compute_client = self.app.client_manager.compute + + def _format_attach(attachments): + """Return a formatted string of a volume's attached instances + + :param volume: a volume.attachments field + :rtype: a string of formatted instances + """ + + msg = '' + for attachment in attachments: + server = attachment['server_id'] + if server in server_cache.keys(): + server = server_cache[server].name + device = attachment['device'] + msg += 'Attached to %s on %s ' % (server, device) + return msg + if parsed_args.long: columns = ( 'ID', @@ -229,7 +248,7 @@ def take_action(self, parsed_args): 'Size', 'Volume Type', 'Bootable', - 'Attached to', + 'Attachments', 'Metadata', ) column_headers = ( @@ -239,7 +258,7 @@ def take_action(self, parsed_args): 'Size', 'Type', 'Bootable', - 'Attached', + 'Attached to', 'Properties', ) else: @@ -248,28 +267,38 @@ def take_action(self, parsed_args): 'Display Name', 'Status', 'Size', - 'Attached to', + 'Attachments', ) column_headers = ( 'ID', 'Display Name', 'Status', 'Size', - 'Attached', + 'Attached to', ) + + # Cache the server list + server_cache = {} + try: + for s in compute_client.servers.list(): + server_cache[s.id] = s + except Exception: + # Just forget it if there's any trouble + pass + search_opts = { 'all_tenants': parsed_args.all_projects, 'display_name': parsed_args.name, 'status': parsed_args.status, } - volume_client = self.app.client_manager.volume data = volume_client.volumes.list(search_opts=search_opts) return (column_headers, (utils.get_item_properties( s, columns, - formatters={'Metadata': utils.format_dict}, + formatters={'Metadata': utils.format_dict, + 'Attachments': _format_attach}, ) for s in data)) From 470b7e53a8d7e7ba088b934c49163412c8ee5ed9 Mon Sep 17 00:00:00 2001 From: wanghong Date: Wed, 10 Dec 2014 11:47:54 +0800 Subject: [PATCH 0285/3494] add multi-delete support for compute/image/net/volume This is part1, add support for these objects: compute.server imagev1.image imagev2.image network.network volume.volume volume.backup volume.snapshot Closes-Bug: #1400597 Change-Id: Ice21fee85203a8a55417e0ead8b509b8fd6705c1 --- doc/source/command-objects/server.rst | 8 +++---- openstackclient/compute/v2/server.py | 14 +++++++----- openstackclient/image/v1/image.py | 18 ++++++++------- openstackclient/image/v2/image.py | 18 ++++++++------- openstackclient/network/v2/network.py | 13 ++++++----- .../tests/compute/v2/test_server.py | 2 +- openstackclient/tests/image/v1/test_image.py | 2 +- openstackclient/tests/image/v2/test_image.py | 2 +- .../tests/network/v2/test_network.py | 2 +- openstackclient/volume/v1/backup.py | 14 +++++++----- openstackclient/volume/v1/snapshot.py | 14 +++++++----- openstackclient/volume/v1/volume.py | 22 ++++++++++--------- 12 files changed, 71 insertions(+), 58 deletions(-) diff --git a/doc/source/command-objects/server.rst b/doc/source/command-objects/server.rst index 482602eefc..7d5cdce56c 100644 --- a/doc/source/command-objects/server.rst +++ b/doc/source/command-objects/server.rst @@ -117,15 +117,15 @@ Create a new server :option:`` New server name -server delete -------------- +server(s) delete +---------------- -Delete server command +Delete server(s) command .. code:: bash os server delete - + [ ...] :option:`` Server (name or ID) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 5ab1d5f3ff..a5d8b0c30a 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -511,25 +511,27 @@ def take_action(self, parsed_args): class DeleteServer(command.Command): - """Delete server command""" + """Delete server(s)""" log = logging.getLogger(__name__ + '.DeleteServer') def get_parser(self, prog_name): parser = super(DeleteServer, self).get_parser(prog_name) parser.add_argument( - 'server', + 'servers', metavar='', - help=_('Server (name or ID)'), + nargs="+", + help=_('Server(s) to delete (name or ID)'), ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) compute_client = self.app.client_manager.compute - server = utils.find_resource( - compute_client.servers, parsed_args.server) - compute_client.servers.delete(server.id) + for server in parsed_args.servers: + server_obj = utils.find_resource( + compute_client.servers, server) + compute_client.servers.delete(server_obj.id) return diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index 32dd388c0f..ca1eead4d8 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -262,16 +262,17 @@ def take_action(self, parsed_args): class DeleteImage(command.Command): - """Delete an image""" + """Delete image(s)""" log = logging.getLogger(__name__ + ".DeleteImage") def get_parser(self, prog_name): parser = super(DeleteImage, self).get_parser(prog_name) parser.add_argument( - "image", + "images", metavar="", - help="Name or ID of image to delete", + nargs="+", + help="Image(s) to delete (name or ID)", ) return parser @@ -279,11 +280,12 @@ def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) image_client = self.app.client_manager.image - image = utils.find_resource( - image_client.images, - parsed_args.image, - ) - image_client.images.delete(image.id) + for image in parsed_args.images: + image_obj = utils.find_resource( + image_client.images, + image, + ) + image_client.images.delete(image_obj.id) class ListImage(lister.Lister): diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index c12ff11a09..63351c6d5e 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -27,16 +27,17 @@ class DeleteImage(command.Command): - """Delete an image""" + """Delete image(s)""" log = logging.getLogger(__name__ + ".DeleteImage") def get_parser(self, prog_name): parser = super(DeleteImage, self).get_parser(prog_name) parser.add_argument( - "image", + "images", metavar="", - help="Name or ID of image to delete", + nargs="+", + help="Image(s) to delete (name or ID)", ) return parser @@ -44,11 +45,12 @@ def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) image_client = self.app.client_manager.image - image = utils.find_resource( - image_client.images, - parsed_args.image, - ) - image_client.images.delete(image.id) + for image in parsed_args.images: + image_obj = utils.find_resource( + image_client.images, + image, + ) + image_client.images.delete(image_obj.id) class ListImage(lister.Lister): diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py index f34666ba88..0d68f82dc1 100644 --- a/openstackclient/network/v2/network.py +++ b/openstackclient/network/v2/network.py @@ -86,26 +86,27 @@ def get_body(self, parsed_args): class DeleteNetwork(command.Command): - """Delete a network""" + """Delete network(s)""" log = logging.getLogger(__name__ + '.DeleteNetwork') def get_parser(self, prog_name): parser = super(DeleteNetwork, self).get_parser(prog_name) parser.add_argument( - 'identifier', + 'networks', metavar="", - help=("Name or identifier of network to delete") + nargs="+", + help=("Network(s) to delete (name or ID)") ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) client = self.app.client_manager.network - _id = common.find(client, 'network', 'networks', - parsed_args.identifier) delete_method = getattr(client, "delete_network") - delete_method(_id) + for network in parsed_args.networks: + _id = common.find(client, 'network', 'networks', network) + delete_method(_id) return diff --git a/openstackclient/tests/compute/v2/test_server.py b/openstackclient/tests/compute/v2/test_server.py index 19c13440db..d51beef369 100644 --- a/openstackclient/tests/compute/v2/test_server.py +++ b/openstackclient/tests/compute/v2/test_server.py @@ -232,7 +232,7 @@ def test_server_delete_no_options(self): compute_fakes.server_id, ] verifylist = [ - ('server', compute_fakes.server_id), + ('servers', [compute_fakes.server_id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/image/v1/test_image.py b/openstackclient/tests/image/v1/test_image.py index a05669300e..4d21ac4858 100644 --- a/openstackclient/tests/image/v1/test_image.py +++ b/openstackclient/tests/image/v1/test_image.py @@ -288,7 +288,7 @@ def test_image_delete_no_options(self): image_fakes.image_id, ] verifylist = [ - ('image', image_fakes.image_id), + ('images', [image_fakes.image_id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/image/v2/test_image.py b/openstackclient/tests/image/v2/test_image.py index 3e9eeebb1d..bc61a89fb6 100644 --- a/openstackclient/tests/image/v2/test_image.py +++ b/openstackclient/tests/image/v2/test_image.py @@ -51,7 +51,7 @@ def test_image_delete_no_options(self): image_fakes.image_id, ] verifylist = [ - ('image', image_fakes.image_id), + ('images', [image_fakes.image_id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/network/v2/test_network.py b/openstackclient/tests/network/v2/test_network.py index 468db5e039..b893229f65 100644 --- a/openstackclient/tests/network/v2/test_network.py +++ b/openstackclient/tests/network/v2/test_network.py @@ -120,7 +120,7 @@ def test_delete(self): FAKE_NAME, ] verifylist = [ - ('identifier', FAKE_NAME), + ('networks', [FAKE_NAME]), ] lister = mock.Mock(return_value={RESOURCES: [RECORD]}) self.app.client_manager.network.list_networks = lister diff --git a/openstackclient/volume/v1/backup.py b/openstackclient/volume/v1/backup.py index 992fa7be71..8c16ba256f 100644 --- a/openstackclient/volume/v1/backup.py +++ b/openstackclient/volume/v1/backup.py @@ -73,25 +73,27 @@ def take_action(self, parsed_args): class DeleteBackup(command.Command): - """Delete backup command""" + """Delete backup(s)""" log = logging.getLogger(__name__ + '.DeleteBackup') def get_parser(self, prog_name): parser = super(DeleteBackup, self).get_parser(prog_name) parser.add_argument( - 'backup', + 'backups', metavar='', - help='Name or ID of backup to delete', + nargs="+", + help='Backup(s) to delete (name or ID)', ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume - backup_id = utils.find_resource(volume_client.backups, - parsed_args.backup).id - volume_client.backups.delete(backup_id) + for backup in parsed_args.backups: + backup_id = utils.find_resource(volume_client.backups, + backup).id + volume_client.backups.delete(backup_id) return diff --git a/openstackclient/volume/v1/snapshot.py b/openstackclient/volume/v1/snapshot.py index 9cc3c4c1d5..c9e1baca92 100644 --- a/openstackclient/volume/v1/snapshot.py +++ b/openstackclient/volume/v1/snapshot.py @@ -74,25 +74,27 @@ def take_action(self, parsed_args): class DeleteSnapshot(command.Command): - """Delete snapshot command""" + """Delete snapshot(s)""" log = logging.getLogger(__name__ + '.DeleteSnapshot') def get_parser(self, prog_name): parser = super(DeleteSnapshot, self).get_parser(prog_name) parser.add_argument( - 'snapshot', + 'snapshots', metavar='', - help='Name or ID of snapshot to delete', + nargs="+", + help='Snapshot(s) to delete (name or ID)', ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume - snapshot_id = utils.find_resource(volume_client.volume_snapshots, - parsed_args.snapshot).id - volume_client.volume_snapshots.delete(snapshot_id) + for snapshot in parsed_args.snapshots: + snapshot_id = utils.find_resource(volume_client.volume_snapshots, + snapshot).id + volume_client.volume_snapshots.delete(snapshot_id) return diff --git a/openstackclient/volume/v1/volume.py b/openstackclient/volume/v1/volume.py index 84c216d320..73ae3a7f9c 100644 --- a/openstackclient/volume/v1/volume.py +++ b/openstackclient/volume/v1/volume.py @@ -155,35 +155,37 @@ def take_action(self, parsed_args): class DeleteVolume(command.Command): - """Delete a volume""" + """Delete volume(s)""" log = logging.getLogger(__name__ + '.DeleteVolume') def get_parser(self, prog_name): parser = super(DeleteVolume, self).get_parser(prog_name) parser.add_argument( - 'volume', + 'volumes', metavar='', - help='Volume to delete (name or ID)', + nargs="+", + help='Volume(s) to delete (name or ID)', ) parser.add_argument( '--force', dest='force', action='store_true', default=False, - help='Attempt forced removal of a volume, regardless of state', + help='Attempt forced removal of volume(s), regardless of state', ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) volume_client = self.app.client_manager.volume - volume = utils.find_resource( - volume_client.volumes, parsed_args.volume) - if parsed_args.force: - volume_client.volumes.force_delete(volume.id) - else: - volume_client.volumes.delete(volume.id) + for volume in parsed_args.volumes: + volume_obj = utils.find_resource( + volume_client.volumes, volume) + if parsed_args.force: + volume_client.volumes.force_delete(volume_obj.id) + else: + volume_client.volumes.delete(volume_obj.id) return From d8f1cbd98461d4c2989384d29c7e2a99223468a9 Mon Sep 17 00:00:00 2001 From: wanghong Date: Wed, 10 Dec 2014 14:09:01 +0800 Subject: [PATCH 0286/3494] add multi-delete support for identity This is part2. Add support for these objects: identity.project(v2.0) identity.role(v2.0) identity.user(v2.0) identity.project(v3) identity.role(v3) identity.user(v3) identity.group(v3) Closes-Bug: #1400597 Change-Id: I270434d657cf4ddc23c3aba2c704d6ef184b0dbc --- doc/source/command-objects/project.rst | 10 ++++---- doc/source/command-objects/role.rst | 10 ++++---- doc/source/command-objects/user.rst | 10 ++++---- openstackclient/identity/v2_0/project.py | 19 +++++++------- openstackclient/identity/v2_0/role.py | 19 +++++++------- openstackclient/identity/v2_0/user.py | 19 +++++++------- openstackclient/identity/v3/group.py | 25 +++++++++++-------- openstackclient/identity/v3/project.py | 25 +++++++++++-------- openstackclient/identity/v3/role.py | 19 +++++++------- openstackclient/identity/v3/user.py | 25 +++++++++++-------- .../tests/identity/v2_0/test_project.py | 2 +- .../tests/identity/v2_0/test_role.py | 2 +- .../tests/identity/v2_0/test_user.py | 2 +- .../tests/identity/v3/test_project.py | 2 +- .../tests/identity/v3/test_role.py | 2 +- .../tests/identity/v3/test_user.py | 2 +- 16 files changed, 103 insertions(+), 90 deletions(-) diff --git a/doc/source/command-objects/project.rst b/doc/source/command-objects/project.rst index 305e611495..56f2196f53 100644 --- a/doc/source/command-objects/project.rst +++ b/doc/source/command-objects/project.rst @@ -47,16 +47,16 @@ Create new project New project name -project delete --------------- +project(s) delete +----------------- -Delete an existing project +Delete project(s) -.. program:: project delete +.. program:: project(s) delete .. code:: bash os project delete - + [ ...] .. option:: --domain diff --git a/doc/source/command-objects/role.rst b/doc/source/command-objects/role.rst index 1cc80d7d98..4c37fc38f8 100644 --- a/doc/source/command-objects/role.rst +++ b/doc/source/command-objects/role.rst @@ -56,16 +56,16 @@ Create new role New role name -role delete ------------ +role(s) delete +-------------- -Delete an existing role +Delete role(s) -.. program:: role delete +.. program:: role(s) delete .. code:: bash os role delete - + [ ...] .. option:: diff --git a/doc/source/command-objects/user.rst b/doc/source/command-objects/user.rst index 24a2725b7d..281e8e0d86 100644 --- a/doc/source/command-objects/user.rst +++ b/doc/source/command-objects/user.rst @@ -69,16 +69,16 @@ Create new user New user name -user delete ------------ +user(s) delete +-------------- -Delete user +Delete user(s) -.. program:: user delete +.. program:: user(s) delete .. code:: bash os user delete - + [ ...] .. option:: --domain diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index 6450d814a7..9b195600fc 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -104,16 +104,17 @@ def take_action(self, parsed_args): class DeleteProject(command.Command): - """Delete an existing project""" + """Delete project(s)""" log = logging.getLogger(__name__ + '.DeleteProject') def get_parser(self, prog_name): parser = super(DeleteProject, self).get_parser(prog_name) parser.add_argument( - 'project', + 'projects', metavar='', - help=_('Project to delete (name or ID)'), + nargs="+", + help=_('Project(s) to delete (name or ID)'), ) return parser @@ -121,12 +122,12 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - project = utils.find_resource( - identity_client.tenants, - parsed_args.project, - ) - - identity_client.tenants.delete(project.id) + for project in parsed_args.projects: + project_obj = utils.find_resource( + identity_client.tenants, + project, + ) + identity_client.tenants.delete(project_obj.id) return diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index 475baa2c5d..d03664e07b 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -114,16 +114,17 @@ def take_action(self, parsed_args): class DeleteRole(command.Command): - """Delete an existing role""" + """Delete role(s)""" log = logging.getLogger(__name__ + '.DeleteRole') def get_parser(self, prog_name): parser = super(DeleteRole, self).get_parser(prog_name) parser.add_argument( - 'role', + 'roles', metavar='', - help=_('Role to delete (name or ID)'), + nargs="+", + help=_('Role(s) to delete (name or ID)'), ) return parser @@ -131,12 +132,12 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - role = utils.find_resource( - identity_client.roles, - parsed_args.role, - ) - - identity_client.roles.delete(role.id) + for role in parsed_args.roles: + role_obj = utils.find_resource( + identity_client.roles, + role, + ) + identity_client.roles.delete(role_obj.id) return diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index 7f1ea6da72..b5bbce3bd3 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -128,16 +128,17 @@ def take_action(self, parsed_args): class DeleteUser(command.Command): - """Delete user""" + """Delete user(s)""" log = logging.getLogger(__name__ + '.DeleteUser') def get_parser(self, prog_name): parser = super(DeleteUser, self).get_parser(prog_name) parser.add_argument( - 'user', + 'users', metavar='', - help=_('User to delete (name or ID)'), + nargs="+", + help=_('User(s) to delete (name or ID)'), ) return parser @@ -145,12 +146,12 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - user = utils.find_resource( - identity_client.users, - parsed_args.user, - ) - - identity_client.users.delete(user.id) + for user in parsed_args.users: + user_obj = utils.find_resource( + identity_client.users, + user, + ) + identity_client.users.delete(user_obj.id) return diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index b617cd7693..327a64d5c0 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -159,16 +159,17 @@ def take_action(self, parsed_args): class DeleteGroup(command.Command): - """Delete group command""" + """Delete group(s)""" log = logging.getLogger(__name__ + '.DeleteGroup') def get_parser(self, prog_name): parser = super(DeleteGroup, self).get_parser(prog_name) parser.add_argument( - 'group', + 'groups', metavar='', - help='Name or ID of group to delete') + nargs="+", + help='Group(s) to delete (name or ID)') parser.add_argument( '--domain', metavar='', @@ -180,16 +181,18 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity + domain = None if parsed_args.domain: domain = common.find_domain(identity_client, parsed_args.domain) - group = utils.find_resource(identity_client.groups, - parsed_args.group, - domain_id=domain.id) - else: - group = utils.find_resource(identity_client.groups, - parsed_args.group) - - identity_client.groups.delete(group.id) + for group in parsed_args.groups: + if domain is not None: + group_obj = utils.find_resource(identity_client.groups, + group, + domain_id=domain.id) + else: + group_obj = utils.find_resource(identity_client.groups, + group) + identity_client.groups.delete(group_obj.id) return diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 42412e410b..28eb427716 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -117,16 +117,17 @@ def take_action(self, parsed_args): class DeleteProject(command.Command): - """Delete an existing project""" + """Delete project(s)""" log = logging.getLogger(__name__ + '.DeleteProject') def get_parser(self, prog_name): parser = super(DeleteProject, self).get_parser(prog_name) parser.add_argument( - 'project', + 'projects', metavar='', - help='Project to delete (name or ID)', + nargs="+", + help='Project(s) to delete (name or ID)', ) parser.add_argument( '--domain', @@ -139,16 +140,18 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity + domain = None if parsed_args.domain: domain = common.find_domain(identity_client, parsed_args.domain) - project = utils.find_resource(identity_client.projects, - parsed_args.project, - domain_id=domain.id) - else: - project = utils.find_resource(identity_client.projects, - parsed_args.project) - - identity_client.projects.delete(project.id) + 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) return diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index f86854bc4c..d680278eea 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -177,16 +177,17 @@ def take_action(self, parsed_args): class DeleteRole(command.Command): - """Delete an existing role""" + """Delete role(s)""" log = logging.getLogger(__name__ + '.DeleteRole') def get_parser(self, prog_name): parser = super(DeleteRole, self).get_parser(prog_name) parser.add_argument( - 'role', + 'roles', metavar='', - help='Role to delete (name or ID)', + nargs="+", + help='Role(s) to delete (name or ID)', ) return parser @@ -194,12 +195,12 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity - role = utils.find_resource( - identity_client.roles, - parsed_args.role, - ) - - identity_client.roles.delete(role.id) + for role in parsed_args.roles: + role_obj = utils.find_resource( + identity_client.roles, + role, + ) + identity_client.roles.delete(role_obj.id) return diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 037af70e25..dc5468ff3a 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -137,16 +137,17 @@ def take_action(self, parsed_args): class DeleteUser(command.Command): - """Delete user""" + """Delete user(s)""" log = logging.getLogger(__name__ + '.DeleteUser') def get_parser(self, prog_name): parser = super(DeleteUser, self).get_parser(prog_name) parser.add_argument( - 'user', + 'users', metavar='', - help='User to delete (name or ID)', + nargs="+", + help='User(s) to delete (name or ID)', ) parser.add_argument( '--domain', @@ -159,16 +160,18 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity + domain = None if parsed_args.domain: domain = common.find_domain(identity_client, parsed_args.domain) - user = utils.find_resource(identity_client.users, - parsed_args.user, - domain_id=domain.id) - else: - user = utils.find_resource(identity_client.users, - parsed_args.user) - - identity_client.users.delete(user.id) + 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) return diff --git a/openstackclient/tests/identity/v2_0/test_project.py b/openstackclient/tests/identity/v2_0/test_project.py index bb69b99d24..0c5ef77f62 100644 --- a/openstackclient/tests/identity/v2_0/test_project.py +++ b/openstackclient/tests/identity/v2_0/test_project.py @@ -326,7 +326,7 @@ def test_project_delete_no_options(self): identity_fakes.project_id, ] verifylist = [ - ('project', identity_fakes.project_id), + ('projects', [identity_fakes.project_id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/identity/v2_0/test_role.py b/openstackclient/tests/identity/v2_0/test_role.py index ee1f19cd8e..2e3a286375 100644 --- a/openstackclient/tests/identity/v2_0/test_role.py +++ b/openstackclient/tests/identity/v2_0/test_role.py @@ -234,7 +234,7 @@ def test_role_delete_no_options(self): identity_fakes.role_name, ] verifylist = [ - ('role', identity_fakes.role_name), + ('roles', [identity_fakes.role_name]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/identity/v2_0/test_user.py b/openstackclient/tests/identity/v2_0/test_user.py index 598e1d2b0f..ccdf240ec1 100644 --- a/openstackclient/tests/identity/v2_0/test_user.py +++ b/openstackclient/tests/identity/v2_0/test_user.py @@ -443,7 +443,7 @@ def test_user_delete_no_options(self): identity_fakes.user_id, ] verifylist = [ - ('user', identity_fakes.user_id), + ('users', [identity_fakes.user_id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/identity/v3/test_project.py b/openstackclient/tests/identity/v3/test_project.py index 1060a27795..d16f9732f2 100644 --- a/openstackclient/tests/identity/v3/test_project.py +++ b/openstackclient/tests/identity/v3/test_project.py @@ -353,7 +353,7 @@ def test_project_delete_no_options(self): identity_fakes.project_id, ] verifylist = [ - ('project', identity_fakes.project_id), + ('projects', [identity_fakes.project_id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/identity/v3/test_role.py b/openstackclient/tests/identity/v3/test_role.py index 3d2a402bc7..1a9e6aa7ef 100644 --- a/openstackclient/tests/identity/v3/test_role.py +++ b/openstackclient/tests/identity/v3/test_role.py @@ -271,7 +271,7 @@ def test_role_delete_no_options(self): identity_fakes.role_name, ] verifylist = [ - ('role', identity_fakes.role_name), + ('roles', [identity_fakes.role_name]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/identity/v3/test_user.py b/openstackclient/tests/identity/v3/test_user.py index bb59ebe568..9740ed82f2 100644 --- a/openstackclient/tests/identity/v3/test_user.py +++ b/openstackclient/tests/identity/v3/test_user.py @@ -461,7 +461,7 @@ def test_user_delete_no_options(self): identity_fakes.user_id, ] verifylist = [ - ('user', identity_fakes.user_id), + ('users', [identity_fakes.user_id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) From ea53cc357a1f5d783b128cb9562ebc6c14203105 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 23 Dec 2014 16:30:47 -0600 Subject: [PATCH 0287/3494] Revert some docs changes from multi-delete The headers in the doc files are the commands, not a description. I missed thiese in the original reviews: https://review.openstack.org/140567 https://review.openstack.org/140581 Change-Id: Iae2631f6b485e8c568ff305e5992c193f80ebe71 --- doc/source/command-objects/project.rst | 6 +++--- doc/source/command-objects/role.rst | 6 +++--- doc/source/command-objects/server.rst | 8 ++++---- doc/source/command-objects/user.rst | 6 +++--- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/doc/source/command-objects/project.rst b/doc/source/command-objects/project.rst index 56f2196f53..6b55b424bf 100644 --- a/doc/source/command-objects/project.rst +++ b/doc/source/command-objects/project.rst @@ -47,12 +47,12 @@ Create new project New project name -project(s) delete ------------------ +project delete +-------------- Delete project(s) -.. program:: project(s) delete +.. program:: project delete .. code:: bash os project delete diff --git a/doc/source/command-objects/role.rst b/doc/source/command-objects/role.rst index 4c37fc38f8..19195eb554 100644 --- a/doc/source/command-objects/role.rst +++ b/doc/source/command-objects/role.rst @@ -56,12 +56,12 @@ Create new role New role name -role(s) delete --------------- +role delete +----------- Delete role(s) -.. program:: role(s) delete +.. program:: role delete .. code:: bash os role delete diff --git a/doc/source/command-objects/server.rst b/doc/source/command-objects/server.rst index 7d5cdce56c..2f5aef1070 100644 --- a/doc/source/command-objects/server.rst +++ b/doc/source/command-objects/server.rst @@ -117,10 +117,10 @@ Create a new server :option:`` New server name -server(s) delete ----------------- +server delete +------------- -Delete server(s) command +Delete server(s) .. code:: bash @@ -128,7 +128,7 @@ Delete server(s) command [ ...] :option:`` - Server (name or ID) + Server to delete (name or ID) server list ----------- diff --git a/doc/source/command-objects/user.rst b/doc/source/command-objects/user.rst index 281e8e0d86..e54c65673c 100644 --- a/doc/source/command-objects/user.rst +++ b/doc/source/command-objects/user.rst @@ -69,12 +69,12 @@ Create new user New user name -user(s) delete --------------- +user delete +----------- Delete user(s) -.. program:: user(s) delete +.. program:: user delete .. code:: bash os user delete From 36ab944d2ecac5227880a6b09b4184bff4c0aba8 Mon Sep 17 00:00:00 2001 From: lin-hua-cheng Date: Mon, 22 Dec 2014 15:09:09 -0800 Subject: [PATCH 0288/3494] Allow service description to be set for KS V3 Change-Id: Ibf84882c9a9f408268c225190436fc1a534e1017 Closes-Bug: #1404997 --- openstackclient/identity/v3/service.py | 14 +++ openstackclient/tests/identity/v3/fakes.py | 2 + .../tests/identity/v3/test_service.py | 87 ++++++++++++++++++- 3 files changed, 99 insertions(+), 4 deletions(-) diff --git a/openstackclient/identity/v3/service.py b/openstackclient/identity/v3/service.py index 4f62226968..f4c5d42629 100644 --- a/openstackclient/identity/v3/service.py +++ b/openstackclient/identity/v3/service.py @@ -43,6 +43,11 @@ def get_parser(self, prog_name): metavar='', help='New service name', ) + parser.add_argument( + '--description', + metavar='', + help='New service description', + ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', @@ -67,6 +72,7 @@ def take_action(self, parsed_args): service = identity_client.services.create( name=parsed_args.name, type=parsed_args.type, + description=parsed_args.description, enabled=enabled, ) @@ -137,6 +143,11 @@ def get_parser(self, prog_name): metavar='', help='New service name', ) + parser.add_argument( + '--description', + metavar='', + help='New service description', + ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', @@ -156,6 +167,7 @@ def take_action(self, parsed_args): if (not parsed_args.name and not parsed_args.type + and not parsed_args.description and not parsed_args.enable and not parsed_args.disable): return @@ -167,6 +179,8 @@ def take_action(self, parsed_args): kwargs['type'] = parsed_args.type if parsed_args.name: kwargs['name'] = parsed_args.name + if parsed_args.description: + kwargs['description'] = parsed_args.description if parsed_args.enable: kwargs['enabled'] = True if parsed_args.disable: diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index 7acaa7f156..fbdac4ed15 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -147,11 +147,13 @@ service_id = 's-123' service_name = 'Texaco' service_type = 'gas' +service_description = 'oil brand' SERVICE = { 'id': service_id, 'name': service_name, 'type': service_type, + 'description': service_description, 'enabled': True, 'links': base_url + 'services/' + service_id, } diff --git a/openstackclient/tests/identity/v3/test_service.py b/openstackclient/tests/identity/v3/test_service.py index 57db77b12a..5e4dc58508 100644 --- a/openstackclient/tests/identity/v3/test_service.py +++ b/openstackclient/tests/identity/v3/test_service.py @@ -51,6 +51,7 @@ def test_service_create_name(self): ] verifylist = [ ('name', identity_fakes.service_name), + ('description', None), ('enable', False), ('disable', False), ('type', identity_fakes.service_type), @@ -64,12 +65,50 @@ def test_service_create_name(self): self.services_mock.create.assert_called_with( name=identity_fakes.service_name, type=identity_fakes.service_type, + description=None, enabled=True, ) - collist = ('enabled', 'id', 'name', 'type') + collist = ('description', 'enabled', 'id', 'name', 'type') self.assertEqual(columns, collist) datalist = ( + identity_fakes.service_description, + True, + identity_fakes.service_id, + identity_fakes.service_name, + identity_fakes.service_type, + ) + self.assertEqual(data, datalist) + + def test_service_create_description(self): + arglist = [ + '--description', identity_fakes.service_description, + identity_fakes.service_type, + ] + verifylist = [ + ('name', None), + ('description', identity_fakes.service_description), + ('enable', False), + ('disable', False), + ('type', identity_fakes.service_type), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # ServiceManager.create(name=, type=, enabled=, **kwargs) + self.services_mock.create.assert_called_with( + name=None, + type=identity_fakes.service_type, + description=identity_fakes.service_description, + enabled=True, + ) + + collist = ('description', 'enabled', 'id', 'name', 'type') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.service_description, True, identity_fakes.service_id, identity_fakes.service_name, @@ -84,6 +123,7 @@ def test_service_create_enable(self): ] verifylist = [ ('name', None), + ('description', None), ('enable', True), ('disable', False), ('type', identity_fakes.service_type), @@ -97,12 +137,14 @@ def test_service_create_enable(self): self.services_mock.create.assert_called_with( name=None, type=identity_fakes.service_type, + description=None, enabled=True, ) - collist = ('enabled', 'id', 'name', 'type') + collist = ('description', 'enabled', 'id', 'name', 'type') self.assertEqual(columns, collist) datalist = ( + identity_fakes.service_description, True, identity_fakes.service_id, identity_fakes.service_name, @@ -117,6 +159,7 @@ def test_service_create_disable(self): ] verifylist = [ ('name', None), + ('description', None), ('enable', False), ('disable', True), ('type', identity_fakes.service_type), @@ -130,12 +173,14 @@ def test_service_create_disable(self): self.services_mock.create.assert_called_with( name=None, type=identity_fakes.service_type, + description=None, enabled=False, ) - collist = ('enabled', 'id', 'name', 'type') + collist = ('description', 'enabled', 'id', 'name', 'type') self.assertEqual(columns, collist) datalist = ( + identity_fakes.service_description, True, identity_fakes.service_id, identity_fakes.service_name, @@ -239,6 +284,7 @@ def test_service_set_no_options(self): verifylist = [ ('type', None), ('name', None), + ('description', None), ('enable', False), ('disable', False), ('service', identity_fakes.service_name), @@ -256,6 +302,7 @@ def test_service_set_type(self): verifylist = [ ('type', identity_fakes.service_type), ('name', None), + ('description', None), ('enable', False), ('disable', False), ('service', identity_fakes.service_name), @@ -283,6 +330,7 @@ def test_service_set_name(self): verifylist = [ ('type', None), ('name', identity_fakes.service_name), + ('description', None), ('enable', False), ('disable', False), ('service', identity_fakes.service_name), @@ -302,6 +350,34 @@ def test_service_set_name(self): **kwargs ) + def test_service_set_description(self): + arglist = [ + '--description', identity_fakes.service_description, + identity_fakes.service_name, + ] + verifylist = [ + ('type', None), + ('name', None), + ('description', identity_fakes.service_description), + ('enable', False), + ('disable', False), + ('service', identity_fakes.service_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.run(parsed_args) + self.assertEqual(result, 0) + + # Set expected values + kwargs = { + 'description': identity_fakes.service_description, + } + # ServiceManager.update(service, name=, type=, enabled=, **kwargs) + self.services_mock.update.assert_called_with( + identity_fakes.service_id, + **kwargs + ) + def test_service_set_enable(self): arglist = [ '--enable', @@ -310,6 +386,7 @@ def test_service_set_enable(self): verifylist = [ ('type', None), ('name', None), + ('description', None), ('enable', True), ('disable', False), ('service', identity_fakes.service_name), @@ -337,6 +414,7 @@ def test_service_set_disable(self): verifylist = [ ('type', None), ('name', None), + ('description', None), ('enable', False), ('disable', True), ('service', identity_fakes.service_name), @@ -388,9 +466,10 @@ def test_service_show(self): identity_fakes.service_name, ) - collist = ('enabled', 'id', 'name', 'type') + collist = ('description', 'enabled', 'id', 'name', 'type') self.assertEqual(columns, collist) datalist = ( + identity_fakes.service_description, True, identity_fakes.service_id, identity_fakes.service_name, From d240b709b9f2638382daa8b18227b9baf021596d Mon Sep 17 00:00:00 2001 From: wanghong Date: Tue, 23 Dec 2014 11:08:24 +0800 Subject: [PATCH 0289/3494] add doc for domain command Change-Id: I8b5575a5f27362fa375746b955e1f17a5a8b29a6 --- doc/source/command-objects/domain.rst | 115 ++++++++++++++++++++++++++ openstackclient/identity/v3/domain.py | 24 +++--- 2 files changed, 127 insertions(+), 12 deletions(-) create mode 100644 doc/source/command-objects/domain.rst diff --git a/doc/source/command-objects/domain.rst b/doc/source/command-objects/domain.rst new file mode 100644 index 0000000000..057296ac5e --- /dev/null +++ b/doc/source/command-objects/domain.rst @@ -0,0 +1,115 @@ +====== +domain +====== + +Identity v3 + +domain create +------------- + +Create new domain + +.. program:: domain create +.. code:: bash + + os domain create + [--description ] + [--enable | --disable] + [--or-show] + + +.. option:: --description + + New domain description + +.. option:: --enable + + Enable domain (default) + +.. option:: --disable + + Disable domain + +.. option:: --or-show + + Return existing domain + + If the domain already exists, return the existing domain data and do not fail. + +.. option:: + + New domain name + +domain delete +------------- + +Delete domain + +.. program:: domain delete +.. code:: bash + + os domain delete + + +.. option:: + + Domain to delete (name or ID) + +domain list +----------- + +List domains + +.. program:: domain list +.. code:: bash + + os domain list + +domain set +---------- + +Set domain properties + +.. program:: domain set +.. code:: bash + + os domain set + [--name ] + [--description ] + [--enable | --disable] + + +.. option:: --name + + New domain name + +.. option:: --description + + New domain description + +.. option:: --enable + + Enable domain + +.. option:: --disable + + Disable domain + +.. option:: + + Domain to modify (name or ID) + +domain show +----------- + +Show domain details + +.. program:: domain show +.. code:: bash + + os domain show + + +.. option:: + + Domain to display (name or ID) diff --git a/openstackclient/identity/v3/domain.py b/openstackclient/identity/v3/domain.py index 9e50fe54c6..727f5b1838 100644 --- a/openstackclient/identity/v3/domain.py +++ b/openstackclient/identity/v3/domain.py @@ -29,7 +29,7 @@ class CreateDomain(show.ShowOne): - """Create domain command""" + """Create new domain""" log = logging.getLogger(__name__ + '.CreateDomain') @@ -42,7 +42,7 @@ def get_parser(self, prog_name): ) parser.add_argument( '--description', - metavar='', + metavar='', help='New domain description', ) enable_group = parser.add_mutually_exclusive_group() @@ -87,7 +87,7 @@ def take_action(self, parsed_args): class DeleteDomain(command.Command): - """Delete domain command""" + """Delete domain""" log = logging.getLogger(__name__ + '.DeleteDomain') @@ -96,7 +96,7 @@ def get_parser(self, prog_name): parser.add_argument( 'domain', metavar='', - help='Name or ID of domain to delete', + help='Domain to delete (name or ID)', ) return parser @@ -110,7 +110,7 @@ def take_action(self, parsed_args): class ListDomain(lister.Lister): - """List domain command""" + """List domains""" log = logging.getLogger(__name__ + '.ListDomain') @@ -126,7 +126,7 @@ def take_action(self, parsed_args): class SetDomain(command.Command): - """Set domain command""" + """Set domain properties""" log = logging.getLogger(__name__ + '.SetDomain') @@ -135,16 +135,16 @@ def get_parser(self, prog_name): parser.add_argument( 'domain', metavar='', - help='Name or ID of domain to change', + help='Domain to modify (name or ID)', ) parser.add_argument( '--name', - metavar='', + metavar='', help='New domain name', ) parser.add_argument( '--description', - metavar='', + metavar='', help='New domain description', ) enable_group = parser.add_mutually_exclusive_group() @@ -152,7 +152,7 @@ def get_parser(self, prog_name): '--enable', dest='enabled', action='store_true', - help='Enable domain (default)', + help='Enable domain', ) enable_group.add_argument( '--disable', @@ -185,7 +185,7 @@ def take_action(self, parsed_args): class ShowDomain(show.ShowOne): - """Show domain command""" + """Show domain details""" log = logging.getLogger(__name__ + '.ShowDomain') @@ -194,7 +194,7 @@ def get_parser(self, prog_name): parser.add_argument( 'domain', metavar='', - help='Name or ID of domain to display', + help='Domain to display (name or ID)', ) return parser From e3ba13b32091831a7e5750d670921fce29424067 Mon Sep 17 00:00:00 2001 From: wanghong Date: Tue, 23 Dec 2014 11:43:02 +0800 Subject: [PATCH 0290/3494] add doc for role assignment command Change-Id: I594d444b6d1ec4e72bed03394178293737f26069 --- .../command-objects/role_assignment.rst | 45 +++++++++++++++++++ .../identity/v3/role_assignment.py | 12 ++--- 2 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 doc/source/command-objects/role_assignment.rst diff --git a/doc/source/command-objects/role_assignment.rst b/doc/source/command-objects/role_assignment.rst new file mode 100644 index 0000000000..749d883e54 --- /dev/null +++ b/doc/source/command-objects/role_assignment.rst @@ -0,0 +1,45 @@ +=============== +role assignment +=============== + +Identity v3 + +role assignment list +-------------------- + +List role assignments + +.. program:: role assignment list +.. code:: bash + + os role assignment list + [--role ] + [--user ] + [--group ] + [--domain ] + [--project ] + [--effective] + +.. option:: --role + + Role to filter (name or ID) + +.. option:: --user + + User to filter (name or ID) + +.. option:: --group + + Group to filter (name or ID) + +.. option:: --domain + + Domain to filter (name or ID) + +.. option:: --project + + Project to filter (name or ID) + +.. option:: --effective + + Returns only effective role assignments (defaults to False) diff --git a/openstackclient/identity/v3/role_assignment.py b/openstackclient/identity/v3/role_assignment.py index 5cc97e8d0f..f053b608c5 100644 --- a/openstackclient/identity/v3/role_assignment.py +++ b/openstackclient/identity/v3/role_assignment.py @@ -21,7 +21,7 @@ class ListRoleAssignment(lister.Lister): - """Lists role assignments according to the given filters""" + """List role assignments""" log = logging.getLogger(__name__ + '.ListRoleAssignment') @@ -36,29 +36,29 @@ def get_parser(self, prog_name): parser.add_argument( '--role', metavar='', - help='Name or ID of role to filter', + help='Role to filter (name or ID)', ) user_or_group = parser.add_mutually_exclusive_group() user_or_group.add_argument( '--user', metavar='', - help='Name or ID of user to filter', + help='User to filter (name or ID)', ) user_or_group.add_argument( '--group', metavar='', - help='Name or ID of group to filter', + help='Group to filter (name or ID)', ) domain_or_project = parser.add_mutually_exclusive_group() domain_or_project.add_argument( '--domain', metavar='', - help='Name or ID of domain to filter', + help='Domain to filter (name or ID)', ) domain_or_project.add_argument( '--project', metavar='', - help='Name or ID of project to filter', + help='Project to filter (name or ID)', ) return parser From e5d71221adb17d05d14a1a692aed39dfc217151f Mon Sep 17 00:00:00 2001 From: wanghong Date: Tue, 23 Dec 2014 12:14:32 +0800 Subject: [PATCH 0291/3494] add doc for group command Change-Id: Iaaa0aeb42f9f940af63863f5d09011b5f7529281 --- doc/source/command-objects/group.rst | 192 +++++++++++++++++++++++++++ openstackclient/identity/v3/group.py | 36 ++--- 2 files changed, 210 insertions(+), 18 deletions(-) create mode 100644 doc/source/command-objects/group.rst diff --git a/doc/source/command-objects/group.rst b/doc/source/command-objects/group.rst new file mode 100644 index 0000000000..85a0c5cd79 --- /dev/null +++ b/doc/source/command-objects/group.rst @@ -0,0 +1,192 @@ +===== +group +===== + +Identity v3 + +group add user +-------------- + +Add user to group + +.. program:: group add user +.. code:: bash + + os group add user + + + +.. option:: + + Group that user will be added to (name or ID) + +.. option:: + + User to add to group (name or ID) + +group contains user +------------------- + +Check user in group + +.. program:: group contains user +.. code:: bash + + os group contains user + + + +.. option:: + + Group to check if user belongs to (name or ID) + +.. option:: + + User to check (name or ID) + +group create +------------ + +Create new group + +.. program:: group create +.. code:: bash + + os group create + [--domain ] + [--description ] + [--or-show] + + +.. option:: --domain + + References the domain ID or name which owns the group + +.. option:: --description + + New group description + +.. option:: --or-show + + Return existing group + + If the group already exists, return the existing group data and do not fail. + +.. option:: + + New group name + +group delete +------------ + +Delete group + +.. program:: group delete +.. code:: bash + + os group delete + [--domain ] + [ ...] + +.. option:: --domain + + Domain where group resides (name or ID) + +.. option:: + + Group(s) to delete (name or ID) + +group list +---------- + +List groups + +.. program:: group list +.. code:: bash + + os group list + [--domain ] + [--user ] + [--long] + +.. option:: --domain + + Filter group list by (name or ID) + +.. option:: --user + + List group memberships for (name or ID) + +.. option:: --long + + List additional fields in output (defaults to false) + +group remove user +----------------- + +Remove user from group + +.. program:: group remove user +.. code:: bash + + os group remove user + + + +.. option:: + + Group that user will be removed from (name or ID) + +.. option:: + + User to remove from group (name or ID) + +group set +--------- + +Set group properties + +.. program:: group set +.. code:: bash + + os group set + [--name ] + [--domain ] + [--description ] + + +.. option:: --name + + New group name + +.. option:: --domain + + New domain that will now own the group (name or ID) + +.. option:: --description + + New group description + +.. option:: + + Group to modify (name or ID) + +group show +---------- + +Show group details + +.. program:: group show +.. code:: bash + + os group show + [--domain ] + + +.. option:: --domain + + Domain where group resides (name or ID) + +.. option:: + + Group to display (name or ID) diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index 327a64d5c0..fbd8dd720b 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -39,12 +39,12 @@ def get_parser(self, prog_name): parser.add_argument( 'group', metavar='', - help='Group name or ID that user will be added to', + help='Group that user will be added to (name or ID)', ) parser.add_argument( 'user', metavar='', - help='User name or ID to add to group', + help='User to add to group (name or ID)', ) return parser @@ -68,7 +68,7 @@ def take_action(self, parsed_args): class CheckUserInGroup(command.Command): - """Checks that user is in a specific group""" + """Check user in group""" log = logging.getLogger(__name__ + '.CheckUserInGroup') @@ -77,12 +77,12 @@ def get_parser(self, prog_name): parser.add_argument( 'group', metavar='', - help='Group name or ID that user will be added to', + help='Group to check if user belongs to (name or ID)', ) parser.add_argument( 'user', metavar='', - help='User name or ID to add to group', + help='User to check (name or ID)', ) return parser @@ -106,7 +106,7 @@ def take_action(self, parsed_args): class CreateGroup(show.ShowOne): - """Create group command""" + """Create new group""" log = logging.getLogger(__name__ + '.CreateGroup') @@ -118,11 +118,11 @@ def get_parser(self, prog_name): help='New group name') parser.add_argument( '--description', - metavar='', + metavar='', help='New group description') parser.add_argument( '--domain', - metavar='', + metavar='', help='References the domain ID or name which owns the group') parser.add_argument( '--or-show', @@ -268,12 +268,12 @@ def get_parser(self, prog_name): parser.add_argument( 'group', metavar='', - help='Group name or ID that user will be removed from', + help='Group that user will be removed from (name or ID)', ) parser.add_argument( 'user', metavar='', - help='User name or ID to remove from group', + help='User to remove from group (name or ID)', ) return parser @@ -297,7 +297,7 @@ def take_action(self, parsed_args): class SetGroup(command.Command): - """Set group command""" + """Set group properties""" log = logging.getLogger(__name__ + '.SetGroup') @@ -306,18 +306,18 @@ def get_parser(self, prog_name): parser.add_argument( 'group', metavar='', - help='Name or ID of group to change') + help='Group to modify (name or ID)') parser.add_argument( '--name', - metavar='', + metavar='', help='New group name') parser.add_argument( '--domain', - metavar='', - help='New domain name or ID that will now own the group') + metavar='', + help='New domain that will now own the group (name or ID)') parser.add_argument( '--description', - metavar='', + metavar='', help='New group description') return parser @@ -341,7 +341,7 @@ def take_action(self, parsed_args): class ShowGroup(show.ShowOne): - """Show group command""" + """Show group details""" log = logging.getLogger(__name__ + '.ShowGroup') @@ -350,7 +350,7 @@ def get_parser(self, prog_name): parser.add_argument( 'group', metavar='', - help='Name or ID of group to display', + help='Group to display (name or ID)', ) parser.add_argument( '--domain', From 1927b03cc740378fcc1d146803cee988ec93e6a2 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 24 Dec 2014 06:08:19 -0700 Subject: [PATCH 0292/3494] Compute calls ignore region selection Calls to compute commands ignore region selection. The region is not passed to the get_endpoint call. Change-Id: I1ccfc56d7cb27a00b8982232a40ace21f2c0e9a2 Closes-Bug: 1405416 --- openstackclient/compute/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py index 3725350ac1..166747d59f 100644 --- a/openstackclient/compute/client.py +++ b/openstackclient/compute/client.py @@ -50,6 +50,7 @@ def make_client(instance): extensions=extensions, http_log_debug=http_log_debug, timings=instance.timing, + region_name=instance._region_name, ) return client From 6e3c9a3d2dbcffd3789d11c65703cec07bf49ee3 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 16 Dec 2014 19:09:58 -0500 Subject: [PATCH 0293/3494] Fix a few issues with 'usage list' * Added number of servers column, was missing * Added a new line character after the initial usage message Change-Id: I6c4e5bda6ba9ceafa92ecf13987c56d0bbe99961 --- openstackclient/compute/v2/usage.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openstackclient/compute/v2/usage.py b/openstackclient/compute/v2/usage.py index ed98af2672..05d6038f0b 100644 --- a/openstackclient/compute/v2/usage.py +++ b/openstackclient/compute/v2/usage.py @@ -60,12 +60,14 @@ def _format_project(project): compute_client = self.app.client_manager.compute columns = ( "tenant_id", + "server_usages", "total_memory_mb_usage", "total_vcpus_usage", "total_local_gb_usage" ) column_headers = ( "Project", + "Servers", "RAM MB-Hours", "CPU Hours", "Disk GB-Hours" @@ -84,7 +86,7 @@ def _format_project(project): else: end = now + datetime.timedelta(days=1) - usage_list = compute_client.usage.list(start, end) + usage_list = compute_client.usage.list(start, end, detailed=True) # Cache the project list project_cache = {} @@ -95,8 +97,8 @@ def _format_project(project): # Just forget it if there's any trouble pass - if len(usage_list) > 0: - sys.stdout.write("Usage from %s to %s:" % ( + if parsed_args.formatter == 'table' and len(usage_list) > 0: + sys.stdout.write("Usage from %s to %s: \n" % ( start.strftime(dateformat), end.strftime(dateformat), )) @@ -106,6 +108,7 @@ def _format_project(project): s, columns, formatters={ 'tenant_id': _format_project, + 'server_usages': lambda x: len(x), 'total_memory_mb_usage': lambda x: float("%.2f" % x), 'total_vcpus_usage': lambda x: float("%.2f" % x), 'total_local_gb_usage': lambda x: float("%.2f" % x), From 5761a0f0b75277140c081ab61b7f3c41edc82e31 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 16 Dec 2014 22:25:01 -0500 Subject: [PATCH 0294/3494] Add usage show command Should show basic usage by project id, if not specified then use the project id the user is authN'ing with. Change-Id: I0284a5efd84075b18e1a7117cc9f8f7fecf16274 Closes-Bug: #1400796 --- openstackclient/compute/v2/usage.py | 74 +++++++++++++++++++++++++++++ setup.cfg | 2 + 2 files changed, 76 insertions(+) diff --git a/openstackclient/compute/v2/usage.py b/openstackclient/compute/v2/usage.py index 05d6038f0b..c71ecb1873 100644 --- a/openstackclient/compute/v2/usage.py +++ b/openstackclient/compute/v2/usage.py @@ -20,6 +20,8 @@ import sys from cliff import lister +from cliff import show +import six from openstackclient.common import utils @@ -114,3 +116,75 @@ def _format_project(project): 'total_local_gb_usage': lambda x: float("%.2f" % x), }, ) for s in usage_list)) + + +class ShowUsage(show.ShowOne): + """Show resource usage for a single project. """ + + log = logging.getLogger(__name__ + ".ShowUsage") + + def get_parser(self, prog_name): + parser = super(ShowUsage, self).get_parser(prog_name) + parser.add_argument( + "--start", + metavar="", + default=None, + help="Usage range start date, ex 2012-01-20" + " (default: 4 weeks ago)." + ) + parser.add_argument( + "--end", + metavar="", + default=None, + help="Usage range end date, ex 2012-01-20 (default: tomorrow)." + ) + parser.add_argument( + "--project", + metavar="", + default=None, + help="Name or ID of project to show usage for." + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + identity_client = self.app.client_manager.identity + compute_client = self.app.client_manager.compute + dateformat = "%Y-%m-%d" + now = datetime.datetime.utcnow() + + if parsed_args.start: + start = datetime.datetime.strptime(parsed_args.start, dateformat) + else: + start = now - datetime.timedelta(weeks=4) + + if parsed_args.end: + end = datetime.datetime.strptime(parsed_args.end, dateformat) + else: + end = now + datetime.timedelta(days=1) + + if parsed_args.project: + project = utils.find_resource( + identity_client.projects, + parsed_args.project, + ).id + else: + # Get the project from the current auth + project = self.app.client_manager.auth_ref.project_id + + usage = compute_client.usage.get(project, start, end) + + if parsed_args.formatter == 'table': + sys.stdout.write("Usage from %s to %s on project %s: \n" % ( + start.strftime(dateformat), + end.strftime(dateformat), + project + )) + + info = {} + info['Servers'] = len(usage.server_usages) + info['RAM MB-Hours'] = float("%.2f" % usage.total_memory_mb_usage) + info['CPU Hours'] = float("%.2f" % usage.total_vcpus_usage) + info['Disk GB-Hours'] = float("%.2f" % usage.total_local_gb_usage) + return zip(*sorted(six.iteritems(info))) diff --git a/setup.cfg b/setup.cfg index 02b7751d43..9a92140a85 100644 --- a/setup.cfg +++ b/setup.cfg @@ -131,6 +131,8 @@ openstack.compute.v2 = server_unrescue = openstackclient.compute.v2.server:UnrescueServer server_unset = openstackclient.compute.v2.server:UnsetServer + usage_show = openstackclient.compute.v2.usage:ShowUsage + openstack.identity.v2 = catalog_list = openstackclient.identity.v2_0.catalog:ListCatalog catalog_show = openstackclient.identity.v2_0.catalog:ShowCatalog From 3ccf1a2606385bdded7301b6a3fc2b12ece2aca7 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 16 Dec 2014 22:28:59 -0500 Subject: [PATCH 0295/3494] Rename `os project usage list` to `os usage list` There really isn't anything project specific about the command, it should really just be `os usage list`. For at least one development cycle we should keep the old command. Change-Id: I4d1df801576c259b527e87369f3121b94393cfa8 --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 9a92140a85..3b208f5517 100644 --- a/setup.cfg +++ b/setup.cfg @@ -131,6 +131,7 @@ openstack.compute.v2 = server_unrescue = openstackclient.compute.v2.server:UnrescueServer server_unset = openstackclient.compute.v2.server:UnsetServer + usage_list = openstackclient.compute.v2.usage:ListUsage usage_show = openstackclient.compute.v2.usage:ShowUsage openstack.identity.v2 = From 7ea5f89043b34c379f774577dee78560275fa797 Mon Sep 17 00:00:00 2001 From: zhiyuan_cai Date: Mon, 29 Dec 2014 10:30:52 +0800 Subject: [PATCH 0296/3494] Catch exception when getting quota Quota show command will list both the quotas of nova and cinder. But if cinder service is not enabled, EndpointNotFound exception will be raised and thus the command is broken. Catch this exception so quotas of nova can be listed. Change-Id: If2d2820675aa6a12e407d608fed846b21c953b2d Closes-Bug: #1390507 --- openstackclient/common/quota.py | 34 ++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index edf4ffdb98..6d04b5c99d 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -147,6 +147,21 @@ def get_parser(self, prog_name): ) return parser + def get_quota(self, client, parsed_args): + try: + if parsed_args.quota_class: + quota = client.quota_classes.get(parsed_args.project) + elif parsed_args.default: + quota = client.quotas.defaults(parsed_args.project) + else: + quota = client.quotas.get(parsed_args.project) + except Exception as e: + if type(e).__name__ == 'EndpointNotFound': + return {} + else: + raise e + return quota._info + def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) @@ -159,23 +174,12 @@ def take_action(self, parsed_args): # does not exist. If this is determined to be the # intended behaviour of the API we will validate # the argument with Identity ourselves later. - if parsed_args.quota_class: - compute_quota = compute_client.quota_classes.get( - parsed_args.project) - volume_quota = volume_client.quota_classes.get( - parsed_args.project) - elif parsed_args.default: - compute_quota = compute_client.quotas.defaults( - parsed_args.project) - volume_quota = volume_client.quotas.defaults( - parsed_args.project) - else: - compute_quota = compute_client.quotas.get(parsed_args.project) - volume_quota = volume_client.quotas.get(parsed_args.project) + compute_quota_info = self.get_quota(compute_client, parsed_args) + volume_quota_info = self.get_quota(volume_client, parsed_args) info = {} - info.update(compute_quota._info) - info.update(volume_quota._info) + info.update(compute_quota_info) + info.update(volume_quota_info) # Map the internal quota names to the external ones for k, v in itertools.chain( From d5caa6a26baf2fccbf276080fef60c81ed4beae3 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 23 Dec 2014 17:04:12 -0600 Subject: [PATCH 0297/3494] Command object docs: container, object Change-Id: Ie3df543a28cbee0cc809310a05f431c97b2c7e70 --- doc/source/command-objects/container.rst | 105 +++++++++++++++++ doc/source/command-objects/object.rst | 140 +++++++++++++++++++++++ openstackclient/object/v1/container.py | 18 +-- openstackclient/object/v1/object.py | 32 +++--- 4 files changed, 270 insertions(+), 25 deletions(-) create mode 100644 doc/source/command-objects/container.rst create mode 100644 doc/source/command-objects/object.rst diff --git a/doc/source/command-objects/container.rst b/doc/source/command-objects/container.rst new file mode 100644 index 0000000000..3afaeb9217 --- /dev/null +++ b/doc/source/command-objects/container.rst @@ -0,0 +1,105 @@ +========= +container +========= + +Object Store v1 + +container create +---------------- + +Create new container + +.. program:: container create +.. code:: bash + + os container create + [ ...] + +.. option:: + + New container name(s) + +container delete +---------------- + +Delete container + +.. program:: container delete +.. code:: bash + + os container delete + [ ...] + +.. option:: + + Container(s) to delete + +container list +-------------- + +List containers + +.. program:: container list +.. code::bash + + os container list + [--prefix ] + [--marker ] + [--end-marker ] + [--limit ] + [--long] + [--all] + +.. option:: --prefix + + Filter list using + +.. option:: --marker + + Anchor for paging + +.. option:: --end-marker + + End anchor for paging + +.. option:: --limit + + Limit the number of containers returned + +.. option:: --long + + List additional fields in output + +.. options:: --all + + List all containers (default is 10000) + +container save +-------------- + +Save container contents locally + +.. program:: container save +.. code:: bash + + os container save + + +.. option:: + + Container to save + +container show +-------------- + +Show container details + +.. program:: container show +.. code:: bash + + os container show + [] + +.. option:: + + Container to display diff --git a/doc/source/command-objects/object.rst b/doc/source/command-objects/object.rst new file mode 100644 index 0000000000..5cbc95d71c --- /dev/null +++ b/doc/source/command-objects/object.rst @@ -0,0 +1,140 @@ +====== +object +====== + +Object Store v1 + +object create +------------- + +Upload object to container + +.. program:: object create +.. code:: bash + + os object create + + [ ...] + +.. option:: + + Container for new object + +.. option:: + + Local filename(s) to upload + +object delete +------------- + +Delete object from container + +.. program:: object delete +.. code:: bash + + os object delete + + [ ...] + +.. option:: + + Delete object(s) from + +.. option:: + + Object(s) to delete + +list object +----------- + +List objects + +.. program object list +.. code:: bash + + os object list + [--prefix ] + [--delimiter ] + [--marker ] + [--end-marker ] + [--limit ] + [--long] + [--all] + ] + +.. option:: --prefix + + Filter list using + +.. option:: --delimiter + + Roll up items with + +.. option:: --marker + + Anchor for paging + +.. option:: --end-marker + + End anchor for paging + +.. option:: --limit + + Limit number of objects returned + +.. option:: --long + + List additional fields in output + +.. options:: --all + + List all objects in (default is 10000) + +.. option:: + + Container to list + +object save +----------- + +Save object locally + +.. program:: object save +.. code:: bash + + os object save + [--file ] + [] + [] + +.. option:: --file + + Destination filename (defaults to object name) + +.. option:: + + Download from + +.. option:: + + Object to save + +object show +----------- + +Show object details + +.. program:: object show +.. code:: bash + + os object show + + + +.. option:: + + Display from + +.. option:: + + Object to display diff --git a/openstackclient/object/v1/container.py b/openstackclient/object/v1/container.py index ead3df45e9..b75888e408 100644 --- a/openstackclient/object/v1/container.py +++ b/openstackclient/object/v1/container.py @@ -27,7 +27,7 @@ class CreateContainer(lister.Lister): - """Create a container""" + """Create new container""" log = logging.getLogger(__name__ + '.CreateContainer') @@ -35,9 +35,9 @@ def get_parser(self, prog_name): parser = super(CreateContainer, self).get_parser(prog_name) parser.add_argument( 'containers', - metavar='', + metavar='', nargs="+", - help='Container name(s) to create', + help='New container name(s)', ) return parser @@ -60,7 +60,7 @@ def take_action(self, parsed_args): class DeleteContainer(command.Command): - """Delete a container""" + """Delete container""" log = logging.getLogger(__name__ + '.DeleteContainer') @@ -70,7 +70,7 @@ def get_parser(self, prog_name): 'containers', metavar='', nargs="+", - help='Container name(s) to delete', + help='Container(s) to delete', ) return parser @@ -157,7 +157,7 @@ def take_action(self, parsed_args): class SaveContainer(command.Command): - """Save the contents of a container locally""" + """Save container contents locally""" log = logging.getLogger(__name__ + ".SaveContainer") @@ -166,7 +166,7 @@ def get_parser(self, prog_name): parser.add_argument( 'container', metavar='', - help='Container name to save', + help='Container to save', ) return parser @@ -179,7 +179,7 @@ def take_action(self, parsed_args): class ShowContainer(show.ShowOne): - """Show container information""" + """Show container details""" log = logging.getLogger(__name__ + '.ShowContainer') @@ -188,7 +188,7 @@ def get_parser(self, prog_name): parser.add_argument( 'container', metavar='', - help='Container name to display', + help='Container to display', ) return parser diff --git a/openstackclient/object/v1/object.py b/openstackclient/object/v1/object.py index cbe9da2fb4..781dd047fa 100644 --- a/openstackclient/object/v1/object.py +++ b/openstackclient/object/v1/object.py @@ -27,7 +27,7 @@ class CreateObject(lister.Lister): - """Upload an object to a container""" + """Upload object to container""" log = logging.getLogger(__name__ + '.CreateObject') @@ -36,13 +36,13 @@ def get_parser(self, prog_name): parser.add_argument( 'container', metavar='', - help='Container to store new object', + help='Container for new object', ) parser.add_argument( 'objects', - metavar='', + metavar='', nargs="+", - help='Local path of object(s) to upload', + help='Local filename(s) to upload', ) return parser @@ -66,7 +66,7 @@ def take_action(self, parsed_args): class DeleteObject(command.Command): - """Delete an object within a container""" + """Delete object from container""" log = logging.getLogger(__name__ + '.DeleteObject') @@ -75,11 +75,11 @@ def get_parser(self, prog_name): parser.add_argument( 'container', metavar='', - help='Container that stores the object to delete', + help='Delete object(s) from ', ) parser.add_argument( 'objects', - metavar='', + metavar='', nargs="+", help='Object(s) to delete', ) @@ -104,8 +104,8 @@ def get_parser(self, prog_name): parser = super(ListObject, self).get_parser(prog_name) parser.add_argument( "container", - metavar="", - help="List contents of container-name", + metavar="", + help="Container to list", ) parser.add_argument( "--prefix", @@ -188,7 +188,7 @@ def take_action(self, parsed_args): class SaveObject(command.Command): - """Save an object locally""" + """Save object locally""" log = logging.getLogger(__name__ + ".SaveObject") @@ -197,17 +197,17 @@ def get_parser(self, prog_name): parser.add_argument( "--file", metavar="", - help="Downloaded object filename [defaults to object name]", + help="Destination filename (defaults to object name)", ) parser.add_argument( 'container', metavar='', - help='Container name that has the object', + help='Download from ', ) parser.add_argument( "object", metavar="", - help="Name of the object to save", + help="Object to save", ) return parser @@ -222,7 +222,7 @@ def take_action(self, parsed_args): class ShowObject(show.ShowOne): - """Show object information""" + """Show object details""" log = logging.getLogger(__name__ + '.ShowObject') @@ -231,12 +231,12 @@ def get_parser(self, prog_name): parser.add_argument( 'container', metavar='', - help='Container name for object to display', + help='Display from ', ) parser.add_argument( 'object', metavar='', - help='Object name to display', + help='Object to display', ) return parser From 4a07e63e7ea4b99b10e4a3fd9ed06c0cf6ee905f Mon Sep 17 00:00:00 2001 From: lin-hua-cheng Date: Fri, 19 Dec 2014 18:40:40 -0800 Subject: [PATCH 0298/3494] type should be required for v2.0 service create Updated the service name to be optional, mostly matching the cli arguments with v3 service create. Implemented the following changes on service create: - if only a single positional is present, it's a . This is not currently legal so it is considered a new case. - if --type option is present the positional is handled as ; display deprecation message - if --name option is present the positional is handled as . Making --type optional is new, but back-compatible - Made --name and --type mutually exclusive. - only '--name ' shall appear in the help output Change-Id: I8fd4adba3d8cd00d5a8cacc2c494d99d492c45a3 Closes-Bug: #1404073 --- openstackclient/identity/v2_0/service.py | 41 ++++++++-- .../tests/identity/v2_0/test_service.py | 75 ++++++++++++++++++- 2 files changed, 104 insertions(+), 12 deletions(-) diff --git a/openstackclient/identity/v2_0/service.py b/openstackclient/identity/v2_0/service.py index e8848ddee2..0b98a90360 100644 --- a/openstackclient/identity/v2_0/service.py +++ b/openstackclient/identity/v2_0/service.py @@ -15,6 +15,7 @@ """Service action implementations""" +import argparse import logging import six @@ -36,15 +37,20 @@ class CreateService(show.ShowOne): def get_parser(self, prog_name): parser = super(CreateService, self).get_parser(prog_name) parser.add_argument( - 'name', - metavar='', - help=_('New service name'), + 'type_or_name', + metavar='', + help=_('New service type (compute, image, identity, volume, etc)'), ) - parser.add_argument( + type_or_name_group = parser.add_mutually_exclusive_group() + type_or_name_group.add_argument( '--type', metavar='', - required=True, - help=_('New service type (compute, image, identity, volume, etc)'), + help=argparse.SUPPRESS, + ) + type_or_name_group.add_argument( + '--name', + metavar='', + help=_('New service name'), ) parser.add_argument( '--description', @@ -57,9 +63,28 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity + type_or_name = parsed_args.type_or_name + name = parsed_args.name + type = parsed_args.type + + # If only a single positional is present, it's a . + # This is not currently legal so it is considered a new case. + if not type and not name: + type = type_or_name + # If --type option is present then positional is handled as ; + # display deprecation message. + elif type: + name = type_or_name + self.log.warning(_('The argument --type is deprecated, use service' + ' create --name type instead.')) + # If --name option is present the positional is handled as . + # Making --type optional is new, but back-compatible + elif name: + type = type_or_name + service = identity_client.services.create( - parsed_args.name, - parsed_args.type, + name, + type, parsed_args.description) info = {} diff --git a/openstackclient/tests/identity/v2_0/test_service.py b/openstackclient/tests/identity/v2_0/test_service.py index 6c93574bf5..a0adea4e9e 100644 --- a/openstackclient/tests/identity/v2_0/test_service.py +++ b/openstackclient/tests/identity/v2_0/test_service.py @@ -44,14 +44,80 @@ def setUp(self): # Get the command object to test self.cmd = service.CreateService(self.app, None) - def test_service_create_name_type(self): + def test_service_create_with_type_positional(self): + arglist = [ + identity_fakes.service_type, + ] + verifylist = [ + ('type_or_name', identity_fakes.service_type), + ('type', None), + ('description', None), + ('name', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # ServiceManager.create(name, service_type, description) + self.services_mock.create.assert_called_with( + None, + identity_fakes.service_type, + None, + ) + + collist = ('description', 'id', 'name', 'type') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.service_description, + identity_fakes.service_id, + identity_fakes.service_name, + identity_fakes.service_type, + ) + self.assertEqual(data, datalist) + + def test_service_create_with_type_option(self): arglist = [ '--type', identity_fakes.service_type, identity_fakes.service_name, ] verifylist = [ + ('type_or_name', identity_fakes.service_name), ('type', identity_fakes.service_type), ('description', None), + ('name', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # ServiceManager.create(name, service_type, description) + self.services_mock.create.assert_called_with( + identity_fakes.service_name, + identity_fakes.service_type, + None, + ) + + collist = ('description', 'id', 'name', 'type') + self.assertEqual(columns, collist) + datalist = ( + identity_fakes.service_description, + identity_fakes.service_id, + identity_fakes.service_name, + identity_fakes.service_type, + ) + self.assertEqual(data, datalist) + + def test_service_create_with_name_option(self): + arglist = [ + '--name', identity_fakes.service_name, + identity_fakes.service_type, + ] + verifylist = [ + ('type_or_name', identity_fakes.service_type), + ('type', None), + ('description', None), ('name', identity_fakes.service_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -78,12 +144,13 @@ def test_service_create_name_type(self): def test_service_create_description(self): arglist = [ - '--type', identity_fakes.service_type, + '--name', identity_fakes.service_name, '--description', identity_fakes.service_description, - identity_fakes.service_name, + identity_fakes.service_type, ] verifylist = [ - ('type', identity_fakes.service_type), + ('type_or_name', identity_fakes.service_type), + ('type', None), ('description', identity_fakes.service_description), ('name', identity_fakes.service_name), ] From b81d0f4d08e7f6a26ed60a7aceaf1e3d46f1c7ac Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 30 Dec 2014 23:23:39 -0500 Subject: [PATCH 0299/3494] Bunch of formatting tweaks to server-image docs Change-Id: Id2dad09ea75e0615519862db007700389db8cd51 --- doc/source/command-objects/server-image.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/doc/source/command-objects/server-image.rst b/doc/source/command-objects/server-image.rst index 681f6e4f2c..4577b25b20 100644 --- a/doc/source/command-objects/server-image.rst +++ b/doc/source/command-objects/server-image.rst @@ -10,6 +10,7 @@ server image create Create a new disk image from a running server +.. program:: server image create .. code:: bash os server image create @@ -17,11 +18,14 @@ Create a new disk image from a running server [--wait] -:option:`--name` +.. option:: --name + Name of new image (default is server name) -:option:`--wait` +.. option:: --wait + Wait for image create to complete -:option:`` +.. describe:: + Server (name or ID) From caef59a4a8bd49d785f60d035fd02e3fc06c8252 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Wed, 31 Dec 2014 02:54:36 -0500 Subject: [PATCH 0300/3494] Add docs for listing availability zones Change-Id: I4c005e1d8089b46feca6cd3266f63c408648f074 --- .../command-objects/availability_zone.rst | 20 +++++++++++++++++++ doc/source/commands.rst | 1 + .../compute/v2/availability_zone.py | 2 +- 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 doc/source/command-objects/availability_zone.rst diff --git a/doc/source/command-objects/availability_zone.rst b/doc/source/command-objects/availability_zone.rst new file mode 100644 index 0000000000..3743523088 --- /dev/null +++ b/doc/source/command-objects/availability_zone.rst @@ -0,0 +1,20 @@ +================= +availability zone +================= + +Compute v2 + +availability zone list +---------------------- + +List availability zones and their status + +.. program availability zone list +.. code:: bash + + os availability zone list + [--long] + +.. option:: --long + + List additional fields in output diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 04c451c8fb..50c72f6330 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -70,6 +70,7 @@ the API resources will be merged, as in the ``quota`` object that has options referring to both Compute and Volume quotas. * ``access token``: Identity - long-lived OAuth-based token +* ``availability zone``: (**Compute**) a logical partition of hosts or volume services * ``aggregate``: (**Compute**) a grouping of servers * ``backup``: Volume - a volume copy * ``catalog``: (**Identity**) service catalog diff --git a/openstackclient/compute/v2/availability_zone.py b/openstackclient/compute/v2/availability_zone.py index c7c9241491..648c0ee4ce 100644 --- a/openstackclient/compute/v2/availability_zone.py +++ b/openstackclient/compute/v2/availability_zone.py @@ -59,7 +59,7 @@ def _xform_availability_zone(az, include_extra): class ListAvailabilityZone(lister.Lister): - """List Availability Zones and their status""" + """List availability zones and their status""" log = logging.getLogger(__name__ + '.ListAvailabilityZone') From b5ce0f145f9342a01370ac34a7a11d40df703249 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 31 Dec 2014 10:05:37 -0600 Subject: [PATCH 0301/3494] Command docs: region Fix up formatting fro region command docs and help Change-Id: Icf8c03da38b30fc69e7fe70f9c14aaa99881d320 --- doc/source/command-objects/region.rst | 68 +++++++++++++++++---------- doc/source/commands.rst | 2 +- openstackclient/identity/v3/region.py | 38 +++++++-------- 3 files changed, 64 insertions(+), 44 deletions(-) diff --git a/doc/source/command-objects/region.rst b/doc/source/command-objects/region.rst index 788ed6facb..d1aedb3161 100644 --- a/doc/source/command-objects/region.rst +++ b/doc/source/command-objects/region.rst @@ -9,24 +9,30 @@ region create Create new region +.. program:: region create .. code:: bash os region create [--parent-region ] - [--description ] - [--url ] + [--description ] + [--url ] -:option:`--parent-region` - Parent region +.. option:: --parent-region + + Parent region ID + +.. option:: --description -:option:`--description` New region description -:option:`--url` +.. option:: --url + New region URL -:option:`` +.. _region_create-region-id: +.. describe:: + New region ID region delete @@ -34,61 +40,75 @@ region delete Delete region +.. program:: region delete .. code:: bash os region delete - + + +.. _region_delete-region-id: +.. describe:: -:option:`` - Region to delete + Region ID to delete region list ----------- List regions +.. program:: region list .. code:: bash os region list [--parent-region ] -:option:`--parent-region` - Filter by a specific parent region +.. option:: --parent-region + + Filter by parent region ID region set ---------- Set region properties +.. program:: region set .. code:: bash os region set [--parent-region ] - [--description ] - [--url ] - + [--description ] + [--url ] + -:option:`--parent-region` - New parent region +.. option:: --parent-region + + New parent region ID + +.. option:: --description -:option:`--description` New region description -:option:`--url` +.. option:: --url + New region URL -:option:`` +.. _region_set-region-id: +.. describe:: + Region ID to modify region show ----------- -Show region +Display region details +.. program:: region show .. code:: bash os region show - + -:option:`` - Region ID to modify +.. _region_show-region-id: +.. describe:: + + Region ID to display diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 04c451c8fb..6aaa25e768 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -97,7 +97,7 @@ referring to both Compute and Volume quotas. * ``policy``: Identity - determines authorization * ``project``: (**Identity**) owns a group of resources * ``quota``: (**Compute**, **Volume**) resource usage restrictions -* ``region``: (**Identity**) +* ``region``: (**Identity**) a subset of an OpenStack deployment * ``request token``: Identity - temporary OAuth-based token * ``role``: Identity - a policy object used to determine authorization * ``security group``: Compute, Network - groups of network access rules diff --git a/openstackclient/identity/v3/region.py b/openstackclient/identity/v3/region.py index cce3417d5a..5fb7391362 100644 --- a/openstackclient/identity/v3/region.py +++ b/openstackclient/identity/v3/region.py @@ -35,22 +35,22 @@ def get_parser(self, prog_name): # seems like poor UX, we will only support user-defined IDs. parser.add_argument( 'region', - metavar='', - help=_('New region'), + metavar='', + help=_('New region ID'), ) parser.add_argument( '--parent-region', - metavar='', - help=_('The parent region of new region'), + metavar='', + help=_('Parent region ID'), ) parser.add_argument( '--description', - metavar='', + metavar='', help=_('New region description'), ) parser.add_argument( '--url', - metavar='', + metavar='', help=_('New region url'), ) @@ -82,8 +82,8 @@ def get_parser(self, prog_name): parser = super(DeleteRegion, self).get_parser(prog_name) parser.add_argument( 'region', - metavar='', - help=_('Region to delete'), + metavar='', + help=_('Region ID to delete'), ) return parser @@ -104,8 +104,8 @@ def get_parser(self, prog_name): parser = super(ListRegion, self).get_parser(prog_name) parser.add_argument( '--parent-region', - metavar='', - help=_('Filter by parent region'), + metavar='', + help=_('Filter by parent region ID'), ) return parser @@ -137,22 +137,22 @@ def get_parser(self, prog_name): parser = super(SetRegion, self).get_parser(prog_name) parser.add_argument( 'region', - metavar='', - help=_('Region to change'), + metavar='', + help=_('Region ID to modify'), ) parser.add_argument( '--parent-region', - metavar='', - help=_('New parent region of the region'), + metavar='', + help=_('New parent region ID'), ) parser.add_argument( '--description', - metavar='', + metavar='', help=_('New region description'), ) parser.add_argument( '--url', - metavar='', + metavar='', help=_('New region url'), ) return parser @@ -179,7 +179,7 @@ def take_action(self, parsed_args): class ShowRegion(show.ShowOne): - """Show region""" + """Display region details""" log = logging.getLogger(__name__ + '.ShowRegion') @@ -187,8 +187,8 @@ def get_parser(self, prog_name): parser = super(ShowRegion, self).get_parser(prog_name) parser.add_argument( 'region', - metavar='', - help=_('Region to display'), + metavar='', + help=_('Region ID to display'), ) return parser From f18f264ed7b12fb73c3760514dcb226e28189572 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 31 Dec 2014 09:54:21 -0600 Subject: [PATCH 0302/3494] Command docs: domain Change the implementation of --enable|--disable on domain create and set commands to our usual style. Change-Id: I10f2b96281a114fa3cf3b001394844770b2a8632 --- doc/source/command-objects/domain.rst | 8 +++---- doc/source/commands.rst | 2 +- openstackclient/identity/v3/domain.py | 24 ++++++++++--------- .../tests/identity/v3/test_domain.py | 10 ++++---- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/doc/source/command-objects/domain.rst b/doc/source/command-objects/domain.rst index 057296ac5e..66697ac3ea 100644 --- a/doc/source/command-objects/domain.rst +++ b/doc/source/command-objects/domain.rst @@ -36,7 +36,7 @@ Create new domain If the domain already exists, return the existing domain data and do not fail. -.. option:: +.. describe:: New domain name @@ -51,7 +51,7 @@ Delete domain os domain delete -.. option:: +.. describe:: Domain to delete (name or ID) @@ -95,7 +95,7 @@ Set domain properties Disable domain -.. option:: +.. describe:: Domain to modify (name or ID) @@ -110,6 +110,6 @@ Show domain details os domain show -.. option:: +.. describe:: Domain to display (name or ID) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 04c451c8fb..f82d307ea2 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -78,7 +78,7 @@ referring to both Compute and Volume quotas. * ``consumer``: Identity - OAuth-based delegatee * ``container``: Object Store - a grouping of objects * ``credentials``: (**Identity**) specific to identity providers -* ``domain``: Identity - a grouping of projects +* ``domain``: (**Identity**) a grouping of projects * ``endpoint``: (**Identity**) the base URL used to contact a specific service * ``extension``: (**Compute**, **Identity**, **Volume**) OpenStack server API extensions * ``flavor``: Compute - pre-defined configurations of servers: ram, root disk, etc diff --git a/openstackclient/identity/v3/domain.py b/openstackclient/identity/v3/domain.py index 727f5b1838..189f097052 100644 --- a/openstackclient/identity/v3/domain.py +++ b/openstackclient/identity/v3/domain.py @@ -48,15 +48,14 @@ def get_parser(self, prog_name): enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', - dest='enabled', action='store_true', - default=True, - help='Enable domain') + help='Enable domain (default)', + ) enable_group.add_argument( '--disable', - dest='enabled', - action='store_false', - help='Disable domain') + action='store_true', + help='Disable domain', + ) parser.add_argument( '--or-show', action='store_true', @@ -68,11 +67,15 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) identity_client = self.app.client_manager.identity + enabled = True + if parsed_args.disable: + enabled = False + try: domain = identity_client.domains.create( name=parsed_args.name, description=parsed_args.description, - enabled=parsed_args.enabled, + enabled=enabled, ) except ksc_exc.Conflict as e: if parsed_args.or_show: @@ -150,13 +153,11 @@ def get_parser(self, prog_name): enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', - dest='enabled', action='store_true', help='Enable domain', ) enable_group.add_argument( '--disable', - dest='disabled', action='store_true', help='Disable domain', ) @@ -172,9 +173,10 @@ def take_action(self, parsed_args): kwargs['name'] = parsed_args.name if parsed_args.description: kwargs['description'] = parsed_args.description - if parsed_args.enabled: + + if parsed_args.enable: kwargs['enabled'] = True - if parsed_args.disabled: + if parsed_args.disable: kwargs['enabled'] = False if not kwargs: diff --git a/openstackclient/tests/identity/v3/test_domain.py b/openstackclient/tests/identity/v3/test_domain.py index 8dad5bcc7a..cfec10e7c3 100644 --- a/openstackclient/tests/identity/v3/test_domain.py +++ b/openstackclient/tests/identity/v3/test_domain.py @@ -46,7 +46,6 @@ def test_domain_create_no_options(self): identity_fakes.domain_name, ] verifylist = [ - ('enabled', True), ('name', identity_fakes.domain_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -81,7 +80,6 @@ def test_domain_create_description(self): ] verifylist = [ ('description', 'new desc'), - ('enabled', True), ('name', identity_fakes.domain_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -115,7 +113,7 @@ def test_domain_create_enable(self): identity_fakes.domain_name, ] verifylist = [ - ('enabled', True), + ('enable', True), ('name', identity_fakes.domain_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -149,7 +147,7 @@ def test_domain_create_disable(self): identity_fakes.domain_name, ] verifylist = [ - ('enabled', False), + ('disable', True), ('name', identity_fakes.domain_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -333,7 +331,7 @@ def test_domain_set_enable(self): identity_fakes.domain_id, ] verifylist = [ - ('enabled', True), + ('enable', True), ('domain', identity_fakes.domain_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -356,7 +354,7 @@ def test_domain_set_disable(self): identity_fakes.domain_id, ] verifylist = [ - ('disabled', True), + ('disable', True), ('domain', identity_fakes.domain_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) From 0720c7819902d5ae27884afa49c973902467a50a Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 31 Dec 2014 12:09:50 -0600 Subject: [PATCH 0303/3494] Command docs: flavor Change-Id: Ie85ff7706ef08b70ab8ba99533465d90904cf393 --- doc/source/command-objects/flavor.rst | 105 ++++++++++++++++++++++++++ doc/source/commands.rst | 2 +- openstackclient/compute/v2/flavor.py | 31 +++++--- 3 files changed, 125 insertions(+), 13 deletions(-) create mode 100644 doc/source/command-objects/flavor.rst diff --git a/doc/source/command-objects/flavor.rst b/doc/source/command-objects/flavor.rst new file mode 100644 index 0000000000..4c98e85889 --- /dev/null +++ b/doc/source/command-objects/flavor.rst @@ -0,0 +1,105 @@ +====== +flavor +====== + +flavor create +------------- + +Create new flavor + +.. program:: flavor create +.. code:: bash + + os flavor create + [--id ] + [--ram ] + [--disk ] + [--ephemeral-disk ] + [--swap ] + [--vcpus ] + [--rxtx-factor ] + [--public | --private] + + +.. option:: --id + + Unique flavor ID; 'auto' creates a UUID (default: auto) + +.. option:: --ram + + Memory size in MB (default 256M) + +.. option:: --disk + + Disk size in GB (default 0G) + +.. option:: --ephemeral-disk + + Ephemeral disk size in GB (default 0G) + +.. option:: --swap + + Swap space size in GB (default 0G) + +.. option:: --vcpus + + Number of vcpus (default 1) + +.. option:: --rxtx-factor + + RX/TX factor (default 1) + +.. option:: --public + + Flavor is available to other projects (default) + +.. option:: --private + + Flavor is not available to other projects + +.. _flavor_create-flavor-name: +.. describe:: + + New flavor name + +flavor delete +------------- + +Delete a flavor + +.. program:: flavor delete +.. code:: bash + + os flavor delete + + +.. _flavor_delete-flavor: +.. describe:: + + Flavor to delete (name or ID) + +flavor list +----------- + +List flavors + +.. program:: flavor list +.. code:: bash + + os flavor list + +flavor show +----------- + +Display flavor details + +.. program:: flavor show +.. code:: bash + + os flavor show + + +.. _flavor_show-flavor: +.. describe:: + + Flavor to display (name or ID) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 50c72f6330..98084ea102 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -82,7 +82,7 @@ referring to both Compute and Volume quotas. * ``domain``: Identity - a grouping of projects * ``endpoint``: (**Identity**) the base URL used to contact a specific service * ``extension``: (**Compute**, **Identity**, **Volume**) OpenStack server API extensions -* ``flavor``: Compute - pre-defined configurations of servers: ram, root disk, etc +* ``flavor``: (**Compute**) pre-defined server configurations: ram, root disk, etc * ``group``: Identity - a grouping of users * ``host``: Compute - the physical computer running a hypervisor * ``hypervisor``: Compute - the virtual machine manager diff --git a/openstackclient/compute/v2/flavor.py b/openstackclient/compute/v2/flavor.py index 6dd00b1b9e..6f3788a029 100644 --- a/openstackclient/compute/v2/flavor.py +++ b/openstackclient/compute/v2/flavor.py @@ -34,64 +34,71 @@ def get_parser(self, prog_name): parser = super(CreateFlavor, self).get_parser(prog_name) parser.add_argument( "name", - metavar="", + metavar="", help="New flavor name", ) parser.add_argument( "--id", metavar="", default='auto', - help="Unique flavor ID; 'auto' will create a UUID " - "(default: auto)") + help="Unique flavor ID; 'auto' creates a UUID " + "(default: auto)", + ) parser.add_argument( "--ram", type=int, metavar="", default=256, - help="Memory size in MB (default 256M)") + help="Memory size in MB (default 256M)", + ) parser.add_argument( "--disk", type=int, metavar="", default=0, - help="Disk size in GB (default 0G)") + help="Disk size in GB (default 0G)", + ) parser.add_argument( "--ephemeral", type=int, metavar="", + default=0, help="Ephemeral disk size in GB (default 0G)", - default=0) + ) parser.add_argument( "--swap", type=int, metavar="", + default=0, help="Swap space size in GB (default 0G)", - default=0) + ) parser.add_argument( "--vcpus", type=int, metavar="", default=1, - help="Number of vcpus (default 1)") + help="Number of vcpus (default 1)", + ) parser.add_argument( "--rxtx-factor", type=int, metavar="", + default=1, help="RX/TX factor (default 1)", - default=1) + ) public_group = parser.add_mutually_exclusive_group() public_group.add_argument( "--public", dest="public", action="store_true", default=True, - help="Flavor is accessible to other projects (default)", + help="Flavor is available to other projects (default)", ) public_group.add_argument( "--private", dest="public", action="store_false", - help="Flavor is inaccessible to other projects", + help="Flavor is not available to other projects", ) return parser @@ -168,7 +175,7 @@ def take_action(self, parsed_args): class ShowFlavor(show.ShowOne): - """Show flavor details""" + """Display flavor details""" log = logging.getLogger(__name__ + ".ShowFlavor") From 480921d0e8009e20372037cba4fb6e381760a9c1 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Wed, 31 Dec 2014 02:49:15 -0500 Subject: [PATCH 0304/3494] Add docs for usage show/list Change-Id: Iaf911d69a0b63d705f8789a4640018a428b87be6 --- doc/source/command-objects/usage.rst | 50 ++++++++++++++++++++++++++++ doc/source/commands.rst | 1 + openstackclient/compute/v2/usage.py | 12 +++---- 3 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 doc/source/command-objects/usage.rst diff --git a/doc/source/command-objects/usage.rst b/doc/source/command-objects/usage.rst new file mode 100644 index 0000000000..551176c700 --- /dev/null +++ b/doc/source/command-objects/usage.rst @@ -0,0 +1,50 @@ +===== +usage +===== + +Compute v2 + +usage list +---------- + +List resource usage per project + +.. program:: usage list +.. code:: bash + + os usage list + --start + --end + +.. option:: --start + + Usage range start date, ex 2012-01-20 (default: 4 weeks ago). + +.. option:: --end + + Usage range end date, ex 2012-01-20 (default: tomorrow) + +usage show +---------- + +Show resource usage for a single project. + +.. program:: usage show +.. code:: bash + + os usage show + --project + --start + --end + +.. option:: --project + + Name or ID of project to show usage for. + +.. option:: --start + + Usage range start date, ex 2012-01-20 (default: 4 weeks ago). + +.. option:: --end + + Usage range end date, ex 2012-01-20 (default: tomorrow) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 04c451c8fb..37d3107de8 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -107,6 +107,7 @@ referring to both Compute and Volume quotas. * ``service``: Identity - a cloud service * ``snapshot``: Volume - a point-in-time copy of a volume * ``token``: (**Identity**) a bearer token managed by Identity service +* ``usage``: (**Compute**) display host resources being consumed * ``user``: (**Identity**) individual cloud resources users * ``user role``: (**Identity**) roles assigned to a user * ``volume``: Volume - block volumes diff --git a/openstackclient/compute/v2/usage.py b/openstackclient/compute/v2/usage.py index c71ecb1873..308241cf25 100644 --- a/openstackclient/compute/v2/usage.py +++ b/openstackclient/compute/v2/usage.py @@ -125,6 +125,12 @@ class ShowUsage(show.ShowOne): def get_parser(self, prog_name): parser = super(ShowUsage, self).get_parser(prog_name) + parser.add_argument( + "--project", + metavar="", + default=None, + help="Name or ID of project to show usage for." + ) parser.add_argument( "--start", metavar="", @@ -138,12 +144,6 @@ def get_parser(self, prog_name): default=None, help="Usage range end date, ex 2012-01-20 (default: tomorrow)." ) - parser.add_argument( - "--project", - metavar="", - default=None, - help="Name or ID of project to show usage for." - ) return parser def take_action(self, parsed_args): From db6986bec6d7799a297e7cf3c5a6da2d5a101541 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 30 Dec 2014 23:37:36 -0500 Subject: [PATCH 0305/3494] Add missing content for token commands Minor tweaks and added some content to token issue/revoke Change-Id: Icdad6354f008f9c109d263e115acd10ff113695a --- doc/source/command-objects/token.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/doc/source/command-objects/token.rst b/doc/source/command-objects/token.rst index aec87d2850..22260f0dec 100644 --- a/doc/source/command-objects/token.rst +++ b/doc/source/command-objects/token.rst @@ -7,6 +7,9 @@ Identity v2, v3 token issue ----------- +Issue new token + +.. program:: token issue .. code:: bash os token issue @@ -14,6 +17,16 @@ token issue token revoke ------------ +*Identity version 2 only.* + +Revoke existing token + +.. program:: token revoke .. code:: bash os token revoke + + +.. describe:: + + Token to be deleted From e7ec6bc6e447ce5f455551874fdf09828c440500 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 16 Dec 2014 22:49:13 -0500 Subject: [PATCH 0306/3494] Rename column to `default project id` for long listing v3 user Previously this column was coming up as empty, since user's have a `default project id`, not just `project id`. Change-Id: I3d7f7eb600e9526b9c6cc2a8c5d6009b9100b1f5 --- openstackclient/identity/v3/user.py | 13 ++-- openstackclient/tests/identity/v3/fakes.py | 2 +- .../tests/identity/v3/test_user.py | 61 +++++++++++-------- 3 files changed, 45 insertions(+), 31 deletions(-) diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 665dd4bb16..d7ccff53db 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -15,6 +15,7 @@ """Identity v3 User action implementations""" +import copy import logging import six @@ -217,17 +218,21 @@ def take_action(self, parsed_args): # List users if parsed_args.long: - columns = ('ID', 'Name', 'Project Id', 'Domain Id', - 'Description', 'Email', 'Enabled') + columns = ['ID', 'Name', 'Default Project Id', 'Domain Id', + 'Description', 'Email', 'Enabled'] + column_headers = copy.deepcopy(columns) + column_headers[2] = 'Project' + column_headers[3] = 'Domain' else: - columns = ('ID', 'Name') + columns = ['ID', 'Name'] + column_headers = copy.deepcopy(columns) data = identity_client.users.list( domain=domain, group=group, ) return ( - columns, + column_headers, (utils.get_item_properties( s, columns, formatters={}, diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index 7acaa7f156..88716e975a 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -179,7 +179,7 @@ USER = { 'id': user_id, 'name': user_name, - 'project_id': project_id, + 'default_project_id': project_id, 'email': user_email, 'enabled': True, 'domain_id': domain_id, diff --git a/openstackclient/tests/identity/v3/test_user.py b/openstackclient/tests/identity/v3/test_user.py index bb59ebe568..af58b230c5 100644 --- a/openstackclient/tests/identity/v3/test_user.py +++ b/openstackclient/tests/identity/v3/test_user.py @@ -102,15 +102,16 @@ def test_user_create_no_options(self): **kwargs ) - collist = ('domain_id', 'email', 'enabled', 'id', 'name', 'project_id') + collist = ('default_project_id', 'domain_id', 'email', + 'enabled', 'id', 'name') self.assertEqual(columns, collist) datalist = ( + identity_fakes.project_id, identity_fakes.domain_id, identity_fakes.user_email, True, identity_fakes.user_id, identity_fakes.user_name, - identity_fakes.project_id, ) self.assertEqual(data, datalist) @@ -147,15 +148,16 @@ def test_user_create_password(self): **kwargs ) - collist = ('domain_id', 'email', 'enabled', 'id', 'name', 'project_id') + collist = ('default_project_id', 'domain_id', 'email', + 'enabled', 'id', 'name') self.assertEqual(columns, collist) datalist = ( + identity_fakes.project_id, identity_fakes.domain_id, identity_fakes.user_email, True, identity_fakes.user_id, identity_fakes.user_name, - identity_fakes.project_id, ) self.assertEqual(data, datalist) @@ -195,15 +197,16 @@ def test_user_create_password_prompt(self): **kwargs ) - collist = ('domain_id', 'email', 'enabled', 'id', 'name', 'project_id') + collist = ('default_project_id', 'domain_id', 'email', + 'enabled', 'id', 'name') self.assertEqual(columns, collist) datalist = ( + identity_fakes.project_id, identity_fakes.domain_id, identity_fakes.user_email, True, identity_fakes.user_id, identity_fakes.user_name, - identity_fakes.project_id, ) self.assertEqual(data, datalist) @@ -239,15 +242,16 @@ def test_user_create_email(self): **kwargs ) - collist = ('domain_id', 'email', 'enabled', 'id', 'name', 'project_id') + collist = ('default_project_id', 'domain_id', 'email', + 'enabled', 'id', 'name') self.assertEqual(columns, collist) datalist = ( + identity_fakes.project_id, identity_fakes.domain_id, identity_fakes.user_email, True, identity_fakes.user_id, identity_fakes.user_name, - identity_fakes.project_id, ) self.assertEqual(data, datalist) @@ -260,7 +264,7 @@ def test_user_create_project(self): ) # Set up to return an updated user USER_2 = copy.deepcopy(identity_fakes.USER) - USER_2['project_id'] = identity_fakes.PROJECT_2['id'] + USER_2['default_project_id'] = identity_fakes.PROJECT_2['id'] self.users_mock.create.return_value = fakes.FakeResource( None, USER_2, @@ -298,15 +302,16 @@ def test_user_create_project(self): **kwargs ) - collist = ('domain_id', 'email', 'enabled', 'id', 'name', 'project_id') + collist = ('default_project_id', 'domain_id', 'email', + 'enabled', 'id', 'name') self.assertEqual(columns, collist) datalist = ( + identity_fakes.PROJECT_2['id'], identity_fakes.domain_id, identity_fakes.user_email, True, identity_fakes.user_id, identity_fakes.user_name, - identity_fakes.PROJECT_2['id'], ) self.assertEqual(data, datalist) @@ -342,15 +347,16 @@ def test_user_create_domain(self): **kwargs ) - collist = ('domain_id', 'email', 'enabled', 'id', 'name', 'project_id') + collist = ('default_project_id', 'domain_id', 'email', + 'enabled', 'id', 'name') self.assertEqual(columns, collist) datalist = ( + identity_fakes.project_id, identity_fakes.domain_id, identity_fakes.user_email, True, identity_fakes.user_id, identity_fakes.user_name, - identity_fakes.project_id, ) self.assertEqual(data, datalist) @@ -385,15 +391,16 @@ def test_user_create_enable(self): **kwargs ) - collist = ('domain_id', 'email', 'enabled', 'id', 'name', 'project_id') + collist = ('default_project_id', 'domain_id', 'email', + 'enabled', 'id', 'name') self.assertEqual(columns, collist) datalist = ( + identity_fakes.project_id, identity_fakes.domain_id, identity_fakes.user_email, True, identity_fakes.user_id, identity_fakes.user_name, - identity_fakes.project_id, ) self.assertEqual(data, datalist) @@ -427,15 +434,16 @@ def test_user_create_disable(self): **kwargs ) - collist = ('domain_id', 'email', 'enabled', 'id', 'name', 'project_id') + collist = ('default_project_id', 'domain_id', 'email', + 'enabled', 'id', 'name') self.assertEqual(columns, collist) datalist = ( + identity_fakes.project_id, identity_fakes.domain_id, identity_fakes.user_email, True, identity_fakes.user_id, identity_fakes.user_name, - identity_fakes.project_id, ) self.assertEqual(data, datalist) @@ -524,7 +532,7 @@ def test_user_list_no_options(self): **kwargs ) - collist = ('ID', 'Name') + collist = ['ID', 'Name'] self.assertEqual(columns, collist) datalist = (( identity_fakes.user_id, @@ -554,7 +562,7 @@ def test_user_list_domain(self): **kwargs ) - collist = ('ID', 'Name') + collist = ['ID', 'Name'] self.assertEqual(columns, collist) datalist = (( identity_fakes.user_id, @@ -584,7 +592,7 @@ def test_user_list_group(self): **kwargs ) - collist = ('ID', 'Name') + collist = ['ID', 'Name'] self.assertEqual(columns, collist) datalist = (( identity_fakes.user_id, @@ -614,15 +622,15 @@ def test_user_list_long(self): **kwargs ) - collist = ( + collist = [ 'ID', 'Name', - 'Project Id', - 'Domain Id', + 'Project', + 'Domain', 'Description', 'Email', 'Enabled', - ) + ] self.assertEqual(columns, collist) datalist = (( identity_fakes.user_id, @@ -1020,14 +1028,15 @@ def test_user_show(self): self.users_mock.get.assert_called_with(identity_fakes.user_id) - collist = ('domain_id', 'email', 'enabled', 'id', 'name', 'project_id') + collist = ('default_project_id', 'domain_id', 'email', + 'enabled', 'id', 'name') self.assertEqual(columns, collist) datalist = ( + identity_fakes.project_id, identity_fakes.domain_id, identity_fakes.user_email, True, identity_fakes.user_id, identity_fakes.user_name, - identity_fakes.project_id, ) self.assertEqual(data, datalist) From 4f7777ca0e1451f77d6935e15f87d27a950b5de4 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 31 Dec 2014 09:59:28 -0600 Subject: [PATCH 0307/3494] Command docs: ec2 credentials Add ec2 credentials docs Change-Id: I1699d1c8e9859153557081966654646966a3268d --- .../command-objects/ec2-credentials.rst | 98 +++++++++++++++++++ doc/source/commands.rst | 1 + openstackclient/identity/v2_0/ec2creds.py | 14 +-- 3 files changed, 107 insertions(+), 6 deletions(-) create mode 100644 doc/source/command-objects/ec2-credentials.rst diff --git a/doc/source/command-objects/ec2-credentials.rst b/doc/source/command-objects/ec2-credentials.rst new file mode 100644 index 0000000000..a5b6754947 --- /dev/null +++ b/doc/source/command-objects/ec2-credentials.rst @@ -0,0 +1,98 @@ +=============== +ec2 credentials +=============== + +Identity v2 + +ec2 credentials create +---------------------- + +Create EC2 credentials + +.. program:: ec2 credentials create +.. code-block:: bash + + os ec2 credentials create + [--project ] + [--user ] + +.. option:: --project + + Specify an alternate project (default: current authenticated project) + +.. option:: --user + + Specify an alternate user (default: current authenticated user) + +The :option:`--project` and :option:`--user` options are typically only +useful for admin users, but may be allowed for other users depending on +the policy of the cloud and the roles granted to the user. + +ec2 credentials delete +---------------------- + +Delete EC2 credentials + +.. program:: ec2 credentials delete +.. code-block:: bash + + os ec2 credentials delete + [--user ] + + +.. option:: --user + + Specify a user + +.. _ec2_credentials_delete-access-key: +.. describe:: access-key + + Credentials access key + +The :option:`--user` option is typically only useful for admin users, but +may be allowed for other users depending on the policy of the cloud and +the roles granted to the user. + +ec2 credentials list +-------------------- + +List EC2 credentials + +.. program:: ec2 credentials list +.. code-block:: bash + + os ec2 credentials list + [--user ] + +.. option:: --user + + Filter list by + +The :option:`--user` option is typically only useful for admin users, but +may be allowed for other users depending on the policy of the cloud and +the roles granted to the user. + +ec2 credentials show +-------------------- + +Display EC2 credentials details + +.. program:: ec2 credentials show +.. code-block:: bash + + os ec2 credentials show + [--user ] + + +.. option:: --user + + Specify a user + +.. _ec2_credentials_show-access-key: +.. describe:: access-key + + Credentials access key + +The :option:`--user` option is typically only useful for admin users, but +may be allowed for other users depending on the policy of the cloud and +the roles granted to the user. diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 0040700e86..7e15053f95 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -80,6 +80,7 @@ referring to both Compute and Volume quotas. * ``container``: Object Store - a grouping of objects * ``credentials``: (**Identity**) specific to identity providers * ``domain``: (**Identity**) a grouping of projects +* ``ec2 cedentials``: (**Identity**) AWS EC2-compatibile credentials * ``endpoint``: (**Identity**) the base URL used to contact a specific service * ``extension``: (**Compute**, **Identity**, **Volume**) OpenStack server API extensions * ``flavor``: (**Compute**) pre-defined server configurations: ram, root disk, etc diff --git a/openstackclient/identity/v2_0/ec2creds.py b/openstackclient/identity/v2_0/ec2creds.py index a20ffd4b57..90553eb1f9 100644 --- a/openstackclient/identity/v2_0/ec2creds.py +++ b/openstackclient/identity/v2_0/ec2creds.py @@ -37,12 +37,14 @@ def get_parser(self, prog_name): parser.add_argument( '--project', metavar='', - help=_('Specify a project [admin only]'), + help=_('Specify an alternate project' + ' (default: current authenticated project)'), ) parser.add_argument( '--user', metavar='', - help=_('Specify a user [admin only]'), + help=_('Specify an alternate user' + ' (default: current authenticated user)'), ) return parser @@ -95,7 +97,7 @@ def get_parser(self, prog_name): parser.add_argument( '--user', metavar='', - help=_('Specify a user [admin only]'), + help=_('Specify a user'), ) return parser @@ -125,7 +127,7 @@ def get_parser(self, prog_name): parser.add_argument( '--user', metavar='', - help=_('Specify a user [admin only]'), + help=_('Specify a user'), ) return parser @@ -154,7 +156,7 @@ def take_action(self, parsed_args): class ShowEC2Creds(show.ShowOne): - """Show EC2 credentials""" + """Display EC2 credentials details""" log = logging.getLogger(__name__ + '.ShowEC2Creds') @@ -168,7 +170,7 @@ def get_parser(self, prog_name): parser.add_argument( '--user', metavar='', - help=_('Specify a user [admin only]'), + help=_('Specify a user'), ) return parser From b56da8dde2ef78f057e67b0b307ee7ce2dff2d7d Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 30 Dec 2014 17:46:02 -0600 Subject: [PATCH 0308/3494] Add endpoint v3 docs (update: change version description formats for API versioning) Change-Id: I499ea1d80ad6ad6392468305f761e695d7261e33 --- doc/source/command-objects/endpoint.rst | 161 +++++++++++++++++++++- openstackclient/identity/v2_0/endpoint.py | 47 ++++--- openstackclient/identity/v3/endpoint.py | 71 ++++++---- 3 files changed, 225 insertions(+), 54 deletions(-) diff --git a/doc/source/command-objects/endpoint.rst b/doc/source/command-objects/endpoint.rst index 128ddfa021..7846a1b1f5 100644 --- a/doc/source/command-objects/endpoint.rst +++ b/doc/source/command-objects/endpoint.rst @@ -7,39 +7,190 @@ Identity v2, v3 endpoint create --------------- +Create new endpoint + +*Identity version 2 only* + +.. program:: endpoint create +.. code:: bash + + os endpoint create + --publicurl + [--adminurl ] + [--internalurl ] + [--region ] + + +.. option:: --publicurl + + New endpoint public URL (required) + +.. option:: --adminurl + + New endpoint admin URL + +.. option:: --internalurl + + New endpoint internal URL + +.. option:: --region + + New endpoint region ID + +.. _endpoint_create-endpoint: +.. describe:: + + New endpoint service (name or ID) + +*Identity version 3 only* + .. program:: endpoint create .. code:: bash os endpoint create - --publicurl - [--adminurl ] - [--internalurl ] - [--region ] + [--region + [--enable | --disable] + + + +.. option:: --region + + New endpoint region ID + +.. option:: --enable + + Enable endpoint (default) + +.. option:: --disable + + Disable endpoint + +.. describe:: + + New endpoint service (name or ID) + +.. describe:: + + New endpoint interface type (admin, public or internal) + +.. describe:: + + New endpoint URL endpoint delete --------------- +Delete endpoint + .. program:: endpoint delete .. code:: bash os endpoint delete +.. _endpoint_delete-endpoint: +.. describe:: + + Endpoint ID to delete + endpoint list ------------- +List endpoints + .. program:: endpoint list .. code:: bash os endpoint list + [--service ] + [--region ] [--long] +.. option:: --service + + Filter by service + + *Identity version 3 only* + +.. option:: --interface + + Filter by interface type (admin, public or internal) + + *Identity version 3 only* + +.. option:: --region + + Filter by region ID + + *Identity version 3 only* + +.. option:: --long + + List additional fields in output + + *Identity version 2 only* + +endpoint set +------------ + +Set endpoint properties + +*Identity version 3 only* + +.. program:: endpoint set +.. code:: bash + + os endpoint set + [--region ] + [--interface ] + [--url ] + [--service ] + [--enable | --disable] + + +.. option:: --region + + New endpoint region ID + +.. option:: --interface + + New endpoint interface type (admin, public or internal) + +.. option:: --url + + New endpoint URL + +.. option:: --service + + New endpoint service (name or ID) + +.. option:: --enable + + Enable endpoint + +.. option:: --disable + + Disable endpoint + +.. _endpoint_set-endpoint: +.. describe:: + + Endpoint ID to modify + endpoint show ------------- +Display endpoint details + .. program:: endpoint show .. code:: bash os endpoint show - + + +.. _endpoint_show-endpoint: +.. describe:: + + Endpoint ID to display diff --git a/openstackclient/identity/v2_0/endpoint.py b/openstackclient/identity/v2_0/endpoint.py index c5189b62e7..370a931d40 100644 --- a/openstackclient/identity/v2_0/endpoint.py +++ b/openstackclient/identity/v2_0/endpoint.py @@ -28,7 +28,7 @@ class CreateEndpoint(show.ShowOne): - """Create endpoint""" + """Create new endpoint""" log = logging.getLogger(__name__ + '.CreateEndpoint') @@ -36,25 +36,30 @@ def get_parser(self, prog_name): parser = super(CreateEndpoint, self).get_parser(prog_name) parser.add_argument( 'service', - metavar='', - help=_('New endpoint service')) - parser.add_argument( - '--region', - metavar='', - help=_('New endpoint region')) + metavar='', + help=_('New endpoint service (name or ID)'), + ) parser.add_argument( '--publicurl', - metavar='', + metavar='', required=True, - help=_('New endpoint public URL')) + help=_('New endpoint public URL (required)'), + ) parser.add_argument( '--adminurl', - metavar='', - help=_('New endpoint admin URL')) + metavar='', + help=_('New endpoint admin URL'), + ) parser.add_argument( '--internalurl', - metavar='', - help=_('New endpoint internal URL')) + metavar='', + help=_('New endpoint internal URL'), + ) + parser.add_argument( + '--region', + metavar='', + help=_('New endpoint region ID'), + ) return parser def take_action(self, parsed_args): @@ -76,7 +81,7 @@ def take_action(self, parsed_args): class DeleteEndpoint(command.Command): - """Delete endpoint command""" + """Delete endpoint""" log = logging.getLogger(__name__ + '.DeleteEndpoint') @@ -85,7 +90,7 @@ def get_parser(self, prog_name): parser.add_argument( 'endpoint', metavar='', - help=_('ID of endpoint to delete')) + help=_('Endpoint ID to delete')) return parser def take_action(self, parsed_args): @@ -96,7 +101,7 @@ def take_action(self, parsed_args): class ListEndpoint(lister.Lister): - """List endpoint command""" + """List endpoints""" log = logging.getLogger(__name__ + '.ListEndpoint') @@ -106,7 +111,8 @@ def get_parser(self, prog_name): '--long', action='store_true', default=False, - help=_('List additional fields in output')) + help=_('List additional fields in output'), + ) return parser def take_action(self, parsed_args): @@ -131,7 +137,7 @@ def take_action(self, parsed_args): class ShowEndpoint(show.ShowOne): - """Show endpoint command""" + """Display endpoint details""" log = logging.getLogger(__name__ + '.ShowEndpoint') @@ -139,8 +145,9 @@ def get_parser(self, prog_name): parser = super(ShowEndpoint, self).get_parser(prog_name) parser.add_argument( 'endpoint_or_service', - metavar='', - help=_('Endpoint ID or name, type or ID of service to display')) + metavar='', + help=_('Endpoint ID to display'), + ) return parser def take_action(self, parsed_args): diff --git a/openstackclient/identity/v3/endpoint.py b/openstackclient/identity/v3/endpoint.py index 0c077c5a34..5b8104e514 100644 --- a/openstackclient/identity/v3/endpoint.py +++ b/openstackclient/identity/v3/endpoint.py @@ -28,7 +28,7 @@ class CreateEndpoint(show.ShowOne): - """Create endpoint command""" + """Create new endpoint""" log = logging.getLogger(__name__ + '.CreateEndpoint') @@ -37,27 +37,31 @@ def get_parser(self, prog_name): parser.add_argument( 'service', metavar='', - help='Name or ID of new endpoint service') + help='New endpoint service (name or ID)', + ) parser.add_argument( 'interface', metavar='', choices=['admin', 'public', 'internal'], - help='New endpoint interface, must be admin, public or internal') + help='New endpoint interface type (admin, public or internal)', + ) parser.add_argument( 'url', metavar='', - help='New endpoint URL') + help='New endpoint URL', + ) parser.add_argument( '--region', - metavar='', - help='New endpoint region') + metavar='', + help='New endpoint region ID', + ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', dest='enabled', action='store_true', default=True, - help='Enable endpoint', + help='Enable endpoint (default)', ) enable_group.add_argument( '--disable', @@ -89,7 +93,7 @@ def take_action(self, parsed_args): class DeleteEndpoint(command.Command): - """Delete endpoint command""" + """Delete endpoint""" log = logging.getLogger(__name__ + '.DeleteEndpoint') @@ -97,8 +101,9 @@ def get_parser(self, prog_name): parser = super(DeleteEndpoint, self).get_parser(prog_name) parser.add_argument( 'endpoint', - metavar='', - help='ID of endpoint to delete') + metavar='', + help='Endpoint ID to delete', + ) return parser def take_action(self, parsed_args): @@ -111,7 +116,7 @@ def take_action(self, parsed_args): class ListEndpoint(lister.Lister): - """List endpoint command""" + """List endpoints""" log = logging.getLogger(__name__ + '.ListEndpoint') @@ -120,17 +125,19 @@ def get_parser(self, prog_name): parser.add_argument( '--service', metavar='', - help='Filter by a specific service') + help='Filter by service', + ) parser.add_argument( '--interface', metavar='', choices=['admin', 'public', 'internal'], - help='Filter by a specific interface, must be admin, public or' - ' internal') + help='Filter by interface type (admin, public or internal)', + ) parser.add_argument( '--region', - metavar='', - help='Filter by a specific region') + metavar='', + help='Filter by region ID', + ) return parser def take_action(self, parsed_args): @@ -160,7 +167,7 @@ def take_action(self, parsed_args): class SetEndpoint(command.Command): - """Set endpoint command""" + """Set endpoint properties""" log = logging.getLogger(__name__ + '.SetEndpoint') @@ -168,25 +175,30 @@ def get_parser(self, prog_name): parser = super(SetEndpoint, self).get_parser(prog_name) parser.add_argument( 'endpoint', - metavar='', - help='ID of endpoint to update') + metavar='', + help='Endpoint ID to modify', + ) + parser.add_argument( + '--region', + metavar='', + help='New endpoint region ID', + ) parser.add_argument( '--interface', metavar='', choices=['admin', 'public', 'internal'], - help='New endpoint interface, must be admin|public|internal') + help='New endpoint interface type (admin, public or internal)', + ) parser.add_argument( '--url', metavar='', - help='New endpoint URL') + help='New endpoint URL', + ) parser.add_argument( '--service', metavar='', - help='Name or ID of new endpoint service') - parser.add_argument( - '--region', - metavar='', - help='New endpoint region') + help='New endpoint service (name or ID)', + ) enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', @@ -238,7 +250,7 @@ def take_action(self, parsed_args): class ShowEndpoint(show.ShowOne): - """Show endpoint command""" + """Display endpoint details""" log = logging.getLogger(__name__ + '.ShowEndpoint') @@ -246,8 +258,9 @@ def get_parser(self, prog_name): parser = super(ShowEndpoint, self).get_parser(prog_name) parser.add_argument( 'endpoint', - metavar='', - help='ID of endpoint to display') + metavar='', + help='Endpoint ID to display', + ) return parser def take_action(self, parsed_args): From 3807354cfecd887094fe55ba7944161d75e38d21 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 31 Dec 2014 09:56:20 -0600 Subject: [PATCH 0309/3494] Command docs: group Fix up formatting for group command docs and help Change-Id: Icda79842d52da90d5eac2b0fdbc0d576d371378d --- doc/source/command-objects/group.rst | 46 ++++++++++++++-------------- doc/source/commands.rst | 2 +- openstackclient/identity/v3/group.py | 39 ++++++++++++----------- 3 files changed, 45 insertions(+), 42 deletions(-) diff --git a/doc/source/command-objects/group.rst b/doc/source/command-objects/group.rst index 85a0c5cd79..3e7e806f17 100644 --- a/doc/source/command-objects/group.rst +++ b/doc/source/command-objects/group.rst @@ -16,18 +16,18 @@ Add user to group -.. option:: +.. describe:: - Group that user will be added to (name or ID) + Group to contain (name or ID) -.. option:: +.. describe:: - User to add to group (name or ID) + User to add to (name or ID) group contains user ------------------- -Check user in group +Check user membership in group .. program:: group contains user .. code:: bash @@ -36,11 +36,11 @@ Check user in group -.. option:: +.. describe:: - Group to check if user belongs to (name or ID) + Group to check (name or ID) -.. option:: +.. describe:: User to check (name or ID) @@ -60,7 +60,7 @@ Create new group .. option:: --domain - References the domain ID or name which owns the group + Domain to contain new group (name or ID) .. option:: --description @@ -72,7 +72,7 @@ Create new group If the group already exists, return the existing group data and do not fail. -.. option:: +.. describe:: New group name @@ -90,9 +90,9 @@ Delete group .. option:: --domain - Domain where group resides (name or ID) + Domain containing group(s) (name or ID) -.. option:: +.. describe:: Group(s) to delete (name or ID) @@ -115,11 +115,11 @@ List groups .. option:: --user - List group memberships for (name or ID) + Filter group list by (name or ID) .. option:: --long - List additional fields in output (defaults to false) + List additional fields in output group remove user ----------------- @@ -133,13 +133,13 @@ Remove user from group -.. option:: +.. describe:: - Group that user will be removed from (name or ID) + Group containing (name or ID) -.. option:: +.. describe:: - User to remove from group (name or ID) + User to remove from (name or ID) group set --------- @@ -161,20 +161,20 @@ Set group properties .. option:: --domain - New domain that will now own the group (name or ID) + New domain to contain (name or ID) .. option:: --description New group description -.. option:: +.. describe:: Group to modify (name or ID) group show ---------- -Show group details +Display group details .. program:: group show .. code:: bash @@ -185,8 +185,8 @@ Show group details .. option:: --domain - Domain where group resides (name or ID) + Domain containing (name or ID) -.. option:: +.. describe:: Group to display (name or ID) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index eba0a22f71..d137a2707c 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -83,7 +83,7 @@ referring to both Compute and Volume quotas. * ``endpoint``: (**Identity**) the base URL used to contact a specific service * ``extension``: (**Compute**, **Identity**, **Volume**) OpenStack server API extensions * ``flavor``: (**Compute**) pre-defined server configurations: ram, root disk, etc -* ``group``: Identity - a grouping of users +* ``group``: (**Identity**) a grouping of users * ``host``: Compute - the physical computer running a hypervisor * ``hypervisor``: Compute - the virtual machine manager * ``identity provider``: Identity - a source of users and authentication diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index fbd8dd720b..94e101f31a 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -39,12 +39,12 @@ def get_parser(self, prog_name): parser.add_argument( 'group', metavar='', - help='Group that user will be added to (name or ID)', + help='Group to contain (name or ID)', ) parser.add_argument( 'user', metavar='', - help='User to add to group (name or ID)', + help='User to add to (name or ID)', ) return parser @@ -68,7 +68,7 @@ def take_action(self, parsed_args): class CheckUserInGroup(command.Command): - """Check user in group""" + """Check user membership in group""" log = logging.getLogger(__name__ + '.CheckUserInGroup') @@ -77,7 +77,7 @@ def get_parser(self, prog_name): parser.add_argument( 'group', metavar='', - help='Group to check if user belongs to (name or ID)', + help='Group to check (name or ID)', ) parser.add_argument( 'user', @@ -115,15 +115,18 @@ def get_parser(self, prog_name): parser.add_argument( 'name', metavar='', - help='New group name') - parser.add_argument( - '--description', - metavar='', - help='New group description') + help='New group name', + ) parser.add_argument( '--domain', metavar='', - help='References the domain ID or name which owns the group') + help='Domain to contain new group (name or ID)', + ) + parser.add_argument( + '--description', + metavar='', + help='New group description', + ) parser.add_argument( '--or-show', action='store_true', @@ -173,7 +176,7 @@ def get_parser(self, prog_name): parser.add_argument( '--domain', metavar='', - help='Domain where group resides (name or ID)', + help='Domain containing group(s) (name or ID)', ) return parser @@ -211,7 +214,7 @@ def get_parser(self, prog_name): parser.add_argument( '--user', metavar='', - help='List group memberships for (name or ID)', + help='Filter group list by (name or ID)', ) parser.add_argument( '--long', @@ -259,7 +262,7 @@ def take_action(self, parsed_args): class RemoveUserFromGroup(command.Command): - """Remove user to group""" + """Remove user from group""" log = logging.getLogger(__name__ + '.RemoveUserFromGroup') @@ -268,12 +271,12 @@ def get_parser(self, prog_name): parser.add_argument( 'group', metavar='', - help='Group that user will be removed from (name or ID)', + help='Group containing (name or ID)', ) parser.add_argument( 'user', metavar='', - help='User to remove from group (name or ID)', + help='User to remove from (name or ID)', ) return parser @@ -314,7 +317,7 @@ def get_parser(self, prog_name): parser.add_argument( '--domain', metavar='', - help='New domain that will now own the group (name or ID)') + help='New domain to contain (name or ID)') parser.add_argument( '--description', metavar='', @@ -341,7 +344,7 @@ def take_action(self, parsed_args): class ShowGroup(show.ShowOne): - """Show group details""" + """Display group details""" log = logging.getLogger(__name__ + '.ShowGroup') @@ -355,7 +358,7 @@ def get_parser(self, prog_name): parser.add_argument( '--domain', metavar='', - help='Domain where group resides (name or ID)', + help='Domain containing (name or ID)', ) return parser From 369ae3f9f06c90e8d57b71befa6bc22a843c2f7d Mon Sep 17 00:00:00 2001 From: zhiyuan_cai Date: Sun, 4 Jan 2015 11:26:18 +0800 Subject: [PATCH 0310/3494] Check if service.name available before access Currently v3 endpoint commands access service.name directly, while name is not a required attribute of service. So if we associate an endpoint to a service without name, we will get an AttributeError executing v3 endpoint commands later. This patch addresses this issue by checking if service.name is available before accessing it. Change-Id: I3dd686ef02a2e21e2049a49cb55634385c2ecfaf Closes-Bug: #1406737 --- openstackclient/identity/v3/endpoint.py | 13 ++- openstackclient/tests/identity/v3/fakes.py | 8 ++ .../tests/identity/v3/test_endpoint.py | 102 ++++++++++++++++-- 3 files changed, 109 insertions(+), 14 deletions(-) diff --git a/openstackclient/identity/v3/endpoint.py b/openstackclient/identity/v3/endpoint.py index 0c077c5a34..eba7ca1471 100644 --- a/openstackclient/identity/v3/endpoint.py +++ b/openstackclient/identity/v3/endpoint.py @@ -27,6 +27,13 @@ from openstackclient.identity import common +def get_service_name(service): + if hasattr(service, 'name'): + return service.name + else: + return '' + + class CreateEndpoint(show.ShowOne): """Create endpoint command""" @@ -83,7 +90,7 @@ def take_action(self, parsed_args): info = {} endpoint._info.pop('links') info.update(endpoint._info) - info['service_name'] = service.name + info['service_name'] = get_service_name(service) info['service_type'] = service.type return zip(*sorted(six.iteritems(info))) @@ -150,7 +157,7 @@ def take_action(self, parsed_args): for ep in data: service = common.find_service(identity_client, ep.service_id) - ep.service_name = service.name + ep.service_name = get_service_name(service) ep.service_type = service.type return (columns, (utils.get_item_properties( @@ -261,6 +268,6 @@ def take_action(self, parsed_args): info = {} endpoint._info.pop('links') info.update(endpoint._info) - info['service_name'] = service.name + info['service_name'] = get_service_name(service) info['service_type'] = service.type return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index 3afb0cd90d..68e67519dc 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -158,6 +158,14 @@ 'links': base_url + 'services/' + service_id, } +SERVICE_WITHOUT_NAME = { + 'id': service_id, + 'type': service_type, + 'description': service_description, + 'enabled': True, + 'links': base_url + 'services/' + service_id, +} + endpoint_id = 'e-123' endpoint_url = 'http://127.0.0.1:35357' endpoint_region = 'RegionOne' diff --git a/openstackclient/tests/identity/v3/test_endpoint.py b/openstackclient/tests/identity/v3/test_endpoint.py index ea05326e20..ecfa71ab3c 100644 --- a/openstackclient/tests/identity/v3/test_endpoint.py +++ b/openstackclient/tests/identity/v3/test_endpoint.py @@ -31,6 +31,9 @@ def setUp(self): self.services_mock = self.app.client_manager.identity.services self.services_mock.reset_mock() + def get_fake_service_name(self): + return identity_fakes.service_name + class TestEndpointCreate(TestEndpoint): @@ -92,7 +95,7 @@ def test_endpoint_create_no_options(self): identity_fakes.endpoint_interface, identity_fakes.endpoint_region, identity_fakes.service_id, - identity_fakes.service_name, + self.get_fake_service_name(), identity_fakes.service_type, identity_fakes.endpoint_url, ) @@ -139,7 +142,7 @@ def test_endpoint_create_region(self): identity_fakes.endpoint_interface, identity_fakes.endpoint_region, identity_fakes.service_id, - identity_fakes.service_name, + self.get_fake_service_name(), identity_fakes.service_type, identity_fakes.endpoint_url, ) @@ -185,7 +188,7 @@ def test_endpoint_create_enable(self): identity_fakes.endpoint_interface, identity_fakes.endpoint_region, identity_fakes.service_id, - identity_fakes.service_name, + self.get_fake_service_name(), identity_fakes.service_type, identity_fakes.endpoint_url, ) @@ -231,7 +234,7 @@ def test_endpoint_create_disable(self): identity_fakes.endpoint_interface, identity_fakes.endpoint_region, identity_fakes.service_id, - identity_fakes.service_name, + self.get_fake_service_name(), identity_fakes.service_type, identity_fakes.endpoint_url, ) @@ -309,7 +312,7 @@ def test_endpoint_list_no_options(self): datalist = (( identity_fakes.endpoint_id, identity_fakes.endpoint_region, - identity_fakes.service_name, + self.get_fake_service_name(), identity_fakes.service_type, True, identity_fakes.endpoint_interface, @@ -319,10 +322,10 @@ def test_endpoint_list_no_options(self): def test_endpoint_list_service(self): arglist = [ - '--service', identity_fakes.service_name, + '--service', identity_fakes.service_id, ] verifylist = [ - ('service', identity_fakes.service_name), + ('service', identity_fakes.service_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -341,7 +344,7 @@ def test_endpoint_list_service(self): datalist = (( identity_fakes.endpoint_id, identity_fakes.endpoint_region, - identity_fakes.service_name, + self.get_fake_service_name(), identity_fakes.service_type, True, identity_fakes.endpoint_interface, @@ -373,7 +376,7 @@ def test_endpoint_list_interface(self): datalist = (( identity_fakes.endpoint_id, identity_fakes.endpoint_region, - identity_fakes.service_name, + self.get_fake_service_name(), identity_fakes.service_type, True, identity_fakes.endpoint_interface, @@ -405,7 +408,7 @@ def test_endpoint_list_region(self): datalist = (( identity_fakes.endpoint_id, identity_fakes.endpoint_region, - identity_fakes.service_name, + self.get_fake_service_name(), identity_fakes.service_type, True, identity_fakes.endpoint_interface, @@ -664,8 +667,85 @@ def test_endpoint_show(self): identity_fakes.endpoint_interface, identity_fakes.endpoint_region, identity_fakes.service_id, - identity_fakes.service_name, + self.get_fake_service_name(), identity_fakes.service_type, identity_fakes.endpoint_url, ) self.assertEqual(datalist, data) + + +class TestEndpointCreateServiceWithoutName(TestEndpointCreate): + + def setUp(self): + super(TestEndpointCreate, self).setUp() + + self.endpoints_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ENDPOINT), + loaded=True, + ) + + # This is the return value for common.find_resource(service) + self.services_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.SERVICE_WITHOUT_NAME), + loaded=True, + ) + + # Get the command object to test + self.cmd = endpoint.CreateEndpoint(self.app, None) + + def get_fake_service_name(self): + return '' + + +class TestEndpointListServiceWithoutName(TestEndpointList): + + def setUp(self): + super(TestEndpointList, self).setUp() + + self.endpoints_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ENDPOINT), + loaded=True, + ), + ] + + # This is the return value for common.find_resource(service) + self.services_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.SERVICE_WITHOUT_NAME), + loaded=True, + ) + + # Get the command object to test + self.cmd = endpoint.ListEndpoint(self.app, None) + + def get_fake_service_name(self): + return '' + + +class TestEndpointShowServiceWithoutName(TestEndpointShow): + + def setUp(self): + super(TestEndpointShow, self).setUp() + + self.endpoints_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ENDPOINT), + loaded=True, + ) + + # This is the return value for common.find_resource(service) + self.services_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.SERVICE_WITHOUT_NAME), + loaded=True, + ) + + # Get the command object to test + self.cmd = endpoint.ShowEndpoint(self.app, None) + + def get_fake_service_name(self): + return '' From 34975edd1456b65986f24f8b17390e3ef99b4ac4 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 5 Jan 2015 01:36:35 -0500 Subject: [PATCH 0311/3494] tweak the server command docs the formatting used for the server commands is not the same as the other command docs, this patch addresses that issue. Change-Id: I5f31cf6a317d9eb35ec46185800fade3dd956dc4 --- doc/source/command-objects/server.rst | 246 +++++++++++++++++--------- 1 file changed, 164 insertions(+), 82 deletions(-) diff --git a/doc/source/command-objects/server.rst b/doc/source/command-objects/server.rst index 2f5aef1070..48cefe6ada 100644 --- a/doc/source/command-objects/server.rst +++ b/doc/source/command-objects/server.rst @@ -14,10 +14,12 @@ Add security group to server -:option:`` +.. describe:: + Server (name or ID) -:option:`` +.. describe:: + Security group to add (name or ID) server add volume @@ -32,13 +34,16 @@ Add volume to server -:option:`--device` +.. option:: --device + Server internal device name for volume -:option:`` +.. describe:: + Server (name or ID) -:option:`` +.. describe:: + Volume to add (name or ID) server create @@ -66,55 +71,72 @@ Create a new server [--wait] -:option:`--image` +.. option:: --image + Create server from this image -:option:`--volume` +.. option:: --volume + Create server from this volume -:option:`--flavor` +.. option:: --flavor + Create server with this flavor -:option:`--security-group` +.. option:: --security-group + Security group to assign to this server (repeat for multiple groups) -:option:`--key-name` +.. option:: --key-name + Keypair to inject into this server (optional extension) -:option:`--property` +.. option:: --property + Set a property on this server (repeat for multiple values) -:option:`--file` +.. option:: --file + File to inject into image before boot (repeat for multiple files) -:option:`--user-data` +.. option:: --user-data + User data file to serve from the metadata server -:option:`--availability-zone` +.. option:: --availability-zone + Select an availability zone for the server -:option:`--block-device-mapping` +.. option:: --block-device-mapping + Map block devices; map is ::: (optional extension) -:option:`--nic` +.. option:: --nic + Specify NIC configuration (optional extension) -:option:`--hint` +.. option:: --hint + Hints for the scheduler (optional extension) -:option:`--config-drive` |True +.. option:: --config-drive |True + Use specified volume as the config drive, or 'True' to use an ephemeral drive -:option:`--min` +.. option:: --min + Minimum number of servers to launch (default=1) -:option:`--max` +.. option:: --max + Maximum number of servers to launch (default=1) -:option:`--wait` +.. option:: --wait + Wait for build to complete -:option:`` +.. describe:: + New server name server delete @@ -127,7 +149,8 @@ Delete server(s) os server delete [ ...] -:option:`` +.. describe:: + Server to delete (name or ID) server list @@ -150,37 +173,48 @@ List servers [--all-projects] [--long] -:option:`--reservation-id` +.. option:: --reservation-id + Only return instances that match the reservation -:option:`--ip` +.. option:: --ip + Regular expression to match IP addresses -:option:`--ip6` +.. option:: --ip6 + Regular expression to match IPv6 addresses -:option:`--name` +.. option:: --name + Regular expression to match names -:option:`--instance-name` +.. option:: --instance-name + Regular expression to match instance name (admin only) -:option:`--status` +.. option:: --status + Search by server status -:option:`--flavor` +.. option:: --flavor + Search by flavor ID -:option:`--image` +.. option:: --image + Search by image ID -:option:`--host` +.. option:: --host + Search by hostname -:option:`--all-projects` +.. option:: --all-projects + Include all projects (admin only) -:option:`--long` +.. option:: --long + List additional fields in output server lock @@ -193,7 +227,8 @@ Lock server os server lock -:option:`` +.. describe:: + Server (name or ID) server migrate @@ -210,25 +245,32 @@ Migrate server to different host [--wait] -:option:`--live` +.. option:: --live + Target hostname -:option:`--shared-migration` +.. option:: --shared-migration + Perform a shared live migration (default) -:option:`--block-migration` +.. option:: --block-migration + Perform a block live migration -:option:`--disk-overcommit` +.. option:: --disk-overcommit + Allow disk over-commit on the destination host -:option:`--no-disk-overcommit` +.. option:: --no-disk-overcommit + Do not over-commit disk on the destination host (default) -:option:`--wait` +.. option:: --wait + Wait for resize to complete -:option:`` +.. describe:: + Server to migrate (name or ID) server pause @@ -241,7 +283,8 @@ Pause server os server pause -:option:`` +.. describe:: + Server (name or ID) server reboot @@ -256,16 +299,20 @@ Perform a hard or soft server reboot [--wait] -:option:`--hard` +.. option:: --hard + Perform a hard reboot -:option:`--soft` +.. option:: --soft + Perform a soft reboot -:option:`--wait` +.. option:: --wait + Wait for reboot to complete -:option:`` +.. describe:: + Server (name or ID) server rebuild @@ -281,16 +328,20 @@ Rebuild server [--wait] -:option:`--image` +.. option:: --image + Recreate server from this image -:option:`--password` +.. option:: --password + Set the password on the rebuilt instance -:option:`--wait` +.. option:: --wait + Wait for rebuild to complete -:option:`` +.. describe:: + Server (name or ID) server remove security group @@ -304,10 +355,12 @@ Remove security group from server -:option:`` +.. describe:: + Name or ID of server to use -:option:`` +.. describe:: + Name or ID of security group to remove from server server remove volume @@ -321,10 +374,12 @@ Remove volume from server -:option:`` +.. describe:: + Server (name or ID) -:option:`` +.. describe:: + Volume to remove (name or ID) server rescue @@ -337,7 +392,8 @@ Put server in rescue mode os server rescue -:option:`` +.. describe:: + Server (name or ID) server resize @@ -356,19 +412,24 @@ Scale server to a new flavor --verify | --revert -:option:`--flavor` +.. option:: --flavor + Resize server to specified flavor -:option:`--verify` +.. option:: --verify + Verify server resize is complete -:option:`--revert` +.. option:: --revert + Restore server state before resize -:option:`--wait` +.. option:: --wait + Wait for resize to complete -:option:`` +.. describe:: + Server (name or ID) A resize operation is implemented by creating a new server and copying @@ -387,7 +448,8 @@ Resume server os server resume -:option:`` +.. describe:: + Server (name or ID) server set @@ -404,17 +466,21 @@ Set server properties --root-password -:option:`--name` +.. option:: --name + New server name -:option:`--root-password` +.. option:: --root-password + Set new root password (interactive only) -:option:`--property` +.. option:: --property + Property to add/change for this server (repeat option to set multiple properties) -:option:`` +.. describe:: + Server (name or ID) server show @@ -428,10 +494,12 @@ Show server details [--diagnostics] -:option:`--diagnostics` +.. option:: --diagnostics + Display server diagnostics information -:option:`` +.. describe:: + Server (name or ID) server ssh @@ -449,28 +517,36 @@ Ssh to server [--public | --private | --address-type ] -:option:`--login` +.. option:: --login + Login name (ssh -l option) -:option:`--port` +.. option:: --port + Destination port (ssh -p option) -:option:`--identity` +.. option:: --identity + Private key file (ssh -i option) -:option:`--option` +.. option:: --option + Options in ssh_config(5) format (ssh -o option) -:option:`--public` +.. option:: --public + Use public IP address -:option:`--private` +.. option:: --private + Use private IP address -:option:`--address-type` +.. option:: --address-type + Use other IP address (public, private, etc) -:option:`` +.. describe:: + Server (name or ID) server suspend @@ -483,7 +559,8 @@ Suspend server os server suspend -:option:`` +.. describe:: + Server (name or ID) server unlock @@ -496,7 +573,8 @@ Unlock server os server unlock -:option:`` +.. describe:: + Server (name or ID) server unpause @@ -509,7 +587,8 @@ Unpause server os server unpause -:option:`` +.. describe:: + Server (name or ID) server unrescue @@ -522,7 +601,8 @@ Restore server from rescue mode os server unrescue -:option:`` +.. describe:: + Server (name or ID) server unset @@ -537,8 +617,10 @@ Unset server properties [--property ] ... -:option:`--property` +.. option:: --property + Property key to remove from server (repeat to set multiple values) -:option:`` +.. describe:: + Server (name or ID) From ca92608974a8fe9a54951d0ea6b24ab59a5b7a06 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 5 Jan 2015 01:27:47 -0500 Subject: [PATCH 0312/3494] Command doc: volume type Change-Id: I7e36daa027639d6a782043d4181c1b328335975a --- doc/source/command-objects/volume-type.rst | 95 ++++++++++++++++++++++ doc/source/commands.rst | 2 +- openstackclient/volume/v1/type.py | 14 ++-- 3 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 doc/source/command-objects/volume-type.rst diff --git a/doc/source/command-objects/volume-type.rst b/doc/source/command-objects/volume-type.rst new file mode 100644 index 0000000000..0898df528b --- /dev/null +++ b/doc/source/command-objects/volume-type.rst @@ -0,0 +1,95 @@ +=========== +volume type +=========== + +Volume v1 + +volume type create +------------------ + +Create new volume type + +.. program:: volume type create +.. code:: bash + + os volume type create + [--property [...] ] + + +.. option:: --property + + Set a property on this volume type (repeat option to set multiple properties) + +.. describe:: + + New volume type name + +volume type delete +------------------ + +Delete volume type + +.. program:: volume type delete +.. code:: bash + + os volume type delete + + +.. describe:: + + Volume type to delete (name or ID) + +volume type list +---------------- + +List volume types + +.. program:: volume type list +.. code:: bash + + os volume type list + [--long] + +.. option:: --long + + List additional fields in output + +volume type set +--------------- + +Set volume type properties + +.. program:: volume type set +.. code:: bash + + os volume type set + [--property [...] ] + + +.. option:: --property + + Property to add or modify for this volume type (repeat option to set multiple properties) + +.. describe:: + + Volume type to modify (name or ID) + +volume type unset +----------------- + +Unset volume type properties + +.. program:: volume type unset +.. code:: bash + + os volume type unset + [--property ] + + +.. option:: --property + + Property to remove from volume type (repeat option to remove multiple properties) + +.. describe:: + + Volume type to modify (name or ID) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 4b2e6355e4..5bec1a8518 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -113,7 +113,7 @@ referring to both Compute and Volume quotas. * ``user``: (**Identity**) individual cloud resources users * ``user role``: (**Identity**) roles assigned to a user * ``volume``: Volume - block volumes -* ``volume type``: Volume - deployment-specific types of volumes available +* ``volume type``: (**Volume**) deployment-specific types of volumes available Actions ------- diff --git a/openstackclient/volume/v1/type.py b/openstackclient/volume/v1/type.py index 71bfc9eaba..46d1828b1e 100644 --- a/openstackclient/volume/v1/type.py +++ b/openstackclient/volume/v1/type.py @@ -71,7 +71,7 @@ def get_parser(self, prog_name): parser.add_argument( 'volume_type', metavar='', - help='Name or ID of volume type to delete', + help='Volume type to delete (name or ID)', ) return parser @@ -115,7 +115,7 @@ def take_action(self, parsed_args): class SetVolumeType(command.Command): - """Set volume type property""" + """Set volume type properties""" log = logging.getLogger(__name__ + '.SetVolumeType') @@ -124,13 +124,13 @@ def get_parser(self, prog_name): parser.add_argument( 'volume_type', metavar='', - help='Volume type name or ID to update', + help='Volume type to modify (name or ID)', ) parser.add_argument( '--property', metavar='', action=parseractions.KeyValueAction, - help='Property to add/change for this volume type ' + help='Property to add or modify for this volume type ' '(repeat option to set multiple properties)', ) return parser @@ -148,7 +148,7 @@ def take_action(self, parsed_args): class UnsetVolumeType(command.Command): - """Unset volume type property""" + """Unset volume type properties""" log = logging.getLogger(__name__ + '.UnsetVolumeType') @@ -157,14 +157,14 @@ def get_parser(self, prog_name): parser.add_argument( 'volume_type', metavar='', - help='Type ID or name to remove', + help='Volume type to modify (name or ID)', ) parser.add_argument( '--property', metavar='', action='append', default=[], - help='Property key to remove from volume ' + help='Property to remove from volume type ' '(repeat option to remove multiple properties)', ) return parser From 6b196d1a17531b56d0a19b95a81a21c6a2ccc625 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 5 Jan 2015 01:44:33 -0500 Subject: [PATCH 0313/3494] Update the command list We've been making changes to the commands.rst file as we add command docs, looks like we missed a few. Also fixed a few typos Change-Id: Ie93280a7e5ba37303a1984a68870b5a4fc5c6e06 --- doc/source/commands.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 4b2e6355e4..bb3db18755 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -77,13 +77,13 @@ referring to both Compute and Volume quotas. * ``console log``: (**Compute**) server console text dump * ``console url``: (**Compute**) server remote console URL * ``consumer``: Identity - OAuth-based delegatee -* ``container``: Object Store - a grouping of objects +* ``container``: (**Object Store**) a grouping of objects * ``credentials``: (**Identity**) specific to identity providers * ``domain``: (**Identity**) a grouping of projects -* ``ec2 cedentials``: (**Identity**) AWS EC2-compatibile credentials +* ``ec2 cedentials``: (**Identity**) AWS EC2-compatible credentials * ``endpoint``: (**Identity**) the base URL used to contact a specific service * ``extension``: (**Compute**, **Identity**, **Volume**) OpenStack server API extensions -* ``flavor``: (**Compute**) pre-defined server configurations: ram, root disk, etc +* ``flavor``: (**Compute**) predefined server configurations: ram, root disk, etc * ``group``: (**Identity**) a grouping of users * ``host``: Compute - the physical computer running a hypervisor * ``hypervisor``: Compute - the virtual machine manager @@ -95,13 +95,14 @@ referring to both Compute and Volume quotas. * ``limits``: (**Compute**, **Volume**) resource usage limits * ``module``: internal - installed Python modules in the OSC process * ``network``: Network - a virtual network for connecting servers and other resources -* ``object``: Object Store - a single file in the Object Store +* ``object``: (**Object Store**) a single file in the Object Store * ``policy``: Identity - determines authorization * ``project``: (**Identity**) owns a group of resources * ``quota``: (**Compute**, **Volume**) resource usage restrictions * ``region``: (**Identity**) a subset of an OpenStack deployment * ``request token``: Identity - temporary OAuth-based token -* ``role``: Identity - a policy object used to determine authorization +* ``role``: (**Identity**) a policy object used to determine authorization +* ``role assignment``: (**Identity**) a relationship between roles, users or groups, and domains or projects * ``security group``: Compute, Network - groups of network access rules * ``security group rule``: Compute, Network - the individual rules that define protocol/IP/port access * ``server``: (**Compute**) virtual machine instance From 265ca582f02c5cf107c677051c65d43534aaddbb Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 5 Jan 2015 01:53:46 -0500 Subject: [PATCH 0314/3494] Command docs: volume Change-Id: Id1e500d5fb19ffdeb0d1bde9e22c3143c0873d0c --- doc/source/command-objects/volume.rst | 189 ++++++++++++++++++++++++++ doc/source/commands.rst | 2 +- openstackclient/volume/v1/volume.py | 45 +++--- 3 files changed, 213 insertions(+), 23 deletions(-) create mode 100644 doc/source/command-objects/volume.rst diff --git a/doc/source/command-objects/volume.rst b/doc/source/command-objects/volume.rst new file mode 100644 index 0000000000..2eec2d7b25 --- /dev/null +++ b/doc/source/command-objects/volume.rst @@ -0,0 +1,189 @@ +====== +volume +====== + +Volume v1 + +volume create +------------- + +Create new volume + +.. program:: volume create +.. code:: bash + + os volume create + --size + [--snapshot-id ] + [--description ] + [--type ] + [--user ] + [--project ] + [--availability-zone ] + [--image ] + [--source ] + [--property [...] ] + + +.. option:: --size (required) + + New volume size in GB + +.. option:: --snapshot-id + + Use as source of new volume + +.. option:: --description + + New volume description + +.. option:: --type + + Use as the new volume type + +.. option:: --user + + Specify an alternate user (name or ID) + +.. option:: --project + + Specify an alternate project (name or ID) + +.. option:: --availability-zone + + Create new volume in + +.. option:: --image + + Use as source of new volume (name or ID) + +.. option:: --source + + Volume to clone (name or ID) + +.. option:: --property + + Set a property on this volume (repeat option to set multiple properties) + +.. describe:: + + New volume name + +The :option:`--project` and :option:`--user` options are typically only +useful for admin users, but may be allowed for other users depending on +the policy of the cloud and the roles granted to the user. + +volume delete +------------- + +Delete volume(s) + +.. program:: volume delete +.. code:: bash + + os volume delete + [--force] + [ ...] + +.. option:: --force + + Attempt forced removal of volume(s), regardless of state (defaults to False) + +.. describe:: + + Volume(s) to delete (name or ID) + +volume list +----------- + +List volumes + +.. program:: volume list +.. code:: bash + + os volume list + [--status ] + [--name ] + [--all-projects] + [--long] + +.. option:: --status + + Filter results by status + +.. option:: --name + + Filter results by name + +.. option:: --all-projects + + Include all projects (admin only) + +.. option:: --long + + List additional fields in output + +volume set +---------- + +Set volume properties + +.. program:: volume set +.. code:: bash + + os volume set + [--name ] + [--description ] + [--property [...] ] + + +.. option:: --name + + New volume name + +.. option:: --description + + New volume description + +.. option:: --property + + Property to add or modify for this volume (repeat option to set multiple properties) + +.. describe:: + + Volume to modify (name or ID) + +volume show +----------- + +Show volume details + +.. program:: volume show +.. code:: bash + + os volume show + + +.. describe:: + + Volume to display (name or ID) + +volume unset +------------ + +Unset volume properties + +.. program:: volume unset +.. code:: bash + + os volume unset + [--property ] + + +.. option:: --property + + Property to remove from volume (repeat option to remove multiple properties) + +.. describe:: + + Volume to modify (name or ID) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 5bec1a8518..f5f6d01513 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -112,7 +112,7 @@ referring to both Compute and Volume quotas. * ``usage``: (**Compute**) display host resources being consumed * ``user``: (**Identity**) individual cloud resources users * ``user role``: (**Identity**) roles assigned to a user -* ``volume``: Volume - block volumes +* ``volume``: (**Volume**) block volumes * ``volume type``: (**Volume**) deployment-specific types of volumes available Actions diff --git a/openstackclient/volume/v1/volume.py b/openstackclient/volume/v1/volume.py index d82f6b4b76..e59331fad3 100644 --- a/openstackclient/volume/v1/volume.py +++ b/openstackclient/volume/v1/volume.py @@ -36,14 +36,14 @@ def get_parser(self, prog_name): parser.add_argument( 'name', metavar='', - help='Name of the new volume', + help='New volume name', ) parser.add_argument( '--size', metavar='', required=True, type=int, - help='New volume size', + help='New volume size in GB', ) parser.add_argument( '--snapshot-id', @@ -53,45 +53,45 @@ def get_parser(self, prog_name): parser.add_argument( '--description', metavar='', - help='Description of the volume', + help='New volume description', ) parser.add_argument( '--type', metavar='', - help='Type of volume', + help='Use as the new volume type', ) parser.add_argument( '--user', metavar='', - help='Specify a different user (admin only)', + help='Specify an alternate user (name or ID)', ) parser.add_argument( '--project', metavar='', - help='Specify a different project (admin only)', + help='Specify an alternate project (name or ID)', ) parser.add_argument( '--availability-zone', metavar='', help='Create new volume in ', ) - parser.add_argument( - '--property', - metavar='', - action=parseractions.KeyValueAction, - help='Property to store for this volume ' - '(repeat option to set multiple properties)', - ) parser.add_argument( '--image', metavar='', - help='Use as source of new volume', + help='Use as source of new volume (name or ID)', ) parser.add_argument( '--source', metavar='', help='Volume to clone (name or ID)', ) + parser.add_argument( + '--property', + metavar='', + action=parseractions.KeyValueAction, + help='Set a property on this volume ' + '(repeat option to set multiple properties)', + ) return parser @@ -172,7 +172,8 @@ def get_parser(self, prog_name): dest='force', action='store_true', default=False, - help='Attempt forced removal of volume(s), regardless of state', + help='Attempt forced removal of volume(s), regardless of state ' + '(defaults to False)', ) return parser @@ -216,7 +217,7 @@ def get_parser(self, prog_name): '--long', action='store_true', default=False, - help='Display properties', + help='List additional fields in output', ) return parser @@ -318,19 +319,19 @@ def get_parser(self, prog_name): ) parser.add_argument( '--name', - metavar='', + metavar='', help='New volume name', ) parser.add_argument( '--description', - metavar='', + metavar='', help='New volume description', ) parser.add_argument( '--property', metavar='', action=parseractions.KeyValueAction, - help='Property to add/change for this volume ' + help='Property to add or modify for this volume ' '(repeat option to set multiple properties)', ) return parser @@ -400,15 +401,15 @@ def get_parser(self, prog_name): parser.add_argument( 'volume', metavar='', - help='Volume to change (name or ID)', + help='Volume to modify (name or ID)', ) parser.add_argument( '--property', metavar='', action='append', default=[], - help='Property key to remove from volume ' - '(repeat to set multiple values)', + help='Property to remove from volume ' + '(repeat option to remove multiple properties)', ) return parser From 55b85403744840e8327a6184ab3464d602e93e3f Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 5 Jan 2015 20:02:53 -0500 Subject: [PATCH 0315/3494] Fixup backup list output Name and Description were not appearing at all, and we didn't have a --long alternative, which had a bunch of useful information. Closes-Bug: #1408585 Change-Id: I7ca42a8d23ad60f6b9cc862799cb08a3e491b6e8 --- openstackclient/volume/v1/backup.py | 54 ++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/openstackclient/volume/v1/backup.py b/openstackclient/volume/v1/backup.py index 8c16ba256f..eab388a59f 100644 --- a/openstackclient/volume/v1/backup.py +++ b/openstackclient/volume/v1/backup.py @@ -15,6 +15,7 @@ """Volume v1 Backup action implementations""" +import copy import logging import six @@ -102,20 +103,55 @@ class ListBackup(lister.Lister): log = logging.getLogger(__name__ + '.ListBackup') + def get_parser(self, prog_name): + parser = super(ListBackup, self).get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + default=False, + help='List additional fields in output', + ) + return parser + def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) - columns = ( - 'ID', - 'Display Name', - 'Display Description', - 'Status', - 'Size' - ) + + def _format_volume_id(volume_id): + """Return a volume name if available + + :param volume_id: a volume ID + :rtype: either the volume ID or name + """ + + volume = volume_id + if volume_id in volume_cache.keys(): + volume = volume_cache[volume_id].display_name + return volume + + if parsed_args.long: + columns = ['ID', 'Name', 'Description', 'Status', 'Size', + 'Availability Zone', 'Volume ID', 'Container'] + column_headers = copy.deepcopy(columns) + column_headers[6] = 'Volume' + else: + columns = ['ID', 'Name', 'Description', 'Status', 'Size'] + column_headers = columns + + # Cache the volume list + volume_cache = {} + try: + for s in self.app.client_manager.volume.volumes.list(): + volume_cache[s.id] = s + except Exception: + # Just forget it if there's any trouble + pass + data = self.app.client_manager.volume.backups.list() - return (columns, + + return (column_headers, (utils.get_item_properties( s, columns, - formatters={}, + formatters={'Volume ID': _format_volume_id}, ) for s in data)) From 79d0e21a4519de57a25ccd4a06a26795dba5636d Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 5 Jan 2015 20:26:35 -0500 Subject: [PATCH 0316/3494] Command doc: backup Change-Id: Iecd4dbddea637bd6540d94b37253a9ba434c9db3 --- doc/source/command-objects/backup.rst | 104 ++++++++++++++++++++++++++ doc/source/commands.rst | 2 +- openstackclient/volume/v1/backup.py | 22 +++--- 3 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 doc/source/command-objects/backup.rst diff --git a/doc/source/command-objects/backup.rst b/doc/source/command-objects/backup.rst new file mode 100644 index 0000000000..ec201aa3d5 --- /dev/null +++ b/doc/source/command-objects/backup.rst @@ -0,0 +1,104 @@ +====== +backup +====== + +Volume v1 + +backup create +------------- + +Create new backup + +.. program:: backup create +.. code:: bash + + os backup create + [--container ] + [--name ] + [--description ] + + +.. option:: --container + + Optional backup container name + +.. option:: --name + + Name of the backup + +.. option:: --description + + Description of the backup + +.. _backup_create-backup: +.. describe:: + + Volume to backup (name or ID) + +backup delete +------------- + +Delete backup(s) + +.. program:: backup delete +.. code:: bash + + os backup delete + [ ...] + +.. _backup_delete-backup: +.. describe:: + + Backup(s) to delete (ID only) + +backup list +----------- + +List backups + +.. program:: backup list +.. code:: bash + + os backup list + +.. _backup_list-backup: +.. option:: --long + + List additional fields in output + +backup restore +-------------- + +Restore backup + +.. program:: backup restore +.. code:: bash + + os backup restore + + + +.. _backup_restore-backup: +.. describe:: + + Backup to restore (ID only) + +.. describe:: + + Volume to restore to (name or ID) + +backup show +----------- + +Display backup details + +.. program:: backup show +.. code:: bash + + os backup show + + +.. _backup_show-backup: +.. describe:: + + Backup to display (ID only) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 4b2e6355e4..4649b012c2 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -72,7 +72,7 @@ referring to both Compute and Volume quotas. * ``access token``: Identity - long-lived OAuth-based token * ``availability zone``: (**Compute**) a logical partition of hosts or volume services * ``aggregate``: (**Compute**) a grouping of servers -* ``backup``: Volume - a volume copy +* ``backup``: (**Volume**) a volume copy * ``catalog``: (**Identity**) service catalog * ``console log``: (**Compute**) server console text dump * ``console url``: (**Compute**) server remote console URL diff --git a/openstackclient/volume/v1/backup.py b/openstackclient/volume/v1/backup.py index eab388a59f..71c8ed3832 100644 --- a/openstackclient/volume/v1/backup.py +++ b/openstackclient/volume/v1/backup.py @@ -27,7 +27,7 @@ class CreateBackup(show.ShowOne): - """Create backup command""" + """Create new backup""" log = logging.getLogger(__name__ + '.CreateBackup') @@ -36,13 +36,13 @@ def get_parser(self, prog_name): parser.add_argument( 'volume', metavar='', - help='The name or ID of the volume to backup', + help='Volume to backup (name or ID)', ) parser.add_argument( '--container', metavar='', required=False, - help='Optional backup container name.', + help='Optional backup container name', ) parser.add_argument( '--name', @@ -84,7 +84,7 @@ def get_parser(self, prog_name): 'backups', metavar='', nargs="+", - help='Backup(s) to delete (name or ID)', + help='Backup(s) to delete (ID only)', ) return parser @@ -99,7 +99,7 @@ def take_action(self, parsed_args): class ListBackup(lister.Lister): - """List backup command""" + """List backups""" log = logging.getLogger(__name__ + '.ListBackup') @@ -156,7 +156,7 @@ def _format_volume_id(volume_id): class RestoreBackup(command.Command): - """Restore backup command""" + """Restore backup""" log = logging.getLogger(__name__ + '.RestoreBackup') @@ -165,11 +165,11 @@ def get_parser(self, prog_name): parser.add_argument( 'backup', metavar='', - help='ID of backup to restore') + help='Backup to restore (ID only)') parser.add_argument( 'volume', - metavar='', - help='ID of volume to restore to') + metavar='', + help='Volume to restore to (name or ID)') return parser def take_action(self, parsed_args): @@ -184,7 +184,7 @@ def take_action(self, parsed_args): class ShowBackup(show.ShowOne): - """Show backup command""" + """Display backup details""" log = logging.getLogger(__name__ + '.ShowBackup') @@ -193,7 +193,7 @@ def get_parser(self, prog_name): parser.add_argument( 'backup', metavar='', - help='Name or ID of backup to display') + help='Backup to display (ID only)') return parser def take_action(self, parsed_args): From e8be3b64c1956b58fa0a5b6d460c8bf07085951c Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 6 Jan 2015 01:43:49 -0500 Subject: [PATCH 0317/3494] Command doc: mapping Also tweaked the code for `mapping set` as it was previously using cliff Show instead of cliff Command. Change-Id: I0ea1383a9f2dddf4b2f717b2aa16bbd60ab1720c --- doc/source/command-objects/mapping.rst | 91 ++++++++++++++++++++++++++ doc/source/commands.rst | 1 + openstackclient/identity/v3/mapping.py | 39 ++++++----- 3 files changed, 113 insertions(+), 18 deletions(-) create mode 100644 doc/source/command-objects/mapping.rst diff --git a/doc/source/command-objects/mapping.rst b/doc/source/command-objects/mapping.rst new file mode 100644 index 0000000000..5c7535bd1c --- /dev/null +++ b/doc/source/command-objects/mapping.rst @@ -0,0 +1,91 @@ +======= +mapping +======= + +Identity v3 + +`Requires: OS-FEDERATION extension` + +mapping create +-------------- + +Create new mapping + +.. program:: mapping create +.. code:: bash + + os mapping create + --rules + + +.. option:: --rules + + Filename that contains a set of mapping rules (required) + +.. _mapping_create-mapping: +.. describe:: + + New mapping name (must be unique) + +mapping delete +-------------- + +Delete a mapping + +.. program:: mapping delete +.. code:: bash + + os mapping delete + + +.. _mapping_delete-mapping: +.. describe:: + + Mapping to delete + +mapping list +------------ + +List mappings + +.. program:: mapping list +.. code:: bash + + os mapping list + +mapping set +----------- + +Set mapping properties + +.. program:: mapping set +.. code:: bash + + os mapping set + [--rules ] + + +.. option:: --rules + + Filename that contains a new set of mapping rules + +.. _mapping_set-mapping: +.. describe:: + + Mapping to modify + +mapping show +------------ + +Display mapping details + +.. program:: mapping show +.. code:: bash + + os mapping show + + +.. _mapping_show-mapping: +.. describe:: + + Mapping to display diff --git a/doc/source/commands.rst b/doc/source/commands.rst index bb3db18755..8976fc029a 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -93,6 +93,7 @@ referring to both Compute and Volume quotas. * ``ip floating``: Compute, Network - a public IP address that can be mapped to a server * ``keypair``: (**Compute**) an SSH public key * ``limits``: (**Compute**, **Volume**) resource usage limits +* ``mapping``: (**Identity**) a definition to translate identity provider attributes to Identity concepts * ``module``: internal - installed Python modules in the OSC process * ``network``: Network - a virtual network for connecting servers and other resources * ``object``: (**Object Store**) a single file in the Object Store diff --git a/openstackclient/identity/v3/mapping.py b/openstackclient/identity/v3/mapping.py index c530a40439..a1f60438f6 100644 --- a/openstackclient/identity/v3/mapping.py +++ b/openstackclient/identity/v3/mapping.py @@ -80,7 +80,7 @@ def _read_rules(self, path): class CreateMapping(show.ShowOne, _RulesReader): - """Create new federation mapping""" + """Create new mapping""" log = logging.getLogger(__name__ + '.CreateMapping') @@ -89,12 +89,12 @@ def get_parser(self, prog_name): parser.add_argument( 'mapping', metavar='', - help='New mapping (must be unique)', + help='New mapping name (must be unique)', ) parser.add_argument( '--rules', - metavar='', required=True, - help='Filename with rules', + metavar='', required=True, + help='Filename that contains a set of mapping rules (required)', ) return parser @@ -112,7 +112,7 @@ def take_action(self, parsed_args): class DeleteMapping(command.Command): - """Delete federation mapping""" + """Delete a mapping""" log = logging.getLogger(__name__ + '.DeleteMapping') @@ -120,7 +120,7 @@ def get_parser(self, prog_name): parser = super(DeleteMapping, self).get_parser(prog_name) parser.add_argument( 'mapping', - metavar='', + metavar='', help='Mapping to delete', ) return parser @@ -134,7 +134,7 @@ def take_action(self, parsed_args): class ListMapping(lister.Lister): - """List federation mappings""" + """List mappings""" log = logging.getLogger(__name__ + '.ListMapping') def take_action(self, parsed_args): @@ -149,8 +149,8 @@ def take_action(self, parsed_args): return (columns, items) -class SetMapping(show.ShowOne, _RulesReader): - """Update federation mapping""" +class SetMapping(command.Command, _RulesReader): + """Set mapping properties""" log = logging.getLogger(__name__ + '.SetMapping') @@ -159,12 +159,12 @@ def get_parser(self, prog_name): parser.add_argument( 'mapping', metavar='', - help='Mapping to update.', + help='Mapping to modify', ) parser.add_argument( '--rules', - metavar='', required=True, - help='Filename with rules', + metavar='', + help='Filename that contains a new set of mapping rules', ) return parser @@ -172,19 +172,22 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity + if not parsed_args.rules: + self.app.log.error("No changes requested") + return + rules = self._read_rules(parsed_args.rules) mapping = identity_client.federation.mappings.update( mapping=parsed_args.mapping, rules=rules) - info = {} - info.update(mapping._info) - return zip(*sorted(six.iteritems(info))) + mapping._info.pop('links', None) + return zip(*sorted(six.iteritems(mapping._info))) class ShowMapping(show.ShowOne): - """Show federation mapping details""" + """Display mapping details""" log = logging.getLogger(__name__ + '.ShowMapping') @@ -192,8 +195,8 @@ def get_parser(self, prog_name): parser = super(ShowMapping, self).get_parser(prog_name) parser.add_argument( 'mapping', - metavar='', - help='Mapping to show', + metavar='', + help='Mapping to display', ) return parser From c9cf126a83459e1b843153a91dfe86d975d0d8bb Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 6 Jan 2015 01:59:59 -0500 Subject: [PATCH 0318/3494] Command doc: identity provider Change-Id: Ie73accfaa3d45205a2521e6e61efd16142c460b2 --- .../command-objects/identity-provider.rst | 100 ++++++++++++++++++ doc/source/commands.rst | 2 +- .../identity/v3/identity_provider.py | 31 +++--- 3 files changed, 114 insertions(+), 19 deletions(-) create mode 100644 doc/source/command-objects/identity-provider.rst diff --git a/doc/source/command-objects/identity-provider.rst b/doc/source/command-objects/identity-provider.rst new file mode 100644 index 0000000000..f08cde0188 --- /dev/null +++ b/doc/source/command-objects/identity-provider.rst @@ -0,0 +1,100 @@ +================= +identity provider +================= + +Identity v3 + +`Requires: OS-FEDERATION extension` + +identity provider create +------------------------ + +Create new identity provider + +.. program:: identity provider create +.. code:: bash + + os identity provider create + [--description ] + [--enable | --disable] + + +.. option:: --description + + New identity provider description + +.. option:: --enable + + Enable the identity provider (default) + +.. option:: --disable + + Disable the identity provider + +.. describe:: + + New identity provider name (must be unique) + +identity provider delete +------------------------ + +Delete an identity provider + +.. program:: identity provider delete +.. code:: bash + + os identity provider delete + + +.. describe:: + + Identity provider to delete + +identity provider list +---------------------- + +List identity providers + +.. program:: identity provider list +.. code:: bash + + os identity provider list + +identity provider set +--------------------- + +Set identity provider properties + +.. program:: identity provider set +.. code:: bash + + os identity provider set + [--enable | --disable] + + +.. option:: --enable + + Enable the identity provider + +.. option:: --disable + + Disable the identity provider + +.. describe:: + + Identity provider to modify + +identity provider show +---------------------- + +Display identity provider details + +.. program:: identity provider show +.. code:: bash + + os identity provider show + + +.. describe:: + + Identity provider to display diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 8976fc029a..cbe46ef7e7 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -87,7 +87,7 @@ referring to both Compute and Volume quotas. * ``group``: (**Identity**) a grouping of users * ``host``: Compute - the physical computer running a hypervisor * ``hypervisor``: Compute - the virtual machine manager -* ``identity provider``: Identity - a source of users and authentication +* ``identity provider``: (**Identity**) a source of users and authentication * ``image``: Image - a disk image * ``ip fixed``: Compute, Network - an internal IP address assigned to a server * ``ip floating``: Compute, Network - a public IP address that can be mapped to a server diff --git a/openstackclient/identity/v3/identity_provider.py b/openstackclient/identity/v3/identity_provider.py index 8a1b22d0cf..f46341a16b 100644 --- a/openstackclient/identity/v3/identity_provider.py +++ b/openstackclient/identity/v3/identity_provider.py @@ -15,7 +15,6 @@ import logging import six -import sys from cliff import command from cliff import lister @@ -33,22 +32,21 @@ def get_parser(self, prog_name): parser = super(CreateIdentityProvider, self).get_parser(prog_name) parser.add_argument( 'identity_provider_id', - metavar='', - help='New identity provider ID (must be unique)' + metavar='', + help='New identity provider name (must be unique)' ) parser.add_argument( '--description', metavar='', help='New identity provider description', ) - enable_identity_provider = parser.add_mutually_exclusive_group() enable_identity_provider.add_argument( '--enable', dest='enabled', action='store_true', default=True, - help='Enable identity provider', + help='Enable identity provider (default)', ) enable_identity_provider.add_argument( '--disable', @@ -79,8 +77,8 @@ def get_parser(self, prog_name): parser = super(DeleteIdentityProvider, self).get_parser(prog_name) parser.add_argument( 'identity_provider', - metavar='', - help='Identity provider ID to delete', + metavar='', + help='Identity provider to delete', ) return parser @@ -118,10 +116,9 @@ def get_parser(self, prog_name): parser = super(SetIdentityProvider, self).get_parser(prog_name) parser.add_argument( 'identity_provider', - metavar='', - help='Identity provider ID to change', + metavar='', + help='Identity provider to modify', ) - enable_identity_provider = parser.add_mutually_exclusive_group() enable_identity_provider.add_argument( '--enable', @@ -144,19 +141,17 @@ def take_action(self, parsed_args): elif parsed_args.disable is True: enabled = False else: - sys.stdout.write("Identity Provider not updated, " - "no arguments present") + self.log.error("No changes requested") return (None, None) identity_provider = federation_client.identity_providers.update( parsed_args.identity_provider, enabled=enabled) - info = {} - info.update(identity_provider._info) - return zip(*sorted(six.iteritems(info))) + identity_provider._info.pop('links', None) + return zip(*sorted(six.iteritems(identity_provider._info))) class ShowIdentityProvider(show.ShowOne): - """Show identity provider details""" + """Display identity provider details""" log = logging.getLogger(__name__ + '.ShowIdentityProvider') @@ -164,8 +159,8 @@ def get_parser(self, prog_name): parser = super(ShowIdentityProvider, self).get_parser(prog_name) parser.add_argument( 'identity_provider', - metavar='', - help='Identity provider ID to show', + metavar='', + help='Identity provider to display', ) return parser From a0c63dedf41bfa9549d6b95902b91f7bfb2b5f55 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 6 Jan 2015 02:29:56 -0500 Subject: [PATCH 0319/3494] Command doc: federation protocol Change-Id: I1289eb0caf31fca21c5c377cf13aebd1434a00ee --- .../command-objects/federation-protocol.rst | 112 ++++++++++++++++++ doc/source/commands.rst | 1 + .../identity/v3/federation_protocol.py | 66 ++++++----- 3 files changed, 152 insertions(+), 27 deletions(-) create mode 100644 doc/source/command-objects/federation-protocol.rst diff --git a/doc/source/command-objects/federation-protocol.rst b/doc/source/command-objects/federation-protocol.rst new file mode 100644 index 0000000000..0ed0980a54 --- /dev/null +++ b/doc/source/command-objects/federation-protocol.rst @@ -0,0 +1,112 @@ +=================== +federation protocol +=================== + +Identity v3 + +`Requires: OS-FEDERATION extension` + +federation protocol create +-------------------------- + +Create new federation protocol + +.. program:: federation protocol create +.. code:: bash + + os federation protocol create + --identity-provider + --mapping + + +.. option:: --identity-provider + + Identity provider that will support the new federation protocol (name or ID) (required) + +.. option:: --mapping + + Mapping that is to be used (name or ID) (required) + +.. describe:: + + New federation protocol name (must be unique per identity provider) + +federation protocol delete +-------------------------- + +Delete a federation protocol + +.. program:: federation protocol delete +.. code:: bash + + os federation protocol delete + --identity-provider + + +.. option:: --identity-provider + + Identity provider that supports (name or ID) (required) + +.. describe:: + + Federation protocol to delete (name or ID) + +federation protocol list +------------------------ + +List federation protocols + +.. program:: federation protocol list +.. code:: bash + + os federation protocol list + --identity-provider + +.. option:: --identity-provider + + Identity provider to list (name or ID) (required) + +federation protocol set +----------------------- + +Set federation protocol properties + +.. program:: federation protocol set +.. code:: bash + + os federation protocol set + --identity-provider + [--mapping ] + + +.. option:: --identity-provider + + Identity provider that supports (name or ID) (required) + +.. option:: --mapping + + Mapping that is to be used (name or ID) + +.. describe:: + + Federation protocol to modify (name or ID) + +federation protocol show +------------------------ + +Display federation protocol details + +.. program:: federation protocol show +.. code:: bash + + os federation protocol show + --identity-provider + + +.. option:: --identity-provider + + Identity provider that supports (name or ID) (required) + +.. describe:: + + Federation protocol to display (name or ID) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index cbe46ef7e7..a09dcb9ec8 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -83,6 +83,7 @@ referring to both Compute and Volume quotas. * ``ec2 cedentials``: (**Identity**) AWS EC2-compatible credentials * ``endpoint``: (**Identity**) the base URL used to contact a specific service * ``extension``: (**Compute**, **Identity**, **Volume**) OpenStack server API extensions +* ``federation protocol``: (**Identity**) the underlying protocol used while federating identities * ``flavor``: (**Compute**) predefined server configurations: ram, root disk, etc * ``group``: (**Identity**) a grouping of users * ``host``: Compute - the physical computer running a hypervisor diff --git a/openstackclient/identity/v3/federation_protocol.py b/openstackclient/identity/v3/federation_protocol.py index 693ec94eae..5a651165b1 100644 --- a/openstackclient/identity/v3/federation_protocol.py +++ b/openstackclient/identity/v3/federation_protocol.py @@ -25,7 +25,7 @@ class CreateProtocol(show.ShowOne): - """Create new Federation Protocol tied to an Identity Provider""" + """Create new federation protocol""" log = logging.getLogger(__name__ + 'CreateProtocol') @@ -34,16 +34,19 @@ def get_parser(self, prog_name): parser.add_argument( 'federation_protocol', metavar='', - help='Protocol (must be unique per Identity Provider') + help='New federation protocol name (must be unique per identity ' + ' provider)') parser.add_argument( '--identity-provider', metavar='', - help=('Identity Provider you want to add the Protocol to ' - '(must already exist)'), required=True) + required=True, + help='Identity provider that will support the new federation ' + ' protocol (name or ID) (required)') parser.add_argument( '--mapping', - metavar='', required=True, - help='Mapping you want to be used (must already exist)') + metavar='', + required=True, + help='Mapping that is to be used (name or ID) (required)') return parser @@ -66,7 +69,7 @@ def take_action(self, parsed_args): class DeleteProtocol(command.Command): - """Delete Federation Protocol tied to a Identity Provider""" + """Delete a federation protocol""" log = logging.getLogger(__name__ + '.DeleteProtocol') @@ -74,12 +77,14 @@ def get_parser(self, prog_name): parser = super(DeleteProtocol, self).get_parser(prog_name) parser.add_argument( 'federation_protocol', - metavar='', - help='Protocol (must be unique per Identity Provider') + metavar='', + help='Federation protocol to delete (name or ID)') parser.add_argument( '--identity-provider', - metavar='', required=True, - help='Identity Provider the Protocol is tied to') + metavar='', + required=True, + help='Identity provider that supports ' + '(name or ID) (required)') return parser @@ -92,7 +97,7 @@ def take_action(self, parsed_args): class ListProtocols(lister.Lister): - """List Protocols tied to an Identity Provider""" + """List federation protocols""" log = logging.getLogger(__name__ + '.ListProtocols') @@ -100,8 +105,9 @@ def get_parser(self, prog_name): parser = super(ListProtocols, self).get_parser(prog_name) parser.add_argument( '--identity-provider', - metavar='', required=True, - help='Identity Provider the Protocol is tied to') + metavar='', + required=True, + help='Identity provider to list (name or ID) (required)') return parser @@ -118,7 +124,7 @@ def take_action(self, parsed_args): class SetProtocol(command.Command): - """Set Protocol tied to an Identity Provider""" + """Set federation protocol properties""" log = logging.getLogger(__name__ + '.SetProtocol') @@ -127,21 +133,26 @@ def get_parser(self, prog_name): parser.add_argument( 'federation_protocol', metavar='', - help='Protocol (must be unique per Identity Provider') + help='Federation protocol to modify (name or ID)') parser.add_argument( '--identity-provider', - metavar='', required=True, - help=('Identity Provider you want to add the Protocol to ' - '(must already exist)')) + metavar='', + required=True, + help='Identity provider that supports ' + '(name or ID) (required)') parser.add_argument( '--mapping', - metavar='', required=True, - help='Mapping you want to be used (must already exist)') + metavar='', + help='Mapping that is to be used (name or ID)') return parser def take_action(self, parsed_args): identity_client = self.app.client_manager.identity + if not parsed_args.mapping: + self.app.log.error("No changes requested") + return + protocol = identity_client.federation.protocols.update( parsed_args.identity_provider, parsed_args.federation_protocol, parsed_args.mapping) @@ -156,7 +167,7 @@ def take_action(self, parsed_args): class ShowProtocol(show.ShowOne): - """Show Protocol tied to an Identity Provider""" + """Display federation protocol details""" log = logging.getLogger(__name__ + '.ShowProtocol') @@ -164,13 +175,14 @@ def get_parser(self, prog_name): parser = super(ShowProtocol, self).get_parser(prog_name) parser.add_argument( 'federation_protocol', - metavar='', - help='Protocol (must be unique per Identity Provider') + metavar='', + help='Federation protocol to display (name or ID)') parser.add_argument( '--identity-provider', - metavar='', required=True, - help=('Identity Provider you want to add the Protocol to ' - '(must already exist)')) + metavar='', + required=True, + help=('Identity provider that supports ' + '(name or ID) (required)')) return parser def take_action(self, parsed_args): From 0ff28d5251f9e25eafdc628e29b093b7c694ea48 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Tue, 16 Dec 2014 15:16:41 -0500 Subject: [PATCH 0320/3494] Allow user list to filter by project Adds a --project filter to `os user list`, which really calls the role assignment manager behind the scenes. Change-Id: I57a75018f12ed3acdf8f6611b6b58bd974f91da2 Closes-Bug: #1397251 --- doc/source/command-objects/user.rst | 18 +++---- openstackclient/identity/v3/user.py | 53 ++++++++++++++++--- .../tests/identity/v3/test_user.py | 47 ++++++++++++++++ 3 files changed, 101 insertions(+), 17 deletions(-) diff --git a/doc/source/command-objects/user.rst b/doc/source/command-objects/user.rst index e54c65673c..a9a98fe18f 100644 --- a/doc/source/command-objects/user.rst +++ b/doc/source/command-objects/user.rst @@ -101,28 +101,26 @@ List users .. code:: bash os user list - [--domain ] [--project ] - [--group ] + [--domain ] + [--group | --project ] [--long] -.. option:: --domain - - Filter users by `` (name or ID) - - .. versionadded:: 3 - .. option:: --project Filter users by `` (name or ID) - *Removed in version 3.* +.. option:: --domain + + Filter users by `` (name or ID) + + *Identity version 3 only* .. option:: --group Filter users by `` membership (name or ID) - .. versionadded:: 3 + *Identity version 3 only* .. option:: --long diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index a60c8c83fa..4fb7b6d1e0 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -188,11 +188,17 @@ def get_parser(self, prog_name): metavar='', help='Filter users by (name or ID)', ) - parser.add_argument( + project_or_group = parser.add_mutually_exclusive_group() + project_or_group.add_argument( '--group', metavar='', help='Filter users by membership (name or ID)', ) + project_or_group.add_argument( + '--project', + metavar='', + help='Filter users by (name or ID)', + ) parser.add_argument( '--long', action='store_true', @@ -219,7 +225,44 @@ def take_action(self, parsed_args): else: group = None - # List users + if parsed_args.project: + if domain is not None: + project = utils.find_resource( + identity_client.projects, + parsed_args.project, + domain_id=domain + ).id + else: + project = utils.find_resource( + identity_client.projects, + parsed_args.project, + ).id + + assignments = identity_client.role_assignments.list( + project=project) + + # NOTE(stevemar): If a user has more than one role on a project + # then they will have two entries in the returned data. Since we + # are looking for any role, let's just track unique user IDs. + user_ids = set() + for assignment in assignments: + if hasattr(assignment, 'user'): + user_ids.add(assignment.user['id']) + + # NOTE(stevemar): Call find_resource once we have unique IDs, so + # it's fewer trips to the Identity API, then collect the data. + data = [] + for user_id in user_ids: + user = utils.find_resource(identity_client.users, user_id) + data.append(user) + + else: + data = identity_client.users.list( + domain=domain, + group=group, + ) + + # Column handling if parsed_args.long: columns = ['ID', 'Name', 'Default Project Id', 'Domain Id', 'Description', 'Email', 'Enabled'] @@ -228,11 +271,7 @@ def take_action(self, parsed_args): column_headers[3] = 'Domain' else: columns = ['ID', 'Name'] - column_headers = copy.deepcopy(columns) - data = identity_client.users.list( - domain=domain, - group=group, - ) + column_headers = columns return ( column_headers, diff --git a/openstackclient/tests/identity/v3/test_user.py b/openstackclient/tests/identity/v3/test_user.py index dd517e19f2..35dd98ee9e 100644 --- a/openstackclient/tests/identity/v3/test_user.py +++ b/openstackclient/tests/identity/v3/test_user.py @@ -44,6 +44,11 @@ def setUp(self): self.users_mock = self.app.client_manager.identity.users self.users_mock.reset_mock() + # Shortcut for RoleAssignmentManager Mock + self.role_assignments_mock = self.app.client_manager.identity.\ + role_assignments + self.role_assignments_mock.reset_mock() + class TestUserCreate(TestUser): @@ -511,6 +516,21 @@ def setUp(self): loaded=True, ) + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + + self.role_assignments_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy( + identity_fakes.ASSIGNMENT_WITH_PROJECT_ID_AND_USER_ID), + loaded=True, + ) + ] + # Get the command object to test self.cmd = user.ListUser(self.app, None) @@ -643,6 +663,33 @@ def test_user_list_long(self): ), ) self.assertEqual(datalist, tuple(data)) + def test_user_list_project(self): + arglist = [ + '--project', identity_fakes.project_name, + ] + verifylist = [ + ('project', identity_fakes.project_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + kwargs = { + 'project': identity_fakes.project_id, + } + + self.role_assignments_mock.list.assert_called_with(**kwargs) + self.users_mock.get.assert_called_with(identity_fakes.user_id) + + collist = ['ID', 'Name'] + self.assertEqual(columns, collist) + datalist = (( + identity_fakes.user_id, + identity_fakes.user_name, + ), ) + self.assertEqual(datalist, tuple(data)) + class TestUserSet(TestUser): From c885c72cba459ca853de10ec1685d70d9c2b7ca2 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 8 Jan 2015 01:29:35 -0500 Subject: [PATCH 0321/3494] Command doc: consumer Change-Id: Ie687e1d7f80810106a64204828299f9d143b8d7c --- doc/source/command-objects/consumer.rst | 83 +++++++++++++++++++++++++ doc/source/commands.rst | 2 +- openstackclient/identity/v3/consumer.py | 22 +++---- 3 files changed, 95 insertions(+), 12 deletions(-) create mode 100644 doc/source/command-objects/consumer.rst diff --git a/doc/source/command-objects/consumer.rst b/doc/source/command-objects/consumer.rst new file mode 100644 index 0000000000..59ace845e0 --- /dev/null +++ b/doc/source/command-objects/consumer.rst @@ -0,0 +1,83 @@ +======== +consumer +======== + +Identity v3 + +`Requires: OS-OAUTH1 extension` + +consumer create +--------------- + +Create new consumer + +.. program:: consumer create +.. code:: bash + + os consumer create + [--description ] + +.. option:: --description + + New consumer description + +consumer delete +--------------- + +Delete a consumer + +.. program:: consumer delete +.. code:: bash + + os consumer delete + + +.. describe:: + + Consumer to delete + +consumer list +------------- + +List consumers + +.. program:: consumer list +.. code:: bash + + os consumer list + +consumer set +------------ + +Set consumer properties + +.. program:: consumer set +.. code:: bash + + os consumer set + [--description ] + + +.. option:: --description + + New consumer description + +.. describe:: + + Consumer to modify + +consumer show +------------- + +Display consumer details + +.. program:: consumer show +.. code:: bash + + os consumer show + + +.. _consumer_show-consumer: +.. describe:: + + Consumer to display diff --git a/doc/source/commands.rst b/doc/source/commands.rst index bb3db18755..9d60984354 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -76,7 +76,7 @@ referring to both Compute and Volume quotas. * ``catalog``: (**Identity**) service catalog * ``console log``: (**Compute**) server console text dump * ``console url``: (**Compute**) server remote console URL -* ``consumer``: Identity - OAuth-based delegatee +* ``consumer``: (**Identity**) OAuth-based delegatee * ``container``: (**Object Store**) a grouping of objects * ``credentials``: (**Identity**) specific to identity providers * ``domain``: (**Identity**) a grouping of projects diff --git a/openstackclient/identity/v3/consumer.py b/openstackclient/identity/v3/consumer.py index b7e57d8dd0..c5e263926c 100644 --- a/openstackclient/identity/v3/consumer.py +++ b/openstackclient/identity/v3/consumer.py @@ -27,7 +27,7 @@ class CreateConsumer(show.ShowOne): - """Create consumer command""" + """Create new consumer""" log = logging.getLogger(__name__ + '.CreateConsumer') @@ -35,7 +35,7 @@ def get_parser(self, prog_name): parser = super(CreateConsumer, self).get_parser(prog_name) parser.add_argument( '--description', - metavar='', + metavar='', help='New consumer description', ) return parser @@ -51,7 +51,7 @@ def take_action(self, parsed_args): class DeleteConsumer(command.Command): - """Delete consumer command""" + """Delete a consumer""" log = logging.getLogger(__name__ + '.DeleteConsumer') @@ -60,7 +60,7 @@ def get_parser(self, prog_name): parser.add_argument( 'consumer', metavar='', - help='ID of consumer to delete', + help='Consumer to delete', ) return parser @@ -74,7 +74,7 @@ def take_action(self, parsed_args): class ListConsumer(lister.Lister): - """List consumer command""" + """List consumers""" log = logging.getLogger(__name__ + '.ListConsumer') @@ -90,7 +90,7 @@ def take_action(self, parsed_args): class SetConsumer(command.Command): - """Set consumer command""" + """Set consumer properties""" log = logging.getLogger(__name__ + '.SetConsumer') @@ -99,11 +99,11 @@ def get_parser(self, prog_name): parser.add_argument( 'consumer', metavar='', - help='ID of consumer to change', + help='Consumer to modify', ) parser.add_argument( '--description', - metavar='', + metavar='', help='New consumer description', ) return parser @@ -118,7 +118,7 @@ def take_action(self, parsed_args): kwargs['description'] = parsed_args.description if not len(kwargs): - sys.stdout.write("Consumer not updated, no arguments present") + sys.stdout.write('Consumer not updated, no arguments present') return consumer = identity_client.oauth1.consumers.update( @@ -127,7 +127,7 @@ def take_action(self, parsed_args): class ShowConsumer(show.ShowOne): - """Show consumer command""" + """Display consumer details""" log = logging.getLogger(__name__ + '.ShowConsumer') @@ -136,7 +136,7 @@ def get_parser(self, prog_name): parser.add_argument( 'consumer', metavar='', - help='ID of consumer to display', + help='Consumer to display', ) return parser From 6025fa83f193fe422cff1fdc93f658ec457e0136 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 8 Jan 2015 02:02:55 -0500 Subject: [PATCH 0322/3494] Request token creation docs + tweaks Added command docs, and changed request token to take in name or id of a project, and also support a domain option. Change-Id: I87363274e5b7a0c687e234f5a4bcaaf166d28840 --- doc/source/command-objects/request-token.rst | 37 ++++++++++++++++ doc/source/commands.rst | 2 +- openstackclient/identity/v3/token.py | 43 ++++++++++++++----- .../tests/identity/v3/test_oauth.py | 12 +++++- 4 files changed, 80 insertions(+), 14 deletions(-) create mode 100644 doc/source/command-objects/request-token.rst diff --git a/doc/source/command-objects/request-token.rst b/doc/source/command-objects/request-token.rst new file mode 100644 index 0000000000..501f67a57f --- /dev/null +++ b/doc/source/command-objects/request-token.rst @@ -0,0 +1,37 @@ +============= +request token +============= + +Identity v3 + +`Requires: OS-OAUTH1 extension` + +request token create +-------------------- + +Create a request token + +.. program:: request token create +.. code:: bash + + os request token create + --consumer-key + --consumer-secret + --project + [--domain ] + +.. option:: --consumer-key + + Consumer key (required) + +.. option:: --description + + Consumer secret (required) + +.. option:: --project + + Project that consumer wants to access (name or ID) (required) + +.. option:: --domain + + Domain owning (name or ID) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 9d60984354..1136f0a2bd 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -100,7 +100,7 @@ referring to both Compute and Volume quotas. * ``project``: (**Identity**) owns a group of resources * ``quota``: (**Compute**, **Volume**) resource usage restrictions * ``region``: (**Identity**) a subset of an OpenStack deployment -* ``request token``: Identity - temporary OAuth-based token +* ``request token``: (**Identity**) temporary OAuth-based token * ``role``: (**Identity**) a policy object used to determine authorization * ``role assignment``: (**Identity**) a relationship between roles, users or groups, and domains or projects * ``security group``: Compute, Network - groups of network access rules diff --git a/openstackclient/identity/v3/token.py b/openstackclient/identity/v3/token.py index 5b09b69f61..86f31a2a4d 100644 --- a/openstackclient/identity/v3/token.py +++ b/openstackclient/identity/v3/token.py @@ -20,6 +20,9 @@ from cliff import show +from openstackclient.common import utils +from openstackclient.identity import common + class AuthorizeRequestToken(show.ShowOne): """Authorize request token""" @@ -53,6 +56,7 @@ def take_action(self, parsed_args): verifier_pin = identity_client.oauth1.request_tokens.authorize( parsed_args.request_key, roles) + info = {} info.update(verifier_pin._info) return zip(*sorted(six.iteritems(info))) @@ -110,7 +114,7 @@ def take_action(self, parsed_args): class CreateRequestToken(show.ShowOne): - """Create request token""" + """Create a request token""" log = logging.getLogger(__name__ + '.CreateRequestToken') @@ -119,33 +123,50 @@ def get_parser(self, prog_name): parser.add_argument( '--consumer-key', metavar='', - help='Consumer key', + help='Consumer key (required)', required=True ) parser.add_argument( '--consumer-secret', metavar='', - help='Consumer secret', + help='Consumer secret (required)', required=True ) parser.add_argument( - '--project-id', - metavar='', - help='Requested project ID', + '--project', + metavar='', + help='Project that consumer wants to access (name or ID)' + ' (required)', required=True ) + parser.add_argument( + '--domain', + metavar='', + help='Domain owning (name or ID)', + ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) - token_client = self.app.client_manager.identity.oauth1.request_tokens + + identity_client = self.app.client_manager.identity + + if parsed_args.domain: + domain = common.find_domain(identity_client, parsed_args.domain) + project = utils.find_resource(identity_client.projects, + parsed_args.project, + domain_id=domain.id) + else: + project = utils.find_resource(identity_client.projects, + parsed_args.project) + + token_client = identity_client.oauth1.request_tokens + request_token = token_client.create( parsed_args.consumer_key, parsed_args.consumer_secret, - parsed_args.project_id) - info = {} - info.update(request_token._info) - return zip(*sorted(six.iteritems(info))) + project.id) + return zip(*sorted(six.iteritems(request_token._info))) class IssueToken(show.ShowOne): diff --git a/openstackclient/tests/identity/v3/test_oauth.py b/openstackclient/tests/identity/v3/test_oauth.py index 15ba04e33f..36a65e4cf9 100644 --- a/openstackclient/tests/identity/v3/test_oauth.py +++ b/openstackclient/tests/identity/v3/test_oauth.py @@ -26,6 +26,8 @@ def setUp(self): self.access_tokens_mock.reset_mock() self.request_tokens_mock = identity_client.oauth1.request_tokens self.request_tokens_mock.reset_mock() + self.projects_mock = identity_client.projects + self.projects_mock.reset_mock() class TestRequestTokenCreate(TestOAuth1): @@ -39,18 +41,24 @@ def setUp(self): loaded=True, ) + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + self.cmd = token.CreateRequestToken(self.app, None) def test_create_request_tokens(self): arglist = [ '--consumer-key', identity_fakes.consumer_id, '--consumer-secret', identity_fakes.consumer_secret, - '--project-id', identity_fakes.project_id, + '--project', identity_fakes.project_id, ] verifylist = [ ('consumer_key', identity_fakes.consumer_id), ('consumer_secret', identity_fakes.consumer_secret), - ('project_id', identity_fakes.project_id), + ('project', identity_fakes.project_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) From 0d7a50d3848484d6562dbd6af87de7836365821a Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 8 Jan 2015 02:54:26 -0500 Subject: [PATCH 0323/3494] Command doc: image Change-Id: Ib1563b58351315dc2a44ad77882f8c834a1214c0 --- doc/source/command-objects/image.rst | 245 +++++++++++++++++++++++++++ doc/source/commands.rst | 2 +- openstackclient/image/v1/image.py | 20 +-- openstackclient/image/v2/image.py | 8 +- 4 files changed, 260 insertions(+), 15 deletions(-) create mode 100644 doc/source/command-objects/image.rst diff --git a/doc/source/command-objects/image.rst b/doc/source/command-objects/image.rst new file mode 100644 index 0000000000..d9b77266f2 --- /dev/null +++ b/doc/source/command-objects/image.rst @@ -0,0 +1,245 @@ +====== +image +====== + +Image v1, v2 + +image create +------------ + +*Only supported for Image v1* + +Create/upload an image + +.. program:: image create +.. code:: bash + + os image create + [--id ] + [--store ] + [--container-format ] + [--disk-format ] + [--owner ] + [--size ] + [--min-disk ] + [--min-ram ] + [--location ] + [--copy-from ] + [--file ] + [--volume ] + [--force] + [--checksum ] + [--protected | --unprotected] + [--public | --private] + [--property [...] ] + + +.. option:: --id + + Image ID to reserve + +.. option:: --store + + Upload image to this store + +.. option:: --container-format + + Image container format (default: bare) + +.. option:: --disk-format + + Image disk format (default: raw) + +.. option:: --owner + + Image owner project name or ID + +.. option:: --size + + Image size, in bytes (only used with --location and --copy-from) + +.. option:: --min-disk + + Minimum disk size needed to boot image, in gigabytes + +.. option:: --min-ram + + Minimum RAM size needed to boot image, in megabytes + +.. option:: --location + + Download image from an existing URL + +.. option:: --copy-from + + Copy image from the data store (similar to --location) + +.. option:: --file + + Upload image from local file + +.. option:: --volume + + Create image from a volume + +.. option:: --force + + Force image creation if volume is in use (only meaningful with --volume) + +.. option:: --checksum + + Image hash used for verification + +.. option:: --protected + + Prevent image from being deleted + +.. option:: --unprotected + + Allow image to be deleted (default) + +.. option:: --public + + Image is accessible to the public + +.. option:: --private + + Image is inaccessible to the public (default) + +.. option:: --property + + Set a property on this image (repeat for multiple values) + +.. describe:: + + New image name + +image delete +------------ + +Delete image(s) + +.. program:: image delete +.. code:: bash + + os image delete + + +.. describe:: + + Image(s) to delete (name or ID) + +image list +---------- + +List available images + +.. program:: image list +.. code:: bash + + os image list + [--page-size ] + [--long] + +.. option:: --page-size + + Number of images to request in each paginated request + +.. option:: --long + + List additional fields in output + +image save +---------- + +Save an image locally + +.. program:: image save +.. code:: bash + + os image save + --file + + +.. option:: --file + + Downloaded image save filename (default: stdout) + +.. describe:: + + Image to save (name or ID) + +image set +--------- + +*Only supported for Image v1* + +Set image properties + +.. program:: image set +.. code:: bash + + os image set + [--name ] + [--owner ] + [--min-disk ] + [--min-ram ] + [--protected | --unprotected] + [--public | --private] + [--property [...] ] + + +.. option:: --name + + New image name + +.. option:: --owner + + New image owner project (name or ID) + +.. option:: --min-disk + + Minimum disk size needed to boot image, in gigabytes + +.. option:: --min-ram + + Minimum RAM size needed to boot image, in megabytes + +.. option:: --protected + + Prevent image from being deleted + +.. option:: --unprotected + + Allow image to be deleted (default) + +.. option:: --public + + Image is accessible to the public + +.. option:: --private + + Image is inaccessible to the public (default) + +.. option:: --property + + Set a property on this image (repeat for multiple values) + +.. describe:: + + Image to modify (name or ID) + +image show +---------- + +Display image details + +.. program:: image show +.. code:: bash + + os image show + + +.. describe:: + + Image to display (name or ID) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 4f8b55792a..01175bd72e 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -89,7 +89,7 @@ referring to both Compute and Volume quotas. * ``host``: Compute - the physical computer running a hypervisor * ``hypervisor``: Compute - the virtual machine manager * ``identity provider``: (**Identity**) a source of users and authentication -* ``image``: Image - a disk image +* ``image``: (**Image**) a disk image * ``ip fixed``: Compute, Network - an internal IP address assigned to a server * ``ip floating``: Compute, Network - a public IP address that can be mapped to a server * ``keypair``: (**Compute**) an SSH public key diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index ca1eead4d8..d7ece25462 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -49,7 +49,7 @@ def get_parser(self, prog_name): parser = super(CreateImage, self).get_parser(prog_name) parser.add_argument( "name", - metavar="", + metavar="", help="New image name", ) parser.add_argument( @@ -159,7 +159,7 @@ def get_parser(self, prog_name): dest="properties", metavar="", action=parseractions.KeyValueAction, - help="Set an image property " + help="Set a property on this image " "(repeat option to set multiple properties)", ) return parser @@ -337,12 +337,12 @@ def get_parser(self, prog_name): parser.add_argument( "--file", metavar="", - help="Downloaded image save filename [default: stdout]", + help="Downloaded image save filename (default: stdout)", ) parser.add_argument( "image", metavar="", - help="Name or ID of image to save", + help="Image to save (name or ID)", ) return parser @@ -360,7 +360,7 @@ def take_action(self, parsed_args): class SetImage(show.ShowOne): - """Change image properties""" + """Set image properties""" log = logging.getLogger(__name__ + ".SetImage") @@ -369,7 +369,7 @@ def get_parser(self, prog_name): parser.add_argument( "image", metavar="", - help="Image name or ID to change", + help="Image to modify (name or ID)", ) parser.add_argument( "--name", @@ -379,7 +379,7 @@ def get_parser(self, prog_name): parser.add_argument( "--owner", metavar="", - help="New image owner project name or ID", + help="New image owner project (name or ID)", ) parser.add_argument( "--min-disk", @@ -420,7 +420,7 @@ def get_parser(self, prog_name): dest="properties", metavar="", action=parseractions.KeyValueAction, - help="Set an image property " + help="Set a property on this image " "(repeat option to set multiple properties)", ) return parser @@ -474,7 +474,7 @@ def take_action(self, parsed_args): class ShowImage(show.ShowOne): - """Show image details""" + """Display image details""" log = logging.getLogger(__name__ + ".ShowImage") @@ -483,7 +483,7 @@ def get_parser(self, prog_name): parser.add_argument( "image", metavar="", - help="Name or ID of image to display", + help="Image to display (name or ID)", ) return parser diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 63351c6d5e..d5ee692ce8 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -102,12 +102,12 @@ def get_parser(self, prog_name): parser.add_argument( "--file", metavar="", - help="Downloaded image save filename [default: stdout]", + help="Downloaded image save filename (default: stdout)", ) parser.add_argument( "image", metavar="", - help="Name or ID of image to save", + help="Image to save (name or ID)", ) return parser @@ -125,7 +125,7 @@ def take_action(self, parsed_args): class ShowImage(show.ShowOne): - """Show image details""" + """Display image details""" log = logging.getLogger(__name__ + ".ShowImage") @@ -134,7 +134,7 @@ def get_parser(self, prog_name): parser.add_argument( "image", metavar="", - help="Name or ID of image to display", + help="Image to display (name or ID)", ) return parser From 017073327052280b3067096bf5a570b0123e1408 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 5 Jan 2015 21:41:39 -0500 Subject: [PATCH 0324/3494] Fix up snapshot command Several issues with the current snapshot command were resolved: * --long for list was added to include volume id/name, and properties * changed output from metadata to properties * added new option to set properties with 'snapshot set' * added new command to unset properties with 'snapshot unset' Change-Id: I5902cfe876cefada701d4d658a50a4282ff300d6 --- openstackclient/volume/v1/snapshot.py | 129 +++++++++++++++++++++++--- setup.cfg | 1 + 2 files changed, 116 insertions(+), 14 deletions(-) diff --git a/openstackclient/volume/v1/snapshot.py b/openstackclient/volume/v1/snapshot.py index c9e1baca92..c3189f0859 100644 --- a/openstackclient/volume/v1/snapshot.py +++ b/openstackclient/volume/v1/snapshot.py @@ -15,14 +15,15 @@ """Volume v1 Snapshot action implementations""" +import copy import logging import six -import sys from cliff import command from cliff import lister from cliff import show +from openstackclient.common import parseractions from openstackclient.common import utils @@ -70,6 +71,10 @@ def take_action(self, parsed_args): parsed_args.description ) + snapshot._info.update( + {'properties': utils.format_dict(snapshot._info.pop('metadata'))} + ) + return zip(*sorted(six.iteritems(snapshot._info))) @@ -103,20 +108,61 @@ class ListSnapshot(lister.Lister): log = logging.getLogger(__name__ + '.ListSnapshot') + def get_parser(self, prog_name): + parser = super(ListSnapshot, self).get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + default=False, + help='List additional fields in output', + ) + return parser + def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) - columns = ( - 'ID', - 'Display Name', - 'Display Description', - 'Status', - 'Size' - ) + + def _format_volume_id(volume_id): + """Return a volume name if available + + :param volume_id: a volume ID + :rtype: either the volume ID or name + """ + + volume = volume_id + if volume_id in volume_cache.keys(): + volume = volume_cache[volume_id].display_name + return volume + + if parsed_args.long: + columns = ['ID', 'Display Name', 'Display Description', 'Status', + 'Size', 'Created At', 'Volume ID', 'Metadata'] + column_headers = copy.deepcopy(columns) + column_headers[6] = 'Volume' + column_headers[7] = 'Properties' + else: + columns = ['ID', 'Display Name', 'Display Description', 'Status', + 'Size'] + column_headers = copy.deepcopy(columns) + + # Always update Name and Description + column_headers[1] = 'Name' + column_headers[2] = 'Description' + + # Cache the volume list + volume_cache = {} + try: + for s in self.app.client_manager.volume.volumes.list(): + volume_cache[s.id] = s + except Exception: + # Just forget it if there's any trouble + pass + data = self.app.client_manager.volume.volume_snapshots.list() - return (columns, + return (column_headers, (utils.get_item_properties( s, columns, - formatters={}, + formatters={'Metadata': utils.format_dict, + 'Volume ID': _format_volume_id}, ) for s in data)) @@ -133,12 +179,19 @@ def get_parser(self, prog_name): help='Name or ID of snapshot to change') parser.add_argument( '--name', - metavar='', + metavar='', help='New snapshot name') parser.add_argument( '--description', - metavar='', + metavar='', help='New snapshot description') + parser.add_argument( + '--property', + metavar='', + action=parseractions.KeyValueAction, + help='Property to add/change for this snapshot ' + '(repeat option to set multiple properties)', + ) return parser def take_action(self, parsed_args): @@ -146,15 +199,21 @@ def take_action(self, parsed_args): volume_client = self.app.client_manager.volume snapshot = utils.find_resource(volume_client.volume_snapshots, parsed_args.snapshot) + + if parsed_args.property: + volume_client.volume_snapshots.set_metadata(snapshot.id, + parsed_args.property) + kwargs = {} if parsed_args.name: kwargs['display_name'] = parsed_args.name if parsed_args.description: kwargs['display_description'] = parsed_args.description - if not kwargs: - sys.stdout.write("Snapshot not updated, no arguments present") + if not kwargs and not parsed_args.property: + self.app.log.error("No changes requested\n") return + snapshot.update(**kwargs) return @@ -178,4 +237,46 @@ def take_action(self, parsed_args): snapshot = utils.find_resource(volume_client.volume_snapshots, parsed_args.snapshot) + snapshot._info.update( + {'properties': utils.format_dict(snapshot._info.pop('metadata'))} + ) + return zip(*sorted(six.iteritems(snapshot._info))) + + +class UnsetSnapshot(command.Command): + """Unset snapshot properties""" + + log = logging.getLogger(__name__ + '.UnsetSnapshot') + + def get_parser(self, prog_name): + parser = super(UnsetSnapshot, self).get_parser(prog_name) + parser.add_argument( + 'snapshot', + metavar='', + help='snapshot to change (name or ID)', + ) + parser.add_argument( + '--property', + metavar='', + action='append', + default=[], + help='Property key to remove from snapshot ' + '(repeat to remove multiple values)', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug('take_action(%s)', parsed_args) + volume_client = self.app.client_manager.volume + snapshot = utils.find_resource( + volume_client.volume_snapshots, parsed_args.snapshot) + + if parsed_args.property: + volume_client.volume_snapshots.delete_metadata( + snapshot.id, + parsed_args.property, + ) + else: + self.app.log.error("No changes requested\n") + return diff --git a/setup.cfg b/setup.cfg index 8d4e1c5009..224b4c6434 100644 --- a/setup.cfg +++ b/setup.cfg @@ -326,6 +326,7 @@ openstack.volume.v1 = snapshot_list = openstackclient.volume.v1.snapshot:ListSnapshot snapshot_set = openstackclient.volume.v1.snapshot:SetSnapshot snapshot_show = openstackclient.volume.v1.snapshot:ShowSnapshot + snapshot_unset = openstackclient.volume.v1.snapshot:UnsetSnapshot volume_create = openstackclient.volume.v1.volume:CreateVolume volume_delete = openstackclient.volume.v1.volume:DeleteVolume From 460b530d8bb31890e65bc60a7004a391efbcc128 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 5 Jan 2015 22:01:37 -0500 Subject: [PATCH 0325/3494] Command doc: snapshot Change-Id: Ibe5cd0a8422788762e0c52b702b7bd54e6a46813 --- doc/source/command-objects/snapshot.rst | 133 ++++++++++++++++++++++++ doc/source/commands.rst | 2 +- openstackclient/volume/v1/snapshot.py | 22 ++-- 3 files changed, 145 insertions(+), 12 deletions(-) create mode 100644 doc/source/command-objects/snapshot.rst diff --git a/doc/source/command-objects/snapshot.rst b/doc/source/command-objects/snapshot.rst new file mode 100644 index 0000000000..7bfd1d9203 --- /dev/null +++ b/doc/source/command-objects/snapshot.rst @@ -0,0 +1,133 @@ +======== +snapshot +======== + +Volume v1 + +snapshot create +--------------- + +Create new snapshot + +.. program:: snapshot create +.. code:: bash + + os snapshot create + [--name ] + [--description ] + [--force] + + +.. option:: --name + + Name of the snapshot + +.. option:: --description + + Description of the snapshot + +.. option:: --force + + Create a snapshot attached to an instance. Default is False + +.. _snapshot_create-snapshot: +.. describe:: + + Volume to snapshot (name or ID) + +snapshot delete +--------------- + +Delete snapshot(s) + +.. program:: snapshot delete +.. code:: bash + + os snapshot delete + [ ...] + +.. _snapshot_delete-snapshot: +.. describe:: + + Snapshot(s) to delete (name or ID) + +snapshot list +------------- + +List snapshots + +.. program:: snapshot list +.. code:: bash + + os snapshot list + +.. option:: --long + + List additional fields in output + +snapshot set +------------ + +Set snapshot properties + +.. program:: snapshot set +.. code:: bash + + os snapshot set + [--name ] + [--description ] + [--property [...] ] + + +.. _snapshot_restore-snapshot: +.. option:: --name + + New snapshot name + +.. option:: --description + + New snapshot description + +.. option:: --property + + Property to add or modify for this snapshot (repeat option to set multiple properties) + +.. describe:: + + Snapshot to modify (name or ID) + +snapshot show +------------- + +Display snapshot details + +.. program:: snapshot show +.. code:: bash + + os snapshot show + + +.. _snapshot_show-snapshot: +.. describe:: + + Snapshot to display (name or ID) + +snapshot unset +-------------- + +Unset snapshot properties + +.. program:: snapshot unset +.. code:: bash + + os snapshot unset + [--property ] + + +.. option:: --property + + Property to remove from snapshot (repeat option to remove multiple properties) + +.. describe:: + + Snapshot to modify (name or ID) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 4649b012c2..56ce5235d0 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -107,7 +107,7 @@ referring to both Compute and Volume quotas. * ``server``: (**Compute**) virtual machine instance * ``server image``: (**Compute**) saved server disk image * ``service``: Identity - a cloud service -* ``snapshot``: Volume - a point-in-time copy of a volume +* ``snapshot``: (**Volume**) a point-in-time copy of a volume * ``token``: (**Identity**) a bearer token managed by Identity service * ``usage``: (**Compute**) display host resources being consumed * ``user``: (**Identity**) individual cloud resources users diff --git a/openstackclient/volume/v1/snapshot.py b/openstackclient/volume/v1/snapshot.py index c3189f0859..5ec2b3c5ca 100644 --- a/openstackclient/volume/v1/snapshot.py +++ b/openstackclient/volume/v1/snapshot.py @@ -28,7 +28,7 @@ class CreateSnapshot(show.ShowOne): - """Create snapshot command""" + """Create new snapshot""" log = logging.getLogger(__name__ + '.CreateSnapshot') @@ -37,7 +37,7 @@ def get_parser(self, prog_name): parser.add_argument( 'volume', metavar='', - help='The name or ID of the volume to snapshot', + help='Volume to snapshot (name or ID)', ) parser.add_argument( '--name', @@ -104,7 +104,7 @@ def take_action(self, parsed_args): class ListSnapshot(lister.Lister): - """List snapshot command""" + """List snapshots""" log = logging.getLogger(__name__ + '.ListSnapshot') @@ -167,7 +167,7 @@ def _format_volume_id(volume_id): class SetSnapshot(command.Command): - """Set snapshot command""" + """Set snapshot properties""" log = logging.getLogger(__name__ + '.SetSnapshot') @@ -176,14 +176,14 @@ def get_parser(self, prog_name): parser.add_argument( 'snapshot', metavar='', - help='Name or ID of snapshot to change') + help='Snapshot to modify (name or ID)') parser.add_argument( '--name', - metavar='', + metavar='', help='New snapshot name') parser.add_argument( '--description', - metavar='', + metavar='', help='New snapshot description') parser.add_argument( '--property', @@ -219,7 +219,7 @@ def take_action(self, parsed_args): class ShowSnapshot(show.ShowOne): - """Show snapshot command""" + """Display snapshot details""" log = logging.getLogger(__name__ + '.ShowSnapshot') @@ -228,7 +228,7 @@ def get_parser(self, prog_name): parser.add_argument( 'snapshot', metavar='', - help='Name or ID of snapshot to display') + help='Snapshot to display (name or ID)') return parser def take_action(self, parsed_args): @@ -254,14 +254,14 @@ def get_parser(self, prog_name): parser.add_argument( 'snapshot', metavar='', - help='snapshot to change (name or ID)', + help='Snapshot to modify (name or ID)', ) parser.add_argument( '--property', metavar='', action='append', default=[], - help='Property key to remove from snapshot ' + help='Property to remove from snapshot ' '(repeat to remove multiple values)', ) return parser From d9c217e5bc40c35af899c34c5965356b8849586e Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 8 Jan 2015 02:14:06 -0500 Subject: [PATCH 0326/3494] Request token authorize Command doc and tweaks to the code Change-Id: I8f251bf9ca77f16b01a509844e79ddde82048b0d --- doc/source/command-objects/request-token.rst | 20 ++++++++++++++ openstackclient/identity/v3/token.py | 26 ++++++++++++------- .../tests/identity/v3/test_oauth.py | 12 +++++++-- 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/doc/source/command-objects/request-token.rst b/doc/source/command-objects/request-token.rst index 501f67a57f..84081cb17b 100644 --- a/doc/source/command-objects/request-token.rst +++ b/doc/source/command-objects/request-token.rst @@ -6,6 +6,26 @@ Identity v3 `Requires: OS-OAUTH1 extension` +request token authorize +----------------------- + +Authorize a request token + +.. program:: request token authorize +.. code:: bash + + os request token authorize + --request-key + --role + +.. option:: --request-key + + Request token to authorize (ID only) (required) + +.. option:: --role + + Roles to authorize (name or ID) (repeat to set multiple values) (required) + request token create -------------------- diff --git a/openstackclient/identity/v3/token.py b/openstackclient/identity/v3/token.py index 86f31a2a4d..bea2ddeb80 100644 --- a/openstackclient/identity/v3/token.py +++ b/openstackclient/identity/v3/token.py @@ -25,7 +25,7 @@ class AuthorizeRequestToken(show.ShowOne): - """Authorize request token""" + """Authorize a request token""" log = logging.getLogger(__name__ + '.AuthorizeRequestToken') @@ -34,13 +34,16 @@ def get_parser(self, prog_name): parser.add_argument( '--request-key', metavar='', - help='Request token key', + help='Request token to authorize (ID only) (required)', required=True ) parser.add_argument( - '--role-ids', - metavar='', - help='Requested role IDs', + '--role', + metavar='', + action='append', + default=[], + help='Roles to authorize (name or ID) ' + '(repeat to set multiple values) (required)', required=True ) return parser @@ -49,17 +52,20 @@ def take_action(self, parsed_args): self.log.debug('take_action(%s)' % parsed_args) identity_client = self.app.client_manager.identity + # NOTE(stevemar): We want a list of role ids roles = [] - for r_id in parsed_args.role_ids.split(): - roles.append(r_id) + for role in parsed_args.role: + role_id = utils.find_resource( + identity_client.roles, + role, + ).id + roles.append(role_id) verifier_pin = identity_client.oauth1.request_tokens.authorize( parsed_args.request_key, roles) - info = {} - info.update(verifier_pin._info) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(six.iteritems(verifier_pin._info))) class CreateAccessToken(show.ShowOne): diff --git a/openstackclient/tests/identity/v3/test_oauth.py b/openstackclient/tests/identity/v3/test_oauth.py index 36a65e4cf9..435042d1cb 100644 --- a/openstackclient/tests/identity/v3/test_oauth.py +++ b/openstackclient/tests/identity/v3/test_oauth.py @@ -28,6 +28,8 @@ def setUp(self): self.request_tokens_mock.reset_mock() self.projects_mock = identity_client.projects self.projects_mock.reset_mock() + self.roles_mock = identity_client.roles + self.roles_mock.reset_mock() class TestRequestTokenCreate(TestOAuth1): @@ -85,6 +87,12 @@ class TestRequestTokenAuthorize(TestOAuth1): def setUp(self): super(TestRequestTokenAuthorize, self).setUp() + self.roles_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE), + loaded=True, + ) + copied_verifier = copy.deepcopy(identity_fakes.OAUTH_VERIFIER) resource = fakes.FakeResource(None, copied_verifier, loaded=True) self.request_tokens_mock.authorize.return_value = resource @@ -93,11 +101,11 @@ def setUp(self): def test_authorize_request_tokens(self): arglist = [ '--request-key', identity_fakes.request_token_id, - '--role-ids', identity_fakes.role_id, + '--role', identity_fakes.role_name, ] verifylist = [ ('request_key', identity_fakes.request_token_id), - ('role_ids', identity_fakes.role_id), + ('role', [identity_fakes.role_name]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) From d2943d2592b2de98a59c44445c5cd2cc564740da Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 8 Jan 2015 02:22:31 -0500 Subject: [PATCH 0327/3494] Command doc: access token Change-Id: I1b7103e28273f0a63c7d6b6003317b9e69702b05 --- doc/source/command-objects/access-token.rst | 42 +++++++++++++++++++++ doc/source/commands.rst | 2 +- openstackclient/identity/v3/token.py | 16 ++++---- 3 files changed, 50 insertions(+), 10 deletions(-) create mode 100644 doc/source/command-objects/access-token.rst diff --git a/doc/source/command-objects/access-token.rst b/doc/source/command-objects/access-token.rst new file mode 100644 index 0000000000..fd22e761f8 --- /dev/null +++ b/doc/source/command-objects/access-token.rst @@ -0,0 +1,42 @@ +============ +access token +============ + +Identity v3 + +`Requires: OS-OAUTH1 extension` + +access token create +------------------- + +Create an access token + +.. program:: access token create +.. code:: bash + + os access token create + --consumer-key + --consumer-secret + --request-key + --request-secret + --verifier + +.. option:: --consumer-key + + Consumer key (required) + +.. option:: --consumer-secret + + Consumer secret (required) + +.. option:: --request-key + + Request token to exchange for access token (required) + +.. option:: --request-secret + + Secret associated with (required) + +.. option:: --verifier + + Verifier associated with (required) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index a4822c1996..3a7fdd231a 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -69,7 +69,7 @@ overlapping purposes there will be options to select which object to use, or the API resources will be merged, as in the ``quota`` object that has options referring to both Compute and Volume quotas. -* ``access token``: Identity - long-lived OAuth-based token +* ``access token``: (**Identity**) long-lived OAuth-based token * ``availability zone``: (**Compute**) a logical partition of hosts or volume services * ``aggregate``: (**Compute**) a grouping of servers * ``backup``: (**Volume**) a volume copy diff --git a/openstackclient/identity/v3/token.py b/openstackclient/identity/v3/token.py index bea2ddeb80..7000b62ce0 100644 --- a/openstackclient/identity/v3/token.py +++ b/openstackclient/identity/v3/token.py @@ -69,7 +69,7 @@ def take_action(self, parsed_args): class CreateAccessToken(show.ShowOne): - """Create access token""" + """Create an access token""" log = logging.getLogger(__name__ + '.CreateAccessToken') @@ -78,31 +78,31 @@ def get_parser(self, prog_name): parser.add_argument( '--consumer-key', metavar='', - help='Consumer key', + help='Consumer key (required)', required=True ) parser.add_argument( '--consumer-secret', metavar='', - help='Consumer secret', + help='Consumer secret (required)', required=True ) parser.add_argument( '--request-key', metavar='', - help='Request token key', + help='Request token to exchange for access token (required)', required=True ) parser.add_argument( '--request-secret', metavar='', - help='Request token secret', + help='Secret associated with (required)', required=True ) parser.add_argument( '--verifier', metavar='', - help='Verifier Pin', + help='Verifier associated with (required)', required=True ) return parser @@ -114,9 +114,7 @@ def take_action(self, parsed_args): parsed_args.consumer_key, parsed_args.consumer_secret, parsed_args.request_key, parsed_args.request_secret, parsed_args.verifier) - info = {} - info.update(access_token._info) - return zip(*sorted(six.iteritems(info))) + return zip(*sorted(six.iteritems(access_token._info))) class CreateRequestToken(show.ShowOne): From 80499dc5a196ebc1ecffd56d2a776c9efc401998 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 9 Jan 2015 18:35:54 +0000 Subject: [PATCH 0328/3494] Updated from global requirements Change-Id: Iad84313636ee2f53777cdf05d60a322f7a252f27 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 35d2396d31..80b2599d29 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ Babel>=1.3 cliff>=1.7.0 # Apache-2.0 cliff-tablib>=1.0 oslo.i18n>=1.0.0 # Apache-2.0 -oslo.utils>=1.1.0 # Apache-2.0 +oslo.utils>=1.2.0 # Apache-2.0 oslo.serialization>=1.0.0 # Apache-2.0 python-glanceclient>=0.15.0 python-keystoneclient>=0.11.1 From 3b99c178949bc0864f927ac610a12fc666537162 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 9 Jan 2015 19:12:18 -0500 Subject: [PATCH 0329/3494] Add versioning to the docs that missed it Change-Id: I8cb90e0d5aca58c4992271e007af91f8cf782643 --- doc/source/command-objects/aggregate.rst | 2 ++ doc/source/command-objects/console-log.rst | 2 ++ doc/source/command-objects/console-url.rst | 2 ++ doc/source/command-objects/credentials.rst | 2 ++ doc/source/command-objects/flavor.rst | 2 ++ doc/source/command-objects/keypair.rst | 2 ++ doc/source/command-objects/limits.rst | 2 ++ doc/source/command-objects/quota.rst | 2 ++ doc/source/command-objects/server-image.rst | 2 ++ doc/source/command-objects/server.rst | 1 + doc/source/command-objects/user-role.rst | 2 ++ 11 files changed, 21 insertions(+) diff --git a/doc/source/command-objects/aggregate.rst b/doc/source/command-objects/aggregate.rst index 474d811f88..6497f63274 100644 --- a/doc/source/command-objects/aggregate.rst +++ b/doc/source/command-objects/aggregate.rst @@ -5,6 +5,8 @@ aggregate Server aggregates provide a mechanism to group servers according to certain criteria. +Compute v2 + aggregate add host ------------------ diff --git a/doc/source/command-objects/console-log.rst b/doc/source/command-objects/console-log.rst index 8e56a07382..9eafb61ae2 100644 --- a/doc/source/command-objects/console-log.rst +++ b/doc/source/command-objects/console-log.rst @@ -4,6 +4,8 @@ console log Server console text dump +Compute v2 + console log show ---------------- diff --git a/doc/source/command-objects/console-url.rst b/doc/source/command-objects/console-url.rst index 45a0a52717..9cab87981e 100644 --- a/doc/source/command-objects/console-url.rst +++ b/doc/source/command-objects/console-url.rst @@ -4,6 +4,8 @@ console url Server remote console URL +Compute v2 + console url show ---------------- diff --git a/doc/source/command-objects/credentials.rst b/doc/source/command-objects/credentials.rst index ea8fc08fff..9f4aabe4f3 100644 --- a/doc/source/command-objects/credentials.rst +++ b/doc/source/command-objects/credentials.rst @@ -2,6 +2,8 @@ credentials =========== +Identity v3 + credentials create ------------------ diff --git a/doc/source/command-objects/flavor.rst b/doc/source/command-objects/flavor.rst index 4c98e85889..31b8e90227 100644 --- a/doc/source/command-objects/flavor.rst +++ b/doc/source/command-objects/flavor.rst @@ -2,6 +2,8 @@ flavor ====== +Compute v2 + flavor create ------------- diff --git a/doc/source/command-objects/keypair.rst b/doc/source/command-objects/keypair.rst index 9ba0ee8f04..89d12a30ea 100644 --- a/doc/source/command-objects/keypair.rst +++ b/doc/source/command-objects/keypair.rst @@ -5,6 +5,8 @@ keypair The badly named keypair is really the public key of an OpenSSH key pair to be used for access to created servers. +Compute v2 + keypair create -------------- diff --git a/doc/source/command-objects/limits.rst b/doc/source/command-objects/limits.rst index ac388e0f56..1eae4889f5 100644 --- a/doc/source/command-objects/limits.rst +++ b/doc/source/command-objects/limits.rst @@ -4,6 +4,8 @@ limits The Compute and Volume APIs have resource usage limits. +Compute v2, Volume v1 + limits show ----------- diff --git a/doc/source/command-objects/quota.rst b/doc/source/command-objects/quota.rst index ba6712c083..053fb47acc 100644 --- a/doc/source/command-objects/quota.rst +++ b/doc/source/command-objects/quota.rst @@ -4,6 +4,8 @@ quota Resource quotas appear in multiple APIs, OpenStackClient presents them as a single object with multiple properties. +Compute v2, Volume v1 + quota set --------- diff --git a/doc/source/command-objects/server-image.rst b/doc/source/command-objects/server-image.rst index 4577b25b20..8b48934298 100644 --- a/doc/source/command-objects/server-image.rst +++ b/doc/source/command-objects/server-image.rst @@ -5,6 +5,8 @@ server image A server image is a disk image created from a running server instance. The image is created in the Image store. +Compute v2 + server image create ------------------- diff --git a/doc/source/command-objects/server.rst b/doc/source/command-objects/server.rst index 48cefe6ada..360ec24edf 100644 --- a/doc/source/command-objects/server.rst +++ b/doc/source/command-objects/server.rst @@ -2,6 +2,7 @@ server ====== +Compute v2 server add security group ------------------------- diff --git a/doc/source/command-objects/user-role.rst b/doc/source/command-objects/user-role.rst index a25e90ff8f..8283f91110 100644 --- a/doc/source/command-objects/user-role.rst +++ b/doc/source/command-objects/user-role.rst @@ -2,6 +2,8 @@ user role ========= +Identity v2 + user role list -------------- From ffb783215986318aded3eef0d97b603062f7c7ee Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 9 Jan 2015 19:58:17 -0500 Subject: [PATCH 0330/3494] Rework role list v2 for --user and --project `os user role list` does the same as v3's `os role list`. We should rework v2's `role list` to basically call `os user role list` under the covers. Closes-Bug: #1409179 Change-Id: I9839f6be139d6a6a3f6bbf79957e218dd8e03fe3 --- openstackclient/identity/v2_0/role.py | 69 ++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index d03664e07b..295de07e7f 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -146,10 +146,75 @@ class ListRole(lister.Lister): log = logging.getLogger(__name__ + '.ListRole') + def get_parser(self, prog_name): + parser = super(ListRole, self).get_parser(prog_name) + parser.add_argument( + '--project', + metavar='', + help='Filter roles by (name or ID)', + ) + parser.add_argument( + '--user', + metavar='', + help='Filter roles by (name or ID)', + ) + return parser + def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) - columns = ('ID', 'Name') - data = self.app.client_manager.identity.roles.list() + identity_client = self.app.client_manager.identity + auth_ref = self.app.client_manager.auth_ref + + # No user or project specified, list all roles in the system + if not parsed_args.user and not parsed_args.project: + columns = ('ID', 'Name') + data = identity_client.roles.list() + elif parsed_args.user and parsed_args.project: + user = utils.find_resource( + identity_client.users, + parsed_args.user, + ) + project = utils.find_resource( + identity_client.projects, + parsed_args.project, + ) + data = identity_client.roles.roles_for_user(user.id, project.id) + + elif parsed_args.user: + user = utils.find_resource( + identity_client.users, + parsed_args.user, + ) + if self.app.client_manager.auth_ref: + project = utils.find_resource( + identity_client.projects, + auth_ref.project_id + ) + else: + msg = _("Project must be specified") + raise exceptions.CommandError(msg) + data = identity_client.roles.roles_for_user(user.id, project.id) + elif parsed_args.project: + project = utils.find_resource( + identity_client.projects, + parsed_args.project, + ) + if self.app.client_manager.auth_ref: + user = utils.find_resource( + identity_client.users, + auth_ref.user_id + ) + else: + msg = _("User must be specified") + raise exceptions.CommandError(msg) + data = identity_client.roles.roles_for_user(user.id, project.id) + + if parsed_args.user or parsed_args.project: + columns = ('ID', 'Name', 'Project', 'User') + for user_role in data: + user_role.user = user.name + user_role.project = project.name + return (columns, (utils.get_item_properties( s, columns, From a8f60a8aa1c8cbbf0d1384131854a422705e7c78 Mon Sep 17 00:00:00 2001 From: wanghong Date: Mon, 12 Jan 2015 12:08:43 +0800 Subject: [PATCH 0331/3494] fix some small issues in catalog show I think there are three issues we should fix: 1. wrong indentation of 'continue' 2. currently, name is optional for service, but according to the currrent logic, if a service doesn't have name attribute we will select it anyway 3. we always loop all catalogs Change-Id: I9fce66677affa396b6a12afea76e87cab9215a58 --- openstackclient/identity/v2_0/catalog.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/openstackclient/identity/v2_0/catalog.py b/openstackclient/identity/v2_0/catalog.py index 1a96fdf60f..dfc99b47f3 100644 --- a/openstackclient/identity/v2_0/catalog.py +++ b/openstackclient/identity/v2_0/catalog.py @@ -83,17 +83,12 @@ def take_action(self, parsed_args): data = None for service in sc.get_data(): - if ( - 'name' in service and - service['name'] != parsed_args.service and - 'type' in service and - service['type'] != parsed_args.service - ): - continue - - data = service - data['endpoints'] = _format_endpoints(data['endpoints']) - if 'endpoints_links' in data: - data.pop('endpoints_links') + if (service.get('name') == parsed_args.service or + service.get('type') == parsed_args.service): + data = service + data['endpoints'] = _format_endpoints(data['endpoints']) + if 'endpoints_links' in data: + data.pop('endpoints_links') + break return zip(*sorted(six.iteritems(data))) From 6ebbd278cfcedc77402b66481762a2217953fa6e Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Wed, 31 Dec 2014 10:03:28 -0600 Subject: [PATCH 0332/3494] Command docs: add service Co-Authored-By: Lin Hua Cheng Change-Id: Icd39e6d769fd4c4797fcf4ef9eb97c71ed166b3b Closes-Bug: #1404434 --- doc/source/command-objects/service.rst | 144 +++++++++++++++++++++++ doc/source/commands.rst | 2 +- openstackclient/identity/v2_0/service.py | 6 +- openstackclient/identity/v3/service.py | 32 +++-- 4 files changed, 170 insertions(+), 14 deletions(-) create mode 100644 doc/source/command-objects/service.rst diff --git a/doc/source/command-objects/service.rst b/doc/source/command-objects/service.rst new file mode 100644 index 0000000000..d4f63de883 --- /dev/null +++ b/doc/source/command-objects/service.rst @@ -0,0 +1,144 @@ +======= +service +======= + +Identity v2, v3 + +service create +-------------- + +Create new service + +.. program:: service create +.. code-block:: bash + + os service create + [--name ] + [--description ] + [--enable | --disable] + + +.. option:: --name + + New service name + +.. option:: --description + + New service description + +.. option:: --enable + + Enable service (default) + + *Identity version 3 only* + +.. option:: --disable + + Disable service + + *Identity version 3 only* + +.. _service_create-type: +.. describe:: + + New service type (compute, image, identity, volume, etc) + +service delete +-------------- + +Delete service + +.. program:: service delete +.. code-block:: bash + + os service delete + + +:option:`` + Service to delete (type, name or ID) + +service list +------------ + +List services + +.. program:: service list +.. code-block:: bash + + os service list + [--long] + +.. option:: --long + + List additional fields in output + + *Identity version 2 only* + +Returns service fields ID and Name, `--long` adds Type and Description +to the output. When Identity API version 3 is selected all columns are +always displayed, `--long` is silently accepted for backward-compatibility. + +service set +----------- + +Set service properties + +* Identity version 3 only* + +.. program:: service set +.. code-block:: bash + + os service set + [--type ] + [--name ] + [--description ] + [--enable | --disable] + + +.. option:: --type + + New service type (compute, image, identity, volume, etc) + +.. option:: --name + + New service name + +.. option:: --description + + New service description + +.. option:: --enable + + Enable service + +.. option:: --disable + + Disable service + +.. _service_set-service: +.. describe:: + + Service to update (type, name or ID) + +service show +------------ + +Display service details + +.. program:: service show +.. code-block:: bash + + os service show + [--catalog] + + +.. option:: --catalog + + Show service catalog information + + *Identity version 2 only* + +.. _service_show-service: +.. describe:: + + Service to display (type, name or ID) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 3a7fdd231a..6fe91a1f76 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -109,7 +109,7 @@ referring to both Compute and Volume quotas. * ``security group rule``: Compute, Network - the individual rules that define protocol/IP/port access * ``server``: (**Compute**) virtual machine instance * ``server image``: (**Compute**) saved server disk image -* ``service``: Identity - a cloud service +* ``service``: (**Identity**) a cloud service * ``snapshot``: (**Volume**) a point-in-time copy of a volume * ``token``: (**Identity**) a bearer token managed by Identity service * ``usage``: (**Compute**) display host resources being consumed diff --git a/openstackclient/identity/v2_0/service.py b/openstackclient/identity/v2_0/service.py index 0b98a90360..208f7fbcee 100644 --- a/openstackclient/identity/v2_0/service.py +++ b/openstackclient/identity/v2_0/service.py @@ -44,7 +44,7 @@ def get_parser(self, prog_name): type_or_name_group = parser.add_mutually_exclusive_group() type_or_name_group.add_argument( '--type', - metavar='', + metavar='', help=argparse.SUPPRESS, ) type_or_name_group.add_argument( @@ -54,7 +54,7 @@ def get_parser(self, prog_name): ) parser.add_argument( '--description', - metavar='', + metavar='', help=_('New service description'), ) return parser @@ -144,7 +144,7 @@ def take_action(self, parsed_args): class ShowService(show.ShowOne): - """Show service details""" + """Display service details""" log = logging.getLogger(__name__ + '.ShowService') diff --git a/openstackclient/identity/v3/service.py b/openstackclient/identity/v3/service.py index f4c5d42629..126292538c 100644 --- a/openstackclient/identity/v3/service.py +++ b/openstackclient/identity/v3/service.py @@ -15,6 +15,7 @@ """Identity v3 Service action implementations""" +import argparse import logging import six @@ -35,12 +36,12 @@ def get_parser(self, prog_name): parser = super(CreateService, self).get_parser(prog_name) parser.add_argument( 'type', - metavar='', + metavar='', help='New service type (compute, image, identity, volume, etc)', ) parser.add_argument( '--name', - metavar='', + metavar='', help='New service name', ) parser.add_argument( @@ -52,12 +53,12 @@ def get_parser(self, prog_name): enable_group.add_argument( '--enable', action='store_true', - help='Enable project', + help='Enable service (default)', ) enable_group.add_argument( '--disable', action='store_true', - help='Disable project', + help='Disable service', ) return parser @@ -90,7 +91,7 @@ def get_parser(self, prog_name): parser.add_argument( 'service', metavar='', - help='Service to delete (name or ID)', + help='Service to delete (type or ID)', ) return parser @@ -109,6 +110,17 @@ class ListService(lister.Lister): log = logging.getLogger(__name__ + '.ListService') + def get_parser(self, prog_name): + """The --long option is here for compatibility only.""" + parser = super(ListService, self).get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=argparse.SUPPRESS, + ) + return parser + def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) @@ -131,11 +143,11 @@ def get_parser(self, prog_name): parser.add_argument( 'service', metavar='', - help='Service to update (name or ID)', + help='Service to update (type, name or ID)', ) parser.add_argument( '--type', - metavar='', + metavar='', help='New service type (compute, image, identity, volume, etc)', ) parser.add_argument( @@ -152,12 +164,12 @@ def get_parser(self, prog_name): enable_group.add_argument( '--enable', action='store_true', - help='Enable project', + help='Enable service', ) enable_group.add_argument( '--disable', action='store_true', - help='Disable project', + help='Disable service', ) return parser @@ -194,7 +206,7 @@ def take_action(self, parsed_args): class ShowService(show.ShowOne): - """Show service details""" + """Display service details""" log = logging.getLogger(__name__ + '.ShowService') From b17c475f8a3c5dee7b9ef86e73620b9f819a8942 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Mon, 12 Jan 2015 15:22:39 -0500 Subject: [PATCH 0333/3494] Upgrade hacking to 0.10 Also resolve the only error that was produced. Change-Id: Ic81ab01aa0cddc15bb27419d7fec3e5a6d4ec0c7 --- openstackclient/tests/fakes.py | 2 +- test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openstackclient/tests/fakes.py b/openstackclient/tests/fakes.py index 93d4294d3d..e25751d643 100644 --- a/openstackclient/tests/fakes.py +++ b/openstackclient/tests/fakes.py @@ -37,7 +37,7 @@ TEST_VERSIONS = fixture.DiscoveryList(href=AUTH_URL) -class FakeStdout: +class FakeStdout(object): def __init__(self): self.content = [] diff --git a/test-requirements.txt b/test-requirements.txt index ac3e715093..e897e60152 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,7 @@ # The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. -hacking>=0.9.1,<0.10 +hacking>=0.10.0,<0.11 coverage>=3.6 discover From fa84566dac0b16bb02438521b0787c495ff8db6b Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Tue, 13 Jan 2015 00:15:54 +0000 Subject: [PATCH 0334/3494] Updated from global requirements Change-Id: I4be717979bd4371bc544312d556934aef4f3a629 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 80b2599d29..d73bf06567 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ cliff>=1.7.0 # Apache-2.0 cliff-tablib>=1.0 oslo.i18n>=1.0.0 # Apache-2.0 oslo.utils>=1.2.0 # Apache-2.0 -oslo.serialization>=1.0.0 # Apache-2.0 +oslo.serialization>=1.2.0 # Apache-2.0 python-glanceclient>=0.15.0 python-keystoneclient>=0.11.1 python-novaclient>=2.18.0 From 673e0d88ffc299230823204793eb4b4a18a37530 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Sat, 10 Jan 2015 02:43:20 -0500 Subject: [PATCH 0335/3494] Command doc: policy Also tweaked a bunch of the code to not show 'blob', but 'rules' instead. Change-Id: I6db798d272ff416a77f169c0e912d2096fa02504 --- doc/source/command-objects/policy.rst | 95 +++++++++++++++++++++++++++ doc/source/commands.rst | 2 +- openstackclient/identity/v3/policy.py | 63 ++++++++++-------- 3 files changed, 130 insertions(+), 30 deletions(-) create mode 100644 doc/source/command-objects/policy.rst diff --git a/doc/source/command-objects/policy.rst b/doc/source/command-objects/policy.rst new file mode 100644 index 0000000000..195a89f25e --- /dev/null +++ b/doc/source/command-objects/policy.rst @@ -0,0 +1,95 @@ +====== +policy +====== + +Identity v3 + +policy create +------------- + +Create new policy + +.. program:: policy create +.. code:: bash + + os policy create + [--type ] + + +.. option:: --type + + New MIME type of the policy rules file (defaults to application/json) + +.. describe:: + + New serialized policy rules file + +policy delete +------------- + +Delete policy + +.. program:: policy delete +.. code:: bash + + os policy delete + + +.. describe:: + + Policy to delete + +policy list +----------- + +List policies + +.. program:: policy list +.. code:: bash + + os policy list + [--long] + +.. option:: --long + + List additional fields in output + +policy set +---------- + +Set policy properties + +.. program:: policy set +.. code:: bash + + os policy set + [--type ] + [--rules ] + + +.. option:: --type + + New MIME type of the policy rules file + +.. describe:: --rules + + New serialized policy rules file + +.. describe:: + + Policy to modify + +policy show +----------- + +Display policy details + +.. program:: policy show +.. code:: bash + + os policy show + + +.. describe:: + + Policy to display diff --git a/doc/source/commands.rst b/doc/source/commands.rst index 6fe91a1f76..6dbaf11720 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -98,7 +98,7 @@ referring to both Compute and Volume quotas. * ``module``: internal - installed Python modules in the OSC process * ``network``: Network - a virtual network for connecting servers and other resources * ``object``: (**Object Store**) a single file in the Object Store -* ``policy``: Identity - determines authorization +* ``policy``: (**Identity**) determines authorization * ``project``: (**Identity**) owns a group of resources * ``quota``: (**Compute**, **Volume**) resource usage restrictions * ``region``: (**Identity**) a subset of an OpenStack deployment diff --git a/openstackclient/identity/v3/policy.py b/openstackclient/identity/v3/policy.py index 802880bfc2..8293542317 100644 --- a/openstackclient/identity/v3/policy.py +++ b/openstackclient/identity/v3/policy.py @@ -27,7 +27,7 @@ class CreatePolicy(show.ShowOne): - """Create policy command""" + """Create new policy""" log = logging.getLogger(__name__ + '.CreatePolicy') @@ -35,20 +35,21 @@ def get_parser(self, prog_name): parser = super(CreatePolicy, self).get_parser(prog_name) parser.add_argument( '--type', - metavar='', + metavar='', default="application/json", - help='New MIME type of the policy blob - i.e.: application/json', + help='New MIME type of the policy rules file ' + '(defaults to application/json)', ) parser.add_argument( - 'blob_file', - metavar='', - help='New policy rule set itself, as a serialized blob, in a file', + 'rules', + metavar='', + help='New serialized policy rules file', ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) - blob = utils.read_blob_file_contents(parsed_args.blob_file) + blob = utils.read_blob_file_contents(parsed_args.rules) identity_client = self.app.client_manager.identity policy = identity_client.policies.create( @@ -56,11 +57,12 @@ def take_action(self, parsed_args): ) policy._info.pop('links') + policy._info.update({'rules': policy._info.pop('blob')}) return zip(*sorted(six.iteritems(policy._info))) class DeletePolicy(command.Command): - """Delete policy command""" + """Delete policy""" log = logging.getLogger(__name__ + '.DeletePolicy') @@ -68,8 +70,8 @@ def get_parser(self, prog_name): parser = super(DeletePolicy, self).get_parser(prog_name) parser.add_argument( 'policy', - metavar='', - help='ID of policy to delete', + metavar='', + help='Policy to delete', ) return parser @@ -81,28 +83,30 @@ def take_action(self, parsed_args): class ListPolicy(lister.Lister): - """List policy command""" + """List policies""" log = logging.getLogger(__name__ + '.ListPolicy') def get_parser(self, prog_name): parser = super(ListPolicy, self).get_parser(prog_name) parser.add_argument( - '--include-blob', + '--long', action='store_true', default=False, - help='Additional fields are listed in output', + help='List additional fields in output', ) return parser def take_action(self, parsed_args): self.log.debug('take_action(%s)', parsed_args) - if parsed_args.include_blob: + if parsed_args.long: columns = ('ID', 'Type', 'Blob') + column_headers = ('ID', 'Type', 'Rules') else: columns = ('ID', 'Type') + column_headers = columns data = self.app.client_manager.identity.policies.list() - return (columns, + return (column_headers, (utils.get_item_properties( s, columns, formatters={}, @@ -110,7 +114,7 @@ def take_action(self, parsed_args): class SetPolicy(command.Command): - """Set policy command""" + """Set policy properties""" log = logging.getLogger(__name__ + '.SetPolicy') @@ -118,18 +122,18 @@ def get_parser(self, prog_name): parser = super(SetPolicy, self).get_parser(prog_name) parser.add_argument( 'policy', - metavar='', - help='ID of policy to change', + metavar='', + help='Policy to modify', ) parser.add_argument( '--type', - metavar='', - help='New MIME Type of the policy blob - i.e.: application/json', + metavar='', + help='New MIME type of the policy rules file', ) parser.add_argument( - '--blob-file', - metavar='', - help='New policy rule set itself, as a serialized blob, in a file', + '--rules', + metavar='', + help='New serialized policy rules file', ) return parser @@ -138,8 +142,8 @@ def take_action(self, parsed_args): identity_client = self.app.client_manager.identity blob = None - if parsed_args.blob_file: - blob = utils.read_blob_file_contents(parsed_args.blob_file) + if parsed_args.rules: + blob = utils.read_blob_file_contents(parsed_args.rules) kwargs = {} if blob: @@ -148,14 +152,14 @@ def take_action(self, parsed_args): kwargs['type'] = parsed_args.type if not kwargs: - sys.stdout.write("Policy not updated, no arguments present \n") + sys.stdout.write('Policy not updated, no arguments present \n') return identity_client.policies.update(parsed_args.policy, **kwargs) return class ShowPolicy(show.ShowOne): - """Show policy command""" + """Display policy details""" log = logging.getLogger(__name__ + '.ShowPolicy') @@ -163,8 +167,8 @@ def get_parser(self, prog_name): parser = super(ShowPolicy, self).get_parser(prog_name) parser.add_argument( 'policy', - metavar='', - help='ID of policy to display', + metavar='', + help='Policy to display', ) return parser @@ -175,4 +179,5 @@ def take_action(self, parsed_args): parsed_args.policy) policy._info.pop('links') + policy._info.update({'rules': policy._info.pop('blob')}) return zip(*sorted(six.iteritems(policy._info))) From 019c155e9b308dab002f23064b969452bc3d7a89 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 9 Jan 2015 19:13:03 -0500 Subject: [PATCH 0336/3494] Fine tune some of the helps commands try and add some consistency with the show and delete commands. replace 'show x' with 'display x' change 'delete a y' with just 'delete y' Change-Id: I47dfa8ee23ac5c41b355796415eb515155832f65 --- doc/source/command-objects/aggregate.rst | 4 ++-- doc/source/command-objects/catalog.rst | 4 ++++ doc/source/command-objects/consumer.rst | 2 +- doc/source/command-objects/container.rst | 2 +- doc/source/command-objects/domain.rst | 2 +- doc/source/command-objects/federation-protocol.rst | 2 +- doc/source/command-objects/flavor.rst | 2 +- doc/source/command-objects/identity-provider.rst | 2 +- doc/source/command-objects/keypair.rst | 6 +++--- doc/source/command-objects/mapping.rst | 2 +- doc/source/command-objects/object.rst | 2 +- doc/source/command-objects/project.rst | 4 +++- doc/source/command-objects/region.rst | 4 ++-- doc/source/command-objects/role.rst | 6 ++++-- doc/source/command-objects/user.rst | 4 +++- openstackclient/compute/v2/aggregate.py | 4 ++-- openstackclient/compute/v2/flavor.py | 2 +- openstackclient/compute/v2/keypair.py | 6 +++--- openstackclient/identity/v2_0/catalog.py | 2 +- openstackclient/identity/v2_0/project.py | 4 ++-- openstackclient/identity/v2_0/role.py | 8 ++++---- openstackclient/identity/v2_0/service.py | 2 +- openstackclient/identity/v2_0/user.py | 2 +- openstackclient/identity/v3/consumer.py | 2 +- openstackclient/identity/v3/domain.py | 2 +- openstackclient/identity/v3/federation_protocol.py | 2 +- openstackclient/identity/v3/identity_provider.py | 2 +- openstackclient/identity/v3/mapping.py | 2 +- openstackclient/identity/v3/project.py | 4 ++-- openstackclient/identity/v3/region.py | 4 ++-- openstackclient/identity/v3/role.py | 4 ++-- openstackclient/identity/v3/service.py | 2 +- openstackclient/identity/v3/user.py | 2 +- openstackclient/object/v1/container.py | 2 +- openstackclient/object/v1/object.py | 2 +- 35 files changed, 59 insertions(+), 49 deletions(-) diff --git a/doc/source/command-objects/aggregate.rst b/doc/source/command-objects/aggregate.rst index 6497f63274..2a40d23440 100644 --- a/doc/source/command-objects/aggregate.rst +++ b/doc/source/command-objects/aggregate.rst @@ -139,7 +139,7 @@ Set aggregate properties aggregate show -------------- -Show a specific aggregate +Display aggregate details .. program aggregate show .. code:: bash @@ -149,4 +149,4 @@ Show a specific aggregate .. describe:: - Aggregate to show (name or ID) + Aggregate to display (name or ID) diff --git a/doc/source/command-objects/catalog.rst b/doc/source/command-objects/catalog.rst index 99746dd716..4e67f3a4e3 100644 --- a/doc/source/command-objects/catalog.rst +++ b/doc/source/command-objects/catalog.rst @@ -7,6 +7,8 @@ Identity v2 catalog list ------------ +List services in the service catalog + .. code:: bash os catalog list @@ -14,6 +16,8 @@ catalog list catalog show ------------ +Display service catalog details + .. code:: bash os catalog show diff --git a/doc/source/command-objects/consumer.rst b/doc/source/command-objects/consumer.rst index 59ace845e0..91294fa20b 100644 --- a/doc/source/command-objects/consumer.rst +++ b/doc/source/command-objects/consumer.rst @@ -24,7 +24,7 @@ Create new consumer consumer delete --------------- -Delete a consumer +Delete consumer .. program:: consumer delete .. code:: bash diff --git a/doc/source/command-objects/container.rst b/doc/source/command-objects/container.rst index 3afaeb9217..845933d4cd 100644 --- a/doc/source/command-objects/container.rst +++ b/doc/source/command-objects/container.rst @@ -92,7 +92,7 @@ Save container contents locally container show -------------- -Show container details +Display container details .. program:: container show .. code:: bash diff --git a/doc/source/command-objects/domain.rst b/doc/source/command-objects/domain.rst index 66697ac3ea..94473570de 100644 --- a/doc/source/command-objects/domain.rst +++ b/doc/source/command-objects/domain.rst @@ -102,7 +102,7 @@ Set domain properties domain show ----------- -Show domain details +Display domain details .. program:: domain show .. code:: bash diff --git a/doc/source/command-objects/federation-protocol.rst b/doc/source/command-objects/federation-protocol.rst index 0ed0980a54..5b4ea48ace 100644 --- a/doc/source/command-objects/federation-protocol.rst +++ b/doc/source/command-objects/federation-protocol.rst @@ -34,7 +34,7 @@ Create new federation protocol federation protocol delete -------------------------- -Delete a federation protocol +Delete federation protocol .. program:: federation protocol delete .. code:: bash diff --git a/doc/source/command-objects/flavor.rst b/doc/source/command-objects/flavor.rst index 31b8e90227..0083da0dec 100644 --- a/doc/source/command-objects/flavor.rst +++ b/doc/source/command-objects/flavor.rst @@ -67,7 +67,7 @@ Create new flavor flavor delete ------------- -Delete a flavor +Delete flavor .. program:: flavor delete .. code:: bash diff --git a/doc/source/command-objects/identity-provider.rst b/doc/source/command-objects/identity-provider.rst index f08cde0188..47e274dd56 100644 --- a/doc/source/command-objects/identity-provider.rst +++ b/doc/source/command-objects/identity-provider.rst @@ -38,7 +38,7 @@ Create new identity provider identity provider delete ------------------------ -Delete an identity provider +Delete identity provider .. program:: identity provider delete .. code:: bash diff --git a/doc/source/command-objects/keypair.rst b/doc/source/command-objects/keypair.rst index 89d12a30ea..04c5721f91 100644 --- a/doc/source/command-objects/keypair.rst +++ b/doc/source/command-objects/keypair.rst @@ -30,7 +30,7 @@ Create new public key keypair delete -------------- -Delete a public key +Delete public key .. program keypair delete .. code:: bash @@ -55,7 +55,7 @@ List public key fingerprints keypair show ------------ -Show public key details +Display public key details .. program keypair show .. code:: bash @@ -70,4 +70,4 @@ Show public key details .. describe:: - Public key to show + Public key to display diff --git a/doc/source/command-objects/mapping.rst b/doc/source/command-objects/mapping.rst index 5c7535bd1c..25af474061 100644 --- a/doc/source/command-objects/mapping.rst +++ b/doc/source/command-objects/mapping.rst @@ -30,7 +30,7 @@ Create new mapping mapping delete -------------- -Delete a mapping +Delete mapping .. program:: mapping delete .. code:: bash diff --git a/doc/source/command-objects/object.rst b/doc/source/command-objects/object.rst index 5cbc95d71c..c45c10516f 100644 --- a/doc/source/command-objects/object.rst +++ b/doc/source/command-objects/object.rst @@ -122,7 +122,7 @@ Save object locally object show ----------- -Show object details +Display object details .. program:: object show .. code:: bash diff --git a/doc/source/command-objects/project.rst b/doc/source/command-objects/project.rst index 6b55b424bf..b39edb4d6a 100644 --- a/doc/source/command-objects/project.rst +++ b/doc/source/command-objects/project.rst @@ -149,6 +149,8 @@ Set project properties project show ------------ +Display project details + .. program:: project show .. code:: bash @@ -165,4 +167,4 @@ project show .. _project_show-project: .. describe:: - Project to show (name or ID) + Project to display (name or ID) diff --git a/doc/source/command-objects/region.rst b/doc/source/command-objects/region.rst index d1aedb3161..cb4a059eab 100644 --- a/doc/source/command-objects/region.rst +++ b/doc/source/command-objects/region.rst @@ -95,7 +95,7 @@ Set region properties .. _region_set-region-id: .. describe:: - Region ID to modify + Region to modify region show ----------- @@ -111,4 +111,4 @@ Display region details .. _region_show-region-id: .. describe:: - Region ID to display + Region to display diff --git a/doc/source/command-objects/role.rst b/doc/source/command-objects/role.rst index 19195eb554..57161b06be 100644 --- a/doc/source/command-objects/role.rst +++ b/doc/source/command-objects/role.rst @@ -142,7 +142,7 @@ Remove role from domain/project : user/group .. describe:: - Role to remove from ``:`` (name or ID) + Role to remove (name or ID) role set -------- @@ -169,6 +169,8 @@ Set role properties role show --------- +Display role details + .. program:: role show .. code:: bash @@ -177,4 +179,4 @@ role show .. describe:: - Role to show (name or ID) + Role to display (name or ID) diff --git a/doc/source/command-objects/user.rst b/doc/source/command-objects/user.rst index a9a98fe18f..9c81a40326 100644 --- a/doc/source/command-objects/user.rst +++ b/doc/source/command-objects/user.rst @@ -191,6 +191,8 @@ Set user properties user show --------- +Display user details + .. program:: user show .. code:: bash @@ -207,4 +209,4 @@ user show .. _user_show-user: .. describe:: - User to show (name or ID) + User to display (name or ID) diff --git a/openstackclient/compute/v2/aggregate.py b/openstackclient/compute/v2/aggregate.py index bfc2b115aa..84ed5c7d27 100644 --- a/openstackclient/compute/v2/aggregate.py +++ b/openstackclient/compute/v2/aggregate.py @@ -290,7 +290,7 @@ def take_action(self, parsed_args): class ShowAggregate(show.ShowOne): - """Show a specific aggregate""" + """Display aggregate details""" log = logging.getLogger(__name__ + '.ShowAggregate') @@ -299,7 +299,7 @@ def get_parser(self, prog_name): parser.add_argument( 'aggregate', metavar='', - help='Aggregate to show (name or ID)', + help='Aggregate to display (name or ID)', ) return parser diff --git a/openstackclient/compute/v2/flavor.py b/openstackclient/compute/v2/flavor.py index 6f3788a029..bb89a85b5e 100644 --- a/openstackclient/compute/v2/flavor.py +++ b/openstackclient/compute/v2/flavor.py @@ -125,7 +125,7 @@ def take_action(self, parsed_args): class DeleteFlavor(command.Command): - """Delete a flavor""" + """Delete flavor""" log = logging.getLogger(__name__ + ".DeleteFlavor") diff --git a/openstackclient/compute/v2/keypair.py b/openstackclient/compute/v2/keypair.py index 6a158d86ff..edf25f8358 100644 --- a/openstackclient/compute/v2/keypair.py +++ b/openstackclient/compute/v2/keypair.py @@ -80,7 +80,7 @@ def take_action(self, parsed_args): class DeleteKeypair(command.Command): - """Delete a public key""" + """Delete public key""" log = logging.getLogger(__name__ + '.DeleteKeypair') @@ -121,7 +121,7 @@ def take_action(self, parsed_args): class ShowKeypair(show.ShowOne): - """Show public key details""" + """Display public key details""" log = logging.getLogger(__name__ + '.ShowKeypair') @@ -130,7 +130,7 @@ def get_parser(self, prog_name): parser.add_argument( 'name', metavar='', - help='Public key to show', + help='Public key to display', ) parser.add_argument( '--public-key', diff --git a/openstackclient/identity/v2_0/catalog.py b/openstackclient/identity/v2_0/catalog.py index 1a96fdf60f..330dcf3b13 100644 --- a/openstackclient/identity/v2_0/catalog.py +++ b/openstackclient/identity/v2_0/catalog.py @@ -59,7 +59,7 @@ def take_action(self, parsed_args): class ShowCatalog(show.ShowOne): - """Show service catalog details""" + """Display service catalog details""" log = logging.getLogger(__name__ + '.ShowCatalog') diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index 9b195600fc..d01807a600 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -241,7 +241,7 @@ def take_action(self, parsed_args): class ShowProject(show.ShowOne): - """Show project details""" + """Display project details""" log = logging.getLogger(__name__ + '.ShowProject') @@ -250,7 +250,7 @@ def get_parser(self, prog_name): parser.add_argument( 'project', metavar='', - help=_('Project to show (name or ID)')) + help=_('Project to display (name or ID)')) return parser def take_action(self, parsed_args): diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index d03664e07b..f5f901ee81 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -226,7 +226,7 @@ def take_action(self, parsed_args): class RemoveRole(command.Command): - """Remove role from project:user""" + """Remove role from project : user""" log = logging.getLogger(__name__ + '.RemoveRole') @@ -235,7 +235,7 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help=_('Role to remove from : (name or ID)'), + help=_('Role to remove (name or ID)'), ) parser.add_argument( '--project', @@ -267,7 +267,7 @@ def take_action(self, parsed_args): class ShowRole(show.ShowOne): - """Show single role""" + """Display role details""" log = logging.getLogger(__name__ + '.ShowRole') @@ -276,7 +276,7 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help=_('Role to show (name or ID)'), + help=_('Role to display (name or ID)'), ) return parser diff --git a/openstackclient/identity/v2_0/service.py b/openstackclient/identity/v2_0/service.py index 0b98a90360..82d4fd7201 100644 --- a/openstackclient/identity/v2_0/service.py +++ b/openstackclient/identity/v2_0/service.py @@ -144,7 +144,7 @@ def take_action(self, parsed_args): class ShowService(show.ShowOne): - """Show service details""" + """Display service details""" log = logging.getLogger(__name__ + '.ShowService') diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index b5bbce3bd3..500aeec32e 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -349,7 +349,7 @@ def take_action(self, parsed_args): class ShowUser(show.ShowOne): - """Show user details""" + """Display user details""" log = logging.getLogger(__name__ + '.ShowUser') diff --git a/openstackclient/identity/v3/consumer.py b/openstackclient/identity/v3/consumer.py index c5e263926c..ffbd5104cf 100644 --- a/openstackclient/identity/v3/consumer.py +++ b/openstackclient/identity/v3/consumer.py @@ -51,7 +51,7 @@ def take_action(self, parsed_args): class DeleteConsumer(command.Command): - """Delete a consumer""" + """Delete consumer""" log = logging.getLogger(__name__ + '.DeleteConsumer') diff --git a/openstackclient/identity/v3/domain.py b/openstackclient/identity/v3/domain.py index 189f097052..38f99a9772 100644 --- a/openstackclient/identity/v3/domain.py +++ b/openstackclient/identity/v3/domain.py @@ -187,7 +187,7 @@ def take_action(self, parsed_args): class ShowDomain(show.ShowOne): - """Show domain details""" + """Display domain details""" log = logging.getLogger(__name__ + '.ShowDomain') diff --git a/openstackclient/identity/v3/federation_protocol.py b/openstackclient/identity/v3/federation_protocol.py index 5a651165b1..57e8255e9c 100644 --- a/openstackclient/identity/v3/federation_protocol.py +++ b/openstackclient/identity/v3/federation_protocol.py @@ -69,7 +69,7 @@ def take_action(self, parsed_args): class DeleteProtocol(command.Command): - """Delete a federation protocol""" + """Delete federation protocol""" log = logging.getLogger(__name__ + '.DeleteProtocol') diff --git a/openstackclient/identity/v3/identity_provider.py b/openstackclient/identity/v3/identity_provider.py index f46341a16b..691446da4d 100644 --- a/openstackclient/identity/v3/identity_provider.py +++ b/openstackclient/identity/v3/identity_provider.py @@ -69,7 +69,7 @@ def take_action(self, parsed_args): class DeleteIdentityProvider(command.Command): - """Delete an identity provider""" + """Delete identity provider""" log = logging.getLogger(__name__ + '.DeleteIdentityProvider') diff --git a/openstackclient/identity/v3/mapping.py b/openstackclient/identity/v3/mapping.py index a1f60438f6..c79331ec7d 100644 --- a/openstackclient/identity/v3/mapping.py +++ b/openstackclient/identity/v3/mapping.py @@ -112,7 +112,7 @@ def take_action(self, parsed_args): class DeleteMapping(command.Command): - """Delete a mapping""" + """Delete mapping""" log = logging.getLogger(__name__ + '.DeleteMapping') diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 28eb427716..9a54e5cd08 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -298,7 +298,7 @@ def take_action(self, parsed_args): class ShowProject(show.ShowOne): - """Show project command""" + """Display project details""" log = logging.getLogger(__name__ + '.ShowProject') @@ -307,7 +307,7 @@ def get_parser(self, prog_name): parser.add_argument( 'project', metavar='', - help='Name or ID of project to display', + help='Project to display (name or ID)', ) parser.add_argument( '--domain', diff --git a/openstackclient/identity/v3/region.py b/openstackclient/identity/v3/region.py index 5fb7391362..5cb51fc5ec 100644 --- a/openstackclient/identity/v3/region.py +++ b/openstackclient/identity/v3/region.py @@ -138,7 +138,7 @@ def get_parser(self, prog_name): parser.add_argument( 'region', metavar='', - help=_('Region ID to modify'), + help=_('Region to modify'), ) parser.add_argument( '--parent-region', @@ -188,7 +188,7 @@ def get_parser(self, prog_name): parser.add_argument( 'region', metavar='', - help=_('Region ID to display'), + help=_('Region to display'), ) return parser diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index d680278eea..0376070907 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -467,7 +467,7 @@ def take_action(self, parsed_args): class ShowRole(show.ShowOne): - """Show single role""" + """Display role details""" log = logging.getLogger(__name__ + '.ShowRole') @@ -476,7 +476,7 @@ def get_parser(self, prog_name): parser.add_argument( 'role', metavar='', - help='Role to show (name or ID)', + help='Role to display (name or ID)', ) return parser diff --git a/openstackclient/identity/v3/service.py b/openstackclient/identity/v3/service.py index f4c5d42629..c6dfa8d089 100644 --- a/openstackclient/identity/v3/service.py +++ b/openstackclient/identity/v3/service.py @@ -194,7 +194,7 @@ def take_action(self, parsed_args): class ShowService(show.ShowOne): - """Show service details""" + """Display service details""" log = logging.getLogger(__name__ + '.ShowService') diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 4fb7b6d1e0..7b847eedb8 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -422,7 +422,7 @@ def take_action(self, parsed_args): class ShowUser(show.ShowOne): - """Show user details""" + """Display user details""" log = logging.getLogger(__name__ + '.ShowUser') diff --git a/openstackclient/object/v1/container.py b/openstackclient/object/v1/container.py index b75888e408..bc4fdec81b 100644 --- a/openstackclient/object/v1/container.py +++ b/openstackclient/object/v1/container.py @@ -179,7 +179,7 @@ def take_action(self, parsed_args): class ShowContainer(show.ShowOne): - """Show container details""" + """Display container details""" log = logging.getLogger(__name__ + '.ShowContainer') diff --git a/openstackclient/object/v1/object.py b/openstackclient/object/v1/object.py index 781dd047fa..752d78423e 100644 --- a/openstackclient/object/v1/object.py +++ b/openstackclient/object/v1/object.py @@ -222,7 +222,7 @@ def take_action(self, parsed_args): class ShowObject(show.ShowOne): - """Show object details""" + """Display object details""" log = logging.getLogger(__name__ + '.ShowObject') From c04b49ef07defdecadcf614d9ff4cde4b3dd029b Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Fri, 9 Jan 2015 23:44:25 -0500 Subject: [PATCH 0337/3494] Tweaks to the catalog doc and show command Looks like providing a service id isn't working, so it the help message was reduced to just type and name. Added a bit more to the docs, too. Change-Id: Id7f8b48bdf99773ad55ca7f204f3c779f84633d5 --- doc/source/command-objects/catalog.rst | 6 ++++++ openstackclient/identity/v2_0/catalog.py | 7 ++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/doc/source/command-objects/catalog.rst b/doc/source/command-objects/catalog.rst index 4e67f3a4e3..89db95d5d4 100644 --- a/doc/source/command-objects/catalog.rst +++ b/doc/source/command-objects/catalog.rst @@ -9,6 +9,7 @@ catalog list List services in the service catalog +.. program:: catalog list .. code:: bash os catalog list @@ -18,7 +19,12 @@ catalog show Display service catalog details +.. program:: catalog show .. code:: bash os catalog show + +.. describe:: + + Service to display (type or name) diff --git a/openstackclient/identity/v2_0/catalog.py b/openstackclient/identity/v2_0/catalog.py index 330dcf3b13..3aeca91827 100644 --- a/openstackclient/identity/v2_0/catalog.py +++ b/openstackclient/identity/v2_0/catalog.py @@ -68,7 +68,7 @@ def get_parser(self, prog_name): parser.add_argument( 'service', metavar='', - help=_('Service to display (type, name or ID)'), + help=_('Service to display (type or name)'), ) return parser @@ -96,4 +96,9 @@ def take_action(self, parsed_args): if 'endpoints_links' in data: data.pop('endpoints_links') + if not data: + self.app.log.error('service %s not found\n' % + parsed_args.service) + return ([], []) + return zip(*sorted(six.iteritems(data))) From 37eda86099d9213cb43a19f3778dc4ca54a84f10 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Tue, 13 Jan 2015 17:03:01 -0600 Subject: [PATCH 0338/3494] Copy HIG from wiki This is a basic port of the Human Interface Guide from the OpenStack wiki to our docs. Content changes for updating, etc, will follow in a separate review. Change-Id: Id031cd6a27e045b249e16d00e41be24c55fb3c29 --- doc/source/humaninterfaceguide.rst | 360 +++++++++++++++++++++++++++++ doc/source/index.rst | 1 + 2 files changed, 361 insertions(+) create mode 100644 doc/source/humaninterfaceguide.rst diff --git a/doc/source/humaninterfaceguide.rst b/doc/source/humaninterfaceguide.rst new file mode 100644 index 0000000000..371e4f7093 --- /dev/null +++ b/doc/source/humaninterfaceguide.rst @@ -0,0 +1,360 @@ +===================== +Human Interface Guide +===================== + +*Note: This page covers the OpenStackClient CLI only but looks familiar +because it was derived from the Horizon HIG.* + +Overview +======== + +What is a HIG? +The Human Interface Guidelines document was created for OpenStack developers +in order to direct the creation of new OpenStackClient command interfaces. + +Personas +======== + +Personas are archetypal users of the system. Keep these types of users in +mind when designing the interface. + +Alice the admin +--------------- + +Alice is an administrator who is responsible for maintaining the OpenStack +cloud installation. She has many years of experience with Linux systems +administration. + +Darren the deployer +------------------- + +Darren is responsible for doing the initial OpenStack deployment on the +host machines. + +Emile the end-user +------------------ + +Emile uses the cloud to do software development inside of the virtual +machines. She uses the command-line tools because she finds it quicker +than using the dashboard. + +Principles +========== + +The principles established in this section define the high-level priorities +to be used when designing and evaluating interactions for the OpenStack +command line interface. Principles are broad in scope and can be considered +the philosophical foundation for the OpenStack experience; while they may +not describe the tactical implementation of design, they should be used +when deciding between multiple courses of design. + +A significant theme for designing for the OpenStack experience concerns +focusing on common uses of the system rather than adding complexity to support +functionality that is rarely used. + +Consistency +=========== + +Consistency between OpenStack experiences will ensure that the command line +interface feels like a single experience instead of a jumble of disparate +products. Fractured experiences only serve to undermine user expectations +about how they should interact with the system, creating an unreliable user +experience. To avoid this, each interaction and visual representation within +the system must be used uniformly and predictably. The architecture and elements +detailed in this document will provide a strong foundation for establishing a +consistent experience. + +Example Review Criteria +~~~~~~~~~~~~~~~~~~~~~~~ + +* Do the command actions adhere to a consistent application of actions? +* Has a new type of command subject or output been introduced? +* Does the design use command elements (options and arguments) as defined? + (See Core Elements.) +* Can any newly proposed command elements (actions or subjects) be accomplished + with existing elemetns? + +* Does the design adhere to the structural model of the core experience? + (See Core Architecture.) +* Are any data objects displayed or manipulated in a way contradictory to how + they are handled elsewhere in the core experience? + +Simplicity +---------- + +To best support new users and create straight forward interactions, designs +should be as simple as possible. When crafting new commands, designs should +minimize the amount of noise present in output: large amounts of +nonessential data, overabundance of possible actions, etc. Designs should +focus on the intent of the command, requiring only the necessary components +and either removing superfluous elements or making +them accessible through optional arguments. An example of this principle occurs +in OpenStack’s use of tables: only the most often used columns are shown by +default. Further data may be accessed through the output control options, +allowing users to specify the types of data that they find useful in their +day-to-day work. + +Example Review Criteria +~~~~~~~~~~~~~~~~~~~~~~~ + +* Can options be used to combine otherwise similar commands? + +* How many of the displayed elements are relevant to the majority of users? +* If multiple actions are required for the user to complete a task, is each + step required or can the process be more efficient? + +User-Centered Design +-------------------- + +Commands should be design based on how a user will interact with the system +and not how the system’s backend is organized. While database structures and +APIs may define what is possible, they often do not define good user +experience; consider user goals and the way in which users will want to +interact with their data, then design for these work flows and mold the +interface to the user, not the user to the interface. + +Commands should be discoverable via the interface itself. + +To determine a list of available commands, use the :code:`-h` or +:code:`--help` options: + +.. code-block:: bash + + $ openstack --help + +For help with an individual command, use the :code:`help` command: + +.. code-block:: bash + + $ openstack help server create + +Example Review Criteria +~~~~~~~~~~~~~~~~~~~~~~~ + +* How quickly can a user figure out how to accomplish a given task? +* Has content been grouped and ordered according to usage relationships? +* Do work flows support user goals or add complexity? + +Transparency +------------ + +Make sure users understand the current state of their infrastructure and +interactions. For example, users should be able to access information about +the state of each machine/virtual machine easily, without having to actively +seek out this information. Whenever the user initiates an action, make sure +a confirmation is displayed[1] to show that an input has been received. Upon +completion of a process, make sure the user is informed. Ensure that the user +never questions the state of their environment. + +[1] This goes against the common UNIX philosophy of only reporting error +conditions and output that is specifically requested. + +Example Review Criteria +~~~~~~~~~~~~~~~~~~~~~~~ + +* Does the user receive feedback when initiating a process? +* When a process is completed? +* Does the user have quick access to the state of their infrastructure? + + +Architecture +============ + +Command Structure +----------------- + +OpenStackClient has a consistent and predictable format for all of its commands. + +* The top level command name is :code:`openstack` +* Sub-commands take the form: + +.. code-block:: bash + + openstack [] [] [] + +Subcommands shall have three distinct parts to its commands (in order that they appear): + +* global options +* command object(s) and action +* command options and arguments + +Output formats: + +* user-friendly tables with headers, etc +* machine-parsable delimited + +Notes: + +* All long options names shall begin with two dashes ('--') and use a single dash + ('-') internally between words (:code:`--like-this`). Underscores ('_') shall not + be used in option names. +* Authentication options conform to the common CLI authentication guidelines in + :doc:`authentication`. + +Global Options +~~~~~~~~~~~~~~ + +Global options are global in the sense that they apply to every command +invocation regardless of action to be performed. They include authentication +credentials and API version selection. Most global options have a corresponding +environment variable that may also be used to set the value. If both are present, +the command-line option takes priority. The environment variable names are derived +from the option name by dropping the leading dashes ('--'), converting each embedded +dash ('-') to an underscore ('_'), and converting to upper case. + +For example, :code:`--os-username` can be set from the environment via +:code:`OS_USERNAME`. + +--help +++++++ + +The standard :code:`--help` global option displays the documentation for invoking +the program and a list of the available commands on standard output. All other +options and commands are ignored when this is present. The traditional short +form help option (:code:`-h`) is also available. + +--version ++++++++++ + +The standard :code:`--version` option displays the name and version on standard +output. All other options and commands are ignored when this is present. + +Command Object(s) and Action +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Commands consist of an object described by one or more words followed by an action. Commands that require two objects have the primary object ahead of the action and the secondary object after the action. Any positional arguments identifying the objects shall appear in the same order as the objects. In badly formed English it is expressed as "(Take) object1 (and perform) action (using) object2 (to it)." + + [] + +Examples: + +* :code:`group add user ` +* :code:`volume type list` # Note that :code:`volume type` is a two-word + single object + +The :code:`help` command is unique as it appears in front of a normal command +and displays the help text for that command rather than execute it. + +Object names are always specified in command in their singular form. This is +contrary to natural language use. + +Command Arguments and Options +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Each command may have its own set of options distinct from the global options. +They follow the same style as the global options and always appear between +the command and any positional arguments the command requires. + +Option Forms +++++++++++++ + +* **boolean**: boolean options shall use a form of :code:`--|--` + (preferred) or :code:`--