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.

Change-Id: I2a802bc314d98dba9ecc54191fcbd7330f183e12
This commit is contained in:
Roey Chen 2016-05-23 01:58:51 -07:00
parent 30a4cf2d5f
commit ddfb880d5a
10 changed files with 236 additions and 61 deletions

View File

@ -175,3 +175,8 @@ class NsxResourceNotFound(n_exc.NotFound):
class NsxQosPolicyMappingNotFound(n_exc.NotFound):
message = _('Unable to find mapping for QoS policy: %(policy)s')
class NumberOfNsgroupCriteriaTagsReached(NsxPluginException):
message = _("Port can be associated with at most %(max_num)s "
"security-groups.")

View File

@ -168,22 +168,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

@ -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,12 +110,17 @@ def list_nsgroups():
@utils.retry_upon_exception_nsxv3(nsx_exc.StaleRevision)
def update_nsgroup(nsgroup_id, display_name=None, description=None):
def update_nsgroup(nsgroup_id, display_name=None, description=None,
membership_criteria=None, members=None):
nsgroup = read_nsgroup(nsgroup_id)
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

@ -292,13 +292,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

@ -37,6 +37,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):
@ -230,6 +233,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

@ -1187,6 +1187,13 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
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)
@ -1567,17 +1574,19 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
'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:
net_id = port_data[pbin.VIF_DETAILS]['nsx-logical-switch-id']
@ -1634,8 +1643,10 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
_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)
# Remove Mac/IP binding from native DHCP server and neutron DB.
@ -1723,7 +1734,7 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
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
@ -1737,8 +1748,8 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
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)
vif_uuid = updated_port['id']
parent_name, tag = self._get_data_from_binding_profile(
@ -1752,10 +1763,14 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
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:
@ -1785,7 +1800,7 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
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)
except nsx_exc.ManagerError as inst:
@ -2591,10 +2606,16 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
tenant_id = secgroup['tenant_id']
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

@ -14,19 +14,23 @@
import logging
from vmware_nsx.common import utils
from vmware_nsx.shell.admin.plugins.common import constants
from vmware_nsx.shell.admin.plugins.common import formatters
from vmware_nsx.shell.admin.plugins.common import utils as admin_utils
from vmware_nsx.shell import resources as shell
from neutron.callbacks import registry
from neutron import context as neutron_context
from neutron.db import common_db_mixin as common_db
from neutron.db import securitygroups_db as sg_db
from vmware_nsx._i18n import _LI
from vmware_nsx.common import utils
from vmware_nsx.db import db as nsx_db
from vmware_nsx.shell.admin.plugins.common import constants
from vmware_nsx.shell.admin.plugins.common import formatters
from vmware_nsx.shell.admin.plugins.common import utils as admin_utils
from vmware_nsx.shell.admin.plugins.nsxv3.resources import ports
from vmware_nsx.shell.admin.plugins.nsxv3.resources import utils as v3_utils
from vmware_nsx.shell import resources as shell
from vmware_nsx._i18n import _LE, _LI, _LW
from vmware_nsx.nsxlib import v3 as nsxlib
from vmware_nsx.nsxlib.v3 import dfw_api as firewall
from vmware_nsx.nsxlib.v3 import security
LOG = logging.getLogger(__name__)
@ -34,17 +38,34 @@ LOG = logging.getLogger(__name__)
class NeutronSecurityGroupApi(sg_db.SecurityGroupDbMixin,
common_db.CommonDbMixin):
def __init__(self):
self.sg_api = super(NeutronSecurityGroupApi, self)
self.neutron_admin_context = neutron_context.get_admin_context()
super(NeutronSecurityGroupApi, self)
self.context = neutron_context.get_admin_context()
def get_security_groups(self):
return self.sg_api.get_security_groups(self.neutron_admin_context)
return super(NeutronSecurityGroupApi,
self).get_security_groups(self.context)
def delete_security_group(self, sg_id):
self.sg_api.delete_security_group(self.neutron_admin_context,
sg_id)
return super(NeutronSecurityGroupApi,
self).delete_security_group(self.context, sg_id)
def get_nsgroup_id(self, sg_id):
return nsx_db.get_nsx_security_group_id(
self.context.session, sg_id)
def get_port_security_groups(self, port_id):
secgroups_bindings = self._get_port_security_group_bindings(
self.context, {'port_id': [port_id]})
return [b['security_group_id'] for b in secgroups_bindings]
def get_security_group_ports(self, security_group_id):
secgroups_bindings = self._get_port_security_group_bindings(
self.context, {'security_group_id': [security_group_id]})
return [b['port_id'] for b in secgroups_bindings]
neutron_sg = NeutronSecurityGroupApi()
neutron_db = v3_utils.NeutronDbClient()
@admin_utils.output_header
@ -129,6 +150,53 @@ def neutron_delete_security_groups(resource, event, trigger, **kwargs):
LOG.warning(str(e))
def _update_ports_dynamic_criteria_tags():
port_client, _ = ports.get_port_and_profile_clients()
for port in neutron_db.get_ports():
secgroups = neutron_sg.get_port_security_groups(port['id'])
# Nothing to do with ports that are not associated with any sec-group.
if not secgroups:
continue
_, lport_id = neutron_db.get_lswitch_and_lport_id(port['id'])
lport = port_client.get(lport_id)
criteria_tags = security.get_lport_tags_for_security_groups(secgroups)
lport['tags'] = utils.update_v3_tags(
lport.get('tags', []), criteria_tags)
port_client._client.update(lport_id, body=lport)
def _update_security_group_dynamic_criteria():
secgroups = neutron_sg.get_security_groups()
for sg in secgroups:
nsgroup_id = neutron_sg.get_nsgroup_id(sg['id'])
membership_criteria = firewall.get_nsgroup_port_tag_expression(
security.PORT_SG_SCOPE, sg['id'])
try:
# We want to add the dynamic criteria and remove all direct members
# they will be added by the manager using the new criteria.
firewall.update_nsgroup(nsgroup_id,
membership_criteria=membership_criteria,
members=[])
except Exception as e:
LOG.warning(_LW("Failed to update membership criteria for nsgroup "
"%(nsgroup_id)s, request to backend returned "
"with error: %(error)s"),
{'nsgroup_id': nsgroup_id, 'error': str(e)})
@admin_utils.output_header
def migrate_nsgroups_to_dynamic_criteria(resource, event, trigger, **kwargs):
if not utils.is_nsx_version_1_1_0(nsxlib.get_version()):
LOG.error(_LE("Dynamic criteria grouping feature isn't supported by "
"this NSX version."))
return
# First, we add the criteria tags for all ports.
_update_ports_dynamic_criteria_tags()
# Update security-groups with dynamic criteria and remove direct members.
_update_security_group_dynamic_criteria()
registry.subscribe(nsx_list_security_groups,
constants.SECURITY_GROUPS,
shell.Operations.LIST.value)
@ -156,3 +224,6 @@ registry.subscribe(neutron_delete_security_groups,
registry.subscribe(neutron_delete_security_groups,
constants.SECURITY_GROUPS,
shell.Operations.NEUTRON_CLEAN.value)
registry.subscribe(migrate_nsgroups_to_dynamic_criteria,
constants.FIREWALL_NSX_GROUPS,
shell.Operations.MIGRATE_TO_DYNAMIC_CRITERIA.value)

View File

@ -44,6 +44,7 @@ class Operations(enum.Enum):
NSX_CLEAN = 'nsx-clean'
NSX_UPDATE = 'nsx-update'
NSX_UPDATE_SECRET = 'nsx-update-secret'
MIGRATE_TO_DYNAMIC_CRITERIA = 'migrate-to-dynamic-criteria'
ops = [op.value for op in Operations]
@ -64,6 +65,9 @@ nsxv3_resources = {
Operations.NSX_CLEAN.value,
Operations.NEUTRON_LIST.value,
Operations.NEUTRON_CLEAN.value]),
constants.FIREWALL_NSX_GROUPS: Resource(
constants.FIREWALL_NSX_GROUPS, [
Operations.MIGRATE_TO_DYNAMIC_CRITERIA.value]),
constants.NETWORKS: Resource(constants.NETWORKS,
[Operations.LIST_MISMATCHES.value]),
constants.PORTS: Resource(constants.PORTS,

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

@ -752,7 +752,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},
@ -770,7 +770,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},
@ -785,7 +785,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},
@ -795,6 +795,35 @@ class TestNsxV3Utils(NsxV3PluginTestCaseMixin):
'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))
class NsxNativeDhcpTestCase(NsxV3PluginTestCaseMixin):