diff options
Diffstat (limited to 'openstackclient')
| -rw-r--r-- | openstackclient/api/auth.py | 39 | ||||
| -rw-r--r-- | openstackclient/api/object_store_v1.py | 39 | ||||
| -rw-r--r-- | openstackclient/common/clientmanager.py | 34 | ||||
| -rw-r--r-- | openstackclient/common/exceptions.py | 4 | ||||
| -rw-r--r-- | openstackclient/common/logs.py | 4 | ||||
| -rw-r--r-- | openstackclient/common/utils.py | 14 | ||||
| -rw-r--r-- | openstackclient/identity/v2_0/token.py | 6 | ||||
| -rw-r--r-- | openstackclient/identity/v3/token.py | 3 | ||||
| -rw-r--r-- | openstackclient/network/v2/subnet_pool.py | 4 | ||||
| -rw-r--r-- | openstackclient/shell.py | 3 | ||||
| -rw-r--r-- | openstackclient/tests/common/test_clientmanager.py | 25 | ||||
| -rw-r--r-- | openstackclient/tests/identity/v2_0/fakes.py | 6 | ||||
| -rw-r--r-- | openstackclient/tests/identity/v2_0/test_token.py | 22 | ||||
| -rw-r--r-- | openstackclient/tests/identity/v3/fakes.py | 6 | ||||
| -rw-r--r-- | openstackclient/tests/identity/v3/test_token.py | 21 | ||||
| -rw-r--r-- | openstackclient/tests/network/v2/fakes.py | 19 |
16 files changed, 183 insertions, 66 deletions
diff --git a/openstackclient/api/auth.py b/openstackclient/api/auth.py index 44287318..3d6f7bcf 100644 --- a/openstackclient/api/auth.py +++ b/openstackclient/api/auth.py @@ -80,13 +80,13 @@ def select_auth_plugin(options): # Do the token/url check first as this must override the default # 'password' set by os-client-config # Also, url and token are not copied into o-c-c's auth dict (yet?) - if options.auth.get('url', None) and options.auth.get('token', None): + if options.auth.get('url') and options.auth.get('token'): # service token authentication auth_plugin_name = 'token_endpoint' elif options.auth_type in [plugin.name for plugin in PLUGIN_LIST]: # A direct plugin name was given, use it auth_plugin_name = options.auth_type - elif options.auth.get('username', None): + elif options.auth.get('username'): if options.identity_api_version == '3': auth_plugin_name = 'v3password' elif options.identity_api_version.startswith('2'): @@ -94,7 +94,7 @@ def select_auth_plugin(options): else: # let keystoneclient figure it out itself auth_plugin_name = 'osc_password' - elif options.auth.get('token', None): + elif options.auth.get('token'): if options.identity_api_version == '3': auth_plugin_name = 'v3token' elif options.identity_api_version.startswith('2'): @@ -135,37 +135,42 @@ def build_auth_params(auth_plugin_name, cmd_options): return (auth_plugin_class, auth_params) -def check_valid_auth_options(options, auth_plugin_name): - """Perform basic option checking, provide helpful error messages""" +def check_valid_auth_options(options, auth_plugin_name, required_scope=True): + """Perform basic option checking, provide helpful error messages. + + :param required_scope: indicate whether a scoped token is required + + """ msg = '' if auth_plugin_name.endswith('password'): - if not options.auth.get('username', None): + if not options.auth.get('username'): msg += _('Set a username with --os-username, OS_USERNAME,' ' or auth.username\n') - if not options.auth.get('auth_url', None): + if not options.auth.get('auth_url'): msg += _('Set an authentication URL, with --os-auth-url,' ' OS_AUTH_URL or auth.auth_url\n') - if (not options.auth.get('project_id', None) and not - options.auth.get('domain_id', None) and not - options.auth.get('domain_name', None) and not - options.auth.get('project_name', None) and not - options.auth.get('tenant_id', None) and not - options.auth.get('tenant_name', None)): + if (required_scope and not + options.auth.get('project_id') and not + options.auth.get('domain_id') and not + options.auth.get('domain_name') and not + options.auth.get('project_name') and not + options.auth.get('tenant_id') and not + options.auth.get('tenant_name')): msg += _('Set a scope, such as a project or domain, set a ' 'project scope with --os-project-name, OS_PROJECT_NAME ' 'or auth.project_name, set a domain scope with ' '--os-domain-name, OS_DOMAIN_NAME or auth.domain_name') elif auth_plugin_name.endswith('token'): - if not options.auth.get('token', None): + if not options.auth.get('token'): msg += _('Set a token with --os-token, OS_TOKEN or auth.token\n') - if not options.auth.get('auth_url', None): + if not options.auth.get('auth_url'): msg += _('Set a service AUTH_URL, with --os-auth-url, ' 'OS_AUTH_URL or auth.auth_url\n') elif auth_plugin_name == 'token_endpoint': - if not options.auth.get('token', None): + if not options.auth.get('token'): msg += _('Set a token with --os-token, OS_TOKEN or auth.token\n') - if not options.auth.get('url', None): + if not options.auth.get('url'): msg += _('Set a service URL, with --os-url, OS_URL or auth.url\n') if msg: diff --git a/openstackclient/api/object_store_v1.py b/openstackclient/api/object_store_v1.py index d9f130bc..307c8fe2 100644 --- a/openstackclient/api/object_store_v1.py +++ b/openstackclient/api/object_store_v1.py @@ -50,7 +50,7 @@ class APIv1(api.BaseAPI): data = { 'account': self._find_account_id(), 'container': container, - 'x-trans-id': response.headers.get('x-trans-id', None), + 'x-trans-id': response.headers.get('x-trans-id'), } return data @@ -176,21 +176,19 @@ class APIv1(api.BaseAPI): 'account': self._find_account_id(), 'container': container, 'object_count': response.headers.get( - 'x-container-object-count', - None, + 'x-container-object-count' ), - 'bytes_used': response.headers.get('x-container-bytes-used', None) + 'bytes_used': response.headers.get('x-container-bytes-used') } if 'x-container-read' in response.headers: - data['read_acl'] = response.headers.get('x-container-read', None) + data['read_acl'] = response.headers.get('x-container-read') if 'x-container-write' in response.headers: - data['write_acl'] = response.headers.get('x-container-write', None) + data['write_acl'] = response.headers.get('x-container-write') if 'x-container-sync-to' in response.headers: - data['sync_to'] = response.headers.get('x-container-sync-to', None) + data['sync_to'] = response.headers.get('x-container-sync-to') if 'x-container-sync-key' in response.headers: - data['sync_key'] = response.headers.get('x-container-sync-key', - None) + data['sync_key'] = response.headers.get('x-container-sync-key') properties = self._get_properties(response.headers, 'x-container-meta-') @@ -248,8 +246,8 @@ class APIv1(api.BaseAPI): 'account': self._find_account_id(), 'container': container, 'object': object, - 'x-trans-id': response.headers.get('X-Trans-Id', None), - 'etag': response.headers.get('Etag', None), + 'x-trans-id': response.headers.get('X-Trans-Id'), + 'etag': response.headers.get('Etag'), } return data @@ -453,21 +451,19 @@ class APIv1(api.BaseAPI): 'account': self._find_account_id(), 'container': container, 'object': object, - 'content-type': response.headers.get('content-type', None), + 'content-type': response.headers.get('content-type'), } if 'content-length' in response.headers: data['content-length'] = response.headers.get( - 'content-length', - None, + 'content-length' ) if 'last-modified' in response.headers: - data['last-modified'] = response.headers.get('last-modified', None) + data['last-modified'] = response.headers.get('last-modified') if 'etag' in response.headers: - data['etag'] = response.headers.get('etag', None) + data['etag'] = response.headers.get('etag') if 'x-object-manifest' in response.headers: data['x-object-manifest'] = response.headers.get( - 'x-object-manifest', - None, + 'x-object-manifest' ) properties = self._get_properties(response.headers, 'x-object-meta-') @@ -506,10 +502,9 @@ class APIv1(api.BaseAPI): data['properties'] = properties # Map containers, bytes and objects a bit nicer - data['Containers'] = response.headers.get('x-account-container-count', - None) - data['Objects'] = response.headers.get('x-account-object-count', None) - data['Bytes'] = response.headers.get('x-account-bytes-used', None) + data['Containers'] = response.headers.get('x-account-container-count') + data['Objects'] = response.headers.get('x-account-object-count') + data['Bytes'] = response.headers.get('x-account-bytes-used') # Add in Account info too data['Account'] = self._find_account_id() return data diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index 5696b9e1..fd88bdec 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -90,7 +90,7 @@ class ClientManager(object): self._cli_options = cli_options self._api_version = api_version self._pw_callback = pw_func - self._url = self._cli_options.auth.get('url', None) + self._url = self._cli_options.auth.get('url') self._region_name = self._cli_options.region_name self._interface = self._cli_options.interface @@ -113,24 +113,40 @@ class ClientManager(object): root_logger = logging.getLogger('') LOG.setLevel(root_logger.getEffectiveLevel()) - def setup_auth(self): + # NOTE(gyee): use this flag to indicate whether auth setup has already + # been completed. If so, do not perform auth setup again. The reason + # we need this flag is that we want to be able to perform auth setup + # outside of auth_ref as auth_ref itself is a property. We can not + # retrofit auth_ref to optionally skip scope check. Some operations + # do not require a scoped token. In those cases, we call setup_auth + # prior to dereferrencing auth_ref. + self._auth_setup_completed = False + + def setup_auth(self, required_scope=True): """Set up authentication + :param required_scope: indicate whether a scoped token is required + This is deferred until authentication is actually attempted because it gets in the way of things that do not require auth. """ + if self._auth_setup_completed: + return + # If no auth type is named by the user, select one based on # the supplied options self.auth_plugin_name = auth.select_auth_plugin(self._cli_options) # Basic option checking to avoid unhelpful error messages - auth.check_valid_auth_options(self._cli_options, self.auth_plugin_name) + auth.check_valid_auth_options(self._cli_options, + self.auth_plugin_name, + required_scope=required_scope) # Horrible hack alert...must handle prompt for null password if # password auth is requested. if (self.auth_plugin_name.endswith('password') and - not self._cli_options.auth.get('password', None)): + not self._cli_options.auth.get('password')): self._cli_options.auth['password'] = self._pw_callback() (auth_plugin, self._auth_params) = auth.build_auth_params( @@ -146,9 +162,9 @@ class ClientManager(object): # PROJECT_DOMAIN_ID to 'OS_DEFAULT_DOMAIN' for better usability. if (self._api_version.get('identity') == '3' and self.auth_plugin_name.endswith('password') and - not self._auth_params.get('project_domain_id', None) and + not self._auth_params.get('project_domain_id') and not self.auth_plugin_name.startswith('v2') and - not self._auth_params.get('project_domain_name', None)): + not self._auth_params.get('project_domain_name')): self._auth_params['project_domain_id'] = default_domain # NOTE(stevemar): If USER_DOMAIN_ID or USER_DOMAIN_NAME is present, @@ -157,8 +173,8 @@ class ClientManager(object): if (self._api_version.get('identity') == '3' and self.auth_plugin_name.endswith('password') and not self.auth_plugin_name.startswith('v2') and - not self._auth_params.get('user_domain_id', None) and - not self._auth_params.get('user_domain_name', None)): + not self._auth_params.get('user_domain_id') and + not self._auth_params.get('user_domain_name')): self._auth_params['user_domain_id'] = default_domain # For compatibility until all clients can be updated @@ -180,6 +196,8 @@ class ClientManager(object): user_agent=USER_AGENT, ) + self._auth_setup_completed = True + return @property diff --git a/openstackclient/common/exceptions.py b/openstackclient/common/exceptions.py index ab043db0..8ec49931 100644 --- a/openstackclient/common/exceptions.py +++ b/openstackclient/common/exceptions.py @@ -122,8 +122,8 @@ def from_response(response, body): if body: if hasattr(body, 'keys'): error = body[body.keys()[0]] - message = error.get('message', None) - details = error.get('details', None) + message = error.get('message') + details = error.get('details') else: # If we didn't get back a properly formed error message we # probably couldn't communicate with Keystone at all. diff --git a/openstackclient/common/logs.py b/openstackclient/common/logs.py index 7ad6e832..221c5997 100644 --- a/openstackclient/common/logs.py +++ b/openstackclient/common/logs.py @@ -169,7 +169,7 @@ class LogConfigurator(object): self.dump_trace = cloud_config.config.get('debug', self.dump_trace) self.console_logger.setLevel(log_level) - log_file = cloud_config.config.get('log_file', None) + log_file = cloud_config.config.get('log_file') if log_file: if not self.file_logger: self.file_logger = logging.FileHandler(filename=log_file) @@ -179,7 +179,7 @@ class LogConfigurator(object): self.file_logger.setLevel(log_level) self.root_logger.addHandler(self.file_logger) - logconfig = cloud_config.config.get('logging', None) + logconfig = cloud_config.config.get('logging') if logconfig: highest_level = logging.NOTSET for k in logconfig.keys(): diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py index 4142f830..840da402 100644 --- a/openstackclient/common/utils.py +++ b/openstackclient/common/utils.py @@ -163,7 +163,7 @@ def get_field(item, field): raise exceptions.CommandError(msg) -def get_item_properties(item, fields, mixed_case_fields=[], formatters={}): +def get_item_properties(item, fields, mixed_case_fields=None, formatters=None): """Return a tuple containing the item properties. :param item: a single item resource (e.g. Server, Project, etc) @@ -172,6 +172,11 @@ def get_item_properties(item, fields, mixed_case_fields=[], formatters={}): :param formatters: dictionary mapping field names to callables to format the values """ + if mixed_case_fields is None: + mixed_case_fields = [] + if formatters is None: + formatters = {} + row = [] for field in fields: @@ -187,7 +192,7 @@ def get_item_properties(item, fields, mixed_case_fields=[], formatters={}): return tuple(row) -def get_dict_properties(item, fields, mixed_case_fields=[], formatters={}): +def get_dict_properties(item, fields, mixed_case_fields=None, formatters=None): """Return a tuple containing the item properties. :param item: a single dict resource @@ -196,6 +201,11 @@ def get_dict_properties(item, fields, mixed_case_fields=[], formatters={}): :param formatters: dictionary mapping field names to callables to format the values """ + if mixed_case_fields is None: + mixed_case_fields = [] + if formatters is None: + formatters = {} + row = [] for field in fields: diff --git a/openstackclient/identity/v2_0/token.py b/openstackclient/identity/v2_0/token.py index db38fae8..6a66a1c6 100644 --- a/openstackclient/identity/v2_0/token.py +++ b/openstackclient/identity/v2_0/token.py @@ -24,6 +24,9 @@ from openstackclient.i18n import _ # noqa class IssueToken(command.ShowOne): """Issue new token""" + # scoped token is optional + required_scope = False + def get_parser(self, prog_name): parser = super(IssueToken, self).get_parser(prog_name) return parser @@ -31,7 +34,8 @@ class IssueToken(command.ShowOne): def take_action(self, parsed_args): token = self.app.client_manager.auth_ref.service_catalog.get_token() - token['project_id'] = token.pop('tenant_id') + if 'tenant_id' in token: + token['project_id'] = token.pop('tenant_id') return zip(*sorted(six.iteritems(token))) diff --git a/openstackclient/identity/v3/token.py b/openstackclient/identity/v3/token.py index 9ebd1799..5f131939 100644 --- a/openstackclient/identity/v3/token.py +++ b/openstackclient/identity/v3/token.py @@ -164,6 +164,9 @@ class CreateRequestToken(command.ShowOne): class IssueToken(command.ShowOne): """Issue new token""" + # scoped token is optional + required_scope = False + def get_parser(self, prog_name): parser = super(IssueToken, self).get_parser(prog_name) return parser diff --git a/openstackclient/network/v2/subnet_pool.py b/openstackclient/network/v2/subnet_pool.py index 1db1652f..5bb45c12 100644 --- a/openstackclient/network/v2/subnet_pool.py +++ b/openstackclient/network/v2/subnet_pool.py @@ -99,14 +99,14 @@ class ListSubnetPool(command.Lister): class ShowSubnetPool(command.ShowOne): - """Show subnet pool details""" + """Display subnet pool details""" def get_parser(self, prog_name): parser = super(ShowSubnetPool, self).get_parser(prog_name) parser.add_argument( 'subnet_pool', metavar="<subnet-pool>", - help=("Subnet pool to show (name or ID)") + help=("Subnet pool to display (name or ID)") ) return parser diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 137446ef..dfec40af 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -353,6 +353,9 @@ class OpenStackShell(app.App): cmd.__class__.__name__, ) if cmd.auth_required: + if hasattr(cmd, 'required_scope'): + # let the command decide whether we need a scoped token + self.client_manager.setup_auth(cmd.required_scope) # Trigger the Identity client to initialize self.client_manager.auth_ref return diff --git a/openstackclient/tests/common/test_clientmanager.py b/openstackclient/tests/common/test_clientmanager.py index 523f79a3..ef46f61c 100644 --- a/openstackclient/tests/common/test_clientmanager.py +++ b/openstackclient/tests/common/test_clientmanager.py @@ -325,3 +325,28 @@ class TestClientManager(utils.TestCase): exc.CommandError, client_manager.setup_auth, ) + + @mock.patch('openstackclient.api.auth.check_valid_auth_options') + def test_client_manager_auth_setup_once(self, check_auth_options_func): + client_manager = clientmanager.ClientManager( + cli_options=FakeOptions( + auth=dict( + auth_url=fakes.AUTH_URL, + username=fakes.USERNAME, + password=fakes.PASSWORD, + project_name=fakes.PROJECT_NAME, + ), + ), + api_version=API_VERSION, + verify=False, + ) + self.assertFalse(client_manager._auth_setup_completed) + client_manager.setup_auth() + self.assertTrue(check_auth_options_func.called) + self.assertTrue(client_manager._auth_setup_completed) + + # now make sure we don't do auth setup the second time around + # by checking whether check_valid_auth_options() gets called again + check_auth_options_func.reset_mock() + client_manager.auth_ref + check_auth_options_func.assert_not_called() diff --git a/openstackclient/tests/identity/v2_0/fakes.py b/openstackclient/tests/identity/v2_0/fakes.py index 6688606a..565606c1 100644 --- a/openstackclient/tests/identity/v2_0/fakes.py +++ b/openstackclient/tests/identity/v2_0/fakes.py @@ -80,6 +80,12 @@ TOKEN = { 'user_id': user_id, } +UNSCOPED_TOKEN = { + 'expires': token_expires, + 'id': token_id, + 'user_id': user_id, +} + endpoint_name = service_name endpoint_adminurl = 'https://admin.example.com/v2/UUID' endpoint_region = 'RegionOne' diff --git a/openstackclient/tests/identity/v2_0/test_token.py b/openstackclient/tests/identity/v2_0/test_token.py index 7687a063..c90477f9 100644 --- a/openstackclient/tests/identity/v2_0/test_token.py +++ b/openstackclient/tests/identity/v2_0/test_token.py @@ -60,6 +60,28 @@ class TestTokenIssue(TestToken): ) self.assertEqual(datalist, data) + def test_token_issue_with_unscoped_token(self): + # make sure we return an unscoped token + self.sc_mock.get_token.return_value = identity_fakes.UNSCOPED_TOKEN + + 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', 'user_id') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.token_expires, + identity_fakes.token_id, + identity_fakes.user_id, + ) + self.assertEqual(datalist, data) + class TestTokenRevoke(TestToken): diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index a06802c5..420604f1 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -244,6 +244,12 @@ TRUST = { token_expires = '2014-01-01T00:00:00Z' token_id = 'tttttttt-tttt-tttt-tttt-tttttttttttt' +UNSCOPED_TOKEN = { + 'expires': token_expires, + 'id': token_id, + 'user_id': user_id, +} + TOKEN_WITH_PROJECT_ID = { 'expires': token_expires, 'id': token_id, diff --git a/openstackclient/tests/identity/v3/test_token.py b/openstackclient/tests/identity/v3/test_token.py index b051aacb..80c397bc 100644 --- a/openstackclient/tests/identity/v3/test_token.py +++ b/openstackclient/tests/identity/v3/test_token.py @@ -85,6 +85,27 @@ class TestTokenIssue(TestToken): ) self.assertEqual(datalist, data) + def test_token_issue_with_unscoped(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.sc_mock.get_token.return_value = \ + identity_fakes.UNSCOPED_TOKEN + + # 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', 'user_id') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.token_expires, + identity_fakes.token_id, + identity_fakes.user_id, + ) + self.assertEqual(datalist, data) + class TestTokenRevoke(TestToken): diff --git a/openstackclient/tests/network/v2/fakes.py b/openstackclient/tests/network/v2/fakes.py index 60895e74..680e9cbf 100644 --- a/openstackclient/tests/network/v2/fakes.py +++ b/openstackclient/tests/network/v2/fakes.py @@ -138,12 +138,11 @@ class FakeNetwork(object): router_external, status, subnets, tenant_id """ # Set default attributes. - project_id = 'project-id-' + uuid.uuid4().hex network_attrs = { 'id': 'network-id-' + uuid.uuid4().hex, 'name': 'network-name-' + uuid.uuid4().hex, 'status': 'ACTIVE', - 'tenant_id': project_id, + 'tenant_id': 'project-id-' + uuid.uuid4().hex, 'admin_state_up': True, 'shared': False, 'subnets': ['a', 'b'], @@ -169,7 +168,9 @@ class FakeNetwork(object): network = fakes.FakeResource(info=copy.deepcopy(network_attrs), methods=copy.deepcopy(network_methods), loaded=True) - network.project_id = project_id + + # Set attributes with special mapping in OpenStack SDK. + network.project_id = network_attrs['tenant_id'] return network @@ -273,7 +274,7 @@ class FakePort(object): methods=copy.deepcopy(port_methods), loaded=True) - # Set attributes with special mappings. + # Set attributes with special mappings in OpenStack SDK. port.project_id = port_attrs['tenant_id'] port.binding_host_id = port_attrs['binding:host_id'] port.binding_profile = port_attrs['binding:profile'] @@ -695,24 +696,19 @@ class FakeSubnetPool(object): A FakeResource object faking the subnet pool """ # Set default attributes. - project_id = 'project-id-' + uuid.uuid4().hex subnet_pool_attrs = { 'id': 'subnet-pool-id-' + uuid.uuid4().hex, 'name': 'subnet-pool-name-' + uuid.uuid4().hex, 'prefixes': ['10.0.0.0/24', '10.1.0.0/24'], 'default_prefixlen': 8, 'address_scope_id': 'address-scope-id-' + uuid.uuid4().hex, - 'tenant_id': project_id, + 'tenant_id': 'project-id-' + uuid.uuid4().hex, 'is_default': False, 'shared': False, 'max_prefixlen': 32, 'min_prefixlen': 8, 'default_quota': None, 'ip_version': 4, - - # OpenStack SDK automatically translates project_id to tenant_id. - # So we need an additional attr to simulate this behavior. - 'project_id': project_id, } # Overwrite default attributes. @@ -735,6 +731,9 @@ class FakeSubnetPool(object): loaded=True ) + # Set attributes with special mapping in OpenStack SDK. + subnet_pool.project_id = subnet_pool_attrs['tenant_id'] + return subnet_pool @staticmethod |
