Skip to content
This repository has been archived by the owner on Jul 22, 2021. It is now read-only.

Commit

Permalink
Allow creating loadbalancer with network_id
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Cedev committed Sep 14, 2016
1 parent 066dee5 commit 4455759
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 40 deletions.
104 changes: 68 additions & 36 deletions neutron_lbaas/db/loadbalancer/loadbalancer_dbv2.py
Expand Up @@ -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
Expand All @@ -28,7 +30,6 @@
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
Expand Down Expand Up @@ -96,33 +97,68 @@ def _get_resources(self, context, model, filters=None):
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
Expand Down Expand Up @@ -278,34 +314,30 @@ def create_loadbalancer_graph(self, context, loadbalancer,
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):
Expand Down
62 changes: 62 additions & 0 deletions neutron_lbaas/extensions/lb_network_vip.py
@@ -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 {}
1 change: 1 addition & 0 deletions neutron_lbaas/services/loadbalancer/plugin.py
Expand Up @@ -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

Expand Down
54 changes: 51 additions & 3 deletions neutron_lbaas/tests/unit/db/loadbalancer/test_db_loadbalancerv2.py
Expand Up @@ -42,6 +42,7 @@
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
Expand All @@ -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(
Expand All @@ -74,16 +76,19 @@ 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):
data = {'loadbalancer': {'vip_subnet_id': subnet_id,
'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)
Expand Down Expand Up @@ -534,6 +539,8 @@ def get_resources(self):
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:
Expand Down Expand Up @@ -772,6 +779,47 @@ def test_create_loadbalancer_with_vip_address_outside_subnet(self):
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'
Expand Down
Expand Up @@ -23,6 +23,7 @@
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
Expand All @@ -42,6 +43,8 @@ def setUp(self):
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,
Expand All @@ -68,7 +71,41 @@ def test_loadbalancer_create(self):
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)

Expand Down
14 changes: 14 additions & 0 deletions releasenotes/notes/create-lb-with-network-id-dba0a71878942af7.yaml
@@ -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.

0 comments on commit 4455759

Please sign in to comment.