Add create and import key pair actions

This patch adds create and import keypair actions into
Angularized key pair panel as global action.

Also, to load public key from file, this patch uses
load-edit directive.

To use load-edit directive from angular-schema-form,
i.e. from separated scope, this patch enables to configure
callback function to get content of textarea in load-edit.

Change-Id: Ie38bff8fba90de99095b589d70da45dcb202fa56
Imprements: blueprint ng-keypair
Needed-By: I3d6ae0b513120cf50c89b40234b602b816adfd48
Needed-By: I9200baa585c18095656d0459c649391b61b553a2
This commit is contained in:
Shu Muto 2017-07-10 20:52:16 +09:00
parent 7dac18b319
commit 5424c6306a
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

@ -325,7 +325,7 @@ COMPRESS_OFFLINE_CONTEXT = 'horizon.themes.offline_context'
# 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.