From 1afeeb95d3d23a4ac4ffeeccd7614ebade7cb381 Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Mon, 10 Dec 2018 12:24:48 -0800 Subject: [PATCH] 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 --- api-ref/source/parameters.yaml | 24 +++++ .../provider-flavor-capability-show-curl | 1 + ...vider-flavor-capability-show-response.json | 8 ++ api-ref/source/v2/provider.inc | 54 +++++++++++ octavia/api/v2/controllers/provider.py | 48 ++++++++++ octavia/api/v2/types/provider.py | 4 + octavia/common/constants.py | 1 + octavia/policies/__init__.py | 2 + octavia/policies/provider_flavor.py | 31 +++++++ octavia/tests/functional/api/v2/base.py | 2 + .../tests/functional/api/v2/test_provider.py | 89 +++++++++++++++++++ 11 files changed, 264 insertions(+) create mode 100644 api-ref/source/v2/examples/provider-flavor-capability-show-curl create mode 100644 api-ref/source/v2/examples/provider-flavor-capability-show-response.json create mode 100644 octavia/policies/provider_flavor.py diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 2e29f94507..9c4d106d9b 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -67,6 +67,12 @@ path-project-id: in: path required: true type: string +path-provider: + description: | + The provider to query. + in: path + required: true + type: string ############################################################################### # Query fields ############################################################################### @@ -335,6 +341,24 @@ flavor: in: body required: true 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: description: | The JSON string containing the flavor metadata. diff --git a/api-ref/source/v2/examples/provider-flavor-capability-show-curl b/api-ref/source/v2/examples/provider-flavor-capability-show-curl new file mode 100644 index 0000000000..79f076add4 --- /dev/null +++ b/api-ref/source/v2/examples/provider-flavor-capability-show-curl @@ -0,0 +1 @@ +curl -X GET -H "X-Auth-Token: " http://198.51.100.10:9876/v2/lbaas/providers/amphora/flavor_capabilities diff --git a/api-ref/source/v2/examples/provider-flavor-capability-show-response.json b/api-ref/source/v2/examples/provider-flavor-capability-show-response.json new file mode 100644 index 0000000000..5fae3c4e59 --- /dev/null +++ b/api-ref/source/v2/examples/provider-flavor-capability-show-response.json @@ -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." + } + ] +} diff --git a/api-ref/source/v2/provider.inc b/api-ref/source/v2/provider.inc index 95389dc3d0..5a520b255c 100644 --- a/api-ref/source/v2/provider.inc +++ b/api-ref/source/v2/provider.inc @@ -49,3 +49,57 @@ Response Example .. literalinclude:: examples/provider-list-response.json :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 diff --git a/octavia/api/v2/controllers/provider.py b/octavia/api/v2/controllers/provider.py index 4ff3c133f7..1d1d30cab2 100644 --- a/octavia/api/v2/controllers/provider.py +++ b/octavia/api/v2/controllers/provider.py @@ -13,16 +13,21 @@ # under the License. from oslo_config import cfg +from oslo_log import log as logging import pecan import six from wsme import types as wtypes 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.types import provider as provider_types from octavia.common import constants +from octavia.common import exceptions CONF = cfg.CONF +LOG = logging.getLogger(__name__) class ProviderController(base.BaseController): @@ -48,3 +53,46 @@ class ProviderController(base.BaseController): if fields is not None: response_list = self._filter_fields(response_list, fields) 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) diff --git a/octavia/api/v2/types/provider.py b/octavia/api/v2/types/provider.py index 95afa7f6b9..ea1e79880c 100644 --- a/octavia/api/v2/types/provider.py +++ b/octavia/api/v2/types/provider.py @@ -24,3 +24,7 @@ class ProviderResponse(types.BaseType): class ProvidersRootResponse(types.BaseType): providers = wtypes.wsattr([ProviderResponse]) + + +class FlavorCapabilitiesResponse(types.BaseType): + flavor_capabilities = wtypes.wsattr([ProviderResponse]) diff --git a/octavia/common/constants.py b/octavia/common/constants.py index 195d0dde9e..0f52a3c8bb 100644 --- a/octavia/common/constants.py +++ b/octavia/common/constants.py @@ -534,6 +534,7 @@ RBAC_L7RULE = '{}:l7rule:'.format(LOADBALANCER_API) RBAC_QUOTA = '{}:quota:'.format(LOADBALANCER_API) RBAC_AMPHORA = '{}:amphora:'.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_PROFILE = '{}:flavor-profile:'.format(LOADBALANCER_API) RBAC_POST = 'post' diff --git a/octavia/policies/__init__.py b/octavia/policies/__init__.py index a141253c8a..242770fc6b 100644 --- a/octavia/policies/__init__.py +++ b/octavia/policies/__init__.py @@ -25,6 +25,7 @@ from octavia.policies import loadbalancer from octavia.policies import member from octavia.policies import pool from octavia.policies import provider +from octavia.policies import provider_flavor from octavia.policies import quota @@ -43,4 +44,5 @@ def list_rules(): provider.list_rules(), quota.list_rules(), amphora.list_rules(), + provider_flavor.list_rules(), ) diff --git a/octavia/policies/provider_flavor.py b/octavia/policies/provider_flavor.py new file mode 100644 index 0000000000..858eacd9b7 --- /dev/null +++ b/octavia/policies/provider_flavor.py @@ -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 diff --git a/octavia/tests/functional/api/v2/base.py b/octavia/tests/functional/api/v2/base.py index 4dd3ed5a59..c229c98f1c 100644 --- a/octavia/tests/functional/api/v2/base.py +++ b/octavia/tests/functional/api/v2/base.py @@ -79,6 +79,8 @@ class BaseAPITest(base_db_test.OctaviaDBTestBase): AMPHORA_STATS_PATH = AMPHORA_PATH + '/stats' PROVIDERS_PATH = '/lbaas/providers' + FLAVOR_CAPABILITIES_PATH = (PROVIDERS_PATH + + '/{provider}/flavor_capabilities') NOT_AUTHORIZED_BODY = { 'debuginfo': None, 'faultcode': 'Client', diff --git a/octavia/tests/functional/api/v2/test_provider.py b/octavia/tests/functional/api/v2/test_provider.py index a340b3a716..107160c3a3 100644 --- a/octavia/tests/functional/api/v2/test_provider.py +++ b/octavia/tests/functional/api/v2/test_provider.py @@ -12,6 +12,16 @@ # License for the specific language governing permissions and limitations # 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 @@ -43,3 +53,82 @@ class TestProvider(base.BaseAPITest): self.assertTrue(octavia_dict in providers_list) self.assertTrue(amphora_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)