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:
parent
066dee5e16
commit
4455759f45
neutron_lbaas
db/loadbalancer
extensions
services/loadbalancer
tests/unit
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):
|
||||
|
62
neutron_lbaas/extensions/lb_network_vip.py
Normal file
62
neutron_lbaas/extensions/lb_network_vip.py
Normal file
@ -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.
|
Loading…
x
Reference in New Issue
Block a user