neutron-lib/neutron_lib/placement/client.py
Bence Romsics 1e6a07c5fc Placement client: improve Placement 4xx exceptions
For 4xx errors Placement sends back a complex JSON object describing
the error. When turned into an exception that becomes a non-trivial
attribute of the error object. Usual ways of logging an exception (that
is LOG.exception) completely ignore that attribute, therefore the real
error message is not logged. For example we only logged the fact that
we received a BadRequest response and nothing else while Placement did
provide a whole lot more detail. Here we dig out that error detail and
re-throw a better exception with it.

Change-Id: Id97116c1c298f54f898a746d6e3c96b1f412bb49
Related-Bug: #1578989
2018-11-26 17:42:37 +01:00

607 lines
26 KiB
Python

# Copyright (c) 2016 IBM
# 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 functools
import re
from keystoneauth1 import exceptions as ks_exc
from keystoneauth1 import loading as keystone
from oslo_log import log as logging
from oslo_utils import versionutils
from six.moves.urllib.parse import urlencode
from neutron_lib._i18n import _
from neutron_lib.exceptions import placement as n_exc
LOG = logging.getLogger(__name__)
API_VERSION_REQUEST_HEADER = 'OpenStack-API-Version'
PLACEMENT_API_WITH_MEMBER_OF = 'placement 1.3'
PLACEMENT_API_WITH_NESTED_RESOURCES = 'placement 1.14'
PLACEMENT_API_RETURN_PROVIDER_BODY = 'placement 1.20'
PLACEMENT_API_LATEST_SUPPORTED = PLACEMENT_API_RETURN_PROVIDER_BODY
def _check_placement_api_available(f):
"""Check if the placement API is available.
:param f: Function to execute.
:returns: The returned value of the function f.
"""
@functools.wraps(f)
def wrapper(self, *a, **k):
try:
if not self._client:
self._client = self._create_client()
return f(self, *a, **k)
except ks_exc.http.HttpError as exc:
if 400 <= exc.http_status <= 499:
# NOTE(bence romsics): Placement has inconsistently formatted
# error messages. Some error response bodies are JSON
# formatted, seemingly machine readible objects. While others
# are free format text. We have to keep the whole thing
# to avoid losing information.
raise n_exc.PlacementClientError(
msg=exc.response.text.replace('\n', ' '))
else:
raise
return wrapper
def _get_version(openstack_api_version):
match = re.search(r"placement (?P<api_version>\d+\.\d+)",
openstack_api_version)
return versionutils.convert_version_to_tuple(match.group('api_version'))
class PlacementAPIClient(object):
"""Client class for placement ReST API."""
def __init__(self, conf,
openstack_api_version=PLACEMENT_API_LATEST_SUPPORTED):
self._openstack_api_version = openstack_api_version
self._target_version = _get_version(openstack_api_version)
self._conf = conf
self._ks_filter = {'service_type': 'placement',
'region_name': self._conf.placement.region_name}
self._api_version_header = {API_VERSION_REQUEST_HEADER:
self._openstack_api_version}
self._client = None
def _create_client(self):
"""Create the HTTP session accessing the placement service."""
# Flush _resource_providers and aggregates so we start from a
# clean slate.
self._resource_providers = {}
self._provider_aggregate_map = {}
auth_plugin = keystone.load_auth_from_conf_options(
self._conf, 'placement')
return keystone.load_session_from_conf_options(
self._conf, 'placement', auth=auth_plugin,
additional_headers={'accept': 'application/json'})
def _extend_header_with_api_version(self, **kwargs):
headers = kwargs.get('headers', {})
if API_VERSION_REQUEST_HEADER not in headers:
if 'headers' not in kwargs:
kwargs['headers'] = self._api_version_header
else:
kwargs['headers'].update(self._api_version_header)
return kwargs
def _get(self, url, **kwargs):
kwargs = self._extend_header_with_api_version(**kwargs)
return self._client.get(url, endpoint_filter=self._ks_filter,
**kwargs)
def _post(self, url, data, **kwargs):
kwargs = self._extend_header_with_api_version(**kwargs)
return self._client.post(url, json=data,
endpoint_filter=self._ks_filter, **kwargs)
def _put(self, url, data, **kwargs):
kwargs = self._extend_header_with_api_version(**kwargs)
return self._client.put(url, json=data,
endpoint_filter=self._ks_filter, **kwargs)
def _delete(self, url, **kwargs):
kwargs = self._extend_header_with_api_version(**kwargs)
return self._client.delete(url, endpoint_filter=self._ks_filter,
**kwargs)
@_check_placement_api_available
def create_resource_provider(self, resource_provider):
"""Create a resource provider.
:param resource_provider: The resource provider. A dict with
the uuid (required),
the name (required) and
the parent_provider_uuid (optional).
:returns: The resource provider created.
"""
url = '/resource_providers'
return self._post(url, resource_provider).json()
@_check_placement_api_available
def update_resource_provider(self, resource_provider):
"""Update the resource provider identified by uuid.
:param resource_provider: The resource provider. A dict with
the uuid (required),
the name (required) and
the parent_provider_uuid (optional).
:raises PlacementResourceProviderNotFound: No such resource provider.
:raises PlacementResourceProviderNameNotUnique: Conflict with another
resource provider with
the same name.
:returns: The updated resource provider.
"""
url = '/resource_providers/%s' % resource_provider['uuid']
# update does not tolerate if the uuid is repeated in the body
update_body = resource_provider.copy()
update_body.pop('uuid')
try:
return self._put(url, update_body).json()
except ks_exc.NotFound:
raise n_exc.PlacementResourceProviderNotFound(
resource_provider=resource_provider['uuid'])
except ks_exc.Conflict:
raise n_exc.PlacementResourceProviderNameNotUnique(
name=resource_provider['name'])
@_check_placement_api_available
def ensure_resource_provider(self, resource_provider):
"""Ensure a resource provider exists by updating or creating it.
:param resource_provider: The resource provider. A dict with
the uuid (required),
the name (required) and
the parent_provider_uuid (optional).
:returns: The Resource Provider updated or created.
Beware, this is not an atomic operation of the API.
"""
try:
return self.update_resource_provider(
resource_provider=resource_provider)
except n_exc.PlacementResourceProviderNotFound:
return self.create_resource_provider(resource_provider)
@_check_placement_api_available
def delete_resource_provider(self, resource_provider_uuid):
"""Delete a resource provider.
:param resource_provider_uuid: UUID of the resource provider.
"""
url = '/resource_providers/%s' % resource_provider_uuid
self._delete(url)
@_check_placement_api_available
def get_resource_provider(self, resource_provider_uuid):
"""Get resource provider by UUID.
:param resource_provider_uuid: UUID of the resource provider.
:raises PlacementResourceProviderNotFound: For failure to find resource
:returns: The Resource Provider matching the UUID.
"""
url = '/resource_providers/%s' % resource_provider_uuid
try:
return self._get(url).json()
except ks_exc.NotFound:
raise n_exc.PlacementResourceProviderNotFound(
resource_provider=resource_provider_uuid)
@_check_placement_api_available
def list_resource_providers(self, name=None, member_of=None,
resources=None, in_tree=None, uuid=None):
"""Get a list of resource providers.
:param name: Name of the resource providers.
:param member_of: List of aggregate UUID to get those resource
providers that are associated with.
NOTE: placement 1.3 needed.
:param resources: Dictionary of resource classes and requested values.
:param in_tree: UUID of a resource provider that the caller wants to
limit the returned providers to those within its
'provider tree'. The returned list will contain only
resource providers with the root_provider_id of the
resource provider with UUID == tree_uuid.
NOTE: placement 1.14 needed.
:param uuid: UUID of the resource provider.
:raises PlacementAPIVersionIncorrect: If placement API target version
is too low
:returns: A list of Resource Provider matching the filters.
"""
url = '/resource_providers'
filters = {}
if name:
filters['name'] = name
if member_of:
needed_version = _get_version(PLACEMENT_API_WITH_MEMBER_OF)
if self._target_version < needed_version:
raise n_exc.PlacementAPIVersionIncorrect(
current_version=self._target_version,
needed_version=needed_version)
filters['member_of'] = member_of
if resources:
filters['resources'] = resources
if in_tree:
needed_version = _get_version(
PLACEMENT_API_WITH_NESTED_RESOURCES)
if self._target_version < needed_version:
raise n_exc.PlacementAPIVersionIncorrect(
current_version=self._target_version,
needed_version=needed_version)
filters['in_tree'] = in_tree
if uuid:
filters['uuid'] = uuid
url = '%s?%s' % (url, urlencode(filters))
return self._get(url).json()
@_check_placement_api_available
def update_resource_provider_inventories(
self, resource_provider_uuid, inventories,
resource_provider_generation=None):
"""Replaces the set of inventory records for a resource provider.
:param resource_provider_uuid: UUID of the resource provider.
:param inventories: The inventories. A dict in the format (see:
Placement API ref: https://goo.gl/F22mtk)
{resource_class(required):
{allocation_ratio(required):
total(required):
max_unit(required):
min_unit(required):
reserved(required):
step_size(required):
}}
:param resource_provider_generation: The generation of the resource
provider. Optional.
:raises PlacementResourceProviderNotFound: if the resource provider
is not found.
:raises PlacementResourceProviderGenerationConflict: if the generation
of the resource
provider does not
match with the
server side.
:returns: The updated set of inventory records.
"""
if resource_provider_generation is None:
resource_provider_generation = self.get_resource_provider(
resource_provider_uuid=resource_provider_uuid)['generation']
url = '/resource_providers/%s/inventories' % resource_provider_uuid
body = {
'resource_provider_generation': resource_provider_generation,
'inventories': inventories
}
try:
return self._put(url, body).json()
except ks_exc.NotFound:
raise n_exc.PlacementResourceProviderNotFound(
resource_provider=resource_provider_uuid)
except ks_exc.Conflict:
raise n_exc.PlacementResourceProviderGenerationConflict(
resource_provider=resource_provider_uuid,
generation=resource_provider_generation)
@_check_placement_api_available
def delete_resource_provider_inventories(self, resource_provider_uuid):
"""Delete all inventory records for the resource provider.
:param resource_provider_uuid: UUID of the resource provider.
:raises PlacementResourceProviderNotFound: If the resource provider
is not found.
:returns: None.
"""
url = '/resource_providers/%s/inventories' % (
resource_provider_uuid)
try:
self._delete(url)
except ks_exc.NotFound as e:
if "No resource provider with uuid" in e.details:
raise n_exc.PlacementResourceProviderNotFound(
resource_provider=resource_provider_uuid)
else:
raise
@_check_placement_api_available
def delete_resource_provider_inventory(self, resource_provider_uuid,
resource_class):
"""Delete inventory of the resource class for a resource provider.
:param resource_provider_uuid: UUID of the resource provider.
:param resource_class: The name of the resource class
:raises PlacementResourceProviderNotFound: If the resource provider
is not found.
:raises PlacementInventoryNotFound: No inventory of class.
:returns: None.
"""
url = '/resource_providers/%s/inventories/%s' % (
resource_provider_uuid, resource_class)
try:
self._delete(url)
except ks_exc.NotFound as e:
if "No resource provider with uuid" in e.details:
raise n_exc.PlacementResourceProviderNotFound(
resource_provider=resource_provider_uuid)
elif "No inventory of class" in e.details:
raise n_exc.PlacementInventoryNotFound(
resource_provider=resource_provider_uuid,
resource_class=resource_class)
else:
raise
@_check_placement_api_available
def get_inventory(self, resource_provider_uuid, resource_class):
"""Get resource provider inventory.
:param resource_provider_uuid: UUID of the resource provider.
:param resource_class: Resource class name of the inventory to be
returned.
:raises PlacementResourceProviderNotFound: If the resource provider is
not found.
:raises PlacementInventoryNotFound: For failure to find inventory
for a resource provider.
:returns: The inventory of the resource class as a dict.
"""
url = '/resource_providers/%s/inventories/%s' % (
resource_provider_uuid, resource_class)
try:
return self._get(url).json()
except ks_exc.NotFound as e:
if "No resource provider with uuid" in e.details:
raise n_exc.PlacementResourceProviderNotFound(
resource_provider=resource_provider_uuid)
elif _("No inventory of class") in e.details:
raise n_exc.PlacementInventoryNotFound(
resource_provider=resource_provider_uuid,
resource_class=resource_class)
else:
raise
@_check_placement_api_available
def update_resource_provider_inventory(
self, resource_provider_uuid, inventory, resource_class,
resource_provider_generation=None):
"""Update resource provider inventory.
:param resource_provider_uuid: UUID of the resource provider.
:param inventory: The inventory to be updated for the resource class.
:param resource_class: The name of the resource class.
:param resource_provider_generation: The generation of the resource
provider. Optional.
:raises PlacementResourceNotFound: If the resource provider or the
resource class is not found.
:raises PlacementInventoryUpdateConflict: If the resource provider
generation does not match
with the server side.
:returns: The updated inventory of the resource class as a dict.
"""
if resource_provider_generation is None:
resource_provider_generation = self.get_resource_provider(
resource_provider_uuid=resource_provider_uuid)['generation']
url = '/resource_providers/%s/inventories/%s' % (
resource_provider_uuid, resource_class)
inventory['resource_provider_generation'] = \
resource_provider_generation
try:
return self._put(url, inventory).json()
except ks_exc.NotFound as e:
raise n_exc.PlacementResourceNotFound(url=e.url)
except ks_exc.Conflict:
raise n_exc.PlacementInventoryUpdateConflict(
resource_provider=resource_provider_uuid,
resource_class=resource_class)
@_check_placement_api_available
def associate_aggregates(self, resource_provider_uuid, aggregates):
"""Associate a list of aggregates with a resource provider.
:param resource_provider_uuid: UUID of the resource provider.
:param aggregates: aggregates to be associated to the resource
provider.
:returns: All aggregates associated with the resource provider.
"""
url = '/resource_providers/%s/aggregates' % resource_provider_uuid
return self._put(url, aggregates).json()
@_check_placement_api_available
def list_aggregates(self, resource_provider_uuid):
"""List resource provider aggregates.
:param resource_provider_uuid: UUID of the resource provider.
:raises PlacementAggregateNotFound: For failure to the aggregates of
a resource provider.
:returns: The list of aggregates together with the resource provider
generation.
"""
url = '/resource_providers/%s/aggregates' % resource_provider_uuid
try:
return self._get(url).json()
except ks_exc.NotFound:
raise n_exc.PlacementAggregateNotFound(
resource_provider=resource_provider_uuid)
@_check_placement_api_available
def list_traits(self):
"""List all traits."""
url = '/traits'
return self._get(url).json()
@_check_placement_api_available
def get_trait(self, name):
"""Check if a given trait exists
:param name: name of the trait to check.
:raises PlacementTraitNotFound: If the trait name not found.
:returns: Evaluates to True if the trait exists.
"""
url = '/traits/%s' % name
try:
return self._get(url)
except ks_exc.NotFound:
raise n_exc.PlacementTraitNotFound(trait=name)
@_check_placement_api_available
def update_trait(self, name):
"""Insert a single custom trait.
:param name: name of the trait to create.
:returns: The Response object so you may access response headers.
"""
url = '/traits/%s' % (name)
return self._put(url, None)
@_check_placement_api_available
def delete_trait(self, name):
"""Delete the specified trait.
:param name: the name of the trait to be deleted.
:raises PlacementTraitNotFound: If the trait did not exist.
:returns: None.
"""
url = '/traits/%s' % (name)
try:
self._delete(url)
except ks_exc.NotFound:
raise n_exc.PlacementTraitNotFound(trait=name)
@_check_placement_api_available
def update_resource_provider_traits(
self, resource_provider_uuid, traits,
resource_provider_generation=None):
"""Replace all associated traits of a resource provider.
:param resource_provider_uuid: UUID of the resource provider for which
to set the traits
:param traits: a list of traits.
:param resource_provider_generation: The generation of the resource
provider. Optional.
:raises PlacementResourceProviderNotFound: If the resource provider
is not found.
:raises PlacementTraitNotFound: If any of the specified traits are not
valid.
:returns: The new traits of the resource provider together with the
resource provider generation.
"""
if resource_provider_generation is None:
resource_provider_generation = self.get_resource_provider(
resource_provider_uuid=resource_provider_uuid)['generation']
url = '/resource_providers/%s/traits' % (resource_provider_uuid)
body = {
'resource_provider_generation': resource_provider_generation,
'traits': traits
}
try:
return self._put(url, body).json()
except ks_exc.NotFound:
raise n_exc.PlacementResourceProviderNotFound(
resource_provider=resource_provider_uuid)
except ks_exc.BadRequest:
raise n_exc.PlacementTraitNotFound(trait=traits)
@_check_placement_api_available
def list_resource_provider_traits(self, resource_provider_uuid):
"""List all traits associated with a resource provider
:param resource_provider_uuid: UUID of the resource provider for which
the traits will be listed
:raises PlacementResourceProviderNotFound: If the resource provider
is not found.
:returns: The associated traits of the resource provider together
with the resource provider generation.
"""
url = '/resource_providers/%s/traits' % (resource_provider_uuid)
try:
return self._get(url).json()
except ks_exc.NotFound:
raise n_exc.PlacementResourceProviderNotFound(
resource_provider=resource_provider_uuid)
@_check_placement_api_available
def delete_resource_provider_traits(self, resource_provider_uuid):
"""Delete resource provider traits.
:param resource_provider_uuid: The UUID of the resource provider for
which to delete all the traits.
:raises PlacementResourceProviderNotFound: If the resource provider
is not found.
:returns: None.
"""
url = '/resource_providers/%s/traits' % (resource_provider_uuid)
try:
self._delete(url)
except ks_exc.NotFound:
raise n_exc.PlacementResourceProviderNotFound(
resource_provider=resource_provider_uuid)
@_check_placement_api_available
def list_resource_classes(self):
"""List resource classes"""
url = '/resource_classes'
return self._get(url).json()
@_check_placement_api_available
def get_resource_class(self, name):
"""Show resource class.
:param name: The name of the resource class to show
:raises PlacementResourceClassNotFound: If the resource class
is not found.
:returns: The name of resource class and its set of links.
"""
url = '/resource_classes/%s' % (name)
try:
return self._get(url).json()
except ks_exc.NotFound:
raise n_exc.PlacementResourceClassNotFound(resource_class=name)
@_check_placement_api_available
def create_resource_class(self, name):
"""Create a custom resource class
:param name: the name of the resource class
:returns: None.
"""
url = '/resource_classes'
body = {'name': name}
self._post(url, body)
@_check_placement_api_available
def update_resource_class(self, name):
"""Create or validate the existence of the resource custom class.
:param name: the name of the resource class to be updated or validated
:returns: None.
"""
url = '/resource_classes/%s' % name
self._put(url, None)
@_check_placement_api_available
def delete_resource_class(self, name):
"""Delete a custom resource class.
:param name: The name of the resource class to be deleted.
:raises PlacementResourceClassNotFound: If the resource class
is not found.
:returns: None.
"""
url = '/resource_classes/%s' % (name)
try:
self._delete(url)
except ks_exc.NotFound:
raise n_exc.PlacementResourceClassNotFound(resource_class=name)