Merge "Add a ReST client for placement API"
This commit is contained in:
commit
96a51ad742
neutron
common
conf
services/segments
tests/unit/extensions
releasenotes/notes
@ -72,6 +72,13 @@ ks_loading.register_session_conf_options(cfg.CONF, NOVA_CONF_SECTION)
|
|||||||
# Register the nova configuration options
|
# Register the nova configuration options
|
||||||
common_config.register_nova_opts()
|
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)
|
logging.register_options(cfg.CONF)
|
||||||
|
|
||||||
|
|
||||||
|
@ -41,6 +41,20 @@ class NetworkQosBindingNotFound(e.NotFound):
|
|||||||
"could not be found.")
|
"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):
|
class PolicyRemoveAuthorizationError(e.NotAuthorized):
|
||||||
message = _("Failed to remove provided policy %(policy_id)s "
|
message = _("Failed to remove provided policy %(policy_id)s "
|
||||||
"because you are not authorized.")
|
"because you are not authorized.")
|
||||||
@ -103,6 +117,11 @@ class OverlappingAllocationPools(e.Conflict):
|
|||||||
"%(pool_1)s %(pool_2)s for subnet %(subnet_cidr)s.")
|
"%(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):
|
class OutOfBoundsAllocationPool(e.BadRequest):
|
||||||
message = _("The allocation pool %(pool)s spans "
|
message = _("The allocation pool %(pool)s spans "
|
||||||
"beyond the subnet cidr %(subnet_cidr)s.")
|
"beyond the subnet cidr %(subnet_cidr)s.")
|
||||||
|
@ -164,3 +164,22 @@ nova_opts = [
|
|||||||
|
|
||||||
def register_nova_opts(cfg=cfg.CONF):
|
def register_nova_opts(cfg=cfg.CONF):
|
||||||
cfg.register_opts(nova_opts, group=NOVA_CONF_SECTION)
|
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
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from keystoneauth1 import exceptions as ks_exc
|
||||||
import mock
|
import mock
|
||||||
import netaddr
|
import netaddr
|
||||||
from neutron_lib import constants
|
from neutron_lib import constants
|
||||||
from neutron_lib import exceptions as n_exc
|
from neutron_lib import exceptions as n_exc
|
||||||
|
from oslo_config import cfg
|
||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
import webob.exc
|
import webob.exc
|
||||||
|
|
||||||
@ -24,6 +26,7 @@ from neutron.callbacks import events
|
|||||||
from neutron.callbacks import exceptions
|
from neutron.callbacks import exceptions
|
||||||
from neutron.callbacks import registry
|
from neutron.callbacks import registry
|
||||||
from neutron.callbacks import resources
|
from neutron.callbacks import resources
|
||||||
|
from neutron.common import exceptions as neutron_exc
|
||||||
from neutron.conf.plugins.ml2.drivers import driver_type
|
from neutron.conf.plugins.ml2.drivers import driver_type
|
||||||
from neutron import context
|
from neutron import context
|
||||||
from neutron.db import agents_db
|
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.plugins.ml2 import config
|
||||||
from neutron.services.segments import db
|
from neutron.services.segments import db
|
||||||
from neutron.services.segments import exceptions as segment_exc
|
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.common import helpers
|
||||||
from neutron.tests.unit.db import test_db_base_plugin_v2
|
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]
|
agent_hosts = [agent['host'] for agent in dhcp_agents]
|
||||||
self.assertIn(DHCP_HOSTA, agent_hosts)
|
self.assertIn(DHCP_HOSTA, agent_hosts)
|
||||||
self.assertIn(DHCP_HOSTB, 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…
x
Reference in New Issue
Block a user