From 0b1fe6a5266ef5869dc54d16feabef5741cc95cb Mon Sep 17 00:00:00 2001 From: Michael Johnson Date: Fri, 30 Nov 2018 17:55:03 -0800 Subject: [PATCH] Adds flavor support to the amphora driver This patch adds support for flavor metadata validation by the amphora driver and support for setting the load balancer topology via a flavor. It also adds "flavor_id" to the load balancer table in the database. Change-Id: I8eae870abdb20dc32917957e32606deef387ec88 --- octavia/api/drivers/amphora_driver/driver.py | 53 +++++++++ .../drivers/amphora_driver/flavor_schema.py | 44 +++++++ octavia/api/drivers/utils.py | 7 +- octavia/api/v2/controllers/flavors.py | 4 +- octavia/api/v2/controllers/load_balancer.py | 83 ++++++++++++- octavia/api/v2/types/load_balancer.py | 9 +- octavia/common/constants.py | 10 +- octavia/common/data_models.py | 4 +- octavia/common/exceptions.py | 5 + .../controller/worker/controller_worker.py | 4 +- .../211982b05afc_add_flavor_id_to_lb.py | 32 +++++ octavia/db/models.py | 3 + octavia/db/prepare.py | 7 ++ octavia/db/repositories.py | 33 +++++- .../drivers/neutron/allowed_address_pairs.py | 3 +- .../tests/functional/api/v2/test_flavors.py | 17 +++ .../functional/api/v2/test_load_balancer.py | 109 +++++++++++++++++- .../tests/functional/db/test_repositories.py | 48 +++++++- .../amphora_driver/test_amphora_driver.py | 44 +++++++ octavia/tests/unit/api/drivers/test_utils.py | 1 - .../worker/tasks/test_network_tasks.py | 11 +- .../worker/test_controller_worker.py | 12 +- requirements.txt | 1 + 23 files changed, 496 insertions(+), 48 deletions(-) create mode 100644 octavia/api/drivers/amphora_driver/flavor_schema.py create mode 100644 octavia/db/migration/alembic_migrations/versions/211982b05afc_add_flavor_id_to_lb.py diff --git a/octavia/api/drivers/amphora_driver/driver.py b/octavia/api/drivers/amphora_driver/driver.py index c9386ac4af..0a25d0a72c 100644 --- a/octavia/api/drivers/amphora_driver/driver.py +++ b/octavia/api/drivers/amphora_driver/driver.py @@ -12,10 +12,14 @@ # License for the specific language governing permissions and limitations # under the License. +from jsonschema import exceptions as js_exceptions +from jsonschema import validate + from oslo_config import cfg from oslo_log import log as logging import oslo_messaging as messaging +from octavia.api.drivers.amphora_driver import flavor_schema from octavia.api.drivers import exceptions from octavia.api.drivers import provider_base as driver_base from octavia.api.drivers import utils as driver_utils @@ -252,3 +256,52 @@ class AmphoraProviderDriver(driver_base.ProviderDriver): payload = {consts.L7RULE_ID: l7rule_id, consts.L7RULE_UPDATES: l7rule_dict} self.client.cast({}, 'update_l7rule', **payload) + + # Flavor + def get_supported_flavor_metadata(self): + """Returns the valid flavor metadata keys and descriptions. + + This extracts the valid flavor metadata keys and descriptions + from the JSON validation schema and returns it as a dictionary. + + :return: Dictionary of flavor metadata keys and descriptions. + :raises DriverError: An unexpected error occurred. + """ + try: + props = flavor_schema.SUPPORTED_FLAVOR_SCHEMA['properties'] + return {k: v.get('description', '') for k, v in props.items()} + except Exception as e: + raise exceptions.DriverError( + user_fault_string='Failed to get the supported flavor ' + 'metadata due to: {}'.format(str(e)), + operator_fault_string='Failed to get the supported flavor ' + 'metadata due to: {}'.format(str(e))) + + def validate_flavor(self, flavor_dict): + """Validates flavor profile data. + + This will validate a flavor profile dataset against the flavor + settings the amphora driver supports. + + :param flavor_dict: The flavor dictionary to validate. + :type flavor: dict + :return: None + :raises DriverError: An unexpected error occurred. + :raises UnsupportedOptionError: If the driver does not support + one of the flavor settings. + """ + try: + validate(flavor_dict, flavor_schema.SUPPORTED_FLAVOR_SCHEMA) + except js_exceptions.ValidationError as e: + error_object = '' + if e.relative_path: + error_object = '{} '.format(e.relative_path[0]) + raise exceptions.UnsupportedOptionError( + user_fault_string='{0}{1}'.format(error_object, e.message), + operator_fault_string=str(e)) + except Exception as e: + raise exceptions.DriverError( + user_fault_string='Failed to validate the flavor metadata ' + 'due to: {}'.format(str(e)), + operator_fault_string='Failed to validate the flavor metadata ' + 'due to: {}'.format(str(e))) diff --git a/octavia/api/drivers/amphora_driver/flavor_schema.py b/octavia/api/drivers/amphora_driver/flavor_schema.py new file mode 100644 index 0000000000..9755bf6a3a --- /dev/null +++ b/octavia/api/drivers/amphora_driver/flavor_schema.py @@ -0,0 +1,44 @@ +# Copyright 2018 Rackspace US Inc. 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 octavia.common import constants as consts + +# This is a JSON schema validation dictionary +# https://json-schema.org/latest/json-schema-validation.html +# +# Note: This is used to generate the amphora driver "supported flavor +# metadata" dictionary. Each property should include a description +# for the user to understand what this flavor setting does. +# +# Where possible, the property name should match the configuration file name +# for the setting. The configuration file setting is the default when a +# setting is not defined in a flavor profile. + +SUPPORTED_FLAVOR_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Octavia Amphora Driver Flavor Metadata Schema", + "description": "This schema is used to validate new flavor profiles " + "submitted for use in an amphora driver flavor profile.", + "type": "object", + "additionalProperties": False, + "properties": { + consts.LOADBALANCER_TOPOLOGY: { + "type": "string", + "description": "The load balancer topology. One of: " + "SINGLE - One amphora per load balancer. " + "ACTIVE_STANDBY - Two amphora per load balancer.", + "enum": list(consts.SUPPORTED_LB_TOPOLOGIES) + } + } +} diff --git a/octavia/api/drivers/utils.py b/octavia/api/drivers/utils.py index 0a3f814727..e3c55dbc7f 100644 --- a/octavia/api/drivers/utils.py +++ b/octavia/api/drivers/utils.py @@ -88,6 +88,10 @@ def _base_to_provider_dict(current_dict, include_project_id=False): del new_dict['tenant_id'] if 'tags' in new_dict: del new_dict['tags'] + if 'flavor_id' in new_dict: + del new_dict['flavor_id'] + if 'topology' in new_dict: + del new_dict['topology'] return new_dict @@ -120,7 +124,8 @@ def db_loadbalancer_to_provider_loadbalancer(db_loadbalancer): db_listeners=db_loadbalancer.listeners) for unsupported_field in ['server_group_id', 'amphorae', 'vrrp_group', 'topology', 'vip']: - del new_loadbalancer_dict[unsupported_field] + if unsupported_field in new_loadbalancer_dict: + del new_loadbalancer_dict[unsupported_field] provider_loadbalancer = driver_dm.LoadBalancer.from_dict( new_loadbalancer_dict) return provider_loadbalancer diff --git a/octavia/api/v2/controllers/flavors.py b/octavia/api/v2/controllers/flavors.py index 7058edba6d..da02f502c3 100644 --- a/octavia/api/v2/controllers/flavors.py +++ b/octavia/api/v2/controllers/flavors.py @@ -133,12 +133,10 @@ class FlavorsController(base.BaseController): self._auth_validate_action(context, context.project_id, constants.RBAC_DELETE) - try: self.repositories.flavor.delete(context.session, id=flavor_id) # Handle when load balancers still reference this flavor except odb_exceptions.DBReferenceError: raise exceptions.ObjectInUse(object='Flavor', id=flavor_id) except sa_exception.NoResultFound: - raise exceptions.NotFound(resource='Flavor', - id=flavor_id) + raise exceptions.NotFound(resource='Flavor', id=flavor_id) diff --git a/octavia/api/v2/controllers/load_balancer.py b/octavia/api/v2/controllers/load_balancer.py index 31d9cf086a..e28f701530 100644 --- a/octavia/api/v2/controllers/load_balancer.py +++ b/octavia/api/v2/controllers/load_balancer.py @@ -19,6 +19,7 @@ from oslo_log import log as logging from oslo_utils import excutils from oslo_utils import strutils import pecan +from sqlalchemy.orm import exc as sa_exception from wsme import types as wtypes from wsmeext import pecan as wsme_pecan @@ -236,6 +237,71 @@ class LoadBalancersController(base.BaseController): e.msg = e.orig_msg raise e + def _get_provider(self, session, load_balancer): + """Decide on the provider for this load balancer.""" + + provider = None + if not isinstance(load_balancer.flavor_id, wtypes.UnsetType): + try: + provider = self.repositories.flavor.get_flavor_provider( + session, load_balancer.flavor_id) + except sa_exception.NoResultFound: + raise exceptions.ValidationException( + detail=_("Invalid flavor_id.")) + + # No provider specified and no flavor specified, use conf default + if (isinstance(load_balancer.provider, wtypes.UnsetType) and + not provider): + provider = CONF.api_settings.default_provider_driver + # Both provider and flavor specified, they must match + elif (not isinstance(load_balancer.provider, wtypes.UnsetType) and + provider): + if provider != load_balancer.provider: + raise exceptions.ProviderFlavorMismatchError( + flav=load_balancer.flavor_id, prov=load_balancer.provider) + # No flavor, but provider, use the provider specified + elif not provider: + provider = load_balancer.provider + # Otherwise, use the flavor provider we found above + + return provider + + def _apply_flavor_to_lb_dict(self, lock_session, driver, lb_dict): + + flavor_dict = {} + if 'flavor_id' in lb_dict: + try: + flavor_dict = ( + self.repositories.flavor.get_flavor_metadata_dict( + lock_session, lb_dict['flavor_id'])) + except sa_exception.NoResultFound: + raise exceptions.ValidationException( + detail=_("Invalid flavor_id.")) + + # Make sure the driver will still accept the flavor metadata + if flavor_dict: + driver_utils.call_provider(driver.name, driver.validate_flavor, + flavor_dict) + + # Apply the flavor settings to the load balanacer + # Use the configuration file settings as defaults + lb_dict[constants.TOPOLOGY] = flavor_dict.get( + constants.LOADBALANCER_TOPOLOGY, + CONF.controller_worker.loadbalancer_topology) + + return flavor_dict + + def _validate_flavor(self, session, load_balancer): + if not isinstance(load_balancer.flavor_id, wtypes.UnsetType): + flavor = self.repositories.flavor.get(session, + id=load_balancer.flavor_id) + if not flavor: + raise exceptions.ValidationException( + detail=_("Invalid flavor_id.")) + if not flavor.enabled: + raise exceptions.DisabledOption(option='flavor', + value=load_balancer.flavor_id) + @wsme_pecan.wsexpose(lb_types.LoadBalancerFullRootResponse, body=lb_types.LoadBalancerRootPOST, status_code=201) def post(self, load_balancer): @@ -255,8 +321,12 @@ class LoadBalancersController(base.BaseController): self._validate_vip_request_object(load_balancer) + self._validate_flavor(context.session, load_balancer) + + provider = self._get_provider(context.session, load_balancer) + # Load the driver early as it also provides validation - driver = driver_factory.get_driver(load_balancer.provider) + driver = driver_factory.get_driver(provider) lock_session = db_api.get_session(autocommit=False) try: @@ -282,14 +352,17 @@ class LoadBalancersController(base.BaseController): listeners = lb_dict.pop('listeners', []) or [] pools = lb_dict.pop('pools', []) or [] - # TODO(johnsom) Remove flavor from the lb_dict - # as it has not been implemented beyond the API yet. - # Remove this line when it is implemented. - lb_dict.pop('flavor', None) + flavor_dict = self._apply_flavor_to_lb_dict(lock_session, driver, + lb_dict) db_lb = self.repositories.create_load_balancer_and_vip( lock_session, lb_dict, vip_dict) + # Pass the flavor dictionary through for the provider drivers + # This is a "virtual" lb_dict item that includes the expanded + # flavor dict instead of just the flavor_id we store in the DB. + lb_dict['flavor'] = flavor_dict + # See if the provider driver wants to create the VIP port octavia_owned = False try: diff --git a/octavia/api/v2/types/load_balancer.py b/octavia/api/v2/types/load_balancer.py index 2a66b72d30..64c59f53ec 100644 --- a/octavia/api/v2/types/load_balancer.py +++ b/octavia/api/v2/types/load_balancer.py @@ -17,7 +17,6 @@ from wsme import types as wtypes from octavia.api.common import types from octavia.api.v2.types import listener from octavia.api.v2.types import pool -from octavia.common import constants class BaseLoadBalancerType(types.BaseType): @@ -53,7 +52,7 @@ class LoadBalancerResponse(BaseLoadBalancerType): listeners = wtypes.wsattr([types.IdOnlyType]) pools = wtypes.wsattr([types.IdOnlyType]) provider = wtypes.wsattr(wtypes.StringType()) - flavor_id = wtypes.wsattr(wtypes.StringType()) + flavor_id = wtypes.wsattr(wtypes.UuidType()) vip_qos_policy_id = wtypes.wsattr(wtypes.UuidType()) tags = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType())) @@ -78,8 +77,6 @@ class LoadBalancerResponse(BaseLoadBalancerType): result.pools = [ pool_model.from_data_model(i) for i in data_model.pools] - if not result.flavor_id: - result.flavor_id = "" if not result.provider: result.provider = "octavia" @@ -123,10 +120,8 @@ class LoadBalancerPOST(BaseLoadBalancerType): listeners = wtypes.wsattr([listener.ListenerSingleCreate], default=[]) pools = wtypes.wsattr([pool.PoolSingleCreate], default=[]) provider = wtypes.wsattr(wtypes.StringType(max_length=64)) - # TODO(johnsom) This should be dynamic based on the loaded flavors - # once flavors are implemented. - flavor_id = wtypes.wsattr(wtypes.Enum(str, *constants.SUPPORTED_FLAVORS)) tags = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(max_length=255))) + flavor_id = wtypes.wsattr(wtypes.UuidType()) class LoadBalancerRootPOST(types.BaseType): diff --git a/octavia/common/constants.py b/octavia/common/constants.py index ff7b4889c9..195d0dde9e 100644 --- a/octavia/common/constants.py +++ b/octavia/common/constants.py @@ -239,7 +239,7 @@ AMPHORAE_NETWORK_CONFIG = 'amphorae_network_config' ADDED_PORTS = 'added_ports' PORTS = 'ports' MEMBER_PORTS = 'member_ports' -LOADBALANCER_TOPOLOGY = 'topology' +TOPOLOGY = 'topology' HEALTHMONITORS = 'healthmonitors' HEALTH_MONITOR_ID = 'health_monitor_id' L7POLICIES = 'l7policies' @@ -548,13 +548,8 @@ RBAC_GET_STATS = 'get_stats' RBAC_GET_STATUS = 'get_status' # PROVIDERS -# TODO(johnsom) When providers are implemented, this should be removed. OCTAVIA = 'octavia' -# FLAVORS -# TODO(johnsom) When flavors are implemented, this should be removed. -SUPPORTED_FLAVORS = () - # systemctl commands DISABLE = 'disable' ENABLE = 'enable' @@ -566,3 +561,6 @@ AMP_NETNS_SVC_PREFIX = 'amphora-netns' HTTP_REUSE = 'has_http_reuse' FLAVOR_DATA = 'flavor_data' + +# Flavor metadata +LOADBALANCER_TOPOLOGY = 'loadbalancer_topology' diff --git a/octavia/common/data_models.py b/octavia/common/data_models.py index eff8d668d1..e4ea43208c 100644 --- a/octavia/common/data_models.py +++ b/octavia/common/data_models.py @@ -437,7 +437,8 @@ class LoadBalancer(BaseDataModel): provisioning_status=None, operating_status=None, enabled=None, topology=None, vip=None, listeners=None, amphorae=None, pools=None, vrrp_group=None, server_group_id=None, - created_at=None, updated_at=None, provider=None, tags=None): + created_at=None, updated_at=None, provider=None, tags=None, + flavor_id=None): self.id = id self.project_id = project_id @@ -457,6 +458,7 @@ class LoadBalancer(BaseDataModel): self.updated_at = updated_at self.provider = provider self.tags = tags or [] + self.flavor_id = flavor_id def update(self, update_dict): for key, value in update_dict.items(): diff --git a/octavia/common/exceptions.py b/octavia/common/exceptions.py index 703a4906a8..5622821504 100644 --- a/octavia/common/exceptions.py +++ b/octavia/common/exceptions.py @@ -377,3 +377,8 @@ class InputFileError(OctaviaException): class ObjectInUse(APIException): msg = _("%(object)s %(id)s is in use and cannot be modified.") code = 409 + + +class ProviderFlavorMismatchError(APIException): + msg = _("Flavor '%(flav)s' is not compatible with provider '%(prov)s'") + code = 400 diff --git a/octavia/controller/worker/controller_worker.py b/octavia/controller/worker/controller_worker.py index 962e0af393..9209722870 100644 --- a/octavia/controller/worker/controller_worker.py +++ b/octavia/controller/worker/controller_worker.py @@ -341,10 +341,10 @@ class ControllerWorker(base_taskflow.BaseTaskFlowEngine): constants.BUILD_TYPE_PRIORITY: constants.LB_CREATE_NORMAL_PRIORITY} - topology = CONF.controller_worker.loadbalancer_topology + topology = lb.topology store[constants.UPDATE_DICT] = { - constants.LOADBALANCER_TOPOLOGY: topology + constants.TOPOLOGY: topology } create_lb_flow = self._lb_flows.get_create_load_balancer_flow( diff --git a/octavia/db/migration/alembic_migrations/versions/211982b05afc_add_flavor_id_to_lb.py b/octavia/db/migration/alembic_migrations/versions/211982b05afc_add_flavor_id_to_lb.py new file mode 100644 index 0000000000..7d4f03de47 --- /dev/null +++ b/octavia/db/migration/alembic_migrations/versions/211982b05afc_add_flavor_id_to_lb.py @@ -0,0 +1,32 @@ +# 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. +"""add_flavor_id_to_lb + +Revision ID: 211982b05afc +Revises: b9c703669314 +Create Date: 2018-11-30 14:57:28.559884 + +""" + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = '211982b05afc' +down_revision = 'b9c703669314' + + +def upgrade(): + op.add_column('load_balancer', + sa.Column('flavor_id', sa.String(36), nullable=True)) diff --git a/octavia/db/models.py b/octavia/db/models.py index 86bbe2a652..10e03d9b4a 100644 --- a/octavia/db/models.py +++ b/octavia/db/models.py @@ -389,6 +389,9 @@ class LoadBalancer(base_models.BASE, base_models.IdMixin, cascade='all,delete-orphan', primaryjoin='and_(foreign(Tags.resource_id)==LoadBalancer.id)' ) + flavor_id = sa.Column( + sa.String(36), + sa.ForeignKey("flavor.id", name="fk_lb_flavor_id"), nullable=True) class VRRPGroup(base_models.BASE): diff --git a/octavia/db/prepare.py b/octavia/db/prepare.py index 99844fb561..ccae760d04 100644 --- a/octavia/db/prepare.py +++ b/octavia/db/prepare.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_config import cfg from oslo_utils import uuidutils from octavia.api.v1.types import l7rule @@ -19,6 +20,8 @@ from octavia.common import constants from octavia.common import exceptions from octavia.common import validate +CONF = cfg.CONF + def create_load_balancer_tree(lb_dict): listeners = lb_dict.pop('listeners') or [] @@ -64,6 +67,10 @@ def create_load_balancer(lb_dict): lb_dict['vip']['load_balancer_id'] = lb_dict.get('id') lb_dict[constants.PROVISIONING_STATUS] = constants.PENDING_CREATE lb_dict[constants.OPERATING_STATUS] = constants.OFFLINE + + # Set defaults later possibly overriden by flavors later + lb_dict['topology'] = CONF.controller_worker.loadbalancer_topology + return lb_dict diff --git a/octavia/db/repositories.py b/octavia/db/repositories.py index 32f7ed60d0..46dfcf3d05 100644 --- a/octavia/db/repositories.py +++ b/octavia/db/repositories.py @@ -24,6 +24,7 @@ from oslo_config import cfg from oslo_db import api as oslo_db_api from oslo_db import exception as db_exception from oslo_log import log as logging +from oslo_serialization import jsonutils from oslo_utils import excutils from oslo_utils import uuidutils from sqlalchemy.orm import joinedload @@ -51,7 +52,18 @@ class BaseRepository(object): :param filters: Filters to decide which entities should be retrieved. :returns: int """ - return session.query(self.model_class).filter_by(**filters).count() + deleted = filters.pop('show_deleted', True) + model = session.query(self.model_class).filter_by(**filters) + + if not deleted: + if hasattr(self.model_class, 'status'): + model = model.filter( + self.model_class.status != consts.DELETED) + else: + model = model.filter( + self.model_class.provisioning_status != consts.DELETED) + + return model.count() def create(self, session, **model_kwargs): """Base create method for a database entity. @@ -1775,6 +1787,25 @@ class QuotasRepository(BaseRepository): class FlavorRepository(BaseRepository): model_class = models.Flavor + def get_flavor_metadata_dict(self, session, flavor_id): + with session.begin(subtransactions=True): + flavor_metadata_json = ( + session.query(models.FlavorProfile.flavor_data) + .filter(models.Flavor.id == flavor_id) + .filter( + models.Flavor.flavor_profile_id == models.FlavorProfile.id) + .one()[0]) + result_dict = ({} if flavor_metadata_json is None + else jsonutils.loads(flavor_metadata_json)) + return result_dict + + def get_flavor_provider(self, session, flavor_id): + with session.begin(subtransactions=True): + return (session.query(models.FlavorProfile.provider_name) + .filter(models.Flavor.id == flavor_id) + .filter(models.Flavor.flavor_profile_id == + models.FlavorProfile.id).one()[0]) + class FlavorProfileRepository(BaseRepository): model_class = models.FlavorProfile diff --git a/octavia/network/drivers/neutron/allowed_address_pairs.py b/octavia/network/drivers/neutron/allowed_address_pairs.py index aae3b5e26d..ec298ff2bd 100644 --- a/octavia/network/drivers/neutron/allowed_address_pairs.py +++ b/octavia/network/drivers/neutron/allowed_address_pairs.py @@ -185,8 +185,7 @@ class AllowedAddressPairsDriver(neutron_base.BaseNeutronDriver): # Currently we are using the VIP network for VRRP # so we need to open up the protocols for it - if (CONF.controller_worker.loadbalancer_topology == - constants.TOPOLOGY_ACTIVE_STANDBY): + if load_balancer.topology == constants.TOPOLOGY_ACTIVE_STANDBY: try: self._create_security_group_rule( sec_grp_id, diff --git a/octavia/tests/functional/api/v2/test_flavors.py b/octavia/tests/functional/api/v2/test_flavors.py index e16f746720..ed4e3457e2 100644 --- a/octavia/tests/functional/api/v2/test_flavors.py +++ b/octavia/tests/functional/api/v2/test_flavors.py @@ -539,3 +539,20 @@ class TestFlavors(base.BaseAPITest): response = self.get(self.FLAVOR_PATH.format( flavor_id=flavor.get('id'))).json.get(self.root_tag) self.assertEqual('name1', response.get('name')) + + def test_delete_in_use(self): + flavor = self.create_flavor('name1', 'description', self.fp.get('id'), + True) + self.assertTrue(uuidutils.is_uuid_like(flavor.get('id'))) + project_id = uuidutils.generate_uuid() + lb_id = uuidutils.generate_uuid() + self.create_load_balancer(lb_id, name='lb1', + project_id=project_id, + description='desc1', + flavor_id=flavor.get('id'), + admin_state_up=False) + self.delete(self.FLAVOR_PATH.format(flavor_id=flavor.get('id')), + status=409) + response = self.get(self.FLAVOR_PATH.format( + flavor_id=flavor.get('id'))).json.get(self.root_tag) + self.assertEqual('name1', response.get('name')) diff --git a/octavia/tests/functional/api/v2/test_load_balancer.py b/octavia/tests/functional/api/v2/test_load_balancer.py index ec67c630e2..ba7ff0fe68 100644 --- a/octavia/tests/functional/api/v2/test_load_balancer.py +++ b/octavia/tests/functional/api/v2/test_load_balancer.py @@ -19,6 +19,7 @@ import mock from oslo_config import cfg from oslo_config import fixture as oslo_fixture from oslo_utils import uuidutils +from sqlalchemy.orm import exc as sa_exception from octavia.api.drivers import exceptions as provider_exceptions from octavia.common import constants @@ -834,7 +835,7 @@ class TestLoadBalancer(base.BaseAPITest): self.assertIn("Provider 'BOGUS' is not enabled.", response.json.get('faultstring')) - def test_create_flavor_bogus(self, **optionals): + def test_create_flavor_bad_type(self, **optionals): lb_json = {'name': 'test1', 'vip_subnet_id': uuidutils.generate_uuid(), 'project_id': self.project_id, @@ -844,7 +845,109 @@ class TestLoadBalancer(base.BaseAPITest): body = self._build_body(lb_json) response = self.post(self.LBS_PATH, body, status=400) self.assertIn("Invalid input for field/attribute flavor_id. Value: " - "'BOGUS'. Value should be one of:", + "'BOGUS'. Value should be UUID format", + response.json.get('faultstring')) + + def test_create_flavor_invalid(self, **optionals): + lb_json = {'name': 'test1', + 'vip_subnet_id': uuidutils.generate_uuid(), + 'project_id': self.project_id, + 'flavor_id': uuidutils.generate_uuid() + } + lb_json.update(optionals) + body = self._build_body(lb_json) + response = self.post(self.LBS_PATH, body, status=400) + self.assertIn("Validation failure: Invalid flavor_id.", + response.json.get('faultstring')) + + def test_create_flavor_disabled(self, **optionals): + fp = self.create_flavor_profile('test1', 'noop_driver', + '{"image": "ubuntu"}') + flavor = self.create_flavor('name1', 'description', + fp.get('id'), False) + test_flavor_id = flavor.get('id') + lb_json = {'name': 'test1', + 'vip_subnet_id': uuidutils.generate_uuid(), + 'project_id': self.project_id, + 'flavor_id': test_flavor_id, + } + lb_json.update(optionals) + body = self._build_body(lb_json) + response = self.post(self.LBS_PATH, body, status=400) + ref_faultstring = ('The selected flavor is not allowed in this ' + 'deployment: {}'.format(test_flavor_id)) + self.assertEqual(ref_faultstring, response.json.get('faultstring')) + + def test_create_flavor_missing(self, **optionals): + fp = self.create_flavor_profile('test1', 'noop_driver', + '{"image": "ubuntu"}') + flavor = self.create_flavor('name1', 'description', fp.get('id'), True) + test_flavor_id = flavor.get('id') + lb_json = {'name': 'test1', + 'vip_subnet_id': uuidutils.generate_uuid(), + 'project_id': self.project_id, + 'flavor_id': test_flavor_id + } + lb_json.update(optionals) + body = self._build_body(lb_json) + with mock.patch('octavia.db.repositories.FlavorRepository.' + 'get_flavor_metadata_dict', + side_effect=sa_exception.NoResultFound): + response = self.post(self.LBS_PATH, body, status=400) + self.assertIn("Validation failure: Invalid flavor_id.", + response.json.get('faultstring')) + + def test_create_flavor_no_provider(self, **optionals): + fp = self.create_flavor_profile('test1', 'noop_driver', + '{"image": "ubuntu"}') + flavor = self.create_flavor('name1', 'description', fp.get('id'), True) + test_flavor_id = flavor.get('id') + lb_json = {'name': 'test1', + 'vip_subnet_id': uuidutils.generate_uuid(), + 'project_id': self.project_id, + 'flavor_id': test_flavor_id, + } + lb_json.update(optionals) + body = self._build_body(lb_json) + response = self.post(self.LBS_PATH, body, status=201) + api_lb = response.json.get(self.root_tag) + self.assertEqual('noop_driver', api_lb.get('provider')) + self.assertEqual(test_flavor_id, api_lb.get('flavor_id')) + + def test_matching_providers(self, **optionals): + fp = self.create_flavor_profile('test1', 'noop_driver', + '{"image": "ubuntu"}') + flavor = self.create_flavor('name1', 'description', fp.get('id'), True) + test_flavor_id = flavor.get('id') + lb_json = {'name': 'test1', + 'vip_subnet_id': uuidutils.generate_uuid(), + 'project_id': self.project_id, + 'flavor_id': test_flavor_id, + 'provider': 'noop_driver' + } + lb_json.update(optionals) + body = self._build_body(lb_json) + response = self.post(self.LBS_PATH, body, status=201) + api_lb = response.json.get(self.root_tag) + self.assertEqual('noop_driver', api_lb.get('provider')) + self.assertEqual(test_flavor_id, api_lb.get('flavor_id')) + + def test_conflicting_providers(self, **optionals): + fp = self.create_flavor_profile('test1', 'noop_driver', + '{"image": "ubuntu"}') + flavor = self.create_flavor('name1', 'description', fp.get('id'), True) + test_flavor_id = flavor.get('id') + lb_json = {'name': 'test1', + 'vip_subnet_id': uuidutils.generate_uuid(), + 'project_id': self.project_id, + 'flavor_id': test_flavor_id, + 'provider': 'noop_driver-alt' + } + lb_json.update(optionals) + body = self._build_body(lb_json) + response = self.post(self.LBS_PATH, body, status=400) + self.assertIn("Flavor '{}' is not compatible with provider " + "'noop_driver-alt'".format(test_flavor_id), response.json.get('faultstring')) def test_get_all_admin(self): @@ -2203,7 +2306,7 @@ class TestLoadBalancerGraph(base.BaseAPITest): # expected that this would be overwritten anyway, so 'ANY' is fine? 'vip_network_id': mock.ANY, 'vip_qos_policy_id': None, - 'flavor_id': '', + 'flavor_id': None, 'provider': 'noop_driver', 'tags': [] } diff --git a/octavia/tests/functional/db/test_repositories.py b/octavia/tests/functional/db/test_repositories.py index 4c2b5a32ec..9b3df9baf4 100644 --- a/octavia/tests/functional/db/test_repositories.py +++ b/octavia/tests/functional/db/test_repositories.py @@ -21,6 +21,7 @@ from oslo_config import cfg from oslo_config import fixture as oslo_fixture from oslo_db import exception as db_exception from oslo_utils import uuidutils +from sqlalchemy.orm import exc as sa_exception from octavia.common import constants from octavia.common import data_models as models @@ -149,7 +150,7 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): 'provider': 'amphora', 'server_group_id': uuidutils.generate_uuid(), 'project_id': uuidutils.generate_uuid(), - 'id': uuidutils.generate_uuid(), + 'id': uuidutils.generate_uuid(), 'flavor_id': None, 'tags': ['test_tag']} vip = {'ip_address': '192.0.2.1', 'port_id': uuidutils.generate_uuid(), @@ -3041,8 +3042,20 @@ class AmphoraRepositoryTest(BaseRepositoryTest): self.assertEqual(amphora, new_amphora) def test_count(self): - amphora = self.create_amphora(self.FAKE_UUID_1) - amp_count = self.amphora_repo.count(self.session, id=amphora.id) + comp_id = uuidutils.generate_uuid() + self.create_amphora(self.FAKE_UUID_1, compute_id=comp_id) + self.create_amphora(self.FAKE_UUID_2, compute_id=comp_id, + status=constants.DELETED) + amp_count = self.amphora_repo.count(self.session, compute_id=comp_id) + self.assertEqual(2, amp_count) + + def test_count_not_deleted(self): + comp_id = uuidutils.generate_uuid() + self.create_amphora(self.FAKE_UUID_1, compute_id=comp_id) + self.create_amphora(self.FAKE_UUID_2, compute_id=comp_id, + status=constants.DELETED) + amp_count = self.amphora_repo.count(self.session, compute_id=comp_id, + show_deleted=False) self.assertEqual(1, amp_count) def test_create(self): @@ -4267,11 +4280,13 @@ class FlavorProfileRepositoryTest(BaseRepositoryTest): class FlavorRepositoryTest(BaseRepositoryTest): + PROVIDER_NAME = 'provider1' + def create_flavor_profile(self): fp = self.flavor_profile_repo.create( self.session, id=uuidutils.generate_uuid(), - name="fp1", provider_name='pr1', - flavor_data="{'image': 'unbuntu'}") + name="fp1", provider_name=self.PROVIDER_NAME, + flavor_data='{"image": "ubuntu"}') return fp def create_flavor(self, flavor_id, name): @@ -4308,3 +4323,26 @@ class FlavorRepositoryTest(BaseRepositoryTest): self.flavor_repo.delete(self.session, id=fl.id) self.assertIsNone(self.flavor_repo.get( self.session, id=fl.id)) + + def test_get_flavor_metadata_dict(self): + ref_dict = {'image': 'ubuntu'} + self.create_flavor(flavor_id=self.FAKE_UUID_2, name='fl1') + flavor_metadata_dict = self.flavor_repo.get_flavor_metadata_dict( + self.session, self.FAKE_UUID_2) + self.assertEqual(ref_dict, flavor_metadata_dict) + + # Test missing flavor + self.assertRaises(sa_exception.NoResultFound, + self.flavor_repo.get_flavor_metadata_dict, + self.session, self.FAKE_UUID_1) + + def test_get_flavor_provider(self): + self.create_flavor(flavor_id=self.FAKE_UUID_2, name='fl1') + provider_name = self.flavor_repo.get_flavor_provider(self.session, + self.FAKE_UUID_2) + self.assertEqual(self.PROVIDER_NAME, provider_name) + + # Test missing flavor + self.assertRaises(sa_exception.NoResultFound, + self.flavor_repo.get_flavor_provider, + self.session, self.FAKE_UUID_1) diff --git a/octavia/tests/unit/api/drivers/amphora_driver/test_amphora_driver.py b/octavia/tests/unit/api/drivers/amphora_driver/test_amphora_driver.py index 68f8264621..355f2e71ee 100644 --- a/octavia/tests/unit/api/drivers/amphora_driver/test_amphora_driver.py +++ b/octavia/tests/unit/api/drivers/amphora_driver/test_amphora_driver.py @@ -420,3 +420,47 @@ class TestAmphoraDriver(base.TestCase): payload = {consts.L7RULE_ID: self.sample_data.l7rule1_id, consts.L7RULE_UPDATES: l7rule_dict} mock_cast.assert_called_with({}, 'update_l7rule', **payload) + + # Flavor + def test_get_supported_flavor_metadata(self): + test_schema = { + "properties": { + "test_name": {"description": "Test description"}, + "test_name2": {"description": "Another description"}}} + ref_dict = {"test_name": "Test description", + "test_name2": "Another description"} + + # mock out the supported_flavor_metadata + with mock.patch('octavia.api.drivers.amphora_driver.flavor_schema.' + 'SUPPORTED_FLAVOR_SCHEMA', test_schema): + result = self.amp_driver.get_supported_flavor_metadata() + self.assertEqual(ref_dict, result) + + # Test for bad schema + with mock.patch('octavia.api.drivers.amphora_driver.flavor_schema.' + 'SUPPORTED_FLAVOR_SCHEMA', 'bogus'): + self.assertRaises(exceptions.DriverError, + self.amp_driver.get_supported_flavor_metadata) + + @mock.patch('jsonschema.validators.requests') + def test_validate_flavor(self, mock_validate): + ref_dict = {consts.LOADBALANCER_TOPOLOGY: consts.TOPOLOGY_SINGLE} + self.amp_driver.validate_flavor(ref_dict) + + # Test bad flavor metadata value is bad + ref_dict = {consts.LOADBALANCER_TOPOLOGY: 'bogus'} + self.assertRaises(exceptions.UnsupportedOptionError, + self.amp_driver.validate_flavor, + ref_dict) + + # Test bad flavor metadata key + ref_dict = {'bogus': 'bogus'} + self.assertRaises(exceptions.UnsupportedOptionError, + self.amp_driver.validate_flavor, + ref_dict) + + # Test for bad schema + with mock.patch('octavia.api.drivers.amphora_driver.flavor_schema.' + 'SUPPORTED_FLAVOR_SCHEMA', 'bogus'): + self.assertRaises(exceptions.DriverError, + self.amp_driver.validate_flavor, 'bogus') diff --git a/octavia/tests/unit/api/drivers/test_utils.py b/octavia/tests/unit/api/drivers/test_utils.py index ecc157f134..be9c598e88 100644 --- a/octavia/tests/unit/api/drivers/test_utils.py +++ b/octavia/tests/unit/api/drivers/test_utils.py @@ -114,7 +114,6 @@ class TestUtils(base.TestCase): 'listeners': self.sample_data.provider_listeners, 'description': '', 'project_id': self.sample_data.project_id, - 'flavor_id': '', 'vip_port_id': self.sample_data.port_id, 'vip_qos_policy_id': self.sample_data.qos_policy_id, 'vip_network_id': self.sample_data.network_id, diff --git a/octavia/tests/unit/controller/worker/tasks/test_network_tasks.py b/octavia/tests/unit/controller/worker/tasks/test_network_tasks.py index 272b078216..f69e1b3ff2 100644 --- a/octavia/tests/unit/controller/worker/tasks/test_network_tasks.py +++ b/octavia/tests/unit/controller/worker/tasks/test_network_tasks.py @@ -54,7 +54,7 @@ AMPS_DATA = [o_data_models.Amphora(id=t_constants.MOCK_AMP_ID1, vrrp_port_id=t_constants.MOCK_VRRP_PORT_ID2, vrrp_ip=t_constants.MOCK_VRRP_IP2) ] -UPDATE_DICT = {constants.LOADBALANCER_TOPOLOGY: None} +UPDATE_DICT = {constants.TOPOLOGY: None} class TestException(Exception): @@ -502,8 +502,7 @@ class TestNetworkTasks(base.TestCase): mock_get_lb_db.return_value = LB # execute - UPDATE_DICT[ - constants.LOADBALANCER_TOPOLOGY] = constants.TOPOLOGY_SINGLE + UPDATE_DICT[constants.TOPOLOGY] = constants.TOPOLOGY_SINGLE update_dict = UPDATE_DICT net.execute(LB, [AMPS_DATA[0]], update_dict) mock_driver.apply_qos_on_port.assert_called_once_with( @@ -511,8 +510,7 @@ class TestNetworkTasks(base.TestCase): self.assertEqual(1, mock_driver.apply_qos_on_port.call_count) standby_topology = constants.TOPOLOGY_ACTIVE_STANDBY mock_driver.reset_mock() - update_dict[ - constants.LOADBALANCER_TOPOLOGY] = standby_topology + update_dict[constants.TOPOLOGY] = standby_topology net.execute(LB, AMPS_DATA, update_dict) mock_driver.apply_qos_on_port.assert_called_with( t_constants.MOCK_QOS_POLICY_ID1, mock.ANY) @@ -524,8 +522,7 @@ class TestNetworkTasks(base.TestCase): net.revert(None, LB, [AMPS_DATA[0]], update_dict) self.assertEqual(0, mock_driver.apply_qos_on_port.call_count) mock_driver.reset_mock() - update_dict[ - constants.LOADBALANCER_TOPOLOGY] = standby_topology + update_dict[constants.TOPOLOGY] = standby_topology net.revert(None, LB, AMPS_DATA, update_dict) self.assertEqual(0, mock_driver.apply_qos_on_port.call_count) diff --git a/octavia/tests/unit/controller/worker/test_controller_worker.py b/octavia/tests/unit/controller/worker/test_controller_worker.py index a3027dc671..944ada82aa 100644 --- a/octavia/tests/unit/controller/worker/test_controller_worker.py +++ b/octavia/tests/unit/controller/worker/test_controller_worker.py @@ -424,6 +424,7 @@ class TestControllerWorker(base.TestCase): } lb_mock = mock.MagicMock() lb_mock.listeners = [] + lb_mock.topology = constants.TOPOLOGY_SINGLE mock_lb_repo_get.side_effect = [None, None, None, lb_mock] cw = controller_worker.ControllerWorker() @@ -467,6 +468,8 @@ class TestControllerWorker(base.TestCase): 'update_dict': {'topology': constants.TOPOLOGY_ACTIVE_STANDBY}, constants.BUILD_TYPE_PRIORITY: constants.LB_CREATE_NORMAL_PRIORITY } + setattr(mock_lb_repo_get.return_value, 'topology', + constants.TOPOLOGY_ACTIVE_STANDBY) setattr(mock_lb_repo_get.return_value, 'listeners', []) cw = controller_worker.ControllerWorker() @@ -501,7 +504,8 @@ class TestControllerWorker(base.TestCase): listeners = [data_models.Listener(id='listener1'), data_models.Listener(id='listener2')] - lb = data_models.LoadBalancer(id=LB_ID, listeners=listeners) + lb = data_models.LoadBalancer(id=LB_ID, listeners=listeners, + topology=constants.TOPOLOGY_SINGLE) mock_lb_repo_get.return_value = lb mock_eng = mock.Mock() mock_taskflow_load.return_value = mock_eng @@ -551,7 +555,9 @@ class TestControllerWorker(base.TestCase): listeners = [data_models.Listener(id='listener1'), data_models.Listener(id='listener2')] - lb = data_models.LoadBalancer(id=LB_ID, listeners=listeners) + lb = data_models.LoadBalancer( + id=LB_ID, listeners=listeners, + topology=constants.TOPOLOGY_ACTIVE_STANDBY) mock_lb_repo_get.return_value = lb mock_eng = mock.Mock() mock_taskflow_load.return_value = mock_eng @@ -564,8 +570,6 @@ class TestControllerWorker(base.TestCase): cw = controller_worker.ControllerWorker() cw.create_load_balancer(LB_ID) - # mock_create_single_topology.assert_not_called() - # mock_create_active_standby_topology.assert_called_once() mock_get_create_load_balancer_flow.assert_called_with( topology=constants.TOPOLOGY_ACTIVE_STANDBY, listeners=lb.listeners) mock_taskflow_load.assert_called_with( diff --git a/requirements.txt b/requirements.txt index f723e3ff49..e1432bf6ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -42,6 +42,7 @@ futures>=3.0.0;python_version=='2.7' or python_version=='2.6' # BSD castellan>=0.16.0 # Apache-2.0 tenacity>=4.9.0 # Apache-2.0 distro>=1.2.0 # Apache-2.0 +jsonschema>=2.6.0 # MIT #for the amphora api Flask!=0.11,>=0.10 # BSD