summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--openstack_dashboard/api/neutron.py132
-rw-r--r--openstack_dashboard/dashboards/project/floating_ip_portforwardings/__init__.py0
-rw-r--r--openstack_dashboard/dashboards/project/floating_ip_portforwardings/panel.py37
-rw-r--r--openstack_dashboard/dashboards/project/floating_ip_portforwardings/tables.py194
-rw-r--r--openstack_dashboard/dashboards/project/floating_ip_portforwardings/tests.py262
-rw-r--r--openstack_dashboard/dashboards/project/floating_ip_portforwardings/urls.py24
-rw-r--r--openstack_dashboard/dashboards/project/floating_ip_portforwardings/views.py110
-rw-r--r--openstack_dashboard/dashboards/project/floating_ip_portforwardings/workflows.py270
-rw-r--r--openstack_dashboard/dashboards/project/floating_ips/tables.py116
-rw-r--r--openstack_dashboard/dashboards/project/floating_ips/tests.py25
-rw-r--r--openstack_dashboard/dashboards/project/floating_ips/views.py15
-rw-r--r--openstack_dashboard/dashboards/project/floating_ips/workflows.py5
-rw-r--r--openstack_dashboard/enabled/_1520_project_floating_ip_portforwardings_panel.py7
-rw-r--r--openstack_dashboard/test/test_data/neutron_data.py44
-rw-r--r--openstack_dashboard/test/unit/api/test_neutron.py75
-rw-r--r--releasenotes/notes/add-support-to-floating-ip-port-forwardings-3d0d43a2d997ce79.yaml8
-rw-r--r--requirements.txt2
17 files changed, 1312 insertions, 14 deletions
diff --git a/openstack_dashboard/api/neutron.py b/openstack_dashboard/api/neutron.py
index dcc9ac78b..3df0e8a8c 100644
--- a/openstack_dashboard/api/neutron.py
+++ b/openstack_dashboard/api/neutron.py
@@ -510,15 +510,38 @@ class SecurityGroupManager(object):
class FloatingIp(base.APIDictWrapper):
_attrs = ['id', 'ip', 'fixed_ip', 'port_id', 'instance_id',
- 'instance_type', 'pool', 'dns_domain', 'dns_name']
+ 'instance_type', 'pool', 'dns_domain', 'dns_name',
+ 'port_forwardings']
def __init__(self, fip):
fip['ip'] = fip['floating_ip_address']
fip['fixed_ip'] = fip['fixed_ip_address']
fip['pool'] = fip['floating_network_id']
+ fip['port_forwardings'] = fip.get('portforwardings', {})
super().__init__(fip)
+class PortForwarding(base.APIDictWrapper):
+ _attrs = ['id', 'floating_ip_id', 'protocol', 'internal_port_range',
+ 'external_port_range', 'internal_ip_address',
+ 'description', 'internal_port_id', 'external_ip_address']
+
+ def __init__(self, pfw, fip):
+ pfw['floating_ip_id'] = fip
+ port_forwarding = pfw
+ if 'port_forwarding' in pfw:
+ port_forwarding = pfw['port_forwarding']
+ port_forwarding['internal_port_range'] = ':'.join(
+ map(str, sorted(
+ map(int, set(port_forwarding.get(
+ 'internal_port_range', '').split(':'))))))
+ port_forwarding['external_port_range'] = ':'.join(
+ map(str, sorted(
+ map(int, set(port_forwarding.get(
+ 'external_port_range', '').split(':'))))))
+ super().__init__(pfw)
+
+
class FloatingIpPool(base.APIDictWrapper):
pass
@@ -544,6 +567,81 @@ class FloatingIpTarget(base.APIDictWrapper):
super().__init__(target)
+class PortForwardingManager(object):
+
+ def __init__(self, request):
+ self.request = request
+ self.client = neutronclient(request)
+
+ @profiler.trace
+ def list(self, floating_ip_id, **search_opts):
+ port_forwarding_rules = self.client.list_port_forwardings(
+ floating_ip_id, **search_opts)
+ port_forwarding_rules = port_forwarding_rules.get('port_forwardings')
+ LOG.debug("Portforwarding rules listed=%s", port_forwarding_rules)
+ return [PortForwarding(port_forwarding_rule, floating_ip_id)
+ for port_forwarding_rule in port_forwarding_rules]
+
+ @profiler.trace
+ def update(self, floating_ip_id, **params):
+ portforwarding_dict = self.create_port_forwarding_dict(**params)
+ portforwarding_id = params['portforwarding_id']
+ LOG.debug("Updating Portforwarding rule with id %s", portforwarding_id)
+ pfw = self.client.update_port_forwarding(
+ floating_ip_id,
+ portforwarding_id,
+ {'port_forwarding': portforwarding_dict}).get('port_forwarding')
+
+ return PortForwarding(pfw, floating_ip_id)
+
+ @profiler.trace
+ def create(self, floating_ip_id, **params):
+ portforwarding_dict = self.create_port_forwarding_dict(**params)
+ portforwarding_rule = self.client.create_port_forwarding(
+ floating_ip_id,
+ {'port_forwarding': portforwarding_dict}).get('port_forwarding')
+ LOG.debug("Created a Portforwarding rule to floating IP %s with id %s",
+ floating_ip_id,
+ portforwarding_rule['id'])
+ return PortForwarding(portforwarding_rule, floating_ip_id)
+
+ def create_port_forwarding_dict(self, **params):
+ portforwarding_dict = {}
+ if 'protocol' in params:
+ portforwarding_dict['protocol'] = str(params['protocol']).lower()
+ if 'internal_port' in params:
+ internal_port = str(params['internal_port'])
+ if ':' not in internal_port:
+ portforwarding_dict['internal_port'] = int(internal_port)
+ else:
+ portforwarding_dict['internal_port_range'] = internal_port
+ if 'external_port' in params:
+ external_port = str(params['external_port'])
+ if ':' not in external_port:
+ portforwarding_dict['external_port'] = int(external_port)
+ else:
+ portforwarding_dict['external_port_range'] = external_port
+ if 'internal_ip_address' in params:
+ portforwarding_dict['internal_ip_address'] = params[
+ 'internal_ip_address']
+ if 'description' in params:
+ portforwarding_dict['description'] = params['description']
+ if 'internal_port_id' in params:
+ portforwarding_dict['internal_port_id'] = params['internal_port_id']
+ return portforwarding_dict
+
+ def delete(self, floating_ip_id, portforwarding_id):
+ self.client.delete_port_forwarding(floating_ip_id, portforwarding_id)
+ LOG.debug(
+ "The Portforwarding rule of floating IP %s with id %s was deleted",
+ floating_ip_id, portforwarding_id)
+
+ def get(self, floating_ip_id, portforwarding_id):
+ pfw = self.client.show_port_forwarding(floating_ip_id,
+ portforwarding_id)
+ return PortForwarding(pfw, portforwarding_id)
+
+
class FloatingIpManager(object):
"""Manager class to implement Floating IP methods
@@ -1956,6 +2054,26 @@ def tenant_floating_ip_list(request, all_tenants=False, **search_opts):
**search_opts)
+def floating_ip_port_forwarding_list(request, fip):
+ return PortForwardingManager(request).list(fip)
+
+
+def floating_ip_port_forwarding_create(request, fip, **params):
+ return PortForwardingManager(request).create(fip, **params)
+
+
+def floating_ip_port_forwarding_update(request, fip, **params):
+ return PortForwardingManager(request).update(fip, **params)
+
+
+def floating_ip_port_forwarding_get(request, fip, pfw):
+ return PortForwardingManager(request).get(fip, pfw)
+
+
+def floating_ip_port_forwarding_delete(request, fip, pfw):
+ return PortForwardingManager(request).delete(fip, pfw)
+
+
def tenant_floating_ip_get(request, floating_ip_id):
return FloatingIpManager(request).get(floating_ip_id)
@@ -2179,6 +2297,18 @@ def is_extension_supported(request, extension_alias):
return False
+@profiler.trace
+def is_extension_floating_ip_port_forwarding_supported(request):
+ try:
+ return is_extension_supported(
+ request, extension_alias='floating-ip-port-forwarding')
+ except Exception as e:
+ LOG.error("It was not possible to check if the "
+ "floating-ip-port-forwarding extension is enabled in "
+ "neutron. Port forwardings will not be enabled.: %s", e)
+ return False
+
+
# TODO(amotoki): Clean up 'default' parameter because the default
# values are pre-defined now, so 'default' argument is meaningless
# in most cases.
diff --git a/openstack_dashboard/dashboards/project/floating_ip_portforwardings/__init__.py b/openstack_dashboard/dashboards/project/floating_ip_portforwardings/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/floating_ip_portforwardings/__init__.py
diff --git a/openstack_dashboard/dashboards/project/floating_ip_portforwardings/panel.py b/openstack_dashboard/dashboards/project/floating_ip_portforwardings/panel.py
new file mode 100644
index 000000000..08c430fa6
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/floating_ip_portforwardings/panel.py
@@ -0,0 +1,37 @@
+# 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 logging
+
+from django.utils.translation import gettext_lazy as _
+
+import horizon
+
+from openstack_dashboard.api import neutron
+
+LOG = logging.getLogger(__name__)
+
+
+class FloatingIpPortforwardingRules(horizon.Panel):
+ name = _("Floating IP port forwarding rules")
+ slug = 'floating_ip_portforwardings'
+ permissions = ('openstack.services.network',)
+ nav = False
+
+ def allowed(self, context):
+ request = context['request']
+ return (
+ super().allowed(context) and
+ request.user.has_perms(self.permissions) and
+ neutron.is_extension_floating_ip_port_forwarding_supported(
+ request)
+ )
diff --git a/openstack_dashboard/dashboards/project/floating_ip_portforwardings/tables.py b/openstack_dashboard/dashboards/project/floating_ip_portforwardings/tables.py
new file mode 100644
index 000000000..b2f5bed14
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/floating_ip_portforwardings/tables.py
@@ -0,0 +1,194 @@
+# 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 django import shortcuts
+from django.urls import reverse
+from django.utils.http import urlencode
+from django.utils.translation import gettext_lazy as _
+from django.utils.translation import ngettext_lazy
+
+from horizon import tables
+
+from openstack_dashboard import api
+from openstack_dashboard import policy
+
+PROTOCOL_CHOICES = (
+ ("Select a protocol", "Select a protocol"),
+ ("UDP", "UDP"),
+ ("TCP", "TCP"),
+)
+
+
+class CreateFloatingIpPortForwardingRule(tables.LinkAction):
+ name = "create"
+ verbose_name = _("Add floating IP port forwarding rule")
+ classes = ("ajax-modal",)
+ icon = "plus"
+ url = "horizon:project:floating_ip_portforwardings:create"
+ floating_ip_id = None
+
+ def allowed(self, request, fip=None):
+ policy_rules = (("network", "create_floatingip_port_forwarding"),)
+ return policy.check(policy_rules, request)
+
+ def single(self, data_table, request, *args):
+ return shortcuts.redirect(
+ 'horizon:project:floating_ip_portforwardings:show')
+
+ def get_url_params(self, datum=None):
+ return urlencode({"floating_ip_id": self.floating_ip_id})
+
+ def get_link_url(self, datum=None):
+ base_url = reverse(self.url)
+ join = "?".join([base_url, self.get_url_params(datum)])
+ return join
+
+
+class EditFloatingIpPortForwardingRule(CreateFloatingIpPortForwardingRule):
+ name = "edit"
+ verbose_name = _("Edit floating IP port forwarding rule")
+ classes = ("ajax-modal", "btn-edit")
+ url = "horizon:project:floating_ip_portforwardings:edit"
+
+ def allowed(self, request, fip=None):
+ policy_rules = (("network", "update_floatingip_port_forwarding"),)
+ return policy.check(policy_rules, request)
+
+ def get_url_params(self, datum=None):
+ portforwading_id = self.table.get_object_id(datum)
+ return urlencode({"floating_ip_id": self.floating_ip_id,
+ "pfwd_id": portforwading_id})
+
+
+class EditFloatingIpPortForwardingRuleFromAllPanel(
+ EditFloatingIpPortForwardingRule):
+ name = "edit-from-all"
+ url = "horizon:project:floating_ip_portforwardings:editToAll"
+
+ def single(self, data_table, request, *args):
+ return shortcuts.redirect(
+ 'horizon:project:floating_ip_portforwardings:index')
+
+ def get_url_params(self, datum=None):
+ portforwading_id = self.table.get_object_id(datum)
+ return urlencode({"floating_ip_id": datum.floating_ip_id,
+ "pfwd_id": portforwading_id})
+
+
+class DeleteRule(tables.DeleteAction):
+ name = "delete"
+ help_text = _(
+ "This action will delete the "
+ "selected floating IP port forwarding rule(s); "
+ "this process cannot be undone.")
+ floating_ip_id = None
+
+ @staticmethod
+ def action_present(count):
+ return ngettext_lazy(
+ u"Delete Rule",
+ u"Delete Rules",
+ count
+ )
+
+ @staticmethod
+ def action_past(count):
+ return ngettext_lazy(
+ u"Deleted Rule",
+ u"Deleted Rules",
+ count
+ )
+
+ def allowed(self, request, fip=None):
+ policy_rules = (("network", "delete_floatingip_port_forwarding"),)
+ return policy.check(policy_rules, request)
+
+ def action(self, request, obj_id):
+ api.neutron.floating_ip_port_forwarding_delete(request,
+ self.floating_ip_id,
+ obj_id)
+
+
+class DeleteRuleFromAllPanel(DeleteRule):
+ name = "delete-from-all"
+
+ def action(self, request, obj_id):
+ datum = self.table.get_object_by_id(obj_id)
+ api.neutron.floating_ip_port_forwarding_delete(request,
+ datum.floating_ip_id,
+ obj_id)
+
+
+class FloatingIpPortForwardingRulesTable(tables.DataTable):
+ protocol = tables.Column("protocol", verbose_name=_("Protocol"))
+ external_port_range = tables.Column("external_port_range",
+ verbose_name=_("External port"))
+ internal_port_range = tables.Column("internal_port_range",
+ verbose_name=_("Internal port"))
+ internal_ip_address = tables.Column("internal_ip_address",
+ verbose_name=_("Internal IP address"))
+ description = tables.Column("description", verbose_name=_("Description"))
+
+ def __init__(self, request, data=None, needs_form_wrapper=None, **kwargs):
+ super().__init__(
+ request, data=data, needs_form_wrapper=needs_form_wrapper,
+ **kwargs)
+
+ floating_ip_id = request.GET.get('floating_ip_id')
+
+ for action in self.get_table_actions():
+ action.floating_ip_id = floating_ip_id
+
+ for action in self._meta.row_actions:
+ action.floating_ip_id = floating_ip_id
+
+ def get_object_display(self, datum):
+ return str(datum.internal_ip_address) + ':' + str(
+ datum.internal_port_range)
+
+ class Meta(object):
+ name = "floating_ip_portforwardings"
+ verbose_name = _("Floating IP port forwarding rules")
+ table_actions = (CreateFloatingIpPortForwardingRule, DeleteRule)
+ row_actions = (EditFloatingIpPortForwardingRule, DeleteRule)
+
+
+class AllFloatingIpPortForwardingRulesTable(tables.DataTable):
+ floating_ip_id = tables.Column("floating_ip_id",
+ verbose_name=_("floating_ip_id"),
+ hidden=True)
+ protocol = tables.Column("protocol", verbose_name=_("Protocol"))
+ external_port_range = tables.Column("external_port_range",
+ verbose_name=_("External port"))
+ internal_port_range = tables.Column("internal_port_range",
+ verbose_name=_("Internal port"))
+ external_ip_address = tables.Column("external_ip_address",
+ verbose_name=_("External IP address"))
+ internal_ip_address = tables.Column("internal_ip_address",
+ verbose_name=_("Internal IP address"))
+ description = tables.Column("description", verbose_name=_("Description"))
+
+ def __init__(self, request, data=None, needs_form_wrapper=None, **kwargs):
+ super().__init__(
+ request, data=data, needs_form_wrapper=needs_form_wrapper, **kwargs)
+
+ def get_object_display(self, datum):
+ return str(datum.internal_ip_address) + ':' + str(
+ datum.internal_port_range)
+
+ class Meta(object):
+ name = "floating_ip_portforwardings"
+ verbose_name = _("Floating IP port forwarding rules")
+ table_actions = (DeleteRuleFromAllPanel,)
+ row_actions = (
+ EditFloatingIpPortForwardingRuleFromAllPanel,
+ DeleteRuleFromAllPanel)
diff --git a/openstack_dashboard/dashboards/project/floating_ip_portforwardings/tests.py b/openstack_dashboard/dashboards/project/floating_ip_portforwardings/tests.py
new file mode 100644
index 000000000..f791b428f
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/floating_ip_portforwardings/tests.py
@@ -0,0 +1,262 @@
+# 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 unittest import mock
+import uuid
+
+from django.urls import reverse
+from django.utils.http import urlencode
+
+from openstack_dashboard import api
+from openstack_dashboard.test import helpers as test
+
+from horizon.tables import views as table_views
+from horizon.workflows import views
+
+INDEX_URL = reverse('horizon:project:floating_ip_portforwardings:index')
+NAMESPACE = "horizon:project:floating_ip_portforwardings"
+
+
+class FloatingIpPortforwardingViewTests(test.TestCase):
+
+ def setUp(self):
+ super().setUp()
+ api_mock = mock.patch.object(
+ api.neutron,
+ 'is_extension_floating_ip_port_forwarding_supported').start()
+ api_mock.return_value = True
+
+ @test.create_mocks({api.neutron: ('tenant_floating_ip_get',
+ 'floating_ip_port_forwarding_list')})
+ def test_floating_ip_portforwarding(self):
+ fip = self._get_fip_targets()[0]
+ self.mock_tenant_floating_ip_get.return_value = fip
+ fip_id = fip.id
+ self.mock_floating_ip_port_forwarding_list.return_value = (
+ fip.port_forwardings)
+
+ params = urlencode({'floating_ip_id': fip_id})
+ url = '?'.join([reverse('%s:show' % NAMESPACE), params])
+ res = self.client.get(url)
+ self.assertTemplateUsed(res, table_views.DataTableView.template_name)
+ table_data = res.context_data['table'].data
+ self.assertEqual(len(table_data), 1)
+ self.assertEqual(fip.port_forwardings[0].id, table_data[0].id)
+
+ @test.create_mocks({api.neutron: ('floating_ip_port_forwarding_list',
+ 'tenant_floating_ip_list')})
+ def test_floating_ip_portforwarding_all(self):
+ fips = self._get_fip_targets()
+ self.mock_tenant_floating_ip_list.return_value = fips
+ fips_dict = {}
+ for f in fips:
+ fips_dict[f.id] = f.port_forwardings
+
+ def pfw_list(request, fip_id):
+ return fips_dict[fip_id]
+
+ self.mock_floating_ip_port_forwarding_list.side_effect = pfw_list
+
+ url = reverse('%s:index' % NAMESPACE)
+
+ res = self.client.get(url)
+ self.assertTemplateUsed(res, table_views.DataTableView.template_name)
+ table_data = res.context_data['table'].data
+ self.assertEqual(len(table_data), len(fips))
+ for pfw in table_data:
+ self.assertIn(pfw.id, list(map(lambda x: x.id,
+ fips_dict[pfw.floating_ip_id])))
+
+ def _get_compute_ports(self):
+ return [p for p in self.ports.list()
+ if not p.device_owner.startswith('network:')]
+
+ def _get_fip_targets(self):
+ server_dict = dict((s.id, s.name) for s in self.servers.list())
+ targets = []
+ port = 10
+ for p in self._get_compute_ports():
+ for ip in p.fixed_ips:
+ targets.append(api.neutron.FloatingIpTarget(
+ p, ip['ip_address'], server_dict.get(p.device_id)))
+ targets[-1].ip = ip['ip_address']
+ targets[-1].port_id = None
+ targets[-1].port_forwardings = [api.neutron.PortForwarding({
+ 'id': str(uuid.uuid4()),
+ 'floating_ip_id': targets[-1].id,
+ 'protocol': 'TCP',
+ 'internal_port_range': str(port),
+ 'external_port_range': str(port + 10),
+ 'internal_ip_address': ip['ip_address'],
+ 'description': '',
+ 'internal_port_id': '',
+ 'external_ip_address': ''}, targets[-1].id)]
+
+ port += 1
+ return targets
+
+ @test.create_mocks({api.neutron: ('tenant_floating_ip_get',
+ 'floating_ip_port_forwarding_list',
+ 'floating_ip_target_list')})
+ def test_create_floating_ip_portforwarding(self):
+ fip = self._get_fip_targets()[0]
+ self.mock_tenant_floating_ip_get.return_value = fip
+ fip_id = fip.id
+ self.mock_floating_ip_port_forwarding_list.return_value = (
+ fip.port_forwardings)
+ self.mock_floating_ip_target_list.return_value = [fip]
+
+ params = urlencode({'floating_ip_id': fip_id})
+ url = '?'.join([reverse('%s:create' % NAMESPACE), params])
+ res = self.client.get(url)
+ self.assertTemplateUsed(res, views.WorkflowView.template_name)
+ workflow = res.context['workflow']
+ choices = dict(
+ workflow.steps[0].action.fields[
+ 'internal_ip_address'].choices)
+ choices.pop('Select an IP-Address')
+
+ self.assertEqual({fip.id}, set(choices.keys()))
+
+ @test.create_mocks({api.neutron: ('tenant_floating_ip_get',
+ 'floating_ip_port_forwarding_list',
+ 'floating_ip_port_forwarding_create',
+ 'floating_ip_target_list')})
+ def test_create_floating_ip_portforwarding_post(self):
+ fip = self._get_fip_targets()[0]
+ self.mock_tenant_floating_ip_get.return_value = fip
+ fip_id = fip.id
+ self.mock_floating_ip_port_forwarding_list.return_value = (
+ fip.port_forwardings)
+ self.mock_floating_ip_target_list.return_value = [fip]
+
+ create_mock = self.mock_floating_ip_port_forwarding_create
+
+ params = urlencode({'floating_ip_id': fip_id})
+ url = '?'.join([reverse('%s:create' % NAMESPACE), params])
+ port = self.ports.get(id=fip.id.split('_')[0])
+ internal_ip = '%s_%s' % (port.id, port.fixed_ips[0]['ip_address'])
+ post_params = {
+ 'floating_ip_id': fip_id,
+ 'description': 'test',
+ 'internal_port': '10',
+ 'protocol': 'TCP',
+ 'internal_ip_address': internal_ip,
+ 'external_port': '123',
+ }
+ expected_params = {
+ 'description': 'test',
+ 'internal_port': '10',
+ 'protocol': 'TCP',
+ 'internal_port_id': internal_ip.split('_')[0],
+ 'internal_ip_address': internal_ip.split('_')[1],
+ 'external_port': '123',
+ }
+ self.client.post(url, post_params)
+ create_mock.assert_called_once_with(mock.ANY, fip_id,
+ **expected_params)
+
+ @test.create_mocks({api.neutron: ('tenant_floating_ip_get',
+ 'floating_ip_port_forwarding_list',
+ 'floating_ip_target_list',
+ 'floating_ip_port_forwarding_get')})
+ def test_update_floating_ip_portforwarding(self):
+ fip = self._get_fip_targets()[0]
+ self.mock_tenant_floating_ip_get.return_value = fip
+ fip_id = fip.id
+ self.mock_floating_ip_port_forwarding_list.return_value = (
+ fip.port_forwardings)
+ self.mock_floating_ip_target_list.return_value = [fip]
+ self.mock_floating_ip_port_forwarding_get.return_value = {
+ 'port_forwarding': fip.port_forwardings[0].to_dict()
+ }
+
+ params = urlencode({'floating_ip_id': fip_id,
+ 'pfwd_id': fip.port_forwardings[0]['id']})
+ url = '?'.join([reverse('%s:edit' % NAMESPACE), params])
+ res = self.client.get(url)
+ self.assertTemplateUsed(res, views.WorkflowView.template_name)
+ workflow = res.context['workflow']
+
+ self.assertEqual(workflow.steps[0].action.initial['floating_ip_id'],
+ fip.port_forwardings[0]['floating_ip_id'])
+ self.assertEqual(workflow.steps[0].action.initial['portforwading_id'],
+ fip.port_forwardings[0]['id'])
+ self.assertEqual(workflow.steps[0].action.initial['protocol'],
+ fip.port_forwardings[0]['protocol'])
+ self.assertEqual(workflow.steps[0].action.initial['internal_port'],
+ fip.port_forwardings[0]['internal_port_range'])
+ self.assertEqual(workflow.steps[0].action.initial['external_port'],
+ fip.port_forwardings[0]['external_port_range'])
+ self.assertEqual(workflow.steps[0].action.initial['description'],
+ fip.port_forwardings[0]['description'])
+
+ @test.create_mocks({api.neutron: ('tenant_floating_ip_get',
+ 'floating_ip_port_forwarding_list',
+ 'floating_ip_target_list',
+ 'floating_ip_port_forwarding_update',
+ 'floating_ip_port_forwarding_get')})
+ def test_update_floating_ip_portforwarding_post(self):
+ fip = self._get_fip_targets()[0]
+ self.mock_tenant_floating_ip_get.return_value = fip
+ fip_id = fip.id
+ self.mock_floating_ip_port_forwarding_list.return_value = (
+ fip.port_forwardings)
+ self.mock_floating_ip_target_list.return_value = [fip]
+ self.mock_floating_ip_port_forwarding_get.return_value = {
+ 'port_forwarding': fip.port_forwardings[0].to_dict()
+ }
+ update_mock = self.mock_floating_ip_port_forwarding_update
+ pfw_id = fip.port_forwardings[0]['id']
+ params = urlencode({'floating_ip_id': fip_id,
+ 'pfwd_id': pfw_id})
+ url = '?'.join([reverse('%s:edit' % NAMESPACE), params])
+ port = self.ports.get(id=fip.id.split('_')[0])
+ internal_ip = '%s_%s' % (port.id, port.fixed_ips[0]['ip_address'])
+
+ post_params = {
+ 'portforwading_id': pfw_id,
+ 'floating_ip_id': fip_id,
+ 'description': 'test',
+ 'internal_port': '10',
+ 'protocol': 'TCP',
+ 'internal_ip_address': internal_ip,
+ 'external_port': '123',
+ }
+ expected_params = {
+ 'portforwarding_id': pfw_id,
+ 'description': 'test',
+ 'internal_port': '10',
+ 'protocol': 'TCP',
+ 'internal_port_id': internal_ip.split('_')[0],
+ 'internal_ip_address': internal_ip.split('_')[1],
+ 'external_port': '123',
+ }
+ self.client.post(url, post_params)
+ update_mock.assert_called_once_with(mock.ANY, fip_id,
+ **expected_params)
+
+ @test.create_mocks({api.neutron: ('tenant_floating_ip_get',
+ 'floating_ip_port_forwarding_list',
+ 'floating_ip_port_forwarding_delete')})
+ def test_delete_floating_ip_portforwarding(self):
+ fip = self._get_fip_targets()[0]
+ self.mock_tenant_floating_ip_get.return_value = fip
+ fip_id = fip.id
+ self.mock_floating_ip_port_forwarding_list.return_value = (
+ fip.port_forwardings)
+ deletion_mock = self.mock_floating_ip_port_forwarding_delete
+ pf_id = fip.port_forwardings[0].id
+ params = urlencode({'floating_ip_id': fip_id})
+ url = '?'.join([reverse('%s:show' % NAMESPACE), params])
+ self.client.post(url, {
+ 'action': 'floating_ip_portforwardings__delete__%s' % pf_id})
+ deletion_mock.assert_called_once_with(mock.ANY, fip_id, pf_id)
diff --git a/openstack_dashboard/dashboards/project/floating_ip_portforwardings/urls.py b/openstack_dashboard/dashboards/project/floating_ip_portforwardings/urls.py
new file mode 100644
index 000000000..6905e4a9a
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/floating_ip_portforwardings/urls.py
@@ -0,0 +1,24 @@
+# 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 django.urls import re_path
+
+from openstack_dashboard.dashboards.project.floating_ip_portforwardings import (
+ views)
+
+urlpatterns = [
+ re_path(r'^$', views.AllRulesView.as_view(), name='index'),
+ re_path(r'^show$', views.IndexView.as_view(), name='show'),
+ re_path(r'^create/$', views.CreateView.as_view(), name='create'),
+ re_path(r'^edit/$', views.EditView.as_view(), name='edit'),
+ re_path(r'^editToAll/$', views.EditToAllView.as_view(), name='editToAll')
+]
diff --git a/openstack_dashboard/dashboards/project/floating_ip_portforwardings/views.py b/openstack_dashboard/dashboards/project/floating_ip_portforwardings/views.py
new file mode 100644
index 000000000..dc4a64eb1
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/floating_ip_portforwardings/views.py
@@ -0,0 +1,110 @@
+# 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.
+
+"""
+Views for managing floating IPs port forwardings
+"""
+import logging
+
+from django.utils.translation import gettext_lazy as _
+
+from neutronclient.common import exceptions as neutron_exc
+
+from horizon import exceptions
+from horizon import tables
+from horizon import workflows
+
+from openstack_dashboard import api
+from openstack_dashboard.dashboards.project.floating_ip_portforwardings import (
+ tables as project_tables)
+from openstack_dashboard.dashboards.project.floating_ip_portforwardings import (
+ workflows as project_workflows)
+
+LOG = logging.getLogger(__name__)
+
+
+class CreateView(workflows.WorkflowView):
+ workflow_class = (
+ project_workflows.FloatingIpPortForwardingRuleCreationWorkflow)
+
+
+class EditView(workflows.WorkflowView):
+ workflow_class = project_workflows.FloatingIpPortForwardingRuleEditWorkflow
+
+
+class EditToAllView(workflows.WorkflowView):
+ workflow_class = (
+ project_workflows.FloatingIpPortForwardingRuleEditWorkflowToAll)
+
+
+class IndexView(tables.DataTableView):
+ table_class = project_tables.FloatingIpPortForwardingRulesTable
+ page_title = _("Manage floating IP port forwarding rules")
+
+ def get_data(self):
+ try:
+ floating_ip_id = self.request.GET.get('floating_ip_id')
+ floating_ip = api.neutron.tenant_floating_ip_get(self.request,
+ floating_ip_id)
+ self.page_title = _(
+ "Manage floating IP port forwarding rules : " + str(
+ floating_ip.ip))
+ return self.get_floating_ip_rules(floating_ip)
+ except neutron_exc.ConnectionFailed:
+ exceptions.handle(self.request)
+ except Exception:
+ exceptions.handle(
+ self.request,
+ _('Unable to retrieve floating IP port forwarding rules.'))
+ return []
+
+ def get_floating_ip_rules(self, floating_ip):
+ if floating_ip.port_id:
+ return []
+
+ floating_ip_portforwarding_rules = []
+ external_ip_address = floating_ip.ip
+ floating_ip_id = floating_ip.id
+ port_forwarding_rules = api.neutron.floating_ip_port_forwarding_list(
+ self.request, floating_ip_id)
+
+ for port_forwarding_rule in port_forwarding_rules:
+ setattr(port_forwarding_rule, 'external_ip_address',
+ external_ip_address)
+
+ floating_ip_portforwarding_rules.extend(port_forwarding_rules)
+
+ return floating_ip_portforwarding_rules
+
+
+class AllRulesView(IndexView):
+ table_class = project_tables.AllFloatingIpPortForwardingRulesTable
+
+ def get_data(self):
+ try:
+ return self.get_all_floating_ip_rules()
+ except neutron_exc.ConnectionFailed:
+ exceptions.handle(self.request)
+ except Exception:
+ exceptions.handle(
+ self.request,
+ _('Unable to retrieve floating IP port forwarding rules.'))
+ return []
+
+ def get_all_floating_ip_rules(self):
+ floating_ip_portforwarding_rules = []
+ floating_ips = api.neutron.tenant_floating_ip_list(self.request)
+ for floating_ip in floating_ips:
+ floating_ip_portforwarding_rules.extend(
+ self.get_floating_ip_rules(floating_ip))
+
+ return floating_ip_portforwarding_rules
diff --git a/openstack_dashboard/dashboards/project/floating_ip_portforwardings/workflows.py b/openstack_dashboard/dashboards/project/floating_ip_portforwardings/workflows.py
new file mode 100644
index 000000000..11c7b1b65
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/floating_ip_portforwardings/workflows.py
@@ -0,0 +1,270 @@
+# 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 logging
+
+from django.core.exceptions import ValidationError
+from django.urls import reverse
+from django.utils.translation import gettext_lazy as _
+
+from neutronclient.common import exceptions as neutron_exc
+
+from horizon import exceptions
+from horizon import forms
+from horizon import workflows
+
+from openstack_dashboard import api
+from openstack_dashboard.dashboards.project.floating_ip_portforwardings import (
+ tables as project_tables)
+
+LOG = logging.getLogger(__name__)
+
+
+class CommonMetaData(object):
+ name = _("Description")
+ help_text = _(
+ "Description:"
+ ""
+ "IP floating rules define external specific traffic that is bound "
+ "from a public IP to an internal address of a specific port.\n"
+ "Protocol: The protocol configured for the IP forwarding rule. "
+ "You can choose between TCP and UDP.\n"
+ "External port: The external port of the floating IP that"
+ " will be "
+ "bound to the internal port in the internal address. This field"
+ " allow values "
+ "between 1 and 65535 and also support ranges using the following"
+ " format:\n"
+ "InitialPort:FinalPort where InitialPort <= FinalPort.\n"
+ "Internal port: The internal port of the given internal IP "
+ "address that will be bound to the port that is exposed to the "
+ "internet via the public floating IP. This field allow values "
+ "between 1 and 65535 and also support ranges using the following"
+ " format:\n"
+ "InitialPort:FinalPort where InitialPort <= FinalPort.\n"
+ "Internal IP address: The internal IP address where the "
+ "internal ports will be running.\n"
+ "Description: Describes the reason why this rule is being "
+ "created.")
+
+
+class CreateFloatingIpPortForwardingRuleAction(workflows.Action):
+ protocol = forms.ThemableChoiceField(
+ required=True,
+ choices=project_tables.PROTOCOL_CHOICES,
+ label=_("Protocol"))
+ external_port = forms.CharField(max_length=11, label=_("External port"))
+ internal_port = forms.CharField(max_length=11, label=_("Internal port"))
+ internal_ip_address = forms.ThemableChoiceField(required=True, label=_(
+ "Internal IP address"))
+ description = forms.CharField(required=False, widget=forms.Textarea,
+ max_length=255, label=_("Description"))
+ floating_ip_id = forms.CharField(max_length=255,
+ widget=forms.HiddenInput())
+
+ class Meta(CommonMetaData):
+ pass
+
+ def ignore_validation(self, portforward=None):
+ return False
+
+ def validate_input_selects(self):
+ err_msg = "You must select a%s"
+ internal_ip_address = self.cleaned_data.get('internal_ip_address')
+ protocol = self.cleaned_data.get('protocol')
+
+ if protocol == "Select a protocol":
+ raise ValidationError(message=err_msg % " Protocol.")
+
+ if internal_ip_address in ('Select an IP-Address',
+ 'No ports available'):
+ raise ValidationError(message=err_msg % "n Ip-Address.")
+
+ def clean(self):
+ request = self.request
+ if request.method == "GET":
+ return self.cleaned_data
+
+ self.validate_input_selects()
+
+ return self.cleaned_data
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ floating_ip_id = self.request.GET.get('floating_ip_id')
+ self.initial['floating_ip_id'] = floating_ip_id
+
+ def populate_internal_ip_address_choices(self, request, context):
+ targets = api.neutron.floating_ip_target_list(self.request)
+ instances = sorted([(target.id, target.name) for target in targets],
+ key=lambda x: x[1])
+ if instances:
+ instances.insert(0, ("Select an IP-Address", _(
+ "Select an IP-Address")))
+ else:
+ instances = (("No ports available", _(
+ "No ports available")),)
+ return instances
+
+
+class EditFloatingIpPortForwardingRuleAction(
+ CreateFloatingIpPortForwardingRuleAction):
+ portforwading_id = forms.CharField(max_length=255,
+ widget=forms.HiddenInput())
+ instance_id = None
+
+ class Meta(CommonMetaData):
+ pass
+
+ def ignore_validation(self, portforward=None):
+ return (super().ignore_validation(portforward) or
+ portforward.id == self.cleaned_data.get(
+ 'portforwading_id'))
+
+ def __init__(self, *args, **kwargs):
+ request = args[0]
+ if request.method == 'POST':
+ super().__init__(
+ *args, **kwargs)
+ else:
+ floating_ip_id = request.GET.get('floating_ip_id')
+ port_forwarding_id = request.GET.get('pfwd_id')
+ port_forwarding = api.neutron.floating_ip_port_forwarding_get(
+ request, floating_ip_id, port_forwarding_id)
+ port_forwarding_rule = port_forwarding['port_forwarding']
+ self.instance_id = "%s_%s" % (
+ port_forwarding_rule['internal_port_id'],
+ port_forwarding_rule['internal_ip_address'])
+ super().__init__(
+ *args, **kwargs)
+ self.initial['portforwading_id'] = port_forwarding_id
+ self.initial['protocol'] = str(
+ port_forwarding_rule['protocol']).upper()
+ self.initial['internal_port'] = port_forwarding_rule[
+ 'internal_port_range']
+ self.initial['external_port'] = port_forwarding_rule[
+ 'external_port_range']
+ if 'description' in port_forwarding_rule.keys():
+ self.initial['description'] = port_forwarding_rule[
+ 'description']
+
+ def populate_internal_ip_address_choices(self, request, context):
+ targets = api.neutron.floating_ip_target_list(self.request)
+ instances = sorted([(target.id, target.name) for target in targets],
+ key=lambda x: '0'
+ if x[0] == self.instance_id else x[1])
+ return instances
+
+
+class CreateFloatingIpPortForwardingRule(workflows.Step):
+ action_class = CreateFloatingIpPortForwardingRuleAction
+ contributes = ("internal_port", "protocol", "external_port",
+ "internal_ip_address", "description", "floating_ip_id",
+ "portforwading_id")
+
+ def contribute(self, data, context):
+ context = super().contribute(data, context)
+ return context
+
+
+class EditFloatingIpPortForwardingRule(
+ CreateFloatingIpPortForwardingRule):
+ action_class = EditFloatingIpPortForwardingRuleAction
+
+ def contribute(self, data, context):
+ context = super().contribute(data, context)
+ return context
+
+
+class FloatingIpPortForwardingRuleCreationWorkflow(workflows.Workflow):
+ slug = "floating_ip_port_forwarding_rule_creation"
+ name = _("Add floating IP port forwarding rule")
+ finalize_button_name = _("Add")
+ success_message = _('Floating IP port forwarding rule %s created. '
+ 'It might take a few minutes to apply all rules.')
+ failure_message = _('Unable to create floating IP port forwarding rule'
+ ' %s.')
+ success_url = "horizon:project:floating_ip_portforwardings:show"
+ default_steps = (CreateFloatingIpPortForwardingRule,)
+
+ def format_status_message(self, message):
+ if "%s" in message:
+ return message % self.context.get('ip_address',
+ _('unknown IP address'))
+ return message
+
+ def handle_using_api_method(self, request, data, api_method,
+ **api_params):
+ try:
+ floating_ip_id = data['floating_ip_id']
+ self.success_url = reverse(
+ self.success_url) + "?floating_ip_id=" + str(
+ floating_ip_id)
+ port_id, internal_ip = data['internal_ip_address'].split('_')
+ self.context['ip_address'] = internal_ip
+ param = {}
+ if data['description']:
+ param['description'] = data['description']
+ if data['internal_port']:
+ param['internal_port'] = data['internal_port']
+ if data['external_port']:
+ param['external_port'] = data['external_port']
+ if internal_ip:
+ param['internal_ip_address'] = internal_ip
+ if data['protocol']:
+ param['protocol'] = data['protocol']
+ if port_id:
+ param['internal_port_id'] = port_id
+
+ param.update(**api_params)
+ api_method(request, floating_ip_id, **param)
+
+ except neutron_exc.Conflict as ex:
+ msg = _('The requested instance port is already'
+ ' associated with another floating IP.')
+ LOG.exception(msg, ex)
+ exceptions.handle(request, msg)
+ self.failure_message = msg
+ return False
+
+ except Exception:
+ exceptions.handle(request)
+ return False
+ return True
+
+ def handle(self, request, data):
+ return self.handle_using_api_method(
+ request, data, api.neutron.floating_ip_port_forwarding_create)
+
+
+class FloatingIpPortForwardingRuleEditWorkflow(
+ FloatingIpPortForwardingRuleCreationWorkflow):
+ slug = "floating_ip_port_forwarding_rule_edit"
+ name = _("Edit floating IP port forwarding rule")
+ finalize_button_name = _("Update")
+ success_message = _('Floating IP port forwarding rule %s updated. '
+ 'It might take a few minutes to apply all rules.')
+ failure_message = _('Unable to updated floating IP port forwarding'
+ ' rule %s.')
+ success_url = "horizon:project:floating_ip_portforwardings:show"
+ default_steps = (EditFloatingIpPortForwardingRule,)
+
+ def handle(self, request, data):
+ return self.handle_using_api_method(
+ request, data, api.neutron.floating_ip_port_forwarding_update,
+ portforwarding_id=data['portforwading_id'])
+
+
+class FloatingIpPortForwardingRuleEditWorkflowToAll(
+ FloatingIpPortForwardingRuleEditWorkflow):
+ slug = "floating_ip_port_forwarding_rule_edit_all"
+ success_url = "horizon:project:floating_ip_portforwardings:index"
diff --git a/openstack_dashboard/dashboards/project/floating_ips/tables.py b/openstack_dashboard/dashboards/project/floating_ips/tables.py
index 1fa3d579e..dbaedc08c 100644
--- a/openstack_dashboard/dashboards/project/floating_ips/tables.py
+++ b/openstack_dashboard/dashboards/project/floating_ips/tables.py
@@ -90,12 +90,60 @@ class ReleaseIPs(tables.BatchAction):
def allowed(self, request, fip=None):
policy_rules = (("network", "delete_floatingip"),)
- return policy.check(policy_rules, request)
+
+ port_forwarding_occurrence = 0
+
+ if fip:
+ pwds = fip.port_forwardings
+ port_forwarding_occurrence = len(pwds)
+
+ return port_forwarding_occurrence == 0 and policy.check(policy_rules,
+ request)
def action(self, request, obj_id):
api.neutron.tenant_floating_ip_release(request, obj_id)
+class ReleaseIPsPortForwarding(ReleaseIPs):
+ name = "release_floating_ip_portforwarding_rule"
+ help_text = _(
+ "This floating IP has port forwarding rules configured to it."
+ " Therefore,"
+ " you will need to remove all of these rules before being able"
+ " to release it.")
+
+ def __init__(self, **kwargs):
+ attributes = {"title": "Release Floating IP with port forwarding rules",
+ "confirm-button-text": "Edit floating IP port"
+ " forwarding rules"}
+ super().__init__(attrs=attributes, **kwargs)
+
+ @staticmethod
+ def action_past(count):
+ return ngettext_lazy(
+ u"Successfully redirected",
+ u"Successfully redirected",
+ count
+ )
+
+ def allowed(self, request, fip=None):
+
+ policy_rules = (("network", "delete_floatingip_port_forwarding"),)
+ pwds = fip.port_forwardings
+ return (
+ len(pwds) > 0 and
+ policy.check(policy_rules, request) and
+ api.neutron.is_extension_floating_ip_port_forwarding_supported(
+ request)
+ )
+
+ def action(self, request, obj_id):
+ self.success_url = reverse(
+ 'horizon:project:floating_ip_portforwardings:show') \
+ + '?floating_ip_id=' \
+ + str(obj_id)
+
+
class AssociateIP(tables.LinkAction):
name = "associate"
verbose_name = _("Associate")
@@ -105,7 +153,9 @@ class AssociateIP(tables.LinkAction):
def allowed(self, request, fip):
policy_rules = (("network", "update_floatingip"),)
- return not fip.port_id and policy.check(policy_rules, request)
+ pwds = fip.port_forwardings
+ return len(pwds) == 0 and not fip.port_id and policy.check(policy_rules,
+ request)
def get_link_url(self, datum):
base_url = reverse(self.url)
@@ -113,6 +163,59 @@ class AssociateIP(tables.LinkAction):
return "?".join([base_url, params])
+class ListAllFloatingIpPortForwardingRules(tables.LinkAction):
+ name = "List floating_ip_portforwardings_rules"
+ verbose_name = _("List all floating IP port forwarding rules")
+ url = "horizon:project:floating_ip_portforwardings:index"
+ classes = ("btn-edit",)
+ icon = "link"
+
+ def exists_floating_ip_with_port_forwarding_rules_configurable(self,
+ request):
+ floating_ips = api.neutron.tenant_floating_ip_list(request)
+ for floating_ip in floating_ips:
+ if not floating_ip.port_id:
+ return True
+
+ return False
+
+ def allowed(self, request, fip):
+ policy_rules = (("network", "get_floatingip_port_forwarding"),)
+ return (self.exists_floating_ip_with_port_forwarding_rules_configurable(
+ request) and policy.check(policy_rules, request) and
+ api.neutron.is_extension_floating_ip_port_forwarding_supported(
+ request))
+
+
+class ConfigureFloatingIpPortForwarding(tables.Action):
+ name = "configure_floating_ip_portforwarding_rules"
+ verbose_name = _("Configure floating IP port forwarding rules")
+ classes = ("btn-edit",)
+ icon = "link"
+
+ def allowed(self, request, fip):
+ policy_rules = (("network", "get_floatingip_port_forwarding"),)
+ return (
+ not fip.port_id and
+ policy.check(policy_rules, request) and
+ api.neutron.is_extension_floating_ip_port_forwarding_supported(
+ request)
+ )
+
+ def single(self, table, request, obj_id):
+ fip = {}
+ try:
+ fip = table.get_object_by_id(filters.get_int_or_uuid(obj_id))
+ except Exception as ex:
+ err_msg = 'Unable to find a floating IP.'
+ LOG.debug(err_msg, ex)
+ exceptions.handle(request,
+ _('Unable to find a floating IP.'))
+ return shortcuts.redirect(
+ reverse('horizon:project:floating_ip_portforwardings:show') +
+ '?floating_ip_id=' + str(fip.id))
+
+
class DisassociateIP(tables.Action):
name = "disassociate"
verbose_name = _("Disassociate")
@@ -163,7 +266,6 @@ STATUS_DISPLAY_CHOICES = (
("error", pgettext_lazy("Current status of a Floating IP", "Error")),
)
-
FLOATING_IPS_FILTER_CHOICES = (
('floating_ip_address', _('Floating IP Address ='), True),
('network_id', _('Network ID ='), True),
@@ -224,5 +326,9 @@ class FloatingIPsTable(tables.DataTable):
class Meta(object):
name = "floating_ips"
verbose_name = _("Floating IPs")
- table_actions = (AllocateIP, ReleaseIPs, FloatingIPsFilterAction)
- row_actions = (AssociateIP, DisassociateIP, ReleaseIPs)
+ table_actions = (
+ ListAllFloatingIpPortForwardingRules, AllocateIP, ReleaseIPs,
+ FloatingIPsFilterAction)
+ row_actions = (AssociateIP, DisassociateIP, ReleaseIPs,
+ ReleaseIPsPortForwarding,
+ ConfigureFloatingIpPortForwarding)
diff --git a/openstack_dashboard/dashboards/project/floating_ips/tests.py b/openstack_dashboard/dashboards/project/floating_ips/tests.py
index 8c94c1f1f..ecaff9048 100644
--- a/openstack_dashboard/dashboards/project/floating_ips/tests.py
+++ b/openstack_dashboard/dashboards/project/floating_ips/tests.py
@@ -35,6 +35,13 @@ NAMESPACE = "horizon:project:floating_ips"
class FloatingIpViewTests(test.TestCase):
+ def setUp(self):
+ super().setUp()
+ api_mock = mock.patch.object(
+ api.neutron,
+ 'is_extension_floating_ip_port_forwarding_supported').start()
+ api_mock.return_value = True
+
@test.create_mocks({api.neutron: ('floating_ip_target_list',
'tenant_floating_ip_list')})
def test_associate(self):
@@ -42,7 +49,6 @@ class FloatingIpViewTests(test.TestCase):
self._get_fip_targets()
self.mock_tenant_floating_ip_list.return_value = \
self.floating_ips.list()
-
url = reverse('%s:associate' % NAMESPACE)
res = self.client.get(url)
self.assertTemplateUsed(res, views.WorkflowView.template_name)
@@ -91,6 +97,7 @@ class FloatingIpViewTests(test.TestCase):
for ip in p.fixed_ips:
targets.append(api.neutron.FloatingIpTarget(
p, ip['ip_address'], server_dict.get(p.device_id)))
+ targets[-1].port_forwardings = []
return targets
@staticmethod
@@ -213,12 +220,14 @@ class FloatingIpViewTests(test.TestCase):
@test.create_mocks({api.nova: ('server_list',),
api.neutron: ('floating_ip_disassociate',
'floating_ip_pools_list',
+ 'floating_ip_port_forwarding_list',
'is_extension_supported',
'tenant_floating_ip_list')})
def test_disassociate_post(self):
floating_ip = self.floating_ips.first()
self.mock_is_extension_supported.return_value = False
+ self.mock_floating_ip_port_forwarding_list.return_value = []
self.mock_server_list.return_value = [self.servers.list(), False]
self.mock_tenant_floating_ip_list.return_value = \
self.floating_ips.list()
@@ -243,6 +252,7 @@ class FloatingIpViewTests(test.TestCase):
@test.create_mocks({api.nova: ('server_list',),
api.neutron: ('floating_ip_disassociate',
+ 'floating_ip_port_forwarding_list',
'floating_ip_pools_list',
'is_extension_supported',
'tenant_floating_ip_list')})
@@ -250,6 +260,7 @@ class FloatingIpViewTests(test.TestCase):
floating_ip = self.floating_ips.first()
self.mock_is_extension_supported.return_value = False
+ self.mock_floating_ip_port_forwarding_list.return_value = []
self.mock_server_list.return_value = [self.servers.list(), False]
self.mock_tenant_floating_ip_list.return_value = \
self.floating_ips.list()
@@ -273,6 +284,7 @@ class FloatingIpViewTests(test.TestCase):
@test.create_mocks({api.neutron: ('tenant_floating_ip_list',
'is_extension_supported',
+ 'floating_ip_port_forwarding_list',
'floating_ip_pools_list'),
api.nova: ('server_list',),
quotas: ('tenant_quota_usages',)})
@@ -283,6 +295,7 @@ class FloatingIpViewTests(test.TestCase):
self.mock_is_extension_supported.return_value = False
self.mock_tenant_floating_ip_list.return_value = floating_ips
+ self.mock_floating_ip_port_forwarding_list.return_value = []
self.mock_floating_ip_pools_list.return_value = floating_pools
self.mock_server_list.return_value = [self.servers.list(), False]
self.mock_tenant_quota_usages.return_value = quota_data
@@ -298,9 +311,9 @@ class FloatingIpViewTests(test.TestCase):
url = 'horizon:project:floating_ips:allocate'
self.assertEqual(url, allocate_action.url)
- self.mock_tenant_floating_ip_list.assert_called_once_with(
+ self.mock_tenant_floating_ip_list.assert_called_with(
test.IsHttpRequest())
- self.mock_floating_ip_pools_list.assert_called_once_with(
+ self.mock_floating_ip_pools_list.assert_called_with(
test.IsHttpRequest())
self.mock_server_list.assert_called_once_with(test.IsHttpRequest(),
detailed=False)
@@ -313,6 +326,7 @@ class FloatingIpViewTests(test.TestCase):
@test.create_mocks({api.neutron: ('tenant_floating_ip_list',
'is_extension_supported',
+ 'floating_ip_port_forwarding_list',
'floating_ip_pools_list'),
api.nova: ('server_list',),
quotas: ('tenant_quota_usages',)})
@@ -324,6 +338,7 @@ class FloatingIpViewTests(test.TestCase):
self.mock_is_extension_supported.return_value = False
self.mock_tenant_floating_ip_list.return_value = floating_ips
+ self.mock_floating_ip_port_forwarding_list.return_value = []
self.mock_floating_ip_pools_list.return_value = floating_pools
self.mock_server_list.return_value = [self.servers.list(), False]
self.mock_tenant_quota_usages.return_value = quota_data
@@ -337,9 +352,9 @@ class FloatingIpViewTests(test.TestCase):
self.assertEqual('Allocate IP To Project (Quota exceeded)',
allocate_action.verbose_name)
- self.mock_tenant_floating_ip_list.assert_called_once_with(
+ self.mock_tenant_floating_ip_list.assert_called_with(
test.IsHttpRequest())
- self.mock_floating_ip_pools_list.assert_called_once_with(
+ self.mock_floating_ip_pools_list.assert_called_with(
test.IsHttpRequest())
self.mock_server_list.assert_called_once_with(test.IsHttpRequest(),
detailed=False)
diff --git a/openstack_dashboard/dashboards/project/floating_ips/views.py b/openstack_dashboard/dashboards/project/floating_ips/views.py
index 879d4c5b5..e211181cd 100644
--- a/openstack_dashboard/dashboards/project/floating_ips/views.py
+++ b/openstack_dashboard/dashboards/project/floating_ips/views.py
@@ -20,6 +20,7 @@
"""
Views for managing floating IPs.
"""
+import logging
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
@@ -41,6 +42,8 @@ from openstack_dashboard.dashboards.project.floating_ips \
from openstack_dashboard.dashboards.project.floating_ips \
import workflows as project_workflows
+LOG = logging.getLogger(__name__)
+
class AssociateView(workflows.WorkflowView):
workflow_class = project_workflows.IPAssociationWorkflow
@@ -129,8 +132,20 @@ class IndexView(tables.DataTableView):
instances_dict = dict((obj.id, obj.name) for obj in instances)
+ fip_pfw_enabled = (
+ api.neutron.is_extension_floating_ip_port_forwarding_supported(
+ self.request))
+
for ip in floating_ips:
ip.instance_name = instances_dict.get(ip.instance_id)
ip.pool_name = pool_dict.get(ip.pool, ip.pool)
+ if fip_pfw_enabled:
+ try:
+ pfws = api.neutron.floating_ip_port_forwarding_list(
+ self.request, ip.id)
+ ip.port_forwardings = pfws
+ except Exception as e:
+ LOG.info("Error fetching port forwardings for floating IP"
+ " %s: %s", ip.id, e)
return floating_ips
diff --git a/openstack_dashboard/dashboards/project/floating_ips/workflows.py b/openstack_dashboard/dashboards/project/floating_ips/workflows.py
index 5defa54d1..dcc13fcdb 100644
--- a/openstack_dashboard/dashboards/project/floating_ips/workflows.py
+++ b/openstack_dashboard/dashboards/project/floating_ips/workflows.py
@@ -94,7 +94,8 @@ class AssociateIPAction(workflows.Action):
exceptions.handle(self.request,
_('Unable to retrieve floating IP addresses.'),
redirect=redirect)
- options = sorted([(ip.id, ip.ip) for ip in ips if not ip.port_id])
+ options = sorted([(ip.id, ip.ip) for ip in ips if
+ not ip.port_id and len(ip.port_forwardings) == 0])
if options:
options.insert(0, ("", _("Select an IP address")))
else:
@@ -124,7 +125,7 @@ class AssociateIPAction(workflows.Action):
# The reason of specifying an empty tuple when q_instance_id is None
# is to make memoized_method _get_target_list work. Two calls of
# _get_target_list from here and __init__ must have a same arguments.
- params = (q_instance_id, ) if q_instance_id else ()
+ params = (q_instance_id,) if q_instance_id else ()
targets = self._get_target_list(*params)
instances = sorted([(target.id, target.name) for target in targets],
# Sort FIP targets by server name for easy browsing
diff --git a/openstack_dashboard/enabled/_1520_project_floating_ip_portforwardings_panel.py b/openstack_dashboard/enabled/_1520_project_floating_ip_portforwardings_panel.py
new file mode 100644
index 000000000..fc3c08916
--- /dev/null
+++ b/openstack_dashboard/enabled/_1520_project_floating_ip_portforwardings_panel.py
@@ -0,0 +1,7 @@
+PANEL_DASHBOARD = 'project'
+PANEL_GROUP = 'network'
+PANEL = 'floating_ip_portforwardings'
+
+ADD_PANEL = \
+ 'openstack_dashboard.dashboards.project.floating_ip_portforwardings.panel' \
+ '.FloatingIpPortforwardingRules'
diff --git a/openstack_dashboard/test/test_data/neutron_data.py b/openstack_dashboard/test/test_data/neutron_data.py
index 78bd93df3..024ec79bc 100644
--- a/openstack_dashboard/test/test_data/neutron_data.py
+++ b/openstack_dashboard/test/test_data/neutron_data.py
@@ -34,6 +34,7 @@ def data(TEST):
TEST.routers_with_rules = utils.TestDataContainer()
TEST.routers_with_routes = utils.TestDataContainer()
TEST.floating_ips = utils.TestDataContainer()
+ TEST.port_forwardings = utils.TestDataContainer()
TEST.security_groups = utils.TestDataContainer()
TEST.security_group_rules = utils.TestDataContainer()
TEST.providers = utils.TestDataContainer()
@@ -63,6 +64,7 @@ def data(TEST):
TEST.api_routers = utils.TestDataContainer()
TEST.api_routers_with_routes = utils.TestDataContainer()
TEST.api_floating_ips = utils.TestDataContainer()
+ TEST.api_port_forwardings = utils.TestDataContainer()
TEST.api_security_groups = utils.TestDataContainer()
TEST.api_security_group_rules = utils.TestDataContainer()
TEST.api_pools = utils.TestDataContainer()
@@ -647,6 +649,7 @@ def data(TEST):
'id': '9012cd70-cfae-4e46-b71e-6a409e9e0063',
'fixed_ip_address': None,
'port_id': None,
+ 'port_forwardings': [],
'router_id': None}
TEST.api_floating_ips.add(fip_dict)
fip_with_instance = copy.deepcopy(fip_dict)
@@ -659,6 +662,7 @@ def data(TEST):
'floating_ip_address': '172.16.88.228',
'floating_network_id': ext_net['id'],
'id': 'a97af8f2-3149-4b97-abbd-e49ad19510f7',
+ 'port_forwardings': [],
'fixed_ip_address': assoc_port['fixed_ips'][0]['ip_address'],
'port_id': assoc_port['id'],
'router_id': router_dict['id']}
@@ -668,6 +672,46 @@ def data(TEST):
'instance_type': 'compute'})
TEST.floating_ips.add(neutron.FloatingIp(fip_with_instance))
+ # port forwardings
+
+ TEST.api_port_forwardings.add({
+ "protocol": "tcp",
+ "internal_ip_address": "10.0.0.11",
+ "internal_port": 25,
+ "internal_port_id": "1238be08-a2a8-4b8d-addf-fb5e2250e480",
+ "external_port": 2230,
+ "internal_port_range": "25:25",
+ "external_port_range": "2230:2230",
+ "description": "",
+ "id": "e0a0274e-4d19-4eab-9e12-9e77a8caf3ea"
+ })
+ TEST.api_port_forwardings.add({
+ "protocol": "tcp",
+ "internal_port": 80,
+ "external_port": 8080,
+ "internal_ip_address": "10.0.0.12",
+ "internal_port_range": "80:90",
+ "internal_port_id": "2057ec54-8be2-11eb-8dcd-0242ac130003",
+ "external_port_range": "8080:8090",
+ "description": "using port ranges",
+ "id": "0f23a90a-8be2-11eb-8dcd-0242ac130003"
+ })
+ TEST.api_port_forwardings.add({
+ "protocol": "tcp",
+ "internal_ip_address": "10.0.0.24",
+ "internal_port": 25,
+ "internal_port_id": "070ef0b2-0175-4299-be5c-01fea8cca522",
+ "external_port": 2229,
+ "internal_port_range": "25:25",
+ "external_port_range": "2229:2229",
+ "description": "Some description",
+ "id": "1798dc82-c0ed-4b79-b12d-4c3c18f90eb2"
+ })
+
+ TEST.port_forwardings.add(neutron.PortForwarding(
+ TEST.api_port_forwardings.first(), fip_dict['id']
+ ))
+
# Security group.
sec_group_1 = {'tenant_id': '1',
diff --git a/openstack_dashboard/test/unit/api/test_neutron.py b/openstack_dashboard/test/unit/api/test_neutron.py
index 19bde8df1..4d9d1e8d0 100644
--- a/openstack_dashboard/test/unit/api/test_neutron.py
+++ b/openstack_dashboard/test/unit/api/test_neutron.py
@@ -2321,6 +2321,81 @@ class NeutronApiSecurityGroupTests(test.APIMockTestCase):
self.qclient.update_port.assert_has_calls(expected_calls)
+class NeutronApiFloatingIpPortForwardingTest(test.APIMockTestCase):
+ def setUp(self):
+ super().setUp()
+ neutronclient = mock.patch.object(api.neutron, 'neutronclient').start()
+ self.client_mock = neutronclient.return_value
+
+ def test_port_forwarding_list(self):
+ pfws = {'port_forwardings': self.api_port_forwardings.list()}
+ self.client_mock.list_port_forwardings.return_value = pfws
+ response = api.neutron.floating_ip_port_forwarding_list(
+ self.request, 'fip')
+ for i in range(len(response)):
+ resp_val = response[i]
+ expected_val = pfws['port_forwardings'][i]
+ for attr in resp_val.to_dict():
+ self.assertEqual(getattr(resp_val, attr), expected_val[attr])
+
+ self.client_mock.list_port_forwardings.assert_called_once_with('fip')
+
+ def test_port_forwarding_get(self):
+ pfw = self.api_port_forwardings.first()
+ pfw_id = pfw['id']
+ self.client_mock.show_port_forwarding.return_value = pfw
+ response = api.neutron.floating_ip_port_forwarding_get(
+ self.request, 'fip', pfw_id)
+ for attr in response.to_dict():
+ self.assertEqual(getattr(response, attr), pfw[attr])
+ self.client_mock.show_port_forwarding.assert_called_once_with(
+ 'fip', pfw_id)
+
+ def test_port_forwarding_create(self):
+ pfw_resp_mock = {'port_forwarding': self.api_port_forwardings.first()}
+ pfw_expected = self.port_forwardings.get().to_dict()
+ pfw = {
+ "protocol": "tcp",
+ "internal_ip_address": "10.0.0.24",
+ "internal_port": 25,
+ "internal_port_id": "070ef0b2-0175-4299-be5c-01fea8cca522",
+ "external_port": 2229,
+ "description": "Some description",
+ }
+ self.client_mock.create_port_forwarding.return_value = pfw_resp_mock
+ response = api.neutron.floating_ip_port_forwarding_create(
+ self.request, 'fip', **pfw)
+ for attr in response.to_dict():
+ self.assertEqual(getattr(response, attr), pfw_expected[attr])
+ self.client_mock.create_port_forwarding.assert_called_once_with(
+ 'fip', {'port_forwarding': pfw})
+
+ def test_port_forwarding_update(self):
+ pfw_resp_mock = {'port_forwarding': self.api_port_forwardings.first()}
+ pfw_expected = self.port_forwardings.get().to_dict()
+ pfw_id = pfw_resp_mock['port_forwarding']['id']
+ pfw = {
+ "protocol": "tcp",
+ "internal_port": 25,
+ "description": "Some description",
+ }
+ self.client_mock.update_port_forwarding.return_value = pfw_resp_mock
+ response = api.neutron.floating_ip_port_forwarding_update(
+ self.request, 'fip', portforwarding_id=pfw_id, **pfw)
+ for attr in response.to_dict():
+ self.assertEqual(getattr(response, attr), pfw_expected[attr])
+ self.client_mock.update_port_forwarding.assert_called_once_with(
+ 'fip', pfw_id, {'port_forwarding': pfw})
+
+ def test_port_forwarding_delete(self):
+ pfw_id = self.api_port_forwardings.first()['id']
+ self.client_mock.delete_port_forwarding.return_value = None
+ api.neutron.floating_ip_port_forwarding_delete(
+ self.request, 'fip', pfw_id)
+ self.client_mock.delete_port_forwarding.assert_called_once_with(
+ 'fip', pfw_id)
+
+
class NeutronApiFloatingIpTests(test.APIMockTestCase):
def setUp(self):
diff --git a/releasenotes/notes/add-support-to-floating-ip-port-forwardings-3d0d43a2d997ce79.yaml b/releasenotes/notes/add-support-to-floating-ip-port-forwardings-3d0d43a2d997ce79.yaml
new file mode 100644
index 000000000..5b9b97a5c
--- /dev/null
+++ b/releasenotes/notes/add-support-to-floating-ip-port-forwardings-3d0d43a2d997ce79.yaml
@@ -0,0 +1,8 @@
+---
+features:
+ - |
+ Add support to portforwardings in the Network Floating IPs dashboard.
+
+ Requires python-neutronclient >= 8.1.0
+
+ This feature is disabled by default. \ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index b252214c9..758643d20 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -37,7 +37,7 @@ pyScss>=1.4.0 # MIT License
python-cinderclient>=8.0.0 # Apache-2.0
python-glanceclient>=2.8.0 # Apache-2.0
python-keystoneclient>=3.22.0 # Apache-2.0
-python-neutronclient>=6.7.0 # Apache-2.0
+python-neutronclient>=8.1.0 # Apache-2.0
python-novaclient>=9.1.0 # Apache-2.0
python-swiftclient>=3.2.0 # Apache-2.0
pytz>=2013.6 # MIT