Commit d43b71b4 authored by Lucas Palm's avatar Lucas Palm Committed by Justin Pomeroy

Add the Angular LBaaS V2 'Create Pool' workflow

This change adds the Create Pool workflow action to the
listeners table and the listeners detail page.  This wizard
allows you to create a new pool, as well as any of the pool's
child resources, including members and a health monitor.

Partially-Implements: blueprint horizon-lbaas-v2-ui
Change-Id: I934921f207373311b8b65f2ba34546238e961ac9
parent a7f1b762
......@@ -560,6 +560,25 @@ class Listener(generic.View):
neutronclient(request).delete_listener(listener_id)
@urls.register
class Pools(generic.View):
"""API for load balancer pools.
"""
url_regex = r'lbaas/pools/$'
@rest_utils.ajax()
def post(self, request):
"""Create a new pool.
Creates a new pool as well as other optional resources such as
members and health monitor.
"""
kwargs = {'loadbalancer_id': request.DATA.get('loadbalancer_id'),
'listener_id': request.DATA.get('parentResourceId')}
return create_pool(request, **kwargs)
@urls.register
class Pool(generic.View):
"""API for retrieving a single pool.
......
......@@ -48,6 +48,7 @@
deleteListener: deleteListener,
getPool: getPool,
deletePool: deletePool,
createPool: createPool,
getMembers: getMembers,
getMember: getMember,
getHealthMonitor: getHealthMonitor
......@@ -249,6 +250,21 @@
});
}
/**
* @name horizon.app.core.openstack-service-api.lbaasv2.createPool
* @description
* Create a new pool
* @param {object} spec
* Specifies the data used to create the new pool.
*/
function createPool(spec) {
return apiService.post('/api/lbaas/pools/', spec)
.error(function () {
toastService.add('error', gettext('Unable to create pool.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.lbaasv2.deletePool
* @description
......
......@@ -163,6 +163,14 @@
path: '/api/lbaas/listeners/1234',
error: 'Unable to delete listener.',
testInput: [ '1234' ]
},
{
func: 'createPool',
method: 'post',
path: '/api/lbaas/pools/',
error: 'Unable to create pool.',
data: { name: 'pool-1' },
testInput: [ { name: 'pool-1' } ]
}
];
......
......@@ -28,7 +28,8 @@
'horizon.app.core.openstack-service-api.policy',
'horizon.framework.util.i18n.gettext',
'horizon.dashboard.project.lbaasv2.loadbalancers.service',
'horizon.dashboard.project.lbaasv2.listeners.actions.delete'
'horizon.dashboard.project.lbaasv2.listeners.actions.delete',
'horizon.dashboard.project.lbaasv2.pools.actions.create'
];
/**
......@@ -45,11 +46,13 @@
* @param gettext The horizon gettext function for translation.
* @param loadBalancersService The LBaaS v2 load balancers service.
* @param deleteService The LBaaS v2 listeners delete service.
* @param createPoolService The LBaaS v2 pools create service.
* @returns Listeners row actions service object.
*/
function tableRowActions(
$q, $route, workflowModal, policy, gettext, loadBalancersService, deleteService
$q, $route, workflowModal, policy, gettext, loadBalancersService, deleteService,
createPoolService
) {
var loadbalancerId, loadBalancerIsActionable, handler;
......@@ -82,6 +85,11 @@
template: {
text: gettext('Edit')
}
},{
service: createPoolService.init(loadbalancerId, loadBalancerIsActionable).create,
template: {
text: gettext('Create Pool')
}
},{
service: deleteService.init(loadbalancerId, loadBalancerIsActionable, handler),
template: {
......
......@@ -82,9 +82,10 @@
}));
it('should define correct table row actions', function() {
expect(actions.length).toBe(2);
expect(actions.length).toBe(3);
expect(actions[0].template.text).toBe('Edit');
expect(actions[1].template.text).toBe('Delete Listener');
expect(actions[1].template.text).toBe('Create Pool');
expect(actions[2].template.text).toBe('Delete Listener');
});
it('should allow editing a listener of an ACTIVE load balancer', function() {
......
/*
* Copyright 2016 IBM Corp.
*
* 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.dashboard.project.lbaasv2.pools')
.factory('horizon.dashboard.project.lbaasv2.pools.actions.create', createService);
createService.$inject = [
'$q',
'$location',
'horizon.dashboard.project.lbaasv2.workflow.modal',
'horizon.app.core.openstack-service-api.policy',
'horizon.framework.util.q.extensions',
'horizon.framework.util.i18n.gettext'
];
/**
* @ngDoc factory
* @name horizon.dashboard.project.lbaasv2.listeners.actions.createService
* @description
* Provides the service for creating a pool resource.
* @param $q The angular service for promises.
* @param $location The angular $location service.
* @param workflowModal The LBaaS workflow modal service.
* @param policy The horizon policy service.
* @param qExtensions Horizon extensions to the $q service.
* @param gettext The horizon gettext function for translation.
* @returns The load balancers pool create service.
*/
function createService(
$q, $location, workflowModal, policy, qExtensions, gettext
) {
var loadbalancerId, listenerId, statePromise;
var create = workflowModal.init({
controller: 'CreatePoolWizardController',
message: gettext('A new pool is being created.'),
handle: onCreate,
allowed: allowed
});
var service = {
init: init,
create: create
};
return service;
//////////////
function init(_loadbalancerId_, _statePromise_) {
loadbalancerId = _loadbalancerId_;
statePromise = _statePromise_;
return service;
}
function allowed(item) {
listenerId = item.id;
return $q.all([
statePromise,
qExtensions.booleanAsPromise(!item.default_pool_id),
policy.ifAllowed({ rules: [['neutron', 'create_pool']] })
]);
}
function onCreate(response) {
var poolId = response.data.id;
$location.path('project/ngloadbalancersv2/' + loadbalancerId + '/listeners/' +
listenerId + '/pools/' + poolId);
}
}
})();
/*
* Copyright 2016 IBM Corp.
*
* 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('LBaaS v2 Create Pool Action Service', function() {
var scope, $q, $location, policy, init, createPoolService, defer;
function allowed(item) {
spyOn(policy, 'ifAllowed').and.returnValue(true);
var promise = createPoolService.create.allowed(item);
var allowed;
promise.then(function() {
allowed = true;
}, function() {
allowed = false;
});
scope.$apply();
expect(policy.ifAllowed).toHaveBeenCalledWith({rules: [['neutron', 'create_pool']]});
return allowed;
}
beforeEach(module('horizon.framework.util'));
beforeEach(module('horizon.framework.conf'));
beforeEach(module('horizon.framework.widgets'));
beforeEach(module('horizon.app.core.openstack-service-api'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(module(function($provide) {
var response = {
data: {
id: '9012'
}
};
var modal = {
open: function() {
return {
result: {
then: function(func) {
func(response);
}
}
};
}
};
$provide.value('$modal', modal);
}));
beforeEach(inject(function ($injector) {
scope = $injector.get('$rootScope').$new();
$q = $injector.get('$q');
policy = $injector.get('horizon.app.core.openstack-service-api.policy');
$location = $injector.get('$location');
createPoolService = $injector.get(
'horizon.dashboard.project.lbaasv2.pools.actions.create');
init = createPoolService.init;
defer = $q.defer();
}));
it('should define the correct service properties', function() {
expect(createPoolService.init).toBeDefined();
expect(createPoolService.create).toBeDefined();
});
it('should have the "allowed" and "perform" functions', function() {
expect(createPoolService.create.allowed).toBeDefined();
expect(createPoolService.create.perform).toBeDefined();
});
it('should allow creating a pool under an ACTIVE load balancer', function() {
defer.resolve();
init('active', defer.promise);
expect(allowed({default_pool_id: ''})).toBe(true);
});
it('should not allow creating a pool under an NON-ACTIVE load balancer', function() {
defer.reject();
init('non-active', defer.promise);
expect(allowed({default_pool_id: ''})).toBe(false);
});
it('should not allow creating a pool if a listener pool already exists', function() {
defer.resolve();
init('active', defer.promise);
expect(allowed({default_pool_id: '1234'})).toBe(false);
});
it('should redirect after create', function() {
defer.resolve();
spyOn($location, 'path').and.callThrough();
init('1234', defer.promise).create.allowed({id: '5678'});
createPoolService.create.perform();
expect($location.path).toHaveBeenCalledWith(
'project/ngloadbalancersv2/1234/listeners/5678/pools/9012');
});
});
})();
/*
* Copyright 2016 IBM Corp.
*
* 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.dashboard.project.lbaasv2.pools')
.controller('CreatePoolWizardController', CreatePoolWizardController);
CreatePoolWizardController.$inject = [
'$scope',
'$routeParams',
'horizon.dashboard.project.lbaasv2.workflow.model',
'horizon.dashboard.project.lbaasv2.workflow.workflow',
'horizon.framework.util.i18n.gettext'
];
function CreatePoolWizardController($scope, $routeParams, model, workflowService, gettext) {
var loadbalancerId = $routeParams.loadbalancerId;
var scope = $scope;
scope.model = model;
scope.submit = scope.model.submit;
scope.workflow = workflowService(
gettext('Create Pool'),
'fa fa-cloud-download',
['pool', 'members', 'monitor']
);
scope.model.initialize('pool', false, loadbalancerId, scope.launchContext.id);
}
})();
/*
* Copyright 2016 IBM Corp.
*
* 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('LBaaS v2 Create Pool Wizard Controller', function() {
var ctrl;
var model = {
submit: function() {
return 'created';
},
initialize: angular.noop
};
var workflow = function() {
return 'foo';
};
var scope = {
launchContext: {id: '1234'}
};
beforeEach(module('horizon.framework.util'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(module(function ($provide) {
$provide.value('horizon.dashboard.project.lbaasv2.workflow.model', model);
$provide.value('horizon.dashboard.project.lbaasv2.workflow.workflow', workflow);
}));
beforeEach(inject(function ($controller) {
spyOn(model, 'initialize');
ctrl = $controller('CreatePoolWizardController', { $scope: scope });
}));
it('defines the controller', function() {
expect(ctrl).toBeDefined();
});
it('calls initialize on the given model', function() {
expect(model.initialize).toHaveBeenCalled();
});
it('sets scope.workflow to the given workflow', function() {
expect(scope.workflow).toBe('foo');
});
it('defines scope.submit', function() {
expect(scope.submit).toBeDefined();
expect(scope.submit()).toBe('created');
});
});
})();
......@@ -117,7 +117,7 @@
* @param id ID of the resource being edited.
*/
function initialize(resource, id, loadBalancerId) {
function initialize(resource, id, loadBalancerId, parentResourceId) {
var promise;
model.certificatesError = false;
......@@ -133,6 +133,7 @@
model.spec = {
loadbalancer_id: loadBalancerId,
parentResourceId: parentResourceId,
loadbalancer: {
name: null,
description: null,
......@@ -206,6 +207,17 @@
]).then(initMemberAddresses);
model.context.submit = createListener;
break;
case 'createpool':
// We get the listener details here because we need to know the listener protocol
// in order to default the new pool's protocol to match.
promise = $q.all([
lbaasv2API.getListener(model.spec.parentResourceId).then(onGetListener),
neutronAPI.getSubnets().then(onGetSubnets),
neutronAPI.getPorts().then(onGetPorts),
novaAPI.getServers().then(onGetServers)
]).then(initMemberAddresses);
model.context.submit = createPool;
break;
case 'editloadbalancer':
promise = $q.all([
lbaasv2API.getLoadBalancer(model.context.id).then(onGetLoadBalancer),
......@@ -268,6 +280,10 @@
return lbaasv2API.createListener(spec);
}
function createPool(spec) {
return lbaasv2API.createPool(spec);
}
function editLoadBalancer(spec) {
return lbaasv2API.editLoadBalancer(model.context.id, spec);
}
......@@ -469,36 +485,39 @@
}
function onGetListener(response) {
var resources = response.data;
setListenerSpec(resources.listener);
model.visibleResources.push('listener');
model.spec.loadbalancer_id = resources.listener.loadbalancers[0].id;
if (resources.listener.protocol === 'TERMINATED_HTTPS') {
keymanagerPromise.then(prepareCertificates).then(function addAvailableCertificates() {
resources.listener.sni_container_refs.forEach(function addAvailableCertificate(ref) {
model.certificates.filter(function matchCertificate(cert) {
return cert.id === ref;
}).forEach(function addCertificate(cert) {
model.spec.certificates.push(cert);
var result = response.data;
setListenerSpec(result.listener || result);
if (result.listener) {
model.visibleResources.push('listener');
model.spec.loadbalancer_id = result.listener.loadbalancers[0].id;
if (result.listener.protocol === 'TERMINATED_HTTPS') {
keymanagerPromise.then(prepareCertificates).then(function addAvailableCertificates() {
result.listener.sni_container_refs.forEach(function addAvailableCertificate(ref) {
model.certificates.filter(function matchCertificate(cert) {
return cert.id === ref;
}).forEach(function addCertificate(cert) {
model.spec.certificates.push(cert);
});
});
});
});
model.visibleResources.push('certificates');
model.visibleResources.push('certificates');
}
}
if (resources.pool) {
setPoolSpec(resources.pool);
if (result.pool) {
setPoolSpec(result.pool);
model.visibleResources.push('pool');
model.visibleResources.push('members');
if (resources.members) {
setMembersSpec(resources.members);
if (result.members) {
setMembersSpec(result.members);
}
if (resources.monitor) {
setMonitorSpec(resources.monitor);
if (result.monitor) {
setMonitorSpec(result.monitor);
model.visibleResources.push('monitor');
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment