group-based-policy/gbpservice/neutron/extensions/patch.py

498 lines
23 KiB
Python

# 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 copy
from neutron.api import extensions
from neutron.api.v2 import resource as neutron_resource
from neutron.db.db_base_plugin_v2 import _constants
from neutron.db import l3_db
from neutron.db import models_v2
from neutron.db import securitygroups_db
from neutron.objects import securitygroup as sg_obj
from neutron.plugins.ml2 import db as ml2_db
from neutron.quota import resource as quota_resource
from neutron_lib.api import attributes
from neutron_lib.api import validators
from neutron_lib.db import model_query
from neutron_lib import exceptions
from neutron_lib.plugins import directory
from oslo_log import log
from oslo_utils import excutils
from gbpservice.common import utils as gbp_utils
from gbpservice.neutron.db import api as db_api
LOG = log.getLogger(__name__)
if not hasattr(quota_resource, 'GBP_PATCHED'):
orig_count_resource = quota_resource._count_resource
def new_count_resource(*kwargs):
request = gbp_utils.get_obj_from_stack(neutron_resource.Request)
orig_plugins = directory._get_plugin_directory()._plugins
if request and request.environ['PATH_INFO'] == (
'/servicechain/service_profiles.json'):
new_plugins = copy.copy(directory._get_plugin_directory()._plugins)
# The service_profile resource is supported by the FLAVORS
# plugin as well as the SERVICECHAIN plugin. At this point
# we know that we are dealing with the service_profile from
# SERVICECHAIN, and since the original implementation of the
# count_resource will think of service_profile from FLAVORS
# (in the sorted order of plugins, FLAVORS preceedes SERVICECHAIN)
# we temporarily remove the FLAVORS plugin reference from the
# plugins directory.
new_plugins.pop('FLAVORS')
directory._get_plugin_directory()._plugins = new_plugins
count_resource = orig_count_resource(*kwargs)
directory._get_plugin_directory()._plugins = orig_plugins
return count_resource
quota_resource._count_resource = new_count_resource
quota_resource.GBP_PATCHED = True
# REVISIT(ivar): Monkey patch to allow explicit router_id to be set in Neutron
# for Floating Ip creation (for internal calls only). Once we split the server,
# this could be part of a GBP Neutron L3 driver.
def _get_assoc_data(self, context, fip, floatingip_db):
(internal_port, internal_subnet_id,
internal_ip_address) = self._internal_fip_assoc_data(
context, fip, floatingip_db['tenant_id'])
if fip.get('router_id'):
router_id = fip['router_id']
del fip['router_id']
else:
router_id = self._get_router_for_floatingip(
context, internal_port, internal_subnet_id,
floatingip_db['floating_network_id'])
return fip['port_id'], internal_ip_address, router_id
l3_db.L3_NAT_dbonly_mixin._get_assoc_data = _get_assoc_data
# REVISIT(ivar): Neutron adds a tenant filter on SG lookup for a given port,
# this breaks our service chain plumbing model so for now we should monkey
# patch the specific method. A follow up with the Neutron team is needed to
# figure out the reason for this and how to proceed for future releases.
def _get_security_groups_on_port(self, context, port):
"""Check that all security groups on port belong to tenant.
:returns: all security groups IDs on port belonging to tenant.
"""
p = port['port']
if not validators.is_attr_set(
p.get(securitygroups_db.ext_sg.SECURITYGROUPS)):
return
if p.get('device_owner') and p['device_owner'].startswith('network:'):
return
port_sg = p.get(securitygroups_db.ext_sg.SECURITYGROUPS, [])
sg_objs = sg_obj.SecurityGroup.get_objects(context, id=port_sg)
valid_groups = set(g['id'] for g in sg_objs)
requested_groups = set(port_sg)
port_sg_missing = requested_groups - valid_groups
if port_sg_missing:
raise securitygroups_db.ext_sg.SecurityGroupNotFound(
id=', '.join(port_sg_missing))
return sg_objs
securitygroups_db.SecurityGroupDbMixin._get_security_groups_on_port = (
_get_security_groups_on_port)
def get_port_from_device_mac(context, device_mac):
LOG.debug("get_port_from_device_mac() called for mac %s", device_mac)
qry = context.session.query(models_v2.Port).filter_by(
mac_address=device_mac).order_by(models_v2.Port.device_owner.desc())
return qry.first()
ml2_db.get_port_from_device_mac = get_port_from_device_mac
def extend_resources(self, version, attr_map):
"""Extend resources with additional resources or attributes.
:param attr_map: the existing mapping from resource name to
attrs definition.
After this function, we will extend the attr_map if an extension
wants to extend this map.
"""
# Match indentation of original version, making comparison easier.
if True:
processed_exts = {}
exts_to_process = self.extensions.copy()
check_optionals = True
# Iterate until there are unprocessed extensions or if no progress
# is made in a whole iteration
while exts_to_process:
processed_ext_count = len(processed_exts)
for ext_name, ext in list(exts_to_process.items()):
# Process extension only if all required extensions
# have been processed already
required_exts_set = set(ext.get_required_extensions())
if required_exts_set - set(processed_exts):
continue
optional_exts_set = set(ext.get_optional_extensions())
if check_optionals and optional_exts_set - set(processed_exts):
continue
extended_attrs = ext.get_extended_resources(version)
for res, resource_attrs in list(extended_attrs.items()):
res_to_update = attr_map.setdefault(res, {})
if self._is_sub_resource(res_to_update):
# kentwu: service_profiles defined in servicechain
# plugin has a name conflict with service_profiles
# sub-resource defined in flavor plugin. The attr_map
# can only have one service_profiles so here we make
# this very same service_profiles to have the
# attributes from both plugins. This behavior is now
# consistent with Pike.
if (ext_name == 'servicechain' and
res == 'service_profiles'):
res_to_update.update(resource_attrs)
# in the case of an existing sub-resource, we need to
# update the parameters content rather than overwrite
# it, and also keep the description of the parent
# resource unmodified
else:
res_to_update['parameters'].update(
resource_attrs['parameters'])
else:
res_to_update.update(resource_attrs)
processed_exts[ext_name] = ext
del exts_to_process[ext_name]
if len(processed_exts) == processed_ext_count:
# if we hit here, it means there are unsatisfied
# dependencies. try again without optionals since optionals
# are only necessary to set order if they are present.
if check_optionals:
check_optionals = False
continue
# Exit loop as no progress was made
break
if exts_to_process:
unloadable_extensions = set(exts_to_process.keys())
LOG.error("Unable to process extensions (%s) because "
"the configured plugins do not satisfy "
"their requirements. Some features will not "
"work as expected.",
', '.join(unloadable_extensions))
self._check_faulty_extensions(unloadable_extensions)
# Extending extensions' attributes map.
for ext in list(processed_exts.values()):
ext.update_attributes_map(attr_map)
extensions.ExtensionManager.extend_resources = extend_resources
def fill_post_defaults(
self, res_dict,
exc_cls=lambda m: exceptions.InvalidInput(error_message=m),
check_allow_post=True):
"""Fill in default values for attributes in a POST request.
When a POST request is made, the attributes with default values do not
need to be specified by the user. This function fills in the values of
any unspecified attributes if they have a default value.
If an attribute is not specified and it does not have a default value,
an exception is raised.
If an attribute is specified and it is not allowed in POST requests, an
exception is raised. The caller can override this behavior by setting
check_allow_post=False (used by some internal admin operations).
:param res_dict: The resource attributes from the request.
:param exc_cls: Exception to be raised on error that must take
a single error message as it's only constructor arg.
:param check_allow_post: Raises an exception if a non-POST-able
attribute is specified.
:raises: exc_cls If check_allow_post is True and this instance of
ResourceAttributes doesn't support POST.
"""
for attr, attr_vals in list(self.attributes.items()):
# kentwu: Patch needed for our GBP service_profiles attribute. Since
# parent and parameters are both sub-resource's attributes picked up
# from flavor plugin so we can just ignore those. These 2 attributes
# don't have allow_post defined so it will just fail without this
# patch.
if attr == 'parent' or attr == 'parameters':
if 'allow_post' not in attr_vals:
continue
if attr_vals['allow_post']:
if 'default' not in attr_vals and attr not in res_dict:
msg = _("Failed to parse request. Required "
"attribute '%s' not specified") % attr
raise exc_cls(msg)
res_dict[attr] = res_dict.get(attr,
attr_vals.get('default'))
elif check_allow_post:
if attr in res_dict:
msg = _("Attribute '%s' not allowed in POST") % attr
raise exc_cls(msg)
attributes.AttributeInfo.fill_post_defaults = fill_post_defaults
# TODO(ivar): while this block would be better place in the patch_neutron
# module, it seems like being part of an "extension" package is the only
# way to make it work at the moment. Tests have shown that Neutorn reloads
# the extensions at every call (at least in the UTs) and this causes the
# AIM_FLC_L7_PARAMS to be reset over and over. By patching at this point,
# we make sure we always have the proper value for that variable.
try:
import six
import sys
from networking_sfc.db import flowclassifier_db
from networking_sfc.db import sfc_db
from networking_sfc.extensions import flowclassifier as fc_ext
from networking_sfc.extensions import sfc as sfc_ext # noqa
from networking_sfc.services.flowclassifier.common import (
exceptions as fc_exc)
from networking_sfc.services.flowclassifier import driver_manager as fc_mgr
from networking_sfc.services.flowclassifier import plugin as fc_plugin
from networking_sfc.services.sfc.common import exceptions as sfc_exc
from networking_sfc.services.sfc import driver_manager as sfc_mgr
from networking_sfc.services.sfc import plugin as sfc_plugin
from neutron_lib.services.trunk import constants
from oslo_utils import uuidutils
from gbpservice.neutron.services.sfc.aim import constants as sfc_cts
if 'flowclassifier' in sys.modules:
sys.modules['flowclassifier'].SUPPORTED_L7_PARAMETERS.update(
sfc_cts.AIM_FLC_L7_PARAMS)
if 'networking_sfc.extensions.flowclassifier' in sys.modules:
sys.modules[
('networking_sfc.extensions.'
'flowclassifier')].SUPPORTED_L7_PARAMETERS.update(
sfc_cts.AIM_FLC_L7_PARAMS)
if 'sfc' in sys.modules:
sys.modules['sfc'].RESOURCE_ATTRIBUTE_MAP['port_pair_groups'][
'port_pair_group_parameters']['validate']['type:dict'].update(
sfc_cts.AIM_PPG_PARAMS)
if 'networking_sfc.extensions.sfc' in sys.modules:
sys.modules['networking_sfc.extensions.sfc'].RESOURCE_ATTRIBUTE_MAP[
'port_pair_groups']['port_pair_group_parameters']['validate'][
'type:dict'].update(sfc_cts.AIM_PPG_PARAMS)
# REVISIT(ivar): The following diff will fix flow classifier creation
# method when using L7 parameters.
# - key: L7Parameter(key, val)
# + key: L7Parameter(keyword=key, value=val)
def create_flow_classifier(self, context, flow_classifier):
fc = flow_classifier['flow_classifier']
tenant_id = fc['tenant_id']
l7_parameters = {
key: flowclassifier_db.L7Parameter(keyword=key, value=val)
for key, val in six.iteritems(fc['l7_parameters'])}
ethertype = fc['ethertype']
protocol = fc['protocol']
source_port_range_min = fc['source_port_range_min']
source_port_range_max = fc['source_port_range_max']
self._check_port_range_valid(source_port_range_min,
source_port_range_max,
protocol)
destination_port_range_min = fc['destination_port_range_min']
destination_port_range_max = fc['destination_port_range_max']
self._check_port_range_valid(destination_port_range_min,
destination_port_range_max,
protocol)
source_ip_prefix = fc['source_ip_prefix']
self._check_ip_prefix_valid(source_ip_prefix, ethertype)
destination_ip_prefix = fc['destination_ip_prefix']
self._check_ip_prefix_valid(destination_ip_prefix, ethertype)
logical_source_port = fc['logical_source_port']
logical_destination_port = fc['logical_destination_port']
with db_api.CONTEXT_WRITER.using(context):
if logical_source_port is not None:
self._get_port(context, logical_source_port)
if logical_destination_port is not None:
self._get_port(context, logical_destination_port)
query = model_query.query_with_hooks(
context, flowclassifier_db.FlowClassifier)
for flow_classifier_db in query.all():
if self.flowclassifier_conflict(
fc,
flow_classifier_db
):
raise fc_ext.FlowClassifierInConflict(
id=flow_classifier_db['id'])
flow_classifier_db = flowclassifier_db.FlowClassifier(
id=uuidutils.generate_uuid(),
tenant_id=tenant_id,
name=fc['name'],
description=fc['description'],
ethertype=ethertype,
protocol=protocol,
source_port_range_min=source_port_range_min,
source_port_range_max=source_port_range_max,
destination_port_range_min=destination_port_range_min,
destination_port_range_max=destination_port_range_max,
source_ip_prefix=source_ip_prefix,
destination_ip_prefix=destination_ip_prefix,
logical_source_port=logical_source_port,
logical_destination_port=logical_destination_port,
l7_parameters=l7_parameters
)
context.session.add(flow_classifier_db)
return self._make_flow_classifier_dict(flow_classifier_db)
flowclassifier_db.FlowClassifierDbPlugin.create_flow_classifier = (
create_flow_classifier)
# Flowclassifier validation should also take into account l7_parameters.
old_validation = (
flowclassifier_db.FlowClassifierDbPlugin.flowclassifier_basic_conflict)
def flowclassifier_basic_conflict(cls, first_flowclassifier,
second_flowclassifier):
def _l7_params_conflict(fc1, fc2):
if (validators.is_attr_set(fc1['l7_parameters']) and
validators.is_attr_set(fc2['l7_parameters'])):
if fc1['l7_parameters'] == fc2['l7_parameters']:
return True
return all(not validators.is_attr_set(fc['l7_parameters'])
for fc in [fc1, fc2])
return cls._old_flowclassifier_basic_conflict(
first_flowclassifier, second_flowclassifier) and (
_l7_params_conflict(first_flowclassifier, second_flowclassifier))
if getattr(flowclassifier_db.FlowClassifierDbPlugin,
'_old_flowclassifier_basic_conflict', None) is None:
flowclassifier_db.FlowClassifierDbPlugin.\
_old_flowclassifier_basic_conflict = old_validation
flowclassifier_db.FlowClassifierDbPlugin.\
flowclassifier_basic_conflict = classmethod(
flowclassifier_basic_conflict)
# NOTE(ivar): Trunk subports don't have a device ID, we need this
# validation to pass
# NOTE(ivar): It would be ideal to re-use the original function and call
# it instead of copying it here. However, it looks like this module gets
# reloaded a number of times and this would cause the original definition
# to be overridden with the new one, thus causing an endless recursion.
def _validate_port_pair_ingress_egress(self, ingress, egress):
if any(port.get('device_owner') == constants.TRUNK_SUBPORT_OWNER
for port in [ingress, egress]):
return
if 'device_id' not in ingress or not ingress['device_id']:
raise sfc_db.ext_sfc.PortPairIngressNoHost(
ingress=ingress['id']
)
if 'device_id' not in egress or not egress['device_id']:
raise sfc_db.ext_sfc.PortPairEgressNoHost(
egress=egress['id']
)
if ingress['device_id'] != egress['device_id']:
raise sfc_db.ext_sfc.PortPairIngressEgressDifferentHost(
ingress=ingress['id'],
egress=egress['id'])
sfc_db.SfcDbPlugin._validate_port_pair_ingress_egress = (
_validate_port_pair_ingress_egress)
if not getattr(sfc_plugin.SfcPlugin, '_patch_db_retry', False):
sfc_plugin.SfcPlugin.create_port_pair = (
db_api.retry_if_session_inactive()(
sfc_plugin.SfcPlugin.create_port_pair))
sfc_plugin.SfcPlugin.create_port_pair_group = (
db_api.retry_if_session_inactive()(
sfc_plugin.SfcPlugin.create_port_pair_group))
sfc_plugin.SfcPlugin.create_port_chain = (
db_api.retry_if_session_inactive()(
sfc_plugin.SfcPlugin.create_port_chain))
sfc_plugin.SfcPlugin.update_port_pair = (
db_api.retry_if_session_inactive()(
sfc_plugin.SfcPlugin.update_port_pair))
sfc_plugin.SfcPlugin.update_port_pair_group = (
db_api.retry_if_session_inactive()(
sfc_plugin.SfcPlugin.update_port_pair_group))
sfc_plugin.SfcPlugin.update_port_chain = (
db_api.retry_if_session_inactive()(
sfc_plugin.SfcPlugin.update_port_chain))
sfc_plugin.SfcPlugin.delete_port_pair = (
db_api.retry_if_session_inactive()(
sfc_plugin.SfcPlugin.delete_port_pair))
sfc_plugin.SfcPlugin.delete_port_pair_group = (
db_api.retry_if_session_inactive()(
sfc_plugin.SfcPlugin.delete_port_pair_group))
sfc_plugin.SfcPlugin.delete_port_chain = (
db_api.retry_if_session_inactive()(
sfc_plugin.SfcPlugin.delete_port_chain))
sfc_plugin.SfcPlugin._patch_db_retry = True
if not getattr(fc_plugin.FlowClassifierPlugin, '_patch_db_retry', False):
fc_plugin.FlowClassifierPlugin.create_flow_classifier = (
db_api.retry_if_session_inactive()(
fc_plugin.FlowClassifierPlugin.create_flow_classifier))
fc_plugin.FlowClassifierPlugin.update_flow_classifier = (
db_api.retry_if_session_inactive()(
fc_plugin.FlowClassifierPlugin.update_flow_classifier))
fc_plugin.FlowClassifierPlugin.delete_flow_classifier = (
db_api.retry_if_session_inactive()(
fc_plugin.FlowClassifierPlugin.delete_flow_classifier))
fc_plugin.FlowClassifierPlugin._patch_db_retry = True
# Overrides SFC implementation to avoid eating retriable exceptions
def get_call_drivers(exception, name):
def _call_drivers(self, method_name, context, raise_orig_exc=False):
for driver in self.ordered_drivers:
try:
getattr(driver.obj, method_name)(context)
except Exception as e:
# This is an internal failure.
if db_api.is_retriable(e):
with excutils.save_and_reraise_exception():
LOG.debug(
"DB exception raised by extension driver "
"'%(name)s' in %(method)s",
{'name': driver.name, 'method': method_name},
exc_info=e)
LOG.exception(e)
LOG.error("%(plugin)s driver '%(name)s' "
"failed in %(method)s",
{'name': driver.name, 'method': method_name,
'plugin': name})
if raise_orig_exc:
raise
else:
raise exception(method=method_name)
return _call_drivers
sfc_mgr.SfcDriverManager._call_drivers = get_call_drivers(
sfc_exc.SfcDriverError, 'SFC')
fc_mgr.FlowClassifierDriverManager._call_drivers = get_call_drivers(
fc_exc.FlowClassifierDriverError, 'FlowClassifier')
except ImportError as e:
LOG.warning("Import error while patching networking-sfc: %s",
str(e))
DEVICE_OWNER_SVI_PORT = 'apic:svi'
_constants.AUTO_DELETE_PORT_OWNERS.append(DEVICE_OWNER_SVI_PORT)