Allow creating loadbalancer with network_id

Create loadbalancer accepts either a vip_subnet_id
or vip_network_id. If vip_network_id is provided the
vip port is created on that network using the default
neutron behavior. If neutron assigns multiple fixed ips,
an ipv4 addresses is chosen as the vip in preference to
ipv6 addresses.

-----

Who would use the feature?
LBaaS users on a network with multiple subnets

Why use the feature?
Large deployments may have many subnets to allocate
vip addresses. Many of these subnets might have
no addresses remaining to allocate. Creating a
loadbalancer by network selects a subnet with an
available address.

What is the exact usage for the feature?

POST /lbaas/loadbalancers
Host: lbaas-service.cloudX.com:8651
Content-Type: application/json
Accept: application/json
X-Auth-Token:887665443383838

{
    "loadbalancer": {
        "name": "loadbalancer1",
        "description": "simple lb",
        "tenant_id": "b7c1a69e88bf4b21a8148f787aef2081",
        "vip_network_id": "a3847aea-fa6d-45bc-9bce-03d4472d209d",
        "admin_state_up": true
    }
}

DocImpact: 2.0 API Create a loadbalancer attributes
APIImpact
Closes-Bug: #1465758
Change-Id: I31f10581369343fde7f928ff0aeb1024eb752dc4
This commit is contained in:
Cedric Shock 2016-08-29 23:46:55 +00:00
parent 066dee5e16
commit 4455759f45
6 changed files with 234 additions and 40 deletions
neutron_lbaas
db/loadbalancer
extensions
services/loadbalancer
tests/unit
db/loadbalancer
services/loadbalancer
releasenotes/notes

@ -15,9 +15,11 @@
import re
import netaddr
from neutron.callbacks import events
from neutron.callbacks import registry
from neutron.callbacks import resources
from neutron.common import ipv6_utils
from neutron.db import api as db_api
from neutron.db import common_db_mixin as base_db
from neutron import manager
@ -28,7 +30,6 @@ from oslo_db import exception
from oslo_log import log as logging
from oslo_utils import excutils
from oslo_utils import uuidutils
from sqlalchemy import exc as sqlalchemy_exc
from sqlalchemy import orm
from sqlalchemy.orm import exc
from sqlalchemy.orm import lazyload
@ -96,33 +97,68 @@ class LoadBalancerPluginDbv2(base_db.CommonDbMixin,
filters=filters)
return [model_instance for model_instance in query]
def _create_port_for_load_balancer(self, context, lb_db, ip_address):
# resolve subnet and create port
subnet = self._core_plugin.get_subnet(context, lb_db.vip_subnet_id)
fixed_ip = {'subnet_id': subnet['id']}
if ip_address and ip_address != n_const.ATTR_NOT_SPECIFIED:
fixed_ip['ip_address'] = ip_address
def _create_port_choose_fixed_ip(self, fixed_ips):
# Neutron will try to allocate IPv4, IPv6, and IPv6 EUI-64 addresses.
# We're most interested in the IPv4 address. An IPv4 vip can be
# routable from IPv6. Creating a port by network can be used to manage
# the dwindling, fragmented IPv4 address space. IPv6 has enough
# addresses that a single subnet can always be created that's big
# enough to allocate all vips.
for fixed_ip in fixed_ips:
ip_address = fixed_ip['ip_address']
ip = netaddr.IPAddress(ip_address)
if ip.version == 4:
return fixed_ip
# An EUI-64 address isn't useful as a vip
for fixed_ip in fixed_ips:
ip_address = fixed_ip['ip_address']
ip = netaddr.IPAddress(ip_address)
if ip.version == 6 and not ipv6_utils.is_eui64_address(ip_address):
return fixed_ip
for fixed_ip in fixed_ips:
return fixed_ip
def _create_port_for_load_balancer(self, context, lb_db, ip_address,
network_id=None):
if lb_db.vip_subnet_id:
assign_subnet = False
# resolve subnet and create port
subnet = self._core_plugin.get_subnet(context, lb_db.vip_subnet_id)
network_id = subnet['network_id']
fixed_ip = {'subnet_id': subnet['id']}
if ip_address and ip_address != n_const.ATTR_NOT_SPECIFIED:
fixed_ip['ip_address'] = ip_address
fixed_ips = [fixed_ip]
elif network_id and network_id != n_const.ATTR_NOT_SPECIFIED:
assign_subnet = True
fixed_ips = n_const.ATTR_NOT_SPECIFIED
else:
attrs = _("vip_subnet_id or vip_network_id")
raise loadbalancerv2.RequiredAttributeNotSpecified(attr_name=attrs)
port_data = {
'tenant_id': lb_db.tenant_id,
'name': 'loadbalancer-' + lb_db.id,
'network_id': subnet['network_id'],
'network_id': network_id,
'mac_address': n_const.ATTR_NOT_SPECIFIED,
'admin_state_up': False,
'device_id': lb_db.id,
'device_owner': n_const.DEVICE_OWNER_LOADBALANCERV2,
'fixed_ips': [fixed_ip]
'fixed_ips': fixed_ips
}
port = self._core_plugin.create_port(context, {'port': port_data})
lb_db.vip_port_id = port['id']
for fixed_ip in port['fixed_ips']:
if fixed_ip['subnet_id'] == lb_db.vip_subnet_id:
lb_db.vip_address = fixed_ip['ip_address']
break
# explicitly sync session with db
context.session.flush()
if assign_subnet:
fixed_ip = self._create_port_choose_fixed_ip(port['fixed_ips'])
lb_db.vip_address = fixed_ip['ip_address']
lb_db.vip_subnet_id = fixed_ip['subnet_id']
else:
for fixed_ip in port['fixed_ips']:
if fixed_ip['subnet_id'] == lb_db.vip_subnet_id:
lb_db.vip_address = fixed_ip['ip_address']
break
def _create_loadbalancer_stats(self, context, loadbalancer_id, data=None):
# This is internal method to add load balancer statistics. It won't
@ -278,34 +314,30 @@ class LoadBalancerPluginDbv2(base_db.CommonDbMixin,
return self.get_loadbalancer(context, lb_db.id)
def create_loadbalancer(self, context, loadbalancer, allocate_vip=True):
self._load_id(context, loadbalancer)
vip_network_id = loadbalancer.pop('vip_network_id', None)
vip_subnet_id = loadbalancer.pop('vip_subnet_id', None)
vip_address = loadbalancer.pop('vip_address')
if vip_subnet_id and vip_subnet_id != n_const.ATTR_NOT_SPECIFIED:
loadbalancer['vip_subnet_id'] = vip_subnet_id
loadbalancer['provisioning_status'] = constants.PENDING_CREATE
loadbalancer['operating_status'] = lb_const.OFFLINE
lb_db = models.LoadBalancer(**loadbalancer)
# create port outside of lb create transaction since it can sometimes
# cause lock wait timeouts
if allocate_vip:
LOG.debug("Plugin will allocate the vip as a neutron port.")
self._create_port_for_load_balancer(context, lb_db,
vip_address, vip_network_id)
with context.session.begin(subtransactions=True):
self._load_id(context, loadbalancer)
vip_address = loadbalancer.pop('vip_address')
loadbalancer['provisioning_status'] = constants.PENDING_CREATE
loadbalancer['operating_status'] = lb_const.OFFLINE
lb_db = models.LoadBalancer(**loadbalancer)
context.session.add(lb_db)
context.session.flush()
lb_db.stats = self._create_loadbalancer_stats(
context, lb_db.id)
context.session.add(lb_db)
context.session.flush()
# create port outside of lb create transaction since it can sometimes
# cause lock wait timeouts
if allocate_vip:
LOG.debug("Plugin will allocate the vip as a neutron port.")
try:
self._create_port_for_load_balancer(context, lb_db,
vip_address)
except Exception:
with excutils.save_and_reraise_exception():
try:
context.session.delete(lb_db)
except sqlalchemy_exc.InvalidRequestError:
# Revert already completed.
pass
context.session.flush()
return data_models.LoadBalancer.from_sqlalchemy_model(lb_db)
def update_loadbalancer(self, context, id, loadbalancer):

@ -0,0 +1,62 @@
# Copyright 2016 A10 Networks
# 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 neutron.api import extensions
from neutron_lib import constants as n_constants
EXTENDED_ATTRIBUTES_2_0 = {
'loadbalancers': {
'vip_subnet_id': {'allow_post': True, 'allow_put': False,
'validate': {'type:uuid': None},
'is_visible': True,
'default': n_constants.ATTR_NOT_SPECIFIED},
'vip_network_id': {'allow_post': True, 'allow_put': False,
'validate': {'type:uuid': None},
'is_visible': False,
'default': n_constants.ATTR_NOT_SPECIFIED}
}
}
class Lb_network_vip(extensions.ExtensionDescriptor):
@classmethod
def get_name(cls):
return "Create loadbalancer with network_id"
@classmethod
def get_alias(cls):
return "lb_network_vip"
@classmethod
def get_description(cls):
return "Create loadbalancer with network_id"
@classmethod
def get_namespace(cls):
return "http://wiki.openstack.org/neutron/LBaaS/API_2.0"
@classmethod
def get_updated(cls):
return "2016-09-09T22:00:00-00:00"
def get_required_extensions(self):
return ["lbaasv2"]
def get_extended_resources(self, version):
if version == "2.0":
return EXTENDED_ATTRIBUTES_2_0
else:
return {}

@ -67,6 +67,7 @@ class LoadBalancerPluginv2(loadbalancerv2.LoadBalancerPluginBaseV2):
"lbaas_agent_schedulerv2",
"service-type",
"lb-graph",
"lb_network_vip",
"hm_max_retries_down"]
path_prefix = loadbalancerv2.LOADBALANCERV2_PREFIX

@ -42,6 +42,7 @@ import neutron_lbaas.extensions
from neutron_lbaas.extensions import healthmonitor_max_retries_down
from neutron_lbaas.extensions import l7
from neutron_lbaas.extensions import lb_graph
from neutron_lbaas.extensions import lb_network_vip
from neutron_lbaas.extensions import loadbalancerv2
from neutron_lbaas.extensions import sharedpools
from neutron_lbaas.services.loadbalancer import constants as lb_const
@ -66,6 +67,7 @@ class LbaasTestMixin(object):
resource_keys = list(loadbalancerv2.RESOURCE_ATTRIBUTE_MAP.keys())
resource_keys.extend(l7.RESOURCE_ATTRIBUTE_MAP.keys())
resource_keys.extend(lb_graph.RESOURCE_ATTRIBUTE_MAP.keys())
resource_keys.extend(lb_network_vip.EXTENDED_ATTRIBUTES_2_0.keys())
resource_keys.extend(healthmonitor_max_retries_down.
EXTENDED_ATTRIBUTES_2_0.keys())
resource_prefix_map = dict(
@ -74,7 +76,7 @@ class LbaasTestMixin(object):
def _get_loadbalancer_optional_args(self):
return ('description', 'vip_address', 'admin_state_up', 'name',
'listeners')
'listeners', 'vip_network_id', 'vip_subnet_id')
def _create_loadbalancer(self, fmt, subnet_id,
expected_res_status=None, **kwargs):
@ -82,8 +84,11 @@ class LbaasTestMixin(object):
'tenant_id': self._tenant_id}}
args = self._get_loadbalancer_optional_args()
for arg in args:
if arg in kwargs and kwargs[arg] is not None:
data['loadbalancer'][arg] = kwargs[arg]
if arg in kwargs:
if kwargs[arg] is not None:
data['loadbalancer'][arg] = kwargs[arg]
else:
data['loadbalancer'].pop(arg, None)
lb_req = self.new_create_request('loadbalancers', data, fmt)
lb_res = lb_req.get_response(self.ext_api)
@ -534,6 +539,8 @@ class ExtendedPluginAwareExtensionManager(object):
extensions_list.append(l7)
if 'lb-graph' in self.extension_aliases:
extensions_list.append(lb_graph)
if 'lb_network_vip' in self.extension_aliases:
extensions_list.append(lb_network_vip)
if 'hm_max_retries_down' in self.extension_aliases:
extensions_list.append(healthmonitor_max_retries_down)
for extension in extensions_list:
@ -772,6 +779,47 @@ class LbaasLoadBalancerTests(LbaasPluginDbTestCase):
with testtools.ExpectedException(webob.exc.HTTPClientError):
self.test_create_loadbalancer(vip_address='9.9.9.9')
def test_create_loadbalancer_with_no_vip_network_or_subnet(self):
with testtools.ExpectedException(webob.exc.HTTPClientError):
self.test_create_loadbalancer(
vip_network_id=None,
vip_subnet_id=None,
expected_res_status=400)
def test_create_loadbalancer_with_vip_network_id(self):
expected = {
'name': 'vip1',
'description': '',
'admin_state_up': True,
'provisioning_status': constants.ACTIVE,
'operating_status': lb_const.ONLINE,
'tenant_id': self._tenant_id,
'listeners': [],
'pools': [],
'provider': 'lbaas'
}
with self.subnet() as subnet:
expected['vip_subnet_id'] = subnet['subnet']['id']
name = expected['name']
extras = {
'vip_network_id': subnet['subnet']['network_id'],
'vip_subnet_id': None
}
with self.loadbalancer(name=name, subnet=subnet, **extras) as lb:
lb_id = lb['loadbalancer']['id']
for k in ('id', 'vip_address', 'vip_subnet_id'):
self.assertTrue(lb['loadbalancer'].get(k, None))
expected['vip_port_id'] = lb['loadbalancer']['vip_port_id']
actual = dict((k, v)
for k, v in lb['loadbalancer'].items()
if k in expected)
self.assertEqual(expected, actual)
self._validate_statuses(lb_id)
return lb
def test_update_loadbalancer(self):
name = 'new_loadbalancer'
description = 'a crazy loadbalancer'

@ -23,6 +23,7 @@ from oslo_utils import uuidutils
from webob import exc
from neutron_lbaas.extensions import healthmonitor_max_retries_down as hm_down
from neutron_lbaas.extensions import lb_network_vip
from neutron_lbaas.extensions import loadbalancerv2
from neutron_lbaas.extensions import sharedpools
from neutron_lbaas.tests import base
@ -42,6 +43,8 @@ class TestLoadBalancerExtensionV2TestCase(base.ExtensionTestCase):
resource_map[k].update(sharedpools.EXTENDED_ATTRIBUTES_2_0[k])
for k in hm_down.EXTENDED_ATTRIBUTES_2_0.keys():
resource_map[k].update(hm_down.EXTENDED_ATTRIBUTES_2_0[k])
for k in lb_network_vip.EXTENDED_ATTRIBUTES_2_0.keys():
resource_map[k].update(lb_network_vip.EXTENDED_ATTRIBUTES_2_0[k])
self._setUpExtension(
'neutron_lbaas.extensions.loadbalancerv2.LoadBalancerPluginBaseV2',
constants.LOADBALANCERV2, resource_map,
@ -68,7 +71,41 @@ class TestLoadBalancerExtensionV2TestCase(base.ExtensionTestCase):
content_type='application/{0}'.format(self.fmt))
data['loadbalancer'].update({
'provider': n_constants.ATTR_NOT_SPECIFIED,
'flavor_id': n_constants.ATTR_NOT_SPECIFIED})
'flavor_id': n_constants.ATTR_NOT_SPECIFIED,
'vip_network_id': n_constants.ATTR_NOT_SPECIFIED})
instance.create_loadbalancer.assert_called_with(mock.ANY,
loadbalancer=data)
self.assertEqual(exc.HTTPCreated.code, res.status_int)
res = self.deserialize(res)
self.assertIn('loadbalancer', res)
self.assertEqual(return_value, res['loadbalancer'])
def test_loadbalancer_create_with_vip_network_id(self):
lb_id = _uuid()
project_id = _uuid()
vip_subnet_id = _uuid()
data = {'loadbalancer': {'name': 'lb1',
'description': 'descr_lb1',
'tenant_id': project_id,
'project_id': project_id,
'vip_network_id': _uuid(),
'admin_state_up': True,
'vip_address': '127.0.0.1'}}
return_value = copy.copy(data['loadbalancer'])
return_value.update({'id': lb_id, 'vip_subnet_id': vip_subnet_id})
del return_value['vip_network_id']
instance = self.plugin.return_value
instance.create_loadbalancer.return_value = return_value
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': n_constants.ATTR_NOT_SPECIFIED,
'flavor_id': n_constants.ATTR_NOT_SPECIFIED,
'vip_subnet_id': n_constants.ATTR_NOT_SPECIFIED})
instance.create_loadbalancer.assert_called_with(mock.ANY,
loadbalancer=data)

@ -0,0 +1,14 @@
---
features:
- |
Adds support for creating a loadbalancer with a
Neutron network id.
* Adds an optional ``vip_network_id`` attribute
when creating a loadbalancer.
* When creating a loadbalancer, ``vip_subnet_id``
is optional if a ``vip_network_id`` is proviced.
* If ``vip_network_id`` is provided the vip will
be allocated on a subnet with an available
address. An IPv4 subnet will be chosen if
possible.