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
This commit is contained in:
Michael Johnson 2018-11-30 17:55:03 -08:00
parent 15282ff9d6
commit 0b1fe6a526
23 changed files with 496 additions and 48 deletions

View File

@ -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)))

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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)

View File

@ -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:

View File

@ -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):

View File

@ -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'

View File

@ -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():

View File

@ -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

View File

@ -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(

View File

@ -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))

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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'))

View File

@ -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': []
}

View File

@ -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)

View File

@ -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')

View File

@ -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,

View File

@ -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)

View File

@ -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(

View File

@ -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