diff --git a/neutron/agent/l3/extensions/port_forwarding.py b/neutron/agent/l3/extensions/port_forwarding.py index 69abdee4d65..e5fe2f70ad3 100644 --- a/neutron/agent/l3/extensions/port_forwarding.py +++ b/neutron/agent/l3/extensions/port_forwarding.py @@ -154,8 +154,8 @@ class PortForwardingAgentExtension(l3_extension.L3AgentExtension): floating_ip_address = str(port_forward.floating_ip_address) protocol = port_forward.protocol internal_ip_address = str(port_forward.internal_ip_address) - internal_port = port_forward.internal_port - external_port = port_forward.external_port + external_port, internal_port = self.extract_ports(port_forward) + chain_rule = (pf_chain_name, '-d %s/32 -p %s -m %s --dport %s ' '-j DNAT --to-destination %s:%s' % ( @@ -165,6 +165,41 @@ class PortForwardingAgentExtension(l3_extension.L3AgentExtension): chain_rule_list.append(chain_rule) return chain_rule_list + @staticmethod + def extract_ports(port_forward): + # The IP table rules handles internal port ranges with "-" + # while the external ports ranges it handles using ":" + internal_port = port_forward.internal_port_range.replace(':', '-') + external_port = port_forward.external_port_range + internal_start, internal_end = internal_port.split('-') + external_start, external_end = external_port.split(':') + internal_port_one_to_one_mask = '' + is_single_external_port = external_start == external_end + is_single_internal_port = internal_start == internal_end + if is_single_external_port: + external_port = external_start + else: + # This mask will ensure that the rules will be applied in N-N + # like 40:50 -> 60:70, the port 40 will be mapped to 60, + # the 41 to 61, 42 to 62...50 to 70. + # In the case of 40:60 -> 70:80, the ports will be rounded + # so the port 41 and 51, will be mapped to 71. + internal_port_one_to_one_mask = '/' + external_start + if is_single_internal_port: + internal_port = internal_start + else: + internal_port = internal_port + internal_port_one_to_one_mask + + are_ranges_different = (int(internal_end) - int(internal_start)) - ( + int(external_end) - int(external_start)) + if are_ranges_different and not ( + is_single_internal_port or is_single_external_port): + LOG.warning("Port forwarding rule with different internal " + "and external port ranges applied. Internal " + "port range: [%s], external port range: [%s].", + internal_port, external_port) + return external_port, internal_port + def _rule_apply(self, iptables_manager, port_forwarding, rule_tag): iptables_manager.ipv4['nat'].clear_rules_by_tag(rule_tag) if DEFAULT_PORT_FORWARDING_CHAIN not in iptables_manager.ipv4[ diff --git a/neutron/common/ovn/extensions.py b/neutron/common/ovn/extensions.py index 761f4326ef4..933215039b7 100644 --- a/neutron/common/ovn/extensions.py +++ b/neutron/common/ovn/extensions.py @@ -27,6 +27,7 @@ from neutron_lib.api.definitions import extra_dhcp_opt from neutron_lib.api.definitions import extraroute from neutron_lib.api.definitions import filter_validation from neutron_lib.api.definitions import fip_pf_description +from neutron_lib.api.definitions import fip_pf_port_range from neutron_lib.api.definitions import fip_port_details from neutron_lib.api.definitions import floating_ip_port_forwarding from neutron_lib.api.definitions import floatingip_pools @@ -146,6 +147,7 @@ ML2_SUPPORTED_API_EXTENSIONS = [ seg_def.ALIAS, expose_port_forwarding_in_fip.ALIAS, fip_pf_description.ALIAS, + fip_pf_port_range.ALIAS, floating_ip_port_forwarding.ALIAS, vlantransparent.ALIAS, logging.ALIAS, diff --git a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD index 83abe15e953..cbb98dbfa41 100644 --- a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -34cf8b009713 +I43e0b669096 diff --git a/neutron/db/migration/alembic_migrations/versions/zed/expand/I43e0b669096_port_forwarding_port_ranges.py b/neutron/db/migration/alembic_migrations/versions/zed/expand/I43e0b669096_port_forwarding_port_ranges.py new file mode 100644 index 00000000000..aeecc012217 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/zed/expand/I43e0b669096_port_forwarding_port_ranges.py @@ -0,0 +1,154 @@ +# Copyright 2021 OpenStack Foundation +# +# 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 alembic import op +from neutron.db import migration +from neutron_lib.db import constants + +import sqlalchemy as sa + + +"""port forwarding rule description +Revision ID: I43e0b669096 +Revises: 34cf8b009713 +Create Date: 2021-12-02 10:00:00.000000 + +""" + +# revision identifiers, used by Alembic. +revision = 'I43e0b669096' +down_revision = '34cf8b009713' + +PF_TABLE_NAME = 'portforwardings' +pf_table = sa.Table( + PF_TABLE_NAME, sa.MetaData(), + sa.Column('id', sa.String(length=constants.UUID_FIELD_SIZE), + nullable=False), + sa.Column('socket', sa.String(length=36), nullable=False), + sa.Column('external_port', sa.Integer(), nullable=False), + sa.Column('internal_ip_address', sa.String(length=64), nullable=False), + sa.Column('internal_port_start', sa.Integer(), nullable=False), + sa.Column('external_port_start', sa.Integer(), nullable=False), + sa.Column('internal_port_end', sa.Integer(), nullable=False), + sa.Column('external_port_end', sa.Integer(), nullable=False), + sa.Column('internal_neutron_port_id', sa.String(constants.UUID_FIELD_SIZE), + nullable=False), +) + + +def upgrade(): + op.add_column(PF_TABLE_NAME, + sa.Column('internal_ip_address', sa.String(length=64), + nullable=False)) + op.add_column(PF_TABLE_NAME, sa.Column('internal_port_start', sa.Integer(), + nullable=False)) + op.add_column(PF_TABLE_NAME, sa.Column('internal_port_end', sa.Integer(), + nullable=False)) + op.add_column(PF_TABLE_NAME, sa.Column('external_port_start', sa.Integer(), + nullable=False)) + op.add_column(PF_TABLE_NAME, sa.Column('external_port_end', sa.Integer(), + nullable=False)) + + foreign_keys = clear_constraints_and_foreign() + migrate_values() + op.create_unique_constraint( + columns=['floatingip_id', 'protocol', + 'external_port_start', 'external_port_end'], + constraint_name='uniq_port_forwardings0floatingip_id0protocol0' + 'external_ports', + table_name=PF_TABLE_NAME) + + op.create_unique_constraint( + columns=['protocol', 'internal_neutron_port_id', 'internal_ip_address', + 'internal_port_start', 'internal_port_end'], + constraint_name='uniq_port_forwardings0ptcl0in_prt_id0in_ip_addr0' + 'in_prts', + table_name=PF_TABLE_NAME) + + op.drop_column(PF_TABLE_NAME, 'socket') + + op.drop_column(PF_TABLE_NAME, 'external_port') + + migration.create_foreign_keys(PF_TABLE_NAME, foreign_keys) + + +def clear_constraints_and_foreign(): + inspect = sa.engine.reflection.Inspector.from_engine(op.get_bind()) + foreign_keys = inspect.get_foreign_keys(PF_TABLE_NAME) + migration.remove_foreign_keys(PF_TABLE_NAME, + foreign_keys) + constraints_name = [ + 'uniq_port_forwardings0internal_neutron_port_id0socket0protocol', + 'uniq_port_forwardings0floatingip_id0external_port0protocol'] + for constraint_name in constraints_name: + op.drop_constraint( + constraint_name=constraint_name, + table_name=PF_TABLE_NAME, + type_='unique' + ) + + return foreign_keys + + +def migrate_values(): + session = sa.orm.Session(bind=op.get_bind()) + values = [] + for row in session.query(pf_table): + values.append({'id': row[0], + 'socket': row[1], + 'external_port': row[2]}) + + with session.begin(subtransactions=True): + for value in values: + internal_ip_address, internal_port = str( + value['socket']).split(':') + external_port = value['external_port'] + internal_port = int(internal_port) + session.execute( + pf_table.update().values( + internal_port_start=internal_port, + internal_port_end=internal_port, + external_port_start=external_port, + external_port_end=external_port, + internal_ip_address=internal_ip_address).where( + pf_table.c.id == value['id'])) + session.commit() + + +def expand_drop_exceptions(): + """Drop and replace the unique constraints for table portforwardings + + Drop the existing portforwardings foreign key uniq constraints and then + replace them with new unique constraints with column ``protocol``. + This is needed to use drop in expand migration to pass test_branches. + """ + + return { + sa.Column: [ + '%s.socket' % PF_TABLE_NAME, + '%s.external_port' % PF_TABLE_NAME + ], + sa.Constraint: [ + "portforwardings_ibfk_1", + "portforwardings_ibfk_2", + "portforwardings_ibfk_3", + "portforwardings_ibfk_4", + "uniq_port_forwardings0floatingip_id0external_port0protocol", + "uniq_port_forwardings0internal_neutron_port_id0socket0protocol", + "portforwardings_floatingip_id_fkey", + "portforwardings_internal_neutron_port_id_fkey", + "portforwardings_standard_attr_id_fkey" + ] + } diff --git a/neutron/db/models/port_forwarding.py b/neutron/db/models/port_forwarding.py index 8c2f509682d..6708b20c73b 100644 --- a/neutron/db/models/port_forwarding.py +++ b/neutron/db/models/port_forwarding.py @@ -21,6 +21,7 @@ from sqlalchemy import orm from neutron.db.models import l3 from neutron.db import models_v2 from neutron_lib.api.definitions import fip_pf_description as apidef +from neutron_lib.api.definitions import fip_pf_port_range as range_apidef from neutron_lib.db import constants as db_const @@ -28,28 +29,34 @@ class PortForwarding(standard_attr.HasStandardAttributes, model_base.BASEV2, model_base.HasId): __table_args__ = ( - sa.UniqueConstraint('floatingip_id', 'external_port', 'protocol', + sa.UniqueConstraint('floatingip_id', 'protocol', + 'external_port_start', 'external_port_end', name='uniq_port_forwardings0floatingip_id0' - 'external_port0protocol'), - sa.UniqueConstraint('internal_neutron_port_id', 'socket', 'protocol', - name='uniq_port_forwardings0' - 'internal_neutron_port_id0socket0' - 'protocol') + 'protocol0external_ports'), + sa.UniqueConstraint('protocol', 'internal_neutron_port_id', + 'internal_ip_address', 'internal_port_start', + 'internal_port_end', + name='uniq_port_forwardings0ptcl0in_prt_id0' + 'in_ip_addr0in_prts') ) floatingip_id = sa.Column(sa.String(db_const.UUID_FIELD_SIZE), sa.ForeignKey('floatingips.id', ondelete="CASCADE"), nullable=False) - external_port = sa.Column(sa.Integer, nullable=False) internal_neutron_port_id = sa.Column( sa.String(db_const.UUID_FIELD_SIZE), sa.ForeignKey('ports.id', ondelete="CASCADE"), nullable=False) protocol = sa.Column(sa.String(40), nullable=False) - socket = sa.Column(sa.String(36), nullable=False) + internal_ip_address = sa.Column(sa.String(64), nullable=False) + internal_port_start = sa.Column(sa.Integer, nullable=False) + external_port_start = sa.Column(sa.Integer, nullable=False) + internal_port_end = sa.Column(sa.Integer, nullable=False) + external_port_end = sa.Column(sa.Integer, nullable=False) port = orm.relationship( models_v2.Port, load_on_pending=True, + foreign_keys=internal_neutron_port_id, backref=orm.backref("port_forwardings", lazy='subquery', uselist=True, cascade='delete') @@ -61,4 +68,4 @@ class PortForwarding(standard_attr.HasStandardAttributes, cascade='delete') ) revises_on_change = ('floating_ip', 'port',) - api_collections = [apidef.ALIAS] + api_collections = [apidef.ALIAS, range_apidef.ALIAS] diff --git a/neutron/extensions/fip_pf_port_range.py b/neutron/extensions/fip_pf_port_range.py new file mode 100644 index 00000000000..919dc8fdbcb --- /dev/null +++ b/neutron/extensions/fip_pf_port_range.py @@ -0,0 +1,20 @@ +# +# 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.definitions import fip_pf_port_range as apidef +from neutron_lib.api import extensions + + +class Fip_pf_port_range(extensions.APIExtensionDescriptor): + + api_definition = apidef diff --git a/neutron/objects/port_forwarding.py b/neutron/objects/port_forwarding.py index 864e69b41f4..959160c96a1 100644 --- a/neutron/objects/port_forwarding.py +++ b/neutron/objects/port_forwarding.py @@ -32,7 +32,8 @@ class PortForwarding(base.NeutronDbObject): # Version 1.0: Initial version # Version 1.1: Change unique constraint # Version 1.2: Add "description" field - VERSION = '1.2' + # Version 1.3: Add "external_port_range" and "internal_port_range" fields + VERSION = '1.3' db_model = models.PortForwarding @@ -40,27 +41,20 @@ class PortForwarding(base.NeutronDbObject): foreign_keys = {'FloatingIP': {'floatingip_id': 'id'}, 'Port': {'internal_port_id': 'id'}} - # Notes: 'socket': 'socket' maybe odd here, but for current OVO and the - # definition of PortForwarding obj, this obj doesn't define a field named - # "socket", but the db model does, it will get the value to store into db. - # And this obj defines some fields like "internal_ip_address" and - # "internal_port" which will construct "socket" field. Also there is - # a reason why it like this. Please see neutron/objects/base.py#n468 - # So if we don't set it into fields_need_translation, the OVO base will - # default skip the field from db. fields_need_translation = { - 'socket': 'socket', 'internal_port_id': 'internal_neutron_port_id' } fields = { 'id': common_types.UUIDField(), 'floatingip_id': common_types.UUIDField(nullable=False), - 'external_port': common_types.PortRangeField(nullable=False), + 'external_port': common_types.PortRangeField(nullable=True), + 'external_port_range': common_types.PortRangesField(nullable=True), 'protocol': common_types.IpProtocolEnumField(nullable=False), 'internal_port_id': common_types.UUIDField(nullable=False), 'internal_ip_address': obj_fields.IPV4AddressField(), - 'internal_port': common_types.PortRangeField(nullable=False), + 'internal_port': common_types.PortRangeField(nullable=True), + 'internal_port_range': common_types.PortRangesField(nullable=True), 'floating_ip_address': obj_fields.IPV4AddressField(), 'router_id': common_types.UUIDField(), 'description': obj_fields.StringField() @@ -85,6 +79,49 @@ class PortForwarding(base.NeutronDbObject): return False return True + def _new_instance(self, **kwargs): + fields_parameters = {f: getattr(self, f) + for f in self.fields if hasattr(self, f)} + sanitized_kwargs = {k: kwargs[k] + for k in kwargs if k in self.fields} + fields_parameters.update(sanitized_kwargs) + return PortForwarding(**fields_parameters) + + def unroll_port_ranges(self): + extrn_port_range = self.external_port_range + intrn_port_range = self.internal_port_range + if not extrn_port_range: + return [self] + + if ':' not in extrn_port_range: + return [self._new_instance( + external_port=int(extrn_port_range), + internal_port=self.internal_port or int(intrn_port_range), + external_port_range=None, + internal_port_range=None + )] + + if ":" not in intrn_port_range: + intrn_port_range = "%s:%s" % (intrn_port_range, intrn_port_range) + + extrn_min, extrn_max = map(int, extrn_port_range.split(':')) + intrn_min, intrn_max = map(int, intrn_port_range.split(':')) + external_ports = list(range(extrn_min, extrn_max + 1)) + internal_ports = list(range(intrn_min, intrn_max + 1)) + intrn_multiplier = 1 if intrn_min != intrn_max else 0 + portforwardings = [] + for i, external_port in enumerate(external_ports): + internal_port = internal_ports[i * intrn_multiplier] + portforwardings.append( + self._new_instance( + external_port=external_port, + internal_port=internal_port, + external_port_range=None, + internal_port_range=None + ), + ) + return portforwardings + def obj_load_attr(self, attrname): if attrname in ['floating_ip_address', 'router_id']: return self._load_attr_from_fip(attrname) @@ -104,27 +141,127 @@ class PortForwarding(base.NeutronDbObject): _target_version = versionutils.convert_version_to_tuple(target_version) if _target_version < (1, 2): primitive.pop('description', None) + if _target_version < (1, 3): + primitive['internal_port'] = int( + str(primitive.pop( + 'internal_port_range', + str(primitive.get('internal_port', '')))).split(':')[0]) + primitive['external_port'] = int( + str(primitive.pop( + 'external_port_range', + str(primitive.get('external_port', '')))).split(':')[0]) + + @staticmethod + def _modify_single_ports_to_db(result): + internal_port = result.pop('internal_port', None) + external_port = result.pop('external_port', None) + if internal_port: + result['internal_port_start'] = internal_port + result['internal_port_end'] = internal_port + + if external_port: + result['external_port_start'] = external_port + result['external_port_end'] = external_port + + @staticmethod + def _modify_ports_range_to_db(result): + internal_port_range = result.pop('internal_port_range', None) + external_port_range = result.pop('external_port_range', None) + if internal_port_range: + if isinstance(internal_port_range, list): + internal_port_range = internal_port_range[0] + if isinstance(internal_port_range, + int) or internal_port_range.isnumeric(): + start = end = str(internal_port_range) + + else: + start, end = internal_port_range.split(':') + + result['internal_port_start'] = start + result['internal_port_end'] = end + + if external_port_range: + if isinstance(external_port_range, list): + external_port_range = external_port_range[0] + if isinstance(external_port_range, + int) or external_port_range.isnumeric(): + start = end = str(external_port_range) + + else: + start, end = external_port_range.split(':') + + result['external_port_start'] = start + result['external_port_end'] = end + + @staticmethod + def _modify_ports_range_from_db(result, + internal_port_start=None, + internal_port_end=None, + external_port_start=None, + external_port_end=None): + + if not internal_port_start or not external_port_start: + return + + result['external_port_range'] = '%s:%s' % (external_port_start, + external_port_end) + result['internal_port_range'] = '%s:%s' % (internal_port_start, + internal_port_end) + + @staticmethod + def _modify_single_ports_from_db(result, + internal_port_start=None, + internal_port_end=None, + external_port_start=None, + external_port_end=None): + + if not internal_port_start or not external_port_start: + return + if internal_port_start == internal_port_end: + result['internal_port'] = int(internal_port_start) + + if external_port_start == external_port_end: + result['external_port'] = int(external_port_start) @classmethod def modify_fields_from_db(cls, db_obj): result = super(PortForwarding, cls).modify_fields_from_db(db_obj) - if 'socket' in result: - groups = result['socket'].split(":") + if 'internal_ip_address' in result: result['internal_ip_address'] = netaddr.IPAddress( - groups[0], version=lib_const.IP_VERSION_4) - result['internal_port'] = int(groups[1]) - del result['socket'] + result['internal_ip_address'], version=lib_const.IP_VERSION_4) + + external_port_start = db_obj.get('external_port_start') + external_port_end = db_obj.get('external_port_end') + internal_port_start = db_obj.get('internal_port_start') + internal_port_end = db_obj.get('internal_port_end') + + cls._modify_single_ports_from_db( + result, + internal_port_start=internal_port_start, + external_port_start=external_port_start, + internal_port_end=internal_port_end, + external_port_end=external_port_end) + cls._modify_ports_range_from_db( + result, + internal_port_start=internal_port_start, + external_port_start=external_port_start, + internal_port_end=internal_port_end, + external_port_end=external_port_end) return result @classmethod def modify_fields_to_db(cls, fields): result = super(PortForwarding, cls).modify_fields_to_db(fields) - if 'internal_ip_address' in result and 'internal_port' in result: - result['socket'] = str( - result['internal_ip_address']) + ":" + str( - result['internal_port']) - del result['internal_ip_address'] - del result['internal_port'] + cls._modify_ports_range_to_db(result) + cls._modify_single_ports_to_db(result) + if 'internal_ip_address' in result: + if isinstance(result['internal_ip_address'], list): + result['internal_ip_address'] = list( + map(str, result['internal_ip_address'])) + else: + result['internal_ip_address'] = str( + result['internal_ip_address']) + return result @classmethod diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_db_sync.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_db_sync.py index f8edb14ef5a..71d1f9b2426 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_db_sync.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_db_sync.py @@ -31,6 +31,7 @@ from neutron.common.ovn import constants as ovn_const from neutron.common.ovn import utils from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf from neutron import manager +from neutron.objects.port_forwarding import PortForwarding from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb.extensions import qos \ as ovn_qos from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import ovn_client @@ -369,6 +370,11 @@ class OvnNbSynchronizer(OvnDbSynchronizer): return to_add, to_remove + def _unroll_port_forwarding(self, db_pf): + pf = PortForwarding(**db_pf) + pfs = pf.unroll_port_ranges() + return [p.to_dict() for p in pfs] + def _calculate_fip_pfs_differences(self, ovn_rtr_lb_pfs, db_pfs): to_add_or_update = set() to_remove = [] @@ -378,20 +384,21 @@ class OvnNbSynchronizer(OvnDbSynchronizer): # a set for each protocol and then comparing it with ovn_pfs db_mapped_pfs = {} for db_pf in db_pfs: - fip_id = db_pf.get('floatingip_id') - protocol = self.l3_plugin.port_forwarding.ovn_lb_protocol( - db_pf.get('protocol')) - db_vip = "{}:{} {}:{}".format( - db_pf.get('floating_ip_address'), db_pf.get('external_port'), - db_pf.get('internal_ip_address'), db_pf.get('internal_port')) + for pf in self._unroll_port_forwarding(db_pf): + fip_id = pf.get('floatingip_id') + protocol = self.l3_plugin.port_forwarding.ovn_lb_protocol( + pf.get('protocol')) + db_vip = "{}:{} {}:{}".format( + pf.get('floating_ip_address'), pf.get('external_port'), + pf.get('internal_ip_address'), pf.get('internal_port')) - fip_dict = db_mapped_pfs.get(fip_id, {}) - fip_dict_proto = fip_dict.get(protocol, set()) - fip_dict_proto.add(db_vip) - if protocol not in fip_dict: - fip_dict[protocol] = fip_dict_proto - if fip_id not in db_mapped_pfs: - db_mapped_pfs[fip_id] = fip_dict + fip_dict = db_mapped_pfs.get(fip_id, {}) + fip_dict_proto = fip_dict.get(protocol, set()) + fip_dict_proto.add(db_vip) + if protocol not in fip_dict: + fip_dict[protocol] = fip_dict_proto + if fip_id not in db_mapped_pfs: + db_mapped_pfs[fip_id] = fip_dict for fip_id in db_mapped_pfs: ovn_pfs_fip_id = ovn_pfs.get(fip_id, {}) # check for cases when ovn has lbs for protocols that are not in diff --git a/neutron/services/portforwarding/drivers/ovn/driver.py b/neutron/services/portforwarding/drivers/ovn/driver.py index 349634c23ba..53be0c5ddc8 100644 --- a/neutron/services/portforwarding/drivers/ovn/driver.py +++ b/neutron/services/portforwarding/drivers/ovn/driver.py @@ -35,9 +35,11 @@ class OVNPortForwardingHandler(object): return pf_const.LB_PROTOCOL_MAP[pf_obj.protocol] @staticmethod - def lb_name(fip_id, proto): - return "{}-{}-{}".format( - pf_const.PORT_FORWARDING_PREFIX, fip_id, proto) + def lb_name(fip_id, proto, external_port=''): + if external_port: + external_port = '-%s' % external_port + return "{}-{}-{}{}".format( + pf_const.PORT_FORWARDING_PREFIX, fip_id, proto, external_port) @classmethod def lb_names(cls, fip_id): @@ -45,19 +47,23 @@ class OVNPortForwardingHandler(object): for proto in pf_const.LB_PROTOCOL_MAP.values()] @classmethod - def _get_lb_attributes(cls, pf_obj): + def _get_lb_attributes(cls, pf_obj, is_range=False): + external_port = pf_obj.external_port if is_range else '' lb_name = cls.lb_name(pf_obj.floatingip_id, - cls._get_lb_protocol(pf_obj)) + cls._get_lb_protocol(pf_obj), + external_port) vip = "{}:{}".format(pf_obj.floating_ip_address, pf_obj.external_port) internal_ip = "{}:{}".format(pf_obj.internal_ip_address, pf_obj.internal_port) rtr_name = 'neutron-{}'.format(pf_obj.router_id) return lb_name, vip, [internal_ip], rtr_name - def _port_forwarding_created(self, ovn_txn, nb_ovn, pf_obj): + def _port_forwarding_created(self, ovn_txn, nb_ovn, pf_obj, + is_range=False): # Add vip to its corresponding load balancer. There can be multiple # vips, so load balancer may already be present. - lb_name, vip, internal_ips, rtr_name = self._get_lb_attributes(pf_obj) + lb_name, vip, internal_ips, rtr_name = self._get_lb_attributes( + pf_obj, is_range=is_range) external_ids = { ovn_const.OVN_DEVICE_OWNER_EXT_ID_KEY: pf_const.PORT_FORWARDING_PLUGIN, @@ -72,21 +78,34 @@ class OVNPortForwardingHandler(object): ovn_txn.add(nb_ovn.lr_lb_add(rtr_name, lb_name, may_exist=True)) def port_forwarding_created(self, ovn_txn, nb_ovn, pf_obj): - LOG.info("CREATE for port-forwarding %s vip %s:%s to %s:%s", - pf_obj.protocol, - pf_obj.floating_ip_address, pf_obj.external_port, - pf_obj.internal_ip_address, pf_obj.internal_port) - self._port_forwarding_created(ovn_txn, nb_ovn, pf_obj) + pf_objs = pf_obj.unroll_port_ranges() + is_range = len(pf_objs) > 1 + for pf_obj in pf_objs: + LOG.info("CREATE for port-forwarding %s vip %s:%s to %s:%s", + pf_obj.protocol, + pf_obj.floating_ip_address, pf_obj.external_port, + pf_obj.internal_ip_address, pf_obj.internal_port) + self._port_forwarding_created(ovn_txn, nb_ovn, pf_obj, + is_range=is_range) def port_forwarding_updated(self, ovn_txn, nb_ovn, pf_obj, orig_pf_obj): - LOG.info("UPDATE for port-forwarding %s vip %s:%s to %s:%s", - pf_obj.protocol, - pf_obj.floating_ip_address, pf_obj.external_port, - pf_obj.internal_ip_address, pf_obj.internal_port) - self._port_forwarding_deleted(ovn_txn, nb_ovn, orig_pf_obj) - self._port_forwarding_created(ovn_txn, nb_ovn, pf_obj) + orig_pf_objs = orig_pf_obj.unroll_port_ranges() + is_range = len(orig_pf_objs) > 1 + for orig_pf_obj in orig_pf_objs: + self._port_forwarding_deleted(ovn_txn, nb_ovn, orig_pf_obj, + is_range=is_range) + pf_objs = pf_obj.unroll_port_ranges() + is_range = len(pf_objs) > 1 + for pf_obj in pf_objs: + LOG.info("UPDATE for port-forwarding %s vip %s:%s to %s:%s", + pf_obj.protocol, + pf_obj.floating_ip_address, pf_obj.external_port, + pf_obj.internal_ip_address, pf_obj.internal_port) + self._port_forwarding_created(ovn_txn, nb_ovn, pf_obj, + is_range=is_range) - def _port_forwarding_deleted(self, ovn_txn, nb_ovn, pf_obj): + def _port_forwarding_deleted(self, ovn_txn, nb_ovn, pf_obj, + is_range=False): # NOTE: load balancer instance is expected to be removed by api once # last vip is removed. # Since router has weak ref to the lb, that gets taken care @@ -97,15 +116,23 @@ class OVNPortForwardingHandler(object): # TODO(flaviof): see about enhancing lb_del so that removal of lb # can optionally take a logical router, which explicitly dissociates # router from removed lb. - lb_name, vip, _internal_ips, _rtr = self._get_lb_attributes(pf_obj) - ovn_txn.add(nb_ovn.lb_del(lb_name, vip, if_exists=True)) + pf_objs = pf_obj.unroll_port_ranges() + is_range = is_range or len(pf_objs) > 1 + for pf_obj in pf_objs: + lb_name, vip, _internal_ips, _rtr = self._get_lb_attributes( + pf_obj, is_range=is_range) + ovn_txn.add(nb_ovn.lb_del(lb_name, vip, if_exists=True)) def port_forwarding_deleted(self, ovn_txn, nb_ovn, pf_obj): - LOG.info("DELETE for port-forwarding %s vip %s:%s to %s:%s", - pf_obj.protocol, - pf_obj.floating_ip_address, pf_obj.external_port, - pf_obj.internal_ip_address, pf_obj.internal_port) - self._port_forwarding_deleted(ovn_txn, nb_ovn, pf_obj) + pf_objs = pf_obj.unroll_port_ranges() + is_range = len(pf_objs) > 1 + for pf_obj in pf_objs: + LOG.info("DELETE for port-forwarding %s vip %s:%s to %s:%s", + pf_obj.protocol, + pf_obj.floating_ip_address, pf_obj.external_port, + pf_obj.internal_ip_address, pf_obj.internal_port) + self._port_forwarding_deleted(ovn_txn, nb_ovn, pf_obj, + is_range=is_range) @registry.has_registry_receivers diff --git a/neutron/services/portforwarding/pf_plugin.py b/neutron/services/portforwarding/pf_plugin.py index dd55ccea95c..9e3ff33825e 100644 --- a/neutron/services/portforwarding/pf_plugin.py +++ b/neutron/services/portforwarding/pf_plugin.py @@ -19,6 +19,7 @@ import copy import netaddr from neutron_lib.api.definitions import expose_port_forwarding_in_fip from neutron_lib.api.definitions import fip_pf_description +from neutron_lib.api.definitions import fip_pf_port_range from neutron_lib.api.definitions import floating_ip_port_forwarding as apidef from neutron_lib.api.definitions import l3 from neutron_lib.callbacks import events @@ -90,7 +91,8 @@ class PortForwardingPlugin(fip_pf.PortForwardingPluginBase): supported_extension_aliases = [apidef.ALIAS, expose_port_forwarding_in_fip.ALIAS, - fip_pf_description.ALIAS] + fip_pf_description.ALIAS, + fip_pf_port_range.ALIAS] __native_pagination_support = True __native_sorting_support = True @@ -352,6 +354,7 @@ class PortForwardingPlugin(fip_pf.PortForwardingPluginBase): port_forwarding = port_forwarding.get(apidef.RESOURCE_NAME) port_forwarding['floatingip_id'] = floatingip_id + self._check_port_collisions(context, floatingip_id, port_forwarding) self._check_port_has_binding_floating_ip(context, port_forwarding) with db_api.CONTEXT_WRITER.using(context): fip_obj = self._get_fip_obj(context, floatingip_id) @@ -438,6 +441,17 @@ class PortForwardingPlugin(fip_pf.PortForwardingPluginBase): }) pf_obj.update_fields(port_forwarding, reset_changes=True) self._check_port_forwarding_update(context, pf_obj) + + port_changed_keys = ['internal_port', 'internal_port_range', + 'external_port', 'external_port_range'] + + if [k for k in port_changed_keys if k in port_forwarding]: + self._check_port_collisions( + context, floatingip_id, port_forwarding, + id, pf_obj.get('internal_port_id'), + pf_obj.get('protocol'), + pf_obj.get('internal_ip_address')) + pf_obj.update() except oslo_db_exc.DBDuplicateEntry: (__, conflict_params) = self._find_existing_port_forwarding( @@ -456,6 +470,103 @@ class PortForwardingPlugin(fip_pf.PortForwardingPluginBase): states=(original_pf_obj, pf_obj))) return pf_obj + def _check_collision(self, pf_objs, port, port_key, id): + for port_forwarding_registry in pf_objs: + if id == port_forwarding_registry['id']: + continue + + existing_port = port_forwarding_registry.get(port_key) + err_msg = _("There is a port collision with the %s. The " + "following ranges collides: %s and %s") + + if self._range_collides(existing_port, port): + raise lib_exc.BadRequest(resource=apidef.RESOURCE_NAME, + msg=err_msg % ( + port_key, + existing_port, + port)) + + def _check_port_collisions(self, context, floatingip_id, pf_dict, + id=None, internal_port_id=None, + protocol=None, internal_ip_address=None): + external_range_pf_dict = pf_dict.get('external_port_range') + if not external_range_pf_dict and 'external_port' in pf_dict: + external_range_pf_dict = '%(port)s:%(port)s' % { + 'port': pf_dict.get('external_port')} + + internal_range_pf_dict = pf_dict.get('internal_port_range') + if not internal_range_pf_dict and 'internal_port' in pf_dict: + internal_range_pf_dict = '%(port)s:%(port)s' % { + 'port': pf_dict.get('internal_port')} + + internal_port_id = pf_dict.get('internal_port_id') or internal_port_id + protocol = pf_dict.get('protocol') or protocol + internal_ip_address = pf_dict.get( + 'internal_ip_address') or internal_ip_address + self._validate_ranges( + internal_port_range=internal_range_pf_dict, + external_port_range=external_range_pf_dict) + + if internal_range_pf_dict: + pf_same_internal_port = pf.PortForwarding.get_objects( + context, internal_port_id=internal_port_id, + protocol=protocol, internal_ip_address=internal_ip_address) + self._check_collision(pf_same_internal_port, + internal_range_pf_dict, + 'internal_port_range', + id) + + if external_range_pf_dict: + pf_same_fips = pf.PortForwarding.get_objects( + context, floatingip_id=floatingip_id, + protocol=protocol) + self._check_collision(pf_same_fips, + external_range_pf_dict, + 'external_port_range', + id) + + def _validate_ranges(self, internal_port_range=None, + external_port_range=None): + err_invalid_relation = _( + "Invalid ranges of internal and/or external ports. " + "The relation between internal and external ports " + "must be N-N or 1-N") + + internal_dif = self._get_port_range(internal_port_range) + external_dif = self._get_port_range(external_port_range) + + if internal_dif > 0 and internal_dif != external_dif: + raise lib_exc.BadRequest(resource=apidef.RESOURCE_NAME, + msg=err_invalid_relation) + + def _get_port_range(self, port_range=None): + if not port_range: + return 0 + + if ':' in port_range: + initial_port, final_port = list(map(int, + str(port_range).split(':'))) + return final_port - initial_port + + return 0 + + def _range_collides(self, range_a, range_b): + range_a = list(map(int, str(range_a).split(':'))) + range_b = list(map(int, str(range_b).split(':'))) + + invalid_port = next((port for port in (range_a + range_b) + if 65535 < port or port < 1), None) + + if invalid_port: + raise lib_exc.BadRequest(resource=apidef.RESOURCE_NAME, + msg="Invalid port value, the " + "port value must be " + "a value between 1 and 65535.") + + initial_intersection = max(range_a[0], range_b[0]) + final_intersection = min(range_a[-1], range_b[-1]) + return initial_intersection <= final_intersection + def _check_router_match(self, context, fip_obj, router_id, pf_dict): internal_port_id = pf_dict['internal_port_id'] if fip_obj.router_id and fip_obj.router_id != router_id: diff --git a/neutron/tests/functional/agent/l3/extensions/test_port_forwarding_extension.py b/neutron/tests/functional/agent/l3/extensions/test_port_forwarding_extension.py index 9cc216221aa..9e6f39e3c45 100644 --- a/neutron/tests/functional/agent/l3/extensions/test_port_forwarding_extension.py +++ b/neutron/tests/functional/agent/l3/extensions/test_port_forwarding_extension.py @@ -48,21 +48,29 @@ class L3AgentFipPortForwardingExtensionTestFramework( self.portforwarding1 = pf_obj.PortForwarding( context=None, id=_uuid(), floatingip_id=self.fip_id1, external_port=1111, protocol='tcp', internal_port_id=_uuid(), + external_port_range='1111:1111', + internal_port_range='11111:11111', internal_ip_address='1.1.1.1', internal_port=11111, floating_ip_address='111.111.111.111', router_id=_uuid()) self.portforwarding2 = pf_obj.PortForwarding( context=None, id=_uuid(), floatingip_id=self.fip_id1, external_port=1112, protocol='tcp', internal_port_id=_uuid(), + external_port_range='1112:1112', + internal_port_range='11112:11112', internal_ip_address='1.1.1.2', internal_port=11112, floating_ip_address='111.111.111.111', router_id=_uuid()) self.portforwarding3 = pf_obj.PortForwarding( context=None, id=_uuid(), floatingip_id=self.fip_id2, external_port=1113, protocol='tcp', internal_port_id=_uuid(), internal_ip_address='1.1.1.3', internal_port=11113, + external_port_range='1113:1113', + internal_port_range='11113:11113', floating_ip_address='111.222.111.222', router_id=_uuid()) self.portforwarding4 = pf_obj.PortForwarding( context=None, id=_uuid(), floatingip_id=self.fip_id3, external_port=2222, protocol='tcp', internal_port_id=_uuid(), + external_port_range='2222:2222', + internal_port_range='22222:22222', internal_ip_address='2.2.2.2', internal_port=22222, floating_ip_address='222.222.222.222', router_id=_uuid()) self.port_forwardings = [self.portforwarding1, self.portforwarding2, diff --git a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovn_db_sync.py b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovn_db_sync.py index 3b51aafa5f1..9d9c088a784 100644 --- a/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovn_db_sync.py +++ b/neutron/tests/functional/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovn_db_sync.py @@ -17,6 +17,7 @@ from collections import namedtuple import netaddr from neutron_lib.api.definitions import dns as dns_apidef from neutron_lib.api.definitions import fip_pf_description as ext_pf_def +from neutron_lib.api.definitions import fip_pf_port_range as ranges_pf_def from neutron_lib.api.definitions import floating_ip_port_forwarding as pf_def from neutron_lib.api.definitions import l3 from neutron_lib.api.definitions import port_security as ps @@ -465,8 +466,8 @@ class TestOvnNbSync(base.TestOVNFunctionalBase): p5_ip = n1_port_details_dict['p5']['fixed_ips'][0]['ip_address'] fip_pf_args = { - pf_def.EXTERNAL_PORT: 2222, - pf_def.INTERNAL_PORT: 22, + ranges_pf_def.EXTERNAL_PORT_RANGE: '2222:2223', + ranges_pf_def.INTERNAL_PORT_RANGE: '22:23', pf_def.INTERNAL_PORT_ID: n1_port_dict['p5'], pf_def.PROTOCOL: "tcp", ext_pf_def.DESCRIPTION_FIELD: 'PortFwd r1_f3_p5:22 tcp', @@ -476,8 +477,8 @@ class TestOvnNbSync(base.TestOVNFunctionalBase): self.context, r1_f3['id'], **fip_args) # Add port forwarding with same external and internal value - fip_pf_args[pf_def.EXTERNAL_PORT] = 80 - fip_pf_args[pf_def.INTERNAL_PORT] = 80 + fip_pf_args[ranges_pf_def.EXTERNAL_PORT_RANGE] = '80:81' + fip_pf_args[ranges_pf_def.INTERNAL_PORT_RANGE] = '80:81' fip_pf_args[ext_pf_def.DESCRIPTION_FIELD] = 'PortFwd r1_f3_p5:80 tcp' self.pf_plugin.create_floatingip_port_forwarding( self.context, r1_f3['id'], **fip_args) @@ -1420,14 +1421,37 @@ class TestOvnNbSync(base.TestOVNFunctionalBase): for fip in fips['floatingips']: for pf in self.pf_plugin.get_floatingip_port_forwardings( self.ctx, floatingip_id=fip['id']): + if pf.get('external_port'): + db_pfs.append(fip_pf_cmp( + fip['id'], + ovn_pf.ovn_lb_protocol(pf['protocol']), + utils.ovn_name(pf['router_id']), + pf['floating_ip_address'], + pf['external_port'], + pf['internal_ip_address'], + pf['internal_port'], + )) + continue + + extrn_1, extrn_2 = pf['external_port_range'].split(':') + intrn_1, intrn_2 = pf['internal_port_range'].split(':') db_pfs.append(fip_pf_cmp( fip['id'], ovn_pf.ovn_lb_protocol(pf['protocol']), utils.ovn_name(pf['router_id']), pf['floating_ip_address'], - pf['external_port'], + int(extrn_1), pf['internal_ip_address'], - pf['internal_port'], + int(intrn_1), + )) + db_pfs.append(fip_pf_cmp( + fip['id'], + ovn_pf.ovn_lb_protocol(pf['protocol']), + utils.ovn_name(pf['router_id']), + pf['floating_ip_address'], + int(extrn_2), + pf['internal_ip_address'], + int(intrn_2) )) nb_pfs = [] diff --git a/neutron/tests/functional/services/portforwarding/test_port_forwarding.py b/neutron/tests/functional/services/portforwarding/test_port_forwarding.py index 38c65a051e7..9881c1c4a08 100644 --- a/neutron/tests/functional/services/portforwarding/test_port_forwarding.py +++ b/neutron/tests/functional/services/portforwarding/test_port_forwarding.py @@ -13,6 +13,7 @@ from unittest import mock from neutron_lib.api.definitions import fip_pf_description as ext_apidef +from neutron_lib.api.definitions import fip_pf_port_range as ext_range_apidef from neutron_lib.api.definitions import floating_ip_port_forwarding as apidef from neutron_lib import exceptions as lib_exc from neutron_lib.exceptions import l3 as lib_l3_exc @@ -120,6 +121,8 @@ class PortForwardingTestCase(PortForwardingTestCaseBase): apidef.RESOURCE_NAME: {apidef.EXTERNAL_PORT: 2225, apidef.INTERNAL_PORT: 25, + ext_range_apidef.EXTERNAL_PORT_RANGE: '2225:2225', + ext_range_apidef.INTERNAL_PORT_RANGE: '25:25', apidef.INTERNAL_PORT_ID: self.port['id'], apidef.PROTOCOL: "tcp", ext_apidef.DESCRIPTION_FIELD: 'Some description', @@ -139,6 +142,8 @@ class PortForwardingTestCase(PortForwardingTestCaseBase): expect = { "external_port": 2225, "internal_port": 25, + "internal_port_range": '25:25', + "external_port_range": '2225:2225', "internal_port_id": self.port['id'], "protocol": "tcp", "internal_ip_address": self.port['fixed_ips'][0]['ip_address'], @@ -201,6 +206,8 @@ class PortForwardingTestCase(PortForwardingTestCaseBase): expect = { "external_port": 2225, "internal_port": 25, + "external_port_range": '2225:2225', + "internal_port_range": '25:25', "internal_port_id": self.port['id'], "protocol": "tcp", "internal_ip_address": self.port['fixed_ips'][0]['ip_address'], @@ -242,6 +249,8 @@ class PortForwardingTestCase(PortForwardingTestCaseBase): expect = { "external_port": 2226, "internal_port": 26, + "external_port_range": '2226:2226', + "internal_port_range": '26:26', "internal_port_id": self.port['id'], "protocol": "udp", "internal_ip_address": self.port['fixed_ips'][0]['ip_address'], @@ -269,7 +278,9 @@ class PortForwardingTestCase(PortForwardingTestCaseBase): self.context, res['id'], self.fip['id'], update_body) expect = { "external_port": 2227, + "external_port_range": '2227:2227', "internal_port": 27, + "internal_port_range": '27:27', "internal_port_id": new_port['id'], "protocol": "tcp", "internal_ip_address": new_port['fixed_ips'][0]['ip_address'], @@ -321,7 +332,10 @@ class PortForwardingTestCase(PortForwardingTestCaseBase): 'internal_port_id': new_port['id'], 'internal_ip_address': new_port['fixed_ips'][0]['ip_address'], 'external_port': self.port_forwarding[ - apidef.RESOURCE_NAME]['external_port'] + 1 + apidef.RESOURCE_NAME]['external_port'] + 1, + 'external_port_range': '%(port)s:%(port)s' % { + 'port': self.port_forwarding[ + apidef.RESOURCE_NAME]['external_port'] + 1} }) new_res = self.pf_plugin.create_floatingip_port_forwarding( self.context, self.fip['id'], self.port_forwarding) @@ -345,6 +359,7 @@ class PortForwardingTestCase(PortForwardingTestCaseBase): new_port = self._create_port(self.fmt, self.net['id']).json['port'] self.port_forwarding[apidef.RESOURCE_NAME].update({ 'external_port': 2226, + 'external_port_range': '2226:2226', 'internal_port_id': new_port['id'], 'internal_ip_address': new_port['fixed_ips'][0]['ip_address'] }) @@ -392,6 +407,8 @@ class PortForwardingTestCase(PortForwardingTestCaseBase): expected = { "external_port": 2225, "internal_port": 25, + "external_port_range": '2225:2225', + "internal_port_range": '25:25', "internal_port_id": self.port['id'], "protocol": "tcp", "internal_ip_address": self.port['fixed_ips'][0]['ip_address'], diff --git a/neutron/tests/tools.py b/neutron/tests/tools.py index 72b797cc53d..aca89a216b3 100644 --- a/neutron/tests/tools.py +++ b/neutron/tests/tools.py @@ -37,6 +37,8 @@ try: except ImportError: _CALLBACK_PRIORITY_SUPPORTED = False +LAST_RANDOM_PORT_RANGE_GENERATED = 1 + class SafeCleanupFixture(fixtures.Fixture): """Catch errors in daughter fixture cleanup.""" @@ -196,7 +198,10 @@ def get_random_prefixlen(version=4): def get_random_port(start=constants.PORT_RANGE_MIN): - return random.randint(start, constants.PORT_RANGE_MAX) + global LAST_RANDOM_PORT_RANGE_GENERATED + LAST_RANDOM_PORT_RANGE_GENERATED = random.randint( + start, constants.PORT_RANGE_MAX) + return LAST_RANDOM_PORT_RANGE_GENERATED def get_random_vlan(): diff --git a/neutron/tests/unit/agent/l3/extensions/test_port_forwarding.py b/neutron/tests/unit/agent/l3/extensions/test_port_forwarding.py index 5a262622d0e..e7399f1fe7e 100644 --- a/neutron/tests/unit/agent/l3/extensions/test_port_forwarding.py +++ b/neutron/tests/unit/agent/l3/extensions/test_port_forwarding.py @@ -60,6 +60,8 @@ class PortForwardingExtensionBaseTestCase( self.portforwarding1 = pf_obj.PortForwarding( context=None, id=_uuid(), floatingip_id=self.floatingip2.id, external_port=1111, protocol='tcp', internal_port_id=_uuid(), + external_port_range='1111:1111', + internal_port_range='11111:11111', internal_ip_address='1.1.1.1', internal_port=11111, floating_ip_address=self.floatingip2.floating_ip_address, router_id=self.floatingip2.router_id) @@ -148,15 +150,16 @@ class FipPortForwardingExtensionTestCase(PortForwardingExtensionBaseTestCase): rule_tag = 'fip_portforwarding-' + target_obj.id chain_name = ( 'pf-' + target_obj.id)[:lib_const.MAX_IPTABLES_CHAIN_LEN_WRAP] + ports = pf.PortForwardingAgentExtension.extract_ports(target_obj) chain_rule = (chain_name, '-d %s/32 -p %s -m %s --dport %s ' '-j DNAT --to-destination %s:%s' % ( target_obj.floating_ip_address, target_obj.protocol, target_obj.protocol, - target_obj.external_port, + ports[0], target_obj.internal_ip_address, - target_obj.internal_port)) + ports[1])) return chain_name, chain_rule, rule_tag def _assert_called_iptables_process(self, mock_add_chain, @@ -183,6 +186,69 @@ class FipPortForwardingExtensionTestCase(PortForwardingExtensionBaseTestCase): lib_const.FLOATINGIP_STATUS_ACTIVE} mock_send_fip_status.assert_called_once_with(mock.ANY, fip_status) + def _test_extract_ports(self, internal_port_range, external_port_range, + expected_internal_port, expected_external_port, + log_called=False): + mock_pf = mock.Mock() + mock_log = mock.patch.object(pf.LOG, 'warning').start() + mock_pf.internal_port_range = internal_port_range + mock_pf.external_port_range = external_port_range + ports = pf.PortForwardingAgentExtension.extract_ports(mock_pf) + self.assertEqual(expected_internal_port, ports[1]) + self.assertEqual(expected_external_port, ports[0]) + if log_called: + mock_log.assert_called_with( + "Port forwarding rule with different internal " + "and external port ranges applied. Internal " + "port range: [%s], external port range: [%s].", + ports[1], ports[0]) + else: + self.assertFalse(mock_log.called) + + def test_extract_ports(self): + test_cases = { + 'internal port range = external port range': { + 'internal': '10:12', + 'external': '13:15', + 'expected_internal': '10-12/13', + 'expected_external': '13:15', + }, + 'internal port range = external port range = 1': { + 'internal': '10:10', + 'external': '13:13', + 'expected_internal': '10', + 'expected_external': '13', + }, + 'internal port range < external port range': { + 'internal': '10:12', + 'external': '13:16', + 'expected_internal': '10-12/13', + 'expected_external': '13:16', + 'log_called': True + }, + 'internal port range > external port range': { + 'internal': '9:12', + 'external': '13:15', + 'expected_internal': '9-12/13', + 'expected_external': '13:15', + 'log_called': True + }, + 'internal port range = 1': { + 'internal': '10:10', + 'external': '13:15', + 'expected_internal': '10', + 'expected_external': '13:15', + }, + 'external port range = 1': { + 'internal': '10:12', + 'external': '13:13', + 'expected_internal': '10-12', + 'expected_external': '13', + } + } + for case in test_cases.values(): + self._test_extract_ports(*case.values()) + @mock.patch.object(pf.PortForwardingAgentExtension, '_sending_port_forwarding_fip_status') @mock.patch.object(iptables_manager.IptablesTable, 'add_rule') @@ -208,6 +274,8 @@ class FipPortForwardingExtensionTestCase(PortForwardingExtensionBaseTestCase): test_portforwarding = pf_obj.PortForwarding( context=None, id=_uuid(), floatingip_id=self.floatingip2.id, external_port=2222, protocol='tcp', internal_port_id=_uuid(), + external_port_range='2222:2222', + internal_port_range='22222:22222', internal_ip_address='2.2.2.2', internal_port=22222, floating_ip_address=self.floatingip2.floating_ip_address, router_id=self.floatingip2.router_id) @@ -229,8 +297,9 @@ class FipPortForwardingExtensionTestCase(PortForwardingExtensionBaseTestCase): update_portforwarding = pf_obj.PortForwarding( context=None, id=self.portforwarding1.id, floatingip_id=self.portforwarding1.floatingip_id, - external_port=2222, protocol='tcp', internal_port_id=_uuid(), - internal_ip_address='2.2.2.2', internal_port=22222, + external_port_range='2222:2223', protocol='tcp', + internal_port_id=_uuid(), internal_ip_address='2.2.2.2', + internal_port_range='22222:22223', floating_ip_address=self.portforwarding1.floating_ip_address, router_id=self.portforwarding1.router_id) self.port_forwardings = [update_portforwarding] diff --git a/neutron/tests/unit/extensions/test_floating_ip_port_forwarding.py b/neutron/tests/unit/extensions/test_floating_ip_port_forwarding.py index 594dde2432f..b4658a180b0 100644 --- a/neutron/tests/unit/extensions/test_floating_ip_port_forwarding.py +++ b/neutron/tests/unit/extensions/test_floating_ip_port_forwarding.py @@ -48,15 +48,23 @@ class FloatingIPPorForwardingTestCase(test_l3.L3BaseForIntTests, internal_ip_address, internal_port_id, tenant_id=None, - description=None): + description=None, + external_port_range=None, + internal_port_range=None): tenant_id = tenant_id or _uuid() data = {'port_forwarding': { - "external_port": external_port, - "internal_port": internal_port, "protocol": protocol, "internal_ip_address": internal_ip_address, "internal_port_id": internal_port_id} } + if external_port_range and internal_port_range: + data['port_forwarding'][ + 'internal_port_range'] = internal_port_range + data['port_forwarding'][ + 'external_port_range'] = external_port_range + else: + data['port_forwarding']['internal_port'] = internal_port + data['port_forwarding']['external_port'] = external_port if description: data['port_forwarding']['description'] = description @@ -152,6 +160,87 @@ class FloatingIPPorForwardingTestCase(test_l3.L3BaseForIntTests, self.assertEqual( "blablablabla", pf_body['port_forwarding']['description']) + def test_create_floatingip_port_forwarding_with_ranges(self): + internal_port_range = '22:24' + external_port_range = '2222:2224' + with self.network() as ext_net: + network_id = ext_net['network']['id'] + self._set_net_external(network_id) + with self.subnet(ext_net, cidr='10.10.10.0/24'), \ + self.router() as router, \ + self.subnet(cidr='11.0.0.0/24') as private_subnet, \ + self.port(private_subnet) as port: + self._add_external_gateway_to_router( + router['router']['id'], + network_id) + self._router_interface_action( + 'add', router['router']['id'], + private_subnet['subnet']['id'], + None) + fip = self._make_floatingip( + self.fmt, + network_id) + self.assertIsNone(fip['floatingip'].get('port_id')) + res = self._create_fip_port_forwarding( + self.fmt, fip['floatingip']['id'], + None, None, + 'tcp', + port['port']['fixed_ips'][0]['ip_address'], + port['port']['id'], + internal_port_range=internal_port_range, + external_port_range=external_port_range) + self.assertEqual(exc.HTTPCreated.code, res.status_int) + pf_body = self.deserialize(self.fmt, res) + self.assertEqual( + internal_port_range, + pf_body['port_forwarding']['internal_port_range']) + self.assertEqual( + external_port_range, + pf_body['port_forwarding']['external_port_range']) + + def test_create_floatingip_port_forwarding_with_ranges_port_collisions( + self): + internal_port_range1 = '22:24' + internal_port_range2 = '23:25' + external_port_range1 = '2222:2224' + external_port_range2 = '2223:2225' + with self.network() as ext_net: + network_id = ext_net['network']['id'] + self._set_net_external(network_id) + with self.subnet(ext_net, cidr='10.10.10.0/24'), \ + self.router() as router, \ + self.subnet(cidr='11.0.0.0/24') as private_subnet, \ + self.port(private_subnet) as port: + self._add_external_gateway_to_router( + router['router']['id'], + network_id) + self._router_interface_action( + 'add', router['router']['id'], + private_subnet['subnet']['id'], + None) + fip = self._make_floatingip( + self.fmt, + network_id) + self.assertIsNone(fip['floatingip'].get('port_id')) + self._create_fip_port_forwarding( + self.fmt, fip['floatingip']['id'], + None, None, + 'tcp', + port['port']['fixed_ips'][0]['ip_address'], + port['port']['id'], + internal_port_range=internal_port_range1, + external_port_range=external_port_range1) + response = self._create_fip_port_forwarding( + self.fmt, fip['floatingip']['id'], + None, None, + 'tcp', + port['port']['fixed_ips'][0]['ip_address'], + port['port']['id'], + internal_port_range=internal_port_range2, + external_port_range=external_port_range2) + self.assertEqual(exc.HTTPBadRequest.code, + response.status_int) + def test_update_floatingip_port_forwarding_with_dup_internal_port(self): with self.network() as ext_net: network_id = ext_net['network']['id'] diff --git a/neutron/tests/unit/objects/test_base.py b/neutron/tests/unit/objects/test_base.py index 40ddb3525ce..64d7852c495 100644 --- a/neutron/tests/unit/objects/test_base.py +++ b/neutron/tests/unit/objects/test_base.py @@ -505,6 +505,11 @@ def get_set_of_random_uuids(): } +def get_random_port_ranges(): + port = tools.LAST_RANDOM_PORT_RANGE_GENERATED + return "%d:%d" % (port, port) + + # NOTE: The keys in this dictionary have alphabetic order. FIELD_TYPE_VALUE_GENERATOR_MAP = { common_types.DictOfMiscValuesField: get_random_dict, @@ -552,7 +557,8 @@ FIELD_TYPE_VALUE_GENERATOR_MAP = { port_numa_affinity_policy.NumaAffinityPoliciesEnumField: tools.get_random_port_numa_affinity_policy, port_device_profile.PortDeviceProfile: - lambda: helpers.get_random_string(255) + lambda: helpers.get_random_string(255), + common_types.PortRangesField: get_random_port_ranges } diff --git a/neutron/tests/unit/objects/test_objects.py b/neutron/tests/unit/objects/test_objects.py index 48c5c31978b..d806729e4ee 100644 --- a/neutron/tests/unit/objects/test_objects.py +++ b/neutron/tests/unit/objects/test_objects.py @@ -80,7 +80,7 @@ object_data = { 'PortBindingLevel': '1.1-50d47f63218f87581b6cd9a62db574e5', 'PortDataPlaneStatus': '1.0-25be74bda46c749653a10357676c0ab2', 'PortDNS': '1.1-c5ca2dc172bdd5fafee3fc986d1d7023', - 'PortForwarding': '1.2-f772f03b82a616603c7f3d4497bf577f', + 'PortForwarding': '1.3-402b1fb5a754808b82a966c95f468113', 'PortSecurity': '1.0-b30802391a87945ee9c07582b4ff95e3', 'PortUplinkStatusPropagation': '1.1-f0a4ca451a941910376c33616dea5de2', 'ProviderResourceAssociation': '1.0-05ab2d5a3017e5ce9dd381328f285f34', diff --git a/neutron/tests/unit/objects/test_port_forwarding.py b/neutron/tests/unit/objects/test_port_forwarding.py index c98a1ecb9db..9ae0f1579ba 100644 --- a/neutron/tests/unit/objects/test_port_forwarding.py +++ b/neutron/tests/unit/objects/test_port_forwarding.py @@ -60,10 +60,12 @@ class PortForwardingObjectTestCase(obj_test_base.BaseObjectIfaceTestCase): if not fp_obj.db_obj: fp_db_attrs = { 'floatingip_id': fp_obj.floatingip_id, - 'external_port': fp_obj.external_port, + 'external_port_start': fp_obj.external_port, + 'external_port_end': fp_obj.external_port, 'internal_neutron_port_id': fp_obj.internal_port_id, 'protocol': fp_obj.protocol, - 'socket': fp_obj.internal_port, + 'internal_port_start': fp_obj.internal_port, + 'internal_port_end': fp_obj.internal_port, 'floating_ip': random_generate_fip_db(fp_obj.floatingip_id) } fp_obj._captured_db_model = ( @@ -134,10 +136,6 @@ class PortForwardingDbObjectTestCase(obj_test_base.BaseDbObjectTestCase, obj.create() self.assertIsNotNone(obj.db_obj) - # Make sure the created obj socket field is correct. - created_socket = obj.db_obj.socket.split(":") - self.assertEqual(created_socket[0], str(obj.internal_ip_address)) - self.assertEqual(created_socket[1], str(obj.internal_port)) fields_to_update = self.get_updatable_fields(self.obj_fields[1]) if fields_to_update: @@ -153,12 +151,6 @@ class PortForwardingDbObjectTestCase(obj_test_base.BaseDbObjectTestCase, setattr(obj, key, val) obj.update() self.assertIsNotNone(obj.db_obj) - # Make sure the updated obj socket field is correct. - updated_socket = obj.db_obj.socket.split(":") - self.assertEqual(updated_socket[0], - str(self.obj_fields[1]['internal_ip_address'])) - self.assertEqual(updated_socket[1], - str(self.obj_fields[1]['internal_port'])) # Then check all update fields had been updated. for k, v in obj.modify_fields_to_db(fields_to_update).items(): self.assertEqual(v, obj.db_obj[k], '%s attribute differs' % k) diff --git a/neutron/tests/unit/services/portforwarding/drivers/ovn/test_driver.py b/neutron/tests/unit/services/portforwarding/drivers/ovn/test_driver.py index aa2cb827fab..76f5c722335 100644 --- a/neutron/tests/unit/services/portforwarding/drivers/ovn/test_driver.py +++ b/neutron/tests/unit/services/portforwarding/drivers/ovn/test_driver.py @@ -15,6 +15,7 @@ from unittest import mock from neutron.common.ovn import constants as ovn_const +from neutron.objects import port_forwarding as port_forwarding_obj from neutron.services.portforwarding.constants import PORT_FORWARDING from neutron.services.portforwarding.constants import PORT_FORWARDING_PLUGIN from neutron.services.portforwarding.drivers.ovn import driver \ @@ -37,8 +38,9 @@ class TestOVNPortForwardingBase(base.BaseTestCase): self.l3_plugin._nb_ovn = fake_resources.FakeOvsdbNbOvnIdl() self.txn = self.l3_plugin._nb_ovn.transaction - def _fake_pf_obj(self, **kwargs): + def _fake_pf_obj(self, is_object=False, **kwargs): pf_obj_defaults_dict = { + 'id': 'id', 'floatingip_id': 'fip_id', 'protocol': 'udp', 'floating_ip_address': 'fip_addr', @@ -48,6 +50,8 @@ class TestOVNPortForwardingBase(base.BaseTestCase): 'router_id': 'rtr_id' } pf_obj_dict = {**pf_obj_defaults_dict, **kwargs} + if is_object: + return port_forwarding_obj.PortForwarding(**pf_obj_dict) return mock.Mock(**pf_obj_dict) def _fake_pf_payload_entry(self, curr_pf_id, orig_pf_id=None, **kwargs): @@ -103,9 +107,19 @@ class TestOVNPortForwardingHandler(TestOVNPortForwardingBase): @mock.patch.object(port_forwarding.LOG, 'info') def test_port_forwarding_created(self, m_info): - fake_pf_obj = self._fake_pf_obj() - exp_lb_name, exp_vip, exp_internal_ips, exp_rtr_name = (self.handler. - _get_lb_attributes(fake_pf_obj)) + fake_pf_obj = self._fake_pf_obj( + is_object=True, + id=uuidutils.generate_uuid(), + floatingip_id=uuidutils.generate_uuid(), + router_id=uuidutils.generate_uuid(), + protocol='udp', + floating_ip_address='10.0.0.1', + external_port=2222, + internal_ip_address='192.168.0.1', + internal_port=22, + ) + exp_lb_name, exp_vip, exp_internal_ips, exp_rtr_name = ( + self.handler._get_lb_attributes(fake_pf_obj)) exp_protocol = self.handler._get_lb_protocol(fake_pf_obj) self.handler.port_forwarding_created( self.txn, self.l3_plugin._nb_ovn, fake_pf_obj) @@ -123,26 +137,304 @@ class TestOVNPortForwardingHandler(TestOVNPortForwardingBase): self.l3_plugin._nb_ovn.lr_lb_add.assert_called_once_with( exp_rtr_name, exp_lb_name, may_exist=True) + @mock.patch.object(port_forwarding.LOG, 'info') + def test_port_forwarding_created_with_ranges(self, m_info): + id = uuidutils.generate_uuid() + floatingip_id = uuidutils.generate_uuid() + router_id = uuidutils.generate_uuid() + fake_pf_obj_ranges = self._fake_pf_obj( + is_object=True, + id=id, + floatingip_id=floatingip_id, + router_id=router_id, + protocol='udp', + floating_ip_address='10.0.0.1', + external_port=None, + external_port_range='2222:2223', + internal_ip_address='192.168.0.1', + internal_port=None, + internal_port_range='22:23' + ) + fake_pf_obj_1 = self._fake_pf_obj( + is_object=True, + id=id, + floatingip_id=floatingip_id, + router_id=router_id, + protocol='udp', + floating_ip_address='10.0.0.1', + external_port=2222, + external_port_range=None, + internal_ip_address='192.168.0.1', + internal_port=22, + internal_port_range=None + ) + fake_pf_obj_2 = self._fake_pf_obj( + is_object=True, + id=id, + floatingip_id=floatingip_id, + router_id=router_id, + protocol='udp', + floating_ip_address='10.0.0.1', + external_port=2223, + external_port_range=None, + internal_ip_address='192.168.0.1', + internal_port=23, + internal_port_range=None + ) + exp_lb_name_1, exp_vip_1, exp_internal_ips_1, exp_rtr_name_1 = ( + self.handler._get_lb_attributes(fake_pf_obj_1, True)) + exp_lb_name_2, exp_vip_2, exp_internal_ips_2, exp_rtr_name_2 = ( + self.handler._get_lb_attributes(fake_pf_obj_2, True)) + exp_protocol_1 = self.handler._get_lb_protocol(fake_pf_obj_1) + exp_protocol_2 = self.handler._get_lb_protocol(fake_pf_obj_2) + self.handler.port_forwarding_created( + self.txn, self.l3_plugin._nb_ovn, fake_pf_obj_ranges) + info_args, _info_kwargs = m_info.call_args_list[0] + self.assertIn('CREATE for port-forwarding', info_args[0]) + self.assertEqual(4, len(self.txn.add.call_args_list)) + info_args, _info_kwargs = m_info.call_args_list[1] + self.assertIn('CREATE for port-forwarding', info_args[0]) + self.assertEqual(4, len(self.txn.add.call_args_list)) + exp_external_ids_1 = { + ovn_const.OVN_DEVICE_OWNER_EXT_ID_KEY: PORT_FORWARDING_PLUGIN, + ovn_const.OVN_FIP_EXT_ID_KEY: fake_pf_obj_1.floatingip_id, + ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: exp_rtr_name_1, + } + exp_external_ids_2 = { + ovn_const.OVN_DEVICE_OWNER_EXT_ID_KEY: PORT_FORWARDING_PLUGIN, + ovn_const.OVN_FIP_EXT_ID_KEY: fake_pf_obj_1.floatingip_id, + ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: exp_rtr_name_2, + } + self.l3_plugin._nb_ovn.lb_add.assert_any_call( + exp_lb_name_1, exp_vip_1, exp_internal_ips_1, exp_protocol_1, + may_exist=True, external_ids=exp_external_ids_1) + self.l3_plugin._nb_ovn.lr_lb_add.assert_any_call( + exp_rtr_name_1, exp_lb_name_1, may_exist=True) + self.l3_plugin._nb_ovn.lb_add.assert_any_call( + exp_lb_name_2, exp_vip_2, exp_internal_ips_2, exp_protocol_2, + may_exist=True, external_ids=exp_external_ids_2) + self.l3_plugin._nb_ovn.lr_lb_add.assert_any_call( + exp_rtr_name_2, exp_lb_name_2, may_exist=True) + + @mock.patch.object(port_forwarding.LOG, 'info') + @mock.patch.object( + port_forwarding.OVNPortForwardingHandler, '_port_forwarding_deleted') + @mock.patch.object( + port_forwarding.OVNPortForwardingHandler, '_port_forwarding_created') + def test_port_forwarding_updated_with_ranges(self, m_created, m_deleted, + m_info): + id = uuidutils.generate_uuid() + floatingip_id = uuidutils.generate_uuid() + router_id = uuidutils.generate_uuid() + fake_pf_obj_ranges_old = self._fake_pf_obj( + is_object=True, + id=id, + floatingip_id=floatingip_id, + router_id=router_id, + protocol='udp', + floating_ip_address='10.0.0.1', + external_port=None, + external_port_range='2222:2223', + internal_ip_address='192.168.0.1', + internal_port=None, + internal_port_range='22:23' + ) + fake_pf_obj_1_old = self._fake_pf_obj( + is_object=True, + id=id, + floatingip_id=floatingip_id, + router_id=router_id, + protocol='udp', + floating_ip_address='10.0.0.1', + external_port=2222, + external_port_range=None, + internal_ip_address='192.168.0.1', + internal_port=22, + internal_port_range=None + ) + fake_pf_obj_2_old = self._fake_pf_obj( + is_object=True, + id=id, + floatingip_id=floatingip_id, + router_id=router_id, + protocol='udp', + floating_ip_address='10.0.0.1', + external_port=2223, + external_port_range=None, + internal_ip_address='192.168.0.1', + internal_port=23, + internal_port_range=None + ) + fake_pf_obj_ranges = self._fake_pf_obj( + is_object=True, + id=id, + floatingip_id=floatingip_id, + router_id=router_id, + protocol='tcp', + floating_ip_address='10.0.0.1', + external_port=None, + external_port_range='2222:2223', + internal_ip_address='192.168.0.1', + internal_port=None, + internal_port_range='22:23' + ) + fake_pf_obj_1 = self._fake_pf_obj( + is_object=True, + id=id, + floatingip_id=floatingip_id, + router_id=router_id, + protocol='tcp', + floating_ip_address='10.0.0.1', + external_port=2222, + external_port_range=None, + internal_ip_address='192.168.0.1', + internal_port=22, + internal_port_range=None + ) + fake_pf_obj_2 = self._fake_pf_obj( + is_object=True, + id=id, + floatingip_id=floatingip_id, + router_id=router_id, + protocol='tcp', + floating_ip_address='10.0.0.1', + external_port=2223, + external_port_range=None, + internal_ip_address='192.168.0.1', + internal_port=23, + internal_port_range=None + ) + + def do_simple_comparation(pf_1, pf_2): + pf_1_dict = {f: getattr(pf_1, f) + for f in pf_1.fields if hasattr(pf_1, f)} + pf_2_dict = {f: getattr(pf_2, f) + for f in pf_2.fields if hasattr(pf_2, f)} + self.assertEqual(pf_1_dict, pf_2_dict) + + self.handler.port_forwarding_updated( + self.txn, self.l3_plugin._nb_ovn, fake_pf_obj_ranges, + fake_pf_obj_ranges_old) + info_args, _info_kwargs = m_info.call_args_list[0] + self.assertIn('UPDATE for port-forwarding', info_args[0]) + deleted_pf_1 = m_deleted.call_args_list[0][0][2] + deleted_pf_2 = m_deleted.call_args_list[1][0][2] + do_simple_comparation(deleted_pf_1, fake_pf_obj_1_old) + do_simple_comparation(deleted_pf_2, fake_pf_obj_2_old) + created_pf_1 = m_created.call_args_list[0][0][2] + created_pf_2 = m_created.call_args_list[1][0][2] + do_simple_comparation(created_pf_1, fake_pf_obj_1) + do_simple_comparation(created_pf_2, fake_pf_obj_2) + @mock.patch.object(port_forwarding.LOG, 'info') @mock.patch.object( port_forwarding.OVNPortForwardingHandler, '_port_forwarding_deleted') @mock.patch.object( port_forwarding.OVNPortForwardingHandler, '_port_forwarding_created') def test_port_forwarding_updated(self, m_created, m_deleted, m_info): - fake_pf_obj = self._fake_pf_obj(protocol='udp') - fake_orig_pf_obj = self._fake_pf_obj(protocol='tcp') + fake_pf_obj = self._fake_pf_obj( + is_object=True, + id=uuidutils.generate_uuid(), + floatingip_id=uuidutils.generate_uuid(), + router_id=uuidutils.generate_uuid(), + protocol='udp', + floating_ip_address='10.0.0.1', + external_port=2222, + internal_ip_address='192.168.0.1', + internal_port=22, + ) + fake_orig_pf_obj = self._fake_pf_obj( + is_object=True, + id=uuidutils.generate_uuid(), + floatingip_id=uuidutils.generate_uuid(), + router_id=uuidutils.generate_uuid(), + protocol='tcp', + floating_ip_address='10.0.0.1', + external_port=2222, + internal_ip_address='192.168.0.1', + internal_port=22, + ) self.handler.port_forwarding_updated( self.txn, self.l3_plugin._nb_ovn, fake_pf_obj, fake_orig_pf_obj) info_args, _info_kwargs = m_info.call_args_list[0] self.assertIn('UPDATE for port-forwarding', info_args[0]) m_deleted.assert_called_once_with(self.txn, self.l3_plugin._nb_ovn, - fake_orig_pf_obj) + fake_orig_pf_obj, is_range=False) m_created.assert_called_once_with(self.txn, self.l3_plugin._nb_ovn, - fake_pf_obj) + fake_pf_obj, is_range=False) + + @mock.patch.object(port_forwarding.LOG, 'info') + def test_port_forwarding_deleted_with_ranges(self, m_info): + id = uuidutils.generate_uuid() + floatingip_id = uuidutils.generate_uuid() + router_id = uuidutils.generate_uuid() + fake_pf_obj_ranges = self._fake_pf_obj( + is_object=True, + id=id, + floatingip_id=floatingip_id, + router_id=router_id, + protocol='udp', + floating_ip_address='10.0.0.1', + external_port=None, + external_port_range='2222:2223', + internal_ip_address='192.168.0.1', + internal_port=None, + internal_port_range='22:23' + ) + fake_pf_obj_1 = self._fake_pf_obj( + is_object=True, + id=id, + floatingip_id=floatingip_id, + router_id=router_id, + protocol='udp', + floating_ip_address='10.0.0.1', + external_port=2222, + external_port_range=None, + internal_ip_address='192.168.0.1', + internal_port=22, + internal_port_range=None + ) + fake_pf_obj_2 = self._fake_pf_obj( + is_object=True, + id=id, + floatingip_id=floatingip_id, + router_id=router_id, + protocol='udp', + floating_ip_address='10.0.0.1', + external_port=2223, + external_port_range=None, + internal_ip_address='192.168.0.1', + internal_port=23, + internal_port_range=None + ) + + exp_lb_name_1, exp_vip_1, _, _ = self.handler._get_lb_attributes( + fake_pf_obj_1, True) + exp_lb_name_2, exp_vip_2, _, _ = self.handler._get_lb_attributes( + fake_pf_obj_2, True) + self.handler.port_forwarding_deleted( + self.txn, self.l3_plugin._nb_ovn, fake_pf_obj_ranges) + info_args, _info_kwargs = m_info.call_args_list[0] + self.assertIn('DELETE for port-forwarding', info_args[0]) + self.assertEqual(2, len(self.txn.add.call_args_list)) + self.l3_plugin._nb_ovn.lb_del.assert_any_call( + exp_lb_name_1, exp_vip_1, if_exists=mock.ANY) + self.l3_plugin._nb_ovn.lb_del.assert_any_call( + exp_lb_name_2, exp_vip_2, if_exists=mock.ANY) @mock.patch.object(port_forwarding.LOG, 'info') def test_port_forwarding_deleted(self, m_info): - fake_pf_obj = self._fake_pf_obj() + fake_pf_obj = self._fake_pf_obj( + is_object=True, + id=uuidutils.generate_uuid(), + floatingip_id=uuidutils.generate_uuid(), + router_id=uuidutils.generate_uuid(), + protocol='udp', + floating_ip_address='10.0.0.1', + external_port=2222, + internal_ip_address='192.168.0.1', + internal_port=22, + ) exp_lb_name, exp_vip, _, _ = self.handler._get_lb_attributes( fake_pf_obj) self.handler.port_forwarding_deleted( diff --git a/neutron/tests/unit/services/portforwarding/test_pf_plugin.py b/neutron/tests/unit/services/portforwarding/test_pf_plugin.py index 308e83953a5..47e61462f1c 100644 --- a/neutron/tests/unit/services/portforwarding/test_pf_plugin.py +++ b/neutron/tests/unit/services/portforwarding/test_pf_plugin.py @@ -184,6 +184,8 @@ class TestPortForwardingPlugin(testlib_api.SqlTestCase): 'floatingip_id': 'fip_id'} pf_obj = mock.Mock() pf_obj.internal_ip_address = "10.0.0.1" + pf_obj.internal_port = 22 + pf_obj.external_port = 222 mock_pf_get_object.return_value = pf_obj port_dict = {'id': 'ID', 'fixed_ips': [{"subnet_id": "test-subnet-id", "ip_address": "10.0.0.1"}]} diff --git a/releasenotes/notes/add-port-forwarding-using-port-ranges-085ca6ae0d3c60a6.yaml b/releasenotes/notes/add-port-forwarding-using-port-ranges-085ca6ae0d3c60a6.yaml new file mode 100644 index 00000000000..66cde6b3c11 --- /dev/null +++ b/releasenotes/notes/add-port-forwarding-using-port-ranges-085ca6ae0d3c60a6.yaml @@ -0,0 +1,8 @@ +--- + +features: + - | + Add support for port ranges in the port forwarding rules. + The supported ranges are N:M with N <= M. Also, the ranges + of internal and external ports relation must be: + internal range = external range or internal range = 1. \ No newline at end of file