Merge "Use POST not GET for keypair generation"
This commit is contained in:
commit
f5b2561a09
@ -14,8 +14,6 @@
|
|||||||
"""API over the nova service."""
|
"""API over the nova service."""
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.http import HttpResponse
|
|
||||||
from django.template.defaultfilters import slugify
|
|
||||||
from django.utils import http as utils_http
|
from django.utils import http as utils_http
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.views import generic
|
from django.views import generic
|
||||||
@ -83,47 +81,6 @@ class Keypairs(generic.View):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@urls.register
|
|
||||||
class Keypair(generic.View):
|
|
||||||
url_regex = r'nova/keypairs/(?P<keypair_name>.+)/$'
|
|
||||||
|
|
||||||
def get(self, request, keypair_name):
|
|
||||||
"""Creates a new keypair and associates it to the current project.
|
|
||||||
|
|
||||||
* Since the response for this endpoint creates a new keypair and
|
|
||||||
is not idempotent, it normally would be represented by a POST HTTP
|
|
||||||
request. However, this solution was adopted as it
|
|
||||||
would support automatic file download across browsers.
|
|
||||||
|
|
||||||
:param keypair_name: the name to associate the keypair to
|
|
||||||
:param regenerate: (optional) if set to the string 'true',
|
|
||||||
replaces the existing keypair with a new keypair
|
|
||||||
|
|
||||||
This returns the new keypair object on success.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
regenerate = request.GET.get('regenerate') == 'true'
|
|
||||||
if regenerate:
|
|
||||||
api.nova.keypair_delete(request, keypair_name)
|
|
||||||
|
|
||||||
keypair = api.nova.keypair_create(request, keypair_name)
|
|
||||||
|
|
||||||
except exceptions.Conflict:
|
|
||||||
return HttpResponse(status=409)
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
return HttpResponse(status=500)
|
|
||||||
|
|
||||||
else:
|
|
||||||
response = HttpResponse(content_type='application/binary')
|
|
||||||
response['Content-Disposition'] = ('attachment; filename=%s.pem'
|
|
||||||
% slugify(keypair_name))
|
|
||||||
response.write(keypair.private_key)
|
|
||||||
response['Content-Length'] = str(len(response.content))
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
@urls.register
|
@urls.register
|
||||||
class Services(generic.View):
|
class Services(generic.View):
|
||||||
"""API for nova services."""
|
"""API for nova services."""
|
||||||
|
@ -36,29 +36,6 @@ KEYPAIR_ERROR_MESSAGES = {
|
|||||||
'and may not be white space.')}
|
'and may not be white space.')}
|
||||||
|
|
||||||
|
|
||||||
class CreateKeypair(forms.SelfHandlingForm):
|
|
||||||
name = forms.RegexField(max_length=255,
|
|
||||||
label=_("Key Pair Name"),
|
|
||||||
regex=KEYPAIR_NAME_REGEX,
|
|
||||||
error_messages=KEYPAIR_ERROR_MESSAGES)
|
|
||||||
|
|
||||||
def handle(self, request, data):
|
|
||||||
return True # We just redirect to the download view.
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
cleaned_data = super(CreateKeypair, self).clean()
|
|
||||||
name = cleaned_data.get('name')
|
|
||||||
try:
|
|
||||||
keypairs = api.nova.keypair_list(self.request)
|
|
||||||
except Exception:
|
|
||||||
exceptions.handle(self.request, ignore=True)
|
|
||||||
keypairs = []
|
|
||||||
if name in [keypair.name for keypair in keypairs]:
|
|
||||||
error_msg = _("The name is already in use.")
|
|
||||||
self._errors['name'] = self.error_class([error_msg])
|
|
||||||
return cleaned_data
|
|
||||||
|
|
||||||
|
|
||||||
class ImportKeypair(forms.SelfHandlingForm):
|
class ImportKeypair(forms.SelfHandlingForm):
|
||||||
name = forms.RegexField(max_length=255,
|
name = forms.RegexField(max_length=255,
|
||||||
label=_("Key Pair Name"),
|
label=_("Key Pair Name"),
|
||||||
|
@ -12,6 +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.core import urlresolvers
|
||||||
from django.utils.translation import string_concat
|
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
|
||||||
@ -78,16 +79,28 @@ class ImportKeyPair(QuotaKeypairMixin, tables.LinkAction):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class CreateKeyPair(QuotaKeypairMixin, tables.LinkAction):
|
class CreateLinkNG(QuotaKeypairMixin, tables.LinkAction):
|
||||||
name = "create"
|
name = "create-keypair-ng"
|
||||||
verbose_name = _("Create Key Pair")
|
verbose_name = _("Create Key Pair")
|
||||||
url = "horizon:project:key_pairs:create"
|
url = "horizon:project:key_pairs:index"
|
||||||
classes = ("ajax-modal",)
|
classes = ("btn-launch",)
|
||||||
icon = "plus"
|
icon = "plus"
|
||||||
policy_rules = (("compute", "os_compute_api:os-keypairs:create"),)
|
policy_rules = (("compute", "os_compute_api:os-keypairs:create"),)
|
||||||
|
|
||||||
|
def get_default_attrs(self):
|
||||||
|
url = urlresolvers.reverse(self.url)
|
||||||
|
ngclick = "modal.createKeyPair({ successUrl: '%s' })" % url
|
||||||
|
self.attrs.update({
|
||||||
|
'ng-controller': 'KeypairController as modal',
|
||||||
|
'ng-click': ngclick
|
||||||
|
})
|
||||||
|
return super(CreateLinkNG, self).get_default_attrs()
|
||||||
|
|
||||||
|
def get_link_url(self, datum=None):
|
||||||
|
return "javascript:void(0);"
|
||||||
|
|
||||||
def allowed(self, request, keypair=None):
|
def allowed(self, request, keypair=None):
|
||||||
if super(CreateKeyPair, self).allowed(request, keypair):
|
if super(CreateLinkNG, self).allowed(request, keypair):
|
||||||
self.verbose_name = _("Create Key Pair")
|
self.verbose_name = _("Create Key Pair")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -113,6 +126,6 @@ class KeyPairsTable(tables.DataTable):
|
|||||||
class Meta(object):
|
class Meta(object):
|
||||||
name = "keypairs"
|
name = "keypairs"
|
||||||
verbose_name = _("Key Pairs")
|
verbose_name = _("Key Pairs")
|
||||||
table_actions = (CreateKeyPair, ImportKeyPair, DeleteKeyPairs,
|
table_actions = (CreateLinkNG, ImportKeyPair, DeleteKeyPairs,
|
||||||
KeypairsFilterAction,)
|
KeypairsFilterAction,)
|
||||||
row_actions = (DeleteKeyPairs,)
|
row_actions = (DeleteKeyPairs,)
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
{% extends "horizon/common/_modal_form.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block modal-body-right %}
|
|
||||||
<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>
|
|
||||||
{% endblock %}
|
|
@ -1,8 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% block title %}{{ page_title }}{% endblock %}
|
|
||||||
|
|
||||||
{% block main %}
|
|
||||||
{% include 'project/key_pairs/_create.html' %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -22,8 +22,6 @@ from mox3.mox import IsA
|
|||||||
import six
|
import six
|
||||||
|
|
||||||
from openstack_dashboard import api
|
from openstack_dashboard import api
|
||||||
from openstack_dashboard.dashboards.project.key_pairs.forms \
|
|
||||||
import CreateKeypair
|
|
||||||
from openstack_dashboard.dashboards.project.key_pairs.forms \
|
from openstack_dashboard.dashboards.project.key_pairs.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
|
||||||
@ -81,37 +79,6 @@ class KeyPairTests(test.TestCase):
|
|||||||
res = self.client.post(INDEX_URL, formData)
|
res = self.client.post(INDEX_URL, formData)
|
||||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||||
|
|
||||||
def test_create_keypair_get(self):
|
|
||||||
res = self.client.get(
|
|
||||||
reverse('horizon:project:key_pairs:create'))
|
|
||||||
self.assertTemplateUsed(
|
|
||||||
res, 'project/key_pairs/create.html')
|
|
||||||
|
|
||||||
def test_download_keypair_get(self):
|
|
||||||
keypair_name = "keypair"
|
|
||||||
context = {'keypair_name': keypair_name}
|
|
||||||
url = reverse('horizon:project:key_pairs:download',
|
|
||||||
kwargs={'keypair_name': keypair_name})
|
|
||||||
res = self.client.get(url, context)
|
|
||||||
self.assertTemplateUsed(
|
|
||||||
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"
|
|
||||||
|
|
||||||
api.nova.keypair_create(IsA(http.HttpRequest),
|
|
||||||
keypair.name).AndReturn(keypair)
|
|
||||||
self.mox.ReplayAll()
|
|
||||||
|
|
||||||
context = {'keypair_name': keypair.name}
|
|
||||||
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',)})
|
@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()
|
||||||
@ -127,22 +94,6 @@ class KeyPairTests(test.TestCase):
|
|||||||
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",)})
|
|
||||||
def test_regenerate_keypair_get(self):
|
|
||||||
keypair = self.keypairs.first()
|
|
||||||
keypair.private_key = "secret"
|
|
||||||
optional_param = "regenerate"
|
|
||||||
api.nova.keypair_delete(IsA(http.HttpRequest), keypair.name)
|
|
||||||
api.nova.keypair_create(IsA(http.HttpRequest),
|
|
||||||
keypair.name).AndReturn(keypair)
|
|
||||||
self.mox.ReplayAll()
|
|
||||||
url = reverse('horizon:project:key_pairs:generate',
|
|
||||||
kwargs={'keypair_name': keypair.name,
|
|
||||||
'optional': optional_param})
|
|
||||||
res = self.client.get(url)
|
|
||||||
|
|
||||||
self.assertTrue(res.has_header('content-disposition'))
|
|
||||||
|
|
||||||
@test.create_stubs({api.nova: ("keypair_import",)})
|
@test.create_stubs({api.nova: ("keypair_import",)})
|
||||||
def test_import_keypair(self):
|
def test_import_keypair(self):
|
||||||
key1_name = "new_key_pair"
|
key1_name = "new_key_pair"
|
||||||
@ -204,22 +155,6 @@ class KeyPairTests(test.TestCase):
|
|||||||
msg = six.text_type(KEYPAIR_ERROR_MESSAGES['invalid'])
|
msg = six.text_type(KEYPAIR_ERROR_MESSAGES['invalid'])
|
||||||
self.assertFormErrors(res, count=1, message=msg)
|
self.assertFormErrors(res, count=1, message=msg)
|
||||||
|
|
||||||
@test.create_stubs({api.nova: ("keypair_create",)})
|
|
||||||
def test_generate_keypair_exception(self):
|
|
||||||
keypair = self.keypairs.first()
|
|
||||||
|
|
||||||
api.nova.keypair_create(IsA(http.HttpRequest), keypair.name) \
|
|
||||||
.AndRaise(self.exceptions.nova)
|
|
||||||
self.mox.ReplayAll()
|
|
||||||
|
|
||||||
context = {'keypair_name': keypair.name}
|
|
||||||
url = reverse('horizon:project:key_pairs:generate',
|
|
||||||
kwargs={'keypair_name': keypair.name})
|
|
||||||
res = self.client.get(url, context)
|
|
||||||
|
|
||||||
self.assertRedirectsNoFollow(
|
|
||||||
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):
|
||||||
key1_name = "new-key-pair with_regex"
|
key1_name = "new-key-pair with_regex"
|
||||||
@ -236,42 +171,3 @@ class KeyPairTests(test.TestCase):
|
|||||||
url = reverse('horizon:project:key_pairs: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)
|
||||||
|
|
||||||
@test.create_stubs({api.nova: ("keypair_create",)})
|
|
||||||
def test_create_keypair_with_regex_name_get(self):
|
|
||||||
keypair = self.keypairs.first()
|
|
||||||
keypair.name = "key-space pair-regex_name-0123456789"
|
|
||||||
keypair.private_key = "secret"
|
|
||||||
|
|
||||||
api.nova.keypair_create(IsA(http.HttpRequest),
|
|
||||||
keypair.name).AndReturn(keypair)
|
|
||||||
self.mox.ReplayAll()
|
|
||||||
|
|
||||||
context = {'keypair_name': keypair.name}
|
|
||||||
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'))
|
|
||||||
|
|
||||||
def test_download_with_regex_name_get(self):
|
|
||||||
keypair_name = "key pair-regex_name-0123456789"
|
|
||||||
context = {'keypair_name': keypair_name}
|
|
||||||
url = reverse('horizon:project:key_pairs:download',
|
|
||||||
kwargs={'keypair_name': keypair_name})
|
|
||||||
res = self.client.get(url, context)
|
|
||||||
self.assertTemplateUsed(
|
|
||||||
res, 'project/key_pairs/download.html')
|
|
||||||
|
|
||||||
@test.create_stubs({api.nova: ('keypair_list',)})
|
|
||||||
def test_create_duplicate_keypair(self):
|
|
||||||
keypair_name = self.keypairs.first().name
|
|
||||||
|
|
||||||
api.nova.keypair_list(IsA(http.HttpRequest)) \
|
|
||||||
.AndReturn(self.keypairs.list())
|
|
||||||
self.mox.ReplayAll()
|
|
||||||
|
|
||||||
form = CreateKeypair(self.request, data={'name': keypair_name})
|
|
||||||
self.assertFalse(form.is_valid())
|
|
||||||
self.assertIn('The name is already in use.',
|
|
||||||
form.errors['name'][0])
|
|
||||||
|
@ -21,14 +21,7 @@ from openstack_dashboard.dashboards.project.key_pairs import views
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^$', views.IndexView.as_view(), name='index'),
|
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'^import/$', views.ImportView.as_view(), name='import'),
|
||||||
url(r'^(?P<keypair_name>[^/]+)/download/$', views.DownloadView.as_view(),
|
|
||||||
name='download'),
|
|
||||||
url(r'^(?P<keypair_name>[^/]+)/generate/$', views.GenerateView.as_view(),
|
|
||||||
name='generate'),
|
|
||||||
url(r'^(?P<keypair_name>[^/]+)/(?P<optional>[^/]+)/generate/$',
|
|
||||||
views.GenerateView.as_view(), name='generate'),
|
|
||||||
url(r'^(?P<keypair_name>[^/]+)/$', views.DetailView.as_view(),
|
url(r'^(?P<keypair_name>[^/]+)/$', views.DetailView.as_view(),
|
||||||
name='detail'),
|
name='detail'),
|
||||||
]
|
]
|
||||||
|
@ -14,12 +14,7 @@
|
|||||||
|
|
||||||
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.template.defaultfilters import slugify
|
|
||||||
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 never_cache
|
|
||||||
|
|
||||||
from horizon import exceptions
|
from horizon import exceptions
|
||||||
from horizon import forms
|
from horizon import forms
|
||||||
@ -55,20 +50,6 @@ class IndexView(tables.DataTableView):
|
|||||||
return keypairs
|
return keypairs
|
||||||
|
|
||||||
|
|
||||||
class CreateView(forms.ModalFormView):
|
|
||||||
form_class = key_pairs_forms.CreateKeypair
|
|
||||||
template_name = 'project/key_pairs/create.html'
|
|
||||||
submit_url = reverse_lazy(
|
|
||||||
"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,
|
|
||||||
kwargs={"keypair_name": self.request.POST['name']})
|
|
||||||
|
|
||||||
|
|
||||||
class ImportView(forms.ModalFormView):
|
class ImportView(forms.ModalFormView):
|
||||||
form_class = key_pairs_forms.ImportKeypair
|
form_class = key_pairs_forms.ImportKeypair
|
||||||
template_name = 'project/key_pairs/import.html'
|
template_name = 'project/key_pairs/import.html'
|
||||||
@ -103,37 +84,3 @@ class DetailView(views.HorizonTemplateView):
|
|||||||
context = super(DetailView, self).get_context_data(**kwargs)
|
context = super(DetailView, self).get_context_data(**kwargs)
|
||||||
context['keypair'] = self._get_data()
|
context['keypair'] = self._get_data()
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class DownloadView(views.HorizonTemplateView):
|
|
||||||
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(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,
|
|
||||||
no_store=True, must_revalidate=True))
|
|
||||||
@method_decorator(never_cache)
|
|
||||||
def get(self, request, keypair_name=None, optional=None):
|
|
||||||
try:
|
|
||||||
if optional == "regenerate":
|
|
||||||
nova.keypair_delete(request, keypair_name)
|
|
||||||
|
|
||||||
keypair = nova.keypair_create(request, keypair_name)
|
|
||||||
except Exception:
|
|
||||||
redirect = reverse('horizon:project:key_pairs:index')
|
|
||||||
exceptions.handle(self.request,
|
|
||||||
_('Unable to create key pair: %(exc)s'),
|
|
||||||
redirect=redirect)
|
|
||||||
|
|
||||||
response = http.HttpResponse(content_type='application/binary')
|
|
||||||
response['Content-Disposition'] = ('attachment; filename=%s.pem'
|
|
||||||
% slugify(keypair.name))
|
|
||||||
response.write(keypair.private_key)
|
|
||||||
response['Content-Length'] = str(len(response.content))
|
|
||||||
return response
|
|
||||||
|
@ -24,9 +24,7 @@
|
|||||||
LaunchInstanceCreateKeyPairController.$inject = [
|
LaunchInstanceCreateKeyPairController.$inject = [
|
||||||
'$uibModalInstance',
|
'$uibModalInstance',
|
||||||
'existingKeypairs',
|
'existingKeypairs',
|
||||||
'horizon.app.core.openstack-service-api.nova',
|
'horizon.app.core.openstack-service-api.nova'
|
||||||
'horizon.framework.widgets.toast.service',
|
|
||||||
'horizon.app.core.openstack-service-api.keypair-download-service'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,22 +33,21 @@
|
|||||||
* @param {Object} $uibModalInstance
|
* @param {Object} $uibModalInstance
|
||||||
* @param {Object} existingKeypairs
|
* @param {Object} existingKeypairs
|
||||||
* @param {Object} nova
|
* @param {Object} nova
|
||||||
* @param {Object} toastService
|
|
||||||
* @param {Object} keypairDownloadService
|
|
||||||
* @description
|
* @description
|
||||||
* Provide a dialog for creation of a new key pair.
|
* Provide a dialog for creation of a new key pair.
|
||||||
* @returns {undefined} Returns nothing
|
* @returns {undefined} Returns nothing
|
||||||
*/
|
*/
|
||||||
function LaunchInstanceCreateKeyPairController($uibModalInstance, existingKeypairs, nova,
|
function LaunchInstanceCreateKeyPairController($uibModalInstance, existingKeypairs, nova) {
|
||||||
toastService, keypairDownloadService) {
|
|
||||||
var ctrl = this;
|
var ctrl = this;
|
||||||
|
|
||||||
ctrl.submit = submit;
|
ctrl.submit = submit;
|
||||||
ctrl.cancel = cancel;
|
ctrl.cancel = cancel;
|
||||||
ctrl.doesKeypairExist = doesKeypairExist;
|
ctrl.doesKeypairExist = doesKeypairExist;
|
||||||
|
ctrl.generate = generate;
|
||||||
|
|
||||||
ctrl.keypair = '';
|
ctrl.keypair = '';
|
||||||
ctrl.keypairExistsError = gettext('Keypair already exists or name contains bad characters.');
|
ctrl.keypairExistsError = gettext('Keypair already exists or name contains bad characters.');
|
||||||
|
ctrl.copyPrivateKey = copyPrivateKey;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @ngdoc function
|
* @ngdoc function
|
||||||
@ -62,6 +59,21 @@
|
|||||||
return exists(ctrl.keypair);
|
return exists(ctrl.keypair);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generate() {
|
||||||
|
nova.createKeypair({name: ctrl.keypair}).then(onKeypairCreated);
|
||||||
|
|
||||||
|
function onKeypairCreated(data) {
|
||||||
|
ctrl.createdKeypair = data.data;
|
||||||
|
ctrl.privateKey = ctrl.createdKeypair.private_key;
|
||||||
|
ctrl.publicKey = ctrl.createdKeypair.public_key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyPrivateKey() {
|
||||||
|
angular.element('textarea').select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @ngdoc function
|
* @ngdoc function
|
||||||
* @name exists
|
* @name exists
|
||||||
@ -84,17 +96,7 @@
|
|||||||
* notified of the problem and given the opportunity to try again.
|
* notified of the problem and given the opportunity to try again.
|
||||||
*/
|
*/
|
||||||
function submit() {
|
function submit() {
|
||||||
keypairDownloadService.createAndDownloadKeypair(ctrl.keypair).then(
|
$uibModalInstance.close(ctrl.createdKeypair);
|
||||||
function success(createdKeypair) {
|
|
||||||
createdKeypair.regenerateUrl = nova.getRegenerateKeypairUrl(createdKeypair.name);
|
|
||||||
$uibModalInstance.close(createdKeypair);
|
|
||||||
},
|
|
||||||
function error() {
|
|
||||||
var errorMessage = interpolate(gettext('Unable to generate "%s". Please try again.'),
|
|
||||||
[ctrl.keypair]);
|
|
||||||
toastService.add('error', errorMessage);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -77,36 +77,16 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should close the modal and return the created keypair', function() {
|
it('should close the modal and return the created keypair', function() {
|
||||||
mockCreationSuccess = true;
|
|
||||||
mockKeypair = {
|
|
||||||
name: "newKeypair"
|
|
||||||
};
|
|
||||||
spyOn(createKeypairServiceMock, 'getRegenerateKeypairUrl').and.returnValue(
|
|
||||||
"a url"
|
|
||||||
);
|
|
||||||
spyOn(modalInstanceMock, 'close');
|
spyOn(modalInstanceMock, 'close');
|
||||||
|
|
||||||
|
ctrl.createdKeypair = {name: 'newKeypair'};
|
||||||
ctrl.submit();
|
ctrl.submit();
|
||||||
|
|
||||||
expect(modalInstanceMock.close).toHaveBeenCalledWith({
|
expect(modalInstanceMock.close).toHaveBeenCalledWith({
|
||||||
name: "newKeypair",
|
name: "newKeypair"
|
||||||
regenerateUrl: "a url"
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should raise a toast error message when create is unsuccessful', function() {
|
|
||||||
mockCreationSuccess = false;
|
|
||||||
spyOn(toastServiceMock, 'add');
|
|
||||||
|
|
||||||
ctrl.keypair = "aKeypair";
|
|
||||||
ctrl.submit();
|
|
||||||
|
|
||||||
expect(toastServiceMock.add).toHaveBeenCalledWith(
|
|
||||||
'error',
|
|
||||||
'Unable to generate "aKeypair". Please try again.'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('defines a submit function', function() {
|
it('defines a submit function', function() {
|
||||||
expect(ctrl.submit).toBeDefined();
|
expect(ctrl.submit).toBeDefined();
|
||||||
});
|
});
|
||||||
|
@ -29,17 +29,30 @@
|
|||||||
{$ ctrl.keypairExistsError $}
|
{$ ctrl.keypairExistsError $}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group" ng-if="ctrl.privateKey">
|
||||||
|
<label for="private-key" translate>
|
||||||
|
Private Key
|
||||||
|
<span class="hz-icon-required fa"></span>
|
||||||
|
</label>
|
||||||
|
<!-- Note: textarea is used here (instead of pre) due to the fact that ctrl.copyPrivateKey() uses
|
||||||
|
the HTMLInputElement.select() function which is only present on input elements -->
|
||||||
|
<textarea class="form-control" id="private-key" rows="15"
|
||||||
|
ng-model="ctrl.privateKey" readonly></textarea>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-default pull-left" ng-click="ctrl.cancel()">
|
|
||||||
<span class="fa fa-close"></span>
|
|
||||||
<translate>Cancel</translate>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="btn btn-primary"
|
<button class="btn btn-primary"
|
||||||
ng-click="ctrl.submit()" ng-disabled="wizardForm.$invalid || ctrl.doesKeypairExist()">
|
ng-click="ctrl.generate()" ng-disabled="wizardForm.$invalid || ctrl.doesKeypairExist() || ctrl.privateKey">
|
||||||
<translate>Create Keypair</translate>
|
<translate>Create Keypair</translate>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-primary"
|
||||||
|
ng-click="ctrl.copyPrivateKey()" ng-disabled="!ctrl.privateKey">
|
||||||
|
<translate>Copy Private Key to Clipboard</translate>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary"
|
||||||
|
ng-click="ctrl.submit()" ng-disabled="!ctrl.privateKey">
|
||||||
|
<translate>Done</translate>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
* @param {Object} launchInstanceModel
|
* @param {Object} launchInstanceModel
|
||||||
* @param {Object} $uibModal
|
* @param {Object} $uibModal
|
||||||
* @param {Object} toastService
|
* @param {Object} toastService
|
||||||
|
* @param {Object} settingsService
|
||||||
* @description
|
* @description
|
||||||
* Allows selection of key pairs.
|
* Allows selection of key pairs.
|
||||||
* @returns {undefined} No return value
|
* @returns {undefined} No return value
|
||||||
|
@ -4,18 +4,6 @@
|
|||||||
You may select an existing key pair, import a key pair, or generate a new key pair.
|
You may select an existing key pair, import a key pair, or generate a new key pair.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div ng-if="ctrl.isKeypairCreated" class="alert alert-info" role="alert">
|
|
||||||
<p translate>A key pair named '{$ctrl.createdKeypair.name$}' was successfully created. This key pair should automatically download.</p>
|
|
||||||
<p translate>If not, you can manually download this keypair at the following link:</p>
|
|
||||||
<a class="btn btn-default" role="button" href="{$ ctrl.createdKeypair.regenerateUrl $}">
|
|
||||||
<span class="fa fa-download"></span>
|
|
||||||
{$ctrl.createdKeypair.name$}
|
|
||||||
</a>
|
|
||||||
<p translate>
|
|
||||||
Note: you will not be able to download this later.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="button" class="btn btn-default"
|
<button type="button" class="btn btn-default"
|
||||||
ng-click="ctrl.createKeyPair()">
|
ng-click="ctrl.createKeyPair()">
|
||||||
<span class="fa fa-plus"></span>
|
<span class="fa fa-plus"></span>
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
'horizon.app.core.cloud-services',
|
'horizon.app.core.cloud-services',
|
||||||
'horizon.app.core.flavors',
|
'horizon.app.core.flavors',
|
||||||
'horizon.app.core.images',
|
'horizon.app.core.images',
|
||||||
|
'horizon.app.core.keypairs',
|
||||||
'horizon.app.core.metadata',
|
'horizon.app.core.metadata',
|
||||||
'horizon.app.core.network_qos',
|
'horizon.app.core.network_qos',
|
||||||
'horizon.app.core.openstack-service-api',
|
'horizon.app.core.openstack-service-api',
|
||||||
|
@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular
|
||||||
|
.module('horizon.app.core.keypairs')
|
||||||
|
.controller('KeypairController', KeypairController);
|
||||||
|
|
||||||
|
KeypairController.$inject = [
|
||||||
|
'horizon.dashboard.project.workflow.launch-instance.basePath',
|
||||||
|
'horizon.app.core.openstack-service-api.nova',
|
||||||
|
'horizon.framework.widgets.modal-wait-spinner.service',
|
||||||
|
'$window',
|
||||||
|
'$uibModal'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc controller
|
||||||
|
* @name KeypairController
|
||||||
|
* @param {string} basePath
|
||||||
|
* @param {Object} $uibModal
|
||||||
|
* @description
|
||||||
|
* Allows creation of key pairs.
|
||||||
|
* @returns {undefined} No return value
|
||||||
|
*/
|
||||||
|
function KeypairController(
|
||||||
|
basePath,
|
||||||
|
nova,
|
||||||
|
spinnerService,
|
||||||
|
$window,
|
||||||
|
$uibModal
|
||||||
|
) {
|
||||||
|
var ctrl = this;
|
||||||
|
|
||||||
|
ctrl.createKeyPair = createKeyPair;
|
||||||
|
|
||||||
|
//////////
|
||||||
|
|
||||||
|
function setKeyPairs(config) {
|
||||||
|
return function(response) {
|
||||||
|
var keyPairs = response.data.items.map(getName);
|
||||||
|
|
||||||
|
$uibModal.open({
|
||||||
|
templateUrl: basePath + 'keypair/create-keypair.html',
|
||||||
|
controller: 'LaunchInstanceCreateKeyPairController as ctrl',
|
||||||
|
windowClass: 'modal-dialog-wizard',
|
||||||
|
resolve: {
|
||||||
|
existingKeypairs: getKeypairs
|
||||||
|
}
|
||||||
|
}).result.then(go(config.successUrl));
|
||||||
|
|
||||||
|
function getName(item) {
|
||||||
|
return item.keypair.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getKeypairs() {
|
||||||
|
return keyPairs;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc function
|
||||||
|
* @name createKeyPair
|
||||||
|
* @description
|
||||||
|
* Launches the modal to create a key pair.
|
||||||
|
* @returns {undefined} No return value
|
||||||
|
*/
|
||||||
|
function createKeyPair(config) {
|
||||||
|
nova.getKeypairs().then(setKeyPairs(config));
|
||||||
|
}
|
||||||
|
|
||||||
|
function go(url) {
|
||||||
|
return function changeUrl() {
|
||||||
|
spinnerService.showModalSpinner(gettext('Please Wait'));
|
||||||
|
$window.location.href = url;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
describe('KeypairController', function() {
|
||||||
|
var ctrl, keyPairCall, $timeout;
|
||||||
|
var nova = {
|
||||||
|
getKeypairs: function() {
|
||||||
|
var kps = {data: {items: [{keypair: {name: 'one'}},{keypair: {name: 'two'}} ]}};
|
||||||
|
keyPairCall.resolve(kps);
|
||||||
|
return keyPairCall.promise;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var spinner = {
|
||||||
|
showModalSpinner: angular.noop
|
||||||
|
};
|
||||||
|
var config = {successUrl: '/some/where'};
|
||||||
|
var $uibModal = {
|
||||||
|
open: angular.noop
|
||||||
|
};
|
||||||
|
var $window = {location: {}};
|
||||||
|
|
||||||
|
beforeEach(module('horizon.app.core.keypairs'));
|
||||||
|
|
||||||
|
beforeEach(
|
||||||
|
inject(
|
||||||
|
function($controller, $rootScope, $q, _$timeout_) {
|
||||||
|
$timeout = _$timeout_;
|
||||||
|
ctrl = $controller('KeypairController', {
|
||||||
|
'horizon.dashboard.project.workflow.launch-instance.basePath': '/here/',
|
||||||
|
'horizon.app.core.openstack-service-api.nova': nova,
|
||||||
|
'horizon.framework.widgets.modal-wait-spinner.service': spinner,
|
||||||
|
'$uibModal': $uibModal,
|
||||||
|
'$window': $window
|
||||||
|
});
|
||||||
|
|
||||||
|
keyPairCall = $q.defer();
|
||||||
|
spyOn($uibModal, 'open').and.returnValue({result: $q.resolve({})});
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('createKeyPair', function() {
|
||||||
|
it('opens the modal', function() {
|
||||||
|
ctrl.createKeyPair(config);
|
||||||
|
$timeout.flush();
|
||||||
|
expect($uibModal.open).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides a function to existingKeypairs that returns keypair names', function() {
|
||||||
|
ctrl.createKeyPair(config);
|
||||||
|
$timeout.flush();
|
||||||
|
var func = $uibModal.open.calls.argsFor(0)[0].resolve.existingKeypairs;
|
||||||
|
expect(func()).toEqual(['one','two']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('relocates to the config successUrl', function() {
|
||||||
|
ctrl.createKeyPair(config);
|
||||||
|
$timeout.flush();
|
||||||
|
expect($window.location.href).toBe('/some/where');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
})();
|
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc overview
|
||||||
|
* @ngname horizon.app.core.keypairs
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Provides all of the services and widgets required
|
||||||
|
* to support and display keypairs related content.
|
||||||
|
*/
|
||||||
|
angular
|
||||||
|
.module('horizon.app.core.keypairs', [
|
||||||
|
])
|
||||||
|
;
|
||||||
|
|
||||||
|
})();
|
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2017 SUSE LLC
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
describe('horizon.app.core.keypairs', function () {
|
||||||
|
it('should be defined', function () {
|
||||||
|
expect(angular.module('horizon.app.core.keypairs')).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
})();
|
@ -1,153 +0,0 @@
|
|||||||
/*
|
|
||||||
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
(function () {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @ngdoc overview
|
|
||||||
* @name horizon.dashboard.project.workflow.keypair.create-keypair-service
|
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* Service to handle creating keypairs and downloading their private keys.
|
|
||||||
* Please note, the implementation has quirks due to what features are
|
|
||||||
* available in which browsers. As a result, the implementation involves
|
|
||||||
* using iframes to specify downloads. Since the API does not allow the
|
|
||||||
* full retrieval of the key pair after creation, and the URL retrieved in
|
|
||||||
* an iframe must be a GET method, and the fact that we shouldn't pass
|
|
||||||
* data as part of the URL (because it is potentially logged and contains
|
|
||||||
* private key data), we have to make the actual API call when performing
|
|
||||||
* the GET. This also means that if the user misses the download for some
|
|
||||||
* reason, we need to provide the ability to regenerate the key pair,
|
|
||||||
* although that is not a feature of this service.
|
|
||||||
*/
|
|
||||||
|
|
||||||
angular
|
|
||||||
.module('horizon.app.core.openstack-service-api')
|
|
||||||
.factory('horizon.app.core.openstack-service-api.keypair-download-service',
|
|
||||||
keypairDownloadService);
|
|
||||||
|
|
||||||
keypairDownloadService.$inject = [
|
|
||||||
'$document',
|
|
||||||
'horizon.app.core.openstack-service-api.nova',
|
|
||||||
'$q',
|
|
||||||
'$timeout'
|
|
||||||
];
|
|
||||||
|
|
||||||
function keypairDownloadService($document, novaAPI, $q, $timeout) {
|
|
||||||
|
|
||||||
var service = {
|
|
||||||
createAndDownloadKeypair: createAndDownloadKeypair
|
|
||||||
};
|
|
||||||
|
|
||||||
return service;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @ngdoc function
|
|
||||||
* @name createAndDownloadKeypair
|
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* This function performs the actions necessary to begin a download
|
|
||||||
* of a newly created key pair. The given name will be used as the
|
|
||||||
* logical name of the key pair and will be used to make a file-system-
|
|
||||||
* friendly filename.
|
|
||||||
* In this implementation, for browser compatibility reasons, the
|
|
||||||
* download is achieved by creating an iframe with the given path for
|
|
||||||
* the create API call given, so the results are streamed directly to the
|
|
||||||
* client. This is not ideal but is due to lack of support in IE for
|
|
||||||
* features like the data: protocol. The iframes require that an element
|
|
||||||
* with the class of 'download-iframes' is present.
|
|
||||||
*
|
|
||||||
* @param {string} name The desired name for the key pair
|
|
||||||
* @returns {promise} A promise resolving if true, rejecting with error
|
|
||||||
*/
|
|
||||||
function createAndDownloadKeypair(name) {
|
|
||||||
addDOMResource(name);
|
|
||||||
return verifyCreatedPromise(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @ngdoc function
|
|
||||||
* @name addDOMResource
|
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* This adds an iframe to the body of the current document, using
|
|
||||||
* the appropriate URL for the API to create/download the new key pair.
|
|
||||||
*
|
|
||||||
* @param {string} keypairName The desired name for the key pair
|
|
||||||
* @returns {undefined} Returns nothing
|
|
||||||
*/
|
|
||||||
function addDOMResource(keypairName) {
|
|
||||||
var url = novaAPI.getCreateKeypairUrl(keypairName);
|
|
||||||
var iframe = angular.element("<iframe></iframe>");
|
|
||||||
iframe.attr('id', keypairName);
|
|
||||||
iframe.attr('src', url);
|
|
||||||
iframe.attr('style', 'display: none;');
|
|
||||||
if ($document.find('.download-iframes').size() === 0) {
|
|
||||||
var iframeContainer = angular.element('<div class="download-iframes"></div>');
|
|
||||||
$document.find('body').append(iframeContainer);
|
|
||||||
}
|
|
||||||
$document.find('.download-iframes').append(iframe);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @ngdoc function
|
|
||||||
* @name verifyCreatedPromise
|
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* This function returns a promise that tries ten times to see if a
|
|
||||||
* key pair of the given name exists in the key pair listing. These
|
|
||||||
* tries are one second apart. Once it has been found, the promise
|
|
||||||
* is resolved with the key pair data. If it is not found within the
|
|
||||||
* period, the promise is rejected.
|
|
||||||
*
|
|
||||||
* @param {string} name The name for the key pair
|
|
||||||
* @returns {promise} A promise resolving if true, rejecting with error
|
|
||||||
*/
|
|
||||||
function verifyCreatedPromise(name) {
|
|
||||||
return $q(function doesKeypairExistPromise(resolve, reject) {
|
|
||||||
|
|
||||||
doesKeypairExist(10);
|
|
||||||
|
|
||||||
function doesKeypairExist(timesToCheck) {
|
|
||||||
$timeout(function doesKeypairExistTimeout() {
|
|
||||||
novaAPI.getKeypairs().then(function isKeypairInResponse(response) {
|
|
||||||
|
|
||||||
var foundKeypairs = response.data.items.filter(function sameName(item) {
|
|
||||||
return item.keypair.name === name;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (foundKeypairs.length === 1) {
|
|
||||||
resolve(foundKeypairs[0].keypair);
|
|
||||||
angular.element('.download-iframes #' + name).remove();
|
|
||||||
} else if (timesToCheck > 1) {
|
|
||||||
doesKeypairExist(timesToCheck - 1);
|
|
||||||
} else {
|
|
||||||
reject();
|
|
||||||
angular.element('.download-iframes #' + name).remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
},
|
|
||||||
1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
})();
|
|
@ -1,166 +0,0 @@
|
|||||||
/*
|
|
||||||
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
(function() {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
describe('Download Keypair Service', function() {
|
|
||||||
|
|
||||||
var service, $scope, existingKeypairs, $timeout;
|
|
||||||
|
|
||||||
var documentMock = {
|
|
||||||
find: function() {
|
|
||||||
return documentFindMock;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var documentFindMock = {
|
|
||||||
append : angular.noop,
|
|
||||||
load: function (callback) {
|
|
||||||
callback();
|
|
||||||
},
|
|
||||||
size: function() {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var novaAPIMock = {
|
|
||||||
getKeypairs: function() {
|
|
||||||
return {
|
|
||||||
then: function(callback) {
|
|
||||||
callback(existingKeypairs);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getCreateKeypairUrl: function() {
|
|
||||||
return "/some/given/path";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(module('horizon.app.core.openstack-service-api'));
|
|
||||||
|
|
||||||
beforeEach(module(function ($provide) {
|
|
||||||
$provide.value('horizon.app.core.openstack-service-api.nova', novaAPIMock);
|
|
||||||
$provide.value('$document', documentMock);
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(inject(function (_$injector_, _$rootScope_, _$timeout_) {
|
|
||||||
service = _$injector_.get(
|
|
||||||
'horizon.app.core.openstack-service-api.keypair-download-service'
|
|
||||||
);
|
|
||||||
$scope = _$rootScope_.$new();
|
|
||||||
$timeout = _$timeout_;
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('adds the download key pair endpoint as a resource to the DOM', function() {
|
|
||||||
spyOn(documentFindMock, 'append');
|
|
||||||
spyOn(documentFindMock, 'load').and.returnValue({});
|
|
||||||
|
|
||||||
service.createAndDownloadKeypair("newKeypair");
|
|
||||||
|
|
||||||
var passedObj = documentFindMock.append.calls.argsFor(0)[0];
|
|
||||||
expect(passedObj).toBeDefined();
|
|
||||||
expect(passedObj.attr('id')).toBe("newKeypair");
|
|
||||||
});
|
|
||||||
|
|
||||||
it('encodes the URI component given slashes, etc.', function() {
|
|
||||||
spyOn(documentFindMock, 'append');
|
|
||||||
spyOn(documentFindMock, 'load').and.returnValue({});
|
|
||||||
|
|
||||||
service.createAndDownloadKeypair("new/Keypair");
|
|
||||||
|
|
||||||
var passedObj = documentFindMock.append.calls.argsFor(0)[0];
|
|
||||||
expect(passedObj).toBeDefined();
|
|
||||||
expect(passedObj.attr('id')).toBe("new/Keypair");
|
|
||||||
});
|
|
||||||
|
|
||||||
it('creates a div with download-iframes if not present', function() {
|
|
||||||
spyOn(documentFindMock, 'append');
|
|
||||||
spyOn(documentFindMock, 'load').and.returnValue({});
|
|
||||||
spyOn(documentFindMock, 'size').and.returnValue(0);
|
|
||||||
|
|
||||||
service.createAndDownloadKeypair("new/Keypair");
|
|
||||||
|
|
||||||
expect(documentFindMock.append.calls.count()).toBe(2);
|
|
||||||
expect(documentFindMock.append.calls.allArgs().some(function(x) {
|
|
||||||
return x[0].attr('class') === 'download-iframes';
|
|
||||||
})).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('checks that the keypair was added and returns a success promise result', function() {
|
|
||||||
|
|
||||||
existingKeypairs = {
|
|
||||||
data: {
|
|
||||||
items:[{
|
|
||||||
keypair: {
|
|
||||||
name: "newKeypair"
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var promiseSuccessful, keypair;
|
|
||||||
|
|
||||||
service.createAndDownloadKeypair("newKeypair").then(
|
|
||||||
function success(createdKeypair) {
|
|
||||||
promiseSuccessful = true;
|
|
||||||
keypair = createdKeypair;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
$timeout.flush();
|
|
||||||
$scope.$apply();
|
|
||||||
|
|
||||||
expect(promiseSuccessful).toEqual(true);
|
|
||||||
expect(keypair).toEqual({name: "newKeypair"});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('checks that the keypair was not added and returns a error promise result', function() {
|
|
||||||
|
|
||||||
existingKeypairs = {
|
|
||||||
data: {
|
|
||||||
items:[]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var promiseErrored;
|
|
||||||
|
|
||||||
service.createAndDownloadKeypair("newKeypair").then(
|
|
||||||
function success() {},
|
|
||||||
function error() {
|
|
||||||
promiseErrored = true;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// checks 10 times after one second
|
|
||||||
$timeout.flush();
|
|
||||||
$timeout.flush();
|
|
||||||
$timeout.flush();
|
|
||||||
$timeout.flush();
|
|
||||||
$timeout.flush();
|
|
||||||
$timeout.flush();
|
|
||||||
$timeout.flush();
|
|
||||||
$timeout.flush();
|
|
||||||
$timeout.flush();
|
|
||||||
$timeout.flush();
|
|
||||||
|
|
||||||
$scope.$apply();
|
|
||||||
|
|
||||||
expect(promiseErrored).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
})();
|
|
@ -23,20 +23,18 @@
|
|||||||
|
|
||||||
novaAPI.$inject = [
|
novaAPI.$inject = [
|
||||||
'horizon.framework.util.http.service',
|
'horizon.framework.util.http.service',
|
||||||
'horizon.framework.widgets.toast.service',
|
'horizon.framework.widgets.toast.service'
|
||||||
'$window'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ngdoc service
|
* @ngdoc service
|
||||||
* @param {Object} apiService
|
* @param {Object} apiService
|
||||||
* @param {Object} toastService
|
* @param {Object} toastService
|
||||||
* @param {Object} $window
|
|
||||||
* @name novaApi
|
* @name novaApi
|
||||||
* @description Provides access to Nova APIs.
|
* @description Provides access to Nova APIs.
|
||||||
* @returns {Object} The service
|
* @returns {Object} The service
|
||||||
*/
|
*/
|
||||||
function novaAPI(apiService, toastService, $window) {
|
function novaAPI(apiService, toastService) {
|
||||||
|
|
||||||
var service = {
|
var service = {
|
||||||
getActionList: getActionList,
|
getActionList: getActionList,
|
||||||
@ -71,8 +69,6 @@
|
|||||||
getServices: getServices,
|
getServices: getServices,
|
||||||
getInstanceMetadata: getInstanceMetadata,
|
getInstanceMetadata: getInstanceMetadata,
|
||||||
editInstanceMetadata: editInstanceMetadata,
|
editInstanceMetadata: editInstanceMetadata,
|
||||||
getCreateKeypairUrl: getCreateKeypairUrl,
|
|
||||||
getRegenerateKeypairUrl: getRegenerateKeypairUrl,
|
|
||||||
createFlavor: createFlavor,
|
createFlavor: createFlavor,
|
||||||
updateFlavor: updateFlavor,
|
updateFlavor: updateFlavor,
|
||||||
deleteFlavor: deleteFlavor,
|
deleteFlavor: deleteFlavor,
|
||||||
@ -743,44 +739,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @ngdoc function
|
|
||||||
* @name getCreateKeypairUrl
|
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* Returns a URL, respecting WEBROOT, that if called as a REST call
|
|
||||||
* would create and return a new key pair with the given name. This
|
|
||||||
* function is provided because to perform a download of the key pair,
|
|
||||||
* an iframe must be given a URL to use (which is further explained in
|
|
||||||
* the key pair download service).
|
|
||||||
*
|
|
||||||
* @param {string} keyPairName
|
|
||||||
* @returns {Object} The result of the API call
|
|
||||||
*/
|
|
||||||
function getCreateKeypairUrl(keyPairName) {
|
|
||||||
// NOTE: WEBROOT by definition must end with a slash (local_settings.py).
|
|
||||||
return $window.WEBROOT + "api/nova/keypairs/" +
|
|
||||||
encodeURIComponent(keyPairName) + "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @ngdoc function
|
|
||||||
* @name getRegenerateKeypairUrl
|
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* Returns a URL, respecting WEBROOT, that if called as a REST call
|
|
||||||
* would regenereate an existing key pair with the given name and return
|
|
||||||
* the new key pair data. This function is provided because to perform
|
|
||||||
* a download of the key pair, an iframe must be given a URL to use
|
|
||||||
* (which is further explained in the key pair download service).
|
|
||||||
*
|
|
||||||
* @param {string} keyPairName
|
|
||||||
* @returns {Object} The result of the API call
|
|
||||||
*/
|
|
||||||
function getRegenerateKeypairUrl(keyPairName) {
|
|
||||||
return getCreateKeypairUrl(keyPairName) + "?regenerate=true";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name createServerSnapshot
|
* @name createServerSnapshot
|
||||||
* @param {Object} newSnapshot - The new server snapshot
|
* @param {Object} newSnapshot - The new server snapshot
|
||||||
|
@ -560,53 +560,4 @@
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//// This is separated due to differences in what is being tested.
|
|
||||||
describe('Keypair functions', function() {
|
|
||||||
|
|
||||||
var service, $window;
|
|
||||||
|
|
||||||
beforeEach(module('horizon.app.core.openstack-service-api'));
|
|
||||||
|
|
||||||
beforeEach(module(function ($provide) {
|
|
||||||
$provide.value('horizon.framework.util.http.service', {});
|
|
||||||
$provide.value('horizon.framework.widgets.toast.service', {});
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(inject(function (_$injector_, _$rootScope_, _$timeout_, _$window_) {
|
|
||||||
service = _$injector_.get(
|
|
||||||
'horizon.app.core.openstack-service-api.nova'
|
|
||||||
);
|
|
||||||
$window = _$window_;
|
|
||||||
$window.WEBROOT = '/';
|
|
||||||
}));
|
|
||||||
|
|
||||||
afterEach(inject(function (_$window_) {
|
|
||||||
$window = _$window_;
|
|
||||||
$window.WEBROOT = '/';
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('returns a link to download the private key for an existing keypair', function() {
|
|
||||||
var link = service.getCreateKeypairUrl("keypairName");
|
|
||||||
expect(link).toEqual('/api/nova/keypairs/keypairName/');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns a WEBROOT link to download the private key for an existing keypair', function() {
|
|
||||||
$window.WEBROOT = '/myroot/';
|
|
||||||
var link = service.getCreateKeypairUrl("keypairName");
|
|
||||||
expect(link).toEqual('/myroot/api/nova/keypairs/keypairName/');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns a link to redownload the private key for an existing keypair', function() {
|
|
||||||
var link = service.getRegenerateKeypairUrl("keypairName");
|
|
||||||
expect(link).toEqual('/api/nova/keypairs/keypairName/?regenerate=true');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns a WEBROOT link to redownload the private key for an existing keypair', function() {
|
|
||||||
$window.WEBROOT = '/myroot/';
|
|
||||||
var link = service.getRegenerateKeypairUrl("keypairName");
|
|
||||||
expect(link).toEqual('/myroot/api/nova/keypairs/keypairName/?regenerate=true');
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
@ -22,8 +22,6 @@ from openstack_dashboard.api.rest import nova
|
|||||||
from openstack_dashboard.test import helpers as test
|
from openstack_dashboard.test import helpers as test
|
||||||
from openstack_dashboard.usage import quotas
|
from openstack_dashboard.usage import quotas
|
||||||
|
|
||||||
from novaclient import exceptions
|
|
||||||
|
|
||||||
|
|
||||||
class NovaRestTestCase(test.TestCase):
|
class NovaRestTestCase(test.TestCase):
|
||||||
#
|
#
|
||||||
@ -216,67 +214,6 @@ class NovaRestTestCase(test.TestCase):
|
|||||||
self.assertEqual('/api/nova/keypairs/Ni%21', response['location'])
|
self.assertEqual('/api/nova/keypairs/Ni%21', response['location'])
|
||||||
nc.keypair_import.assert_called_once_with(request, 'Ni!', 'hi')
|
nc.keypair_import.assert_called_once_with(request, 'Ni!', 'hi')
|
||||||
|
|
||||||
def test_keypair_create_and_download(self):
|
|
||||||
self._test_keypair_create_and_download(False)
|
|
||||||
|
|
||||||
def test_keypair_recreate_and_download(self):
|
|
||||||
self._test_keypair_create_and_download(True)
|
|
||||||
|
|
||||||
@mock.patch.object(nova.api, 'nova')
|
|
||||||
def _test_keypair_create_and_download(self, recreate_keypair, nc):
|
|
||||||
params = {}
|
|
||||||
|
|
||||||
if recreate_keypair:
|
|
||||||
params = {'regenerate': 'true'}
|
|
||||||
|
|
||||||
request = self.mock_rest_request(GET=params)
|
|
||||||
|
|
||||||
keypair_create_response = mock.Mock()
|
|
||||||
keypair_create_response.private_key = "private key content"
|
|
||||||
nc.keypair_create.return_value = keypair_create_response
|
|
||||||
|
|
||||||
with mock.patch.object(settings, 'DEBUG', True):
|
|
||||||
response = nova.Keypair().get(request, "Ni!")
|
|
||||||
|
|
||||||
if recreate_keypair:
|
|
||||||
nc.keypair_delete.assert_called_once_with(request, 'Ni!')
|
|
||||||
else:
|
|
||||||
nc.keypair_delete.assert_not_called()
|
|
||||||
|
|
||||||
nc.keypair_create.assert_called_once_with(request, 'Ni!')
|
|
||||||
self.assertStatusCode(response, 200)
|
|
||||||
self.assertEqual(
|
|
||||||
'attachment; filename=ni.pem',
|
|
||||||
response['Content-Disposition'])
|
|
||||||
self.assertEqual(
|
|
||||||
"private key content",
|
|
||||||
response.content.decode('utf-8'))
|
|
||||||
self.assertEqual('19', response['Content-Length'])
|
|
||||||
|
|
||||||
@mock.patch.object(nova.api, 'nova')
|
|
||||||
def test_keypair_fail_to_create_because_already_exists(self, nc):
|
|
||||||
request = self.mock_rest_request(GET={})
|
|
||||||
|
|
||||||
conflict_exception = exceptions.Conflict(409, 'keypair exists!')
|
|
||||||
nc.keypair_create.side_effect = conflict_exception
|
|
||||||
|
|
||||||
with mock.patch.object(settings, 'DEBUG', True):
|
|
||||||
response = nova.Keypair().get(request, "Ni!")
|
|
||||||
|
|
||||||
self.assertEqual(409, response.status_code)
|
|
||||||
|
|
||||||
@mock.patch.object(nova.api, 'nova')
|
|
||||||
def test_keypair_fail_to_create(self, nc):
|
|
||||||
request = self.mock_rest_request(GET={})
|
|
||||||
|
|
||||||
surprise_exception = exceptions.ClientException(501, 'Boom!')
|
|
||||||
nc.keypair_create.side_effect = surprise_exception
|
|
||||||
|
|
||||||
with mock.patch.object(settings, 'DEBUG', True):
|
|
||||||
response = nova.Keypair().get(request, "Ni!")
|
|
||||||
|
|
||||||
self.assertEqual(500, response.status_code)
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Availability Zones
|
# Availability Zones
|
||||||
#
|
#
|
||||||
|
Loading…
Reference in New Issue
Block a user