Merge "Add a ReST client for placement API"
This commit is contained in:
commit
96a51ad742
@ -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