Add flavor option to loadbalancerv2 creation
Builds on the Neutron services flavor framework in the referenced Depends-On change id. This solution uses a flavor of service type LOADBALANCERV2 associated with a flavor profile containing a driver. If a flavor_id is passed during loadbalancerv2 creation, the flavor is used to find the provider for the driver and populate the existing lbaas provider field. This allows a user to specify a flavor like 'Gold' with the operator dynamically controlling which provider is associated with that flavor for subsequent creates. The flavor_id is persisted in the DB. The selected provider controls subsequent control operations against the created loadbalancer. Also ensure everything works gracefully if flavors plugin is not loaded. Adding api and doc impact tags as the new optional flavor_id parameter should be described to consumers. Co-Authored-By: James Arendt <james.arendt@hp.com> Co-Authored-By: Madhusudhan Kandadai <madhusudhan.kandadai@hpe.com> ApiImpact DocImpact Change-Id: Ic3b459692d092658df93d4c855079311d9bc7ace Depends-On: I5c22ab655a8e2a2e586c10eae9de9b72db49755f Partially implements: blueprint neutron-flavor-framework
This commit is contained in:
parent
4a6a4c5ad1
commit
96a558b5d0
|
@ -194,6 +194,8 @@ class LoadBalancer(model_base.BASEV2, models_v2.HasId, models_v2.HasTenant):
|
|||
# balancer ID and should not be cleared out in this table
|
||||
viewonly=True
|
||||
)
|
||||
flavor_id = sa.Column(sa.String(36), sa.ForeignKey(
|
||||
'flavors.id', name='fk_lbaas_loadbalancers_flavors_id'))
|
||||
|
||||
@property
|
||||
def root_loadbalancer(self):
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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
|
||||
|
||||
Revision ID: 3426acbc12de
|
||||
Revises: 4a408dd491c2
|
||||
Create Date: 2015-12-02 15:24:35.775474
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '3426acbc12de'
|
||||
down_revision = '4a408dd491c2'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('lbaas_loadbalancers',
|
||||
sa.Column(u'flavor_id', sa.String(36), nullable=True))
|
||||
op.create_foreign_key(u'fk_lbaas_loadbalancers_flavors_id',
|
||||
u'lbaas_loadbalancers',
|
||||
u'flavors',
|
||||
[u'flavor_id'],
|
||||
[u'id'])
|
|
@ -124,6 +124,14 @@ class CertManagerError(nexception.NeutronException):
|
|||
message = _("Could not process TLS container %(ref)s, %(reason)s")
|
||||
|
||||
|
||||
class ProviderFlavorConflict(nexception.Conflict):
|
||||
message = _("Cannot specify both a flavor and a provider")
|
||||
|
||||
|
||||
class FlavorsPluginNotLoaded(nexception.NotFound):
|
||||
message = _("Flavors plugin not found")
|
||||
|
||||
|
||||
RESOURCE_ATTRIBUTE_MAP = {
|
||||
'loadbalancers': {
|
||||
'id': {'allow_post': False, 'allow_put': False,
|
||||
|
@ -162,7 +170,11 @@ RESOURCE_ATTRIBUTE_MAP = {
|
|||
'provisioning_status': {'allow_post': False, 'allow_put': False,
|
||||
'is_visible': True},
|
||||
'operating_status': {'allow_post': False, 'allow_put': False,
|
||||
'is_visible': True}
|
||||
'is_visible': True},
|
||||
'flavor_id': {'allow_post': True, 'allow_put': False,
|
||||
'is_visible': True,
|
||||
'validate': {'type:string': attr.NAME_MAX_LEN},
|
||||
'default': attr.ATTR_NOT_SPECIFIED}
|
||||
},
|
||||
'listeners': {
|
||||
'id': {'allow_post': False, 'allow_put': False,
|
||||
|
|
|
@ -504,7 +504,7 @@ class LoadBalancer(BaseDataModel):
|
|||
vip_subnet_id=None, vip_port_id=None, vip_address=None,
|
||||
provisioning_status=None, operating_status=None,
|
||||
admin_state_up=None, vip_port=None, stats=None,
|
||||
provider=None, listeners=None):
|
||||
provider=None, listeners=None, flavor_id=None):
|
||||
self.id = id
|
||||
self.tenant_id = tenant_id
|
||||
self.name = name
|
||||
|
@ -519,6 +519,7 @@ class LoadBalancer(BaseDataModel):
|
|||
self.stats = stats
|
||||
self.provider = provider
|
||||
self.listeners = listeners or []
|
||||
self.flavor_id = flavor_id
|
||||
|
||||
def attached_to_loadbalancer(self):
|
||||
return True
|
||||
|
@ -530,6 +531,10 @@ class LoadBalancer(BaseDataModel):
|
|||
for listener in self.listeners]
|
||||
if self.provider:
|
||||
ret_dict['provider'] = self.provider.provider_name
|
||||
|
||||
if not self.flavor_id:
|
||||
del ret_dict['flavor_id']
|
||||
|
||||
return ret_dict
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -19,7 +19,10 @@ from neutron.api.v2 import attributes as attrs
|
|||
from neutron.common import exceptions as n_exc
|
||||
from neutron import context as ncontext
|
||||
from neutron.db import servicetype_db as st_db
|
||||
from neutron.extensions import flavors
|
||||
from neutron import manager
|
||||
from neutron.plugins.common import constants
|
||||
from neutron.services.flavors import flavors_plugin
|
||||
from neutron.services import provider_configuration as pconf
|
||||
from neutron.services import service_base
|
||||
from oslo_config import cfg
|
||||
|
@ -507,8 +510,52 @@ class LoadBalancerPluginv2(loadbalancerv2.LoadBalancerPluginBaseV2):
|
|||
def get_plugin_description(self):
|
||||
return "Neutron LoadBalancer Service Plugin v2"
|
||||
|
||||
def _insert_provider_name_from_flavor(self, context, loadbalancer):
|
||||
"""Select provider based on flavor."""
|
||||
|
||||
# TODO(jwarendt) Support passing flavor metainfo from the
|
||||
# selected flavor profile into the provider, not just selecting
|
||||
# the provider, when flavor templating arrives.
|
||||
|
||||
if ('provider' in loadbalancer and
|
||||
loadbalancer['provider'] != attrs.ATTR_NOT_SPECIFIED):
|
||||
raise loadbalancerv2.ProviderFlavorConflict()
|
||||
|
||||
plugin = manager.NeutronManager.get_service_plugins().get(
|
||||
constants.FLAVORS)
|
||||
if not plugin:
|
||||
raise loadbalancerv2.FlavorsPluginNotLoaded()
|
||||
|
||||
# Will raise FlavorNotFound if doesn't exist
|
||||
fl_db = flavors_plugin.FlavorsPlugin.get_flavor(
|
||||
plugin,
|
||||
context,
|
||||
loadbalancer['flavor_id'])
|
||||
|
||||
if fl_db['service_type'] != constants.LOADBALANCERV2:
|
||||
raise flavors.InvalidFlavorServiceType(
|
||||
service_type=fl_db['service_type'])
|
||||
|
||||
if not fl_db['enabled']:
|
||||
raise flavors.FlavorDisabled()
|
||||
|
||||
providers = flavors_plugin.FlavorsPlugin.get_flavor_next_provider(
|
||||
plugin,
|
||||
context,
|
||||
fl_db['id'])
|
||||
|
||||
provider = providers[0].get('provider')
|
||||
|
||||
LOG.debug("Selected provider %s" % provider)
|
||||
|
||||
loadbalancer['provider'] = provider
|
||||
|
||||
def create_loadbalancer(self, context, loadbalancer):
|
||||
loadbalancer = loadbalancer.get('loadbalancer')
|
||||
if loadbalancer['flavor_id'] != attrs.ATTR_NOT_SPECIFIED:
|
||||
self._insert_provider_name_from_flavor(context, loadbalancer)
|
||||
else:
|
||||
del loadbalancer['flavor_id']
|
||||
provider_name = self._get_provider_name(loadbalancer)
|
||||
driver = self.drivers[provider_name]
|
||||
lb_db = self.db.create_loadbalancer(
|
||||
|
|
|
@ -290,6 +290,23 @@ class LoadBalancersTestJSON(base.BaseTestCase):
|
|||
vip_subnet_id=self.subnet['id'],
|
||||
tenant_id=tenant)
|
||||
|
||||
@test.attr(type='negative')
|
||||
def test_create_load_balancer_invalid_flavor_field(self):
|
||||
"""Test create load balancer with an invalid flavor field"""
|
||||
self.assertRaises(exceptions.NotFound,
|
||||
self.load_balancers_client.create_load_balancer,
|
||||
vip_subnet_id=self.subnet['id'],
|
||||
flavor_id="NO_SUCH_FLAVOR")
|
||||
|
||||
@test.attr(type='negative')
|
||||
def test_create_load_balancer_provider_flavor_conflict(self):
|
||||
"""Test create load balancer with both a provider and a flavor"""
|
||||
self.assertRaises(exceptions.Conflict,
|
||||
self.load_balancers_client.create_load_balancer,
|
||||
vip_subnet_id=self.subnet['id'],
|
||||
flavor_id="NO_SUCH_FLAVOR",
|
||||
provider="NO_SUCH_PROVIDER")
|
||||
|
||||
@test.attr(type='smoke')
|
||||
def test_update_load_balancer(self):
|
||||
"""Test update load balancer"""
|
||||
|
|
|
@ -486,7 +486,8 @@ class LoadBalancerExtensionV2TestCase(base.ExtensionTestCase):
|
|||
res = self.api.post(_get_path('lbaas/loadbalancers', fmt=self.fmt),
|
||||
self.serialize(data),
|
||||
content_type='application/{0}'.format(self.fmt))
|
||||
data['loadbalancer'].update({'provider': attr.ATTR_NOT_SPECIFIED})
|
||||
data['loadbalancer'].update({'provider': attr.ATTR_NOT_SPECIFIED,
|
||||
'flavor_id': attr.ATTR_NOT_SPECIFIED})
|
||||
instance.create_loadbalancer.assert_called_with(mock.ANY,
|
||||
loadbalancer=data)
|
||||
|
||||
|
@ -495,6 +496,34 @@ class LoadBalancerExtensionV2TestCase(base.ExtensionTestCase):
|
|||
self.assertIn('loadbalancer', res)
|
||||
self.assertEqual(return_value, res['loadbalancer'])
|
||||
|
||||
def test_loadbalancer_create_invalid_flavor(self):
|
||||
data = {'loadbalancer': {'name': 'lb1',
|
||||
'description': 'descr_lb1',
|
||||
'tenant_id': _uuid(),
|
||||
'vip_subnet_id': _uuid(),
|
||||
'admin_state_up': True,
|
||||
'flavor_id': 123,
|
||||
'vip_address': '127.0.0.1'}}
|
||||
res = self.api.post(_get_path('lbaas/loadbalancers', fmt=self.fmt),
|
||||
self.serialize(data),
|
||||
content_type='application/{0}'.format(self.fmt),
|
||||
expect_errors=True)
|
||||
self.assertEqual(400, res.status_int)
|
||||
|
||||
def test_loadbalancer_create_valid_flavor(self):
|
||||
data = {'loadbalancer': {'name': 'lb1',
|
||||
'description': 'descr_lb1',
|
||||
'tenant_id': _uuid(),
|
||||
'vip_subnet_id': _uuid(),
|
||||
'admin_state_up': True,
|
||||
'flavor_id': _uuid(),
|
||||
'vip_address': '127.0.0.1'}}
|
||||
res = self.api.post(_get_path('lbaas/loadbalancers', fmt=self.fmt),
|
||||
self.serialize(data),
|
||||
content_type='application/{0}'.format(self.fmt),
|
||||
expect_errors=True)
|
||||
self.assertEqual(201, res.status_int)
|
||||
|
||||
def test_loadbalancer_list(self):
|
||||
lb_id = _uuid()
|
||||
return_value = [{'name': 'lb1',
|
||||
|
|
|
@ -172,6 +172,7 @@ class LBaaSAgentSchedulerTestCase(test_agent.AgentDBTestMixIn,
|
|||
'loadbalancer': {
|
||||
'vip_subnet_id': subnet['id'],
|
||||
'provider': 'lbaas',
|
||||
'flavor_id': attributes.ATTR_NOT_SPECIFIED,
|
||||
'vip_address': attributes.ATTR_NOT_SPECIFIED,
|
||||
'admin_state_up': True,
|
||||
'tenant_id': self._tenant_id}}
|
||||
|
@ -206,6 +207,7 @@ class LBaaSAgentSchedulerTestCase(test_agent.AgentDBTestMixIn,
|
|||
'loadbalancer': {
|
||||
'vip_subnet_id': subnet['id'],
|
||||
'provider': 'lbaas',
|
||||
'flavor_id': attributes.ATTR_NOT_SPECIFIED,
|
||||
'vip_address': attributes.ATTR_NOT_SPECIFIED,
|
||||
'admin_state_up': True,
|
||||
'tenant_id': self._tenant_id}}
|
||||
|
|
Loading…
Reference in New Issue