From 5c601bebeb01876f1ea744e0ae95f83d1c30120d Mon Sep 17 00:00:00 2001 From: Hongbin Lu Date: Mon, 20 Nov 2017 23:19:31 +0000 Subject: [PATCH] Support filtering port with IP address substring Neutron currently supports filtering ports by matching the exact IP address. This patch adds support for substring matching using "LIKE" SQL operator. This patch also added a new API extension to show whether or not the substring matching capability is available. APIImpact add IP address substring filtering on listing ports API-ref: I97259b85a2dce5a54bb6ea2cb9d9779ec0a25504 Co-Authored-By: Zhenyu Zheng Change-Id: I9549b2ba676e1bad0812682c3f3f3c97de15f5f6 Closes-Bug: #1718605 --- .../extensions/ip_substring_port_filtering.py | 23 +++++++ .../ip_substring_port_filtering_lib.py | 64 +++++++++++++++++++ neutron/plugins/ml2/plugin.py | 17 ++++- .../tests/contrib/hooks/api_all_extensions | 1 + neutron/tests/unit/plugins/ml2/test_plugin.py | 61 ++++++++++++++++++ ...tring-port-filtering-f5c3d89c4a91e867.yaml | 4 ++ 6 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 neutron/extensions/ip_substring_port_filtering.py create mode 100644 neutron/extensions/ip_substring_port_filtering_lib.py create mode 100644 releasenotes/notes/ip-substring-port-filtering-f5c3d89c4a91e867.yaml diff --git a/neutron/extensions/ip_substring_port_filtering.py b/neutron/extensions/ip_substring_port_filtering.py new file mode 100644 index 00000000000..4d64a55344b --- /dev/null +++ b/neutron/extensions/ip_substring_port_filtering.py @@ -0,0 +1,23 @@ +# Copyright (c) 2017 Huawei Technology, 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 neutron_lib.api import extensions + +from neutron.extensions import ip_substring_port_filtering_lib as apidef + + +class Ip_substring_port_filtering(extensions.APIExtensionDescriptor): + """Extension class supporting IP substring port filtering.""" + + api_definition = apidef diff --git a/neutron/extensions/ip_substring_port_filtering_lib.py b/neutron/extensions/ip_substring_port_filtering_lib.py new file mode 100644 index 00000000000..8861eb9cf5d --- /dev/null +++ b/neutron/extensions/ip_substring_port_filtering_lib.py @@ -0,0 +1,64 @@ +# 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. + +""" +TODO(hongbin): This module should be deleted once neutron-lib containing +https://review.openstack.org/#/c/525284/ change is released. +""" + +# The alias of the extension. +ALIAS = 'ip-substring-filtering' + +# Whether or not this extension is simply signaling behavior to the user +# or it actively modifies the attribute map. +IS_SHIM_EXTENSION = True + +# Whether the extension is marking the adoption of standardattr model for +# legacy resources, or introducing new standardattr attributes. False or +# None if the standardattr model is adopted since the introduction of +# resource extension. +# If this is True, the alias for the extension should be prefixed with +# 'standard-attr-'. +IS_STANDARD_ATTR_EXTENSION = False + +# The name of the extension. +NAME = 'IP address substring filtering' + +# The description of the extension. +DESCRIPTION = "Provides IP address substring filtering when listing ports" + +# A timestamp of when the extension was introduced. +UPDATED_TIMESTAMP = "2017-11-28T09:00:00-00:00" + +# The resource attribute map for the extension. +RESOURCE_ATTRIBUTE_MAP = { +} + +# The subresource attribute map for the extension. +SUB_RESOURCE_ATTRIBUTE_MAP = { +} + +# The action map. +ACTION_MAP = { +} + +# The action status. +ACTION_STATUS = { +} + +# The list of required extensions. +REQUIRED_EXTENSIONS = [ +] + +# The list of optional extensions. +OPTIONAL_EXTENSIONS = [ +] diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py index 3619e797490..6599b84e527 100644 --- a/neutron/plugins/ml2/plugin.py +++ b/neutron/plugins/ml2/plugin.py @@ -46,6 +46,7 @@ from oslo_utils import excutils from oslo_utils import importutils from oslo_utils import uuidutils import sqlalchemy +from sqlalchemy import or_ from sqlalchemy.orm import exc as sa_exc from neutron._i18n import _ @@ -153,7 +154,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, "availability_zone", "network_availability_zone", "default-subnetpools", - "subnet-service-types"] + "subnet-service-types", + "ip-substring-filtering"] @property def supported_extension_aliases(self): @@ -1849,6 +1851,19 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, return port.id return device + def _get_ports_query(self, context, filters=None, *args, **kwargs): + filters = filters or {} + fixed_ips = filters.get('fixed_ips', {}) + ip_addresses_s = fixed_ips.get('ip_address_substr') + query = super(Ml2Plugin, self)._get_ports_query(context, filters, + *args, **kwargs) + if ip_addresses_s: + substr_filter = or_(*[models_v2.Port.fixed_ips.any( + models_v2.IPAllocation.ip_address.like('%%%s%%' % ip)) + for ip in ip_addresses_s]) + query = query.filter(substr_filter) + return query + def filter_hosts_with_network_access( self, context, network_id, candidate_hosts): segments = segments_db.get_network_segments(context, network_id) diff --git a/neutron/tests/contrib/hooks/api_all_extensions b/neutron/tests/contrib/hooks/api_all_extensions index c9075a32ac4..083e7b8f1cd 100644 --- a/neutron/tests/contrib/hooks/api_all_extensions +++ b/neutron/tests/contrib/hooks/api_all_extensions @@ -15,6 +15,7 @@ NETWORK_API_EXTENSIONS+=",external-net" NETWORK_API_EXTENSIONS+=",extra_dhcp_opt" NETWORK_API_EXTENSIONS+=",extraroute" NETWORK_API_EXTENSIONS+=",flavors" +NETWORK_API_EXTENSIONS+=",ip-substring-filtering" NETWORK_API_EXTENSIONS+=",l3-flavors" NETWORK_API_EXTENSIONS+=",l3-ha" NETWORK_API_EXTENSIONS+=",l3_agent_scheduler" diff --git a/neutron/tests/unit/plugins/ml2/test_plugin.py b/neutron/tests/unit/plugins/ml2/test_plugin.py index 4456d8a6a7a..e6c12e15ccc 100644 --- a/neutron/tests/unit/plugins/ml2/test_plugin.py +++ b/neutron/tests/unit/plugins/ml2/test_plugin.py @@ -1216,6 +1216,67 @@ class TestMl2PortsV2(test_plugin.TestPortsV2, Ml2PluginV2TestCase): # make sure that the grenade went off during the commit self.assertTrue(listener.except_raised) + def test_list_ports_filtered_by_fixed_ip_substring(self): + # for this test we need to enable overlapping ips + cfg.CONF.set_default('allow_overlapping_ips', True) + with self.port() as port1, self.port(): + fixed_ips = port1['port']['fixed_ips'][0] + query_params = """ +fixed_ips=ip_address_substr%%3D%s&fixed_ips=subnet_id%%3D%s +""".strip() % (fixed_ips['ip_address'][:-1], + fixed_ips['subnet_id']) + self._test_list_resources('port', [port1], + query_params=query_params) + query_params = """ +fixed_ips=ip_address_substr%%3D%s&fixed_ips=subnet_id%%3D%s +""".strip() % (fixed_ips['ip_address'][1:], + fixed_ips['subnet_id']) + self._test_list_resources('port', [port1], + query_params=query_params) + query_params = """ +fixed_ips=ip_address_substr%%3D%s&fixed_ips=subnet_id%%3D%s +""".strip() % ('192.168.', + fixed_ips['subnet_id']) + self._test_list_resources('port', [], + query_params=query_params) + + def test_list_ports_filtered_by_fixed_ip_substring_dual_stack(self): + with self.subnet() as subnet: + # Get a IPv4 and IPv6 address + tenant_id = subnet['subnet']['tenant_id'] + net_id = subnet['subnet']['network_id'] + res = self._create_subnet( + self.fmt, + tenant_id=tenant_id, + net_id=net_id, + cidr='2607:f0d0:1002:51::/124', + ip_version=6, + gateway_ip=constants.ATTR_NOT_SPECIFIED) + subnet2 = self.deserialize(self.fmt, res) + kwargs = {"fixed_ips": + [{'subnet_id': subnet['subnet']['id']}, + {'subnet_id': subnet2['subnet']['id']}]} + res = self._create_port(self.fmt, net_id=net_id, **kwargs) + port1 = self.deserialize(self.fmt, res) + res = self._create_port(self.fmt, net_id=net_id, **kwargs) + port2 = self.deserialize(self.fmt, res) + fixed_ips = port1['port']['fixed_ips'] + self.assertEqual(2, len(fixed_ips)) + query_params = """ +fixed_ips=ip_address_substr%%3D%s&fixed_ips=ip_address%%3D%s +""".strip() % (fixed_ips[0]['ip_address'][:-1], + fixed_ips[1]['ip_address']) + self._test_list_resources('port', [port1], + query_params=query_params) + query_params = """ +fixed_ips=ip_address_substr%%3D%s&fixed_ips=ip_address%%3D%s +""".strip() % ('192.168.', + fixed_ips[1]['ip_address']) + self._test_list_resources('port', [], + query_params=query_params) + self._delete('ports', port1['port']['id']) + self._delete('ports', port2['port']['id']) + class TestMl2PortsV2WithRevisionPlugin(Ml2PluginV2TestCase): diff --git a/releasenotes/notes/ip-substring-port-filtering-f5c3d89c4a91e867.yaml b/releasenotes/notes/ip-substring-port-filtering-f5c3d89c4a91e867.yaml new file mode 100644 index 00000000000..de833814a48 --- /dev/null +++ b/releasenotes/notes/ip-substring-port-filtering-f5c3d89c4a91e867.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Support substring matching when filtering ports by IP address.