# 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. try: import simplejson JSONDecodeError = simplejson.scanner.JSONDecodeError except ImportError: JSONDecodeError = ValueError from six.moves import urllib from keystoneauth1 import adapter from openstack import _log from openstack import exceptions from openstack import resource def _extract_name(url, service_type=None): '''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] and url_parts[0][0] == 'v' and url_parts[0][1] and url_parts[0][1].isdigit()): 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: if service_type == 'object-store': name_parts = ['account'] else: name_parts = ['discovery'] # Strip out anything that's empty or None return [part for part in name_parts if part] # The _check_resource decorator is used on Proxy methods to ensure that # the `actual` argument is in fact the type of the `expected` argument. # It does so under two cases: # 1. When strict=False, if and only if `actual` is a Resource instance, # it is checked to see that it's an instance of the `expected` class. # This allows `actual` to be other types, such as strings, when it makes # sense to accept a raw id value. # 2. When strict=True, `actual` must be an instance of the `expected` class. def _check_resource(strict=False): def wrap(method): def check(self, expected, actual=None, *args, **kwargs): if (strict and actual is not None and not isinstance(actual, resource.Resource)): raise ValueError("A %s must be passed" % expected.__name__) elif (isinstance(actual, resource.Resource) and not isinstance(actual, expected)): raise ValueError("Expected %s but received %s" % ( expected.__name__, actual.__class__.__name__)) return method(self, expected, actual, *args, **kwargs) return check return wrap class Proxy(adapter.Adapter): """Represents a service.""" retriable_status_codes = None """HTTP status codes that should be retried by default. The number of retries is defined by the configuration in parameters called ``_status_code_retries``. """ def __init__( self, session, statsd_client=None, statsd_prefix=None, prometheus_counter=None, prometheus_histogram=None, *args, **kwargs): # NOTE(dtantsur): keystoneauth defaults retriable_status_codes to None, # override it with a class-level value. kwargs.setdefault('retriable_status_codes', self.retriable_status_codes) super(Proxy, self).__init__(session=session, *args, **kwargs) self._statsd_client = statsd_client self._statsd_prefix = statsd_prefix self._prometheus_counter = prometheus_counter self._prometheus_histogram = prometheus_histogram if self.service_type: log_name = 'openstack.{0}'.format(self.service_type) else: log_name = 'openstack' self.log = _log.setup_logging(log_name) def request( self, url, method, error_message=None, raise_exc=False, connect_retries=1, *args, **kwargs): response = super(Proxy, self).request( url, method, connect_retries=connect_retries, raise_exc=False, **kwargs) for h in response.history: self._report_stats(h) self._report_stats(response) return response def _report_stats(self, response): if self._statsd_client: self._report_stats_statsd(response) if self._prometheus_counter and self._prometheus_histogram: self._report_stats_prometheus(response) def _report_stats_statsd(self, response): name_parts = _extract_name(response.request.url, self.service_type) key = '.'.join( [self._statsd_prefix, self.service_type, response.request.method] + name_parts) self._statsd_client.timing(key, int(response.elapsed.seconds * 1000)) self._statsd_client.incr(key) def _report_stats_prometheus(self, response): labels = dict( method=response.request.method, endpoint=response.request.url, service_type=self.service_type, status_code=response.status_code, ) self._prometheus_counter.labels(**labels).inc() self._prometheus_histogram.labels(**labels).observe( response.elapsed.seconds) def _version_matches(self, version): api_version = self.get_api_major_version() if api_version: return api_version[0] == version return False def _get_connection(self): """Get the Connection object associated with this Proxy. When the Session is created, a reference to the Connection is attached to the ``_sdk_connection`` attribute. We also add a reference to it directly on ourselves. Use one of them. """ return getattr( self, '_connection', getattr( self.session, '_sdk_connection', None)) def _get_resource(self, resource_type, value, **attrs): """Get a resource object to work on :param resource_type: The type of resource to operate on. This should be a subclass of :class:`~openstack.resource.Resource` with a ``from_id`` method. :param value: The ID of a resource or an object of ``resource_type`` class if using an existing instance, or ``munch.Munch``, or None to create a new instance. :param path_args: A dict containing arguments for forming the request URL, if needed. """ conn = self._get_connection() if value is None: # Create a bare resource res = resource_type.new(connection=conn, **attrs) elif (isinstance(value, dict) and not isinstance(value, resource.Resource)): res = resource_type._from_munch( value, connection=conn) res._update(**attrs) elif not isinstance(value, resource_type): # Create from an ID res = resource_type.new( id=value, connection=conn, **attrs) else: # An existing resource instance res = value res._update(**attrs) return res def _get_uri_attribute(self, child, parent, name): """Get a value to be associated with a URI attribute `child` will not be None here as it's a required argument on the proxy method. `parent` is allowed to be None if `child` is an actual resource, but when an ID is given for the child one must also be provided for the parent. An example of this is that a parent is a Server and a child is a ServerInterface. """ if parent is None: value = getattr(child, name) else: value = resource.Resource._get_id(parent) return value def _find(self, resource_type, name_or_id, ignore_missing=True, **attrs): """Find a resource :param name_or_id: The name or ID of a resource to find. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the resource does not exist. When set to ``True``, None will be returned when attempting to find a nonexistent resource. :param dict attrs: Attributes to be passed onto the :meth:`~openstack.resource.Resource.find` method, such as query parameters. :returns: An instance of ``resource_type`` or None """ return resource_type.find(self, name_or_id, ignore_missing=ignore_missing, **attrs) @_check_resource(strict=False) def _delete(self, resource_type, value, ignore_missing=True, **attrs): """Delete a resource :param resource_type: The type of resource to delete. This should be a :class:`~openstack.resource.Resource` subclass with a ``from_id`` method. :param value: The value to delete. Can be either the ID of a resource or a :class:`~openstack.resource.Resource` subclass. :param bool ignore_missing: When set to ``False`` :class:`~openstack.exceptions.ResourceNotFound` will be raised when the resource does not exist. When set to ``True``, no exception will be set when attempting to delete a nonexistent resource. :param dict attrs: Attributes to be passed onto the :meth:`~openstack.resource.Resource.delete` method, such as the ID of a parent resource. :returns: The result of the ``delete`` :raises: ``ValueError`` if ``value`` is a :class:`~openstack.resource.Resource` that doesn't match the ``resource_type``. :class:`~openstack.exceptions.ResourceNotFound` when ignore_missing if ``False`` and a nonexistent resource is attempted to be deleted. """ res = self._get_resource(resource_type, value, **attrs) try: rv = res.delete(self) except exceptions.ResourceNotFound: if ignore_missing: return None raise return rv @_check_resource(strict=False) def _update(self, resource_type, value, base_path=None, **attrs): """Update a resource :param resource_type: The type of resource to update. :type resource_type: :class:`~openstack.resource.Resource` :param value: The resource to update. This must either be a :class:`~openstack.resource.Resource` or an id that corresponds to a resource. :param str base_path: Base part of the URI for updating resources, if different from :data:`~openstack.resource.Resource.base_path`. :param dict attrs: Attributes to be passed onto the :meth:`~openstack.resource.Resource.update` method to be updated. These should correspond to either :class:`~openstack.resource.Body` or :class:`~openstack.resource.Header` values on this resource. :returns: The result of the ``update`` :rtype: :class:`~openstack.resource.Resource` """ res = self._get_resource(resource_type, value, **attrs) return res.commit(self, base_path=base_path) def _create(self, resource_type, base_path=None, **attrs): """Create a resource from attributes :param resource_type: The type of resource to create. :type resource_type: :class:`~openstack.resource.Resource` :param str base_path: Base part of the URI for creating resources, if different from :data:`~openstack.resource.Resource.base_path`. :param path_args: A dict containing arguments for forming the request URL, if needed. :param dict attrs: Attributes to be passed onto the :meth:`~openstack.resource.Resource.create` method to be created. These should correspond to either :class:`~openstack.resource.Body` or :class:`~openstack.resource.Header` values on this resource. :returns: The result of the ``create`` :rtype: :class:`~openstack.resource.Resource` """ conn = self._get_connection() res = resource_type.new(connection=conn, **attrs) return res.create(self, base_path=base_path) @_check_resource(strict=False) def _get(self, resource_type, value=None, requires_id=True, base_path=None, **attrs): """Fetch a resource :param resource_type: The type of resource to get. :type resource_type: :class:`~openstack.resource.Resource` :param value: The value to get. Can be either the ID of a resource or a :class:`~openstack.resource.Resource` subclass. :param str base_path: Base part of the URI for fetching resources, if different from :data:`~openstack.resource.Resource.base_path`. :param dict attrs: Attributes to be passed onto the :meth:`~openstack.resource.Resource.get` method. These should correspond to either :class:`~openstack.resource.Body` or :class:`~openstack.resource.Header` values on this resource. :returns: The result of the ``fetch`` :rtype: :class:`~openstack.resource.Resource` """ res = self._get_resource(resource_type, value, **attrs) return res.fetch( self, requires_id=requires_id, base_path=base_path, error_message="No {resource_type} found for {value}".format( resource_type=resource_type.__name__, value=value)) def _list(self, resource_type, value=None, paginated=True, base_path=None, **attrs): """List a resource :param resource_type: The type of resource to delete. This should be a :class:`~openstack.resource.Resource` subclass with a ``from_id`` method. :param value: The resource to list. It can be the ID of a resource, or a :class:`~openstack.resource.Resource` object. When set to None, a new bare resource is created. :param bool paginated: When set to ``False``, expect all of the data to be returned in one response. When set to ``True``, the resource supports data being returned across multiple pages. :param str base_path: Base part of the URI for listing resources, if different from :data:`~openstack.resource.Resource.base_path`. :param dict attrs: Attributes to be passed onto the :meth:`~openstack.resource.Resource.list` method. These should correspond to either :class:`~openstack.resource.URI` values or appear in :data:`~openstack.resource.Resource._query_mapping`. :returns: A generator of Resource objects. :raises: ``ValueError`` if ``value`` is a :class:`~openstack.resource.Resource` that doesn't match the ``resource_type``. """ return resource_type.list( self, paginated=paginated, base_path=base_path, **attrs) def _head(self, resource_type, value=None, base_path=None, **attrs): """Retrieve a resource's header :param resource_type: The type of resource to retrieve. :type resource_type: :class:`~openstack.resource.Resource` :param value: The value of a specific resource to retreive headers for. Can be either the ID of a resource, a :class:`~openstack.resource.Resource` subclass, or ``None``. :param str base_path: Base part of the URI for heading resources, if different from :data:`~openstack.resource.Resource.base_path`. :param dict attrs: Attributes to be passed onto the :meth:`~openstack.resource.Resource.head` method. These should correspond to :class:`~openstack.resource.URI` values. :returns: The result of the ``head`` call :rtype: :class:`~openstack.resource.Resource` """ res = self._get_resource(resource_type, value, **attrs) return res.head(self, base_path=base_path) def _json_response(response, result_key=None, error_message=None): """Temporary method to use to bridge from ShadeAdapter to SDK calls.""" exceptions.raise_from_response(response, error_message=error_message) if not response.content: # This doesn't have any content return response # Some REST calls do not return json content. Don't decode it. if 'application/json' not in response.headers.get('Content-Type'): return response try: result_json = response.json() except JSONDecodeError: return response return result_json class _ShadeAdapter(Proxy): """Wrapper for shade methods that expect json unpacking.""" def request(self, url, method, error_message=None, **kwargs): response = super(_ShadeAdapter, self).request(url, method, **kwargs) return _json_response(response, error_message=error_message)