From 1a58a1fd6097ee59fa5ec6369b54eceaaa2126cf Mon Sep 17 00:00:00 2001 From: Rob Cresswell Date: Wed, 7 Dec 2016 16:14:18 +0000 Subject: [PATCH] Make Key Pairs tab a panel under Compute As part of the breaking up of Access and Security, move the Key Pairs tab to a new panel under Compute. Separate patches will address Floating IPs, Security Groups, and API Access. Fixes include: - Should be significantly faster to access Key Pairs, as we are no longer running multiple API calls for the other Access & Security tabs at the same time. Hooray for speed! - Should be easier for new users to find where Key Pairs are located. - Reduce reuse of identical translatable strings - Use common templates instead of duplication - Updated policy rules and added missing rules to table get_data - Small cleanup of the Key Pair download page, which was previously using modal classes despite not being a modal. Change-Id: I66f1f65a2cb49bd10e0364b12efba4346f373ed3 Implements: blueprint reorganise-access-and-security --- .../access_and_security/floating_ips/tests.py | 14 +-- .../project/access_and_security/tabs.py | 21 +--- .../keypairs/download.html | 19 ---- .../project/access_and_security/tests.py | 21 +--- .../project/access_and_security/urls.py | 3 - .../keypairs => key_pairs}/__init__.py | 0 .../keypairs => key_pairs}/forms.py | 0 .../dashboards/project/key_pairs/panel.py | 25 +++++ .../keypairs => key_pairs}/tables.py | 15 ++- .../templates/key_pairs}/_create.html | 5 +- .../templates/key_pairs}/_import.html | 1 - .../templates/key_pairs}/create.html | 2 +- .../templates/key_pairs}/detail.html | 2 +- .../templates/key_pairs/download.html | 17 +++ .../templates/key_pairs}/import.html | 2 +- .../keypairs => key_pairs}/tests.py | 100 ++++++++++-------- .../keypairs => key_pairs}/urls.py | 6 +- .../keypairs => key_pairs}/views.py | 84 ++++++++------- .../enabled/_1080_project_key_pairs_panel.py | 5 + ...-access-and-security-ea7780aa9e7b83e7.yaml | 5 + 20 files changed, 173 insertions(+), 174 deletions(-) delete mode 100644 openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/keypairs/download.html rename openstack_dashboard/dashboards/project/{access_and_security/keypairs => key_pairs}/__init__.py (100%) rename openstack_dashboard/dashboards/project/{access_and_security/keypairs => key_pairs}/forms.py (100%) create mode 100644 openstack_dashboard/dashboards/project/key_pairs/panel.py rename openstack_dashboard/dashboards/project/{access_and_security/keypairs => key_pairs}/tables.py (88%) rename openstack_dashboard/dashboards/project/{access_and_security/templates/access_and_security/keypairs => key_pairs/templates/key_pairs}/_create.html (56%) rename openstack_dashboard/dashboards/project/{access_and_security/templates/access_and_security/keypairs => key_pairs/templates/key_pairs}/_import.html (95%) rename openstack_dashboard/dashboards/project/{access_and_security/templates/access_and_security/keypairs => key_pairs/templates/key_pairs}/create.html (70%) rename openstack_dashboard/dashboards/project/{access_and_security/templates/access_and_security/keypairs => key_pairs/templates/key_pairs}/detail.html (93%) create mode 100644 openstack_dashboard/dashboards/project/key_pairs/templates/key_pairs/download.html rename openstack_dashboard/dashboards/project/{access_and_security/templates/access_and_security/keypairs => key_pairs/templates/key_pairs}/import.html (70%) rename openstack_dashboard/dashboards/project/{access_and_security/keypairs => key_pairs}/tests.py (75%) rename openstack_dashboard/dashboards/project/{access_and_security/keypairs => key_pairs}/urls.py (91%) rename openstack_dashboard/dashboards/project/{access_and_security/keypairs => key_pairs}/views.py (60%) create mode 100644 openstack_dashboard/enabled/_1080_project_key_pairs_panel.py create mode 100644 releasenotes/notes/bp/reorganise-access-and-security-ea7780aa9e7b83e7.yaml diff --git a/openstack_dashboard/dashboards/project/access_and_security/floating_ips/tests.py b/openstack_dashboard/dashboards/project/access_and_security/floating_ips/tests.py index d328566638..d1513b54d9 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/floating_ips/tests.py +++ b/openstack_dashboard/dashboards/project/access_and_security/floating_ips/tests.py @@ -224,12 +224,10 @@ class FloatingIpViewTests(test.TestCase): 'tenant_floating_ip_list', 'security_group_list', 'floating_ip_pools_list',), - api.nova: ('keypair_list', - 'server_list',), + api.nova: ('server_list',), quotas: ('tenant_quota_usages',), api.base: ('is_service_enabled',)}) def test_allocate_button_attributes(self): - keypairs = self.keypairs.list() floating_ips = self.floating_ips.list() floating_pools = self.pools.list() quota_data = self.quota_usages.first() @@ -248,9 +246,6 @@ class FloatingIpViewTests(test.TestCase): api.network.floating_ip_pools_list( IsA(http.HttpRequest)) \ .AndReturn(floating_pools) - api.nova.keypair_list( - IsA(http.HttpRequest)) \ - .AndReturn(keypairs) api.nova.server_list( IsA(http.HttpRequest)) \ .AndReturn([self.servers.list(), False]) @@ -286,12 +281,10 @@ class FloatingIpViewTests(test.TestCase): 'tenant_floating_ip_list', 'security_group_list', 'floating_ip_pools_list',), - api.nova: ('keypair_list', - 'server_list',), + api.nova: ('server_list',), quotas: ('tenant_quota_usages',), api.base: ('is_service_enabled',)}) def test_allocate_button_disabled_when_quota_exceeded(self): - keypairs = self.keypairs.list() floating_ips = self.floating_ips.list() floating_pools = self.pools.list() quota_data = self.quota_usages.first() @@ -310,9 +303,6 @@ class FloatingIpViewTests(test.TestCase): api.network.floating_ip_pools_list( IsA(http.HttpRequest)) \ .AndReturn(floating_pools) - api.nova.keypair_list( - IsA(http.HttpRequest)) \ - .AndReturn(keypairs) api.nova.server_list( IsA(http.HttpRequest)) \ .AndReturn([self.servers.list(), False]) diff --git a/openstack_dashboard/dashboards/project/access_and_security/tabs.py b/openstack_dashboard/dashboards/project/access_and_security/tabs.py index 76fb8ddcf1..585aef1de0 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/tabs.py +++ b/openstack_dashboard/dashboards/project/access_and_security/tabs.py @@ -32,8 +32,6 @@ from openstack_dashboard.dashboards.project.access_and_security.\ api_access.tables import EndpointsTable from openstack_dashboard.dashboards.project.access_and_security.\ floating_ips.tables import FloatingIPsTable -from openstack_dashboard.dashboards.project.access_and_security.\ - keypairs.tables import KeypairsTable from openstack_dashboard.dashboards.project.access_and_security.\ security_groups.tables import SecurityGroupsTable @@ -58,23 +56,6 @@ class SecurityGroupsTab(tabs.TableTab): return sorted(security_groups, key=lambda group: group.name) -class KeypairsTab(tabs.TableTab): - table_classes = (KeypairsTable,) - name = _("Key Pairs") - slug = "keypairs_tab" - template_name = "horizon/common/_detail_table.html" - permissions = ('openstack.services.compute',) - - def get_keypairs_data(self): - try: - keypairs = nova.keypair_list(self.request) - except Exception: - keypairs = [] - exceptions.handle(self.request, - _('Unable to retrieve key pair list.')) - return keypairs - - class FloatingIPsTab(tabs.TableTab): table_classes = (FloatingIPsTable,) name = _("Floating IPs") @@ -146,5 +127,5 @@ class APIAccessTab(tabs.TableTab): class AccessAndSecurityTabs(tabs.TabGroup): slug = "access_security_tabs" - tabs = (SecurityGroupsTab, KeypairsTab, FloatingIPsTab, APIAccessTab) + tabs = (SecurityGroupsTab, FloatingIPsTab, APIAccessTab) sticky = True diff --git a/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/keypairs/download.html b/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/keypairs/download.html deleted file mode 100644 index c7431785b0..0000000000 --- a/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/keypairs/download.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends 'base.html' %} -{% load i18n %} -{% block title %}{% blocktrans %}Download Key Pair{% endblocktrans %}{% endblock %} - -{% block main %} - - - -{% endblock %} diff --git a/openstack_dashboard/dashboards/project/access_and_security/tests.py b/openstack_dashboard/dashboards/project/access_and_security/tests.py index 2e0af777fc..8b9bc25709 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/tests.py +++ b/openstack_dashboard/dashboards/project/access_and_security/tests.py @@ -41,13 +41,11 @@ class AccessAndSecurityTests(test.TestCase): 'tenant_floating_ip_list', 'floating_ip_pools_list', 'security_group_list',), - api.nova: ('keypair_list', - 'server_list',), + api.nova: ('server_list',), api.base: ('is_service_enabled',), quotas: ('tenant_quota_usages',), api.keystone: ('list_ec2_credentials',)}) def _test_index(self, ec2_enabled=True, instanceless_ips=False): - keypairs = self.keypairs.list() sec_groups = self.security_groups.list() floating_ips = self.floating_ips.list() floating_pools = self.pools.list() @@ -62,8 +60,6 @@ class AccessAndSecurityTests(test.TestCase): if not instanceless_ips: api.nova.server_list(IsA(http.HttpRequest)) \ .AndReturn([self.servers.list(), False]) - api.nova.keypair_list(IsA(http.HttpRequest)) \ - .AndReturn(keypairs) api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ .AndReturn(floating_ips) api.network.floating_ip_pools_list(IsA(http.HttpRequest)) \ @@ -87,7 +83,6 @@ class AccessAndSecurityTests(test.TestCase): res = self.client.get(INDEX_URL) self.assertTemplateUsed(res, 'project/access_and_security/index.html') - self.assertItemsEqual(res.context['keypairs_table'].data, keypairs) self.assertItemsEqual(res.context['floating_ips_table'].data, floating_ips) @@ -171,12 +166,10 @@ class SecurityGroupTabTests(test.TestCase): 'tenant_floating_ip_list', 'security_group_list', 'floating_ip_pools_list',), - api.nova: ('keypair_list', - 'server_list',), + api.nova: ('server_list',), quotas: ('tenant_quota_usages',), api.base: ('is_service_enabled',)}) def test_create_button_attributes(self): - keypairs = self.keypairs.list() floating_ips = self.floating_ips.list() floating_pools = self.pools.list() sec_groups = self.security_groups.list() @@ -195,9 +188,6 @@ class SecurityGroupTabTests(test.TestCase): api.network.security_group_list( IsA(http.HttpRequest)) \ .AndReturn(sec_groups) - api.nova.keypair_list( - IsA(http.HttpRequest)) \ - .AndReturn(keypairs) api.nova.server_list( IsA(http.HttpRequest)) \ .AndReturn([self.servers.list(), False]) @@ -235,13 +225,11 @@ class SecurityGroupTabTests(test.TestCase): 'tenant_floating_ip_list', 'security_group_list', 'floating_ip_pools_list',), - api.nova: ('keypair_list', - 'server_list',), + api.nova: ('server_list',), quotas: ('tenant_quota_usages',), api.base: ('is_service_enabled',)}) def _test_create_button_disabled_when_quota_exceeded(self, network_enabled): - keypairs = self.keypairs.list() floating_ips = self.floating_ips.list() floating_pools = self.pools.list() sec_groups = self.security_groups.list() @@ -260,9 +248,6 @@ class SecurityGroupTabTests(test.TestCase): api.network.security_group_list( IsA(http.HttpRequest)) \ .AndReturn(sec_groups) - api.nova.keypair_list( - IsA(http.HttpRequest)) \ - .AndReturn(keypairs) api.nova.server_list( IsA(http.HttpRequest)) \ .AndReturn([self.servers.list(), False]) diff --git a/openstack_dashboard/dashboards/project/access_and_security/urls.py b/openstack_dashboard/dashboards/project/access_and_security/urls.py index 5461791432..575576d34a 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/urls.py +++ b/openstack_dashboard/dashboards/project/access_and_security/urls.py @@ -23,8 +23,6 @@ from openstack_dashboard.dashboards.project.access_and_security.\ api_access import urls as api_access_urls from openstack_dashboard.dashboards.project.access_and_security.\ floating_ips import urls as fip_urls -from openstack_dashboard.dashboards.project.access_and_security.\ - keypairs import urls as keypair_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 @@ -33,7 +31,6 @@ from openstack_dashboard.dashboards.project.access_and_security import views urlpatterns = [ url(r'^$', views.IndexView.as_view(), name='index'), url(r'api_access/', include(api_access_urls, namespace='api_access')), - url(r'keypairs/', include(keypair_urls, namespace='keypairs')), url(r'floating_ips/', include(fip_urls, namespace='floating_ips')), url(r'security_groups/', include(sec_group_urls, namespace='security_groups')), diff --git a/openstack_dashboard/dashboards/project/access_and_security/keypairs/__init__.py b/openstack_dashboard/dashboards/project/key_pairs/__init__.py similarity index 100% rename from openstack_dashboard/dashboards/project/access_and_security/keypairs/__init__.py rename to openstack_dashboard/dashboards/project/key_pairs/__init__.py diff --git a/openstack_dashboard/dashboards/project/access_and_security/keypairs/forms.py b/openstack_dashboard/dashboards/project/key_pairs/forms.py similarity index 100% rename from openstack_dashboard/dashboards/project/access_and_security/keypairs/forms.py rename to openstack_dashboard/dashboards/project/key_pairs/forms.py diff --git a/openstack_dashboard/dashboards/project/key_pairs/panel.py b/openstack_dashboard/dashboards/project/key_pairs/panel.py new file mode 100644 index 0000000000..a0f9da2410 --- /dev/null +++ b/openstack_dashboard/dashboards/project/key_pairs/panel.py @@ -0,0 +1,25 @@ +# 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.utils.translation import ugettext_lazy as _ + +import horizon + + +class KeyPairs(horizon.Panel): + name = _("Key Pairs") + slug = 'key_pairs' + permissions = ('openstack.services.compute',) + policy_rules = (("compute", "os_compute_api:os-keypairs:index"), + ("compute", "os_compute_api:os-keypairs:create"),) diff --git a/openstack_dashboard/dashboards/project/access_and_security/keypairs/tables.py b/openstack_dashboard/dashboards/project/key_pairs/tables.py similarity index 88% rename from openstack_dashboard/dashboards/project/access_and_security/keypairs/tables.py rename to openstack_dashboard/dashboards/project/key_pairs/tables.py index 352650d052..3d5182bc84 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/keypairs/tables.py +++ b/openstack_dashboard/dashboards/project/key_pairs/tables.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -from django.utils.translation import string_concat # noqa +from django.utils.translation import string_concat from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ungettext_lazy @@ -51,19 +51,19 @@ class DeleteKeyPairs(tables.DeleteAction): class ImportKeyPair(tables.LinkAction): name = "import" verbose_name = _("Import Key Pair") - url = "horizon:project:access_and_security:keypairs:import" + url = "horizon:project:key_pairs:import" classes = ("ajax-modal",) icon = "upload" - policy_rules = (("compute", "compute_extension:keypairs:create"),) + policy_rules = (("compute", "os_compute_api:os-keypairs:create"),) class CreateKeyPair(tables.LinkAction): name = "create" verbose_name = _("Create Key Pair") - url = "horizon:project:access_and_security:keypairs:create" + url = "horizon:project:key_pairs:create" classes = ("ajax-modal",) icon = "plus" - policy_rules = (("compute", "compute_extension:keypairs:create"),) + policy_rules = (("compute", "os_compute_api:os-keypairs:create"),) def allowed(self, request, keypair=None): usages = quotas.tenant_quota_usages(request) @@ -90,9 +90,8 @@ class KeypairsFilterAction(tables.FilterAction): if query in keypair.name.lower()] -class KeypairsTable(tables.DataTable): - detail_link = "horizon:project:access_and_security:keypairs:detail" - +class KeyPairsTable(tables.DataTable): + detail_link = "horizon:project:key_pairs:detail" name = tables.Column("name", verbose_name=_("Key Pair Name"), link=detail_link) fingerprint = tables.Column("fingerprint", verbose_name=_("Fingerprint")) diff --git a/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/keypairs/_create.html b/openstack_dashboard/dashboards/project/key_pairs/templates/key_pairs/_create.html similarity index 56% rename from openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/keypairs/_create.html rename to openstack_dashboard/dashboards/project/key_pairs/templates/key_pairs/_create.html index b777a21823..652689e168 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/keypairs/_create.html +++ b/openstack_dashboard/dashboards/project/key_pairs/templates/key_pairs/_create.html @@ -2,7 +2,6 @@ {% load i18n %} {% block modal-body-right %} -

{% trans "Description:" %}

-

{% trans "Key pairs are ssh credentials which are injected into images when they are launched. Creating a new key pair registers the public key and downloads the private key (a .pem file)." %}

-

{% trans "Protect and use the key as you would any normal ssh private key." %}

+

{% trans "Key pairs are SSH credentials which are injected into images when they are launched. Creating a new key pair registers the public key and downloads the private key (a .pem file)." %}

+

{% trans "Protect and use the key as you would any normal SSH private key." %}

{% endblock %} diff --git a/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/keypairs/_import.html b/openstack_dashboard/dashboards/project/key_pairs/templates/key_pairs/_import.html similarity index 95% rename from openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/keypairs/_import.html rename to openstack_dashboard/dashboards/project/key_pairs/templates/key_pairs/_import.html index a247df76aa..b17c61b62a 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/keypairs/_import.html +++ b/openstack_dashboard/dashboards/project/key_pairs/templates/key_pairs/_import.html @@ -2,7 +2,6 @@ {% load i18n %} {% block modal-body-right %} -

{% trans "Description:" %}

{% trans "Key Pairs are how you login to your instance after it is launched." %}

{% trans "Choose a key pair name you will recognise and paste your SSH public key into the space provided." %}

{% trans "SSH key pairs can be generated with the ssh-keygen command:" %}

diff --git a/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/keypairs/create.html b/openstack_dashboard/dashboards/project/key_pairs/templates/key_pairs/create.html similarity index 70% rename from openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/keypairs/create.html rename to openstack_dashboard/dashboards/project/key_pairs/templates/key_pairs/create.html index cbeeb0e395..05ae6f15a7 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/keypairs/create.html +++ b/openstack_dashboard/dashboards/project/key_pairs/templates/key_pairs/create.html @@ -1,6 +1,6 @@ {% extends 'base.html' %} {% load i18n %} -{% block title %}{% trans "Create Key Pair" %}{% endblock %} +{% block title %}{{ page_title }}{% endblock %} {% block main %} {% include 'project/access_and_security/keypairs/_create.html' %} diff --git a/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/keypairs/detail.html b/openstack_dashboard/dashboards/project/key_pairs/templates/key_pairs/detail.html similarity index 93% rename from openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/keypairs/detail.html rename to openstack_dashboard/dashboards/project/key_pairs/templates/key_pairs/detail.html index 0fa36db261..0d29f95ecc 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/keypairs/detail.html +++ b/openstack_dashboard/dashboards/project/key_pairs/templates/key_pairs/detail.html @@ -1,7 +1,7 @@ {% extends 'base.html' %} {% load i18n sizeformat %} -{% block title %}{% trans "Key Pair Details" %}{% endblock %} +{% block title %}{{ page_title }}{% endblock %} {% block page_header %} {% include "horizon/common/_detail_header.html" %} diff --git a/openstack_dashboard/dashboards/project/key_pairs/templates/key_pairs/download.html b/openstack_dashboard/dashboards/project/key_pairs/templates/key_pairs/download.html new file mode 100644 index 0000000000..54ab6040ff --- /dev/null +++ b/openstack_dashboard/dashboards/project/key_pairs/templates/key_pairs/download.html @@ -0,0 +1,17 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{{ page_title }}{% endblock %} + +{% block main %} +
+ {% blocktrans %}The key pair "{{ keypair_name }}" should download automatically. If not, use the button below.{% endblocktrans %} +
+ + {% blocktrans %}Regenerate and download Key Pair "{{ keypair_name }}"{% endblocktrans %} + + +{% endblock %} diff --git a/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/keypairs/import.html b/openstack_dashboard/dashboards/project/key_pairs/templates/key_pairs/import.html similarity index 70% rename from openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/keypairs/import.html rename to openstack_dashboard/dashboards/project/key_pairs/templates/key_pairs/import.html index a989458360..a88b2d72c9 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/templates/access_and_security/keypairs/import.html +++ b/openstack_dashboard/dashboards/project/key_pairs/templates/key_pairs/import.html @@ -1,6 +1,6 @@ {% extends 'base.html' %} {% load i18n %} -{% block title %}{% trans "Import Key Pair" %}{% endblock %} +{% block title %}{{ page_title }}{% endblock %} {% block main %} {% include 'project/access_and_security/keypairs/_import.html' %} diff --git a/openstack_dashboard/dashboards/project/access_and_security/keypairs/tests.py b/openstack_dashboard/dashboards/project/key_pairs/tests.py similarity index 75% rename from openstack_dashboard/dashboards/project/access_and_security/keypairs/tests.py rename to openstack_dashboard/dashboards/project/key_pairs/tests.py index 75e10fcc0b..c7f9027886 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/keypairs/tests.py +++ b/openstack_dashboard/dashboards/project/key_pairs/tests.py @@ -18,50 +18,58 @@ from django.core.urlresolvers import reverse from django import http - -from mox3.mox import IsA # noqa +from mox3.mox import IsA import six from openstack_dashboard import api -from openstack_dashboard.dashboards.project.access_and_security.\ - keypairs.forms import CreateKeypair -from openstack_dashboard.dashboards.project.access_and_security.\ - keypairs.forms import KEYPAIR_ERROR_MESSAGES +from openstack_dashboard.dashboards.project.key_pairs.forms \ + import CreateKeypair +from openstack_dashboard.dashboards.project.key_pairs.forms \ + import KEYPAIR_ERROR_MESSAGES from openstack_dashboard.test import helpers as test +from openstack_dashboard.usage import quotas -INDEX_VIEW_URL = reverse('horizon:project:access_and_security:index') +INDEX_URL = reverse('horizon:project:key_pairs:index') -class KeyPairViewTests(test.TestCase): +class KeyPairTests(test.TestCase): + @test.create_stubs({ + api.nova: ('keypair_list',), + quotas: ('tenant_quota_usages',), + }) + def test_index(self): + keypairs = self.keypairs.list() + quota_data = self.quota_usages.first() + + quotas.tenant_quota_usages(IsA(http.HttpRequest)).MultipleTimes() \ + .AndReturn(quota_data) + api.nova.keypair_list(IsA(http.HttpRequest)) \ + .AndReturn(keypairs) + + self.mox.ReplayAll() + res = self.client.get(INDEX_URL) + + self.assertTemplateUsed(res, 'horizon/common/_data_table_view.html') + self.assertItemsEqual(res.context['keypairs_table'].data, keypairs) + + @test.create_stubs({api.nova: ('keypair_list', 'keypair_delete')}) def test_delete_keypair(self): keypair = self.keypairs.first() - self.mox.StubOutWithMock(api.network, 'floating_ip_supported') - self.mox.StubOutWithMock(api.nova, 'keypair_list') - self.mox.StubOutWithMock(api.nova, 'keypair_delete') - - # floating_ip_supported is called in Floating IP tab allowed(). - api.network.floating_ip_supported(IsA(http.HttpRequest)) \ - .AndReturn(True) api.nova.keypair_list(IsA(http.HttpRequest)) \ .AndReturn(self.keypairs.list()) api.nova.keypair_delete(IsA(http.HttpRequest), keypair.name) self.mox.ReplayAll() formData = {'action': 'keypairs__delete__%s' % keypair.name} - res = self.client.post(INDEX_VIEW_URL, formData) - self.assertRedirectsNoFollow(res, INDEX_VIEW_URL) + res = self.client.post(INDEX_URL, formData) + self.assertRedirectsNoFollow(res, INDEX_URL) + @test.create_stubs({api.nova: ('keypair_list', 'keypair_delete')}) def test_delete_keypair_exception(self): keypair = self.keypairs.first() - self.mox.StubOutWithMock(api.network, 'floating_ip_supported') - self.mox.StubOutWithMock(api.nova, 'keypair_list') - self.mox.StubOutWithMock(api.nova, 'keypair_delete') - # floating_ip_supported is called in Floating IP tab allowed(). - api.network.floating_ip_supported(IsA(http.HttpRequest)) \ - .AndReturn(True) api.nova.keypair_list(IsA(http.HttpRequest)) \ .AndReturn(self.keypairs.list()) api.nova.keypair_delete(IsA(http.HttpRequest), keypair.name) \ @@ -69,56 +77,56 @@ class KeyPairViewTests(test.TestCase): self.mox.ReplayAll() formData = {'action': 'keypairs__delete__%s' % keypair.name} - res = self.client.post(INDEX_VIEW_URL, formData) - self.assertRedirectsNoFollow(res, INDEX_VIEW_URL) + res = self.client.post(INDEX_URL, formData) + self.assertRedirectsNoFollow(res, INDEX_URL) def test_create_keypair_get(self): res = self.client.get( - reverse('horizon:project:access_and_security:keypairs:create')) + reverse('horizon:project:key_pairs:create')) self.assertTemplateUsed( - res, 'project/access_and_security/keypairs/create.html') + res, 'project/key_pairs/create.html') def test_download_keypair_get(self): keypair_name = "keypair" context = {'keypair_name': keypair_name} - url = reverse('horizon:project:access_and_security:keypairs:download', + url = reverse('horizon:project:key_pairs:download', kwargs={'keypair_name': keypair_name}) res = self.client.get(url, context) self.assertTemplateUsed( - res, 'project/access_and_security/keypairs/download.html') + res, 'project/key_pairs/download.html') + @test.create_stubs({api.nova: ('keypair_create',)}) def test_generate_keypair_get(self): keypair = self.keypairs.first() keypair.private_key = "secret" - self.mox.StubOutWithMock(api.nova, 'keypair_create') api.nova.keypair_create(IsA(http.HttpRequest), keypair.name).AndReturn(keypair) self.mox.ReplayAll() context = {'keypair_name': keypair.name} - url = reverse('horizon:project:access_and_security:keypairs:generate', + url = reverse('horizon:project:key_pairs:generate', kwargs={'keypair_name': keypair.name}) res = self.client.get(url, context) self.assertTrue(res.has_header('content-disposition')) + @test.create_stubs({api.nova: ('keypair_get',)}) def test_keypair_detail_get(self): keypair = self.keypairs.first() - keypair.private_key = "secrete" + keypair.private_key = "secret" - self.mox.StubOutWithMock(api.nova, 'keypair_get') api.nova.keypair_get(IsA(http.HttpRequest), keypair.name).AndReturn(keypair) self.mox.ReplayAll() context = {'keypair_name': keypair.name} - url = reverse('horizon:project:access_and_security:keypairs:detail', + url = reverse('horizon:project:key_pairs:detail', kwargs={'keypair_name': keypair.name}) res = self.client.get(url, context) self.assertContains(res, "
%s
" % keypair.name, 1, 200) - @test.create_stubs({api.nova: ("keypair_create", "keypair_delete")}) + @test.create_stubs({api.nova: ("keypair_create", "keypair_delete",)}) def test_regenerate_keypair_get(self): keypair = self.keypairs.first() keypair.private_key = "secret" @@ -127,7 +135,7 @@ class KeyPairViewTests(test.TestCase): api.nova.keypair_create(IsA(http.HttpRequest), keypair.name).AndReturn(keypair) self.mox.ReplayAll() - url = reverse('horizon:project:access_and_security:keypairs:generate', + url = reverse('horizon:project:key_pairs:generate', kwargs={'keypair_name': keypair.name, 'optional': optional_param}) res = self.client.get(url) @@ -147,7 +155,7 @@ class KeyPairViewTests(test.TestCase): formData = {'method': 'ImportKeypair', 'name': key1_name, 'public_key': public_key} - url = reverse('horizon:project:access_and_security:keypairs:import') + url = reverse('horizon:project:key_pairs:import') res = self.client.post(url, formData) self.assertMessageCount(res, success=1) @@ -163,7 +171,7 @@ class KeyPairViewTests(test.TestCase): formData = {'method': 'ImportKeypair', 'name': key_name, 'public_key': public_key} - url = reverse('horizon:project:access_and_security:keypairs:import') + url = reverse('horizon:project:key_pairs:import') res = self.client.post(url, formData, follow=True) self.assertEqual(res.redirect_chain, []) msg = 'Unable to import key pair.' @@ -176,7 +184,7 @@ class KeyPairViewTests(test.TestCase): formData = {'method': 'ImportKeypair', 'name': key_name, 'public_key': public_key} - url = reverse('horizon:project:access_and_security:keypairs:import') + url = reverse('horizon:project:key_pairs:import') res = self.client.post(url, formData, follow=True) self.assertEqual(res.redirect_chain, []) msg = six.text_type(KEYPAIR_ERROR_MESSAGES['invalid']) @@ -189,7 +197,7 @@ class KeyPairViewTests(test.TestCase): formData = {'method': 'ImportKeypair', 'name': key_name, 'public_key': public_key} - url = reverse('horizon:project:access_and_security:keypairs:import') + url = reverse('horizon:project:key_pairs:import') res = self.client.post(url, formData, follow=True) self.assertEqual(res.redirect_chain, []) msg = six.text_type(KEYPAIR_ERROR_MESSAGES['invalid']) @@ -204,12 +212,12 @@ class KeyPairViewTests(test.TestCase): self.mox.ReplayAll() context = {'keypair_name': keypair.name} - url = reverse('horizon:project:access_and_security:keypairs:generate', + url = reverse('horizon:project:key_pairs:generate', kwargs={'keypair_name': keypair.name}) res = self.client.get(url, context) self.assertRedirectsNoFollow( - res, reverse('horizon:project:access_and_security:index')) + res, reverse('horizon:project:key_pairs:index')) @test.create_stubs({api.nova: ("keypair_import",)}) def test_import_keypair_with_regex_defined_name(self): @@ -224,7 +232,7 @@ class KeyPairViewTests(test.TestCase): formData = {'method': 'ImportKeypair', 'name': key1_name, 'public_key': public_key} - url = reverse('horizon:project:access_and_security:keypairs:import') + url = reverse('horizon:project:key_pairs:import') res = self.client.post(url, formData) self.assertMessageCount(res, success=1) @@ -239,7 +247,7 @@ class KeyPairViewTests(test.TestCase): self.mox.ReplayAll() context = {'keypair_name': keypair.name} - url = reverse('horizon:project:access_and_security:keypairs:generate', + url = reverse('horizon:project:key_pairs:generate', kwargs={'keypair_name': keypair.name}) res = self.client.get(url, context) @@ -248,11 +256,11 @@ class KeyPairViewTests(test.TestCase): def test_download_with_regex_name_get(self): keypair_name = "key pair-regex_name-0123456789" context = {'keypair_name': keypair_name} - url = reverse('horizon:project:access_and_security:keypairs:download', + url = reverse('horizon:project:key_pairs:download', kwargs={'keypair_name': keypair_name}) res = self.client.get(url, context) self.assertTemplateUsed( - res, 'project/access_and_security/keypairs/download.html') + res, 'project/key_pairs/download.html') @test.create_stubs({api.nova: ('keypair_list',)}) def test_create_duplicate_keypair(self): diff --git a/openstack_dashboard/dashboards/project/access_and_security/keypairs/urls.py b/openstack_dashboard/dashboards/project/key_pairs/urls.py similarity index 91% rename from openstack_dashboard/dashboards/project/access_and_security/keypairs/urls.py rename to openstack_dashboard/dashboards/project/key_pairs/urls.py index fb1a418bfa..afe68a2977 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/keypairs/urls.py +++ b/openstack_dashboard/dashboards/project/key_pairs/urls.py @@ -17,12 +17,10 @@ # under the License. from django.conf.urls import url - -from openstack_dashboard.dashboards.project.access_and_security.keypairs \ - import views - +from openstack_dashboard.dashboards.project.key_pairs import views urlpatterns = [ + url(r'^$', views.IndexView.as_view(), name='index'), url(r'^create/$', views.CreateView.as_view(), name='create'), url(r'^import/$', views.ImportView.as_view(), name='import'), url(r'^(?P[^/]+)/download/$', views.DownloadView.as_view(), diff --git a/openstack_dashboard/dashboards/project/access_and_security/keypairs/views.py b/openstack_dashboard/dashboards/project/key_pairs/views.py similarity index 60% rename from openstack_dashboard/dashboards/project/access_and_security/keypairs/views.py rename to openstack_dashboard/dashboards/project/key_pairs/views.py index 2c774c24ad..9b920ababe 100644 --- a/openstack_dashboard/dashboards/project/access_and_security/keypairs/views.py +++ b/openstack_dashboard/dashboards/project/key_pairs/views.py @@ -1,8 +1,4 @@ -# Copyright 2012 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2012 Nebula, Inc. +# Copyright 2016 Cisco Systems # # 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 @@ -16,9 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. -""" -Views for managing keypairs. -""" from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse_lazy from django import http @@ -27,30 +20,49 @@ from django.utils.decorators import method_decorator from django.utils.translation import ugettext_lazy as _ from django.views.decorators.cache import cache_control from django.views.decorators.cache import never_cache -from django.views.generic import View # noqa from horizon import exceptions from horizon import forms +from horizon import messages +from horizon import tables from horizon.utils import memoized from horizon import views +from openstack_dashboard.api import nova +from openstack_dashboard.dashboards.project.key_pairs \ + import forms as key_pairs_forms +from openstack_dashboard.dashboards.project.key_pairs \ + import tables as key_pairs_tables +from openstack_dashboard import policy -from openstack_dashboard import api -from openstack_dashboard.dashboards.project.access_and_security.keypairs \ - import forms as project_forms +class IndexView(tables.DataTableView): + table_class = key_pairs_tables.KeyPairsTable + page_title = _("Key Pairs") + + def get_data(self): + if not policy.check( + (("compute", "os_compute_api:os-keypairs:index"),), + self.request): + msg = _("Insufficient privilege level to retrieve key pair list.") + messages.info(self.request, msg) + return [] + try: + keypairs = nova.keypair_list(self.request) + except Exception: + keypairs = [] + exceptions.handle(self.request, + _('Unable to retrieve key pair list.')) + return keypairs class CreateView(forms.ModalFormView): - form_class = project_forms.CreateKeypair - form_id = "create_keypair_form" - template_name = 'project/access_and_security/keypairs/create.html' - submit_label = _("Create Key Pair") + form_class = key_pairs_forms.CreateKeypair + template_name = 'project/key_pairs/create.html' submit_url = reverse_lazy( - "horizon:project:access_and_security:keypairs:create") - success_url = 'horizon:project:access_and_security:keypairs:download' - page_title = _("Create Key Pair") - cancel_url = reverse_lazy( - "horizon:project:access_and_security:index") + "horizon:project:key_pairs:create") + success_url = 'horizon:project:key_pairs:download' + submit_label = page_title = _("Create Key Pair") + cancel_url = reverse_lazy("horizon:project:key_pairs:index") def get_success_url(self): return reverse(self.success_url, @@ -58,30 +70,28 @@ class CreateView(forms.ModalFormView): class ImportView(forms.ModalFormView): - form_class = project_forms.ImportKeypair - form_id = "import_keypair_form" - template_name = 'project/access_and_security/keypairs/import.html' - submit_label = _("Import Key Pair") + form_class = key_pairs_forms.ImportKeypair + template_name = 'project/key_pairs/import.html' submit_url = reverse_lazy( - "horizon:project:access_and_security:keypairs:import") - success_url = reverse_lazy('horizon:project:access_and_security:index') - page_title = _("Import Key Pair") + "horizon:project:key_pairs:import") + success_url = reverse_lazy('horizon:project:key_pairs:index') + submit_label = page_title = _("Import Key Pair") def get_object_id(self, keypair): return keypair.name class DetailView(views.HorizonTemplateView): - template_name = 'project/access_and_security/keypairs/detail.html' + template_name = 'project/key_pairs/detail.html' page_title = _("Key Pair Details") @memoized.memoized_method def _get_data(self): try: - keypair = api.nova.keypair_get(self.request, - self.kwargs['keypair_name']) + keypair = nova.keypair_get(self.request, + self.kwargs['keypair_name']) except Exception: - redirect = reverse('horizon:project:access_and_security:index') + redirect = reverse('horizon:project:key_pairs:index') msg = _('Unable to retrieve details for keypair "%s".')\ % (self.kwargs['keypair_name']) exceptions.handle(self.request, msg, @@ -96,14 +106,14 @@ class DetailView(views.HorizonTemplateView): class DownloadView(views.HorizonTemplateView): - template_name = 'project/access_and_security/keypairs/download.html' + template_name = 'project/key_pairs/download.html' page_title = _("Download Key Pair") def get_context_data(self, keypair_name=None): return {'keypair_name': keypair_name} -class GenerateView(View): +class GenerateView(views.HorizonTemplateView): # TODO(Itxaka): Remove cache_control in django >= 1.9 # https://code.djangoproject.com/ticket/13008 @method_decorator(cache_control(max_age=0, no_cache=True, @@ -112,11 +122,11 @@ class GenerateView(View): def get(self, request, keypair_name=None, optional=None): try: if optional == "regenerate": - api.nova.keypair_delete(request, keypair_name) + nova.keypair_delete(request, keypair_name) - keypair = api.nova.keypair_create(request, keypair_name) + keypair = nova.keypair_create(request, keypair_name) except Exception: - redirect = reverse('horizon:project:access_and_security:index') + redirect = reverse('horizon:project:key_pairs:index') exceptions.handle(self.request, _('Unable to create key pair: %(exc)s'), redirect=redirect) diff --git a/openstack_dashboard/enabled/_1080_project_key_pairs_panel.py b/openstack_dashboard/enabled/_1080_project_key_pairs_panel.py new file mode 100644 index 0000000000..6cf23b506f --- /dev/null +++ b/openstack_dashboard/enabled/_1080_project_key_pairs_panel.py @@ -0,0 +1,5 @@ +PANEL_DASHBOARD = 'project' +PANEL_GROUP = 'compute' +PANEL = 'key_pairs' + +ADD_PANEL = 'openstack_dashboard.dashboards.project.key_pairs.panel.KeyPairs' diff --git a/releasenotes/notes/bp/reorganise-access-and-security-ea7780aa9e7b83e7.yaml b/releasenotes/notes/bp/reorganise-access-and-security-ea7780aa9e7b83e7.yaml new file mode 100644 index 0000000000..89f7e98d6b --- /dev/null +++ b/releasenotes/notes/bp/reorganise-access-and-security-ea7780aa9e7b83e7.yaml @@ -0,0 +1,5 @@ +--- +features: + - The Access & Security panel's tabs have been moved to their own panels for + clearer navigation and better performance. Key Pairs now resides in the + Compute panel group.