NSX-V Service insertion support
The service insertion feature allows us to redirect some of the NSX traffic to an external security vendor like Palo-Alto or checkpoint for advanced inspection. The implementation contains: Enable the flow classifier plugin, and use it to create redirect rules on NSX When the flow classifier plugin is initialized a new security group is created and added to the configured service profile When a vm port with port security is created/updated, it is added to this security group When the admin user create a flow classifier entry, a backed redirect rule will be created. DocImpact: new NSXV Configuration parameters: service_insertion_profile_id = <service profile id, i.e. serviceprofile-1> DocImpact: The flow classifier methods should be added to the policy.json as admin only Change-Id: I67a132d4b35764c6940516a8365a2749d574aad2
This commit is contained in:
parent
e64909eac9
commit
ce9003f498
@ -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"
|
||||
}
|
||||
|
@ -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
|
||||
|
1
tox.ini
1
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 =
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
0
vmware_nsx/services/flowclassifier/__init__.py
Normal file
0
vmware_nsx/services/flowclassifier/__init__.py
Normal file
30
vmware_nsx/services/flowclassifier/nsx_v/README.rst
Normal file
30
vmware_nsx/services/flowclassifier/nsx_v/README.rst
Normal file
@ -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 = <service profile id. i.e. serviceprofile-1>
|
||||
|
||||
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``
|
341
vmware_nsx/services/flowclassifier/nsx_v/driver.py
Normal file
341
vmware_nsx/services/flowclassifier/nsx_v/driver.py
Normal file
@ -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)
|
62
vmware_nsx/services/flowclassifier/nsx_v/utils.py
Normal file
62
vmware_nsx/services/flowclassifier/nsx_v/utils.py
Normal file
@ -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
|
@ -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,
|
||||
|
@ -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 = """
|
||||
<serviceProfile><objectId>%s</objectId>
|
||||
<objectTypeName>ServiceProfile</objectTypeName>
|
||||
<type><typeName>ServiceProfile</typeName></type>
|
||||
<name>Service_Vendor</name>
|
||||
<serviceProfileBinding><distributedVirtualPortGroups/>
|
||||
<virtualWires/><excludedVnics/><virtualServers/>
|
||||
<securityGroups><string>securitygroup-30</string>
|
||||
</securityGroups></serviceProfileBinding>
|
||||
</serviceProfile>
|
||||
"""
|
||||
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)
|
||||
|
@ -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'))
|
Loading…
Reference in New Issue
Block a user