Move Floating IPs from Access & Security to panel

This patch makes the Floating IPs tab in Access & Security its own panel
under Project > Network

Change-Id: Ibb83ae5a0448d2824c10f867e620cec8219b7b72
Implements: blueprint reorganise-access-and-security
This commit is contained in:
Rob Cresswell 2017-01-26 11:17:59 +00:00
parent 4f654e30c3
commit 99849ad88f
18 changed files with 125 additions and 231 deletions

View File

@ -23,9 +23,9 @@ from horizon import messages
from horizon import tables
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.floating_ips \
import tables as project_tables
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

View File

@ -32,8 +32,8 @@ 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
from openstack_dashboard.dashboards.project.floating_ips \
import tables as project_tables
def get_floatingip_pools(request):

View File

@ -25,10 +25,6 @@ from horizon import tabs
from neutronclient.common import exceptions as neutron_exc
from openstack_dashboard.api import network
from openstack_dashboard.api import nova
from openstack_dashboard.dashboards.project.access_and_security.\
floating_ips.tables import FloatingIPsTable
from openstack_dashboard.dashboards.project.access_and_security.\
security_groups.tables import SecurityGroupsTable
@ -53,60 +49,7 @@ class SecurityGroupsTab(tabs.TableTab):
return sorted(security_groups, key=lambda group: group.name)
class FloatingIPsTab(tabs.TableTab):
table_classes = (FloatingIPsTable,)
name = _("Floating IPs")
slug = "floating_ips_tab"
template_name = "horizon/common/_detail_table.html"
permissions = ('openstack.services.compute',)
def get_floating_ips_data(self):
try:
floating_ips = network.tenant_floating_ip_list(self.request)
except neutron_exc.ConnectionFailed:
floating_ips = []
exceptions.handle(self.request)
except Exception:
floating_ips = []
exceptions.handle(self.request,
_('Unable to retrieve floating IP addresses.'))
try:
floating_ip_pools = network.floating_ip_pools_list(self.request)
except neutron_exc.ConnectionFailed:
floating_ip_pools = []
exceptions.handle(self.request)
except Exception:
floating_ip_pools = []
exceptions.handle(self.request,
_('Unable to retrieve floating IP pools.'))
pool_dict = dict([(obj.id, obj.name) for obj in floating_ip_pools])
attached_instance_ids = [ip.instance_id for ip in floating_ips
if ip.instance_id is not None]
if attached_instance_ids:
instances = []
try:
# TODO(tsufiev): we should pass attached_instance_ids to
# nova.server_list as soon as Nova API allows for this
instances, has_more = nova.server_list(self.request)
except Exception:
exceptions.handle(self.request,
_('Unable to retrieve instance list.'))
instances_dict = dict([(obj.id, obj.name) for obj in instances])
for ip in floating_ips:
ip.instance_name = instances_dict.get(ip.instance_id)
ip.pool_name = pool_dict.get(ip.pool, ip.pool)
return floating_ips
def allowed(self, request):
return network.floating_ip_supported(request)
class AccessAndSecurityTabs(tabs.TabGroup):
slug = "access_security_tabs"
tabs = (SecurityGroupsTab, FloatingIPsTab)
tabs = (SecurityGroupsTab,)
sticky = True

View File

@ -1,7 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Allocate Floating IP" %}{% endblock %}
{% block main %}
{% include 'project/access_and_security/floating_ips/_allocate.html' %}
{% endblock %}

View File

@ -23,7 +23,6 @@ from django import http
from mox3.mox import IsA # noqa
import six
from horizon.workflows import views
from openstack_dashboard import api
from openstack_dashboard.test import helpers as test
from openstack_dashboard.usage import quotas
@ -35,32 +34,14 @@ class AccessAndSecurityTests(test.TestCase):
def setUp(self):
super(AccessAndSecurityTests, self).setUp()
@test.create_stubs({api.network: ('floating_ip_supported',
'tenant_floating_ip_list',
'floating_ip_pools_list',
'security_group_list',),
api.nova: ('server_list',),
@test.create_stubs({api.network: ('security_group_list',),
api.base: ('is_service_enabled',),
quotas: ('tenant_quota_usages',)})
def _test_index(self, instanceless_ips=False):
def _test_index(self):
sec_groups = self.security_groups.list()
floating_ips = self.floating_ips.list()
floating_pools = self.pools.list()
if instanceless_ips:
for fip in floating_ips:
fip.instance_id = None
quota_data = self.quota_usages.first()
quota_data['security_groups']['available'] = 10
api.network.floating_ip_supported(IsA(http.HttpRequest)) \
.AndReturn(True)
if not instanceless_ips:
api.nova.server_list(IsA(http.HttpRequest)) \
.AndReturn([self.servers.list(), False])
api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \
.AndReturn(floating_ips)
api.network.floating_ip_pools_list(IsA(http.HttpRequest)) \
.AndReturn(floating_pools)
api.network.security_group_list(IsA(http.HttpRequest)) \
.AndReturn(sec_groups)
quotas.tenant_quota_usages(IsA(http.HttpRequest)).MultipleTimes() \
@ -74,8 +55,6 @@ class AccessAndSecurityTests(test.TestCase):
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, 'project/access_and_security/index.html')
self.assertItemsEqual(res.context['floating_ips_table'].data,
floating_ips)
# Security groups
sec_groups_from_ctx = res.context['security_groups_table'].data
@ -93,81 +72,22 @@ class AccessAndSecurityTests(test.TestCase):
def test_index(self):
self._test_index()
def test_index_with_instanceless_fips(self):
self._test_index(instanceless_ips=True)
@test.create_stubs({api.network: ('floating_ip_target_list',
'tenant_floating_ip_list',)})
def test_association(self):
servers = [api.nova.Server(s, self.request)
for s in self.servers.list()]
# Add duplicate instance name to test instance name with [ID]
# Change id and private IP
server3 = api.nova.Server(self.servers.first(), self.request)
server3.id = 101
server3.addresses = deepcopy(server3.addresses)
server3.addresses['private'][0]['addr'] = "10.0.0.5"
servers.append(server3)
targets = [api.nova.FloatingIpTarget(s) for s in servers]
api.network.tenant_floating_ip_list(
IsA(http.HttpRequest)) \
.AndReturn(self.floating_ips.list())
api.network.floating_ip_target_list(
IsA(http.HttpRequest)) \
.AndReturn(targets)
self.mox.ReplayAll()
res = self.client.get(reverse("horizon:project:access_and_security:"
"floating_ips:associate"))
self.assertTemplateUsed(res, views.WorkflowView.template_name)
self.assertContains(res, '<option value="1">server_1 (1)</option>')
self.assertContains(res, '<option value="101">server_1 (101)</option>')
self.assertContains(res, '<option value="2">server_2 (2)</option>')
class AccessAndSecurityNeutronProxyTests(AccessAndSecurityTests):
def setUp(self):
super(AccessAndSecurityNeutronProxyTests, self).setUp()
self.floating_ips = self.floating_ips_uuid
class SecurityGroupTabTests(test.TestCase):
def setUp(self):
super(SecurityGroupTabTests, self).setUp()
@test.create_stubs({api.network: ('floating_ip_supported',
'tenant_floating_ip_list',
'security_group_list',
'floating_ip_pools_list',),
api.nova: ('server_list',),
@test.create_stubs({api.network: ('security_group_list',),
quotas: ('tenant_quota_usages',),
api.base: ('is_service_enabled',)})
def test_create_button_attributes(self):
floating_ips = self.floating_ips.list()
floating_pools = self.pools.list()
sec_groups = self.security_groups.list()
quota_data = self.quota_usages.first()
quota_data['security_groups']['available'] = 10
api.network.floating_ip_supported(
IsA(http.HttpRequest)) \
.AndReturn(True)
api.network.tenant_floating_ip_list(
IsA(http.HttpRequest)) \
.AndReturn(floating_ips)
api.network.floating_ip_pools_list(
IsA(http.HttpRequest)) \
.AndReturn(floating_pools)
api.network.security_group_list(
IsA(http.HttpRequest)) \
.AndReturn(sec_groups)
api.nova.server_list(
IsA(http.HttpRequest)) \
.AndReturn([self.servers.list(), False])
quotas.tenant_quota_usages(
IsA(http.HttpRequest)).MultipleTimes() \
.AndReturn(quota_data)
@ -195,36 +115,18 @@ class SecurityGroupTabTests(test.TestCase):
url = 'horizon:project:access_and_security:security_groups:create'
self.assertEqual(url, create_action.url)
@test.create_stubs({api.network: ('floating_ip_supported',
'tenant_floating_ip_list',
'security_group_list',
'floating_ip_pools_list',),
api.nova: ('server_list',),
@test.create_stubs({api.network: ('security_group_list',),
quotas: ('tenant_quota_usages',),
api.base: ('is_service_enabled',)})
def _test_create_button_disabled_when_quota_exceeded(self,
network_enabled):
floating_ips = self.floating_ips.list()
floating_pools = self.pools.list()
sec_groups = self.security_groups.list()
quota_data = self.quota_usages.first()
quota_data['security_groups']['available'] = 0
api.network.floating_ip_supported(
IsA(http.HttpRequest)) \
.AndReturn(True)
api.network.tenant_floating_ip_list(
IsA(http.HttpRequest)) \
.AndReturn(floating_ips)
api.network.floating_ip_pools_list(
IsA(http.HttpRequest)) \
.AndReturn(floating_pools)
api.network.security_group_list(
IsA(http.HttpRequest)) \
.AndReturn(sec_groups)
api.nova.server_list(
IsA(http.HttpRequest)) \
.AndReturn([self.servers.list(), False])
quotas.tenant_quota_usages(
IsA(http.HttpRequest)).MultipleTimes() \
.AndReturn(quota_data)

View File

@ -19,8 +19,6 @@
from django.conf.urls import include
from django.conf.urls import url
from openstack_dashboard.dashboards.project.access_and_security.\
floating_ips import urls as fip_urls
from openstack_dashboard.dashboards.project.access_and_security.\
security_groups import urls as sec_group_urls
from openstack_dashboard.dashboards.project.access_and_security import views
@ -28,7 +26,6 @@ from openstack_dashboard.dashboards.project.access_and_security import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'floating_ips/', include(fip_urls, namespace='floating_ips')),
url(r'security_groups/',
include(sec_group_urls, namespace='security_groups')),
]

View File

@ -0,0 +1,27 @@
# Copyright 2017 Cisco Systems, Inc.
#
# 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 FloatingIps(horizon.Panel):
name = _("Floating IPs")
slug = 'floating_ips'
@staticmethod
def can_register():
network_config = getattr(settings, 'OPENSTACK_NEUTRON_NETWORK', {})
return network_config.get('enable_router', True)

View File

@ -41,10 +41,10 @@ class AllocateIP(tables.LinkAction):
verbose_name = _("Allocate IP To Project")
classes = ("ajax-modal",)
icon = "link"
url = "horizon:project:access_and_security:floating_ips:allocate"
url = "horizon:project:floating_ips:allocate"
def single(self, data_table, request, *args):
return shortcuts.redirect('horizon:project:access_and_security:index')
return shortcuts.redirect('horizon:project:floating_ips:index')
def allowed(self, request, fip=None):
usages = quotas.tenant_quota_usages(request)
@ -106,7 +106,7 @@ class ReleaseIPs(tables.BatchAction):
class AssociateIP(tables.LinkAction):
name = "associate"
verbose_name = _("Associate")
url = "horizon:project:access_and_security:floating_ips:associate"
url = "horizon:project:floating_ips:associate"
classes = ("ajax-modal",)
icon = "link"
@ -152,7 +152,7 @@ class DisassociateIP(tables.Action):
except Exception:
exceptions.handle(request,
_('Unable to disassociate floating IP.'))
return shortcuts.redirect('horizon:project:access_and_security:index')
return shortcuts.redirect('horizon:project:floating_ips:index')
def get_instance_info(fip):

View File

@ -0,0 +1,6 @@
{% extends 'base.html' %}
{% load i18n %}
{% block main %}
{% include 'project/floating_ips/_allocate.html' %}
{% endblock %}

View File

@ -31,8 +31,8 @@ from openstack_dashboard.usage import quotas
from horizon.workflows import views
INDEX_URL = reverse('horizon:project:access_and_security:index')
NAMESPACE = "horizon:project:access_and_security:floating_ips"
INDEX_URL = reverse('horizon:project:floating_ips:index')
NAMESPACE = "horizon:project:floating_ips"
class FloatingIpViewTests(test.TestCase):
@ -167,7 +167,6 @@ class FloatingIpViewTests(test.TestCase):
@test.create_stubs({api.nova: ('server_list',),
api.network: ('floating_ip_disassociate',
'floating_ip_supported',
'tenant_floating_ip_get',
'tenant_floating_ip_list',),
api.neutron: ('is_extension_supported',)})
@ -176,8 +175,6 @@ class FloatingIpViewTests(test.TestCase):
api.nova.server_list(IsA(http.HttpRequest)) \
.AndReturn([self.servers.list(), False])
api.network.floating_ip_supported(IsA(http.HttpRequest)) \
.AndReturn(True)
api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \
.AndReturn(self.floating_ips.list())
api.neutron.is_extension_supported(IsA(http.HttpRequest),
@ -194,7 +191,6 @@ class FloatingIpViewTests(test.TestCase):
@test.create_stubs({api.nova: ('server_list',),
api.network: ('floating_ip_disassociate',
'floating_ip_supported',
'tenant_floating_ip_get',
'tenant_floating_ip_list',),
api.neutron: ('is_extension_supported',)})
@ -203,8 +199,6 @@ class FloatingIpViewTests(test.TestCase):
api.nova.server_list(IsA(http.HttpRequest)) \
.AndReturn([self.servers.list(), False])
api.network.floating_ip_supported(IsA(http.HttpRequest)) \
.AndReturn(True)
api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \
.AndReturn(self.floating_ips.list())
api.neutron.is_extension_supported(IsA(http.HttpRequest),
@ -220,9 +214,7 @@ class FloatingIpViewTests(test.TestCase):
res = self.client.post(INDEX_URL, {"action": action})
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({api.network: ('floating_ip_supported',
'tenant_floating_ip_list',
'security_group_list',
@test.create_stubs({api.network: ('tenant_floating_ip_list',
'floating_ip_pools_list',),
api.nova: ('server_list',),
quotas: ('tenant_quota_usages',),
@ -232,17 +224,10 @@ class FloatingIpViewTests(test.TestCase):
floating_pools = self.pools.list()
quota_data = self.quota_usages.first()
quota_data['floating_ips']['available'] = 10
sec_groups = self.security_groups.list()
api.network.floating_ip_supported(
IsA(http.HttpRequest)) \
.AndReturn(True)
api.network.tenant_floating_ip_list(
IsA(http.HttpRequest)) \
.AndReturn(floating_ips)
api.network.security_group_list(
IsA(http.HttpRequest)).MultipleTimes()\
.AndReturn(sec_groups)
api.network.floating_ip_pools_list(
IsA(http.HttpRequest)) \
.AndReturn(floating_pools)
@ -252,7 +237,6 @@ class FloatingIpViewTests(test.TestCase):
quotas.tenant_quota_usages(
IsA(http.HttpRequest)).MultipleTimes() \
.AndReturn(quota_data)
api.base.is_service_enabled(
IsA(http.HttpRequest),
'network').MultipleTimes() \
@ -260,8 +244,7 @@ class FloatingIpViewTests(test.TestCase):
self.mox.ReplayAll()
res = self.client.get(INDEX_URL +
"?tab=access_security_tabs__floating_ips_tab")
res = self.client.get(INDEX_URL)
allocate_action = self.getAndAssertTableAction(res, 'floating_ips',
'allocate')
@ -270,12 +253,10 @@ class FloatingIpViewTests(test.TestCase):
six.text_type(allocate_action.verbose_name))
self.assertIsNone(allocate_action.policy_rules)
url = 'horizon:project:access_and_security:floating_ips:allocate'
url = 'horizon:project:floating_ips:allocate'
self.assertEqual(url, allocate_action.url)
@test.create_stubs({api.network: ('floating_ip_supported',
'tenant_floating_ip_list',
'security_group_list',
@test.create_stubs({api.network: ('tenant_floating_ip_list',
'floating_ip_pools_list',),
api.nova: ('server_list',),
quotas: ('tenant_quota_usages',),
@ -285,17 +266,10 @@ class FloatingIpViewTests(test.TestCase):
floating_pools = self.pools.list()
quota_data = self.quota_usages.first()
quota_data['floating_ips']['available'] = 0
sec_groups = self.security_groups.list()
api.network.floating_ip_supported(
IsA(http.HttpRequest)) \
.AndReturn(True)
api.network.tenant_floating_ip_list(
IsA(http.HttpRequest)) \
.AndReturn(floating_ips)
api.network.security_group_list(
IsA(http.HttpRequest)).MultipleTimes()\
.AndReturn(sec_groups)
api.network.floating_ip_pools_list(
IsA(http.HttpRequest)) \
.AndReturn(floating_pools)
@ -305,7 +279,6 @@ class FloatingIpViewTests(test.TestCase):
quotas.tenant_quota_usages(
IsA(http.HttpRequest)).MultipleTimes() \
.AndReturn(quota_data)
api.base.is_service_enabled(
IsA(http.HttpRequest),
'network').MultipleTimes() \
@ -313,8 +286,7 @@ class FloatingIpViewTests(test.TestCase):
self.mox.ReplayAll()
res = self.client.get(INDEX_URL +
"?tab=access_security_tabs__floating_ips_tab")
res = self.client.get(INDEX_URL)
allocate_action = self.getAndAssertTableAction(res, 'floating_ips',
'allocate')

View File

@ -18,11 +18,10 @@
from django.conf.urls import url
from openstack_dashboard.dashboards.project.access_and_security.\
floating_ips import views
from openstack_dashboard.dashboards.project.floating_ips import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^associate/$', views.AssociateView.as_view(), name='associate'),
url(r'^allocate/$', views.AllocateView.as_view(), name='allocate'),
]

View File

@ -28,15 +28,18 @@ from neutronclient.common import exceptions as neutron_exc
from horizon import exceptions
from horizon import forms
from horizon import tables
from horizon import workflows
from openstack_dashboard import api
from openstack_dashboard.usage import quotas
from openstack_dashboard.dashboards.project.access_and_security.\
floating_ips import forms as project_forms
from openstack_dashboard.dashboards.project.access_and_security.\
floating_ips import workflows as project_workflows
from openstack_dashboard.dashboards.project.floating_ips \
import forms as project_forms
from openstack_dashboard.dashboards.project.floating_ips \
import tables as project_tables
from openstack_dashboard.dashboards.project.floating_ips \
import workflows as project_workflows
class AssociateView(workflows.WorkflowView):
@ -47,11 +50,10 @@ class AllocateView(forms.ModalFormView):
form_class = project_forms.FloatingIpAllocate
form_id = "associate_floating_ip_form"
page_title = _("Allocate Floating IP")
template_name = 'project/access_and_security/floating_ips/allocate.html'
template_name = 'project/floating_ips/allocate.html'
submit_label = _("Allocate IP")
submit_url = reverse_lazy(
"horizon:project:access_and_security:floating_ips:allocate")
success_url = reverse_lazy('horizon:project:access_and_security:index')
submit_url = reverse_lazy("horizon:project:floating_ips:allocate")
success_url = reverse_lazy('horizon:project:floating_ips:index')
def get_object_display(self, obj):
return obj.ip
@ -78,3 +80,51 @@ class AllocateView(forms.ModalFormView):
if not pool_list:
pool_list = [(None, _("No floating IP pools available"))]
return {'pool_list': pool_list}
class IndexView(tables.DataTableView):
table_class = project_tables.FloatingIPsTable
page_title = _("Floating IPs")
def get_data(self):
try:
floating_ips = api.network.tenant_floating_ip_list(self.request)
except neutron_exc.ConnectionFailed:
floating_ips = []
exceptions.handle(self.request)
except Exception:
floating_ips = []
exceptions.handle(self.request,
_('Unable to retrieve floating IP addresses.'))
try:
floating_ip_pools = \
api.network.floating_ip_pools_list(self.request)
except neutron_exc.ConnectionFailed:
floating_ip_pools = []
exceptions.handle(self.request)
except Exception:
floating_ip_pools = []
exceptions.handle(self.request,
_('Unable to retrieve floating IP pools.'))
pool_dict = dict([(obj.id, obj.name) for obj in floating_ip_pools])
attached_instance_ids = [ip.instance_id for ip in floating_ips
if ip.instance_id is not None]
if attached_instance_ids:
instances = []
try:
# TODO(tsufiev): we should pass attached_instance_ids to
# nova.server_list as soon as Nova API allows for this
instances, has_more = api.nova.server_list(self.request)
except Exception:
exceptions.handle(self.request,
_('Unable to retrieve instance list.'))
instances_dict = dict([(obj.id, obj.name) for obj in instances])
for ip in floating_ips:
ip.instance_name = instances_dict.get(ip.instance_id)
ip.pool_name = pool_dict.get(ip.pool, ip.pool)
return floating_ips

View File

@ -26,7 +26,7 @@ from openstack_dashboard import api
from openstack_dashboard.utils import filters
ALLOCATE_URL = "horizon:project:access_and_security:floating_ips:allocate"
ALLOCATE_URL = "horizon:project:floating_ips:allocate"
class AssociateIPAction(workflows.Action):
@ -72,7 +72,7 @@ class AssociateIPAction(workflows.Action):
def populate_ip_id_choices(self, request, context):
ips = []
redirect = reverse('horizon:project:access_and_security:index')
redirect = reverse('horizon:project:floating_ips:index')
try:
ips = api.network.tenant_floating_ip_list(self.request)
except neutron_exc.ConnectionFailed:
@ -95,7 +95,7 @@ class AssociateIPAction(workflows.Action):
try:
targets = api.network.floating_ip_target_list(self.request)
except Exception:
redirect = reverse('horizon:project:access_and_security:index')
redirect = reverse('horizon:project:floating_ips:index')
exceptions.handle(self.request,
_('Unable to retrieve instance list.'),
redirect=redirect)
@ -146,7 +146,7 @@ class IPAssociationWorkflow(workflows.Workflow):
finalize_button_name = _("Associate")
success_message = _('IP address %s associated.')
failure_message = _('Unable to associate IP address %s.')
success_url = "horizon:project:access_and_security:index"
success_url = "horizon:project:floating_ips:index"
default_steps = (AssociateIP,)
def format_status_message(self, message):

View File

@ -36,8 +36,7 @@ from horizon.templatetags import sizeformat
from horizon.utils import filters
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.access_and_security.floating_ips \
import workflows
from openstack_dashboard.dashboards.project.floating_ips import workflows
from openstack_dashboard.dashboards.project.instances import tabs
from openstack_dashboard.dashboards.project.instances.workflows \
import resize_instance
@ -617,7 +616,7 @@ class DecryptInstancePassword(tables.LinkAction):
class AssociateIP(policy.PolicyTargetMixin, tables.LinkAction):
name = "associate"
verbose_name = _("Associate Floating IP")
url = "horizon:project:access_and_security:floating_ips:associate"
url = "horizon:project:floating_ips:associate"
classes = ("ajax-modal",)
icon = "link"
policy_rules = (("compute", "network:associate_floating_ip"),)

View File

@ -0,0 +1,6 @@
PANEL_DASHBOARD = 'project'
PANEL_GROUP = 'network'
PANEL = 'floating_ips'
ADD_PANEL = \
'openstack_dashboard.dashboards.project.floating_ips.panel.FloatingIps'