diff --git a/heat/engine/clients/os/openstacksdk.py b/heat/engine/clients/os/openstacksdk.py index 56c17b45bf..15a08bf305 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 a56a0163d5..1b1aa1d770 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 59f54147b0..6915c10779 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 0000000000..35e1fe2d40 --- /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.