summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrendan Shephard <bshephar@redhat.com>2022-08-11 10:39:20 +1000
committerBrendan Shephard <bshephar@redhat.com>2022-08-16 13:50:52 +1000
commit79f5868e044713440be34c032cbb480e8ae7aba6 (patch)
treea6d182bbcabbd438b7c2977d14ef3bec4ff32970
parent2a82e9ba6ab890c762c4d221628f20cc28d49a27 (diff)
downloadheat-79f5868e044713440be34c032cbb480e8ae7aba6.tar.gz
Floating IP port forwarding resource
Add a new heat resource for Floating IP port forwarding extension. Story: 2009321 Task: 43742 Change-Id: I729f11873940a83e77038c5ba8e8eb50965623f6
-rw-r--r--heat/engine/clients/os/openstacksdk.py6
-rw-r--r--heat/engine/resources/openstack/neutron/floatingip.py156
-rw-r--r--heat/tests/openstack/neutron/test_neutron_floating_ip.py262
-rw-r--r--releasenotes/notes/add-port-forwarding-resource-e32b5515f1b47a28.yaml6
4 files changed, 430 insertions, 0 deletions
diff --git a/heat/engine/clients/os/openstacksdk.py b/heat/engine/clients/os/openstacksdk.py
index 56c17b45b..15a08bf30 100644
--- a/heat/engine/clients/os/openstacksdk.py
+++ b/heat/engine/clients/os/openstacksdk.py
@@ -72,6 +72,12 @@ class OpenStackSDKPlugin(client_plugin.ClientPlugin):
def find_network_segment(self, value):
return self.client().network.find_segment(value).id
+ def find_network_port(self, value):
+ return self.client().network.find_port(value).id
+
+ def find_network_ip(self, value):
+ return self.client().network.find_ip(value).id
+
class SegmentConstraint(constraints.BaseCustomConstraint):
diff --git a/heat/engine/resources/openstack/neutron/floatingip.py b/heat/engine/resources/openstack/neutron/floatingip.py
index a56a0163d..1b1aa1d77 100644
--- a/heat/engine/resources/openstack/neutron/floatingip.py
+++ b/heat/engine/resources/openstack/neutron/floatingip.py
@@ -455,8 +455,164 @@ class FloatingIPAssociation(neutron.NeutronResource):
self.resource_id_set(self.id)
+class FloatingIPPortForward(neutron.NeutronResource):
+ """A resource for creating port forwarding for floating IPs.
+
+ This resource creates port forwarding for floating IPs.
+ These are sub-resource of exsisting Floating ips, which requires the
+ service_plugin and extension port_forwarding enabled and that the floating
+ ip is not associated with a neutron port.
+ """
+
+ default_client_name = 'openstack'
+
+ required_service_extension = 'floating-ip-port-forwarding'
+
+ support_status = support.SupportStatus(
+ status=support.SUPPORTED,
+ version='19.0.0',
+ )
+
+ PROPERTIES = (
+ INTERNAL_IP_ADDRESS, INTERNAL_PORT_NUMBER, EXTERNAL_PORT,
+ INTERNAL_PORT, PROTOCOL, FLOATINGIP
+ ) = (
+ 'internal_ip_address', 'internal_port_number',
+ 'external_port', 'internal_port', 'protocol', 'floating_ip'
+ )
+
+ properties_schema = {
+ INTERNAL_IP_ADDRESS: properties.Schema(
+ properties.Schema.STRING,
+ _('Internal IP address to port forwarded to.'),
+ required=True,
+ update_allowed=True,
+ constraints=[
+ constraints.CustomConstraint('ip_addr')
+ ]
+ ),
+ INTERNAL_PORT_NUMBER: properties.Schema(
+ properties.Schema.INTEGER,
+ _('Internal port number to port forward to.'),
+ update_allowed=True,
+ constraints=[
+ constraints.Range(min=1, max=65535)
+ ]
+ ),
+ EXTERNAL_PORT: properties.Schema(
+ properties.Schema.INTEGER,
+ _('External port address to port forward from.'),
+ required=True,
+ update_allowed=True,
+ constraints=[
+ constraints.Range(min=1, max=65535)
+ ]
+ ),
+ INTERNAL_PORT: properties.Schema(
+ properties.Schema.STRING,
+ _('Name or ID of the internal_ip_address port.'),
+ required=True,
+ update_allowed=True,
+ constraints=[
+ constraints.CustomConstraint('neutron.port')
+ ]
+ ),
+ PROTOCOL: properties.Schema(
+ properties.Schema.STRING,
+ _('Port protocol to forward.'),
+ required=True,
+ update_allowed=True,
+ constraints=[
+ constraints.AllowedValues([
+ 'tcp', 'udp', 'icmp', 'icmp6', 'sctp', 'dccp'])
+ ]
+ ),
+ FLOATINGIP: properties.Schema(
+ properties.Schema.STRING,
+ _('Name or ID of the floating IP create port forwarding on.'),
+ required=True,
+ ),
+ }
+
+ def translation_rules(self, props):
+ client_plugin = self.client_plugin()
+ return [
+ translation.TranslationRule(
+ props,
+ translation.TranslationRule.RESOLVE,
+ [self.FLOATINGIP],
+ client_plugin=client_plugin,
+ finder='find_network_ip'
+ ),
+ translation.TranslationRule(
+ props,
+ translation.TranslationRule.RESOLVE,
+ [self.INTERNAL_PORT],
+ client_plugin=client_plugin,
+ finder='find_network_port'
+ )
+ ]
+
+ def add_dependencies(self, deps):
+ super(FloatingIPPortForward, self).add_dependencies(deps)
+
+ for resource in self.stack.values():
+ if resource.has_interface('OS::Neutron::RouterInterface'):
+
+ def port_on_subnet(resource, subnet):
+ if not resource.has_interface('OS::Neutron::Port'):
+ return False
+ fixed_ips = resource.properties.get(
+ port.Port.FIXED_IPS) or []
+ for fixed_ip in fixed_ips:
+ port_subnet = (
+ fixed_ip.get(port.Port.FIXED_IP_SUBNET)
+ or fixed_ip.get(port.Port.FIXED_IP_SUBNET_ID))
+ return subnet == port_subnet
+ return False
+
+ interface_subnet = (
+ resource.properties.get(router.RouterInterface.SUBNET) or
+ resource.properties.get(router.RouterInterface.SUBNET_ID))
+ for d in deps.graph()[self]:
+ if port_on_subnet(d, interface_subnet):
+ deps += (self, resource)
+ break
+
+ def handle_create(self):
+ props = self.prepare_properties(self.properties, self.name)
+ fp = self.client().network.create_floating_ip_port_forwarding(
+ props.pop(self.FLOATINGIP),
+ **props)
+ self.resource_id_set(fp.id)
+
+ def handle_delete(self):
+ if not self.resource_id:
+ return
+
+ self.client().network.delete_floating_ip_port_forwarding(
+ self.properties[self.FLOATINGIP],
+ self.resource_id,
+ ignore_missing=True
+ )
+
+ def handle_check(self):
+ self.client().network.get_port_forwarding(
+ self.resource_id,
+ self.properties[self.FLOATINGIP]
+ )
+
+ def handle_update(self, prop_diff):
+ if prop_diff:
+ self.client().network.update_floating_ip_port_forwarding(
+ self.properties[self.FLOATINGIP],
+ self.resource_id,
+ **prop_diff)
+
+
def resource_mapping():
return {
'OS::Neutron::FloatingIP': FloatingIP,
'OS::Neutron::FloatingIPAssociation': FloatingIPAssociation,
+ 'OS::Neutron::FloatingIPPortForward': FloatingIPPortForward,
}
diff --git a/heat/tests/openstack/neutron/test_neutron_floating_ip.py b/heat/tests/openstack/neutron/test_neutron_floating_ip.py
index 59f54147b..6915c1077 100644
--- a/heat/tests/openstack/neutron/test_neutron_floating_ip.py
+++ b/heat/tests/openstack/neutron/test_neutron_floating_ip.py
@@ -17,6 +17,8 @@ from unittest import mock
from neutronclient.common import exceptions as qe
from neutronclient.neutron import v2_0 as neutronV20
from neutronclient.v2_0 import client as neutronclient
+from openstack import exceptions
+from oslo_utils import excutils
from heat.common import exception
from heat.common import template_format
@@ -56,6 +58,16 @@ resources:
floatingip_id: { get_resource: floating_ip }
port_id: { get_resource: port_floating }
+ port_forwarding:
+ type: OS::Neutron::FloatingIPPortForward
+ properties:
+ internal_ip_address: 10.0.0.10
+ internal_port_number: 8080
+ external_port: 80
+ protocol: tcp
+ internal_port: { get_resource: port_floating }
+ floating_ip: { get_resource: floating_ip }
+
router:
type: OS::Neutron::Router
@@ -136,6 +148,29 @@ class NeutronFloatingIPTest(common.HeatTestCase):
self.patchobject(neutron.NeutronClientPlugin, 'has_extension',
return_value=True)
+ class FakeOpenStackPlugin(object):
+
+ @excutils.exception_filter
+ def ignore_not_found(self, ex):
+ if not isinstance(ex, exceptions.ResourceNotFound):
+ raise ex
+
+ def find_network_port(self, value):
+ return('9c1eb3fe-7bba-479d-bd43-fdb0bc7cd151')
+
+ def find_network_ip(self, value):
+ return('477e8273-60a7-4c41-b683-1d497e53c384')
+
+ self.ctx = utils.dummy_context()
+ tpl = template_format.parse(neutron_floating_template)
+ self.stack = utils.parse_stack(tpl)
+ self.sdkclient = mock.Mock()
+ self.port_forward = self.stack['port_forwarding']
+ self.port_forward.client = mock.Mock(return_value=self.sdkclient)
+ self.port_forward.client_plugin = mock.Mock(
+ return_value=FakeOpenStackPlugin()
+ )
+
def test_floating_ip_validate(self):
t = template_format.parse(neutron_floating_no_assoc_template)
stack = utils.parse_stack(t)
@@ -743,3 +778,230 @@ class NeutronFloatingIPTest(common.HeatTestCase):
deps.graph.return_value = {fipa: [port]}
fipa.add_dependencies(deps)
self.assertEqual([], dep_list)
+
+ def test_fip_port_forward_create(self):
+ pfid = mock.Mock(id='180941c5-9e82-41c7-b64d-6a57302ec211')
+
+ props = {'internal_ip_address': '10.0.0.10',
+ 'internal_port_number': 8080,
+ 'external_port': 80,
+ 'internal_port': '9c1eb3fe-7bba-479d-bd43-fdb0bc7cd151',
+ 'protocol': 'tcp'}
+
+ mock_create = self.patchobject(self.sdkclient.network,
+ 'create_floating_ip_port_forwarding',
+ return_value=pfid)
+
+ self.mockclient.create_port.return_value = {
+ 'port': {
+ "status": "BUILD",
+ "id": "9c1eb3fe-7bba-479d-bd43-fdb0bc7cd151"
+ }
+ }
+ self.mockclient.show_port.return_value = {
+ 'port': {
+ "status": "ACTIVE",
+ "id": "9c1eb3fe-7bba-479d-bd43-fdb0bc7cd151"
+ }
+ }
+ self.mockclient.create_floatingip.return_value = {
+ 'floatingip': {
+ "status": "ACTIVE",
+ "id": "477e8273-60a7-4c41-b683-1d497e53c384"
+ }
+ }
+
+ p = self.stack['port_floating']
+ scheduler.TaskRunner(p.create)()
+ self.assertEqual((p.CREATE, p.COMPLETE), p.state)
+ stk_defn.update_resource_data(self.stack.defn,
+ p.name,
+ p.node_data())
+
+ fip = self.stack['floating_ip']
+ scheduler.TaskRunner(fip.create)()
+ self.assertEqual((fip.CREATE, fip.COMPLETE), fip.state)
+ stk_defn.update_resource_data(self.stack.defn,
+ fip.name,
+ fip.node_data())
+
+ port_forward = self.stack['port_forwarding']
+ scheduler.TaskRunner(port_forward.create)()
+ self.assertEqual((port_forward.CREATE, port_forward.COMPLETE),
+ port_forward.state)
+ mock_create.assert_called_once_with(
+ '477e8273-60a7-4c41-b683-1d497e53c384',
+ **props)
+
+ def test_fip_port_forward_update(self):
+ pfid = mock.Mock(id='180941c5-9e82-41c7-b64d-6a57302ec211')
+ fip_id = '477e8273-60a7-4c41-b683-1d497e53c384'
+
+ prop_diff = {'external_port': 8080}
+
+ mock_update = self.patchobject(self.sdkclient.network,
+ 'update_floating_ip_port_forwarding',
+ return_value=pfid)
+ self.patchobject(self.sdkclient.network,
+ 'create_floating_ip_port_forwarding',
+ return_value=pfid)
+ self.mockclient.create_port.return_value = {
+ 'port': {
+ "status": "BUILD",
+ "id": "9c1eb3fe-7bba-479d-bd43-fdb0bc7cd151"
+ }
+ }
+ self.mockclient.show_port.return_value = {
+ 'port': {
+ "status": "ACTIVE",
+ "id": "9c1eb3fe-7bba-479d-bd43-fdb0bc7cd151"
+ }
+ }
+ self.mockclient.create_floatingip.return_value = {
+ 'floatingip': {
+ "status": "ACTIVE",
+ "id": "477e8273-60a7-4c41-b683-1d497e53c384"
+ }
+ }
+
+ p = self.stack['port_floating']
+ scheduler.TaskRunner(p.create)()
+ self.assertEqual((p.CREATE, p.COMPLETE), p.state)
+ stk_defn.update_resource_data(self.stack.defn,
+ p.name,
+ p.node_data())
+
+ fip = self.stack['floating_ip']
+ scheduler.TaskRunner(fip.create)()
+ self.assertEqual((fip.CREATE, fip.COMPLETE), fip.state)
+ stk_defn.update_resource_data(self.stack.defn,
+ fip.name,
+ fip.node_data())
+
+ port_forward = self.stack['port_forwarding']
+ scheduler.TaskRunner(port_forward.create)()
+ self.port_forward.handle_update(prop_diff)
+
+ mock_update.assert_called_once_with(
+ fip_id,
+ '180941c5-9e82-41c7-b64d-6a57302ec211',
+ **prop_diff)
+
+ def test_fip_port_forward_delete(self):
+ pfid = mock.Mock(id='180941c5-9e82-41c7-b64d-6a57302ec211')
+ fip_id = '477e8273-60a7-4c41-b683-1d497e53c384'
+
+ self.patchobject(self.sdkclient.network,
+ 'create_floating_ip_port_forwarding',
+ return_value=pfid)
+
+ mock_delete = self.patchobject(self.sdkclient.network,
+ 'delete_floating_ip_port_forwarding',
+ return_value=None)
+
+ self.mockclient.create_port.return_value = {
+ 'port': {
+ "status": "BUILD",
+ "id": "9c1eb3fe-7bba-479d-bd43-fdb0bc7cd151"
+ }
+ }
+ self.mockclient.show_port.return_value = {
+ 'port': {
+ "status": "ACTIVE",
+ "id": "9c1eb3fe-7bba-479d-bd43-fdb0bc7cd151"
+ }
+ }
+ self.mockclient.create_floatingip.return_value = {
+ 'floatingip': {
+ "status": "ACTIVE",
+ "id": "477e8273-60a7-4c41-b683-1d497e53c384"
+ }
+ }
+
+ p = self.stack['port_floating']
+ scheduler.TaskRunner(p.create)()
+ self.assertEqual((p.CREATE, p.COMPLETE), p.state)
+ stk_defn.update_resource_data(self.stack.defn,
+ p.name,
+ p.node_data())
+
+ fip = self.stack['floating_ip']
+ scheduler.TaskRunner(fip.create)()
+ self.assertEqual((fip.CREATE, fip.COMPLETE), fip.state)
+ stk_defn.update_resource_data(self.stack.defn,
+ fip.name,
+ fip.node_data())
+
+ port_forward = self.stack['port_forwarding']
+ scheduler.TaskRunner(port_forward.create)()
+ self.port_forward.handle_delete()
+ mock_delete.assert_called_once_with(
+ fip_id,
+ '180941c5-9e82-41c7-b64d-6a57302ec211',
+ ignore_missing=True
+ )
+
+ def test_fip_port_forward_check(self):
+ pfid = mock.Mock(id='180941c5-9e82-41c7-b64d-6a57302ec211')
+ fip_id = '477e8273-60a7-4c41-b683-1d497e53c384'
+
+ self.patchobject(self.sdkclient.network,
+ 'create_floating_ip_port_forwarding',
+ return_value=pfid)
+
+ self.mockclient.create_port.return_value = {
+ 'port': {
+ "status": "BUILD",
+ "id": "9c1eb3fe-7bba-479d-bd43-fdb0bc7cd151"
+ }
+ }
+ self.mockclient.show_port.return_value = {
+ 'port': {
+ "status": "ACTIVE",
+ "id": "9c1eb3fe-7bba-479d-bd43-fdb0bc7cd151"
+ }
+ }
+ self.mockclient.create_floatingip.return_value = {
+ 'floatingip': {
+ "status": "ACTIVE",
+ "id": "477e8273-60a7-4c41-b683-1d497e53c384"
+ }
+ }
+
+ p = self.stack['port_floating']
+ scheduler.TaskRunner(p.create)()
+ self.assertEqual((p.CREATE, p.COMPLETE), p.state)
+ stk_defn.update_resource_data(self.stack.defn,
+ p.name,
+ p.node_data())
+
+ fip = self.stack['floating_ip']
+ scheduler.TaskRunner(fip.create)()
+ self.assertEqual((fip.CREATE, fip.COMPLETE), fip.state)
+ stk_defn.update_resource_data(self.stack.defn,
+ fip.name,
+ fip.node_data())
+
+ port_forward = self.stack['port_forwarding']
+ scheduler.TaskRunner(port_forward.create)()
+ self.port_forward.handle_check()
+ mock_check = self.sdkclient.network.get_port_forwarding
+
+ mock_check.assert_called_once_with(
+ '180941c5-9e82-41c7-b64d-6a57302ec211',
+ fip_id
+ )
+
+ def test_pf_add_dependencies(self):
+ port = self.stack['port_floating']
+ r_int = self.stack['router_interface']
+ pf_port = self.stack['port_forwarding']
+ deps = mock.MagicMock()
+ dep_list = []
+
+ def iadd(obj):
+ dep_list.append(obj[1])
+ deps.__iadd__.side_effect = iadd
+ deps.graph.return_value = {pf_port: [port]}
+ pf_port.add_dependencies(deps)
+ self.assertEqual([r_int], dep_list)
diff --git a/releasenotes/notes/add-port-forwarding-resource-e32b5515f1b47a28.yaml b/releasenotes/notes/add-port-forwarding-resource-e32b5515f1b47a28.yaml
new file mode 100644
index 000000000..35e1fe2d4
--- /dev/null
+++ b/releasenotes/notes/add-port-forwarding-resource-e32b5515f1b47a28.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ OS::Neutron::FloatingIPPortForward added. This feature allows
+ an operator to create port-forwarding rules in Neutron for
+ their floating ips.