Add provider driver capabilities API

This patch adds an API that allows operators to query a provider driver
for the list of supported flavor capabilities.

Change-Id: Ia3d62acdc3b1af2e666f58d32a06d2238706dee6
This commit is contained in:
Michael Johnson
2018-12-10 12:24:48 -08:00
parent 0b1fe6a526
commit 1afeeb95d3
11 changed files with 264 additions and 0 deletions

View File

@@ -67,6 +67,12 @@ path-project-id:
in: path in: path
required: true required: true
type: string type: string
path-provider:
description: |
The provider to query.
in: path
required: true
type: string
############################################################################### ###############################################################################
# Query fields # Query fields
############################################################################### ###############################################################################
@@ -335,6 +341,24 @@ flavor:
in: body in: body
required: true required: true
type: object type: object
flavor-capabilities:
description: |
The provider flavor capabilities dictonary object.
in: body
required: true
type: object
flavor-capability-description:
description: |
The provider flavor capability description.
in: body
required: true
type: string
flavor-capability-name:
description: |
The provider flavor capability name.
in: body
required: true
type: string
flavor-data: flavor-data:
description: | description: |
The JSON string containing the flavor metadata. The JSON string containing the flavor metadata.

View File

@@ -0,0 +1 @@
curl -X GET -H "X-Auth-Token: <token>" http://198.51.100.10:9876/v2/lbaas/providers/amphora/flavor_capabilities

View File

@@ -0,0 +1,8 @@
{
"flavor_capabilities": [
{
"name": "loadbalancer_topology",
"description": "The load balancer topology. One of: SINGLE - One amphora per load balancer. ACTIVE_STANDBY - Two amphora per load balancer."
}
]
}

View File

@@ -49,3 +49,57 @@ Response Example
.. literalinclude:: examples/provider-list-response.json .. literalinclude:: examples/provider-list-response.json
:language: javascript :language: javascript
Show Provider Flavor Capabilities
=================================
.. rest_method:: GET /v2/lbaas/providers/{provider}/flavor_capabilities
Shows the provider driver flavor capabilities. These are the features of the
provider driver that can be configured in an Octavia flavor. This API returns
a list of dictionaries with the name and description of each flavor capability
of the provider.
The list might be empty and a provider driver may not implement this feature.
**New in version 2.6**
.. rest_status_code:: success ../http-status.yaml
- 200
.. rest_status_code:: error ../http-status.yaml
- 400
- 401
- 403
- 500
Request
-------
.. rest_parameters:: ../parameters.yaml
- fields: fields
- provider: path-provider
Curl Example
------------
.. literalinclude:: examples/provider-flavor-capability-show-curl
:language: bash
Response Parameters
-------------------
.. rest_parameters:: ../parameters.yaml
- flavor_capabilities: flavor-capabilities
- name: flavor-capability-name
- description: flavor-capability-description
Response Example
----------------
.. literalinclude:: examples/provider-flavor-capability-show-response.json
:language: javascript

View File

@@ -13,16 +13,21 @@
# under the License. # under the License.
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging
import pecan import pecan
import six import six
from wsme import types as wtypes from wsme import types as wtypes
from wsmeext import pecan as wsme_pecan from wsmeext import pecan as wsme_pecan
from octavia.api.drivers import driver_factory
from octavia.api.drivers import exceptions as driver_except
from octavia.api.v2.controllers import base from octavia.api.v2.controllers import base
from octavia.api.v2.types import provider as provider_types from octavia.api.v2.types import provider as provider_types
from octavia.common import constants from octavia.common import constants
from octavia.common import exceptions
CONF = cfg.CONF CONF = cfg.CONF
LOG = logging.getLogger(__name__)
class ProviderController(base.BaseController): class ProviderController(base.BaseController):
@@ -48,3 +53,46 @@ class ProviderController(base.BaseController):
if fields is not None: if fields is not None:
response_list = self._filter_fields(response_list, fields) response_list = self._filter_fields(response_list, fields)
return provider_types.ProvidersRootResponse(providers=response_list) return provider_types.ProvidersRootResponse(providers=response_list)
@pecan.expose()
def _lookup(self, provider, *remainder):
"""Overridden pecan _lookup method for custom routing.
Currently it checks if this was a flavor capabilities request and
routes the request to the FlavorCapabilitiesController.
"""
if provider and remainder and remainder[0] == 'flavor_capabilities':
return (FlavorCapabilitiesController(provider=provider),
remainder[1:])
return None
class FlavorCapabilitiesController(base.BaseController):
RBAC_TYPE = constants.RBAC_PROVIDER_FLAVOR
def __init__(self, provider):
super(FlavorCapabilitiesController, self).__init__()
self.provider = provider
@wsme_pecan.wsexpose(provider_types.FlavorCapabilitiesResponse,
[wtypes.text], ignore_extra_args=True,
status_code=200)
def get_all(self, fields=None):
context = pecan.request.context.get('octavia_context')
self._auth_validate_action(context, context.project_id,
constants.RBAC_GET_ALL)
self.driver = driver_factory.get_driver(self.provider)
try:
metadata_dict = self.driver.get_supported_flavor_metadata()
except driver_except.NotImplementedError as e:
LOG.warning('Provider %s get_supported_flavor_metadata() '
'reported: %s', self.provider, e.operator_fault_string)
raise exceptions.ProviderNotImplementedError(
prov=self.provider, user_msg=e.user_fault_string)
response_list = [
provider_types.ProviderResponse(name=key, description=value) for
key, value in six.iteritems(metadata_dict)]
if fields is not None:
response_list = self._filter_fields(response_list, fields)
return provider_types.FlavorCapabilitiesResponse(
flavor_capabilities=response_list)

View File

@@ -24,3 +24,7 @@ class ProviderResponse(types.BaseType):
class ProvidersRootResponse(types.BaseType): class ProvidersRootResponse(types.BaseType):
providers = wtypes.wsattr([ProviderResponse]) providers = wtypes.wsattr([ProviderResponse])
class FlavorCapabilitiesResponse(types.BaseType):
flavor_capabilities = wtypes.wsattr([ProviderResponse])

View File

@@ -534,6 +534,7 @@ RBAC_L7RULE = '{}:l7rule:'.format(LOADBALANCER_API)
RBAC_QUOTA = '{}:quota:'.format(LOADBALANCER_API) RBAC_QUOTA = '{}:quota:'.format(LOADBALANCER_API)
RBAC_AMPHORA = '{}:amphora:'.format(LOADBALANCER_API) RBAC_AMPHORA = '{}:amphora:'.format(LOADBALANCER_API)
RBAC_PROVIDER = '{}:provider:'.format(LOADBALANCER_API) RBAC_PROVIDER = '{}:provider:'.format(LOADBALANCER_API)
RBAC_PROVIDER_FLAVOR = '{}:provider-flavor:'.format(LOADBALANCER_API)
RBAC_FLAVOR = '{}:flavor:'.format(LOADBALANCER_API) RBAC_FLAVOR = '{}:flavor:'.format(LOADBALANCER_API)
RBAC_FLAVOR_PROFILE = '{}:flavor-profile:'.format(LOADBALANCER_API) RBAC_FLAVOR_PROFILE = '{}:flavor-profile:'.format(LOADBALANCER_API)
RBAC_POST = 'post' RBAC_POST = 'post'

View File

@@ -25,6 +25,7 @@ from octavia.policies import loadbalancer
from octavia.policies import member from octavia.policies import member
from octavia.policies import pool from octavia.policies import pool
from octavia.policies import provider from octavia.policies import provider
from octavia.policies import provider_flavor
from octavia.policies import quota from octavia.policies import quota
@@ -43,4 +44,5 @@ def list_rules():
provider.list_rules(), provider.list_rules(),
quota.list_rules(), quota.list_rules(),
amphora.list_rules(), amphora.list_rules(),
provider_flavor.list_rules(),
) )

View File

@@ -0,0 +1,31 @@
# Copyright 2018 Rackspace, US Inc.
# 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 oslo_policy import policy
from octavia.common import constants
rules = [
policy.DocumentedRuleDefault(
'{rbac_obj}{action}'.format(rbac_obj=constants.RBAC_PROVIDER_FLAVOR,
action=constants.RBAC_GET_ALL),
constants.RULE_API_ADMIN,
"List the provider flavor capabilities.",
[{'method': 'GET',
'path': '/v2/lbaas/providers/{provider}/capabilities'}]
),
]
def list_rules():
return rules

View File

@@ -79,6 +79,8 @@ class BaseAPITest(base_db_test.OctaviaDBTestBase):
AMPHORA_STATS_PATH = AMPHORA_PATH + '/stats' AMPHORA_STATS_PATH = AMPHORA_PATH + '/stats'
PROVIDERS_PATH = '/lbaas/providers' PROVIDERS_PATH = '/lbaas/providers'
FLAVOR_CAPABILITIES_PATH = (PROVIDERS_PATH +
'/{provider}/flavor_capabilities')
NOT_AUTHORIZED_BODY = { NOT_AUTHORIZED_BODY = {
'debuginfo': None, 'faultcode': 'Client', 'debuginfo': None, 'faultcode': 'Client',

View File

@@ -12,6 +12,16 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import mock
from oslo_config import cfg
from oslo_config import fixture as oslo_fixture
from oslo_utils import uuidutils
from octavia.api.drivers import exceptions
from octavia.common import constants
import octavia.common.context
from octavia.tests.functional.api.v2 import base from octavia.tests.functional.api.v2 import base
@@ -43,3 +53,82 @@ class TestProvider(base.BaseAPITest):
self.assertTrue(octavia_dict in providers_list) self.assertTrue(octavia_dict in providers_list)
self.assertTrue(amphora_dict in providers_list) self.assertTrue(amphora_dict in providers_list)
self.assertTrue(noop_dict in providers_list) self.assertTrue(noop_dict in providers_list)
class TestFlavorCapabilities(base.BaseAPITest):
root_tag = 'flavor_capabilities'
def setUp(self):
super(TestFlavorCapabilities, self).setUp()
def test_nonexistent_provider(self):
self.get(self.FLAVOR_CAPABILITIES_PATH.format(provider='bogus'),
status=400)
def test_noop_provider(self):
ref_capabilities = [{'description': 'The glance image tag to use for '
'this load balancer.', 'name': 'amp_image_tag'}]
result = self.get(
self.FLAVOR_CAPABILITIES_PATH.format(provider='noop_driver'))
self.assertEqual(ref_capabilities, result.json.get(self.root_tag))
def test_amphora_driver(self):
ref_description = ("The load balancer topology. One of: SINGLE - One "
"amphora per load balancer. ACTIVE_STANDBY - Two "
"amphora per load balancer.")
result = self.get(
self.FLAVOR_CAPABILITIES_PATH.format(provider='amphora'))
capabilities = result.json.get(self.root_tag)
capability_dict = [i for i in capabilities if
i['name'] == 'loadbalancer_topology'][0]
self.assertEqual(ref_description,
capability_dict['description'])
# Some drivers might not have implemented this yet, test that case
@mock.patch('octavia.api.drivers.noop_driver.driver.NoopProviderDriver.'
'get_supported_flavor_metadata')
def test_not_implemented(self, mock_get_metadata):
mock_get_metadata.side_effect = exceptions.NotImplementedError()
self.get(self.FLAVOR_CAPABILITIES_PATH.format(provider='noop_driver'),
status=501)
def test_authorized(self):
ref_capabilities = [{'description': 'The glance image tag to use '
'for this load balancer.',
'name': 'amp_image_tag'}]
self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF))
auth_strategy = self.conf.conf.api_settings.get('auth_strategy')
self.conf.config(group='api_settings', auth_strategy=constants.TESTING)
project_id = uuidutils.generate_uuid()
with mock.patch.object(octavia.common.context.Context, 'project_id',
project_id):
override_credentials = {
'service_user_id': None,
'user_domain_id': None,
'is_admin_project': True,
'service_project_domain_id': None,
'service_project_id': None,
'roles': ['load-balancer_member'],
'user_id': None,
'is_admin': True,
'service_user_domain_id': None,
'project_domain_id': None,
'service_roles': [],
'project_id': project_id}
with mock.patch(
"oslo_context.context.RequestContext.to_policy_values",
return_value=override_credentials):
result = self.get(self.FLAVOR_CAPABILITIES_PATH.format(
provider='noop_driver'))
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
self.assertEqual(ref_capabilities, result.json.get(self.root_tag))
def test_not_authorized(self):
self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF))
auth_strategy = self.conf.conf.api_settings.get('auth_strategy')
self.conf.config(group='api_settings', auth_strategy=constants.TESTING)
self.get(self.FLAVOR_CAPABILITIES_PATH.format(provider='noop_driver'),
status=403)
self.conf.config(group='api_settings', auth_strategy=auth_strategy)