From 1f8ac3e9f6e60f49470cce1c407dfdea8abe1943 Mon Sep 17 00:00:00 2001 From: Adit Sarfaty Date: Tue, 11 Dec 2018 14:50:15 +0200 Subject: [PATCH] NSX|P add port security support Adding segment profiles to the backend port Including mac learning support, port security & spoofguard. In addition - adding the exclude port tag for ports without port security Change-Id: Ief4a3989316f7b7097c5be6145aae169cde87e8e --- devstack/tools/nsxp_cleanup.py | 4 + vmware_nsx/plugins/common_v3/plugin.py | 20 +++ vmware_nsx/plugins/nsx_p/plugin.py | 111 ++++++++++++++--- vmware_nsx/plugins/nsx_v3/plugin.py | 20 --- vmware_nsx/tests/unit/nsx_p/test_plugin.py | 136 +++++++++++++++++++++ 5 files changed, 257 insertions(+), 34 deletions(-) diff --git a/devstack/tools/nsxp_cleanup.py b/devstack/tools/nsxp_cleanup.py index bed62f78b4..a99b90182c 100755 --- a/devstack/tools/nsxp_cleanup.py +++ b/devstack/tools/nsxp_cleanup.py @@ -206,6 +206,10 @@ class NSXClient(object): segment_ports = self.get_os_nsx_segment_ports(segment_id) for p in segment_ports: try: + self.nsxpolicy.segment_port_security_profiles.delete( + segment_id, p['id']) + self.nsxpolicy.segment_port_discovery_profiles.delete( + segment_id, p['id']) self.nsxpolicy.segment_port.delete(segment_id, p['id']) except exceptions.ManagerError as e: print("Failed to delete segment port %s: %s" % (p['id'], e)) diff --git a/vmware_nsx/plugins/common_v3/plugin.py b/vmware_nsx/plugins/common_v3/plugin.py index 0a2c7c2001..f72e3be843 100644 --- a/vmware_nsx/plugins/common_v3/plugin.py +++ b/vmware_nsx/plugins/common_v3/plugin.py @@ -400,6 +400,16 @@ class NsxPluginV3Base(plugin.NsxPluginBase, LOG.warning(err_msg) raise n_exc.InvalidInput(error_message=err_msg) + def _assert_on_port_admin_state(self, port_data, device_owner): + """Do not allow changing the admin state of some ports""" + if (device_owner == l3_db.DEVICE_OWNER_ROUTER_INTF or + device_owner == l3_db.DEVICE_OWNER_ROUTER_GW): + if port_data.get("admin_state_up") is False: + err_msg = _("admin_state_up=False router ports are not " + "supported") + LOG.warning(err_msg) + raise n_exc.InvalidInput(error_message=err_msg) + def _validate_update_port(self, context, id, original_port, port_data): qos_selected = validators.is_attr_set(port_data.get (qos_consts.QOS_POLICY_ID)) @@ -1174,3 +1184,13 @@ class NsxPluginV3Base(plugin.NsxPluginBase, super(NsxPluginV3Base, self).delete_port(context, port_id) if nsx_port_id: self.nsxlib.logical_port.delete(nsx_port_id) + + def _is_excluded_port(self, device_owner, port_security): + if device_owner == l3_db.DEVICE_OWNER_ROUTER_INTF: + return False + if device_owner == constants.DEVICE_OWNER_DHCP: + if not self._has_native_dhcp_metadata(): + return True + elif not port_security: + return True + return False diff --git a/vmware_nsx/plugins/nsx_p/plugin.py b/vmware_nsx/plugins/nsx_p/plugin.py index 22442e3fc6..a660ca47f4 100644 --- a/vmware_nsx/plugins/nsx_p/plugin.py +++ b/vmware_nsx/plugins/nsx_p/plugin.py @@ -68,6 +68,7 @@ from vmware_nsx.common import utils from vmware_nsx.db import db as nsx_db from vmware_nsx.db import extended_security_group_rule as extend_sg_rule from vmware_nsx.db import maclearning as mac_db +from vmware_nsx.extensions import maclearning as mac_ext from vmware_nsx.extensions import projectpluginmap from vmware_nsx.extensions import providersecuritygroup as provider_sg from vmware_nsx.extensions import secgroup_rule_local_ip_prefix as sg_prefix @@ -80,6 +81,7 @@ from vmware_nsxlib.v3 import exceptions as nsx_lib_exc from vmware_nsxlib.v3 import nsx_constants as nsxlib_consts from vmware_nsxlib.v3 import policy_constants from vmware_nsxlib.v3 import policy_defs +from vmware_nsxlib.v3 import security from vmware_nsxlib.v3 import utils as nsxlib_utils LOG = log.getLogger(__name__) @@ -97,7 +99,8 @@ NSX_P_PROVIDER_SECTION_CATEGORY = policy_constants.CATEGORY_INFRASTRUCTURE SPOOFGUARD_PROFILE_UUID = 'neutron-spoofguard-profile' NO_SPOOFGUARD_PROFILE_UUID = policy_defs.SpoofguardProfileDef.DEFAULT_PROFILE MAC_DISCOVERY_PROFILE_UUID = 'neutron-mac-discovery-profile' -NO_SEG_SECURITY_PROFILE_UUID = ( +NO_SEG_SECURITY_PROFILE_UUID = 'neutron-no-segment-security-profile' +SEG_SECURITY_PROFILE_UUID = ( policy_defs.SegmentSecurityProfileDef.DEFAULT_PROFILE) @@ -141,7 +144,8 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, "security-group-logging", "provider-security-group", "port-security-groups-filtering", - "vlan-transparent"] + "vlan-transparent", + 'mac-learning'] @resource_registry.tracked_resources( network=models_v2.Network, @@ -284,14 +288,32 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, mac_learning_enabled=True, tags=self.nsxpolicy.build_v3_api_version_tag()) - # No Port security segment-security profile - # (default NSX profile. just verify it exists) + # No Port security segment-security profile (find it or create) try: self.nsxpolicy.segment_security_profile.get( NO_SEG_SECURITY_PROFILE_UUID) + except nsx_lib_exc.ResourceNotFound: + self.nsxpolicy.segment_security_profile.create_or_overwrite( + NO_SEG_SECURITY_PROFILE_UUID, + profile_id=NO_SEG_SECURITY_PROFILE_UUID, + bpdu_filter_enable=False, + dhcp_client_block_enabled=False, + dhcp_client_block_v6_enabled=False, + dhcp_server_block_enabled=False, + dhcp_server_block_v6_enabled=False, + non_ip_traffic_block_enabled=False, + ra_guard_enabled=False, + rate_limits_enabled=False, + tags=self.nsxpolicy.build_v3_api_version_tag()) + + # Port security segment-security profile + # (default NSX profile. just verify it exists) + try: + self.nsxpolicy.segment_security_profile.get( + SEG_SECURITY_PROFILE_UUID) except nsx_lib_exc.ResourceNotFound: msg = (_("Cannot find segment security profile %s") % - NO_SEG_SECURITY_PROFILE_UUID) + SEG_SECURITY_PROFILE_UUID) raise nsx_exc.NsxPluginException(err_msg=msg) @staticmethod @@ -613,10 +635,9 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, return tags - def _create_port_on_backend(self, context, port_data): + def _create_port_on_backend(self, context, port_data, is_psec_on): # TODO(annak): admin_state not supported by policy # TODO(annak): handle exclude list - # TODO(annak): switching profiles when supported name = self._build_port_name(context, port_data) address_bindings = self._build_port_address_bindings( context, port_data) @@ -632,6 +653,10 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, tags.extend(self.nsxpolicy.build_v3_api_version_project_tag( context.tenant_name)) + if self._is_excluded_port(device_owner, is_psec_on): + tags.append({'scope': security.PORT_SG_SCOPE, + 'tag': nsxlib_consts.EXCLUDE_PORT}) + segment_id = self._get_network_nsx_segment_id( context, port_data['network_id']) self.nsxpolicy.segment_port.create_or_overwrite( @@ -643,6 +668,34 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, attachment_type=attachment_type, tags=tags) + # add the security profiles to the port + if is_psec_on: + spoofguard_profile = SPOOFGUARD_PROFILE_UUID + seg_sec_profile = SEG_SECURITY_PROFILE_UUID + else: + spoofguard_profile = NO_SPOOFGUARD_PROFILE_UUID + seg_sec_profile = NO_SEG_SECURITY_PROFILE_UUID + self.nsxpolicy.segment_port_security_profiles.create_or_overwrite( + name, segment_id, port_data['id'], + spoofguard_profile_id=spoofguard_profile, + segment_security_profile_id=seg_sec_profile) + + # add the mac discovery profile to the port + mac_discovery_profile = None + mac_disc_profile_must = False + if is_psec_on: + address_pairs = port_data.get(addr_apidef.ADDRESS_PAIRS) + if validators.is_attr_set(address_pairs) and address_pairs: + mac_disc_profile_must = True + mac_learning_enabled = ( + validators.is_attr_set(port_data.get(mac_ext.MAC_LEARNING)) and + port_data.get(mac_ext.MAC_LEARNING) is True) + if mac_disc_profile_must or mac_learning_enabled: + mac_discovery_profile = MAC_DISCOVERY_PROFILE_UUID + self.nsxpolicy.segment_port_discovery_profiles.create_or_overwrite( + name, segment_id, port_data['id'], + mac_discovery_profile_id=mac_discovery_profile) + def base_create_port(self, context, port): neutron_db = super(NsxPolicyPlugin, self).create_port(context, port) self._extension_manager.process_create_port( @@ -680,11 +733,25 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, self._process_port_create_security_group(context, port_data, sgids) self._process_port_create_provider_security_group( context, port_data, psgids) - #TODO(asarfaty): Handle mac learning + + # Handle port mac learning + if validators.is_attr_set(port_data.get(mac_ext.MAC_LEARNING)): + # Make sure mac_learning and port sec are not both enabled + if port_data.get(mac_ext.MAC_LEARNING) and is_psec_on: + msg = _('Mac learning requires that port security be ' + 'disabled') + LOG.error(msg) + raise n_exc.InvalidInput(error_message=msg) + # save the mac learning value in the DB + self._create_mac_learning_state(context, port_data) + elif mac_ext.MAC_LEARNING in port_data: + # This is due to the fact that the default is + # ATTR_NOT_SPECIFIED + port_data.pop(mac_ext.MAC_LEARNING) if not is_external_net: try: - self._create_port_on_backend(context, port_data) + self._create_port_on_backend(context, port_data, is_psec_on) except Exception as e: with excutils.save_and_reraise_exception(): LOG.error('Failed to create port %(id)s on NSX ' @@ -721,17 +788,22 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, if not self._network_is_external(context, net_id): try: segment_id = self._get_network_nsx_segment_id(context, net_id) - self.nsxpolicy.segment_port.delete(segment_id, port_data['id']) + self.nsxpolicy.segment_port_security_profiles.delete( + segment_id, port_id) + self.nsxpolicy.segment_port_discovery_profiles.delete( + segment_id, port_id) + self.nsxpolicy.segment_port.delete(segment_id, port_id) except Exception as ex: LOG.error("Failed to delete port %(id)s on NSX backend " "due to %(e)s", {'id': port_id, 'e': ex}) # Do not fail the neutron action def _update_port_on_backend(self, context, lport_id, - original_port, updated_port): + original_port, updated_port, + is_psec_on): # For now port create and update are the same # Update might evolve with more features - return self._create_port_on_backend(context, updated_port) + return self._create_port_on_backend(context, updated_port, is_psec_on) def update_port(self, context, port_id, port): with db_api.CONTEXT_WRITER.using(context): @@ -740,6 +812,8 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, original_port = super(NsxPolicyPlugin, self).get_port( context, port_id) port_data = port['port'] + self._validate_update_port(context, port_id, original_port, + port_data) validate_port_sec = self._should_validate_port_sec_on_update_port( port_data) is_external_net = self._network_is_external( @@ -784,14 +858,23 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, vif_type=self._vif_type_by_vnic_type(direct_vnic_type)) self._extend_nsx_port_dict_binding(context, updated_port) - #TODO(asarfaty): Handle mac learning + mac_learning_state = updated_port.get(mac_ext.MAC_LEARNING) + if mac_learning_state is not None: + if port_security and mac_learning_state: + msg = _('Mac learning requires that port security be ' + 'disabled') + LOG.error(msg) + raise n_exc.InvalidInput(error_message=msg) + self._update_mac_learning_state(context, port_id, + mac_learning_state) # update the port in the backend, only if it exists in the DB # (i.e not external net) if not is_external_net: try: self._update_port_on_backend(context, port_id, - original_port, updated_port) + original_port, updated_port, + port_security) except Exception as e: LOG.error('Failed to update port %(id)s on NSX ' 'backend. Exception: %(e)s', diff --git a/vmware_nsx/plugins/nsx_v3/plugin.py b/vmware_nsx/plugins/nsx_v3/plugin.py index 5d67cc596b..c7eb527184 100644 --- a/vmware_nsx/plugins/nsx_v3/plugin.py +++ b/vmware_nsx/plugins/nsx_v3/plugin.py @@ -1842,16 +1842,6 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, LOG.error(err_msg) raise n_exc.InvalidInput(error_message=err_msg) - def _is_excluded_port(self, device_owner, port_security): - if device_owner == l3_db.DEVICE_OWNER_ROUTER_INTF: - return False - if device_owner == const.DEVICE_OWNER_DHCP: - if not cfg.CONF.nsx_v3.native_dhcp_metadata: - return True - elif not port_security: - return True - return False - def _create_port_at_the_backend(self, context, port_data, l2gw_port_check, psec_is_on, is_ens_tz_port): @@ -2077,16 +2067,6 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, LOG.warning(err_msg) raise n_exc.InvalidInput(error_message=err_msg) - def _assert_on_port_admin_state(self, port_data, device_owner): - """Do not allow changing the admin state of some ports""" - if (device_owner == l3_db.DEVICE_OWNER_ROUTER_INTF or - device_owner == l3_db.DEVICE_OWNER_ROUTER_GW): - if port_data.get("admin_state_up") is False: - err_msg = _("admin_state_up=False router ports are not " - "supported") - LOG.warning(err_msg) - raise n_exc.InvalidInput(error_message=err_msg) - def _filter_ipv4_dhcp_fixed_ips(self, context, fixed_ips): ips = [] for fixed_ip in fixed_ips: diff --git a/vmware_nsx/tests/unit/nsx_p/test_plugin.py b/vmware_nsx/tests/unit/nsx_p/test_plugin.py index 5a174fde62..94517b21d6 100644 --- a/vmware_nsx/tests/unit/nsx_p/test_plugin.py +++ b/vmware_nsx/tests/unit/nsx_p/test_plugin.py @@ -34,6 +34,7 @@ from neutron_lib.callbacks import registry from neutron_lib.callbacks import resources from neutron_lib import constants from neutron_lib import context +from neutron_lib import exceptions as n_exc from neutron_lib.plugins import directory from vmware_nsx.common import utils @@ -402,6 +403,141 @@ class NsxPTestPorts(test_db_base_plugin_v2.TestPortsV2, self.assertEqual(res['port']['fixed_ips'], data['port']['fixed_ips']) + def test_create_port_with_mac_learning_true(self): + plugin = directory.get_plugin() + ctx = context.get_admin_context() + with self.network() as network: + data = {'port': { + 'network_id': network['network']['id'], + 'tenant_id': self._tenant_id, + 'name': 'qos_port', + 'admin_state_up': True, + 'device_id': 'fake_device', + 'device_owner': 'fake_owner', + 'fixed_ips': [], + 'port_security_enabled': False, + 'mac_address': '00:00:00:00:00:01', + 'mac_learning_enabled': True} + } + port = plugin.create_port(ctx, data) + self.assertTrue(port['mac_learning_enabled']) + + def test_create_port_with_mac_learning_false(self): + plugin = directory.get_plugin() + ctx = context.get_admin_context() + with self.network() as network: + data = {'port': { + 'network_id': network['network']['id'], + 'tenant_id': self._tenant_id, + 'name': 'qos_port', + 'admin_state_up': True, + 'device_id': 'fake_device', + 'device_owner': 'fake_owner', + 'fixed_ips': [], + 'port_security_enabled': False, + 'mac_address': '00:00:00:00:00:01', + 'mac_learning_enabled': False} + } + port = plugin.create_port(ctx, data) + self.assertFalse(port['mac_learning_enabled']) + + def test_update_port_with_mac_learning_true(self): + plugin = directory.get_plugin() + ctx = context.get_admin_context() + with self.network() as network: + data = {'port': { + 'network_id': network['network']['id'], + 'tenant_id': self._tenant_id, + 'name': 'qos_port', + 'admin_state_up': True, + 'device_id': 'fake_device', + 'device_owner': 'fake_owner', + 'fixed_ips': [], + 'port_security_enabled': False, + 'mac_address': '00:00:00:00:00:01'} + } + port = plugin.create_port(ctx, data) + data['port']['mac_learning_enabled'] = True + update_res = plugin.update_port(ctx, port['id'], data) + self.assertTrue(update_res['mac_learning_enabled']) + + def test_update_port_with_mac_learning_false(self): + plugin = directory.get_plugin() + ctx = context.get_admin_context() + with self.network() as network: + data = {'port': { + 'network_id': network['network']['id'], + 'tenant_id': self._tenant_id, + 'name': 'qos_port', + 'admin_state_up': True, + 'device_id': 'fake_device', + 'device_owner': 'fake_owner', + 'fixed_ips': [], + 'port_security_enabled': False, + 'mac_address': '00:00:00:00:00:01'} + } + port = plugin.create_port(ctx, data) + data['port']['mac_learning_enabled'] = False + update_res = plugin.update_port(ctx, port['id'], data) + self.assertFalse(update_res['mac_learning_enabled']) + + def test_update_port_with_mac_learning_failes(self): + plugin = directory.get_plugin() + ctx = context.get_admin_context() + with self.network() as network: + data = {'port': { + 'network_id': network['network']['id'], + 'tenant_id': self._tenant_id, + 'name': 'qos_port', + 'admin_state_up': True, + 'device_id': 'fake_device', + 'device_owner': constants.DEVICE_OWNER_FLOATINGIP, + 'fixed_ips': [], + 'port_security_enabled': False, + 'mac_address': '00:00:00:00:00:01'} + } + port = plugin.create_port(ctx, data) + data['port']['mac_learning_enabled'] = True + self.assertRaises( + n_exc.InvalidInput, + plugin.update_port, ctx, port['id'], data) + + def _create_l3_ext_network( + self, physical_network=DEFAULT_TIER0_ROUTER_UUID): + name = 'l3_ext_net' + net_type = utils.NetworkTypes.L3_EXT + providernet_args = {pnet.NETWORK_TYPE: net_type, + pnet.PHYSICAL_NETWORK: physical_network} + return self.network(name=name, + router__external=True, + providernet_args=providernet_args, + arg_list=(pnet.NETWORK_TYPE, + pnet.PHYSICAL_NETWORK)) + + def test_fail_create_port_with_ext_net(self): + expected_error = 'InvalidInput' + with self._create_l3_ext_network() as network: + with self.subnet(network=network, cidr='10.0.0.0/24'): + device_owner = constants.DEVICE_OWNER_COMPUTE_PREFIX + 'X' + res = self._create_port(self.fmt, + network['network']['id'], + exc.HTTPBadRequest.code, + device_owner=device_owner) + data = self.deserialize(self.fmt, res) + self.assertEqual(expected_error, data['NeutronError']['type']) + + def test_fail_update_port_with_ext_net(self): + with self._create_l3_ext_network() as network: + with self.subnet(network=network, cidr='10.0.0.0/24') as subnet: + with self.port(subnet=subnet) as port: + device_owner = constants.DEVICE_OWNER_COMPUTE_PREFIX + 'X' + data = {'port': {'device_owner': device_owner}} + req = self.new_update_request('ports', + data, port['port']['id']) + res = req.get_response(self.api) + self.assertEqual(exc.HTTPBadRequest.code, + res.status_int) + class NsxPTestSecurityGroup(NsxPPluginTestCaseMixin, test_securitygroup.TestSecurityGroups,