NSXv3: Support CH nsgroup membership using dynamic criteria tags

CH release adds new way to associate resources with nsgroups by
creating specific tags on the resources.
We would like to support this feature in the plugin for better performance.
This patch make use of this feature to associate logical-ports with nsgroups
(Neutron ports with security-groups), for every LP-NSGroup association,
a special tag will be added to the LP.
The plugin will use this NSX feature only when supported by the NSX
version, and given that the designated boolean config option is set to True.

Partial-Bug: #1633729
Change-Id: I16c301cdad84c57a4b8b91635d05d0e1cb1fb20e
This commit is contained in:
Roey Chen 2016-05-23 01:58:51 -07:00 committed by Gary Kotton
parent 036d0b9ee2
commit fffa4952f2
9 changed files with 178 additions and 55 deletions

View File

@ -171,3 +171,8 @@ class SecurityGroupMaximumCapacityReached(NsxPluginException):
class NsxResourceNotFound(n_exc.NotFound):
message = _("%(res_name)s %(res_id)s not found on the backend.")
class NumberOfNsgroupCriteriaTagsReached(NsxPluginException):
message = _("Port can be associated with at most %(max_num)s "
"security-groups.")

View File

@ -13,10 +13,11 @@
# License for the specific language governing permissions and limitations
# under the License.
from distutils import version
import hashlib
from neutron.api.v2 import attributes
from neutron import version
from neutron import version as n_version
from neutron_lib import exceptions
from oslo_config import cfg
from oslo_log import log
@ -30,9 +31,10 @@ LOG = log.getLogger(__name__)
MAX_DISPLAY_NAME_LEN = 40
MAX_RESOURCE_TYPE_LEN = 20
MAX_TAG_LEN = 40
NEUTRON_VERSION = version.version_info.release_string()
NEUTRON_VERSION = n_version.version_info.release_string()
NSX_NEUTRON_PLUGIN = 'NSX Neutron plugin'
OS_NEUTRON_ID_SCOPE = 'os-neutron-id'
NSXV3_VERSION_1_1_0 = '1.1.0'
# Allowed network types for the NSX Plugin
@ -63,6 +65,11 @@ class NsxV3NetworkTypes:
VXLAN = 'vxlan'
def is_nsx_version_1_1_0(nsx_version):
return (version.LooseVersion(nsx_version) >=
version.LooseVersion(NSXV3_VERSION_1_1_0))
def get_tags(**kwargs):
tags = ([dict(tag=value, scope=key)
for key, value in six.iteritems(kwargs)])
@ -113,7 +120,7 @@ def build_v3_api_version_tag():
return [{'scope': OS_NEUTRON_ID_SCOPE,
'tag': NSX_NEUTRON_PLUGIN},
{'scope': "os-api-version",
'tag': version.version_info.release_string()}]
'tag': n_version.version_info.release_string()}]
def _validate_resource_type_length(resource_type):
@ -148,7 +155,7 @@ def build_v3_tags_payload(resource, resource_type, project_name):
{'scope': 'os-project-name',
'tag': project_name[:MAX_TAG_LEN]},
{'scope': 'os-api-version',
'tag': version.version_info.release_string()[:MAX_TAG_LEN]}]
'tag': n_version.version_info.release_string()[:MAX_TAG_LEN]}]
def add_v3_tag(tags, resource_type, tag):
@ -157,22 +164,23 @@ def add_v3_tag(tags, resource_type, tag):
return tags
def update_v3_tags(tags, resources):
port_tags = dict((t['scope'], t['tag']) for t in tags)
resources = resources or []
# Update tags
for resource in resources:
tag = resource['tag'][:MAX_TAG_LEN]
resource_type = resource['resource_type']
if resource_type in port_tags:
if tag:
port_tags[resource_type] = tag
else:
port_tags.pop(resource_type, None)
else:
port_tags[resource_type] = tag
# Create the new set of tags
return [{'scope': k, 'tag': v} for k, v in port_tags.items()]
def update_v3_tags(current_tags, tags_update):
current_scopes = set([tag['scope'] for tag in current_tags])
updated_scopes = set([tag['scope'] for tag in tags_update])
tags = [{'scope': tag['scope'], 'tag': tag['tag']}
for tag in (current_tags + tags_update)
if tag['scope'] in (current_scopes ^ updated_scopes)]
modified_scopes = current_scopes & updated_scopes
for tag in tags_update:
if tag['scope'] in modified_scopes:
# If the tag value is empty or None, then remove the tag completely
if tag['tag']:
tag['tag'] = tag['tag'][:MAX_TAG_LEN]
tags.append(tag)
return tags
def retry_upon_exception_nsxv3(exc, delay=500, max_delay=2000,

View File

@ -25,6 +25,12 @@ from vmware_nsx.nsxlib.v3 import client
LOG = log.getLogger(__name__)
def get_version():
node = client.get_resource("node")
version = node.get('node_version')
return version
def get_edge_cluster(edge_cluster_uuid):
resource = "edge-clusters/%s" % edge_cluster_uuid
return client.get_resource(resource)

View File

@ -40,6 +40,7 @@ REJECT = 'REJECT'
# filtering operators and expressions
EQUALS = 'EQUALS'
NSGROUP_SIMPLE_EXPRESSION = 'NSGroupSimpleExpression'
NSGROUP_TAG_EXPRESSION = 'NSGroupTagExpression'
# nsgroup members update actions
ADD_MEMBERS = 'ADD_MEMBERS'
@ -86,11 +87,20 @@ def get_nsservice(resource_type, **properties):
return {'service': service}
def create_nsgroup(display_name, description, tags):
def get_nsgroup_port_tag_expression(scope, tag):
return {'resource_type': NSGROUP_TAG_EXPRESSION,
'target_type': LOGICAL_PORT,
'scope': scope,
'tag': tag}
def create_nsgroup(display_name, description, tags, membership_criteria=None):
body = {'display_name': display_name,
'description': description,
'tags': tags,
'members': []}
if membership_criteria:
body.update({'membership_criteria': [membership_criteria]})
return nsxclient.create_resource('ns-groups', body)
@ -100,10 +110,17 @@ def list_nsgroups():
@utils.retry_upon_exception_nsxv3(nsx_exc.StaleRevision)
def update_nsgroup(nsgroup_id, display_name, description):
def update_nsgroup(nsgroup_id, display_name=None, description=None,
membership_criteria=None, members=None):
nsgroup = read_nsgroup(nsgroup_id)
nsgroup.update({'display_name': display_name,
'description': description})
if display_name is not None:
nsgroup['display_name'] = display_name
if description is not None:
nsgroup['description'] = description
if members is not None:
nsgroup['members'] = members
if membership_criteria is not None:
nsgroup['membership_criteria'] = [membership_criteria]
return nsxclient.update_resource('ns-groups/%s' % nsgroup_id, nsgroup)

View File

@ -284,13 +284,13 @@ class LogicalPort(AbstractRESTResource):
def update(self, lport_id, vif_uuid,
name=None, admin_state=None,
address_bindings=None, switch_profile_ids=None,
resources=None,
tags_update=None,
attachment_type=nsx_constants.ATTACHMENT_VIF,
parent_name=None, parent_tag=None):
lport = self.get(lport_id)
tags = lport.get('tags', [])
if resources:
tags = utils.update_v3_tags(tags, resources)
if tags_update:
tags = utils.update_v3_tags(tags, tags_update)
attachment = self._prepare_attachment(vif_uuid, parent_name,
parent_tag, address_bindings,
attachment_type)

View File

@ -36,6 +36,9 @@ LOG = log.getLogger(__name__)
DEFAULT_SECTION = 'OS Default Section for Neutron Security-Groups'
DEFAULT_SECTION_TAG_NAME = 'neutron_default_dfw_section'
PORT_SG_SCOPE = 'os-security-group'
MAX_NSGROUPS_CRITERIA_TAGS = 10
def _get_l4_protocol_name(protocol_number):
@ -192,6 +195,19 @@ def _get_remote_nsg_mapping(context, sg_rule, nsgroup_id):
return remote_nsgroup_id
def get_lport_tags_for_security_groups(secgroups):
if len(secgroups) > MAX_NSGROUPS_CRITERIA_TAGS:
raise nsx_exc.NumberOfNsgroupCriteriaTagsReached(
max_num=MAX_NSGROUPS_CRITERIA_TAGS)
tags = []
for sg in secgroups:
tags = utils.add_v3_tag(tags, PORT_SG_SCOPE, sg)
if not tags:
# This port shouldn't be associated with any security-group
tags = [{'scope': PORT_SG_SCOPE, 'tag': None}]
return tags
def update_lport_with_security_groups(context, lport_id, original, updated):
added = set(updated) - set(original)
removed = set(original) - set(updated)

View File

@ -128,6 +128,8 @@ class NsxV3Plugin(addr_pair_db.AllowedAddressPairsMixin,
def __init__(self):
super(NsxV3Plugin, self).__init__()
LOG.info(_LI("Starting NsxV3Plugin"))
self._nsx_version = nsxlib.get_version()
LOG.info(_LI("NSX Version: %s"), self._nsx_version)
self._api_cluster = nsx_cluster.NSXClusteredAPI()
self._nsx_client = nsx_client.NSX3Client(self._api_cluster)
@ -724,6 +726,13 @@ class NsxV3Plugin(addr_pair_db.AllowedAddressPairsMixin,
if resource_type:
tags = utils.add_v3_tag(tags, resource_type, device_id)
if utils.is_nsx_version_1_1_0(self._nsx_version):
# If port has no security-groups then we don't need to add any
# security criteria tag.
if port_data[ext_sg.SECURITYGROUPS]:
tags += security.get_lport_tags_for_security_groups(
port_data[ext_sg.SECURITYGROUPS])
parent_name, tag = self._get_data_from_binding_profile(
context, port_data)
address_bindings = (self._build_address_bindings(port_data)
@ -864,17 +873,19 @@ class NsxV3Plugin(addr_pair_db.AllowedAddressPairsMixin,
'backend. Exception: %(e)s'),
{'id': neutron_db['id'], 'e': e})
self._cleanup_port(context, neutron_db['id'], None)
try:
if sgids:
if not utils.is_nsx_version_1_1_0(self._nsx_version):
try:
security.update_lport_with_security_groups(
context, lport['id'], [], sgids)
except Exception:
with excutils.save_and_reraise_exception():
LOG.debug("Couldn't associate port %s with "
"one or more security-groups, reverting "
"logical-port creation (%s).",
port_data['id'], lport['id'])
self._cleanup_port(context, neutron_db['id'], lport['id'])
context, lport['id'], [], sgids or [])
except Exception:
with excutils.save_and_reraise_exception():
LOG.debug("Couldn't associate port %s with "
"one or more security-groups, reverting "
"logical-port creation (%s).",
port_data['id'], lport['id'])
self._cleanup_port(
context, neutron_db['id'], lport['id'])
try:
nsx_db.add_neutron_nsx_port_mapping(
@ -924,8 +935,10 @@ class NsxV3Plugin(addr_pair_db.AllowedAddressPairsMixin,
_net_id, nsx_port_id = nsx_db.get_nsx_switch_and_port_id(
context.session, port_id)
self._port_client.delete(nsx_port_id)
security.update_lport_with_security_groups(
context, nsx_port_id, port.get(ext_sg.SECURITYGROUPS, []), [])
if not utils.is_nsx_version_1_1_0(self._nsx_version):
security.update_lport_with_security_groups(
context, nsx_port_id,
port.get(ext_sg.SECURITYGROUPS, []), [])
self.disassociate_floatingips(context, port_id)
nsx_rpc.handle_port_metadata_access(self, context, port,
is_delete=True)
@ -1010,7 +1023,7 @@ class NsxV3Plugin(addr_pair_db.AllowedAddressPairsMixin,
original_device_id = original_port.get('device_id')
updated_device_owner = updated_port.get('device_owner')
updated_device_id = updated_port.get('device_id')
resources = []
tags_update = []
if original_device_id != updated_device_id:
# Determine if we need to update or drop the tag. If the
# updated_device_id exists then the tag will be updated. This
@ -1024,8 +1037,8 @@ class NsxV3Plugin(addr_pair_db.AllowedAddressPairsMixin,
resource_type = self._get_resource_type_for_device_id(
original_device_owner, updated_device_id)
if resource_type:
resources = [{'resource_type': resource_type,
'tag': updated_device_id}]
tags_update = utils.add_v3_tag(tags_update, resource_type,
updated_device_id)
parent_name, tag = self._get_data_from_binding_profile(
context, updated_port)
@ -1047,10 +1060,14 @@ class NsxV3Plugin(addr_pair_db.AllowedAddressPairsMixin,
name = self._get_port_name(context, updated_port)
security.update_lport_with_security_groups(
context, lport_id,
original_port.get(ext_sg.SECURITYGROUPS, []),
updated_port.get(ext_sg.SECURITYGROUPS, []))
if utils.is_nsx_version_1_1_0(self._nsx_version):
tags_update += security.get_lport_tags_for_security_groups(
updated_port.get(ext_sg.SECURITYGROUPS, []))
else:
security.update_lport_with_security_groups(
context, lport_id,
original_port.get(ext_sg.SECURITYGROUPS, []),
updated_port.get(ext_sg.SECURITYGROUPS, []))
# Update the DHCP profile
if updated_device_owner == const.DEVICE_OWNER_DHCP:
@ -1063,7 +1080,7 @@ class NsxV3Plugin(addr_pair_db.AllowedAddressPairsMixin,
admin_state=updated_port.get('admin_state_up'),
address_bindings=address_bindings,
switch_profile_ids=switch_profile_ids,
resources=resources,
tags_update=tags_update,
parent_name=parent_name,
parent_tag=tag)
@ -1757,10 +1774,16 @@ class NsxV3Plugin(addr_pair_db.AllowedAddressPairsMixin,
self._ensure_default_security_group(context, tenant_id)
try:
if utils.is_nsx_version_1_1_0(self._nsx_version):
tag_expression = (
firewall.get_nsgroup_port_tag_expression(
security.PORT_SG_SCOPE, secgroup['id']))
else:
tag_expression = None
# NOTE(roeyc): We first create the nsgroup so that once the sg is
# saved into db its already backed up by an nsx resource.
ns_group = firewall.create_nsgroup(
name, secgroup['description'], tags)
name, secgroup['description'], tags, tag_expression)
# security-group rules are located in a dedicated firewall section.
firewall_section = (
firewall.create_empty_section(

View File

@ -35,7 +35,7 @@ NSG_IDS = ['11111111-1111-1111-1111-111111111111',
def _mock_create_and_list_nsgroups(test_method):
nsgroups = []
def _create_nsgroup_mock(name, desc, tags):
def _create_nsgroup_mock(name, desc, tags, membership_criteria=None):
nsgroup = {'id': NSG_IDS[len(nsgroups)],
'display_name': name,
'desc': desc,
@ -56,6 +56,19 @@ def _mock_create_and_list_nsgroups(test_method):
class TestSecurityGroups(test_nsxv3.NsxV3PluginTestCaseMixin,
test_ext_sg.TestSecurityGroups):
pass
class TestSecurityGroupsNoDynamicCriteria(test_nsxv3.NsxV3PluginTestCaseMixin,
test_ext_sg.TestSecurityGroups):
def setUp(self):
super(TestSecurityGroupsNoDynamicCriteria, self).setUp()
mock_nsx_version = mock.patch.object(nsx_plugin.utils,
'is_nsx_version_1_1_0',
new=lambda v: False)
mock_nsx_version.start()
self._patchers.append(mock_nsx_version)
@_mock_create_and_list_nsgroups
@mock.patch.object(firewall, 'remove_nsgroup_member')
@ -63,7 +76,7 @@ class TestSecurityGroups(test_nsxv3.NsxV3PluginTestCaseMixin,
def test_create_port_with_multiple_security_groups(self,
add_member_mock,
remove_member_mock):
super(TestSecurityGroups,
super(TestSecurityGroupsNoDynamicCriteria,
self).test_create_port_with_multiple_security_groups()
# The first nsgroup is associated with the default secgroup, which is
@ -78,7 +91,7 @@ class TestSecurityGroups(test_nsxv3.NsxV3PluginTestCaseMixin,
def test_update_port_with_multiple_security_groups(self,
add_member_mock,
remove_member_mock):
super(TestSecurityGroups,
super(TestSecurityGroupsNoDynamicCriteria,
self).test_update_port_with_multiple_security_groups()
calls = [mock.call(NSG_IDS[0], firewall.LOGICAL_PORT, mock.ANY),
@ -95,7 +108,7 @@ class TestSecurityGroups(test_nsxv3.NsxV3PluginTestCaseMixin,
def test_update_port_remove_security_group_empty_list(self,
add_member_mock,
remove_member_mock):
super(TestSecurityGroups,
super(TestSecurityGroupsNoDynamicCriteria,
self).test_update_port_remove_security_group_empty_list()
add_member_mock.assert_called_with(

View File

@ -95,6 +95,12 @@ class NsxV3PluginTestCaseMixin(test_plugin.NeutronDbPluginV2TestCase,
_patch_object(nsx_plugin, 'nsx_client', new=mock_client_module)
_patch_object(nsx_plugin, 'nsx_cluster', new=mock_cluster_module)
# Mock the nsx v3 version
mock_nsxlib_get_version = mock.patch(
"vmware_nsx.nsxlib.v3.get_version",
return_value='1.1.0')
mock_nsxlib_get_version.start()
# populate pre-existing mock resources
cluster_id = uuidutils.generate_uuid()
self.mock_api.post(
@ -603,7 +609,7 @@ class TestNsxV3Utils(NsxV3PluginTestCaseMixin):
{'scope': 'os-project-name', 'tag': 'Z' * 40},
{'scope': 'os-api-version',
'tag': version.version_info.release_string()}]
resources = [{'resource_type': 'os-instance-uuid',
resources = [{'scope': 'os-instance-uuid',
'tag': 'A' * 40}]
tags = utils.update_v3_tags(tags, resources)
expected = [{'scope': 'os-neutron-net-id', 'tag': 'X' * 40},
@ -621,7 +627,7 @@ class TestNsxV3Utils(NsxV3PluginTestCaseMixin):
{'scope': 'os-project-name', 'tag': 'Z' * 40},
{'scope': 'os-api-version',
'tag': version.version_info.release_string()}]
resources = [{'resource_type': 'os-neutron-net-id',
resources = [{'scope': 'os-neutron-net-id',
'tag': ''}]
tags = utils.update_v3_tags(tags, resources)
expected = [{'scope': 'os-project-id', 'tag': 'Y' * 40},
@ -636,7 +642,7 @@ class TestNsxV3Utils(NsxV3PluginTestCaseMixin):
{'scope': 'os-project-name', 'tag': 'Z' * 40},
{'scope': 'os-api-version',
'tag': version.version_info.release_string()}]
resources = [{'resource_type': 'os-project-id',
resources = [{'scope': 'os-project-id',
'tag': 'A' * 40}]
tags = utils.update_v3_tags(tags, resources)
expected = [{'scope': 'os-neutron-net-id', 'tag': 'X' * 40},
@ -645,3 +651,32 @@ class TestNsxV3Utils(NsxV3PluginTestCaseMixin):
{'scope': 'os-api-version',
'tag': version.version_info.release_string()}]
self.assertEqual(sorted(expected), sorted(tags))
def test_update_v3_tags_repetitive_scopes(self):
tags = [{'scope': 'os-neutron-net-id', 'tag': 'X' * 40},
{'scope': 'os-project-id', 'tag': 'Y' * 40},
{'scope': 'os-project-name', 'tag': 'Z' * 40},
{'scope': 'os-security-group', 'tag': 'SG1'},
{'scope': 'os-security-group', 'tag': 'SG2'}]
tags_update = [{'scope': 'os-security-group', 'tag': 'SG3'},
{'scope': 'os-security-group', 'tag': 'SG4'}]
tags = utils.update_v3_tags(tags, tags_update)
expected = [{'scope': 'os-neutron-net-id', 'tag': 'X' * 40},
{'scope': 'os-project-id', 'tag': 'Y' * 40},
{'scope': 'os-project-name', 'tag': 'Z' * 40},
{'scope': 'os-security-group', 'tag': 'SG3'},
{'scope': 'os-security-group', 'tag': 'SG4'}]
self.assertEqual(sorted(expected), sorted(tags))
def test_update_v3_tags_repetitive_scopes_remove(self):
tags = [{'scope': 'os-neutron-net-id', 'tag': 'X' * 40},
{'scope': 'os-project-id', 'tag': 'Y' * 40},
{'scope': 'os-project-name', 'tag': 'Z' * 40},
{'scope': 'os-security-group', 'tag': 'SG1'},
{'scope': 'os-security-group', 'tag': 'SG2'}]
tags_update = [{'scope': 'os-security-group', 'tag': None}]
tags = utils.update_v3_tags(tags, tags_update)
expected = [{'scope': 'os-neutron-net-id', 'tag': 'X' * 40},
{'scope': 'os-project-id', 'tag': 'Y' * 40},
{'scope': 'os-project-name', 'tag': 'Z' * 40}]
self.assertEqual(sorted(expected), sorted(tags))