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:
parent
ff086a43b4
commit
ebe62dcd33
@ -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)
|
||||
|
||||
|
||||
|
@ -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.")
|
||||
|
@ -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)
|
||||
|
163
neutron/services/segments/placement_client.py
Normal file
163
neutron/services/segments/placement_client.py
Normal 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)
|
@ -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)
|
||||
|
@ -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'.
|
Loading…
Reference in New Issue
Block a user