Enable multiple L3 GW services on NVP plugin

Bug 1130211

This patch allows for using multiple layer-3 gateways leveraging the
provider networks extension.

Change-Id: I293920c2565f4670e9be9b22dc1b60431b62f7e7
This commit is contained in:
Salvatore Orlando 2013-02-25 15:41:06 +01:00
parent 9d44d51c3a
commit 14a3d99bb8
9 changed files with 263 additions and 38 deletions

View File

@ -0,0 +1,66 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2013 OpenStack LLC
#
# 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.
#
"""nvp_net_binding
Revision ID: 1341ed32cc1e
Revises: 3b54bf9e29f7
Create Date: 2013-02-26 01:28:29.182195
"""
# revision identifiers, used by Alembic.
revision = '1341ed32cc1e'
down_revision = '3b54bf9e29f7'
# Change to ['*'] if this migration applies to all plugins
migration_for_plugins = [
'quantum.plugins.nicira.nicira_nvp_plugin.QuantumPlugin.NvpPluginV2'
]
from alembic import op
import sqlalchemy as sa
from quantum.db import migration
def upgrade(active_plugin=None, options=None):
if not migration.should_run(active_plugin, migration_for_plugins):
return
op.alter_column('nvp_network_bindings', 'tz_uuid',
name='phy_uuid',
existing_type=sa.String(36),
existing_nullable=True)
op.alter_column('nvp_network_bindings', 'binding_type',
type_=sa.Enum('flat', 'vlan', 'stt', 'gre', 'l3_ext',
name='nvp_network_bindings_binding_type'),
existing_nullable=True)
def downgrade(active_plugin=None, options=None):
if not migration.should_run(active_plugin, migration_for_plugins):
return
op.alter_column('nvp_network_bindings', 'phy_uuid',
name='tz_uuid',
existing_type=sa.String(36),
existing_nullable=True)
op.alter_column('nvp_network_bindings', 'binding_type',
type_=sa.Enum('flat', 'vlan', 'stt', 'gre',
name='nvp_network_bindings_binding_type'),
existing_nullable=True)

View File

@ -78,6 +78,7 @@ NVP_EXTGW_NAT_RULES_ORDER = 255
# Provider network extension - allowed network types for the NVP Plugin
class NetworkTypes:
""" Allowed provider network types for the NVP Plugin """
L3_EXT = 'l3_ext'
STT = 'stt'
GRE = 'gre'
FLAT = 'flat'
@ -330,6 +331,7 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
def _create_and_attach_router_port(self, cluster, context,
router_id, port_data,
attachment_type, attachment,
attachment_vlan=None,
subnet_ids=None):
# Use a fake IP address if gateway port is not 'real'
ip_addresses = (port_data.get('fake_ext_gw') and
@ -352,33 +354,43 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
"%(router_id)s"),
{'port_id': port_data.get('id'),
'router_id': router_id})
self._update_router_port_attachment(cluster, context, router_id,
port_data, attachment_type,
attachment, attachment_vlan,
lrouter_port['uuid'])
return lrouter_port
def _update_router_port_attachment(self, cluster, context,
router_id, port_data,
attachment_type, attachment,
attachment_vlan=None,
nvp_router_port_id=None):
if not nvp_router_port_id:
nvp_router_port_id = self._find_router_gw_port(context, port_data)
try:
# Add a L3 gateway attachment
# TODO(Salvatore-Orlando): Allow per router specification of
# l3 gw service uuid as well as per-tenant specification
nvplib.plug_router_port_attachment(cluster, router_id,
lrouter_port['uuid'],
nvp_router_port_id,
attachment,
attachment_type)
attachment_type,
attachment_vlan)
LOG.debug(_("Attached %(att)s to NVP router port %(port)s"),
{'att': attachment, 'port': lrouter_port['uuid']})
{'att': attachment, 'port': nvp_router_port_id})
except NvpApiClient.NvpApiException:
# Must remove NVP logical port
nvplib.delete_router_lport(cluster, router_id,
lrouter_port['uuid'])
nvp_router_port_id)
LOG.exception(_("Unable to plug attachment in NVP logical "
"router port %(r_port_id)s, associated with "
"Quantum %(q_port_id)s"),
{'r_port_id': lrouter_port['uuid'],
{'r_port_id': nvp_router_port_id,
'q_port_id': port_data.get('id')})
raise nvp_exc.NvpPluginException(
err_msg=(_("Unable to plug attachment in router port "
"%(r_port_id)s for quantum port id %(q_port_id)s "
"on router %(router_id)s") %
{'r_port_id': lrouter_port['uuid'],
{'r_port_id': nvp_router_port_id,
'q_port_id': port_data.get('id'),
'router_id': router_id}))
return lrouter_port
def _get_port_by_device_id(self, context, device_id, device_owner):
""" Retrieve ports associated with a specific device id.
@ -596,6 +608,15 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
port_data['name'],
True,
ip_addresses)
ext_network = self.get_network(context, port_data['network_id'])
if ext_network.get(pnet.NETWORK_TYPE) == NetworkTypes.L3_EXT:
# Update attachment
self._update_router_port_attachment(
cluster, context, router_id, port_data,
"L3GatewayAttachment",
ext_network[pnet.PHYSICAL_NETWORK],
ext_network[pnet.SEGMENTATION_ID],
lr_port['uuid'])
# Set the SNAT rule for each subnet (only first IP)
for cidr in self._find_router_subnets_cidrs(context, router_id):
nvplib.create_lrouter_snat_rule(
@ -635,6 +656,12 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
cluster, router_id, "SourceNatRule",
max_num_expected=1, min_num_expected=1,
source_ip_addresses=cidr)
# Reset attachment
self._update_router_port_attachment(
cluster, context, router_id, port_data,
"L3GatewayAttachment",
self.default_cluster.default_l3_gw_service_uuid,
nvp_router_port_id=lr_port['uuid'])
except NvpApiClient.ResourceNotFound:
raise nvp_exc.NvpPluginException(
@ -780,6 +807,10 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
if binding:
raise q_exc.VlanIdInUse(vlan_id=segmentation_id,
physical_network=physical_network)
elif network_type == NetworkTypes.L3_EXT:
if (segmentation_id_set and
(segmentation_id < 1 or segmentation_id > 4094)):
err_msg = _("%s out of range (1 to 4094)") % segmentation_id
else:
err_msg = _("%(net_type_param)s %(net_type_value)s not "
"supported") % {'net_type_param': pnet.NETWORK_TYPE,
@ -796,10 +827,10 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
network['id'])
# With NVP plugin 'normal' overlay networks will have no binding
# TODO(salvatore-orlando) make sure users can specify a distinct
# tz_uuid as 'provider network' for STT net type
# phy_uuid as 'provider network' for STT net type
if binding:
network[pnet.NETWORK_TYPE] = binding.binding_type
network[pnet.PHYSICAL_NETWORK] = binding.tz_uuid
network[pnet.PHYSICAL_NETWORK] = binding.phy_uuid
network[pnet.SEGMENTATION_ID] = binding.vlan_id
def _handle_lswitch_selection(self, cluster, network,
@ -829,7 +860,7 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
cluster, network.tenant_id,
"%s-ext-%s" % (network.name, len(lswitches)),
network_binding.binding_type,
network_binding.tz_uuid,
network_binding.phy_uuid,
network_binding.vlan_id,
network.id)
return selected_lswitch
@ -907,7 +938,7 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
context.session, new_net['id'],
net_data.get(pnet.NETWORK_TYPE),
net_data.get(pnet.PHYSICAL_NETWORK),
net_data.get(pnet.SEGMENTATION_ID))
net_data.get(pnet.SEGMENTATION_ID, 0))
self._extend_network_dict_provider(context, new_net,
net_binding)
self._extend_network_port_security_dict(context, new_net)

View File

@ -47,10 +47,22 @@ def get_network_binding_by_vlanid(session, vlan_id):
return
def add_network_binding(session, network_id, binding_type, tz_uuid, vlan_id):
def get_network_binding_by_vlanid_and_phynet(session, vlan_id,
physical_network):
session = session or db.get_session()
try:
binding = (session.query(nicira_models.NvpNetworkBinding).
filter_by(vlan_id=vlan_id, phy_uuid=physical_network).
one())
return binding
except exc.NoResultFound:
return
def add_network_binding(session, network_id, binding_type, phy_uuid, vlan_id):
with session.begin(subtransactions=True):
binding = nicira_models.NvpNetworkBinding(network_id, binding_type,
tz_uuid, vlan_id)
phy_uuid, vlan_id)
session.add(binding)
return binding

View File

@ -33,22 +33,22 @@ class NvpNetworkBinding(model_base.BASEV2):
ForeignKey('networks.id', ondelete="CASCADE"),
primary_key=True)
# 'flat', 'vlan', stt' or 'gre'
binding_type = Column(Enum('flat', 'vlan', 'stt', 'gre',
binding_type = Column(Enum('flat', 'vlan', 'stt', 'gre', 'l3_ext',
name='nvp_network_bindings_binding_type'),
nullable=False)
tz_uuid = Column(String(36))
phy_uuid = Column(String(36))
vlan_id = Column(Integer)
def __init__(self, network_id, binding_type, tz_uuid, vlan_id):
def __init__(self, network_id, binding_type, phy_uuid, vlan_id):
self.network_id = network_id
self.binding_type = binding_type
self.tz_uuid = tz_uuid
self.phy_uuid = phy_uuid
self.vlan_id = vlan_id
def __repr__(self):
return "<NetworkBinding(%s,%s,%s,%s)>" % (self.network_id,
self.binding_type,
self.tz_uuid,
self.phy_uuid,
self.vlan_id)

View File

@ -869,18 +869,22 @@ def find_router_gw_port(context, cluster, router_id):
# TODO(salvatore-orlando): Consider storing it in Quantum DB
results = query_lrouter_lports(
cluster, router_id,
filters={'attachment_gwsvc_uuid': cluster.default_l3_gw_service_uuid})
if len(results):
# Return logical router port
return results[0]
relations="LogicalPortAttachment")
for lport in results:
if '_relations' in lport:
attachment = lport['_relations'].get('LogicalPortAttachment')
if attachment and attachment.get('type') == 'L3GatewayAttachment':
return lport
def plug_router_port_attachment(cluster, router_id, port_id,
attachment_uuid, nvp_attachment_type):
attachment_uuid, nvp_attachment_type,
attachment_vlan=None):
"""Attach a router port to the given attachment.
Current attachment types:
- PatchAttachment [-> logical switch port uuid]
- L3GatewayAttachment [-> L3GatewayService uuid]
For the latter attachment type a VLAN ID can be specified as well
"""
uri = _build_uri_path(LROUTERPORT_RESOURCE, port_id, router_id,
is_attachment=True)
@ -890,6 +894,8 @@ def plug_router_port_attachment(cluster, router_id, port_id,
attach_obj["peer_port_uuid"] = attachment_uuid
elif nvp_attachment_type == "L3GatewayAttachment":
attach_obj["l3_gateway_service_uuid"] = attachment_uuid
if attachment_vlan:
attach_obj['vlan_id'] = attachment_vlan
else:
raise Exception(_("Invalid NVP attachment type '%s'"),
nvp_attachment_type)

View File

@ -3,6 +3,8 @@
{
%(peer_port_href_field)s
%(peer_port_uuid_field)s
%(l3_gateway_service_uuid_field)s
%(vlan_id)s
"type": "%(type)s",
"schema": "/ws.v1/schema/%(type)s"
}

View File

@ -245,10 +245,9 @@ class FakeClient:
if not '_relations' in src or not src['_relations'].get(relation):
return # Item does not have relation
relation_data = src['_relations'].get(relation)
dst_relations = dst.get('_relations')
if not dst_relations:
dst_relations = {}
dst_relations = dst.get('_relations', {})
dst_relations[relation] = relation_data
dst['_relations'] = dst_relations
def _fill_attachment(self, att_data, ls_uuid=None,
lr_uuid=None, lp_uuid=None):
@ -266,7 +265,8 @@ class FakeClient:
else:
new_data['%s_field' % field_name] = ""
for field in ['vif_uuid', 'peer_port_href', 'peer_port_uuid']:
for field in ['vif_uuid', 'peer_port_href', 'vlan_id',
'peer_port_uuid', 'l3_gateway_service_uuid']:
populate_field(field)
return new_data
@ -460,13 +460,11 @@ class FakeClient:
if not is_attachment:
resource.update(json.loads(body))
else:
relations = resource.get("_relations")
if not relations:
relations = {}
relations['LogicalPortAttachment'] = json.loads(body)
resource['_relations'] = relations
relations = resource.get("_relations", {})
body_2 = json.loads(body)
resource['att_type'] = body_2['type']
relations['LogicalPortAttachment'] = body_2
resource['_relations'] = relations
if body_2['type'] == "PatchAttachment":
# We need to do a trick here
if self.LROUTER_RESOURCE in res_type:
@ -486,6 +484,7 @@ class FakeClient:
elif body_2['type'] == "L3GatewayAttachment":
resource['attachment_gwsvc_uuid'] = (
body_2['l3_gateway_service_uuid'])
resource['vlan_id'] = body_2.get('vlan_id')
elif body_2['type'] == "L2GatewayAttachment":
resource['attachment_gwsvc_uuid'] = (
body_2['l2_gateway_service_uuid'])

View File

@ -14,7 +14,6 @@
# limitations under the License.
import contextlib
import logging
import os
import mock
@ -26,6 +25,7 @@ import webob.exc
from quantum.common import constants
import quantum.common.test_lib as test_lib
from quantum import context
from quantum.extensions import l3
from quantum.extensions import providernet as pnet
from quantum.extensions import securitygroup as secgrp
from quantum import manager
@ -34,6 +34,7 @@ from quantum.plugins.nicira.nicira_nvp_plugin.extensions import nvp_networkgw
from quantum.plugins.nicira.nicira_nvp_plugin.extensions import (nvp_qos
as ext_qos)
from quantum.plugins.nicira.nicira_nvp_plugin import nvplib
from quantum.plugins.nicira.nicira_nvp_plugin import QuantumPlugin
from quantum.tests.unit.nicira import fake_nvpapiclient
import quantum.tests.unit.nicira.test_networkgw as test_l2_gw
import quantum.tests.unit.test_db_plugin as test_plugin
@ -42,7 +43,6 @@ import quantum.tests.unit.test_extension_security_group as ext_sg
from quantum.tests.unit import test_extensions
import quantum.tests.unit.test_l3_plugin as test_l3_plugin
LOG = logging.getLogger(__name__)
NICIRA_PKG_PATH = nvp_plugin.__name__
NICIRA_EXT_PATH = "../../plugins/nicira/nicira_nvp_plugin/extensions"
@ -261,6 +261,115 @@ class TestNiciraSecurityGroup(ext_sg.TestSecurityGroups,
class TestNiciraL3NatTestCase(test_l3_plugin.L3NatDBTestCase,
NiciraPluginV2TestCase):
def _create_l3_ext_network(self, vlan_id=None):
name = 'l3_ext_net'
net_type = QuantumPlugin.NetworkTypes.L3_EXT
providernet_args = {pnet.NETWORK_TYPE: net_type,
pnet.PHYSICAL_NETWORK: 'l3_gw_uuid'}
if vlan_id:
providernet_args[pnet.SEGMENTATION_ID] = vlan_id
return self.network(name=name,
router__external=True,
providernet_args=providernet_args,
arg_list=(pnet.NETWORK_TYPE,
pnet.PHYSICAL_NETWORK,
pnet.SEGMENTATION_ID))
def _test_create_l3_ext_network(self, vlan_id=None):
name = 'l3_ext_net'
net_type = QuantumPlugin.NetworkTypes.L3_EXT
expected = [('subnets', []), ('name', name), ('admin_state_up', True),
('status', 'ACTIVE'), ('shared', False),
(l3.EXTERNAL, True),
(pnet.NETWORK_TYPE, net_type),
(pnet.PHYSICAL_NETWORK, 'l3_gw_uuid'),
(pnet.SEGMENTATION_ID, vlan_id)]
with self._create_l3_ext_network(vlan_id) as net:
for k, v in expected:
self.assertEqual(net['network'][k], v)
def _nvp_validate_ext_gw(self, router_id, l3_gw_uuid, vlan_id):
""" Verify data on fake NVP API client in order to validate
plugin did set them properly"""
ports = [port for port in self.fc._fake_lrouter_lport_dict.values()
if (port['lr_uuid'] == router_id and
port['att_type'] == "L3GatewayAttachment")]
self.assertEqual(len(ports), 1)
self.assertEqual(ports[0]['attachment_gwsvc_uuid'], l3_gw_uuid)
self.assertEqual(ports[0].get('vlan_id'), vlan_id)
def test_create_l3_ext_network_without_vlan(self):
self._test_create_l3_ext_network()
def _test_router_create_with_gwinfo_and_l3_ext_net(self, vlan_id=None):
with self._create_l3_ext_network(vlan_id) as net:
with self.subnet(network=net) as s:
data = {'router': {'tenant_id': 'whatever'}}
data['router']['name'] = 'router1'
data['router']['external_gateway_info'] = {
'network_id': s['subnet']['network_id']}
router_req = self.new_create_request('routers', data,
self.fmt)
try:
res = router_req.get_response(self.ext_api)
router = self.deserialize(self.fmt, res)
self.assertEqual(
s['subnet']['network_id'],
(router['router']['external_gateway_info']
['network_id']))
self._nvp_validate_ext_gw(router['router']['id'],
'l3_gw_uuid', vlan_id)
finally:
self._delete('routers', router['router']['id'])
def test_router_create_with_gwinfo_and_l3_ext_net(self):
self._test_router_create_with_gwinfo_and_l3_ext_net()
def test_router_create_with_gwinfo_and_l3_ext_net_with_vlan(self):
self._test_router_create_with_gwinfo_and_l3_ext_net(444)
def _test_router_update_gateway_on_l3_ext_net(self, vlan_id=None):
with self.router() as r:
with self.subnet() as s1:
with self._create_l3_ext_network(vlan_id) as net:
with self.subnet(network=net) as s2:
self._set_net_external(s1['subnet']['network_id'])
try:
self._add_external_gateway_to_router(
r['router']['id'],
s1['subnet']['network_id'])
body = self._show('routers', r['router']['id'])
net_id = (body['router']
['external_gateway_info']['network_id'])
self.assertEqual(net_id,
s1['subnet']['network_id'])
# Plug network with external mapping
self._set_net_external(s2['subnet']['network_id'])
self._add_external_gateway_to_router(
r['router']['id'],
s2['subnet']['network_id'])
body = self._show('routers', r['router']['id'])
net_id = (body['router']
['external_gateway_info']['network_id'])
self.assertEqual(net_id,
s2['subnet']['network_id'])
self._nvp_validate_ext_gw(body['router']['id'],
'l3_gw_uuid', vlan_id)
finally:
# Cleanup
self._remove_external_gateway_from_router(
r['router']['id'],
s2['subnet']['network_id'])
def test_router_update_gateway_on_l3_ext_net(self):
self._test_router_update_gateway_on_l3_ext_net()
def test_router_update_gateway_on_l3_ext_net_with_vlan(self):
self._test_router_update_gateway_on_l3_ext_net(444)
def test_create_l3_ext_network_with_vlan(self):
self._test_create_l3_ext_network(666)
def test_floatingip_with_assoc_fails(self):
self._test_floatingip_with_assoc_fails(
'quantum.plugins.nicira.nicira_nvp_plugin.'

View File

@ -333,7 +333,7 @@ class L3NatTestCaseBase(test_db_plugin.QuantumDbPluginV2TestCase):
new_args = dict(itertools.izip(map(lambda x: x.replace('__', ':'),
kwargs),
kwargs.values()))
arg_list = (l3.EXTERNAL,)
arg_list = new_args.pop('arg_list', ()) + (l3.EXTERNAL,)
return super(L3NatTestCaseBase, self)._create_network(
fmt, name, admin_state_up, arg_list=arg_list, **new_args)