diff --git a/releasenotes/notes/nsxv3-vlan-selection-30c3d1dc1abe41d1.yaml b/releasenotes/notes/nsxv3-vlan-selection-30c3d1dc1abe41d1.yaml new file mode 100644 index 0000000000..bb0cdb969f --- /dev/null +++ b/releasenotes/notes/nsxv3-vlan-selection-30c3d1dc1abe41d1.yaml @@ -0,0 +1,9 @@ +--- +prelude: > + The NSX-V3 plugin can decide on the VLAN tag for a provider network. +features: + - | + The NSX-V3 plugin can decide on the VLAN tag for a provider network, + according to pre-defined configuration set per transport zone UUID, + noting a specific range or letting the plugin decide according to + min/max constants. diff --git a/vmware_nsx/common/config.py b/vmware_nsx/common/config.py index 2c7a29e153..e425aa6966 100644 --- a/vmware_nsx/common/config.py +++ b/vmware_nsx/common/config.py @@ -438,6 +438,12 @@ nsx_v3_opts = [ default=False, help=_("(Optional) Indicates whether ENS transport zones can " "be used")), + cfg.ListOpt('network_vlan_ranges', + default=[], + help=_("List of :: " + "specifying Transport Zone UUID usable for VLAN " + "provider networks, as well as ranges of VLAN " + "tags on each available for allocation to networks.")), ] DEFAULT_STATUS_CHECK_INTERVAL = 2000 diff --git a/vmware_nsx/db/db.py b/vmware_nsx/db/db.py index 71981732c4..8d47f35a27 100644 --- a/vmware_nsx/db/db.py +++ b/vmware_nsx/db/db.py @@ -51,6 +51,13 @@ def get_network_bindings(session, network_id): all()) +def get_network_bindings_by_phy_uuid(session, phy_uuid): + session = session or db.get_reader_session() + return (session.query(nsx_models.TzNetworkBinding). + filter_by(phy_uuid=phy_uuid). + all()) + + def get_network_bindings_by_vlanid_and_physical_net(session, vlan_id, phy_uuid): session = session or db.get_reader_session() diff --git a/vmware_nsx/plugins/nsx_v3/plugin.py b/vmware_nsx/plugins/nsx_v3/plugin.py index 4f9e672716..eddeccbeb1 100644 --- a/vmware_nsx/plugins/nsx_v3/plugin.py +++ b/vmware_nsx/plugins/nsx_v3/plugin.py @@ -81,6 +81,7 @@ from oslo_log import log from oslo_utils import excutils from oslo_utils import importutils from oslo_utils import uuidutils +from six import moves from sqlalchemy import exc as sql_exc import webob.exc @@ -249,6 +250,8 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, self.cfg_group = 'nsx_v3' # group name for nsx_v3 section in nsx.ini self.tier0_groups_dict = {} + self._network_vlans = n_utils.parse_network_vlan_ranges( + cfg.CONF.nsx_v3.network_vlan_ranges) # Initialize the network availability zones, which will be used only # when native_dhcp_metadata is True self.init_availability_zones() @@ -803,9 +806,9 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, # Validate VLAN id if not vlan_id: - err_msg = (_('Segmentation ID must be specified with %s ' - 'network type') % - utils.NsxV3NetworkTypes.VLAN) + vlan_id = self._generate_segment_id(context, + physical_net, + network_data) 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)') % @@ -1043,6 +1046,26 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, return (bindings[0].binding_type == utils.NsxV3NetworkTypes.NSX_NETWORK) + def _generate_segment_id(self, context, physical_network, net_data): + bindings = nsx_db.get_network_bindings_by_phy_uuid( + context.session, physical_network) + vlan_ranges = self._network_vlans.get(physical_network, []) + if vlan_ranges: + vlan_ids = set() + for vlan_min, vlan_max in vlan_ranges: + vlan_ids |= set(moves.range(vlan_min, vlan_max + 1)) + else: + vlan_min = const.MIN_VLAN_TAG + vlan_max = const.MAX_VLAN_TAG + vlan_ids = set(moves.range(vlan_min, vlan_max + 1)) + used_ids_in_range = set([binding.vlan_id for binding in bindings + if binding.vlan_id in vlan_ids]) + free_ids = list(vlan_ids ^ used_ids_in_range) + if len(free_ids) == 0: + raise n_exc.NoNetworkAvailable() + net_data[pnet.SEGMENTATION_ID] = free_ids[0] + return net_data[pnet.SEGMENTATION_ID] + def create_network(self, context, network): net_data = network['network'] external = net_data.get(extnet_apidef.EXTERNAL) diff --git a/vmware_nsx/tests/unit/nsx_v3/test_plugin.py b/vmware_nsx/tests/unit/nsx_v3/test_plugin.py index bd017f14a1..1e667f8f7b 100644 --- a/vmware_nsx/tests/unit/nsx_v3/test_plugin.py +++ b/vmware_nsx/tests/unit/nsx_v3/test_plugin.py @@ -19,6 +19,7 @@ from neutron.db import models_v2 from neutron.extensions import address_scope from neutron.extensions import l3 from neutron.extensions import securitygroup as secgrp +from neutron.plugins.common import utils as n_utils from neutron.tests.unit import _test_extension_portbindings as test_bindings from neutron.tests.unit.db import test_db_base_plugin_v2 as test_plugin from neutron.tests.unit.extensions import test_address_scope @@ -29,6 +30,7 @@ from neutron.tests.unit.extensions \ import test_l3_ext_gw_mode as test_ext_gw_mode from neutron.tests.unit.scheduler \ import test_dhcp_agent_scheduler as test_dhcpagent +from neutron.tests.unit import testlib_api from neutron_lib.api.definitions import external_net as extnet_apidef from neutron_lib.api.definitions import extraroute as xroute_apidef from neutron_lib.api.definitions import l3_ext_gw_mode as l3_egm_apidef @@ -546,6 +548,57 @@ class TestNetworksV2(test_plugin.TestNetworksV2, NsxV3PluginTestCaseMixin): data = self.deserialize('json', result) self.assertEqual('vlan', data['network'].get(pnet.NETWORK_TYPE)) + def _test_generate_tag(self, vlan_id): + net_type = 'vlan' + name = 'phys_net' + plugin = directory.get_plugin() + plugin._network_vlans = n_utils.parse_network_vlan_ranges( + cfg.CONF.nsx_v3.network_vlan_ranges) + expected = [('subnets', []), ('name', name), + ('admin_state_up', True), + ('status', 'ACTIVE'), + ('shared', False), + (pnet.NETWORK_TYPE, net_type), + (pnet.PHYSICAL_NETWORK, + 'fb69d878-958e-4f32-84e4-50286f26226b'), + (pnet.SEGMENTATION_ID, vlan_id)] + providernet_args = {pnet.NETWORK_TYPE: net_type, + pnet.PHYSICAL_NETWORK: + 'fb69d878-958e-4f32-84e4-50286f26226b'} + + with mock.patch('vmware_nsxlib.v3.core_resources.NsxLibTransportZone.' + 'get_transport_type', return_value='VLAN'): + with self.network(name=name, providernet_args=providernet_args, + arg_list=(pnet.NETWORK_TYPE, + pnet.PHYSICAL_NETWORK)) as net: + for k, v in expected: + self.assertEqual(net['network'][k], v) + + def test_create_phys_vlan_generate(self): + cfg.CONF.set_override('network_vlan_ranges', + 'fb69d878-958e-4f32-84e4-50286f26226b', + 'nsx_v3') + self._test_generate_tag(1) + + def test_create_phys_vlan_generate_range(self): + cfg.CONF.set_override('network_vlan_ranges', + 'fb69d878-958e-4f32-84e4-' + '50286f26226b:100:110', + 'nsx_v3') + self._test_generate_tag(100) + + def test_create_phys_vlan_network_outofrange_returns_503(self): + cfg.CONF.set_override('network_vlan_ranges', + 'fb69d878-958e-4f32-84e4-' + '50286f26226b:9:10', + 'nsx_v3') + self._test_generate_tag(9) + self._test_generate_tag(10) + with testlib_api.ExpectedException(exc.HTTPClientError) as ctx_manager: + self._test_generate_tag(11) + + self.assertEqual(ctx_manager.exception.code, 503) + class TestSubnetsV2(test_plugin.TestSubnetsV2, NsxV3PluginTestCaseMixin):