# Copyright (c) 2016 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. ''' Wrapper around keystoneauth Session to wrap calls in TaskManager ''' import functools from keystoneauth1 import adapter from six.moves import urllib from shade import _log from shade import exc from shade import meta from shade import task_manager def extract_name(url): '''Produce a key name to use in logging/metrics from the URL path. We want to be able to logic/metric sane general things, so we pull the url apart to generate names. The function returns a list because there are two different ways in which the elements want to be combined below (one for logging, one for statsd) Some examples are likely useful: /servers -> ['servers'] /servers/{id} -> ['servers'] /servers/{id}/os-security-groups -> ['servers', 'os-security-groups'] /v2.0/networks.json -> ['networks'] ''' url_path = urllib.parse.urlparse(url).path.strip() # Remove / from the beginning to keep the list indexes of interesting # things consistent if url_path.startswith('/'): url_path = url_path[1:] # Special case for neutron, which puts .json on the end of urls if url_path.endswith('.json'): url_path = url_path[:-len('.json')] url_parts = url_path.split('/') if url_parts[-1] == 'detail': # Special case detail calls # GET /servers/detail # returns ['servers', 'detail'] name_parts = url_parts[-2:] else: # Strip leading version piece so that # GET /v2.0/networks # returns ['networks'] if url_parts[0] in ('v1', 'v2', 'v2.0'): url_parts = url_parts[1:] name_parts = [] # Pull out every other URL portion - so that # GET /servers/{id}/os-security-groups # returns ['servers', 'os-security-groups'] for idx in range(0, len(url_parts)): if not idx % 2 and url_parts[idx]: name_parts.append(url_parts[idx]) # Keystone Token fetching is a special case, so we name it "tokens" if url_path.endswith('tokens'): name_parts = ['tokens'] # Getting the root of an endpoint is doing version discovery if not name_parts: name_parts = ['discovery'] # Strip out anything that's empty or None return [part for part in name_parts if part] class ShadeAdapter(adapter.Adapter): def __init__(self, shade_logger, manager, *args, **kwargs): super(ShadeAdapter, self).__init__(*args, **kwargs) self.shade_logger = shade_logger self.manager = manager self.request_log = _log.setup_logging('shade.request_ids') def _log_request_id(self, response, obj=None): # Log the request id and object id in a specific logger. This way # someone can turn it on if they're interested in this kind of tracing. request_id = response.headers.get('x-openstack-request-id') if not request_id: return response tmpl = "{meth} call to {service} for {url} used request id {req}" kwargs = dict( meth=response.request.method, service=self.service_type, url=response.request.url, req=request_id) if isinstance(obj, dict): obj_id = obj.get('id', obj.get('uuid')) if obj_id: kwargs['obj_id'] = obj_id tmpl += " returning object {obj_id}" self.request_log.debug(tmpl.format(**kwargs)) return response def _munch_response(self, response, result_key=None): exc.raise_from_response(response) if not response.content: # This doens't have any content return self._log_request_id(response) # Some REST calls do not return json content. Don't decode it. if 'application/json' not in response.headers.get('Content-Type'): return self._log_request_id(response) try: result_json = response.json() except Exception: return self._log_request_id(response) if isinstance(result_json, list): self._log_request_id(response) return meta.obj_list_to_dict(result_json) result = None if isinstance(result_json, dict): # Wrap the keys() call in list() because in python3 keys returns # a "dict_keys" iterator-like object rather than a list json_keys = list(result_json.keys()) if len(json_keys) > 1 and result_key: result = result_json[result_key] elif len(json_keys) == 1: result = result_json[json_keys[0]] if result is None: # Passthrough the whole body - sometimes (hi glance) things # come through without a top-level container. Also, sometimes # you need to deal with pagination result = result_json self._log_request_id(response, result) if isinstance(result, list): return meta.obj_list_to_dict(result) elif isinstance(result, dict): return meta.obj_to_dict(result) return result def request(self, url, method, run_async=False, *args, **kwargs): name_parts = extract_name(url) name = '.'.join([self.service_type, method] + name_parts) class_name = "".join([ part.lower().capitalize() for part in name.split('.')]) request_method = functools.partial( super(ShadeAdapter, self).request, url, method) class RequestTask(task_manager.BaseTask): def __init__(self, **kw): super(RequestTask, self).__init__(**kw) self.name = name self.__class__.__name__ = str(class_name) self.run_async = run_async def main(self, client): self.args.setdefault('raise_exc', False) return request_method(**self.args) response = self.manager.submit_task(RequestTask(**kwargs)) if run_async: return response else: return self._munch_response(response)