diff --git a/openstack_dashboard/api/network.py b/openstack_dashboard/api/network.py index 015bf3f4c6..7389531b43 100644 --- a/openstack_dashboard/api/network.py +++ b/openstack_dashboard/api/network.py @@ -50,16 +50,18 @@ def floating_ip_pools_list(request): return NetworkClient(request).floating_ips.list_pools() -def tenant_floating_ip_list(request): - return NetworkClient(request).floating_ips.list() +def tenant_floating_ip_list(request, all_tenants=False): + return NetworkClient(request).floating_ips.list(all_tenants=all_tenants) def tenant_floating_ip_get(request, floating_ip_id): return NetworkClient(request).floating_ips.get(floating_ip_id) -def tenant_floating_ip_allocate(request, pool=None): - return NetworkClient(request).floating_ips.allocate(pool) +def tenant_floating_ip_allocate(request, pool=None, tenant_id=None, **params): + return NetworkClient(request).floating_ips.allocate(pool, + tenant_id, + **params) def tenant_floating_ip_release(request, floating_ip_id): diff --git a/openstack_dashboard/api/network_base.py b/openstack_dashboard/api/network_base.py index af4a28fd82..f6d4046cde 100644 --- a/openstack_dashboard/api/network_base.py +++ b/openstack_dashboard/api/network_base.py @@ -51,8 +51,8 @@ class FloatingIpManager(object): pass @abc.abstractmethod - def list(self): - """Fetches a list all floating IPs. + def list(self, all_tenants=False): + """Fetches a list of all floating IPs. A returned value is a list of FloatingIp object. """ @@ -67,7 +67,7 @@ class FloatingIpManager(object): pass @abc.abstractmethod - def allocate(self, pool=None): + def allocate(self, pool=None, tenant_id=None, **params): """Allocates a floating IP to the tenant. You must provide a pool name or id for which you would like to diff --git a/openstack_dashboard/api/neutron.py b/openstack_dashboard/api/neutron.py index 9bc0b1065e..ce07242650 100644 --- a/openstack_dashboard/api/neutron.py +++ b/openstack_dashboard/api/neutron.py @@ -417,10 +417,15 @@ class FloatingIpManager(network_base.FloatingIpManager): self._set_instance_info(fip) return FloatingIp(fip) - def allocate(self, pool): - body = {'floatingip': {'floating_network_id': pool, - 'tenant_id': self.request.user.project_id}} - fip = self.client.create_floatingip(body).get('floatingip') + def allocate(self, pool, tenant_id=None, **params): + if not tenant_id: + tenant_id = self.request.user.project_id + create_dict = {'floating_network_id': pool, + 'tenant_id': tenant_id} + if 'floating_ip_address' in params: + create_dict['floating_ip_address'] = params['floating_ip_address'] + fip = self.client.create_floatingip( + {'floatingip': create_dict}).get('floatingip') self._set_instance_info(fip) return FloatingIp(fip) diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py index 47da4d15d4..7ed9e63702 100644 --- a/openstack_dashboard/api/nova.py +++ b/openstack_dashboard/api/nova.py @@ -415,14 +415,16 @@ class FloatingIpManager(network_base.FloatingIpManager): return [FloatingIpPool(pool) for pool in self.client.floating_ip_pools.list()] - def list(self): - return [FloatingIp(fip) - for fip in self.client.floating_ips.list()] + def list(self, all_tenants=False): + return [FloatingIp(fip) for fip in + self.client.floating_ips.list( + all_tenants=all_tenants)] def get(self, floating_ip_id): return FloatingIp(self.client.floating_ips.get(floating_ip_id)) - def allocate(self, pool): + def allocate(self, pool, tenant_id=None, **params): + # NOTE: tenant_id will never be used here. return FloatingIp(self.client.floating_ips.create(pool=pool)) def release(self, floating_ip_id): diff --git a/openstack_dashboard/dashboards/admin/floating_ips/__init__.py b/openstack_dashboard/dashboards/admin/floating_ips/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/dashboards/admin/floating_ips/forms.py b/openstack_dashboard/dashboards/admin/floating_ips/forms.py new file mode 100644 index 0000000000..8729d3f441 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/floating_ips/forms.py @@ -0,0 +1,64 @@ +# Copyright 2016 Letv Cloud Computing +# All Rights Reserved. +# +# 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.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import messages + +from openstack_dashboard import api + + +class AdminFloatingIpAllocate(forms.SelfHandlingForm): + pool = forms.ChoiceField(label=_("Pool")) + tenant = forms.ChoiceField(label=_("Project")) + floating_ip_address = forms.IPField( + label=_("Floating IP Address (optional)"), + required=False, + initial="", + help_text=_("The IP address of the new floating IP (e.g. 202.2.3.4). " + "You need to specify an explicit address which is under " + "the public network CIDR (e.g. 202.2.3.0/24)."), + mask=False) + + def __init__(self, *args, **kwargs): + super(AdminFloatingIpAllocate, self).__init__(*args, **kwargs) + floating_pool_list = kwargs.get('initial', {}).get('pool_list', []) + self.fields['pool'].choices = floating_pool_list + tenant_list = kwargs.get('initial', {}).get('tenant_list', []) + self.fields['tenant'].choices = tenant_list + + def handle(self, request, data): + try: + # Admin ignore quota + param = {} + if data['floating_ip_address']: + param['floating_ip_address'] = data['floating_ip_address'] + # TODO(liuyulong): use subnet id to allocate floating IP. + fip = api.network.tenant_floating_ip_allocate( + request, + pool=data['pool'], + tenant_id=data['tenant'], + **param) + messages.success( + request, + _('Allocated floating IP %(ip)s.') % {"ip": fip.ip}) + return fip + except Exception: + redirect = reverse('horizon:admin:floating_ips:index') + msg = _('Unable to allocate floating IP.') + exceptions.handle(request, msg, redirect=redirect) diff --git a/openstack_dashboard/dashboards/admin/floating_ips/panel.py b/openstack_dashboard/dashboards/admin/floating_ips/panel.py new file mode 100644 index 0000000000..2503c795cd --- /dev/null +++ b/openstack_dashboard/dashboards/admin/floating_ips/panel.py @@ -0,0 +1,30 @@ +# Copyright 2016 Letv Cloud Computing +# All Rights Reserved. +# +# 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.conf import settings +from django.utils.translation import ugettext_lazy as _ + +import horizon + + +class AdminFloatingIps(horizon.Panel): + name = _("Floating IPs") + slug = 'floating_ips' + permissions = ('openstack.services.network', ) + + @staticmethod + def can_register(): + network_config = getattr(settings, 'OPENSTACK_NEUTRON_NETWORK', {}) + return network_config.get('enable_router', True) diff --git a/openstack_dashboard/dashboards/admin/floating_ips/tables.py b/openstack_dashboard/dashboards/admin/floating_ips/tables.py new file mode 100644 index 0000000000..162277f5e2 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/floating_ips/tables.py @@ -0,0 +1,91 @@ +# Copyright 2016 Letv Cloud Computing +# All Rights Reserved. +# +# 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 import shortcuts +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import messages +from horizon import tables + +from openstack_dashboard import api +from openstack_dashboard import policy +from openstack_dashboard.dashboards.project.access_and_security.\ + floating_ips import tables as project_tables +from openstack_dashboard.utils import filters + + +LOG = logging.getLogger(__name__) + + +class FloatingIPFilterAction(tables.FilterAction): + + def filter(self, table, fips, filter_string): + """Naive case-insensitive search.""" + q = filter_string.lower() + return [ip for ip in fips + if q in ip.ip.lower()] + + +class AdminAllocateFloatingIP(project_tables.AllocateIP): + url = "horizon:admin:floating_ips:allocate" + + def single(self, data_table, request, *args): + return shortcuts.redirect('horizon:admin:floating_ips:index') + + def allowed(self, request, fip=None): + policy_rules = (("network", "create_floatingip"),) + return policy.check(policy_rules, request) + + +class AdminReleaseFloatingIP(project_tables.ReleaseIPs): + pass + + +class AdminSimpleDisassociateIP(project_tables.DisassociateIP): + + def single(self, table, request, obj_id): + try: + fip = table.get_object_by_id(filters.get_int_or_uuid(obj_id)) + api.network.floating_ip_disassociate(request, fip.id) + LOG.info('Disassociating Floating IP "%s".' % obj_id) + messages.success(request, + _('Successfully disassociated Floating IP: %s') + % fip.ip) + except Exception: + exceptions.handle(request, + _('Unable to disassociate floating IP.')) + return shortcuts.redirect('horizon:admin:floating_ips:index') + + +class FloatingIPsTable(project_tables.FloatingIPsTable): + tenant = tables.Column("tenant_name", verbose_name=_("Project")) + ip = tables.Column("ip", + link=("horizon:admin:floating_ips:detail"), + verbose_name=_("IP Address"), + attrs={'data-type': "ip"}) + + class Meta(object): + name = "floating_ips" + verbose_name = _("Floating IPs") + status_columns = ["status"] + table_actions = (FloatingIPFilterAction, + AdminAllocateFloatingIP, + AdminReleaseFloatingIP) + row_actions = (AdminSimpleDisassociateIP, + AdminReleaseFloatingIP) + columns = ('tenant', 'ip', 'fixed_ip', 'pool', 'status') diff --git a/openstack_dashboard/dashboards/admin/floating_ips/templates/floating_ips/_allocate.html b/openstack_dashboard/dashboards/admin/floating_ips/templates/floating_ips/_allocate.html new file mode 100644 index 0000000000..3998de5815 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/floating_ips/templates/floating_ips/_allocate.html @@ -0,0 +1,9 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-header %}{% trans "Allocate Floating IP" %}{% endblock %} + +{% block modal-body-right %} + <h3>{% trans "Description:" %}</h3> + <p>{% trans "From here you can allocate a floating IP to a specific project." %}</p> +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/floating_ips/templates/floating_ips/allocate.html b/openstack_dashboard/dashboards/admin/floating_ips/templates/floating_ips/allocate.html new file mode 100644 index 0000000000..fb7f9e628e --- /dev/null +++ b/openstack_dashboard/dashboards/admin/floating_ips/templates/floating_ips/allocate.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Allocate Floating IP" %}{% endblock %} + +{% block main %} + {% include 'admin/floating_ips/_allocate.html' %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/floating_ips/templates/floating_ips/detail.html b/openstack_dashboard/dashboards/admin/floating_ips/templates/floating_ips/detail.html new file mode 100644 index 0000000000..8ea9a7efed --- /dev/null +++ b/openstack_dashboard/dashboards/admin/floating_ips/templates/floating_ips/detail.html @@ -0,0 +1,48 @@ +{% extends 'base.html' %} +{% load i18n sizeformat %} + +{% block title %}{% trans "Floating IP Details"%}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_detail_header.html" %} +{% endblock %} + +{% block main %} +<div class="detail"> + <dl class="dl-horizontal"> + <dt>{% trans "ID" %}</dt> + <dd>{{ floating_ip.id|default:_("None") }}</dd> + + <dt>{% trans "Project ID" %}</dt> + <dd>{{ floating_ip.tenant_id|default:"-" }}</dd> + + <dt>{% trans "Floating IP address" %}</dt> + <dd>{{ floating_ip.ip|default:_("None") }}</dd> + <dt>{% trans "Status" %}</dt> + <dd>{{ floating_ip.status|default:_("None") }}</dd> + + <dt>{% trans "Pool" %}</dt> + {% url 'horizon:admin:networks:detail' floating_ip.pool as network_url %} + <dd><a href="{{ network_url }}">{{ floating_ip.pool_name|default:_("None") }}</a></dd> + + <dt>{% trans "Mapped IP Address" %}</dt> + {% if floating_ip.instance_id and floating_ip.instance_type == 'compute' %} + {% url 'horizon:admin:instances:detail' floating_ip.instance_id as instance_url %} + <dd><a href="{{ instance_url }}">{{ floating_ip.mapped_fixed_ip }}</a></dd> + {% elif floating_ip.port_id and floating_ip.fixed_ip and floating_ip.instance_type != 'compute' %} + {% url 'horizon:admin:networks:ports:detail' floating_ip.port_id as port_url %} + <dd><a href="{{ port_url }}">{{ floating_ip.fixed_ip }}</a></dd> + {% else %} + <dd>{% trans "No associated fixed IP" %}</dd> + {% endif %} + + <dt>{% trans "Router" %}</dt> + {% if floating_ip.router_id %} + {% url 'horizon:admin:routers:detail' floating_ip.router_id as router_url %} + <dd><a href="{{ router_url }}">{{ floating_ip.router_name }}</a></dd> + {% else %} + <dd>{% trans "No router" %}</dd> + {% endif %} + </dl> +</div> +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/floating_ips/templates/floating_ips/index.html b/openstack_dashboard/dashboards/admin/floating_ips/templates/floating_ips/index.html new file mode 100644 index 0000000000..24dbc00d6a --- /dev/null +++ b/openstack_dashboard/dashboards/admin/floating_ips/templates/floating_ips/index.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Floating IPs" %}{% endblock %} + +{% block main %} + {{ table.render }} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/floating_ips/tests.py b/openstack_dashboard/dashboards/admin/floating_ips/tests.py new file mode 100644 index 0000000000..a67b9018c6 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/floating_ips/tests.py @@ -0,0 +1,275 @@ +# Copyright 2016 Letv Cloud Computing +# All Rights Reserved. +# +# 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.core.urlresolvers import reverse +from django import http +from mox3.mox import IsA # noqa + +from openstack_dashboard import api +from openstack_dashboard.test import helpers as test + +INDEX_URL = reverse('horizon:admin:floating_ips:index') + + +class AdminFloatingIpViewTest(test.BaseAdminViewTests): + @test.create_stubs({api.network: ('tenant_floating_ip_list', ), + api.nova: ('server_list', ), + api.keystone: ('tenant_list', ), + api.neutron: ('network_list', )}) + def test_index(self): + # Use neutron test data + fips = self.q_floating_ips.list() + servers = self.servers.list() + tenants = self.tenants.list() + api.network.tenant_floating_ip_list(IsA(http.HttpRequest), + all_tenants=True).AndReturn(fips) + api.nova.server_list(IsA(http.HttpRequest), all_tenants=True) \ + .AndReturn([servers, False]) + api.keystone.tenant_list(IsA(http.HttpRequest))\ + .AndReturn([tenants, False]) + params = {"router:external": True} + api.neutron.network_list(IsA(http.HttpRequest), **params) \ + .AndReturn(self.networks.list()) + + self.mox.ReplayAll() + + res = self.client.get(INDEX_URL) + self.assertTemplateUsed(res, 'admin/floating_ips/index.html') + self.assertIn('floating_ips_table', res.context) + floating_ips_table = res.context['floating_ips_table'] + floating_ips = floating_ips_table.data + self.assertEqual(len(floating_ips), 2) + + row_actions = floating_ips_table.get_row_actions(floating_ips[0]) + self.assertEqual(len(row_actions), 1) + row_actions = floating_ips_table.get_row_actions(floating_ips[1]) + self.assertEqual(len(row_actions), 2) + + @test.create_stubs({api.network: ('tenant_floating_ip_get', ), + api.neutron: ('network_get', )}) + def test_floating_ip_detail_get(self): + fip = self.q_floating_ips.first() + network = self.networks.first() + api.network.tenant_floating_ip_get( + IsA(http.HttpRequest), fip.id).AndReturn(fip) + api.neutron.network_get( + IsA(http.HttpRequest), fip.pool).AndReturn(network) + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:admin:floating_ips:detail', + args=[fip.id])) + self.assertTemplateUsed(res, + 'admin/floating_ips/detail.html') + self.assertEqual(res.context['floating_ip'].ip, fip.ip) + + @test.create_stubs({api.network: ('tenant_floating_ip_get',)}) + def test_floating_ip_detail_exception(self): + fip = self.q_floating_ips.first() + # Only supported by neutron, so raise a neutron exception + api.network.tenant_floating_ip_get( + IsA(http.HttpRequest), + fip.id).AndRaise(self.exceptions.neutron) + + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:admin:floating_ips:detail', + args=[fip.id])) + + self.assertRedirectsNoFollow(res, INDEX_URL) + + @test.create_stubs({api.network: ('tenant_floating_ip_list', )}) + def test_index_no_floating_ips(self): + api.network.tenant_floating_ip_list(IsA(http.HttpRequest), + all_tenants=True).AndReturn([]) + self.mox.ReplayAll() + + res = self.client.get(INDEX_URL) + self.assertTemplateUsed(res, 'admin/floating_ips/index.html') + + @test.create_stubs({api.network: ('tenant_floating_ip_list', )}) + def test_index_error(self): + api.network.tenant_floating_ip_list(IsA(http.HttpRequest), + all_tenants=True) \ + .AndRaise(self.exceptions.neutron) + self.mox.ReplayAll() + + res = self.client.get(INDEX_URL) + self.assertTemplateUsed(res, 'admin/floating_ips/index.html') + + @test.create_stubs({api.neutron: ('network_list',), + api.keystone: ('tenant_list',)}) + def test_admin_allocate_get(self): + pool = self.networks.first() + tenants = self.tenants.list() + + api.keystone.tenant_list(IsA(http.HttpRequest))\ + .AndReturn([tenants, False]) + search_opts = {'router:external': True} + api.neutron.network_list(IsA(http.HttpRequest), **search_opts) \ + .AndReturn([pool]) + + self.mox.ReplayAll() + + url = reverse('horizon:admin:floating_ips:allocate') + res = self.client.get(url) + self.assertTemplateUsed(res, 'admin/floating_ips/allocate.html') + allocate_form = res.context['form'] + + pool_choices = allocate_form.fields['pool'].choices + self.assertEqual(len(pool_choices), 1) + tenant_choices = allocate_form.fields['tenant'].choices + self.assertEqual(len(tenant_choices), 3) + + @test.create_stubs({api.neutron: ('network_list',), + api.keystone: ('tenant_list',)}) + def test_admin_allocate_post_invalid_ip_version(self): + tenant = self.tenants.first() + pool = self.networks.first() + tenants = self.tenants.list() + + api.keystone.tenant_list(IsA(http.HttpRequest))\ + .AndReturn([tenants, False]) + search_opts = {'router:external': True} + api.neutron.network_list(IsA(http.HttpRequest), **search_opts) \ + .AndReturn([pool]) + self.mox.ReplayAll() + + form_data = {'pool': pool.id, + 'tenant': tenant.id, + 'floating_ip_address': 'fc00::1'} + url = reverse('horizon:admin:floating_ips:allocate') + res = self.client.post(url, form_data) + self.assertContains(res, "Invalid version for IP address") + + @test.create_stubs({api.network: ('tenant_floating_ip_allocate',), + api.neutron: ('network_list',), + api.keystone: ('tenant_list',)}) + def test_admin_allocate_post(self): + tenant = self.tenants.first() + floating_ip = self.floating_ips.first() + pool = self.networks.first() + tenants = self.tenants.list() + + api.keystone.tenant_list(IsA(http.HttpRequest))\ + .AndReturn([tenants, False]) + search_opts = {'router:external': True} + api.neutron.network_list(IsA(http.HttpRequest), **search_opts) \ + .AndReturn([pool]) + api.network.tenant_floating_ip_allocate( + IsA(http.HttpRequest), + pool=pool.id, + tenant_id=tenant.id).AndReturn(floating_ip) + self.mox.ReplayAll() + + form_data = {'pool': pool.id, + 'tenant': tenant.id} + url = reverse('horizon:admin:floating_ips:allocate') + res = self.client.post(url, form_data) + self.assertRedirectsNoFollow(res, INDEX_URL) + + @test.create_stubs({api.network: ('tenant_floating_ip_list', + 'floating_ip_disassociate'), + api.nova: ('server_list', ), + api.keystone: ('tenant_list', ), + api.neutron: ('network_list', )}) + def test_admin_disassociate_floatingip(self): + # Use neutron test data + fips = self.q_floating_ips.list() + floating_ip = self.q_floating_ips.list()[1] + servers = self.servers.list() + tenants = self.tenants.list() + api.network.tenant_floating_ip_list(IsA(http.HttpRequest), + all_tenants=True).AndReturn(fips) + api.nova.server_list(IsA(http.HttpRequest), all_tenants=True) \ + .AndReturn([servers, False]) + api.keystone.tenant_list(IsA(http.HttpRequest))\ + .AndReturn([tenants, False]) + params = {"router:external": True} + api.neutron.network_list(IsA(http.HttpRequest), **params) \ + .AndReturn(self.networks.list()) + api.network.floating_ip_disassociate(IsA(http.HttpRequest), + floating_ip.id) + self.mox.ReplayAll() + + form_data = { + "action": + "floating_ips__disassociate__%s" % floating_ip.id} + res = self.client.post(INDEX_URL, form_data) + + self.assertNoFormErrors(res) + + @test.create_stubs({api.network: ('tenant_floating_ip_list', ), + api.nova: ('server_list', ), + api.keystone: ('tenant_list', ), + api.neutron: ('network_list', )}) + def test_admin_delete_floatingip(self): + # Use neutron test data + fips = self.q_floating_ips.list() + floating_ip = self.q_floating_ips.list()[1] + servers = self.servers.list() + tenants = self.tenants.list() + api.network.tenant_floating_ip_list(IsA(http.HttpRequest), + all_tenants=True).AndReturn(fips) + api.nova.server_list(IsA(http.HttpRequest), all_tenants=True) \ + .AndReturn([servers, False]) + api.keystone.tenant_list(IsA(http.HttpRequest))\ + .AndReturn([tenants, False]) + params = {"router:external": True} + api.neutron.network_list(IsA(http.HttpRequest), **params) \ + .AndReturn(self.networks.list()) + + self.mox.ReplayAll() + + form_data = { + "action": + "floating_ips__delete__%s" % floating_ip.id} + res = self.client.post(INDEX_URL, form_data) + + self.assertNoFormErrors(res) + + @test.create_stubs({api.network: ('tenant_floating_ip_list', ), + api.nova: ('server_list', ), + api.keystone: ('tenant_list', ), + api.neutron: ('network_list', )}) + def test_floating_ip_table_actions(self): + # Use neutron test data + fips = self.q_floating_ips.list() + servers = self.servers.list() + tenants = self.tenants.list() + api.network.tenant_floating_ip_list(IsA(http.HttpRequest), + all_tenants=True).AndReturn(fips) + api.nova.server_list(IsA(http.HttpRequest), all_tenants=True) \ + .AndReturn([servers, False]) + api.keystone.tenant_list(IsA(http.HttpRequest))\ + .AndReturn([tenants, False]) + params = {"router:external": True} + api.neutron.network_list(IsA(http.HttpRequest), **params) \ + .AndReturn(self.networks.list()) + + self.mox.ReplayAll() + + res = self.client.get(INDEX_URL) + self.assertTemplateUsed(res, 'admin/floating_ips/index.html') + self.assertIn('floating_ips_table', res.context) + floating_ips_table = res.context['floating_ips_table'] + floating_ips = floating_ips_table.data + self.assertEqual(len(floating_ips), 2) + # table actions + self.assertContains(res, 'id="floating_ips__action_allocate"') + self.assertContains(res, 'id="floating_ips__action_release"') + # row actions + self.assertContains(res, 'floating_ips__release__%s' % fips[0].id) + self.assertContains(res, 'floating_ips__release__%s' % fips[1].id) + self.assertContains(res, 'floating_ips__disassociate__%s' % fips[1].id) diff --git a/openstack_dashboard/dashboards/admin/floating_ips/urls.py b/openstack_dashboard/dashboards/admin/floating_ips/urls.py new file mode 100644 index 0000000000..8c19a5274d --- /dev/null +++ b/openstack_dashboard/dashboards/admin/floating_ips/urls.py @@ -0,0 +1,26 @@ +# Copyright 2016 Letv Cloud Computing +# All Rights Reserved. +# +# 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.conf.urls import url + +from openstack_dashboard.dashboards.admin.floating_ips import views + + +urlpatterns = [ + url(r'^$', views.IndexView.as_view(), name='index'), + url(r'^allocate/$', views.AllocateView.as_view(), name='allocate'), + url(r'^(?P<floating_ip_id>[^/]+)/detail/$', + views.DetailView.as_view(), name='detail') +] diff --git a/openstack_dashboard/dashboards/admin/floating_ips/views.py b/openstack_dashboard/dashboards/admin/floating_ips/views.py new file mode 100644 index 0000000000..05919d2c78 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/floating_ips/views.py @@ -0,0 +1,189 @@ +# Copyright 2016 Letv Cloud Computing +# All Rights Reserved. +# +# 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 collections import OrderedDict + +from django.core.urlresolvers import reverse +from django.core.urlresolvers import reverse_lazy +from django.utils.translation import ugettext_lazy as _ +import netaddr + +from horizon import exceptions +from horizon import forms +from horizon import tables +from horizon.utils import memoized +from horizon import views + +from openstack_dashboard import api + +from openstack_dashboard.dashboards.admin.floating_ips \ + import forms as fip_forms +from openstack_dashboard.dashboards.admin.floating_ips \ + import tables as fip_tables +from openstack_dashboard.dashboards.project.access_and_security.\ + floating_ips import tables as project_tables + + +def get_floatingip_pools(request): + pools = [] + try: + search_opts = {'router:external': True} + pools = api.neutron.network_list(request, **search_opts) + except Exception: + exceptions.handle(request, + _("Unable to retrieve floating IP pools.")) + return pools + + +def get_tenant_list(request): + tenants = [] + try: + tenants, has_more = api.keystone.tenant_list(request) + except Exception: + msg = _('Unable to retrieve project list.') + exceptions.handle(request, msg) + return tenants + + +class IndexView(tables.DataTableView): + table_class = fip_tables.FloatingIPsTable + template_name = 'admin/floating_ips/index.html' + page_title = _("Floating IPs") + + @memoized.memoized_method + def get_data(self): + floating_ips = [] + try: + floating_ips = api.network.tenant_floating_ip_list( + self.request, + all_tenants=True) + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve floating IP list.')) + + if floating_ips: + instances = [] + try: + instances, has_more = api.nova.server_list(self.request, + all_tenants=True) + except Exception: + exceptions.handle( + self.request, + _('Unable to retrieve instance list.')) + instances_dict = dict([(obj.id, obj.name) for obj in instances]) + + tenants = get_tenant_list(self.request) + tenant_dict = OrderedDict([(t.id, t) for t in tenants]) + + pools = get_floatingip_pools(self.request) + pool_dict = dict([(obj.id, obj.name) for obj in pools]) + + for ip in floating_ips: + ip.instance_name = instances_dict.get(ip.instance_id) + ip.pool_name = pool_dict.get(ip.pool, ip.pool) + tenant = tenant_dict.get(ip.tenant_id, None) + ip.tenant_name = getattr(tenant, "name", None) + + return floating_ips + + +class DetailView(views.HorizonTemplateView): + template_name = 'admin/floating_ips/detail.html' + page_title = _("Floating IP Details") + + def _get_corresponding_data(self, resource, resource_id): + function_dict = {"floating IP": api.network.tenant_floating_ip_get, + "instance": api.nova.server_get, + "network": api.neutron.network_get, + "router": api.neutron.router_get} + url = reverse('horizon:admin:floating_ips:index') + try: + res = function_dict[resource]( + self.request, resource_id) + if resource in ["network", "router"]: + res.set_id_as_name_if_empty(length=0) + return res + except KeyError: + msg = _('Unknow resource type for detail API.') + exceptions.handle(self.request, msg, redirect=url) + except Exception: + msg = _('Unable to retrieve details for ' + '%(resource)s "%(resource_id)s".') % { + "resource": resource, + "resource_id": resource_id} + exceptions.handle(self.request, msg, redirect=url) + + def get_context_data(self, **kwargs): + context = super(DetailView, self).get_context_data(**kwargs) + + floating_ip_id = self.kwargs['floating_ip_id'] + floating_ip = self._get_corresponding_data("floating IP", + floating_ip_id) + + network = self._get_corresponding_data("network", floating_ip.pool) + floating_ip.pool_name = network.name + + if floating_ip.instance_id and floating_ip.instance_type == 'compute': + instance = self._get_corresponding_data( + "instance", floating_ip.instance_id) + floating_ip.instance_name = instance.name + floating_ip.mapped_fixed_ip = project_tables.get_instance_info( + floating_ip) + + if floating_ip.router_id: + router = self._get_corresponding_data("router", + floating_ip.router_id) + floating_ip.router_name = router.name + table = fip_tables.FloatingIPsTable(self.request) + context['floating_ip'] = floating_ip + context["url"] = reverse('horizon:admin:floating_ips:index') + context["actions"] = table.render_row_actions(floating_ip) + return context + + +class AllocateView(forms.ModalFormView): + form_class = fip_forms.AdminFloatingIpAllocate + form_id = "allocate_floating_ip_form" + template_name = 'admin/floating_ips/allocate.html' + modal_header = _("Allocate Floating IP") + submit_label = _("Allocate Floating IP") + submit_url = reverse_lazy("horizon:admin:floating_ips:allocate") + cancel_url = reverse_lazy('horizon:admin:floating_ips:index') + success_url = reverse_lazy('horizon:admin:floating_ips:index') + page_title = _("Allocate Floating IP") + + @memoized.memoized_method + def get_initial(self): + tenants = get_tenant_list(self.request) + tenant_list = [(t.id, t.name) for t in tenants] + if not tenant_list: + tenant_list = [(None, _("No project available"))] + + pools = get_floatingip_pools(self.request) + pool_list = [] + for pool in pools: + for subnet in pool.subnets: + if netaddr.IPNetwork(subnet.cidr).version != 4: + continue + pool_display_name = (_("%(cidr)s %(pool_name)s") + % {'cidr': subnet.cidr, + 'pool_name': pool.name}) + pool_list.append((pool.id, pool_display_name)) + if not pool_list: + pool_list = [ + (None, _("No floating IP pools with IPv4 subnet available"))] + + return {'pool_list': pool_list, + 'tenant_list': tenant_list} diff --git a/openstack_dashboard/enabled/_2111_admin_floating_ips_panel.py b/openstack_dashboard/enabled/_2111_admin_floating_ips_panel.py new file mode 100644 index 0000000000..a3b03a4f45 --- /dev/null +++ b/openstack_dashboard/enabled/_2111_admin_floating_ips_panel.py @@ -0,0 +1,10 @@ +# The slug of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'floating_ips' +# The slug of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'admin' +# The slug of the panel group the PANEL is associated with. +PANEL_GROUP = 'admin' + +# Python panel class of the PANEL to be added. +ADD_PANEL = \ + 'openstack_dashboard.dashboards.admin.floating_ips.panel.AdminFloatingIps' diff --git a/openstack_dashboard/test/api_tests/network_tests.py b/openstack_dashboard/test/api_tests/network_tests.py index 70d386dfb8..b8416619d4 100644 --- a/openstack_dashboard/test/api_tests/network_tests.py +++ b/openstack_dashboard/test/api_tests/network_tests.py @@ -127,7 +127,7 @@ class NetworkApiNovaFloatingIpTests(NetworkApiNovaTestBase): fips = self.api_floating_ips.list() novaclient = self.stub_novaclient() novaclient.floating_ips = self.mox.CreateMockAnything() - novaclient.floating_ips.list().AndReturn(fips) + novaclient.floating_ips.list(all_tenants=False).AndReturn(fips) self.mox.ReplayAll() ret = api.network.tenant_floating_ip_list(self.request) diff --git a/openstack_dashboard/test/test_data/neutron_data.py b/openstack_dashboard/test/test_data/neutron_data.py index b192db11f4..e005a716b8 100644 --- a/openstack_dashboard/test/test_data/neutron_data.py +++ b/openstack_dashboard/test/test_data/neutron_data.py @@ -440,7 +440,10 @@ def data(TEST): 'port_id': None, 'router_id': None} TEST.api_q_floating_ips.add(fip_dict) - TEST.q_floating_ips.add(neutron.FloatingIp(fip_dict)) + fip_with_instance = copy.deepcopy(fip_dict) + fip_with_instance.update({'instance_id': None, + 'instance_type': None}) + TEST.q_floating_ips.add(neutron.FloatingIp(fip_with_instance)) # Associated (with compute port on 1st network). fip_dict = {'tenant_id': '1', @@ -451,7 +454,10 @@ def data(TEST): 'port_id': assoc_port['id'], 'router_id': router_dict['id']} TEST.api_q_floating_ips.add(fip_dict) - TEST.q_floating_ips.add(neutron.FloatingIp(fip_dict)) + fip_with_instance = copy.deepcopy(fip_dict) + fip_with_instance.update({'instance_id': '1', + 'instance_type': 'compute'}) + TEST.q_floating_ips.add(neutron.FloatingIp(fip_with_instance)) # Security group. diff --git a/releasenotes/notes/bp-admin-manage-fips-5aa409d3502b031a.yaml b/releasenotes/notes/bp-admin-manage-fips-5aa409d3502b031a.yaml new file mode 100644 index 0000000000..62374688a7 --- /dev/null +++ b/releasenotes/notes/bp-admin-manage-fips-5aa409d3502b031a.yaml @@ -0,0 +1,4 @@ +--- +features: + - > + [`blueprint manage-ips Add ability to manage floating IPs in syspanel <https://blueprints.launchpad.net/horizon/+spec/manage-ips>`_] Admin dashboard Floating IPs panel has been added to Horizon.