Merge "Use POST not GET for keypair generation"
This commit is contained in:
commit
f5b2561a09
@ -14,8 +14,6 @@
|
||||
"""API over the nova service."""
|
||||
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.translation import ugettext_lazy as _
|
||||
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
|
||||
class Services(generic.View):
|
||||
"""API for nova services."""
|
||||
|
@ -36,29 +36,6 @@ KEYPAIR_ERROR_MESSAGES = {
|
||||
'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):
|
||||
name = forms.RegexField(max_length=255,
|
||||
label=_("Key Pair Name"),
|
||||
|
@ -12,6 +12,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.core import urlresolvers
|
||||
from django.utils.translation import string_concat
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ungettext_lazy
|
||||
@ -78,16 +79,28 @@ class ImportKeyPair(QuotaKeypairMixin, tables.LinkAction):
|
||||
return True
|
||||
|
||||
|
||||
class CreateKeyPair(QuotaKeypairMixin, tables.LinkAction):
|
||||
name = "create"
|
||||
class CreateLinkNG(QuotaKeypairMixin, tables.LinkAction):
|
||||
name = "create-keypair-ng"
|
||||
verbose_name = _("Create Key Pair")
|
||||
url = "horizon:project:key_pairs:create"
|
||||
classes = ("ajax-modal",)
|
||||
url = "horizon:project:key_pairs:index"
|
||||
classes = ("btn-launch",)
|
||||
icon = "plus"
|
||||
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):
|
||||
if super(CreateKeyPair, self).allowed(request, keypair):
|
||||
if super(CreateLinkNG, self).allowed(request, keypair):
|
||||
self.verbose_name = _("Create Key Pair")
|
||||
return True
|
||||
|
||||
@ -113,6 +126,6 @@ class KeyPairsTable(tables.DataTable):
|
||||
class Meta(object):
|
||||
name = "keypairs"
|
||||
verbose_name = _("Key Pairs")
|
||||
table_actions = (CreateKeyPair, ImportKeyPair, DeleteKeyPairs,
|
||||
table_actions = (CreateLinkNG, ImportKeyPair, DeleteKeyPairs,
|
||||
KeypairsFilterAction,)
|
||||
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
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.dashboards.project.key_pairs.forms \
|
||||
import CreateKeypair
|
||||
from openstack_dashboard.dashboards.project.key_pairs.forms \
|
||||
import KEYPAIR_ERROR_MESSAGES
|
||||
from openstack_dashboard.test import helpers as test
|
||||
@ -81,37 +79,6 @@ class KeyPairTests(test.TestCase):
|
||||
res = self.client.post(INDEX_URL, formData)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
def test_create_keypair_get(self):
|
||||
res = self.client.get(
|
||||
reverse('horizon:project: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',)})
|
||||
def test_keypair_detail_get(self):
|
||||
keypair = self.keypairs.first()
|
||||
@ -127,22 +94,6 @@ class KeyPairTests(test.TestCase):
|
||||
res = self.client.get(url, context)
|
||||
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",)})
|
||||
def test_import_keypair(self):
|
||||
key1_name = "new_key_pair"
|
||||
@ -204,22 +155,6 @@ class KeyPairTests(test.TestCase):
|
||||
msg = six.text_type(KEYPAIR_ERROR_MESSAGES['invalid'])
|
||||
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",)})
|
||||
def test_import_keypair_with_regex_defined_name(self):
|
||||
key1_name = "new-key-pair with_regex"
|
||||
@ -236,42 +171,3 @@ class KeyPairTests(test.TestCase):
|
||||
url = reverse('horizon:project:key_pairs:import')
|
||||
res = self.client.post(url, formData)
|
||||
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 = [
|
||||
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
url(r'^create/$', views.CreateView.as_view(), name='create'),
|
||||
url(r'^import/$', views.ImportView.as_view(), name='import'),
|
||||
url(r'^(?P<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(),
|
||||
name='detail'),
|
||||
]
|
||||
|
@ -14,12 +14,7 @@
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
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.views.decorators.cache import cache_control
|
||||
from django.views.decorators.cache import never_cache
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
@ -55,20 +50,6 @@ class IndexView(tables.DataTableView):
|
||||
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):
|
||||
form_class = key_pairs_forms.ImportKeypair
|
||||
template_name = 'project/key_pairs/import.html'
|
||||
@ -103,37 +84,3 @@ class DetailView(views.HorizonTemplateView):
|
||||
context = super(DetailView, self).get_context_data(**kwargs)
|
||||
context['keypair'] = self._get_data()
|
||||
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 = [
|
||||
'$uibModalInstance',
|
||||
'existingKeypairs',
|
||||
'horizon.app.core.openstack-service-api.nova',
|
||||
'horizon.framework.widgets.toast.service',
|
||||
'horizon.app.core.openstack-service-api.keypair-download-service'
|
||||
'horizon.app.core.openstack-service-api.nova'
|
||||
];
|
||||
|
||||
/**
|
||||
@ -35,22 +33,21 @@
|
||||
* @param {Object} $uibModalInstance
|
||||
* @param {Object} existingKeypairs
|
||||
* @param {Object} nova
|
||||
* @param {Object} toastService
|
||||
* @param {Object} keypairDownloadService
|
||||
* @description
|
||||
* Provide a dialog for creation of a new key pair.
|
||||
* @returns {undefined} Returns nothing
|
||||
*/
|
||||
function LaunchInstanceCreateKeyPairController($uibModalInstance, existingKeypairs, nova,
|
||||
toastService, keypairDownloadService) {
|
||||
function LaunchInstanceCreateKeyPairController($uibModalInstance, existingKeypairs, nova) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.submit = submit;
|
||||
ctrl.cancel = cancel;
|
||||
ctrl.doesKeypairExist = doesKeypairExist;
|
||||
ctrl.generate = generate;
|
||||
|
||||
ctrl.keypair = '';
|
||||
ctrl.keypairExistsError = gettext('Keypair already exists or name contains bad characters.');
|
||||
ctrl.copyPrivateKey = copyPrivateKey;
|
||||
|
||||
/*
|
||||
* @ngdoc function
|
||||
@ -62,6 +59,21 @@
|
||||
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
|
||||
* @name exists
|
||||
@ -84,17 +96,7 @@
|
||||
* notified of the problem and given the opportunity to try again.
|
||||
*/
|
||||
function submit() {
|
||||
keypairDownloadService.createAndDownloadKeypair(ctrl.keypair).then(
|
||||
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);
|
||||
}
|
||||
);
|
||||
$uibModalInstance.close(ctrl.createdKeypair);
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -77,36 +77,16 @@
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
ctrl.createdKeypair = {name: 'newKeypair'};
|
||||
ctrl.submit();
|
||||
|
||||
expect(modalInstanceMock.close).toHaveBeenCalledWith({
|
||||
name: "newKeypair",
|
||||
regenerateUrl: "a url"
|
||||
name: "newKeypair"
|
||||
});
|
||||
});
|
||||
|
||||
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() {
|
||||
expect(ctrl.submit).toBeDefined();
|
||||
});
|
||||
|
@ -29,17 +29,30 @@
|
||||
{$ ctrl.keypairExistsError $}
|
||||
</span>
|
||||
</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 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"
|
||||
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>
|
||||
</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>
|
||||
|
@ -36,6 +36,7 @@
|
||||
* @param {Object} launchInstanceModel
|
||||
* @param {Object} $uibModal
|
||||
* @param {Object} toastService
|
||||
* @param {Object} settingsService
|
||||
* @description
|
||||
* Allows selection of key pairs.
|
||||
* @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.
|
||||
</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"
|
||||
ng-click="ctrl.createKeyPair()">
|
||||
<span class="fa fa-plus"></span>
|
||||
|
@ -36,6 +36,7 @@
|
||||
'horizon.app.core.cloud-services',
|
||||
'horizon.app.core.flavors',
|
||||
'horizon.app.core.images',
|
||||
'horizon.app.core.keypairs',
|
||||
'horizon.app.core.metadata',
|
||||
'horizon.app.core.network_qos',
|
||||
'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 = [
|
||||
'horizon.framework.util.http.service',
|
||||
'horizon.framework.widgets.toast.service',
|
||||
'$window'
|
||||
'horizon.framework.widgets.toast.service'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @param {Object} apiService
|
||||
* @param {Object} toastService
|
||||
* @param {Object} $window
|
||||
* @name novaApi
|
||||
* @description Provides access to Nova APIs.
|
||||
* @returns {Object} The service
|
||||
*/
|
||||
function novaAPI(apiService, toastService, $window) {
|
||||
function novaAPI(apiService, toastService) {
|
||||
|
||||
var service = {
|
||||
getActionList: getActionList,
|
||||
@ -71,8 +69,6 @@
|
||||
getServices: getServices,
|
||||
getInstanceMetadata: getInstanceMetadata,
|
||||
editInstanceMetadata: editInstanceMetadata,
|
||||
getCreateKeypairUrl: getCreateKeypairUrl,
|
||||
getRegenerateKeypairUrl: getRegenerateKeypairUrl,
|
||||
createFlavor: createFlavor,
|
||||
updateFlavor: updateFlavor,
|
||||
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
|
||||
* @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.usage import quotas
|
||||
|
||||
from novaclient import exceptions
|
||||
|
||||
|
||||
class NovaRestTestCase(test.TestCase):
|
||||
#
|
||||
@ -216,67 +214,6 @@ class NovaRestTestCase(test.TestCase):
|
||||
self.assertEqual('/api/nova/keypairs/Ni%21', response['location'])
|
||||
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
|
||||
#
|
||||
|
Loading…
Reference in New Issue
Block a user