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
This commit is contained in:
Rob Cresswell 2016-12-07 16:14:18 +00:00
parent 763d5dd4b3
commit 1a58a1fd60
20 changed files with 173 additions and 174 deletions

View File

@ -224,12 +224,10 @@ class FloatingIpViewTests(test.TestCase):
'tenant_floating_ip_list', 'tenant_floating_ip_list',
'security_group_list', 'security_group_list',
'floating_ip_pools_list',), 'floating_ip_pools_list',),
api.nova: ('keypair_list', api.nova: ('server_list',),
'server_list',),
quotas: ('tenant_quota_usages',), quotas: ('tenant_quota_usages',),
api.base: ('is_service_enabled',)}) api.base: ('is_service_enabled',)})
def test_allocate_button_attributes(self): def test_allocate_button_attributes(self):
keypairs = self.keypairs.list()
floating_ips = self.floating_ips.list() floating_ips = self.floating_ips.list()
floating_pools = self.pools.list() floating_pools = self.pools.list()
quota_data = self.quota_usages.first() quota_data = self.quota_usages.first()
@ -248,9 +246,6 @@ class FloatingIpViewTests(test.TestCase):
api.network.floating_ip_pools_list( api.network.floating_ip_pools_list(
IsA(http.HttpRequest)) \ IsA(http.HttpRequest)) \
.AndReturn(floating_pools) .AndReturn(floating_pools)
api.nova.keypair_list(
IsA(http.HttpRequest)) \
.AndReturn(keypairs)
api.nova.server_list( api.nova.server_list(
IsA(http.HttpRequest)) \ IsA(http.HttpRequest)) \
.AndReturn([self.servers.list(), False]) .AndReturn([self.servers.list(), False])
@ -286,12 +281,10 @@ class FloatingIpViewTests(test.TestCase):
'tenant_floating_ip_list', 'tenant_floating_ip_list',
'security_group_list', 'security_group_list',
'floating_ip_pools_list',), 'floating_ip_pools_list',),
api.nova: ('keypair_list', api.nova: ('server_list',),
'server_list',),
quotas: ('tenant_quota_usages',), quotas: ('tenant_quota_usages',),
api.base: ('is_service_enabled',)}) api.base: ('is_service_enabled',)})
def test_allocate_button_disabled_when_quota_exceeded(self): def test_allocate_button_disabled_when_quota_exceeded(self):
keypairs = self.keypairs.list()
floating_ips = self.floating_ips.list() floating_ips = self.floating_ips.list()
floating_pools = self.pools.list() floating_pools = self.pools.list()
quota_data = self.quota_usages.first() quota_data = self.quota_usages.first()
@ -310,9 +303,6 @@ class FloatingIpViewTests(test.TestCase):
api.network.floating_ip_pools_list( api.network.floating_ip_pools_list(
IsA(http.HttpRequest)) \ IsA(http.HttpRequest)) \
.AndReturn(floating_pools) .AndReturn(floating_pools)
api.nova.keypair_list(
IsA(http.HttpRequest)) \
.AndReturn(keypairs)
api.nova.server_list( api.nova.server_list(
IsA(http.HttpRequest)) \ IsA(http.HttpRequest)) \
.AndReturn([self.servers.list(), False]) .AndReturn([self.servers.list(), False])

View File

@ -32,8 +32,6 @@ from openstack_dashboard.dashboards.project.access_and_security.\
api_access.tables import EndpointsTable api_access.tables import EndpointsTable
from openstack_dashboard.dashboards.project.access_and_security.\ from openstack_dashboard.dashboards.project.access_and_security.\
floating_ips.tables import FloatingIPsTable 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.\ from openstack_dashboard.dashboards.project.access_and_security.\
security_groups.tables import SecurityGroupsTable security_groups.tables import SecurityGroupsTable
@ -58,23 +56,6 @@ class SecurityGroupsTab(tabs.TableTab):
return sorted(security_groups, key=lambda group: group.name) 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): class FloatingIPsTab(tabs.TableTab):
table_classes = (FloatingIPsTable,) table_classes = (FloatingIPsTable,)
name = _("Floating IPs") name = _("Floating IPs")
@ -146,5 +127,5 @@ class APIAccessTab(tabs.TableTab):
class AccessAndSecurityTabs(tabs.TabGroup): class AccessAndSecurityTabs(tabs.TabGroup):
slug = "access_security_tabs" slug = "access_security_tabs"
tabs = (SecurityGroupsTab, KeypairsTab, FloatingIPsTab, APIAccessTab) tabs = (SecurityGroupsTab, FloatingIPsTab, APIAccessTab)
sticky = True sticky = True

View File

@ -1,19 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% blocktrans %}Download Key Pair{% endblocktrans %}{% endblock %}
{% block main %}
<div class="modal-header">
<h3>{% blocktrans %}The key pair &quot;{{ keypair_name }}&quot; should download automatically. If not, use the link below.{% endblocktrans %}</h3>
</div>
<div class="modal-body clearfix">
<a href="{% url 'horizon:project:access_and_security:keypairs:generate' keypair_name "regenerate" %}">
{% blocktrans %}Re-generate key pair &quot;{{ keypair_name }}&quot;{% endblocktrans %}
</a>
</div>
<script type="text/javascript" charset="utf-8">
$(document).ready(function() {
document.location = '{% url 'horizon:project:access_and_security:keypairs:generate' keypair_name %}';
});
</script>
{% endblock %}

View File

@ -41,13 +41,11 @@ class AccessAndSecurityTests(test.TestCase):
'tenant_floating_ip_list', 'tenant_floating_ip_list',
'floating_ip_pools_list', 'floating_ip_pools_list',
'security_group_list',), 'security_group_list',),
api.nova: ('keypair_list', api.nova: ('server_list',),
'server_list',),
api.base: ('is_service_enabled',), api.base: ('is_service_enabled',),
quotas: ('tenant_quota_usages',), quotas: ('tenant_quota_usages',),
api.keystone: ('list_ec2_credentials',)}) api.keystone: ('list_ec2_credentials',)})
def _test_index(self, ec2_enabled=True, instanceless_ips=False): def _test_index(self, ec2_enabled=True, instanceless_ips=False):
keypairs = self.keypairs.list()
sec_groups = self.security_groups.list() sec_groups = self.security_groups.list()
floating_ips = self.floating_ips.list() floating_ips = self.floating_ips.list()
floating_pools = self.pools.list() floating_pools = self.pools.list()
@ -62,8 +60,6 @@ class AccessAndSecurityTests(test.TestCase):
if not instanceless_ips: if not instanceless_ips:
api.nova.server_list(IsA(http.HttpRequest)) \ api.nova.server_list(IsA(http.HttpRequest)) \
.AndReturn([self.servers.list(), False]) .AndReturn([self.servers.list(), False])
api.nova.keypair_list(IsA(http.HttpRequest)) \
.AndReturn(keypairs)
api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \ api.network.tenant_floating_ip_list(IsA(http.HttpRequest)) \
.AndReturn(floating_ips) .AndReturn(floating_ips)
api.network.floating_ip_pools_list(IsA(http.HttpRequest)) \ api.network.floating_ip_pools_list(IsA(http.HttpRequest)) \
@ -87,7 +83,6 @@ class AccessAndSecurityTests(test.TestCase):
res = self.client.get(INDEX_URL) res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, 'project/access_and_security/index.html') 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, self.assertItemsEqual(res.context['floating_ips_table'].data,
floating_ips) floating_ips)
@ -171,12 +166,10 @@ class SecurityGroupTabTests(test.TestCase):
'tenant_floating_ip_list', 'tenant_floating_ip_list',
'security_group_list', 'security_group_list',
'floating_ip_pools_list',), 'floating_ip_pools_list',),
api.nova: ('keypair_list', api.nova: ('server_list',),
'server_list',),
quotas: ('tenant_quota_usages',), quotas: ('tenant_quota_usages',),
api.base: ('is_service_enabled',)}) api.base: ('is_service_enabled',)})
def test_create_button_attributes(self): def test_create_button_attributes(self):
keypairs = self.keypairs.list()
floating_ips = self.floating_ips.list() floating_ips = self.floating_ips.list()
floating_pools = self.pools.list() floating_pools = self.pools.list()
sec_groups = self.security_groups.list() sec_groups = self.security_groups.list()
@ -195,9 +188,6 @@ class SecurityGroupTabTests(test.TestCase):
api.network.security_group_list( api.network.security_group_list(
IsA(http.HttpRequest)) \ IsA(http.HttpRequest)) \
.AndReturn(sec_groups) .AndReturn(sec_groups)
api.nova.keypair_list(
IsA(http.HttpRequest)) \
.AndReturn(keypairs)
api.nova.server_list( api.nova.server_list(
IsA(http.HttpRequest)) \ IsA(http.HttpRequest)) \
.AndReturn([self.servers.list(), False]) .AndReturn([self.servers.list(), False])
@ -235,13 +225,11 @@ class SecurityGroupTabTests(test.TestCase):
'tenant_floating_ip_list', 'tenant_floating_ip_list',
'security_group_list', 'security_group_list',
'floating_ip_pools_list',), 'floating_ip_pools_list',),
api.nova: ('keypair_list', api.nova: ('server_list',),
'server_list',),
quotas: ('tenant_quota_usages',), quotas: ('tenant_quota_usages',),
api.base: ('is_service_enabled',)}) api.base: ('is_service_enabled',)})
def _test_create_button_disabled_when_quota_exceeded(self, def _test_create_button_disabled_when_quota_exceeded(self,
network_enabled): network_enabled):
keypairs = self.keypairs.list()
floating_ips = self.floating_ips.list() floating_ips = self.floating_ips.list()
floating_pools = self.pools.list() floating_pools = self.pools.list()
sec_groups = self.security_groups.list() sec_groups = self.security_groups.list()
@ -260,9 +248,6 @@ class SecurityGroupTabTests(test.TestCase):
api.network.security_group_list( api.network.security_group_list(
IsA(http.HttpRequest)) \ IsA(http.HttpRequest)) \
.AndReturn(sec_groups) .AndReturn(sec_groups)
api.nova.keypair_list(
IsA(http.HttpRequest)) \
.AndReturn(keypairs)
api.nova.server_list( api.nova.server_list(
IsA(http.HttpRequest)) \ IsA(http.HttpRequest)) \
.AndReturn([self.servers.list(), False]) .AndReturn([self.servers.list(), False])

View File

@ -23,8 +23,6 @@ from openstack_dashboard.dashboards.project.access_and_security.\
api_access import urls as api_access_urls api_access import urls as api_access_urls
from openstack_dashboard.dashboards.project.access_and_security.\ from openstack_dashboard.dashboards.project.access_and_security.\
floating_ips import urls as fip_urls 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.\ from openstack_dashboard.dashboards.project.access_and_security.\
security_groups import urls as sec_group_urls security_groups import urls as sec_group_urls
from openstack_dashboard.dashboards.project.access_and_security import views 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 = [ urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'), url(r'^$', views.IndexView.as_view(), name='index'),
url(r'api_access/', include(api_access_urls, namespace='api_access')), 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'floating_ips/', include(fip_urls, namespace='floating_ips')),
url(r'security_groups/', url(r'security_groups/',
include(sec_group_urls, namespace='security_groups')), include(sec_group_urls, namespace='security_groups')),

View File

@ -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"),)

View File

@ -12,7 +12,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # 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 ugettext_lazy as _
from django.utils.translation import ungettext_lazy from django.utils.translation import ungettext_lazy
@ -51,19 +51,19 @@ class DeleteKeyPairs(tables.DeleteAction):
class ImportKeyPair(tables.LinkAction): class ImportKeyPair(tables.LinkAction):
name = "import" name = "import"
verbose_name = _("Import Key Pair") verbose_name = _("Import Key Pair")
url = "horizon:project:access_and_security:keypairs:import" url = "horizon:project:key_pairs:import"
classes = ("ajax-modal",) classes = ("ajax-modal",)
icon = "upload" icon = "upload"
policy_rules = (("compute", "compute_extension:keypairs:create"),) policy_rules = (("compute", "os_compute_api:os-keypairs:create"),)
class CreateKeyPair(tables.LinkAction): class CreateKeyPair(tables.LinkAction):
name = "create" name = "create"
verbose_name = _("Create Key Pair") verbose_name = _("Create Key Pair")
url = "horizon:project:access_and_security:keypairs:create" url = "horizon:project:key_pairs:create"
classes = ("ajax-modal",) classes = ("ajax-modal",)
icon = "plus" icon = "plus"
policy_rules = (("compute", "compute_extension:keypairs:create"),) policy_rules = (("compute", "os_compute_api:os-keypairs:create"),)
def allowed(self, request, keypair=None): def allowed(self, request, keypair=None):
usages = quotas.tenant_quota_usages(request) usages = quotas.tenant_quota_usages(request)
@ -90,9 +90,8 @@ class KeypairsFilterAction(tables.FilterAction):
if query in keypair.name.lower()] if query in keypair.name.lower()]
class KeypairsTable(tables.DataTable): class KeyPairsTable(tables.DataTable):
detail_link = "horizon:project:access_and_security:keypairs:detail" detail_link = "horizon:project:key_pairs:detail"
name = tables.Column("name", verbose_name=_("Key Pair Name"), name = tables.Column("name", verbose_name=_("Key Pair Name"),
link=detail_link) link=detail_link)
fingerprint = tables.Column("fingerprint", verbose_name=_("Fingerprint")) fingerprint = tables.Column("fingerprint", verbose_name=_("Fingerprint"))

View File

@ -2,7 +2,6 @@
{% load i18n %} {% load i18n %}
{% block modal-body-right %} {% block modal-body-right %}
<h3>{% trans "Description:" %}</h3> <p>{% 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)." %}</p>
<p>{% 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)." %}</p> <p>{% trans "Protect and use the key as you would any normal SSH private key." %}</p>
<p>{% trans "Protect and use the key as you would any normal ssh private key." %}</p>
{% endblock %} {% endblock %}

View File

@ -2,7 +2,6 @@
{% load i18n %} {% load i18n %}
{% block modal-body-right %} {% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans "Key Pairs are how you login to your instance after it is launched." %}</p> <p>{% trans "Key Pairs are how you login to your instance after it is launched." %}</p>
<p>{% trans "Choose a key pair name you will recognise and paste your SSH public key into the space provided." %}</p> <p>{% trans "Choose a key pair name you will recognise and paste your SSH public key into the space provided." %}</p>
<p>{% trans "SSH key pairs can be generated with the ssh-keygen command:" %}</p> <p>{% trans "SSH key pairs can be generated with the ssh-keygen command:" %}</p>

View File

@ -1,6 +1,6 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Create Key Pair" %}{% endblock %} {% block title %}{{ page_title }}{% endblock %}
{% block main %} {% block main %}
{% include 'project/access_and_security/keypairs/_create.html' %} {% include 'project/access_and_security/keypairs/_create.html' %}

View File

@ -1,7 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n sizeformat %} {% load i18n sizeformat %}
{% block title %}{% trans "Key Pair Details" %}{% endblock %} {% block title %}{{ page_title }}{% endblock %}
{% block page_header %} {% block page_header %}
{% include "horizon/common/_detail_header.html" %} {% include "horizon/common/_detail_header.html" %}

View File

@ -0,0 +1,17 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{{ page_title }}{% endblock %}
{% block main %}
<div class="alert alert-info">
{% blocktrans %}The key pair &quot;{{ keypair_name }}&quot; should download automatically. If not, use the button below.{% endblocktrans %}
</div>
<a class="btn btn-default" href="{% url 'horizon:project:key_pairs:generate' keypair_name "regenerate" %}">
{% blocktrans %}Regenerate and download Key Pair &quot;{{ keypair_name }}&quot;{% endblocktrans %}
</a>
<script type="text/javascript" charset="utf-8">
$(document).ready(function() {
document.location = '{% url 'horizon:project:key_pairs:generate' keypair_name %}';
});
</script>
{% endblock %}

View File

@ -1,6 +1,6 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Import Key Pair" %}{% endblock %} {% block title %}{{ page_title }}{% endblock %}
{% block main %} {% block main %}
{% include 'project/access_and_security/keypairs/_import.html' %} {% include 'project/access_and_security/keypairs/_import.html' %}

View File

@ -18,50 +18,58 @@
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django import http from django import http
from mox3.mox import IsA
from mox3.mox import IsA # noqa
import six import six
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard.dashboards.project.access_and_security.\ from openstack_dashboard.dashboards.project.key_pairs.forms \
keypairs.forms import CreateKeypair import CreateKeypair
from openstack_dashboard.dashboards.project.access_and_security.\ from openstack_dashboard.dashboards.project.key_pairs.forms \
keypairs.forms import KEYPAIR_ERROR_MESSAGES import KEYPAIR_ERROR_MESSAGES
from openstack_dashboard.test import helpers as test 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): def test_delete_keypair(self):
keypair = self.keypairs.first() 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)) \ api.nova.keypair_list(IsA(http.HttpRequest)) \
.AndReturn(self.keypairs.list()) .AndReturn(self.keypairs.list())
api.nova.keypair_delete(IsA(http.HttpRequest), keypair.name) api.nova.keypair_delete(IsA(http.HttpRequest), keypair.name)
self.mox.ReplayAll() self.mox.ReplayAll()
formData = {'action': 'keypairs__delete__%s' % keypair.name} formData = {'action': 'keypairs__delete__%s' % keypair.name}
res = self.client.post(INDEX_VIEW_URL, formData) res = self.client.post(INDEX_URL, formData)
self.assertRedirectsNoFollow(res, INDEX_VIEW_URL) self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({api.nova: ('keypair_list', 'keypair_delete')})
def test_delete_keypair_exception(self): def test_delete_keypair_exception(self):
keypair = self.keypairs.first() 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)) \ api.nova.keypair_list(IsA(http.HttpRequest)) \
.AndReturn(self.keypairs.list()) .AndReturn(self.keypairs.list())
api.nova.keypair_delete(IsA(http.HttpRequest), keypair.name) \ api.nova.keypair_delete(IsA(http.HttpRequest), keypair.name) \
@ -69,56 +77,56 @@ class KeyPairViewTests(test.TestCase):
self.mox.ReplayAll() self.mox.ReplayAll()
formData = {'action': 'keypairs__delete__%s' % keypair.name} formData = {'action': 'keypairs__delete__%s' % keypair.name}
res = self.client.post(INDEX_VIEW_URL, formData) res = self.client.post(INDEX_URL, formData)
self.assertRedirectsNoFollow(res, INDEX_VIEW_URL) self.assertRedirectsNoFollow(res, INDEX_URL)
def test_create_keypair_get(self): def test_create_keypair_get(self):
res = self.client.get( res = self.client.get(
reverse('horizon:project:access_and_security:keypairs:create')) reverse('horizon:project:key_pairs:create'))
self.assertTemplateUsed( self.assertTemplateUsed(
res, 'project/access_and_security/keypairs/create.html') res, 'project/key_pairs/create.html')
def test_download_keypair_get(self): def test_download_keypair_get(self):
keypair_name = "keypair" keypair_name = "keypair"
context = {'keypair_name': keypair_name} 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}) kwargs={'keypair_name': keypair_name})
res = self.client.get(url, context) res = self.client.get(url, context)
self.assertTemplateUsed( 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): def test_generate_keypair_get(self):
keypair = self.keypairs.first() keypair = self.keypairs.first()
keypair.private_key = "secret" keypair.private_key = "secret"
self.mox.StubOutWithMock(api.nova, 'keypair_create')
api.nova.keypair_create(IsA(http.HttpRequest), api.nova.keypair_create(IsA(http.HttpRequest),
keypair.name).AndReturn(keypair) keypair.name).AndReturn(keypair)
self.mox.ReplayAll() self.mox.ReplayAll()
context = {'keypair_name': keypair.name} 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}) kwargs={'keypair_name': keypair.name})
res = self.client.get(url, context) res = self.client.get(url, context)
self.assertTrue(res.has_header('content-disposition')) self.assertTrue(res.has_header('content-disposition'))
@test.create_stubs({api.nova: ('keypair_get',)})
def test_keypair_detail_get(self): def test_keypair_detail_get(self):
keypair = self.keypairs.first() 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), api.nova.keypair_get(IsA(http.HttpRequest),
keypair.name).AndReturn(keypair) keypair.name).AndReturn(keypair)
self.mox.ReplayAll() self.mox.ReplayAll()
context = {'keypair_name': keypair.name} 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}) kwargs={'keypair_name': keypair.name})
res = self.client.get(url, context) res = self.client.get(url, context)
self.assertContains(res, "<dd>%s</dd>" % keypair.name, 1, 200) self.assertContains(res, "<dd>%s</dd>" % 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): def test_regenerate_keypair_get(self):
keypair = self.keypairs.first() keypair = self.keypairs.first()
keypair.private_key = "secret" keypair.private_key = "secret"
@ -127,7 +135,7 @@ class KeyPairViewTests(test.TestCase):
api.nova.keypair_create(IsA(http.HttpRequest), api.nova.keypair_create(IsA(http.HttpRequest),
keypair.name).AndReturn(keypair) keypair.name).AndReturn(keypair)
self.mox.ReplayAll() self.mox.ReplayAll()
url = reverse('horizon:project:access_and_security:keypairs:generate', url = reverse('horizon:project:key_pairs:generate',
kwargs={'keypair_name': keypair.name, kwargs={'keypair_name': keypair.name,
'optional': optional_param}) 'optional': optional_param})
res = self.client.get(url) res = self.client.get(url)
@ -147,7 +155,7 @@ class KeyPairViewTests(test.TestCase):
formData = {'method': 'ImportKeypair', formData = {'method': 'ImportKeypair',
'name': key1_name, 'name': key1_name,
'public_key': public_key} '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) res = self.client.post(url, formData)
self.assertMessageCount(res, success=1) self.assertMessageCount(res, success=1)
@ -163,7 +171,7 @@ class KeyPairViewTests(test.TestCase):
formData = {'method': 'ImportKeypair', formData = {'method': 'ImportKeypair',
'name': key_name, 'name': key_name,
'public_key': public_key} '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) res = self.client.post(url, formData, follow=True)
self.assertEqual(res.redirect_chain, []) self.assertEqual(res.redirect_chain, [])
msg = 'Unable to import key pair.' msg = 'Unable to import key pair.'
@ -176,7 +184,7 @@ class KeyPairViewTests(test.TestCase):
formData = {'method': 'ImportKeypair', formData = {'method': 'ImportKeypair',
'name': key_name, 'name': key_name,
'public_key': public_key} '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) res = self.client.post(url, formData, follow=True)
self.assertEqual(res.redirect_chain, []) self.assertEqual(res.redirect_chain, [])
msg = six.text_type(KEYPAIR_ERROR_MESSAGES['invalid']) msg = six.text_type(KEYPAIR_ERROR_MESSAGES['invalid'])
@ -189,7 +197,7 @@ class KeyPairViewTests(test.TestCase):
formData = {'method': 'ImportKeypair', formData = {'method': 'ImportKeypair',
'name': key_name, 'name': key_name,
'public_key': public_key} '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) res = self.client.post(url, formData, follow=True)
self.assertEqual(res.redirect_chain, []) self.assertEqual(res.redirect_chain, [])
msg = six.text_type(KEYPAIR_ERROR_MESSAGES['invalid']) msg = six.text_type(KEYPAIR_ERROR_MESSAGES['invalid'])
@ -204,12 +212,12 @@ class KeyPairViewTests(test.TestCase):
self.mox.ReplayAll() self.mox.ReplayAll()
context = {'keypair_name': keypair.name} 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}) kwargs={'keypair_name': keypair.name})
res = self.client.get(url, context) res = self.client.get(url, context)
self.assertRedirectsNoFollow( self.assertRedirectsNoFollow(
res, reverse('horizon:project:access_and_security:index')) res, reverse('horizon:project:key_pairs:index'))
@test.create_stubs({api.nova: ("keypair_import",)}) @test.create_stubs({api.nova: ("keypair_import",)})
def test_import_keypair_with_regex_defined_name(self): def test_import_keypair_with_regex_defined_name(self):
@ -224,7 +232,7 @@ class KeyPairViewTests(test.TestCase):
formData = {'method': 'ImportKeypair', formData = {'method': 'ImportKeypair',
'name': key1_name, 'name': key1_name,
'public_key': public_key} '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) res = self.client.post(url, formData)
self.assertMessageCount(res, success=1) self.assertMessageCount(res, success=1)
@ -239,7 +247,7 @@ class KeyPairViewTests(test.TestCase):
self.mox.ReplayAll() self.mox.ReplayAll()
context = {'keypair_name': keypair.name} 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}) kwargs={'keypair_name': keypair.name})
res = self.client.get(url, context) res = self.client.get(url, context)
@ -248,11 +256,11 @@ class KeyPairViewTests(test.TestCase):
def test_download_with_regex_name_get(self): def test_download_with_regex_name_get(self):
keypair_name = "key pair-regex_name-0123456789" keypair_name = "key pair-regex_name-0123456789"
context = {'keypair_name': keypair_name} 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}) kwargs={'keypair_name': keypair_name})
res = self.client.get(url, context) res = self.client.get(url, context)
self.assertTemplateUsed( self.assertTemplateUsed(
res, 'project/access_and_security/keypairs/download.html') res, 'project/key_pairs/download.html')
@test.create_stubs({api.nova: ('keypair_list',)}) @test.create_stubs({api.nova: ('keypair_list',)})
def test_create_duplicate_keypair(self): def test_create_duplicate_keypair(self):

View File

@ -17,12 +17,10 @@
# under the License. # under the License.
from django.conf.urls import url from django.conf.urls import url
from openstack_dashboard.dashboards.project.key_pairs import views
from openstack_dashboard.dashboards.project.access_and_security.keypairs \
import views
urlpatterns = [ urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^create/$', views.CreateView.as_view(), name='create'), url(r'^create/$', views.CreateView.as_view(), name='create'),
url(r'^import/$', views.ImportView.as_view(), name='import'), url(r'^import/$', views.ImportView.as_view(), name='import'),
url(r'^(?P<keypair_name>[^/]+)/download/$', views.DownloadView.as_view(), url(r'^(?P<keypair_name>[^/]+)/download/$', views.DownloadView.as_view(),

View File

@ -1,8 +1,4 @@
# Copyright 2012 United States Government as represented by the # Copyright 2016 Cisco Systems
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2012 Nebula, Inc.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # 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 # License for the specific language governing permissions and limitations
# under the License. # under the License.
"""
Views for managing keypairs.
"""
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy
from django import http 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.utils.translation import ugettext_lazy as _
from django.views.decorators.cache import cache_control from django.views.decorators.cache import cache_control
from django.views.decorators.cache import never_cache from django.views.decorators.cache import never_cache
from django.views.generic import View # noqa
from horizon import exceptions from horizon import exceptions
from horizon import forms from horizon import forms
from horizon import messages
from horizon import tables
from horizon.utils import memoized from horizon.utils import memoized
from horizon import views 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 \ class IndexView(tables.DataTableView):
import forms as project_forms 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): class CreateView(forms.ModalFormView):
form_class = project_forms.CreateKeypair form_class = key_pairs_forms.CreateKeypair
form_id = "create_keypair_form" template_name = 'project/key_pairs/create.html'
template_name = 'project/access_and_security/keypairs/create.html'
submit_label = _("Create Key Pair")
submit_url = reverse_lazy( submit_url = reverse_lazy(
"horizon:project:access_and_security:keypairs:create") "horizon:project:key_pairs:create")
success_url = 'horizon:project:access_and_security:keypairs:download' success_url = 'horizon:project:key_pairs:download'
page_title = _("Create Key Pair") submit_label = page_title = _("Create Key Pair")
cancel_url = reverse_lazy( cancel_url = reverse_lazy("horizon:project:key_pairs:index")
"horizon:project:access_and_security:index")
def get_success_url(self): def get_success_url(self):
return reverse(self.success_url, return reverse(self.success_url,
@ -58,30 +70,28 @@ class CreateView(forms.ModalFormView):
class ImportView(forms.ModalFormView): class ImportView(forms.ModalFormView):
form_class = project_forms.ImportKeypair form_class = key_pairs_forms.ImportKeypair
form_id = "import_keypair_form" template_name = 'project/key_pairs/import.html'
template_name = 'project/access_and_security/keypairs/import.html'
submit_label = _("Import Key Pair")
submit_url = reverse_lazy( submit_url = reverse_lazy(
"horizon:project:access_and_security:keypairs:import") "horizon:project:key_pairs:import")
success_url = reverse_lazy('horizon:project:access_and_security:index') success_url = reverse_lazy('horizon:project:key_pairs:index')
page_title = _("Import Key Pair") submit_label = page_title = _("Import Key Pair")
def get_object_id(self, keypair): def get_object_id(self, keypair):
return keypair.name return keypair.name
class DetailView(views.HorizonTemplateView): 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") page_title = _("Key Pair Details")
@memoized.memoized_method @memoized.memoized_method
def _get_data(self): def _get_data(self):
try: try:
keypair = api.nova.keypair_get(self.request, keypair = nova.keypair_get(self.request,
self.kwargs['keypair_name']) self.kwargs['keypair_name'])
except Exception: 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".')\ msg = _('Unable to retrieve details for keypair "%s".')\
% (self.kwargs['keypair_name']) % (self.kwargs['keypair_name'])
exceptions.handle(self.request, msg, exceptions.handle(self.request, msg,
@ -96,14 +106,14 @@ class DetailView(views.HorizonTemplateView):
class DownloadView(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") page_title = _("Download Key Pair")
def get_context_data(self, keypair_name=None): def get_context_data(self, keypair_name=None):
return {'keypair_name': keypair_name} return {'keypair_name': keypair_name}
class GenerateView(View): class GenerateView(views.HorizonTemplateView):
# TODO(Itxaka): Remove cache_control in django >= 1.9 # TODO(Itxaka): Remove cache_control in django >= 1.9
# https://code.djangoproject.com/ticket/13008 # https://code.djangoproject.com/ticket/13008
@method_decorator(cache_control(max_age=0, no_cache=True, @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): def get(self, request, keypair_name=None, optional=None):
try: try:
if optional == "regenerate": 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: except Exception:
redirect = reverse('horizon:project:access_and_security:index') redirect = reverse('horizon:project:key_pairs:index')
exceptions.handle(self.request, exceptions.handle(self.request,
_('Unable to create key pair: %(exc)s'), _('Unable to create key pair: %(exc)s'),
redirect=redirect) redirect=redirect)

View File

@ -0,0 +1,5 @@
PANEL_DASHBOARD = 'project'
PANEL_GROUP = 'compute'
PANEL = 'key_pairs'
ADD_PANEL = 'openstack_dashboard.dashboards.project.key_pairs.panel.KeyPairs'

View File

@ -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.