Floating IP port forwarding resource
Add a new heat resource for Floating IP port forwarding extension. Story: 2009321 Task: 43742 Change-Id: I729f11873940a83e77038c5ba8e8eb50965623f6
This commit is contained in:
parent
2a82e9ba6a
commit
79f5868e04
@ -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):
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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.
|
Loading…
x
Reference in New Issue
Block a user