From ddfb880d5aee732cfaeb14c2bc99b0b3aea6370b Mon Sep 17 00:00:00 2001 From: Roey Chen Date: Mon, 23 May 2016 01:58:51 -0700 Subject: [PATCH] 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 --- vmware_nsx/common/exceptions.py | 5 + vmware_nsx/common/utils.py | 33 +++---- vmware_nsx/nsxlib/v3/dfw_api.py | 19 +++- vmware_nsx/nsxlib/v3/resources.py | 6 +- vmware_nsx/nsxlib/v3/security.py | 16 ++++ vmware_nsx/plugins/nsx_v3/plugin.py | 63 ++++++++---- .../plugins/nsxv3/resources/securitygroups.py | 95 ++++++++++++++++--- vmware_nsx/shell/resources.py | 4 + .../unit/extensions/test_securitygroup.py | 21 +++- vmware_nsx/tests/unit/nsx_v3/test_plugin.py | 35 ++++++- 10 files changed, 236 insertions(+), 61 deletions(-) diff --git a/vmware_nsx/common/exceptions.py b/vmware_nsx/common/exceptions.py index af8d1421f3..bac918f0c7 100644 --- a/vmware_nsx/common/exceptions.py +++ b/vmware_nsx/common/exceptions.py @@ -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.") diff --git a/vmware_nsx/common/utils.py b/vmware_nsx/common/utils.py index e11ca65a00..a408a2f324 100644 --- a/vmware_nsx/common/utils.py +++ b/vmware_nsx/common/utils.py @@ -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, diff --git a/vmware_nsx/nsxlib/v3/dfw_api.py b/vmware_nsx/nsxlib/v3/dfw_api.py index 3da0af3bc6..1fdafd1bd9 100644 --- a/vmware_nsx/nsxlib/v3/dfw_api.py +++ b/vmware_nsx/nsxlib/v3/dfw_api.py @@ -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) diff --git a/vmware_nsx/nsxlib/v3/resources.py b/vmware_nsx/nsxlib/v3/resources.py index f77b39e71e..aaaf13e6de 100644 --- a/vmware_nsx/nsxlib/v3/resources.py +++ b/vmware_nsx/nsxlib/v3/resources.py @@ -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) diff --git a/vmware_nsx/nsxlib/v3/security.py b/vmware_nsx/nsxlib/v3/security.py index a2065816b6..eb793892fe 100644 --- a/vmware_nsx/nsxlib/v3/security.py +++ b/vmware_nsx/nsxlib/v3/security.py @@ -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) diff --git a/vmware_nsx/plugins/nsx_v3/plugin.py b/vmware_nsx/plugins/nsx_v3/plugin.py index 7b40213d39..44338e67a3 100644 --- a/vmware_nsx/plugins/nsx_v3/plugin.py +++ b/vmware_nsx/plugins/nsx_v3/plugin.py @@ -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( diff --git a/vmware_nsx/shell/admin/plugins/nsxv3/resources/securitygroups.py b/vmware_nsx/shell/admin/plugins/nsxv3/resources/securitygroups.py index 7d4940af0d..532b3ecf3b 100644 --- a/vmware_nsx/shell/admin/plugins/nsxv3/resources/securitygroups.py +++ b/vmware_nsx/shell/admin/plugins/nsxv3/resources/securitygroups.py @@ -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) diff --git a/vmware_nsx/shell/resources.py b/vmware_nsx/shell/resources.py index b809a128ea..01c97e254b 100644 --- a/vmware_nsx/shell/resources.py +++ b/vmware_nsx/shell/resources.py @@ -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, diff --git a/vmware_nsx/tests/unit/extensions/test_securitygroup.py b/vmware_nsx/tests/unit/extensions/test_securitygroup.py index 6249419c89..9c9ebe1987 100644 --- a/vmware_nsx/tests/unit/extensions/test_securitygroup.py +++ b/vmware_nsx/tests/unit/extensions/test_securitygroup.py @@ -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( diff --git a/vmware_nsx/tests/unit/nsx_v3/test_plugin.py b/vmware_nsx/tests/unit/nsx_v3/test_plugin.py index ee6c51e314..7db182a802 100644 --- a/vmware_nsx/tests/unit/nsx_v3/test_plugin.py +++ b/vmware_nsx/tests/unit/nsx_v3/test_plugin.py @@ -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):