Rehome placement client to neutron-lib

The placement client is going to be used by other services aside from
segments; e.g. QoS plugin. It makes sense to move this file to a common
place, like neutron-lib.

Closes-Bug: #1723452
Partial-Bug: #1578989

Change-Id: I2f7d204828a620152ec9e005e057fc7fd77f9126
This commit is contained in:
Rodolfo Alonso Hernandez 2017-10-13 18:47:55 +01:00
parent e2a9c397f7
commit 9ef0860033
9 changed files with 457 additions and 0 deletions

View File

View File

@ -0,0 +1,206 @@
# 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
from keystoneauth1 import exceptions as ks_exc
from keystoneauth1 import loading as keystone
from oslo_log import log as logging
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_AGGREGATES = 'placement 1.1'
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.
:raises PlacementEndpointNotFound: If the placement API endpoint configured
is not found.
"""
@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.EndpointNotFound:
LOG.warning('Please enable the placement service.')
except ks_exc.MissingAuthPlugin:
LOG.warning('No authentication information found for placement '
'API. Please enable the placement service.')
except ks_exc.Unauthorized:
LOG.warning('Placement service credentials do not work. '
'Please enable the placement service.')
except ks_exc.DiscoveryFailure:
LOG.warning('Discovering suitable URL for placement API failed.')
except ks_exc.ConnectFailure:
LOG.warning('Placement API service is not responding.')
return wrapper
class PlacementAPIClient(object):
"""Client class for placement ReST API."""
def __init__(self, conf,
openstack_api_version=PLACEMENT_API_WITH_AGGREGATES):
self._openstack_api_version = openstack_api_version
self._conf = conf
self._ks_filter = {'service_type': 'placement',
'region_name': self._conf.placement.region_name}
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 _get(self, url, **kwargs):
return self._client.get(url, endpoint_filter=self._ks_filter,
**kwargs)
def _post(self, url, data, **kwargs):
return self._client.post(url, json=data,
endpoint_filter=self._ks_filter, **kwargs)
def _put(self, url, data, **kwargs):
return self._client.put(url, json=data,
endpoint_filter=self._ks_filter, **kwargs)
def _delete(self, url, **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 name
(required) and the uuid (required).
"""
url = '/resource_providers'
self._post(url, 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 create_inventory(self, resource_provider_uuid, inventory):
"""Create an inventory.
:param resource_provider_uuid: UUID of the resource provider.
:param inventory: The inventory. A dict with resource_class (required),
total (required), reserved (required), min_unit
(required), max_unit (required), step_size
(required) and allocation_ratio (required).
"""
url = '/resource_providers/%s/inventories' % resource_provider_uuid
self._post(url, inventory)
@_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 PlacementInventoryNotFound: For failure to find inventory
for a resource provider.
"""
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_inventory(self, resource_provider_uuid, inventory,
resource_class):
"""Update an inventory.
:param resource_provider_uuid: UUID of the resource provider.
:param inventory: The inventory, in a dictionary.
:param resource_class: The resource class of the inventory to update.
:raises PlacementInventoryUpdateConflict: For failure to update
inventory due to outdated
resource_provider_generation.
"""
url = '/resource_providers/%s/inventories/%s' % (
resource_provider_uuid, resource_class)
try:
self._put(url, inventory)
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.
"""
url = '/resource_providers/%s/aggregates' % resource_provider_uuid
self._put(url, aggregates,
headers={API_VERSION_REQUEST_HEADER:
self._openstack_api_version})
@_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.
"""
url = '/resource_providers/%s/aggregates' % resource_provider_uuid
try:
return self._get(
url, headers={API_VERSION_REQUEST_HEADER:
self._openstack_api_version}).json()
except ks_exc.NotFound:
raise n_exc.PlacementAggregateNotFound(
resource_provider=resource_provider_uuid)

View File

@ -0,0 +1,39 @@
# 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.
from neutron_lib._i18n import _
from neutron_lib import exceptions
class PlacementEndpointNotFound(exceptions.NotFound):
message = _("Placement API endpoint not found.")
class PlacementResourceProviderNotFound(exceptions.NotFound):
message = _("Placement resource provider not found %(resource_provider)s.")
class PlacementInventoryNotFound(exceptions.NotFound):
message = _("Placement inventory not found for resource provider "
"%(resource_provider)s, resource class %(resource_class)s.")
class PlacementInventoryUpdateConflict(exceptions.Conflict):
message = _("Placement inventory update conflict for resource provider "
"%(resource_provider)s, resource class %(resource_class)s.")
class PlacementAggregateNotFound(exceptions.NotFound):
message = _("Aggregate not found for resource provider "
"%(resource_provider)s.")

View File

@ -154,3 +154,47 @@ class APIDefinitionFixture(fixtures.Fixture):
def all_api_definitions_fixture(cls):
"""Return a fixture that handles all neutron-lib api-defs."""
return APIDefinitionFixture(*tuple(definitions._ALL_API_DEFINITIONS))
class PlacementAPIClientFixture(fixtures.Fixture):
"""Placement API client fixture.
This class is intended to be used as a fixture within unit tests and
therefore consumers must register it using useFixture() within their
unit test class.
"""
def __init__(self, placement_api_client):
"""Creates a new PlacementAPIClientFixture.
:param placement_api_client: Placement API client object.
"""
super(PlacementAPIClientFixture, self).__init__()
self.placement_api_client = placement_api_client
def _setUp(self):
self.addCleanup(self._restore)
def mock_create_client():
self.placement_api_client.client = mock.Mock()
self._mock_create_client = mock.patch.object(
self.placement_api_client, '_create_client',
side_effect=mock_create_client)
self._mock_get = mock.patch.object(self.placement_api_client, '_get')
self._mock_post = mock.patch.object(self.placement_api_client, '_post')
self._mock_put = mock.patch.object(self.placement_api_client, '_put')
self._mock_delete = mock.patch.object(self.placement_api_client,
'_delete')
self._mock_create_client.start()
self.mock_get = self._mock_get.start()
self.mock_post = self._mock_post.start()
self.mock_put = self._mock_put.start()
self.mock_delete = self._mock_delete.start()
def _restore(self):
self._mock_create_client.stop()
self._mock_get.stop()
self._mock_post.stop()
self._mock_put.stop()
self._mock_delete.stop()

View File

@ -0,0 +1,131 @@
# 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 keystoneauth1 import exceptions as ks_exc
from oslo_utils import uuidutils
from neutron_lib._i18n import _
from neutron_lib.clients import placement
from neutron_lib.exceptions import placement as n_exc
from neutron_lib import fixture
from neutron_lib.tests import _base as base
RESOURCE_PROVIDER_UUID = uuidutils.generate_uuid()
RESOURCE_CLASS_NAME = 'resource_class_name'
class TestPlacementAPIClient(base.BaseTestCase):
def setUp(self):
super(TestPlacementAPIClient, self).setUp()
config = mock.Mock()
config.region_name = 'region_name'
self.openstack_api_version = 'version 1.1'
self.placement_api_client = placement.PlacementAPIClient(
config, self.openstack_api_version)
self.placement_fixture = self.useFixture(
fixture.PlacementAPIClientFixture(self.placement_api_client))
def test_create_resource_provider(self):
self.placement_api_client.create_resource_provider(
RESOURCE_PROVIDER_UUID)
self.placement_fixture.mock_post.assert_called_once_with(
'/resource_providers', RESOURCE_PROVIDER_UUID)
def test_delete_resource_provider(self):
self.placement_api_client.delete_resource_provider(
RESOURCE_PROVIDER_UUID)
self.placement_fixture.mock_delete.assert_called_once_with(
'/resource_providers/%s' % RESOURCE_PROVIDER_UUID)
def test_create_inventory(self):
inventory = mock.ANY
self.placement_api_client.create_inventory(RESOURCE_PROVIDER_UUID,
inventory)
self.placement_fixture.mock_post.assert_called_once_with(
'/resource_providers/%s/inventories' % RESOURCE_PROVIDER_UUID,
inventory)
def test_get_inventory(self):
self.placement_api_client.get_inventory(RESOURCE_PROVIDER_UUID,
RESOURCE_CLASS_NAME)
self.placement_fixture.mock_get.assert_called_once_with(
'/resource_providers/%(rp_uuid)s/inventories/%(rc_name)s' %
{'rp_uuid': RESOURCE_PROVIDER_UUID,
'rc_name': RESOURCE_CLASS_NAME})
def test_get_inventory_no_resource_provider(self):
_exception = ks_exc.NotFound()
_exception.details = "No resource provider with uuid"
self.placement_fixture.mock_get.side_effect = _exception
self.assertRaises(n_exc.PlacementResourceProviderNotFound,
self.placement_api_client.get_inventory,
RESOURCE_PROVIDER_UUID, RESOURCE_CLASS_NAME)
def test_get_inventory_no_inventory(self):
_exception = ks_exc.NotFound()
_exception.details = _("No inventory of class")
self.placement_fixture.mock_get.side_effect = _exception
self.assertRaises(n_exc.PlacementInventoryNotFound,
self.placement_api_client.get_inventory,
RESOURCE_PROVIDER_UUID, RESOURCE_CLASS_NAME)
def test_get_inventory_not_found(self):
_exception = ks_exc.NotFound()
_exception.details = "Any other exception explanation"
self.placement_fixture.mock_get.side_effect = _exception
self.assertRaises(ks_exc.NotFound,
self.placement_api_client.get_inventory,
RESOURCE_PROVIDER_UUID, RESOURCE_CLASS_NAME)
def test_update_inventory(self):
inventory = mock.ANY
self.placement_api_client.update_inventory(
RESOURCE_PROVIDER_UUID, inventory, RESOURCE_CLASS_NAME)
self.placement_fixture.mock_put.assert_called_once_with(
'/resource_providers/%(rp_uuid)s/inventories/%(rc_name)s' %
{'rp_uuid': RESOURCE_PROVIDER_UUID,
'rc_name': RESOURCE_CLASS_NAME},
inventory)
def test_update_inventory_conflict_exception(self):
inventory = mock.ANY
self.placement_fixture.mock_put.side_effect = ks_exc.Conflict()
self.assertRaises(n_exc.PlacementInventoryUpdateConflict,
self.placement_api_client.update_inventory,
RESOURCE_PROVIDER_UUID, inventory,
RESOURCE_CLASS_NAME)
def test_associate_aggregates(self):
headers = {'OpenStack-API-Version': self.openstack_api_version}
self.placement_api_client.associate_aggregates(RESOURCE_PROVIDER_UUID,
mock.ANY)
self.placement_fixture.mock_put.assert_called_once_with(
'/resource_providers/%s/aggregates' % RESOURCE_PROVIDER_UUID,
mock.ANY, headers=headers)
def test_list_aggregates(self):
headers = {'OpenStack-API-Version': self.openstack_api_version}
self.placement_api_client.list_aggregates(RESOURCE_PROVIDER_UUID)
self.placement_fixture.mock_get.assert_called_once_with(
'/resource_providers/%s/aggregates' % RESOURCE_PROVIDER_UUID,
headers=headers)
def test_list_aggregates_no_resource_provider(self):
self.placement_fixture.mock_get.side_effect = ks_exc.NotFound()
self.assertRaises(n_exc.PlacementAggregateNotFound,
self.placement_api_client.list_aggregates,
RESOURCE_PROVIDER_UUID)

View File

@ -19,6 +19,7 @@ from oslotest import base
from neutron_lib.api import attributes
from neutron_lib.api.definitions import port
from neutron_lib.callbacks import registry
from neutron_lib.clients import placement
from neutron_lib.db import model_base
from neutron_lib import fixture
from neutron_lib.plugins import directory
@ -122,3 +123,32 @@ class APIDefinitionFixtureTestCase(base.BaseTestCase):
self.assertNotIn('test_attr',
port.RESOURCE_ATTRIBUTE_MAP[port.COLLECTION_NAME])
self.assertNotIn('test_attr', api_def_ref[port.COLLECTION_NAME])
class PlacementAPIClientFixtureTestCase(base.BaseTestCase):
def _create_client_and_fixture(self):
placement_client = placement.PlacementAPIClient(mock.Mock())
placement_fixture = self.useFixture(
fixture.PlacementAPIClientFixture(placement_client))
return placement_client, placement_fixture
def test_post(self):
p_client, p_fixture = self._create_client_and_fixture()
p_client.create_resource_provider('resource')
p_fixture.mock_post.assert_called_once()
def test_put(self):
p_client, p_fixture = self._create_client_and_fixture()
p_client.update_inventory('resource', mock.ANY, 'class_name')
p_fixture.mock_put.assert_called_once()
def test_delete(self):
p_client, p_fixture = self._create_client_and_fixture()
p_client.delete_resource_provider('resource')
p_fixture.mock_delete.assert_called_once()
def test_get(self):
p_client, p_fixture = self._create_client_and_fixture()
p_client.list_aggregates('resource')
p_fixture.mock_get.assert_called_once()

View File

@ -0,0 +1,6 @@
---
features:
- The neutron placement API client is now available as
``neutron_lib.clients.placement``.
- A new fixture for testing placement API calls has been added as
``neutron_lib.fixtures.PlacementAPIClientFixture``.

View File

@ -6,6 +6,7 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0
SQLAlchemy!=1.1.5,!=1.1.6,!=1.1.7,!=1.1.8,>=1.0.10 # MIT
debtcollector>=1.2.0 # Apache-2.0
keystoneauth1>=3.3.0 # Apache-2.0
stevedore>=1.20.0 # Apache-2.0
oslo.concurrency>=3.20.0 # Apache-2.0
oslo.config>=4.6.0 # Apache-2.0