From 4455759f4506a43d70f811a32ee60b13af6afd8d Mon Sep 17 00:00:00 2001 From: Cedric Shock Date: Mon, 29 Aug 2016 23:46:55 +0000 Subject: [PATCH] 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 --- .../db/loadbalancer/loadbalancer_dbv2.py | 104 ++++++++++++------ neutron_lbaas/extensions/lb_network_vip.py | 62 +++++++++++ neutron_lbaas/services/loadbalancer/plugin.py | 1 + .../db/loadbalancer/test_db_loadbalancerv2.py | 54 ++++++++- .../loadbalancer/test_loadbalancer_plugin.py | 39 ++++++- ...e-lb-with-network-id-dba0a71878942af7.yaml | 14 +++ 6 files changed, 234 insertions(+), 40 deletions(-) create mode 100644 neutron_lbaas/extensions/lb_network_vip.py create mode 100644 releasenotes/notes/create-lb-with-network-id-dba0a71878942af7.yaml diff --git a/neutron_lbaas/db/loadbalancer/loadbalancer_dbv2.py b/neutron_lbaas/db/loadbalancer/loadbalancer_dbv2.py index c3fa003a4..f1aa3b2da 100644 --- a/neutron_lbaas/db/loadbalancer/loadbalancer_dbv2.py +++ b/neutron_lbaas/db/loadbalancer/loadbalancer_dbv2.py @@ -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_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 @@ 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 @@ -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): diff --git a/neutron_lbaas/extensions/lb_network_vip.py b/neutron_lbaas/extensions/lb_network_vip.py new file mode 100644 index 000000000..203cce583 --- /dev/null +++ b/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 {} diff --git a/neutron_lbaas/services/loadbalancer/plugin.py b/neutron_lbaas/services/loadbalancer/plugin.py index 1b6533c8f..a74d17980 100644 --- a/neutron_lbaas/services/loadbalancer/plugin.py +++ b/neutron_lbaas/services/loadbalancer/plugin.py @@ -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 diff --git a/neutron_lbaas/tests/unit/db/loadbalancer/test_db_loadbalancerv2.py b/neutron_lbaas/tests/unit/db/loadbalancer/test_db_loadbalancerv2.py index 6a6bbcb10..e330d8451 100755 --- a/neutron_lbaas/tests/unit/db/loadbalancer/test_db_loadbalancerv2.py +++ b/neutron_lbaas/tests/unit/db/loadbalancer/test_db_loadbalancerv2.py @@ -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 @@ -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 @@ def _create_loadbalancer(self, fmt, 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) @@ -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: @@ -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' diff --git a/neutron_lbaas/tests/unit/services/loadbalancer/test_loadbalancer_plugin.py b/neutron_lbaas/tests/unit/services/loadbalancer/test_loadbalancer_plugin.py index a15cc4700..1ae240c0b 100644 --- a/neutron_lbaas/tests/unit/services/loadbalancer/test_loadbalancer_plugin.py +++ b/neutron_lbaas/tests/unit/services/loadbalancer/test_loadbalancer_plugin.py @@ -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 @@ -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, @@ -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) diff --git a/releasenotes/notes/create-lb-with-network-id-dba0a71878942af7.yaml b/releasenotes/notes/create-lb-with-network-id-dba0a71878942af7.yaml new file mode 100644 index 000000000..33ab42471 --- /dev/null +++ b/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.