From fedfa5962f7da5d6155ea4248835d74f6fa11862 Mon Sep 17 00:00:00 2001 From: Janet Yu Date: Sun, 12 Jul 2015 22:53:33 -0700 Subject: [PATCH] NSXv3: Support network creation options Handle admin state and provider network options during NSXv3 network creation. Change-Id: I13a848f943d0e983542d16f26a821bd9a3899442 --- devstack/lib/vmware_nsx_v3 | 7 +- .../neutron/plugins/vmware/common/config.py | 16 +- .../neutron/plugins/vmware/common/utils.py | 8 + .../plugins/vmware/nsxlib/v3/__init__.py | 11 +- .../plugins/vmware/plugins/nsx_v3_plugin.py | 157 +++++++++++++++++- .../neutron/tests/unit/vmware/nsx_v3_mocks.py | 26 +-- .../unit/vmware/nsxlib/v3/test_switch.py | 57 ++++++- .../tests/unit/vmware/test_constants_v3.py | 34 ---- 8 files changed, 254 insertions(+), 62 deletions(-) diff --git a/devstack/lib/vmware_nsx_v3 b/devstack/lib/vmware_nsx_v3 index 8482f4c0c7..becdc53acd 100644 --- a/devstack/lib/vmware_nsx_v3 +++ b/devstack/lib/vmware_nsx_v3 @@ -85,11 +85,14 @@ function neutron_plugin_configure_plugin_agent { } function neutron_plugin_configure_service { - if [[ "$DEFAULT_TZ_UUID" != "" ]]; then - iniset /$Q_PLUGIN_CONF_FILE DEFAULT default_tz_uuid $DEFAULT_TZ_UUID + if [[ "$DEFAULT_OVERLAY_TZ_UUID" != "" ]]; then + iniset /$Q_PLUGIN_CONF_FILE nsx_v3 default_overlay_tz_uuid $DEFAULT_OVERLAY_TZ_UUID else die $LINENO "The VMware NSX plugin won't work without a default transport zone." fi + if [[ "$DEFAULT_VLAN_TZ_UUID" != "" ]]; then + iniset /$Q_PLUGIN_CONF_FILE nsx_v3 default_vlan_tz_uuid $DEFAULT_VLAN_TZ_UUID + fi if [[ "$DEFAULT_EDGE_CLUSTER_UUID" != "" ]]; then iniset /$Q_PLUGIN_CONF_FILE nsx_v3 default_edge_cluster_uuid $DEFAULT_EDGE_CLUSTER_UUID Q_L3_ENABLED=True diff --git a/vmware_nsx/neutron/plugins/vmware/common/config.py b/vmware_nsx/neutron/plugins/vmware/common/config.py index 0037fd27a9..6ef44b4e11 100644 --- a/vmware_nsx/neutron/plugins/vmware/common/config.py +++ b/vmware_nsx/neutron/plugins/vmware/common/config.py @@ -172,9 +172,23 @@ nsx_v3_opts = [ help=_('IP address of the NSX manager')), cfg.StrOpt('default_edge_cluster_uuid', help=_("Default edge cluster identifier")), + cfg.StrOpt('default_overlay_tz_uuid', + deprecated_name='default_tz_uuid', + help=_("This is the UUID of the default NSX overlay transport " + "zone that will be used for creating tunneled isolated " + "Neutron networks. It needs to be created in NSX " + "before starting Neutron with the NSX plugin.")), + cfg.StrOpt('default_vlan_tz_uuid', + help=_("This is the UUID of the default NSX VLAN transport " + "zone that will be used for bridging between Neutron " + "networks. It needs to be created in NSX before " + "starting Neutron with the NSX plugin.")), + cfg.StrOpt('default_edge_cluster_uuid', + help=_("Default edge cluster identifier")), cfg.IntOpt('retries', default=10, - help=_('Maximum number of times to retry API request'))] + help=_('Maximum number of times to retry API request')) +] DEFAULT_STATUS_CHECK_INTERVAL = 2000 DEFAULT_MINIMUM_POOLED_EDGES = 1 diff --git a/vmware_nsx/neutron/plugins/vmware/common/utils.py b/vmware_nsx/neutron/plugins/vmware/common/utils.py index 9da72f2e20..db65b20d46 100644 --- a/vmware_nsx/neutron/plugins/vmware/common/utils.py +++ b/vmware_nsx/neutron/plugins/vmware/common/utils.py @@ -47,6 +47,14 @@ class NsxVNetworkTypes: PORTGROUP = 'portgroup' +# Allowed network types for the NSXv3 Plugin +class NsxV3NetworkTypes: + """Allowed provider network types for the NSXv3 Plugin.""" + FLAT = 'flat' + VLAN = 'vlan' + VXLAN = 'vxlan' + + def get_tags(**kwargs): tags = ([dict(tag=value, scope=key) for key, value in six.iteritems(kwargs)]) diff --git a/vmware_nsx/neutron/plugins/vmware/nsxlib/v3/__init__.py b/vmware_nsx/neutron/plugins/vmware/nsxlib/v3/__init__.py index bf29ff5759..8ddeb59ccf 100644 --- a/vmware_nsx/neutron/plugins/vmware/nsxlib/v3/__init__.py +++ b/vmware_nsx/neutron/plugins/vmware/nsxlib/v3/__init__.py @@ -26,7 +26,7 @@ LOG = log.getLogger(__name__) def create_logical_switch(display_name, transport_zone_id, tags, replication_mode=nsx_constants.MTEP, - admin_state=nsx_constants.ADMIN_STATE_UP): + admin_state=True, vlan_id=None): # TODO(salv-orlando): Validate Replication mode and admin_state # NOTE: These checks might be moved to the API client library if one that # performs such checks in the client is available @@ -34,10 +34,17 @@ def create_logical_switch(display_name, transport_zone_id, tags, resource = 'logical-switches' body = {'transport_zone_id': transport_zone_id, 'replication_mode': replication_mode, - 'admin_state': admin_state, 'display_name': display_name, 'tags': tags} + if admin_state: + body['admin_state'] = nsx_constants.ADMIN_STATE_UP + else: + body['admin_state'] = nsx_constants.ADMIN_STATE_DOWN + + if vlan_id: + body['vlan'] = vlan_id + return client.create_resource(resource, body) diff --git a/vmware_nsx/neutron/plugins/vmware/plugins/nsx_v3_plugin.py b/vmware_nsx/neutron/plugins/vmware/plugins/nsx_v3_plugin.py index 8c06aaec9f..44f40e2134 100644 --- a/vmware_nsx/neutron/plugins/vmware/plugins/nsx_v3_plugin.py +++ b/vmware_nsx/neutron/plugins/vmware/plugins/nsx_v3_plugin.py @@ -28,8 +28,10 @@ from neutron.api.rpc.handlers import metadata_rpc from neutron.api.v2 import attributes from neutron.extensions import l3 from neutron.extensions import portbindings as pbin +from neutron.extensions import providernet as pnet from neutron.common import constants as const +from neutron.common import exceptions as n_exc from neutron.common import rpc as n_rpc from neutron.common import topics from neutron.db import agents_db @@ -40,6 +42,8 @@ from neutron.db import models_v2 from neutron.db import portbindings_db from neutron.db import securitygroups_db from neutron.i18n import _LE, _LW +from neutron.plugins.common import constants as plugin_const +from neutron.plugins.common import utils as n_utils from vmware_nsx.neutron.plugins.vmware.common import config # noqa from vmware_nsx.neutron.plugins.vmware.common import exceptions as nsx_exc @@ -65,7 +69,8 @@ class NsxV3Plugin(db_base_plugin_v2.NeutronDbPluginV2, supported_extension_aliases = ["quotas", "binding", "security-group", - "router"] + "router", + "provider"] def __init__(self): super(NsxV3Plugin, self).__init__() @@ -95,17 +100,130 @@ class NsxV3Plugin(db_base_plugin_v2.NeutronDbPluginV2, self.supported_extension_aliases.extend( ['agent', 'dhcp_agent_scheduler']) + def _validate_provider_create(self, context, network_data): + physical_net = network_data.get(pnet.PHYSICAL_NETWORK) + if not attributes.is_attr_set(physical_net): + physical_net = None + + vlan_id = network_data.get(pnet.SEGMENTATION_ID) + if not attributes.is_attr_set(vlan_id): + vlan_id = None + + err_msg = None + net_type = network_data.get(pnet.NETWORK_TYPE) + if attributes.is_attr_set(net_type): + if net_type == utils.NsxV3NetworkTypes.FLAT: + if vlan_id is not None: + err_msg = (_("Segmentation ID cannot be specified with " + "%s network type") % + utils.NsxV3NetworkTypes.FLAT) + else: + # Set VLAN id to 0 for flat networks + vlan_id = '0' + if physical_net is None: + physical_net = cfg.CONF.nsx_v3.default_vlan_tz_uuid + elif net_type == utils.NsxV3NetworkTypes.VLAN: + # Use default VLAN transport zone if physical network not given + if physical_net is None: + physical_net = cfg.CONF.nsx_v3.default_vlan_tz_uuid + + # Validate VLAN id + if not vlan_id: + err_msg = (_('Segmentation ID must be specified with %s ' + 'network type') % + utils.NsxV3NetworkTypes.VLAN) + elif not n_utils.is_valid_vlan_tag(vlan_id): + err_msg = (_('Segmentation ID %(segmentation_id)s out of ' + 'range (%(min_id)s through %(max_id)s)') % + {'segmentation_id': vlan_id, + 'min_id': plugin_const.MIN_VLAN_TAG, + 'max_id': plugin_const.MAX_VLAN_TAG}) + else: + # Verify VLAN id is not already allocated + bindings = ( + nsx_db.get_network_bindings_by_vlanid_and_physical_net( + context.session, vlan_id, physical_net) + ) + if bindings: + raise n_exc.VlanIdInUse( + vlan_id=vlan_id, physical_network=physical_net) + elif net_type == utils.NsxV3NetworkTypes.VXLAN: + if vlan_id: + err_msg = (_("Segmentation ID cannot be specified with " + "%s network type") % + utils.NsxV3NetworkTypes.VXLAN) + else: + err_msg = (_('%(net_type_param)s %(net_type_value)s not ' + 'supported') % + {'net_type_param': pnet.NETWORK_TYPE, + 'net_type_value': net_type}) + else: + net_type = None + + if err_msg: + raise n_exc.InvalidInput(error_message=err_msg) + + if physical_net is None: + # Default to transport type overlay + physical_net = cfg.CONF.nsx_v3.default_overlay_tz_uuid + + return net_type, physical_net, vlan_id + + def _extend_network_dict_provider(self, context, network, bindings=None): + if not bindings: + bindings = nsx_db.get_network_bindings(context.session, + network['id']) + # With NSX plugin, "normal" overlay networks will have no binding + if bindings: + # Network came in through provider networks API + network[pnet.NETWORK_TYPE] = bindings[0].binding_type + network[pnet.PHYSICAL_NETWORK] = bindings[0].phy_uuid + network[pnet.SEGMENTATION_ID] = bindings[0].vlan_id + def create_network(self, context, network): + # TODO(jwy): Handle creating external network (--router:external) + is_provider_net = any(attributes.is_attr_set(network['network'].get(f)) + for f in (pnet.NETWORK_TYPE, + pnet.PHYSICAL_NETWORK, + pnet.SEGMENTATION_ID)) + net_type, physical_net, vlan_id = self._validate_provider_create( + context, network['network']) + net_name = network['network']['name'] tags = utils.build_v3_tags_payload(network['network']) - result = nsxlib.create_logical_switch( - network['network']['name'], - cfg.CONF.default_tz_uuid, tags) - network['network']['id'] = result['id'] + admin_state = network['network'].get('admin_state_up', True) + + # Create network on the backend + LOG.debug('create_network: %(net_name)s, %(physical_net)s, %(tags)s, ' + '%(admin_state)s, %(vlan_id)s', + {'net_name': net_name, + 'physical_net': physical_net, + 'tags': tags, + 'admin_state': admin_state, + 'vlan_id': vlan_id}) + result = nsxlib.create_logical_switch(net_name, physical_net, tags, + admin_state=admin_state, + vlan_id=vlan_id) + net_id = result['id'] + network['network']['id'] = net_id tenant_id = self._get_tenant_id_for_create(context, network['network']) self._ensure_default_security_group(context, tenant_id) - network = super(NsxV3Plugin, self).create_network(context, network) - # TODO(salv-orlando): Undo logical switch creation on failure + with context.session.begin(subtransactions=True): + # Create network in Neutron + try: + network = super(NsxV3Plugin, self).create_network(context, + network) + except Exception: + with excutils.save_and_reraise_exception(): + # Undo creation on the backend + LOG.exception(_LE('Failed to create network %s'), net_id) + nsxlib.delete_logical_switch(net_id) + + if is_provider_net: + # Save provider network fields, needed by get_network() + nsx_db.add_network_binding(context.session, net_id, net_type, + physical_net, vlan_id) + return network def delete_network(self, context, network_id): @@ -136,6 +254,31 @@ class NsxV3Plugin(db_base_plugin_v2.NeutronDbPluginV2, 'ip_address': fixed_ip['ip_address']}) return address_bindings + def get_network(self, context, id, fields=None): + with context.session.begin(subtransactions=True): + # Get network from Neutron database + network = self._get_network(context, id) + # Don't do field selection here otherwise we won't be able to add + # provider networks fields + net = self._make_network_dict(network, context=context) + self._extend_network_dict_provider(context, net) + return self._fields(net, fields) + + def get_networks(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, + page_reverse=False): + # Get networks from Neutron database + filters = filters or {} + with context.session.begin(subtransactions=True): + networks = ( + super(NsxV3Plugin, self).get_networks( + context, filters, fields, sorts, + limit, marker, page_reverse)) + # Add provider network fields + for net in networks: + self._extend_network_dict_provider(context, net) + return [self._fields(network, fields) for network in networks] + def create_port(self, context, port): port_id = uuidutils.generate_uuid() tags = utils.build_v3_tags_payload(port['port']) diff --git a/vmware_nsx/neutron/tests/unit/vmware/nsx_v3_mocks.py b/vmware_nsx/neutron/tests/unit/vmware/nsx_v3_mocks.py index d5c218d09b..1a6ae563ec 100644 --- a/vmware_nsx/neutron/tests/unit/vmware/nsx_v3_mocks.py +++ b/vmware_nsx/neutron/tests/unit/vmware/nsx_v3_mocks.py @@ -22,18 +22,18 @@ from vmware_nsx.neutron.plugins.vmware.common import nsx_constants FAKE_NAME = "fake_name" -def create_logical_switch(display_name, transport_zone_id, tags, - replication_mode=nsx_constants.MTEP, - admin_state=nsx_constants.ADMIN_STATE_UP): - FAKE_TZ_UUID = uuidutils.generate_uuid() - FAKE_SWITCH_UUID = uuidutils.generate_uuid() +def make_fake_switch(switch_uuid=None, tz_uuid=None, name=FAKE_NAME): + if not switch_uuid: + switch_uuid = uuidutils.generate_uuid() + if not tz_uuid: + tz_uuid = uuidutils.generate_uuid() - FAKE_SWITCH = { - "id": FAKE_SWITCH_UUID, - "display_name": FAKE_NAME, + fake_switch = { + "id": switch_uuid, + "display_name": name, "resource_type": "LogicalSwitch", "address_bindings": [], - "transport_zone_id": FAKE_TZ_UUID, + "transport_zone_id": tz_uuid, "replication_mode": nsx_constants.MTEP, "admin_state": nsx_constants.ADMIN_STATE_UP, "vni": 50056, @@ -60,7 +60,13 @@ def create_logical_switch(display_name, transport_zone_id, tags, } ], } - return FAKE_SWITCH + return fake_switch + + +def create_logical_switch(display_name, transport_zone_id, tags, + replication_mode=nsx_constants.MTEP, + admin_state=True, vlan_id=None): + return make_fake_switch() def create_logical_port(lswitch_id, vif_uuid, tags, diff --git a/vmware_nsx/neutron/tests/unit/vmware/nsxlib/v3/test_switch.py b/vmware_nsx/neutron/tests/unit/vmware/nsxlib/v3/test_switch.py index 50da4e91aa..f3ea54f5ae 100644 --- a/vmware_nsx/neutron/tests/unit/vmware/nsxlib/v3/test_switch.py +++ b/vmware_nsx/neutron/tests/unit/vmware/nsxlib/v3/test_switch.py @@ -17,10 +17,12 @@ import mock from oslo_log import log +from oslo_utils import uuidutils +from vmware_nsx.neutron.plugins.vmware.common import nsx_constants from vmware_nsx.neutron.plugins.vmware.nsxlib import v3 as nsxlib +from vmware_nsx.neutron.tests.unit.vmware import nsx_v3_mocks from vmware_nsx.neutron.tests.unit.vmware.nsxlib.v3 import nsxlib_testcase -from vmware_nsx.neutron.tests.unit.vmware import test_constants_v3 LOG = log.getLogger(__name__) @@ -33,9 +35,52 @@ class NsxLibSwitchTestCase(nsxlib_testcase.NsxLibTestCase): """ Test creating a switch returns the correct response and 200 status """ - mock_create_resource.return_value = test_constants_v3.FAKE_SWITCH + tz_uuid = uuidutils.generate_uuid() + fake_switch = nsx_v3_mocks.make_fake_switch(tz_uuid=tz_uuid) + mock_create_resource.return_value = fake_switch - result = nsxlib.create_logical_switch(test_constants_v3.FAKE_NAME, - test_constants_v3.FAKE_TZ_UUID, - tags={}) - self.assertEqual(test_constants_v3.FAKE_SWITCH, result) + result = nsxlib.create_logical_switch(nsx_v3_mocks.FAKE_NAME, tz_uuid, + []) + self.assertEqual(fake_switch, result) + + @mock.patch("vmware_nsx.neutron.plugins.vmware.nsxlib.v3" + ".client.create_resource") + def test_create_logical_switch_admin_down(self, mock_create_resource): + """ + Test creating switch with admin_state down + """ + tz_uuid = uuidutils.generate_uuid() + fake_switch = nsx_v3_mocks.make_fake_switch(tz_uuid=tz_uuid) + fake_switch['admin_state'] = nsx_constants.ADMIN_STATE_DOWN + mock_create_resource.return_value = fake_switch + + result = nsxlib.create_logical_switch(nsx_v3_mocks.FAKE_NAME, tz_uuid, + [], admin_state=False) + self.assertEqual(fake_switch, result) + + @mock.patch("vmware_nsx.neutron.plugins.vmware.nsxlib.v3" + ".client.create_resource") + def test_create_logical_switch_vlan(self, mock_create_resource): + """ + Test creating switch with provider:network_type VLAN + """ + tz_uuid = uuidutils.generate_uuid() + fake_switch = nsx_v3_mocks.make_fake_switch() + fake_switch['vlan_id'] = '123' + mock_create_resource.return_value = fake_switch + + result = nsxlib.create_logical_switch(nsx_v3_mocks.FAKE_NAME, tz_uuid, + []) + self.assertEqual(fake_switch, result) + + @mock.patch("vmware_nsx.neutron.plugins.vmware.nsxlib.v3" + ".client.delete_resource") + def test_delete_logical_switch(self, mock_delete_resource): + """ + Test deleting switch + """ + mock_delete_resource.return_value = None + + fake_switch = nsx_v3_mocks.make_fake_switch() + result = nsxlib.delete_logical_switch(fake_switch['id']) + self.assertIsNone(result) diff --git a/vmware_nsx/neutron/tests/unit/vmware/test_constants_v3.py b/vmware_nsx/neutron/tests/unit/vmware/test_constants_v3.py index cf5a860334..1a28f40763 100644 --- a/vmware_nsx/neutron/tests/unit/vmware/test_constants_v3.py +++ b/vmware_nsx/neutron/tests/unit/vmware/test_constants_v3.py @@ -17,42 +17,8 @@ from oslo_utils import uuidutils FAKE_NAME = "fake_name" -FAKE_TZ_UUID = uuidutils.generate_uuid() FAKE_SWITCH_UUID = uuidutils.generate_uuid() -FAKE_SWITCH = { - "id": FAKE_SWITCH_UUID, - "display_name": FAKE_NAME, - "resource_type": "LogicalSwitch", - "address_bindings": [], - "transport_zone_id": FAKE_TZ_UUID, - "replication_mode": "MTEP", - "admin_state": "UP", - "vni": 50056, - "switching_profile_ids": [ - { - "value": "64814784-7896-3901-9741-badeff705639", - "key": "IpDiscoverySwitchingProfile" - }, - { - "value": "fad98876-d7ff-11e4-b9d6-1681e6b88ec1", - "key": "SpoofGuardSwitchingProfile" - }, - { - "value": "93b4b7e8-f116-415d-a50c-3364611b5d09", - "key": "PortMirroringSwitchingProfile" - }, - { - "value": "fbc4fb17-83d9-4b53-a286-ccdf04301888", - "key": "SwitchSecuritySwitchingProfile" - }, - { - "value": "f313290b-eba8-4262-bd93-fab5026e9495", - "key": "QosSwitchingProfile" - } - ], -} - FAKE_PORT_UUID = uuidutils.generate_uuid() FAKE_PORT = { "id": FAKE_PORT_UUID,