Merge "Floating IP port forwarding resource"

This commit is contained in:
Zuul 2022-08-17 06:14:33 +00:00 committed by Gerrit Code Review
commit f98d164be9
4 changed files with 430 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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