Extend database to support portforwardings with port range

This patch is the second of a series of patches
to implement floating ip port forwarding with
port ranges.

The specification is defined in:
https://github.com/openstack/neutron-specs/blob/master/specs/wallaby/port-forwarding-port-ranges.rst

Implements: blueprint floatingips-portforwarding-ranges
Related-Bug: #1885921
Change-Id: I43e0b669096df865f37c74ddbd050b3b177fd5e5
This commit is contained in:
Pedro Martins 2019-08-24 18:54:17 -03:00 committed by Pedro Henrique
parent 7f9c8e244a
commit b271c82d10
22 changed files with 1125 additions and 113 deletions

View File

@ -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[

View File

@ -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,

View File

@ -1 +1 @@
34cf8b009713
I43e0b669096

View File

@ -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"
]
}

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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,

View File

@ -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 = []

View File

@ -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'],

View File

@ -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():

View File

@ -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]

View File

@ -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']

View File

@ -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
}

View File

@ -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',

View File

@ -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)

View File

@ -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(

View File

@ -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"}]}

View File

@ -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.