diff --git a/etc/policy.json b/etc/policy.json index 4cc5dbbe90..64ce9f2669 100644 --- a/etc/policy.json +++ b/etc/policy.json @@ -143,5 +143,10 @@ "create_security_group:logging": "rule:admin_only", "update_security_group:logging": "rule:admin_only", - "get_security_group:logging": "rule:admin_only" + "get_security_group:logging": "rule:admin_only", + + "create_flow_classifier": "rule:admin_only", + "update_flow_classifier": "rule:admin_only", + "delete_flow_classifier": "rule:admin_only", + "get_flow_classifier": "rule:admin_only" } diff --git a/setup.cfg b/setup.cfg index 6dff41e704..abb047b6bd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,6 +44,8 @@ tempest.test_plugins = vmware-nsx-tempest-plugin = vmware_nsx_tempest.plugin:VMwareNsxTempestPlugin oslo.config.opts = nsx = vmware_nsx.opts:list_opts +networking_sfc.flowclassifier.drivers = + vmware-nsxv-sfc = vmware_nsx.services.flowclassifier.nsx_v.driver:NsxvFlowClassifierDriver [build_sphinx] source-dir = doc/source diff --git a/tox.ini b/tox.ini index e8d0ae61c3..abd092c393 100644 --- a/tox.ini +++ b/tox.ini @@ -16,6 +16,7 @@ deps = -r{toxinidir}/requirements.txt # release (branch) tags git+https://git.openstack.org/openstack/neutron.git@master#egg=neutron git+https://git.openstack.org/openstack/networking-l2gw.git@master#egg=networking-l2gw + git+https://git.openstack.org/openstack/networking-sfc.git@master#egg=networking-sfc git+https://git.openstack.org/openstack/neutron-lbaas.git@master#egg=neutron-lbaas whitelist_externals = sh commands = diff --git a/vmware_nsx/common/config.py b/vmware_nsx/common/config.py index 7ef5689845..3834eead5c 100644 --- a/vmware_nsx/common/config.py +++ b/vmware_nsx/common/config.py @@ -593,6 +593,10 @@ nsxv_opts = [ "all the dhcp enabled networks.\nNote: this option can " "only be supported at NSX manager version 6.2.3 or " "higher.")), + cfg.StrOpt('service_insertion_profile_id', + help=_("(Optional) The profile id of the redirect firewall " + "rules that will be used for the Service Insertion " + "feature.")), ] # Register the configuration options diff --git a/vmware_nsx/plugins/nsx_v/plugin.py b/vmware_nsx/plugins/nsx_v/plugin.py index 56f884843a..0fd83c3b4a 100644 --- a/vmware_nsx/plugins/nsx_v/plugin.py +++ b/vmware_nsx/plugins/nsx_v/plugin.py @@ -103,6 +103,7 @@ from vmware_nsx.plugins.nsx_v.vshield import edge_firewall_driver from vmware_nsx.plugins.nsx_v.vshield import edge_utils from vmware_nsx.plugins.nsx_v.vshield import securitygroup_utils from vmware_nsx.plugins.nsx_v.vshield import vcns_driver +from vmware_nsx.services.flowclassifier.nsx_v import utils as fc_utils LOG = logging.getLogger(__name__) PORTGROUP_PREFIX = 'dvportgroup' @@ -223,6 +224,8 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, self.metadata_proxy_handler = ( nsx_v_md_proxy.NsxVMetadataProxyHandler(self)) + self._si_handler = fc_utils.NsxvServiceInsertionHandler(self) + def init_complete(self, resource, event, trigger, **kwargs): self.init_is_complete = True @@ -1382,6 +1385,11 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, elif cfg.CONF.nsxv.spoofguard_enabled: # Add vm to the exclusion list, since it has no port security self._add_vm_to_exclude_list(context, device_id, id) + # if service insertion is enabled - add this vnic to the service + # insertion security group + if self._si_handler.enabled and original_port[psec.PORTSECURITY]: + self._add_member_to_security_group(self._si_handler.sg_id, + vnic_id) delete_security_groups = self._check_update_deletes_security_groups( port) @@ -1497,6 +1505,14 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, context, device_id, id) self._delete_port_vnic_index_mapping(context, id) self._delete_dhcp_static_binding(context, original_port) + + # if service insertion is enabled - remove this vnic from the + # service insertion security group + if (self._si_handler.enabled and + original_port[psec.PORTSECURITY]): + self._remove_member_from_security_group( + self._si_handler.sg_id, + vnic_id) else: # port security enabled / disabled if port_sec_change: @@ -1512,6 +1528,10 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, # port security self._remove_vm_from_exclude_list(context, device_id, id) + # add the vm to the service insertion + if self._si_handler.enabled: + self._add_member_to_security_group( + self._si_handler.sg_id, vnic_id) elif cfg.CONF.nsxv.spoofguard_enabled: try: self._remove_vnic_from_spoofguard_policy( @@ -1523,6 +1543,10 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, # Add vm to the exclusion list, since it has no port # security now self._add_vm_to_exclude_list(context, device_id, id) + # remove the vm from the service insertion + if self._si_handler.enabled: + self._remove_member_from_security_group( + self._si_handler.sg_id, vnic_id) # Update vnic with the newest approved IP addresses if (has_port_security and @@ -1593,6 +1617,13 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, sgids = neutron_db_port.get(ext_sg.SECURITYGROUPS) self._delete_security_groups_port_mapping( context.session, vnic_id, sgids) + + # if service insertion is enabled - remove this vnic from the + # service insertion security group + if self._si_handler.enabled and neutron_db_port[psec.PORTSECURITY]: + self._remove_member_from_security_group(self._si_handler.sg_id, + vnic_id) + if (cfg.CONF.nsxv.spoofguard_enabled and neutron_db_port[psec.PORTSECURITY]): try: diff --git a/vmware_nsx/plugins/nsx_v/vshield/vcns.py b/vmware_nsx/plugins/nsx_v/vshield/vcns.py index 27d1de9c40..8704295fec 100644 --- a/vmware_nsx/plugins/nsx_v/vshield/vcns.py +++ b/vmware_nsx/plugins/nsx_v/vshield/vcns.py @@ -42,12 +42,14 @@ FIREWALL_RULE_RESOURCE = "rules" #NSXv Constants FIREWALL_PREFIX = '/api/4.0/firewall/globalroot-0/config' +FIREWALL_REDIRECT_SEC_TYPE = 'layer3redirectsections' SECURITYGROUP_PREFIX = '/api/2.0/services/securitygroup' VDN_PREFIX = '/api/2.0/vdn' SERVICES_PREFIX = '/api/2.0/services' SPOOFGUARD_PREFIX = '/api/4.0/services/spoofguard' TRUSTSTORE_PREFIX = '%s/%s' % (SERVICES_PREFIX, 'truststore') EXCLUDELIST_PREFIX = '/api/2.1/app/excludelist' +SERVICE_INSERTION_PROFILE_PREFIX = '/api/2.0/si/serviceprofile' #LbaaS Constants LOADBALANCER_SERVICE = "loadbalancer/config" @@ -547,6 +549,17 @@ class Vcns(object): uri = self._build_uri_path(edge_id, BRIDGE) return self.do_request(HTTP_DELETE, uri, format='xml', decode=False) + def create_redirect_section(self, request): + """Creates a layer 3 redirect section in nsx rule table. + + The method will return the uri to newly created section. + """ + sec_type = FIREWALL_REDIRECT_SEC_TYPE + uri = '%s/%s?autoSaveDraft=false' % (FIREWALL_PREFIX, sec_type) + uri += '&operation=insert_before&anchorId=1002' + return self.do_request(HTTP_POST, uri, request, format='xml', + decode=False, encode=False) + def create_section(self, type, request, insert_before=None): """Creates a layer 3 or layer 2 section in nsx rule table. @@ -911,3 +924,14 @@ class Vcns(object): uri = '%s/%s/%s?noOfDays=%s' % (TRUSTSTORE_PREFIX, CSR, csr_id, nsxv_constants.CERT_NUMBER_OF_DAYS) return self.do_request(HTTP_PUT, uri) + + def get_service_insertion_profile(self, profile_id): + profiles_uri = '%s/%s' % (SERVICE_INSERTION_PROFILE_PREFIX, profile_id) + return self.do_request(HTTP_GET, profiles_uri, format='xml', + decode=False) + + def update_service_insertion_profile_binding(self, profile_id, request): + profiles_uri = '%s/%s/%s' % (SERVICE_INSERTION_PROFILE_PREFIX, + profile_id, 'binding') + return self.do_request(HTTP_POST, profiles_uri, request, format='xml', + decode=False) diff --git a/vmware_nsx/services/flowclassifier/__init__.py b/vmware_nsx/services/flowclassifier/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vmware_nsx/services/flowclassifier/nsx_v/README.rst b/vmware_nsx/services/flowclassifier/nsx_v/README.rst new file mode 100644 index 0000000000..ce1790f9f2 --- /dev/null +++ b/vmware_nsx/services/flowclassifier/nsx_v/README.rst @@ -0,0 +1,30 @@ +=============================================================== + Enabling NSX Flow Classifier for service insertion in DevStack +=============================================================== + +1. Download DevStack + +2. Update the ``local.conf`` file:: + + [[local|localrc]] + enable_plugin networking-sfc https://git.openstack.org/openstack/networking-sfc master + + [[post-config|$NEUTRON_CONF]] + [DEFAULT] + service_plugins = networking_sfc.services.flowclassifier.plugin.FlowClassifierPlugin + + [flowclassifier] + drivers = vmware-nsxv-sfc + + [nsxv] + service_insertion_profile_id = + +3. In order to prevent tenants from changing the flow classifier, please add the following + lines to the policy.json file: + + "create_flow_classifier": "rule:admin_only", + "update_flow_classifier": "rule:admin_only", + "delete_flow_classifier": "rule:admin_only", + "get_flow_classifier": "rule:admin_only" + +4. run ``stack.sh`` diff --git a/vmware_nsx/services/flowclassifier/nsx_v/__init__.py b/vmware_nsx/services/flowclassifier/nsx_v/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vmware_nsx/services/flowclassifier/nsx_v/driver.py b/vmware_nsx/services/flowclassifier/nsx_v/driver.py new file mode 100644 index 0000000000..fe2afb24d0 --- /dev/null +++ b/vmware_nsx/services/flowclassifier/nsx_v/driver.py @@ -0,0 +1,341 @@ +# Copyright 2016 VMware, Inc. +# +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import xml.etree.ElementTree as et + +from networking_sfc.services.flowclassifier.common import exceptions as exc +from networking_sfc.services.flowclassifier.drivers import base as fc_driver +from oslo_config import cfg +from oslo_log import helpers as log_helpers +from oslo_log import log as logging + +from vmware_nsx._i18n import _, _LE +from vmware_nsx.common import config # noqa +from vmware_nsx.common import exceptions as nsx_exc +from vmware_nsx.common import locking +from vmware_nsx.plugins.nsx_v.vshield import vcns as nsxv_api +from vmware_nsx.plugins.nsx_v.vshield import vcns_driver +from vmware_nsx.services.flowclassifier.nsx_v import utils as fc_utils + +LOG = logging.getLogger(__name__) + +REDIRECT_FW_SECTION_NAME = 'OS Flow Classifier Rules' +MAX_PORTS_IN_RANGE = 15 + + +class NsxvFlowClassifierDriver(fc_driver.FlowClassifierDriverBase): + """FlowClassifier Driver For NSX-V.""" + + _redirect_section_id = None + + def initialize(self): + self._nsxv = vcns_driver.VcnsDriver(None) + self.init_profile_id() + self.init_security_group() + self.init_security_group_in_profile() + + #TODO(asarfaty) - Add a new config for any->any redirect: + # create any->any flow classifier entry (and backed rule) + # if not exist yet + + def init_profile_id(self): + """Init the service insertion profile ID + + Initialize the profile id that should be assigned to the redirect + rules from the nsx configuration and verify that it exists on backend. + """ + if not cfg.CONF.nsxv.service_insertion_profile_id: + raise cfg.RequiredOptError("service_profile_id") + self._profile_id = cfg.CONF.nsxv.service_insertion_profile_id + + # Verify that this moref exists + if not self._nsxv.vcns.validate_inventory(self._profile_id): + error = (_("Configured service profile ID: %s not found") % + self._profile_id) + raise nsx_exc.NsxPluginException(err_msg=error) + + def init_security_group(self): + """Init the service insertion security group + + Look for the service insertion security group in the backend. + If it was not found - create it + This security group will contain all the VMs vnics that should + be inspected by the redirect rules + """ + # check if this group exist, and create it if not. + sg_name = fc_utils.SERVICE_INSERTION_SG_NAME + sg_id = self._nsxv.vcns.get_security_group_id(sg_name) + if not sg_id: + description = ("OpenStack Service Insertion Security Group, " + "managed by Neutron nsx-v plugin.") + sg = {"securitygroup": {"name": sg_name, + "description": description}} + h, sg_id = ( + self._nsxv.vcns.create_security_group(sg)) + + # TODO(asarfaty) - if the security group was just created + # also add all the current compute ports with port-security + # to this security group (for upgrades scenarios) + + self._security_group_id = sg_id + + def init_security_group_in_profile(self): + """Attach the security group to the service profile + """ + data = self._nsxv.vcns.get_service_insertion_profile(self._profile_id) + if data and len(data) > 1: + profile = et.fromstring(data[1]) + profile_binding = profile.find('serviceProfileBinding') + sec_groups = profile_binding.find('securityGroups') + for sec in sec_groups.iter('string'): + if sec.text == self._security_group_id: + # Already there + return + # add the security group to the binding + et.SubElement(sec_groups, 'string').text = self._security_group_id + self._nsxv.vcns.update_service_insertion_profile_binding( + self._profile_id, + et.tostring(profile_binding, encoding="us-ascii")) + + def get_redirect_fw_section_id(self): + if not self._redirect_section_id: + # try to find it + self._redirect_section_id = self._nsxv.vcns.get_section_id( + REDIRECT_FW_SECTION_NAME) + if not self._redirect_section_id: + # create it for the first time + section = et.Element('section') + section.attrib['name'] = REDIRECT_FW_SECTION_NAME + self._nsxv.vcns.create_redirect_section(et.tostring(section)) + self._redirect_section_id = self._nsxv.vcns.get_section_id( + REDIRECT_FW_SECTION_NAME) + + return self._redirect_section_id + + def get_redirect_fw_section_uri(self): + return '%s/%s/%s' % (nsxv_api.FIREWALL_PREFIX, + nsxv_api.FIREWALL_REDIRECT_SEC_TYPE, + self.get_redirect_fw_section_id()) + + def get_redirect_fw_section_from_backend(self): + section_uri = self.get_redirect_fw_section_uri() + section_resp = self._nsxv.vcns.get_section(section_uri) + if section_resp and len(section_resp) > 1: + xml_section = section_resp[1] + return et.fromstring(xml_section) + + def update_redirect_section_in_backed(self, section): + section_uri = self.get_redirect_fw_section_uri() + self._nsxv.vcns.update_section( + section_uri, + et.tostring(section, encoding="us-ascii"), + None) + + def _rule_ip_type(self, flow_classifier): + if flow_classifier.get('ethertype') == 'IPv6': + return 'Ipv6Address' + return 'Ipv4Address' + + def _rule_ports(self, type, flow_classifier): + min_port = flow_classifier.get(type + '_port_range_min') + max_port = flow_classifier.get(type + '_port_range_max') + return self._ports_list(min_port, max_port) + + def _ports_list(self, min_port, max_port): + """Return a string of comma separated ports. i.e. '80,81' + """ + # convert the range into a string, and remove the '[]' around it + return str(range(min_port, max_port + 1))[1:-1] + + def _rule_name(self, flow_classifier): + # The name of the rule will include the name & id of the classifier + # so we can later find it in order to update/delete it. + # Both the flow classifier DB & the backend has max name length of 255 + # so we may have to trim the name a bit + return (flow_classifier.get('name')[:200] + '-' + + flow_classifier.get('id')) + + def _is_the_same_rule(self, rule, flow_classifier_id): + return rule.find('name').text.endswith(flow_classifier_id) + + def init_redirect_fw_rule(self, redirect_rule, flow_classifier): + et.SubElement(redirect_rule, 'name').text = self._rule_name( + flow_classifier) + et.SubElement(redirect_rule, 'action').text = 'redirect' + et.SubElement(redirect_rule, 'direction').text = 'inout' + si_profile = et.SubElement(redirect_rule, 'siProfile') + et.SubElement(si_profile, 'objectId').text = self._profile_id + + et.SubElement(redirect_rule, 'packetType').text = flow_classifier.get( + 'ethertype').lower() + + # init the source & destination + if flow_classifier.get('source_ip_prefix'): + sources = et.SubElement(redirect_rule, 'sources') + sources.attrib['excluded'] = 'false' + source = et.SubElement(sources, 'source') + et.SubElement(source, 'type').text = self._rule_ip_type( + flow_classifier) + et.SubElement(source, 'value').text = flow_classifier.get( + 'source_ip_prefix') + + if flow_classifier.get('destination_ip_prefix'): + destinations = et.SubElement(redirect_rule, 'destinations') + destinations.attrib['excluded'] = 'false' + destination = et.SubElement(destinations, 'destination') + et.SubElement(destination, 'type').text = self._rule_ip_type( + flow_classifier) + et.SubElement(destination, 'value').text = flow_classifier.get( + 'destination_ip_prefix') + + # init the service + if (flow_classifier.get('destination_port_range_min') or + flow_classifier.get('source_port_range_min')): + services = et.SubElement(redirect_rule, 'services') + service = et.SubElement(services, 'service') + et.SubElement(service, 'isValid').text = 'true' + if flow_classifier.get('source_port_range_min'): + source_port = et.SubElement(service, 'sourcePort') + source_port.text = self._rule_ports('source', + flow_classifier) + + if flow_classifier.get('destination_port_range_min'): + dest_port = et.SubElement(service, 'destinationPort') + dest_port.text = self._rule_ports('destination', + flow_classifier) + + prot = et.SubElement(service, 'protocolName') + prot.text = flow_classifier.get('protocol').upper() + + # Add the classifier description + if flow_classifier.get('description'): + notes = et.SubElement(redirect_rule, 'notes') + notes.text = flow_classifier.get('description') + + def _loc_fw_section(self): + return locking.LockManager.get_lock('redirect-fw-section') + + @log_helpers.log_method_call + def create_flow_classifier(self, context): + """Create a redirect rule at the backend + """ + flow_classifier = context.current + with self._loc_fw_section(): + section = self.get_redirect_fw_section_from_backend() + new_rule = et.SubElement(section, 'rule') + self.init_redirect_fw_rule(new_rule, flow_classifier) + self.update_redirect_section_in_backed(section) + + @log_helpers.log_method_call + def update_flow_classifier(self, context): + """Update the backend redirect rule + """ + flow_classifier = context.current + + with self._loc_fw_section(): + section = self.get_redirect_fw_section_from_backend() + redirect_rule = None + for rule in section.iter('rule'): + if self._is_the_same_rule(rule, flow_classifier['id']): + redirect_rule = rule + break + + if redirect_rule is None: + msg = _("Failed to find redirect rule %s " + "on backed") % flow_classifier['id'] + raise exc.FlowClassifierException(message=msg) + else: + # The flowclassifier plugin currently supports updating only + # name or description + name = redirect_rule.find('name') + name.text = self._rule_name(flow_classifier) + notes = redirect_rule.find('notes') + notes.text = flow_classifier.get('description') or '' + self.update_redirect_section_in_backed(section) + + @log_helpers.log_method_call + def delete_flow_classifier(self, context): + """Delete the backend redirect rule + """ + flow_classifier_id = context.current['id'] + with self._loc_fw_section(): + section = self.get_redirect_fw_section_from_backend() + redirect_rule = None + for rule in section.iter('rule'): + if self._is_the_same_rule(rule, flow_classifier_id): + redirect_rule = rule + section.remove(redirect_rule) + break + + if redirect_rule is None: + LOG.error(_LE("Failed to delete redirect rule %s: " + "Could not find rule on backed"), + flow_classifier_id) + # should not fail the deletion + else: + self.update_redirect_section_in_backed(section) + + @log_helpers.log_method_call + def create_flow_classifier_precommit(self, context): + """Validate the flow classifier data before committing the transaction + + The NSX-v redirect rules does not support: + - logical ports + - l7 parameters + - source ports range / destination port range with more than 15 ports + """ + flow_classifier = context.current + + # Logical source port + logical_source_port = flow_classifier['logical_source_port'] + if logical_source_port is not None: + msg = _('The NSXv driver does not support setting ' + 'logical source port in FlowClassifier') + raise exc.FlowClassifierBadRequest(message=msg) + + # Logical destination port + logical_destination_port = flow_classifier['logical_destination_port'] + if logical_destination_port is not None: + msg = _('The NSXv driver does not support setting ' + 'logical destination port in FlowClassifier') + raise exc.FlowClassifierBadRequest(message=msg) + + # L7 parameters + l7_params = flow_classifier['l7_parameters'] + if l7_params is not None and len(l7_params.keys()) > 0: + msg = _('The NSXv driver does not support setting ' + 'L7 parameters in FlowClassifier') + raise exc.FlowClassifierBadRequest(message=msg) + + # Source ports range - up to 15 ports. + sport_min = flow_classifier['source_port_range_min'] + sport_max = flow_classifier['source_port_range_max'] + if (sport_min is not None and sport_max is not None and + (sport_max + 1 - sport_min) > MAX_PORTS_IN_RANGE): + msg = _('The NSXv driver does not support setting ' + 'more than %d source ports in a ' + 'FlowClassifier') % MAX_PORTS_IN_RANGE + raise exc.FlowClassifierBadRequest(message=msg) + + # Destination ports range - up to 15 ports. + dport_min = flow_classifier['destination_port_range_min'] + dport_max = flow_classifier['destination_port_range_max'] + if (dport_min is not None and dport_max is not None and + (dport_max + 1 - dport_min) > MAX_PORTS_IN_RANGE): + msg = _('The NSXv driver does not support setting ' + 'more than %d destination ports in a ' + 'FlowClassifier') % MAX_PORTS_IN_RANGE + raise exc.FlowClassifierBadRequest(message=msg) diff --git a/vmware_nsx/services/flowclassifier/nsx_v/utils.py b/vmware_nsx/services/flowclassifier/nsx_v/utils.py new file mode 100644 index 0000000000..e6bed765c7 --- /dev/null +++ b/vmware_nsx/services/flowclassifier/nsx_v/utils.py @@ -0,0 +1,62 @@ +# Copyright 2016 VMware, Inc. +# +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from networking_sfc.extensions import flowclassifier +from neutron import manager +from oslo_log import log as logging + +LOG = logging.getLogger(__name__) +SERVICE_INSERTION_SG_NAME = 'Service Insertion Security Group' + + +class NsxvServiceInsertionHandler(object): + + def __init__(self, core_plugin): + super(NsxvServiceInsertionHandler, self).__init__() + self._nsxv = core_plugin.nsx_v + self._initialized = False + + def _initialize_handler(self): + if not self._initialized: + self._enabled = False + self._sg_id = None + if self.is_service_insertion_enabled(): + self._enabled = True + self._sg_id = self.get_service_inserion_sg_id() + self._initialized = True + + def is_service_insertion_enabled(self): + # Note - this cannot be called during init, since the manager is busy + if (manager.NeutronManager.get_service_plugins().get( + flowclassifier.FLOW_CLASSIFIER_EXT)): + return True + return False + + def get_service_inserion_sg_id(self): + # Note - this cannot be called during init, since the nsxv flow + # classifier driver creates this group + return self._nsxv.vcns.get_security_group_id( + SERVICE_INSERTION_SG_NAME) + + @property + def enabled(self): + self._initialize_handler() + return self._enabled + + @property + def sg_id(self): + self._initialize_handler() + return self._sg_id diff --git a/vmware_nsx/tests/unit/nsx_v/test_plugin.py b/vmware_nsx/tests/unit/nsx_v/test_plugin.py index 1a076e50a5..d093d8651f 100644 --- a/vmware_nsx/tests/unit/nsx_v/test_plugin.py +++ b/vmware_nsx/tests/unit/nsx_v/test_plugin.py @@ -3826,6 +3826,39 @@ class TestNSXPortSecurity(test_psec.TestPortSecurity, self._toggle_port_security(port1['port']['id'], False, True) self._toggle_port_security(port2['port']['id'], False, False) + def test_service_insertion(self): + # init the plugin mocks + p = manager.NeutronManager.get_plugin() + self.fc2.add_member_to_security_group = ( + mock.Mock().add_member_to_security_group) + self.fc2.remove_member_from_security_group = ( + mock.Mock().remove_member_from_security_group) + + # mock the service insertion handler + p._si_handler = mock.Mock() + p._si_handler.enabled = True + p._si_handler.sg_id = '11' + + # create a compute port with port security + device_id = _uuid() + port = self._create_compute_port('net1', device_id, True) + + # add vnic to the port, and verify that the port was added to the + # service insertion security group + vnic_id = 3 + vnic_index = '%s.%03d' % (device_id, vnic_id) + self.fc2.add_member_to_security_group.reset_mock() + self._add_vnic_to_port(port['port']['id'], False, vnic_id) + self.fc2.add_member_to_security_group.assert_any_call( + p._si_handler.sg_id, vnic_index) + + # disable the port security and make sure it is removed from the + # security group + self.fc2.remove_member_from_security_group.reset_mock() + self._toggle_port_security(port['port']['id'], False, True) + self.fc2.remove_member_from_security_group.assert_any_call( + p._si_handler.sg_id, vnic_index) + class TestSharedRouterTestCase(L3NatTest, L3NatTestCaseBase, test_l3_plugin.L3NatTestCaseMixin, diff --git a/vmware_nsx/tests/unit/nsx_v/vshield/fake_vcns.py b/vmware_nsx/tests/unit/nsx_v/vshield/fake_vcns.py index 8f25e1fdeb..2d570d17ee 100644 --- a/vmware_nsx/tests/unit/nsx_v/vshield/fake_vcns.py +++ b/vmware_nsx/tests/unit/nsx_v/vshield/fake_vcns.py @@ -919,6 +919,9 @@ class FakeVcns(object): response += self.get_security_group(k) return header, response + def create_redirect_section(self, request): + return self.create_section('layer3redirect', request) + def create_section(self, type, request, insert_before=None): section = ET.fromstring(request) section_name = section.attrib.get('name') @@ -1197,3 +1200,25 @@ class FakeVcns(object): } response = {'staticRoutes': {'staticRoutes': []}} return (header, response) + + def get_service_insertion_profile(self, profile_id): + headers = {'status': 200} + response = """ + %s + ServiceProfile + ServiceProfile + Service_Vendor + + + securitygroup-30 + + + """ + response_format = response % profile_id + + return (headers, response_format) + + def update_service_insertion_profile_binding(self, profile_id, request): + response = '' + headers = {'status': 200} + return (headers, response) diff --git a/vmware_nsx/tests/unit/services/flowclassifier/__init__.py b/vmware_nsx/tests/unit/services/flowclassifier/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vmware_nsx/tests/unit/services/flowclassifier/test_nsxv_driver.py b/vmware_nsx/tests/unit/services/flowclassifier/test_nsxv_driver.py new file mode 100644 index 0000000000..29e7b8cad7 --- /dev/null +++ b/vmware_nsx/tests/unit/services/flowclassifier/test_nsxv_driver.py @@ -0,0 +1,266 @@ +# Copyright 2016 VMware, Inc. +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import mock +from oslo_config import cfg +from oslo_utils import importutils + +from vmware_nsx.services.flowclassifier.nsx_v import driver as nsx_v_driver +from vmware_nsx.tests import unit as vmware +from vmware_nsx.tests.unit.nsx_v.vshield import fake_vcns + +from neutron.api import extensions as api_ext +from neutron.common import config +from neutron import context +from neutron.extensions import portbindings + +from networking_sfc.db import flowclassifier_db as fdb +from networking_sfc.extensions import flowclassifier +from networking_sfc.services.flowclassifier.common import context as fc_ctx +from networking_sfc.services.flowclassifier.common import exceptions as fc_exc +from networking_sfc.tests import base +from networking_sfc.tests.unit.db import test_flowclassifier_db + + +class TestNsxvFlowClassifierDriver( + test_flowclassifier_db.FlowClassifierDbPluginTestCaseBase, + base.NeutronDbPluginV2TestCase): + + resource_prefix_map = dict([ + (k, flowclassifier.FLOW_CLASSIFIER_PREFIX) + for k in flowclassifier.RESOURCE_ATTRIBUTE_MAP.keys() + ]) + + def setUp(self): + # init the flow classifier plugin + flowclassifier_plugin = ( + test_flowclassifier_db.DB_FLOWCLASSIFIER_PLUGIN_CLASS) + + service_plugins = { + flowclassifier.FLOW_CLASSIFIER_EXT: flowclassifier_plugin + } + fdb.FlowClassifierDbPlugin.supported_extension_aliases = [ + flowclassifier.FLOW_CLASSIFIER_EXT] + fdb.FlowClassifierDbPlugin.path_prefix = ( + flowclassifier.FLOW_CLASSIFIER_PREFIX + ) + + super(TestNsxvFlowClassifierDriver, self).setUp( + ext_mgr=None, + plugin=None, + service_plugins=service_plugins + ) + + self.flowclassifier_plugin = importutils.import_object( + flowclassifier_plugin) + ext_mgr = api_ext.PluginAwareExtensionManager( + test_flowclassifier_db.extensions_path, + { + flowclassifier.FLOW_CLASSIFIER_EXT: self.flowclassifier_plugin + } + ) + app = config.load_paste_app('extensions_test_app') + self.ext_api = api_ext.ExtensionMiddleware(app, ext_mgr=ext_mgr) + self.ctx = context.get_admin_context() + + # use the fake vcns + mock_vcns = mock.patch(vmware.VCNS_NAME, autospec=True) + mock_vcns_instance = mock_vcns.start() + self.fc2 = fake_vcns.FakeVcns() + mock_vcns_instance.return_value = self.fc2 + + # use the nsxv flow classifier driver + self._profile_id = 'serviceprofile-1' + cfg.CONF.set_override('service_insertion_profile_id', + self._profile_id, 'nsxv') + self.driver = nsx_v_driver.NsxvFlowClassifierDriver() + self.driver.initialize() + + self._fc_name = 'test1' + self._fc_description = 'test 1' + self._fc_source = '10.10.0.0/24' + self._fc_dest = '20.10.0.0/24' + self._fc_prot = 'TCP' + self._fc_source_ports = range(100, 115) + self._fc_dest_ports = range(80, 81) + self._fc = {'name': self._fc_name, + 'description': self._fc_description, + 'logical_source_port': None, + 'logical_destination_port': None, + 'source_ip_prefix': self._fc_source, + 'destination_ip_prefix': self._fc_dest, + 'protocol': self._fc_prot, + 'source_port_range_min': self._fc_source_ports[0], + 'source_port_range_max': self._fc_source_ports[-1], + 'destination_port_range_min': self._fc_dest_ports[0], + 'destination_port_range_max': self._fc_dest_ports[-1]} + + def tearDown(self): + super(TestNsxvFlowClassifierDriver, self).tearDown() + + def test_driver_init(self): + self.assertEqual(self.driver._profile_id, self._profile_id) + self.assertEqual(self.driver._security_group_id, '0') + + def test_create_flow_classifier_precommit(self): + with self.flow_classifier(flow_classifier=self._fc) as fc: + fc_context = fc_ctx.FlowClassifierContext( + self.flowclassifier_plugin, self.ctx, + fc['flow_classifier'] + ) + # just make sure it does not raise an exception + self.driver.create_flow_classifier_precommit(fc_context) + + def test_create_flow_classifier_precommit_logical_source_port(self): + with self.port( + name='port1', + device_owner='compute', + device_id='test', + arg_list=( + portbindings.HOST_ID, + ), + **{portbindings.HOST_ID: 'test'} + ) as src_port: + with self.flow_classifier(flow_classifier={ + 'name': 'test1', + 'logical_source_port': src_port['port']['id'] + }) as fc: + fc_context = fc_ctx.FlowClassifierContext( + self.flowclassifier_plugin, self.ctx, + fc['flow_classifier'] + ) + self.assertRaises( + fc_exc.FlowClassifierBadRequest, + self.driver.create_flow_classifier_precommit, + fc_context) + + def test_create_flow_classifier_precommit_logical_dest_port(self): + with self.port( + name='port1', + device_owner='compute', + device_id='test', + arg_list=( + portbindings.HOST_ID, + ), + **{portbindings.HOST_ID: 'test'} + ) as dst_port: + with self.flow_classifier(flow_classifier={ + 'name': 'test1', + 'logical_destination_port': dst_port['port']['id'] + }) as fc: + fc_context = fc_ctx.FlowClassifierContext( + self.flowclassifier_plugin, self.ctx, + fc['flow_classifier'] + ) + self.assertRaises( + fc_exc.FlowClassifierBadRequest, + self.driver.create_flow_classifier_precommit, + fc_context) + + def test_create_flow_classifier_precommit_src_port_range(self): + with self.flow_classifier(flow_classifier={ + 'name': 'test1', + 'protocol': 'tcp', + 'source_port_range_min': 100, + 'source_port_range_max': 116, + }) as fc: + fc_context = fc_ctx.FlowClassifierContext( + self.flowclassifier_plugin, self.ctx, + fc['flow_classifier'] + ) + self.assertRaises( + fc_exc.FlowClassifierBadRequest, + self.driver.create_flow_classifier_precommit, + fc_context) + + def test_create_flow_classifier_precommit_dst_port_range(self): + with self.flow_classifier(flow_classifier={ + 'name': 'test1', + 'protocol': 'tcp', + 'destination_port_range_min': 100, + 'destination_port_range_max': 116, + }) as fc: + fc_context = fc_ctx.FlowClassifierContext( + self.flowclassifier_plugin, self.ctx, + fc['flow_classifier'] + ) + self.assertRaises( + fc_exc.FlowClassifierBadRequest, + self.driver.create_flow_classifier_precommit, + fc_context) + + def _validate_rule_structure(self, rule): + self.assertEqual(self._fc_description, rule.find('notes').text) + self.assertEqual('ipv4', rule.find('packetType').text) + self.assertEqual( + self._fc_source, + rule.find('sources').find('source').find('value').text) + self.assertEqual( + self._fc_dest, + rule.find('destinations').find('destination').find('value').text) + self.assertEqual( + str(self._fc_source_ports)[1:-1], + rule.find('services').find('service').find('sourcePort').text) + self.assertEqual( + str(self._fc_dest_ports)[1:-1], + rule.find('services').find('service').find('destinationPort').text) + self.assertEqual( + self._fc_prot, + rule.find('services').find('service').find('protocolName').text) + self.assertTrue(rule.find('name').text.startswith(self._fc_name)) + + def test_create_flow_classifier(self): + with self.flow_classifier(flow_classifier=self._fc) as fc: + fc_context = fc_ctx.FlowClassifierContext( + self.flowclassifier_plugin, self.ctx, + fc['flow_classifier'] + ) + with mock.patch.object( + self.driver, + 'update_redirect_section_in_backed') as mock_update_section: + self.driver.create_flow_classifier(fc_context) + self.assertTrue(mock_update_section.called) + section = mock_update_section.call_args[0][0] + self._validate_rule_structure(section.find('rule')) + + def test_update_flow_classifier(self): + with self.flow_classifier(flow_classifier=self._fc) as fc: + fc_context = fc_ctx.FlowClassifierContext( + self.flowclassifier_plugin, self.ctx, + fc['flow_classifier'] + ) + self.driver.create_flow_classifier(fc_context) + with mock.patch.object( + self.driver, + 'update_redirect_section_in_backed') as mock_update_section: + self.driver.update_flow_classifier(fc_context) + self.assertTrue(mock_update_section.called) + section = mock_update_section.call_args[0][0] + self._validate_rule_structure(section.find('rule')) + + def test_delete_flow_classifier(self): + with self.flow_classifier(flow_classifier=self._fc) as fc: + fc_context = fc_ctx.FlowClassifierContext( + self.flowclassifier_plugin, self.ctx, + fc['flow_classifier'] + ) + self.driver.create_flow_classifier(fc_context) + with mock.patch.object( + self.driver, + 'update_redirect_section_in_backed') as mock_update_section: + self.driver.delete_flow_classifier(fc_context) + self.assertTrue(mock_update_section.called) + section = mock_update_section.call_args[0][0] + # make sure the rule is not there + self.assertEqual(None, section.find('rule'))