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:
parent
15282ff9d6
commit
0b1fe6a526
@ -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)))
|
||||
|
44
octavia/api/drivers/amphora_driver/flavor_schema.py
Normal file
44
octavia/api/drivers/amphora_driver/flavor_schema.py
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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):
|
||||
|
@ -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'
|
||||
|
@ -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():
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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))
|
@ -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):
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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'))
|
||||
|
@ -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': []
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user