Merge "Add create and import key pair actions"

This commit is contained in:
Zuul 2017-12-15 03:56:26 +00:00 committed by Gerrit Code Review
commit ad17915e43
18 changed files with 639 additions and 7 deletions

View File

@ -48,7 +48,7 @@ Default:
{
'images_panel': True,
'key_pairs_panel': False,
'key_pairs_panel': True,
'flavors_panel': False,
'domains_panel': False,
'users_panel': False,

View File

@ -1,3 +1,4 @@
<action action-classes="'$action-classes$ btn btn-default'">
<span class="fa fa-$icon$"></span>
$text$
</action>

View File

@ -1,3 +1,4 @@
<action action-classes="'$action-classes$'" item="$item$">
<span class="fa fa-$icon$"></span>
$text$
</action>

View File

@ -208,6 +208,7 @@
.replace(
'$action-classes$', getActionClasses(action, index, permittedActions.length)
)
.replace('$icon$', action.template.icon)
.replace('$text$', action.template.text)
.replace('$title$', action.template.title)
.replace('$description$', action.template.description)

View File

@ -52,7 +52,8 @@
maxBytes: '@',
key: '@',
required: '=',
rows: '@'
rows: '@',
onTextareaChange: '&'
},
link: link,
templateUrl: basePath + 'load-edit.html'
@ -113,6 +114,7 @@
} else {
$scope.textModified = false;
}
$scope.onTextareaChange({textContent: $scope.textContent});
});
}

View File

@ -327,7 +327,7 @@ SHOW_KEYSTONE_V2_RC = True
# Dictionary of currently available angular features
ANGULAR_FEATURES = {
'images_panel': True,
'key_pairs_panel': False,
'key_pairs_panel': True,
'flavors_panel': False,
'domains_panel': False,
'users_panel': False,

View File

@ -21,3 +21,7 @@ hz-details {
}
}
}
textarea#public_key {
height: 22em;
}

View File

@ -30,16 +30,37 @@
registerKeypairActions.$inject = [
'horizon.framework.conf.resource-type-registry.service',
'horizon.app.core.keypairs.actions.create.service',
'horizon.app.core.keypairs.actions.import.service',
'horizon.app.core.keypairs.actions.delete.service',
'horizon.app.core.keypairs.resourceType'
];
function registerKeypairActions(
registry,
createKeypairService,
importKeypairService,
deleteKeypairService,
resourceType
) {
var keypairResourceType = registry.getResourceType(resourceType);
keypairResourceType.globalActions
.append({
id: 'createKeypairService',
service: createKeypairService,
template: {
type: 'create',
text: gettext('Create Key Pair')
}
})
.append({
id: 'importKeypairService',
service: importKeypairService,
template: {
text: gettext('Import Public Key'),
icon: 'upload'
}
});
keypairResourceType.batchActions
.append({
id: 'batchDeleteKeypairAction',

View File

@ -0,0 +1,5 @@
<p translate>
Key Pairs are how you login to your instance after it is launched.
Choose a key pair name you will recognize.
Names may only include alphanumeric characters, spaces, or dashes.
</p>

View File

@ -0,0 +1,159 @@
/**
* 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.app.core.keypairs.create.service
* @description Service for the key pair create modal
*/
angular
.module('horizon.app.core.keypairs.actions')
.factory('horizon.app.core.keypairs.actions.create.service', createService);
createService.$inject = [
'horizon.app.core.keypairs.basePath',
'horizon.app.core.keypairs.resourceType',
'horizon.app.core.openstack-service-api.nova',
'horizon.app.core.openstack-service-api.policy',
'horizon.framework.util.actions.action-result.service',
'horizon.framework.util.file.text-download',
'horizon.framework.widgets.form.ModalFormService',
'horizon.framework.widgets.toast.service'
];
function createService(
basePath, resourceType, nova, policy, actionResult, download, modal, toast
) {
var keypairs = [];
var caption = gettext("Create Key Pair");
var invalidMsg = gettext("Key pair already exists.");
// schema
var schema = {
type: "object",
properties: {
"name": {
title: gettext("Key Pair Name"),
type: "string",
pattern: "^[A-Za-z0-9 -]+$"
}
}
};
// form
var form = [
{
type: "section",
htmlClass: "row",
items: [
{
type: "section",
htmlClass: "col-sm-6",
items: [
{
key: "name",
validationMessage: {
keypairExists: invalidMsg
},
$validators: {
keypairExists: function (name) {
return (keypairs.indexOf(name) === -1);
}
},
required: true
}
]
},
{
type: "section",
htmlClass: "col-sm-6",
items: [
{
type: "template",
templateUrl: basePath + "actions/create.description.html"
}
]
}
]
}
];
// model
var model;
var service = {
perform: perform,
allowed: allowed,
getKeypairs: getKeypairs
};
return service;
//////////////
function allowed() {
return policy.ifAllowed({ rules: [['compute', 'os_compute_api:os-keypairs:create']] });
}
function perform() {
getKeypairs();
model = {
name: ""
};
var config = {
"title": caption,
"submitText": caption,
"schema": schema,
"form": form,
"model": model,
"submitIcon": "plus"
};
return modal.open(config).then(submit);
}
function submit(context) {
return nova.createKeypair(context.model).then(success);
}
/**
* @ngdoc function
* @name success
* @description
* Informs the user about the created key pair.
* @param {Object} keypair The new key pair object
* @returns {undefined} No return value
*/
function success(response) {
var successMsg = gettext('Key pair %(name)s was successfully created.');
toast.add('success', interpolate(successMsg, { name: response.data.name }, true));
download.downloadTextFile(response.data.private_key, response.data.name + '.pem');
var result = actionResult.getActionResult().created(resourceType, response.data.name);
return result.result;
}
function getKeypairs() {
nova.getKeypairs().then(function(response) {
keypairs = response.data.items.map(getName);
});
}
function getName(item) {
return item.keypair.name;
}
}
})();

View File

@ -0,0 +1,74 @@
/**
* 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.actions.create.service', function() {
var service, nova, $q, $scope, deferred, deferredKeypairs, deferredNewKeypair, toast;
var model = {
name: "Hiroshige"
};
var modal = {
open: function (config) {
config.model = model;
deferred = $q.defer();
deferred.resolve(config);
return deferred.promise;
}
};
///////////////////
beforeEach(module('horizon.app.core'));
beforeEach(module('horizon.framework'));
beforeEach(module('horizon.app.core.keypairs.actions'));
beforeEach(module(function($provide) {
$provide.value('horizon.framework.widgets.form.ModalFormService', modal);
}));
beforeEach(inject(function($injector, _$rootScope_, _$q_) {
$scope = _$rootScope_.$new();
$q = _$q_;
service = $injector.get('horizon.app.core.keypairs.actions.create.service');
nova = $injector.get('horizon.app.core.openstack-service-api.nova');
toast = $injector.get('horizon.framework.widgets.toast.service');
deferredKeypairs = $q.defer();
deferredKeypairs.resolve({data: {items: [{keypair: {name: "Hokusai"}}]}});
spyOn(nova, 'getKeypairs').and.returnValue(deferredKeypairs.promise);
deferredNewKeypair = $q.defer();
deferredNewKeypair.resolve({data: {items: [{keypair: {name: "Hiroshige"}}]}});
spyOn(nova, 'createKeypair').and.returnValue(deferredNewKeypair.promise);
spyOn(modal, 'open').and.callThrough();
spyOn(toast, 'add').and.callFake(angular.noop);
}));
it('should check the policy if the user is allowed to create key pair', function() {
var allowed = service.allowed();
expect(allowed).toBeTruthy();
});
it('should open the modal and submit', inject(function() {
service.perform();
expect(nova.getKeypairs).toHaveBeenCalled();
expect(modal.open).toHaveBeenCalled();
$scope.$apply();
expect(nova.createKeypair).toHaveBeenCalled();
expect(toast.add).toHaveBeenCalled();
}));
});
})();

View File

@ -0,0 +1,23 @@
<p translate>
Key Pairs are how you login to your instance after it is launched.
Choose a key pair name you will recognize and paste your SSH public key into the
space provided.
</p>
<p translate>
There are two ways to generate a key pair. From a Linux system,
generate the key pair with the <samp>ssh-keygen</samp> command:
</p>
<p>
<code>ssh-keygen -t rsa -f cloud.key</code>
</p>
<p translate>
This command generates a pair of keys: a private key (cloud.key)
and a public key (cloud.key.pub).
</p>
<p translate>
From a Windows system, you can use PuTTYGen to create private/public keys.
Use the PuTTY Key Generator to create and save the keys, then copy
the public key in the red highlighted box to your <samp>.ssh/authorized_keys</samp>
file.
</p>

View File

@ -0,0 +1,43 @@
/**
* 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 controller
* @name horizon.app.core.keypairs.actions.ImportPublicKeyController
* @ngController
*
* @description
* Controller for the loading public key file
*/
angular
.module('horizon.app.core.keypairs.actions')
.controller('horizon.app.core.keypairs.actions.ImportPublicKeyController',
importPublicKeyController);
importPublicKeyController.$inject = [
'$scope'
];
function importPublicKeyController($scope) {
var ctrl = this;
ctrl.title = $scope.schema.properties.public_key.title;
ctrl.public_key = "";
ctrl.onPublicKeyChange = function (publicKey) {
$scope.model.public_key = publicKey;
};
}
})();

View File

@ -0,0 +1,54 @@
/**
* 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.actions.ImportPublicKeyController', function() {
var ctrl, scope;
beforeEach(module('horizon.app.core.keypairs'));
beforeEach(inject(function ($injector, _$rootScope_) {
scope = _$rootScope_.$new();
scope.schema = {
properties: {
public_key: {
title: 'Public Key'
}
}
};
scope.model = {
public_key: ''
};
var controller = $injector.get('$controller');
ctrl = controller(
'horizon.app.core.keypairs.actions.ImportPublicKeyController',
{ $scope: scope }
);
}));
it('gets title from scope.schema.properties.public_key.title', function() {
expect(ctrl.title).toBe('Public Key');
});
it('sets public key into scope.model.public_key', function() {
var key = 'public key string';
ctrl.onPublicKeyChange(key);
expect(scope.model.public_key).toBeDefined(key);
});
});
})();

View File

@ -0,0 +1,9 @@
<div ng-controller="horizon.app.core.keypairs.actions.ImportPublicKeyController as ctrl">
<load-edit title="{$ ctrl.title $}"
model="ctrl.public_key"
max-bytes="{$ 16 * 1024 $}"
key="public-key"
rows=8 required="true"
on-textarea-change="ctrl.onPublicKeyChange(textContent)">
</load-edit>
</div>

View File

@ -0,0 +1,157 @@
/**
* 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.app.core.keypairs.import.service
* @description Service for the key pair import modal
*/
angular
.module('horizon.app.core.keypairs.actions')
.factory('horizon.app.core.keypairs.actions.import.service', importService);
importService.$inject = [
'horizon.app.core.keypairs.basePath',
'horizon.app.core.keypairs.resourceType',
'horizon.app.core.openstack-service-api.nova',
'horizon.app.core.openstack-service-api.policy',
'horizon.framework.util.actions.action-result.service',
'horizon.framework.widgets.form.ModalFormService',
'horizon.framework.widgets.toast.service'
];
function importService(
basePath, resourceType, nova, policy, actionResult, modal, toast
) {
var keypairs = [];
var caption = gettext("Import Public Key");
var invalidMsg = gettext("Key pair already exists.");
// schema
var schema = {
type: "object",
properties: {
"name": {
title: gettext("Key Pair Name"),
type: "string",
pattern: "^[A-Za-z0-9 -]+$"
},
"public_key": {
title: gettext("Public Key"),
type: "string"
}
}
};
// form
var form = [
{
type: "section",
htmlClass: "row",
items: [
{
type: "section",
htmlClass: "col-sm-6",
items: [
{
key: "name",
validationMessage: {
keypairExists: invalidMsg
},
$validators: {
keypairExists: function (name) {
return (keypairs.indexOf(name) === -1);
}
},
required: true
},
{
type: "template",
templateUrl: basePath + "actions/import.public-key.html"
}
]
},
{
type: "section",
htmlClass: "col-sm-6",
items: [
{
type: "template",
templateUrl: basePath + "actions/import.description.html"
}
]
}
]
}
];
// model
var model;
var service = {
perform: perform,
allowed: allowed
};
return service;
//////////////
function allowed() {
return policy.ifAllowed({ rules: [['compute', 'os_compute_api:os-keypairs:create']] });
}
function perform() {
getKeypairs();
model = {
name: "",
public_key: ""
};
var config = {
"title": caption,
"submitText": caption,
"schema": schema,
"form": form,
"model": model,
"submitIcon": "upload"
};
return modal.open(config).then(submit);
}
function submit(context) {
return nova.createKeypair(context.model).then(success);
}
function success(response) {
var successMsg = gettext('Successfully imported key pair %(name)s.');
toast.add('success', interpolate(successMsg, { name: response.data.name }, true));
var result = actionResult.getActionResult().created(resourceType, response.data.name);
return result.result;
}
function getKeypairs() {
nova.getKeypairs().then(function(response) {
keypairs = response.data.items.map(getName);
});
}
function getName(item) {
return item.keypair.name;
}
}
})();

View File

@ -0,0 +1,75 @@
/**
* 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.actions.import.service', function() {
var service, nova, $q, $scope, deferred, deferredKeypairs, deferredNewKeypair, toast;
var model = {
name: "Hiroshige",
public_key: "secret"
};
var modal = {
open: function (config) {
config.model = model;
deferred = $q.defer();
deferred.resolve(config);
return deferred.promise;
}
};
///////////////////
beforeEach(module('horizon.app.core'));
beforeEach(module('horizon.framework'));
beforeEach(module('horizon.app.core.keypairs.actions'));
beforeEach(module(function($provide) {
$provide.value('horizon.framework.widgets.form.ModalFormService', modal);
}));
beforeEach(inject(function($injector, _$rootScope_, _$q_) {
$scope = _$rootScope_.$new();
$q = _$q_;
service = $injector.get('horizon.app.core.keypairs.actions.import.service');
nova = $injector.get('horizon.app.core.openstack-service-api.nova');
toast = $injector.get('horizon.framework.widgets.toast.service');
deferredKeypairs = $q.defer();
deferredKeypairs.resolve({data: {items: [{keypair: {name: "Hokusai"}}]}});
spyOn(nova, 'getKeypairs').and.returnValue(deferredKeypairs.promise);
deferredNewKeypair = $q.defer();
deferredNewKeypair.resolve({data: {items: [{keypair: {name: "Hiroshige"}}]}});
spyOn(nova, 'createKeypair').and.returnValue(deferredNewKeypair.promise);
spyOn(modal, 'open').and.callThrough();
spyOn(toast, 'add').and.callFake(angular.noop);
}));
it('should check the policy if the user is allowed to import key pair', function() {
var allowed = service.allowed();
expect(allowed).toBeTruthy();
});
it('should open the modal and submit', inject(function() {
service.perform();
expect(nova.getKeypairs).toHaveBeenCalled();
expect(modal.open).toHaveBeenCalled();
$scope.$apply();
expect(nova.createKeypair).toHaveBeenCalled();
expect(toast.add).toHaveBeenCalled();
}));
});
})();

View File

@ -2,7 +2,10 @@
features:
- |
[`blueprint ng-keypairs <https://blueprints.launchpad.net/horizon/+spec/ng-keypairs>`_]
Add Angular Key Pairs panel. The Key Pairs panel allows users to view
a list of created or imported key pairs. This panel displays a table
view of the name and fingerprint of each key pair. Also, public key
is shown in expanded row.
AngularJS-based Key Pairs panel is added. The features in the legacy
panel are fully implemented. The Key Pairs panel now may be configured
to use either the legacy or AngularJS-based codes.
The ANGULAR_FEATURES setting now allows for a `key_pairs_panel`.
If set to True, then the AngularJS-Based Key Pairs panel will be used,
while the Django version will be used if set to False. Default value
for key_pairs_panel is True.