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_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): 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 @@ 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' 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 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) 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.