diff --git a/neutron/api/rpc/callbacks/resources.py b/neutron/api/rpc/callbacks/resources.py index 970ed165283..ce4de181f86 100644 --- a/neutron/api/rpc/callbacks/resources.py +++ b/neutron/api/rpc/callbacks/resources.py @@ -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} diff --git a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD index 31d6c4434d7..9ff92286731 100644 --- a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -61663558142c +867d39095bf4 diff --git a/neutron/db/migration/alembic_migrations/versions/rocky/expand/867d39095bf4_port_forwarding.py b/neutron/db/migration/alembic_migrations/versions/rocky/expand/867d39095bf4_port_forwarding.py new file mode 100644 index 00000000000..8aec629ec77 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/rocky/expand/867d39095bf4_port_forwarding.py @@ -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') + ) diff --git a/neutron/db/models/port_forwarding.py b/neutron/db/models/port_forwarding.py new file mode 100644 index 00000000000..1b90818222a --- /dev/null +++ b/neutron/db/models/port_forwarding.py @@ -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') + ) diff --git a/neutron/objects/port_forwarding.py b/neutron/objects/port_forwarding.py new file mode 100644 index 00000000000..fcea29bf6bd --- /dev/null +++ b/neutron/objects/port_forwarding.py @@ -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']) diff --git a/neutron/tests/unit/objects/test_base.py b/neutron/tests/unit/objects/test_base.py index 16626abcc70..a06551a1d0b 100644 --- a/neutron/tests/unit/objects/test_base.py +++ b/neutron/tests/unit/objects/test_base.py @@ -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, diff --git a/neutron/tests/unit/objects/test_objects.py b/neutron/tests/unit/objects/test_objects.py index b4cea51f75c..b66a2f91bd7 100644 --- a/neutron/tests/unit/objects/test_objects.py +++ b/neutron/tests/unit/objects/test_objects.py @@ -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', diff --git a/neutron/tests/unit/objects/test_port_forwarding.py b/neutron/tests/unit/objects/test_port_forwarding.py new file mode 100644 index 00000000000..0500fbd565c --- /dev/null +++ b/neutron/tests/unit/objects/test_port_forwarding.py @@ -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