Add a ReST client for placement API

This patchset adds a ReST client for the placement API. This
client is used to update the IPv4 inventories associated with
routed networks segments. This information is used by the
Nova scheduler to decide the placement of instances in hosts,
based on the availability of IPv4 addresses in routed
networks segments

DocImpact: Adds [placement] section to neutron.conf with two
           options: region_name and endpoint_type

Change-Id: I2aa614d4e6229161047b08c8bdcbca0e2e5d1f0b
Partially-Implements: blueprint routed-networks
This commit is contained in:
Miguel Lavalle 2016-12-24 18:38:57 -06:00
parent ff086a43b4
commit ebe62dcd33
6 changed files with 359 additions and 0 deletions

View File

@ -72,6 +72,13 @@ ks_loading.register_session_conf_options(cfg.CONF, NOVA_CONF_SECTION)
# Register the nova configuration options
common_config.register_nova_opts()
ks_loading.register_auth_conf_options(cfg.CONF,
common_config.PLACEMENT_CONF_SECTION)
# Register the placement configuration options
common_config.register_placement_opts()
logging.register_options(cfg.CONF)

View File

@ -41,6 +41,20 @@ class NetworkQosBindingNotFound(e.NotFound):
"could not be found.")
class PlacementResourceProviderNotFound(e.NotFound):
message = _("Placement resource provider not found %(resource_provider)s.")
class PlacementInventoryNotFound(e.NotFound):
message = _("Placement inventory not found for resource provider "
"%(resource_provider)s, resource class %(resource_class)s.")
class PlacementAggregateNotFound(e.NotFound):
message = _("Aggregate not found for resource provider "
"%(resource_provider)s.")
class PolicyRemoveAuthorizationError(e.NotAuthorized):
message = _("Failed to remove provided policy %(policy_id)s "
"because you are not authorized.")
@ -103,6 +117,11 @@ class OverlappingAllocationPools(e.Conflict):
"%(pool_1)s %(pool_2)s for subnet %(subnet_cidr)s.")
class PlacementInventoryUpdateConflict(e.Conflict):
message = _("Placement inventory update conflict for resource provider "
"%(resource_provider)s, resource class %(resource_class)s.")
class OutOfBoundsAllocationPool(e.BadRequest):
message = _("The allocation pool %(pool)s spans "
"beyond the subnet cidr %(subnet_cidr)s.")

View File

@ -164,3 +164,22 @@ nova_opts = [
def register_nova_opts(cfg=cfg.CONF):
cfg.register_opts(nova_opts, group=NOVA_CONF_SECTION)
PLACEMENT_CONF_SECTION = 'placement'
placement_opts = [
cfg.StrOpt('region_name',
help=_('Name of placement region to use. Useful if keystone '
'manages more than one region.')),
cfg.StrOpt('endpoint_type',
default='public',
choices=['public', 'admin', 'internal'],
help=_('Type of the placement endpoint to use. This endpoint '
'will be looked up in the keystone catalog and should '
'be one of public, internal or admin.')),
]
def register_placement_opts(cfg=cfg.CONF):
cfg.register_opts(placement_opts, group=PLACEMENT_CONF_SECTION)

View File

@ -0,0 +1,163 @@
# 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.
from keystoneauth1 import exceptions as ks_exc
from keystoneauth1 import loading as ks_loading
from keystoneauth1 import session
from oslo_config import cfg
from oslo_log import log as logging
from neutron._i18n import _
from neutron.common import exceptions as n_exc
LOG = logging.getLogger(__name__)
PLACEMENT_API_WITH_AGGREGATES = 'placement 1.1'
class PlacementAPIClient(object):
"""Client class for placement ReST API."""
ks_filter = {'service_type': 'placement',
'region_name': cfg.CONF.placement.region_name}
def __init__(self):
auth_plugin = ks_loading.load_auth_from_conf_options(
cfg.CONF, 'placement')
self._client = session.Session(auth=auth_plugin)
self._disabled = False
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)
def create_resource_provider(self, resource_provider):
"""Create a resource provider.
:param resource_provider: The resource provider
:type resource_provider: dict: name (required), uuid (required)
"""
url = '/resource_providers'
self._post(url, resource_provider)
def delete_resource_provider(self, resource_provider_uuid):
"""Delete a resource provider.
:param resource_provider_uuid: UUID of the resource provider
:type resource_provider_uuid: str
"""
url = '/resource_providers/%s' % resource_provider_uuid
self._delete(url)
def create_inventory(self, resource_provider_uuid, inventory):
"""Create an inventory.
:param resource_provider_uuid: UUID of the resource provider
:type resource_provider_uuid: str
:param inventory: The inventory
:type inventory: dict: resource_class (required), total (required),
reserved (required), min_unit (required), max_unit (required),
step_size (required), allocation_ratio (required)
"""
url = '/resource_providers/%s/inventories' % resource_provider_uuid
self._post(url, inventory)
def get_inventory(self, resource_provider_uuid, resource_class):
"""Get resource provider inventory.
:param resource_provider_uuid: UUID of the resource provider
:type resource_provider_uuid: str
:param resource_class: Resource class name of the inventory to be
returned
:type resource_class: str
:raises n_exc.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
def update_inventory(self, resource_provider_uuid, inventory,
resource_class):
"""Update an inventory.
:param resource_provider_uuid: UUID of the resource provider
:type resource_provider_uuid: str
:param inventory: The inventory
:type inventory: dict
:param resource_class: The resource class of the inventory to update
:type resource_class: str
:raises n_exc.PlacementInventoryUpdateConflict: For failure to updste
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)
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
:type resource_provider_uuid: str
:param aggregates: aggregates to be associated to the resource provider
:type aggregates: list of UUIDs
"""
url = '/resource_providers/%s/aggregates' % resource_provider_uuid
self._put(url, aggregates,
headers={'openstack-api-version':
PLACEMENT_API_WITH_AGGREGATES})
def list_aggregates(self, resource_provider_uuid):
"""List resource provider aggregates.
:param resource_provider_uuid: UUID of the resource provider
:type resource_provider_uuid: str
"""
url = '/resource_providers/%s/aggregates' % resource_provider_uuid
try:
return self._get(
url, headers={'openstack-api-version':
PLACEMENT_API_WITH_AGGREGATES}).json()
except ks_exc.NotFound:
raise n_exc.PlacementAggregateNotFound(
resource_provider=resource_provider_uuid)

View File

@ -12,10 +12,12 @@
# License for the specific language governing permissions and limitations
# under the License.
from keystoneauth1 import exceptions as ks_exc
import mock
import netaddr
from neutron_lib import constants
from neutron_lib import exceptions as n_exc
from oslo_config import cfg
from oslo_utils import uuidutils
import webob.exc
@ -24,6 +26,7 @@ from neutron.callbacks import events
from neutron.callbacks import exceptions
from neutron.callbacks import registry
from neutron.callbacks import resources
from neutron.common import exceptions as neutron_exc
from neutron.conf.plugins.ml2.drivers import driver_type
from neutron import context
from neutron.db import agents_db
@ -40,6 +43,8 @@ from neutron.plugins.common import constants as p_constants
from neutron.plugins.ml2 import config
from neutron.services.segments import db
from neutron.services.segments import exceptions as segment_exc
from neutron.services.segments import placement_client
from neutron.tests import base
from neutron.tests.common import helpers
from neutron.tests.unit.db import test_db_base_plugin_v2
@ -1476,3 +1481,135 @@ class TestDhcpAgentSegmentScheduling(HostSegmentMappingTestCase):
agent_hosts = [agent['host'] for agent in dhcp_agents]
self.assertIn(DHCP_HOSTA, agent_hosts)
self.assertIn(DHCP_HOSTB, agent_hosts)
class PlacementAPIClientTestCase(base.DietTestCase):
"""Test the Placement API client."""
def setUp(self):
super(PlacementAPIClientTestCase, self).setUp()
self.mock_load_auth_p = mock.patch(
'keystoneauth1.loading.load_auth_from_conf_options')
self.mock_load_auth = self.mock_load_auth_p.start()
self.mock_request_p = mock.patch(
'keystoneauth1.session.Session.request')
self.mock_request = self.mock_request_p.start()
self.client = placement_client.PlacementAPIClient()
@mock.patch('keystoneauth1.session.Session')
@mock.patch('keystoneauth1.loading.load_auth_from_conf_options')
def test_constructor(self, load_auth_mock, ks_sess_mock):
placement_client.PlacementAPIClient()
load_auth_mock.assert_called_once_with(cfg.CONF, 'placement')
ks_sess_mock.assert_called_once_with(auth=load_auth_mock.return_value)
def test_create_resource_provider(self):
expected_payload = 'fake_resource_provider'
self.client.create_resource_provider(expected_payload)
expected_url = '/resource_providers'
self.mock_request.assert_called_once_with(
expected_url, 'POST',
endpoint_filter={'region_name': mock.ANY,
'service_type': 'placement'},
json=expected_payload)
def test_delete_resource_provider(self):
rp_uuid = uuidutils.generate_uuid()
self.client.delete_resource_provider(rp_uuid)
expected_url = '/resource_providers/%s' % rp_uuid
self.mock_request.assert_called_once_with(
expected_url, 'DELETE',
endpoint_filter={'region_name': mock.ANY,
'service_type': 'placement'})
def test_create_inventory(self):
expected_payload = 'fake_inventory'
rp_uuid = uuidutils.generate_uuid()
self.client.create_inventory(rp_uuid, expected_payload)
expected_url = '/resource_providers/%s/inventories' % rp_uuid
self.mock_request.assert_called_once_with(
expected_url, 'POST',
endpoint_filter={'region_name': mock.ANY,
'service_type': 'placement'},
json=expected_payload)
def test_get_inventory(self):
rp_uuid = uuidutils.generate_uuid()
resource_class = 'fake_resource_class'
self.client.get_inventory(rp_uuid, resource_class)
expected_url = '/resource_providers/%s/inventories/%s' % (
rp_uuid, resource_class)
self.mock_request.assert_called_once_with(
expected_url, 'GET',
endpoint_filter={'region_name': mock.ANY,
'service_type': 'placement'})
def _test_get_inventory_not_found(self, details, expected_exception):
rp_uuid = uuidutils.generate_uuid()
resource_class = 'fake_resource_class'
self.mock_request.side_effect = ks_exc.NotFound(details=details)
self.assertRaises(expected_exception, self.client.get_inventory,
rp_uuid, resource_class)
def test_get_inventory_not_found_no_resource_provider(self):
self._test_get_inventory_not_found(
"No resource provider with uuid",
neutron_exc.PlacementResourceProviderNotFound)
def test_get_inventory_not_found_no_inventory(self):
self._test_get_inventory_not_found(
"No inventory of class", neutron_exc.PlacementInventoryNotFound)
def test_get_inventory_not_found_unknown_cause(self):
self._test_get_inventory_not_found("Unknown cause", ks_exc.NotFound)
def test_update_inventory(self):
expected_payload = 'fake_inventory'
rp_uuid = uuidutils.generate_uuid()
resource_class = 'fake_resource_class'
self.client.update_inventory(rp_uuid, expected_payload, resource_class)
expected_url = '/resource_providers/%s/inventories/%s' % (
rp_uuid, resource_class)
self.mock_request.assert_called_once_with(
expected_url, 'PUT',
endpoint_filter={'region_name': mock.ANY,
'service_type': 'placement'},
json=expected_payload)
def test_update_inventory_conflict(self):
rp_uuid = uuidutils.generate_uuid()
expected_payload = 'fake_inventory'
resource_class = 'fake_resource_class'
self.mock_request.side_effect = ks_exc.Conflict
self.assertRaises(neutron_exc.PlacementInventoryUpdateConflict,
self.client.update_inventory, rp_uuid,
expected_payload, resource_class)
def test_associate_aggregates(self):
expected_payload = 'fake_aggregates'
rp_uuid = uuidutils.generate_uuid()
self.client.associate_aggregates(rp_uuid, expected_payload)
expected_url = '/resource_providers/%s/aggregates' % rp_uuid
self.mock_request.assert_called_once_with(
expected_url, 'PUT',
endpoint_filter={'region_name': mock.ANY,
'service_type': 'placement'},
json=expected_payload,
headers={'openstack-api-version': 'placement 1.1'})
def test_list_aggregates(self):
rp_uuid = uuidutils.generate_uuid()
self.client.list_aggregates(rp_uuid)
expected_url = '/resource_providers/%s/aggregates' % rp_uuid
self.mock_request.assert_called_once_with(
expected_url, 'GET',
endpoint_filter={'region_name': mock.ANY,
'service_type': 'placement'},
headers={'openstack-api-version': 'placement 1.1'})
def test_list_aggregates_not_found(self):
rp_uuid = uuidutils.generate_uuid()
self.mock_request.side_effect = ks_exc.NotFound
self.assertRaises(neutron_exc.PlacementAggregateNotFound,
self.client.list_aggregates, rp_uuid)

View File

@ -0,0 +1,14 @@
---
prelude: >
Add configuration options to enable the segments plugin to use the
placement ReST API. This API enables the segments plugin to influence
the placement of instances based on the availability of IPv4 addresses
in routed networks.
features:
- A new section is added to neutron.conf, `[placement]`.
- The `[placement]` section has two new options.
- First option, `region_name`, indicates the placement region to use. This
option is useful if keystone manages more than one region.
- Second option, `endpoint_type`, indicates the type of the placement
endpoint to use. This endpoint will be looked up in the keystone catalog
and should be one of 'public', 'internal' or 'admin'.