[server side] Floating IP port forwarding OVO and db script

This patch implements the port forwarding OVO and db layer code.
Such as:
* Introduces a new OVO named 'PortForwarding'.
* Introduces a new db model for OVO.
* A migration db script for port forwarding function.

Partially-Implements: blueprint port-forwarding
This patch partially implements the following spec:
https://specs.openstack.org/openstack/neutron-specs/specs/rocky/port-forwarding.html

The race issue fix in:
https://review.openstack.org/#/c/574673/

Fip extend port forwarding field addition in:
https://review.openstack.org/#/c/575326/

Partial-Bug: #1491317
Change-Id: If24e1b3161e2a86ccc5cc21acf05d0a17f6856e7
This commit is contained in:
ZhaoBo 2018-07-03 15:45:44 +08:00
parent e3e91eb44c
commit 5bd6281f9c
8 changed files with 403 additions and 1 deletions

View File

@ -13,6 +13,7 @@
from neutron._i18n import _
from neutron.objects.logapi import logging_resource as log_object
from neutron.objects import network
from neutron.objects import port_forwarding
from neutron.objects import ports
from neutron.objects.qos import policy
from neutron.objects import securitygroup
@ -30,6 +31,7 @@ NETWORK = network.Network.obj_name()
SUBNET = subnet.Subnet.obj_name()
SECURITYGROUP = securitygroup.SecurityGroup.obj_name()
SECURITYGROUPRULE = securitygroup.SecurityGroupRule.obj_name()
PORTFORWARDING = port_forwarding.PortForwarding.obj_name()
_VALID_CLS = (
@ -42,6 +44,7 @@ _VALID_CLS = (
securitygroup.SecurityGroup,
securitygroup.SecurityGroupRule,
log_object.Log,
port_forwarding.PortForwarding,
)
_TYPE_TO_CLS_MAP = {cls.obj_name(): cls for cls in _VALID_CLS}

View File

@ -1 +1 @@
61663558142c
867d39095bf4

View File

@ -0,0 +1,59 @@
# Copyright 2018 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.
#
"""port forwarding
Revision ID: 867d39095bf4
Revises: 61663558142c
Create Date: 2018-01-15 01:52:31.308888
"""
from alembic import op
import sqlalchemy as sa
from neutron_lib.db import constants
# revision identifiers, used by Alembic.
revision = '867d39095bf4'
down_revision = '61663558142c'
def upgrade():
op.create_table(
'portforwardings',
sa.Column('id', sa.String(length=constants.UUID_FIELD_SIZE),
nullable=False),
sa.Column('floatingip_id',
sa.String(length=constants.UUID_FIELD_SIZE),
nullable=False),
sa.Column('external_port', sa.Integer(), nullable=False),
sa.Column('internal_neutron_port_id',
sa.String(length=constants.UUID_FIELD_SIZE),
nullable=False),
sa.Column('protocol', sa.String(length=40), nullable=False),
sa.Column('socket', sa.String(length=36), nullable=False),
sa.ForeignKeyConstraint(['floatingip_id'], ['floatingips.id'],
ondelete='CASCADE'),
sa.ForeignKeyConstraint(['internal_neutron_port_id'], ['ports.id'],
ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('floatingip_id', 'external_port',
name='uniq_port_forwardings0floatingip_id0'
'external_port'),
sa.UniqueConstraint('internal_neutron_port_id', 'socket',
name='uniq_port_forwardings0'
'internal_neutron_port_id0socket')
)

View File

@ -0,0 +1,58 @@
# Copyright 2018 Openstack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from neutron_lib.db import model_base
import sqlalchemy as sa
from sqlalchemy import orm
from neutron.db.models import l3
from neutron.db import models_v2
from neutron_lib.db import constants as db_const
class PortForwarding(model_base.BASEV2, model_base.HasId):
__table_args__ = (
sa.UniqueConstraint('floatingip_id', 'external_port',
name='uniq_port_forwardings0floatingip_id0'
'external_port'),
sa.UniqueConstraint('internal_neutron_port_id', 'socket',
name='uniq_port_forwardings0'
'internal_neutron_port_id0socket'),
)
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)
port = orm.relationship(
models_v2.Port, load_on_pending=True,
backref=orm.backref("port_forwardings",
lazy='subquery', uselist=True,
cascade='delete')
)
floating_ip = orm.relationship(
l3.FloatingIP, load_on_pending=True,
backref=orm.backref("port_forwardings",
lazy='subquery', uselist=True,
cascade='delete')
)

View File

@ -0,0 +1,135 @@
# Copyright (c) 2018 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.
import itertools
import netaddr
from neutron.db.models import l3
from neutron.db.models import port_forwarding as models
from neutron.objects import base
from neutron.objects import common_types
from neutron.objects import router
from neutron_lib import constants as lib_const
from oslo_versionedobjects import fields as obj_fields
FIELDS_NOT_SUPPORT_FILTER = ['internal_ip_address', 'internal_port']
@base.NeutronObjectRegistry.register
class PortForwarding(base.NeutronDbObject):
# Version 1.0: Initial version
VERSION = '1.0'
db_model = models.PortForwarding
primary_keys = ['id']
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),
'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),
'floating_ip_address': obj_fields.IPV4AddressField(),
'router_id': common_types.UUIDField()
}
synthetic_fields = ['floating_ip_address', 'router_id']
fields_no_update = {
'id', 'floatingip_id'
}
def __eq__(self, other):
for attr in self.fields:
if getattr(self, attr) != getattr(other, attr):
return False
return True
def obj_load_attr(self, attrname):
if attrname == 'floating_ip_address' or attrname == 'router_id':
return self._load_attr_from_fip(attrname)
super(PortForwarding, self).obj_load_attr(attrname)
def _load_attr_from_fip(self, attrname):
# get all necessary info from fip obj
fip_obj = router.FloatingIP.get_object(
self.obj_context, id=self.floatingip_id)
value = getattr(fip_obj, attrname)
setattr(self, attrname, value)
self.obj_reset_changes([attrname])
def from_db_object(self, db_obj):
super(PortForwarding, self).from_db_object(db_obj)
self._load_attr_from_fip(attrname='router_id')
self._load_attr_from_fip(attrname='floating_ip_address')
@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(":")
result['internal_ip_address'] = netaddr.IPAddress(
groups[0], version=lib_const.IP_VERSION_4)
result['internal_port'] = int(groups[1])
del result['socket']
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']
return result
@classmethod
def get_port_forwarding_obj_by_routers(cls, context, router_ids):
query = context.session.query(cls.db_model, l3.FloatingIP)
query = query.join(l3.FloatingIP,
cls.db_model.floatingip_id == l3.FloatingIP.id)
query = query.filter(l3.FloatingIP.router_id.in_(router_ids))
return cls._unique_port_forwarding_iterator(query)
@classmethod
def _unique_port_forwarding_iterator(cls, query):
q = query.order_by(l3.FloatingIP.router_id)
keyfunc = lambda row: row[1]
group_iterator = itertools.groupby(q, keyfunc)
for key, value in group_iterator:
for row in value:
yield (row[1]['router_id'], row[1]['floating_ip_address'],
row[0]['id'], row[1]['id'])

View File

@ -523,6 +523,8 @@ FIELD_TYPE_VALUE_GENERATOR_MAP = {
obj_fields.DateTimeField: tools.get_random_datetime,
obj_fields.DictOfStringsField: get_random_dict_of_strings,
obj_fields.IPAddressField: tools.get_random_ip_address,
obj_fields.IPV4AddressField: lambda: tools.get_random_ip_address(
version=constants.IP_VERSION_4),
obj_fields.IntegerField: tools.get_random_integer,
obj_fields.ListOfObjectsField: lambda: [],
obj_fields.ListOfStringsField: tools.get_random_string_list,

View File

@ -67,6 +67,7 @@ object_data = {
'PortBindingLevel': '1.1-50d47f63218f87581b6cd9a62db574e5',
'PortDataPlaneStatus': '1.0-25be74bda46c749653a10357676c0ab2',
'PortDNS': '1.1-c5ca2dc172bdd5fafee3fc986d1d7023',
'PortForwarding': '1.0-db61273978c497239be5389a8aeb1c61',
'PortSecurity': '1.0-b30802391a87945ee9c07582b4ff95e3',
'ProviderResourceAssociation': '1.0-05ab2d5a3017e5ce9dd381328f285f34',
'ProvisioningBlock': '1.0-c19d6d05bfa8143533471c1296066125',

View File

@ -0,0 +1,144 @@
# Copyright (c) 2018 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.
import mock
import netaddr
from neutron.objects import port_forwarding
from neutron.objects import router
from neutron.tests import tools
from neutron.tests.unit.objects import test_base as obj_test_base
from neutron.tests.unit import testlib_api
class PortForwardingObjectTestCase(obj_test_base.BaseObjectIfaceTestCase):
_test_class = port_forwarding.PortForwarding
def setUp(self):
super(PortForwardingObjectTestCase, self).setUp()
self.fip_db_fields = self.get_random_db_fields(router.FloatingIP)
del self.fip_db_fields['floating_ip_address']
def random_generate_fip_obj(db_fields, **floatingip):
if db_fields.get(
'id', None) and floatingip.get(
'id', None) and db_fields.get('id') == floatingip.get('id'):
return db_fields
db_fields['id'] = floatingip.get('id', None)
db_fields['floating_ip_address'] = tools.get_random_ip_address(
version=4)
return self.fip_db_fields
self.mock_fip_obj = mock.patch.object(
router.FloatingIP, 'get_object',
side_effect=lambda _, **y: router.FloatingIP.db_model(
**random_generate_fip_obj(self.fip_db_fields, **y))).start()
class PortForwardingDbObjectTestCase(obj_test_base.BaseDbObjectTestCase,
testlib_api.SqlTestCase):
_test_class = port_forwarding.PortForwarding
def setUp(self):
super(PortForwardingDbObjectTestCase, self).setUp()
self.update_obj_fields(
{'floatingip_id':
lambda: self._create_test_fip_id_for_port_forwarding(),
'internal_port_id': lambda: self._create_test_port_id()})
# 'portforwardings' table will store the 'internal_ip_address' and
# 'internal_port' as a single 'socket' column.
# Port forwarding object accepts 'internal_ip_address' and
# 'internal_port', but can not filter the records in db, so the
# valid filters can not contain them.
not_supported_filter_fields = ['internal_ip_address', 'internal_port']
invalid_fields = set(
self._test_class.synthetic_fields).union(
set(not_supported_filter_fields))
valid_field = [f for f in self._test_class.fields
if f not in invalid_fields][0]
self.valid_field_filter = {valid_field:
self.obj_fields[-1][valid_field]}
def _create_test_fip_id_for_port_forwarding(self):
fake_fip = '172.23.3.0'
ext_net_id = self._create_external_network_id()
router_id = self._create_test_router_id()
values = {
'floating_ip_address': netaddr.IPAddress(fake_fip),
'floating_network_id': ext_net_id,
'floating_port_id': self._create_test_port_id(
network_id=ext_net_id),
'router_id': router_id,
}
fip_obj = router.FloatingIP(self.context, **values)
fip_obj.create()
return fip_obj.id
def test_db_obj(self):
# The reason for rewriting this test is:
# 1. Currently, the existing test_db_obj test in
# obj_test_base.BaseDbObjectTestCase is not suitable for the case,
# for example, the db model is not the same with obj fields
# definition.
# 2. For port forwarding, the db model will store and accept 'socket',
# but the obj fields just only support accepting the parameters
# generate 'socket', such as 'internal_ip_address' and
# 'internal_port'.
obj = self._make_object(self.obj_fields[0])
self.assertIsNone(obj.db_obj)
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:
old_fields = {}
for key, val in fields_to_update.items():
db_model_attr = (
obj.fields_need_translation.get(key, key))
old_fields[db_model_attr] = obj.db_obj[
db_model_attr] if hasattr(
obj.db_obj, db_model_attr) else getattr(
obj, db_model_attr)
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)
obj.delete()
self.assertIsNone(obj.db_obj)
def test_get_objects_queries_constant(self):
# NOTE(bzhao) Port Forwarding uses query FLoatingIP for injecting
# floating_ip_address and router_id, not depends on relationship,
# so it will cost extra SQL query each time for finding the
# associated Floating IP by floatingip_id each time(or each
# Port Forwarding Object). Rework this if this customized OVO
# needs to be changed.
pass