Merge "[server side] Floating IP port forwarding OVO and db script"
This commit is contained in:
commit
587bbe171e
@ -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}
|
||||
|
@ -1 +1 @@
|
||||
61663558142c
|
||||
867d39095bf4
|
||||
|
@ -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')
|
||||
)
|
58
neutron/db/models/port_forwarding.py
Normal file
58
neutron/db/models/port_forwarding.py
Normal 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')
|
||||
)
|
135
neutron/objects/port_forwarding.py
Normal file
135
neutron/objects/port_forwarding.py
Normal 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'])
|
@ -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,
|
||||
|
@ -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',
|
||||
|
144
neutron/tests/unit/objects/test_port_forwarding.py
Normal file
144
neutron/tests/unit/objects/test_port_forwarding.py
Normal 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
|
Loading…
Reference in New Issue
Block a user