From 5c238e9117b1333c3d66798e5ad7e9e0fe44fc22 Mon Sep 17 00:00:00 2001
From: LIU Yulong <liuyulong@letv.com>
Date: Mon, 22 Dec 2014 15:10:38 +0800
Subject: [PATCH] Add floating IP panel to admin dashboard

Now system administrators have CRUD abilities to manage floating IP.
1.floating IP list table
2.allocate floating IP to specific tenant
3.release/delete floating IP

Partially implements blueprint: manage-ips
Partially implements blueprint: syspanel-floating-ip-list

Change-Id: Ie5ec59740887d3845b933b37e6e875dbf08a4918
---
 openstack_dashboard/api/network.py            |  10 +-
 openstack_dashboard/api/network_base.py       |   6 +-
 openstack_dashboard/api/neutron.py            |  13 +-
 openstack_dashboard/api/nova.py               |  10 +-
 .../dashboards/admin/floating_ips/__init__.py |   0
 .../dashboards/admin/floating_ips/forms.py    |  64 ++++
 .../dashboards/admin/floating_ips/panel.py    |  30 ++
 .../dashboards/admin/floating_ips/tables.py   |  91 ++++++
 .../templates/floating_ips/_allocate.html     |   9 +
 .../templates/floating_ips/allocate.html      |   7 +
 .../templates/floating_ips/detail.html        |  48 +++
 .../templates/floating_ips/index.html         |   7 +
 .../dashboards/admin/floating_ips/tests.py    | 275 ++++++++++++++++++
 .../dashboards/admin/floating_ips/urls.py     |  26 ++
 .../dashboards/admin/floating_ips/views.py    | 189 ++++++++++++
 .../enabled/_2111_admin_floating_ips_panel.py |  10 +
 .../test/api_tests/network_tests.py           |   2 +-
 .../test/test_data/neutron_data.py            |  10 +-
 ...bp-admin-manage-fips-5aa409d3502b031a.yaml |   4 +
 19 files changed, 793 insertions(+), 18 deletions(-)
 create mode 100644 openstack_dashboard/dashboards/admin/floating_ips/__init__.py
 create mode 100644 openstack_dashboard/dashboards/admin/floating_ips/forms.py
 create mode 100644 openstack_dashboard/dashboards/admin/floating_ips/panel.py
 create mode 100644 openstack_dashboard/dashboards/admin/floating_ips/tables.py
 create mode 100644 openstack_dashboard/dashboards/admin/floating_ips/templates/floating_ips/_allocate.html
 create mode 100644 openstack_dashboard/dashboards/admin/floating_ips/templates/floating_ips/allocate.html
 create mode 100644 openstack_dashboard/dashboards/admin/floating_ips/templates/floating_ips/detail.html
 create mode 100644 openstack_dashboard/dashboards/admin/floating_ips/templates/floating_ips/index.html
 create mode 100644 openstack_dashboard/dashboards/admin/floating_ips/tests.py
 create mode 100644 openstack_dashboard/dashboards/admin/floating_ips/urls.py
 create mode 100644 openstack_dashboard/dashboards/admin/floating_ips/views.py
 create mode 100644 openstack_dashboard/enabled/_2111_admin_floating_ips_panel.py
 create mode 100644 releasenotes/notes/bp-admin-manage-fips-5aa409d3502b031a.yaml

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.