Add the Angular LBaaS V2 'Create Listener' workflow

This change adds the Create Listener workflow action to
the listeners table on the load balancer detail page.  This wizard
allows you to crete a new listener, as well as any resource below
it in the hierarchy.

Partially-Implements: blueprint horizon-lbaas-v2-ui
Change-Id: I40488eac6c116e363071fb82ba3473a8b0430ed9
This commit is contained in:
Lucas Palm 2016-02-26 13:51:33 -06:00 committed by Justin Pomeroy
parent 73f1e37304
commit af63722e3a
17 changed files with 882 additions and 48 deletions

View File

@ -59,6 +59,31 @@ def poll_loadbalancer_status(request, loadbalancer_id, callback,
callback(request, **kwargs)
def create_loadbalancer(request):
data = request.DATA
spec = {
'vip_subnet_id': data['loadbalancer']['subnet']
}
if data['loadbalancer'].get('name'):
spec['name'] = data['loadbalancer']['name']
if data['loadbalancer'].get('description'):
spec['description'] = data['loadbalancer']['description']
if data['loadbalancer'].get('ip'):
spec['vip_address'] = data['loadbalancer']['ip']
loadbalancer = neutronclient(request).create_loadbalancer(
{'loadbalancer': spec}).get('loadbalancer')
if data.get('listener'):
# There is work underway to add a new API to LBaaS v2 that will
# allow us to pass in all information at once. Until that is
# available we use a separate thread to poll for the load
# balancer status and create the other resources when it becomes
# active.
args = (request, loadbalancer['id'], create_listener)
kwargs = {'from_state': 'PENDING_CREATE'}
thread.start_new_thread(poll_loadbalancer_status, args, kwargs)
return loadbalancer
def create_listener(request, **kwargs):
"""Create a new listener.
@ -391,28 +416,7 @@ class LoadBalancers(generic.View):
Creates a new load balancer as well as other optional resources such as
a listener, pool, monitor, etc.
"""
data = request.DATA
spec = {
'vip_subnet_id': data['loadbalancer']['subnet']
}
if data['loadbalancer'].get('name'):
spec['name'] = data['loadbalancer']['name']
if data['loadbalancer'].get('description'):
spec['description'] = data['loadbalancer']['description']
if data['loadbalancer'].get('ip'):
spec['vip_address'] = data['loadbalancer']['ip']
loadbalancer = neutronclient(request).create_loadbalancer(
{'loadbalancer': spec}).get('loadbalancer')
if data.get('listener'):
# There is work underway to add a new API to LBaaS v2 that will
# allow us to pass in all information at once. Until that is
# available we use a separate thread to poll for the load
# balancer status and create the other resources when it becomes
# active.
args = (request, loadbalancer['id'], create_listener)
kwargs = {'from_state': 'PENDING_CREATE'}
thread.start_new_thread(poll_loadbalancer_status, args, kwargs)
return loadbalancer
return create_loadbalancer(request)
@urls.register
@ -473,6 +477,16 @@ class Listeners(generic.View):
loadbalancer_id)
return {'items': listener_list}
@rest_utils.ajax()
def post(self, request):
"""Create a new listener.
Creates a new listener as well as other optional resources such as
a pool, members, and health monitor.
"""
kwargs = {'loadbalancer_id': request.DATA.get('loadbalancer_id')}
return create_listener(request, **kwargs)
def _filter_listeners(self, listener_list, loadbalancer_id):
filtered_listeners = []

View File

@ -43,6 +43,7 @@
editLoadBalancer: editLoadBalancer,
getListeners: getListeners,
getListener: getListener,
createListener: createListener,
editListener: editListener,
getPool: getPool,
getMembers: getMembers,
@ -181,6 +182,21 @@
});
}
/**
* @name horizon.app.core.openstack-service-api.lbaasv2.createListener
* @description
* Create a new listener
* @param {object} spec
* Specifies the data used to create the new listener.
*/
function createListener(spec) {
return apiService.post('/api/lbaas/listeners/', spec)
.error(function () {
toastService.add('error', gettext('Unable to create listener.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.lbaasv2.editListener
* @description

View File

@ -134,6 +134,14 @@
data: { name: 'loadbalancer-1' },
testInput: [ '1234', { name: 'loadbalancer-1' } ]
},
{
func: 'createListener',
method: 'post',
path: '/api/lbaas/listeners/',
error: 'Unable to create listener.',
data: { name: 'listener-1' },
testInput: [ { name: 'listener-1' } ]
},
{
func: 'editListener',
method: 'put',

View File

@ -0,0 +1,97 @@
/*
* 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.listeners')
.factory('horizon.dashboard.project.lbaasv2.listeners.actions.batchActions',
tableBatchActions);
tableBatchActions.$inject = [
'$q',
'$location',
'horizon.dashboard.project.lbaasv2.workflow.modal',
'horizon.app.core.openstack-service-api.policy',
'horizon.framework.util.i18n.gettext',
'horizon.dashboard.project.lbaasv2.loadbalancers.service'
];
/**
* @ngdoc service
* @ngname horizon.dashboard.project.lbaasv2.listeners.actions.batchActions
*
* @description
* Provides the service for the Listeners table batch actions.
*
* @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 gettext The horizon gettext function for translation.
* @param loadBalancersService The LBaaS v2 load balancers service.
* @returns Listeners table batch actions service object.
*/
function tableBatchActions($q, $location, workflowModal, policy, gettext, loadBalancersService) {
var loadBalancerIsActive, loadBalancerId;
var create = workflowModal.init({
controller: 'CreateListenerWizardController',
message: gettext('A new listener is being created.'),
handle: onCreate,
allowed: canCreate
});
var service = {
actions: actions,
init: init
};
return service;
///////////////
function init(loadbalancerId) {
loadBalancerId = loadbalancerId;
loadBalancerIsActive = loadBalancersService.isActive(loadbalancerId);
return service;
}
function actions() {
return [{
service: create,
template: {
type: 'create',
text: gettext('Create Listener')
}
}];
}
function canCreate() {
return $q.all([
loadBalancerIsActive,
policy.ifAllowed({ rules: [['neutron', 'create_listener']] })
]);
}
function onCreate(response) {
var id = response.data.id;
$location.path('project/ngloadbalancersv2/' + loadBalancerId + '/listeners/' + id);
}
}
})();

View File

@ -0,0 +1,104 @@
/*
* 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 Listeners Table Batch Actions Service', function() {
var $location, actions, policy, $scope, $q;
beforeEach(module('horizon.framework.util'));
beforeEach(module('horizon.framework.conf'));
beforeEach(module('horizon.framework.widgets.toast'));
beforeEach(module('horizon.app.core.openstack-service-api'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(module(function($provide) {
var response = {
data: {
id: '5678'
}
};
var modal = {
open: function() {
return {
result: {
then: function(func) {
func(response);
}
}
};
}
};
$provide.value('$modal', modal);
$provide.value('horizon.dashboard.project.lbaasv2.loadbalancers.service', {
isActive: function() {
return $q.when();
}
});
$provide.value('horizon.app.core.openstack-service-api.policy', {
ifAllowed: function() {
return $q.when();
}
});
}));
beforeEach(inject(function ($injector) {
$location = $injector.get('$location');
$scope = $injector.get('$rootScope').$new();
$q = $injector.get('$q');
policy = $injector.get('horizon.app.core.openstack-service-api.policy');
var batchActionsService = $injector.get(
'horizon.dashboard.project.lbaasv2.listeners.actions.batchActions');
actions = batchActionsService.init('1234').actions();
$scope.$apply();
}));
it('should define correct table batch actions', function() {
expect(actions.length).toBe(1);
expect(actions[0].template.text).toBe('Create Listener');
});
it('should have the "allowed" and "perform" functions', function() {
actions.forEach(function(action) {
expect(action.service.allowed).toBeDefined();
expect(action.service.perform).toBeDefined();
});
});
it('should check policy to allow creating a listener', function() {
spyOn(policy, 'ifAllowed').and.callThrough();
var promise = actions[0].service.allowed();
var allowed;
promise.then(function() {
allowed = true;
}, function() {
allowed = false;
});
$scope.$apply();
expect(allowed).toBe(true);
expect(policy.ifAllowed).toHaveBeenCalledWith({rules: [['neutron', 'create_listener']]});
});
it('should redirect after create', function() {
spyOn($location, 'path').and.callThrough();
actions[0].service.perform();
expect($location.path).toHaveBeenCalledWith('project/ngloadbalancersv2/1234/listeners/5678');
});
});
})();

View File

@ -0,0 +1,44 @@
/*
* 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.listeners')
.controller('CreateListenerWizardController', CreateListenerWizardController);
CreateListenerWizardController.$inject = [
'$scope',
'$routeParams',
'horizon.dashboard.project.lbaasv2.workflow.model',
'horizon.dashboard.project.lbaasv2.workflow.workflow',
'horizon.framework.util.i18n.gettext'
];
function CreateListenerWizardController($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 Listener'),
'fa fa-cloud-download',
['listener', 'pool', 'members', 'monitor']
);
scope.model.initialize('listener', false, loadbalancerId);
}
})();

View File

@ -0,0 +1,61 @@
/*
* 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 Listener Wizard Controller', function() {
var ctrl;
var model = {
submit: function() {
return 'created';
},
initialize: angular.noop
};
var workflow = function() {
return 'foo';
};
var scope = {};
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('CreateListenerWizardController', { $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');
});
});
})();

View File

@ -23,7 +23,8 @@
ListenersTableController.$inject = [
'horizon.app.core.openstack-service-api.lbaasv2',
'$routeParams',
'horizon.dashboard.project.lbaasv2.listeners.actions.rowActions'
'horizon.dashboard.project.lbaasv2.listeners.actions.rowActions',
'horizon.dashboard.project.lbaasv2.listeners.actions.batchActions'
];
/**
@ -36,16 +37,18 @@
* @param api The LBaaS V2 service API.
* @param $routeParams The angular $routeParams service.
* @param rowActions The listener row actions service.
* @param batchActions The listener batch actions service.
* @returns undefined
*/
function ListenersTableController(api, $routeParams, rowActions) {
function ListenersTableController(api, $routeParams, rowActions, batchActions) {
var ctrl = this;
ctrl.items = [];
ctrl.src = [];
ctrl.checked = {};
ctrl.loadbalancerId = $routeParams.loadbalancerId;
ctrl.batchActions = batchActions.init(ctrl.loadbalancerId);
ctrl.rowActions = rowActions.init(ctrl.loadbalancerId);
init();

View File

@ -17,7 +17,7 @@
'use strict';
describe('LBaaS v2 Listeners Table Controller', function() {
var controller, lbaasv2API, rowActions;
var controller, lbaasv2API, rowActions, batchActions;
var items = [];
function fakeAPI() {
@ -48,6 +48,8 @@
lbaasv2API = $injector.get('horizon.app.core.openstack-service-api.lbaasv2');
controller = $injector.get('$controller');
rowActions = $injector.get('horizon.dashboard.project.lbaasv2.listeners.actions.rowActions');
batchActions = $injector.get(
'horizon.dashboard.project.lbaasv2.listeners.actions.batchActions');
spyOn(rowActions, 'init').and.callFake(initMock);
spyOn(lbaasv2API, 'getListeners').and.callFake(fakeAPI);
}));
@ -65,7 +67,10 @@
expect(ctrl.checked).toEqual({});
expect(ctrl.loadbalancerId).toEqual('1234');
expect(rowActions.init).toHaveBeenCalledWith(ctrl.loadbalancerId);
expect(ctrl.rowActions).toBeDefined();
expect(ctrl.rowActions).toEqual(rowActions);
expect(ctrl.batchActions).toBeDefined();
expect(ctrl.batchActions).toEqual(batchActions);
});
it('should invoke lbaasv2 apis', function() {

View File

@ -17,7 +17,9 @@
This is where batch actions like searching, creating, and deleting.
-->
<th colspan="7" class="search-header">
<hz-search-bar icon-classes="fa-search"></hz-search-bar>
<hz-search-bar icon-classes="fa-search">
<actions allowed="table.batchActions.actions" type="batch"></actions>
</hz-search-bar>
</th>
</tr>

View File

@ -0,0 +1,75 @@
/*
* 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';
/**
* @ngdoc directive
* @name horizon.dashboard.project.lbaasv2:validateUnique
* @element ng-model
* @description
* The `validateUnique` directive provides validation
* for form input elements to ensure values are unique.
*
* Validator returns true if model/view value is not in
* the array of values specified.
*
* @restrict A
*
* @example
* ```
* <input type="number" ng-model="value"
* validate-unique="[80,443]">
* ```
*/
angular
.module('horizon.dashboard.project.lbaasv2')
.directive('validateUnique', validateUnique);
function validateUnique() {
var directive = {
require: 'ngModel',
restrict: 'A',
link: link
};
return directive;
//////////
function link(scope, element, attrs, ctrl) {
ctrl.$parsers.push(uniqueValidator);
ctrl.$formatters.push(uniqueValidator);
attrs.$observe('validateUnique', function () {
uniqueValidator(ctrl.$modelValue);
});
function uniqueValidator(value) {
var values = scope.$eval(attrs.validateUnique);
if (angular.isArray(values) && values.length > 0 && values.indexOf(value) > -1) {
ctrl.$setValidity('unique', false);
} else {
ctrl.$setValidity('unique', true);
}
// Return the value rather than undefined if invalid
return value;
}
}
}
})();

View File

@ -0,0 +1,69 @@
/*
* 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('validate-unique directive', function () {
var $compile, scope, element, port, name;
var markup =
'<form>' +
'<input type="number" ng-model="port" validate-unique="ports">' +
'<input type="string" ng-model="name" validate-unique="names">' +
'</form>';
beforeEach(module('horizon.framework.widgets'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(inject(function ($injector) {
$compile = $injector.get('$compile');
scope = $injector.get('$rootScope').$new();
// generate dom from markup
element = $compile(markup)(scope);
port = element.children('input[type="number"]');
name = element.children('input[type="string"]');
// setup up initial data
scope.ports = [80, 443];
scope.names = ['name1', 'name2'];
scope.$apply();
}));
it('should be initially empty', function () {
expect(port.val()).toEqual('');
expect(name.val()).toEqual('');
expect(port.hasClass('ng-valid')).toBe(true);
expect(name.hasClass('ng-valid')).toBe(true);
});
it('should be invalid if values are not unique', function () {
scope.port = 80;
scope.name = 'name1';
scope.$apply();
expect(port.hasClass('ng-valid')).toBe(false);
expect(name.hasClass('ng-valid')).toBe(false);
});
it('should be valid if values are unique', function () {
scope.port = 81;
scope.name = 'name3';
scope.$apply();
expect(port.hasClass('ng-valid')).toBe(true);
expect(name.hasClass('ng-valid')).toBe(true);
});
});
})();

View File

@ -41,14 +41,21 @@
ctrl.protocolChange = protocolChange;
// Error text for invalid fields
ctrl.portError = gettext('The port must be a number between 1 and 65535.');
ctrl.portNumberError = gettext('The port must be a number between 1 and 65535.');
ctrl.portUniqueError = gettext(
'The port must be unique among all listeners attached to this load balancer.');
ctrl.certificatesError = gettext('There was an error obtaining certificates from the ' +
'key-manager service. The TERMINATED_HTTPS protocol is unavailable.');
////////////
function protocolChange(protocol) {
$scope.model.spec.listener.port = { HTTP: 80, TERMINATED_HTTPS: 443 }[protocol];
var defaultPort = { HTTP: 80, TERMINATED_HTTPS: 443 }[protocol];
while (listenerPortExists(defaultPort)) {
defaultPort += 1;
}
$scope.model.spec.listener.port = defaultPort;
var members = $scope.model.members.concat($scope.model.spec.members);
members.forEach(function setMemberPort(member) {
member.port = { HTTP: 80, TERMINATED_HTTPS: 80 }[protocol];
@ -64,5 +71,11 @@
workflow.remove('certificates');
}
}
function listenerPortExists(port) {
return $scope.model.listenerPorts.some(function(element) {
return element === port;
});
}
}
})();

View File

@ -32,10 +32,12 @@
remove: angular.noop
};
listener = {
protocol: null
protocol: null,
port: 80
};
scope = {
model: {
listenerPorts: [80],
members: [{port: ''}, {port: ''}],
spec: {
listener: listener,
@ -48,7 +50,8 @@
}));
it('should define error messages for invalid fields', function() {
expect(ctrl.portError).toBeDefined();
expect(ctrl.portNumberError).toBeDefined();
expect(ctrl.portUniqueError).toBeDefined();
expect(ctrl.certificatesError).toBeDefined();
});
@ -77,7 +80,7 @@
it('should update port on protocol change to HTTP', function() {
ctrl.protocolChange('HTTP');
expect(listener.port).toBe(80);
expect(listener.port).toBe(81);
});
it('should update port on protocol change to TERMINATED_HTTPS', function() {

View File

@ -53,9 +53,13 @@
</label>
<input name="port" id="port" type="number" class="form-control"
ng-model="model.spec.listener.port" ng-pattern="/^\d+$/" min="1" max="65535"
ng-required="true" ng-disabled="model.context.id">
<span class="help-block" ng-show="listenerDetailsForm.port.$invalid && listenerDetailsForm.port.$dirty">
{$ ::ctrl.portError $}
ng-required="true" ng-disabled="model.context.id"
validate-unique="model.listenerPorts">
<span class="help-block" ng-show="(listenerDetailsForm.port.$error.number || listenerDetailsForm.port.$error.required) && listenerDetailsForm.port.$dirty">
{$ ::ctrl.portNumberError $}
</span>
<span class="help-block" ng-show="!(listenerDetailsForm.port.$error.number || listenerDetailsForm.port.$error.required) && listenerDetailsForm.port.$error.unique && listenerDetailsForm.port.$dirty">
{$ ::ctrl.portUniqueError $}
</span>
</div>
</div>

View File

@ -90,6 +90,7 @@
monitorTypes: ['HTTP', 'PING', 'TCP'],
monitorMethods: ['GET', 'HEAD'],
certificates: [],
listenerPorts: [],
/**
* api methods for UI controllers
@ -116,7 +117,7 @@
* @param id ID of the resource being edited.
*/
function initialize(resource, id) {
function initialize(resource, id, loadBalancerId) {
var promise;
model.certificatesError = false;
@ -128,9 +129,10 @@
model.visibleResources = [];
model.certificates = [];
model.listenerPorts = [];
model.spec = {
loadbalancer_id: null,
loadbalancer_id: loadBalancerId,
loadbalancer: {
name: null,
description: null,
@ -165,19 +167,23 @@
certificates: []
};
if (model.initializing) {
return promise;
if (!model.initializing) {
model.initializing = true;
promise = initializeResources();
}
model.initializing = true;
return promise;
}
var type = (id ? 'edit' : 'create') + resource;
function initializeResources() {
var promise;
var type = (model.context.id ? 'edit' : 'create') + model.context.resource;
keymanagerPromise = serviceCatalog.ifTypeEnabled('key-manager');
if (type === 'createloadbalancer' || resource === 'listener') {
if (type === 'createloadbalancer' || model.context.resource === 'listener') {
keymanagerPromise.then(angular.noop, certificatesNotSupported);
}
switch ((id ? 'edit' : 'create') + resource) {
switch (type) {
case 'createloadbalancer':
promise = $q.all([
lbaasv2API.getLoadBalancers().then(onGetLoadBalancers),
@ -188,6 +194,16 @@
]).then(initMemberAddresses);
model.context.submit = createLoadBalancer;
break;
case 'createlistener':
promise = $q.all([
lbaasv2API.getListeners(model.spec.loadbalancer_id).then(onGetListeners),
neutronAPI.getSubnets().then(onGetSubnets),
neutronAPI.getPorts().then(onGetPorts),
novaAPI.getServers().then(onGetServers),
keymanagerPromise.then(prepareCertificates)
]).then(initMemberAddresses);
model.context.submit = createListener;
break;
case 'editloadbalancer':
promise = $q.all([
lbaasv2API.getLoadBalancer(model.context.id).then(onGetLoadBalancer),
@ -204,7 +220,7 @@
model.context.submit = editListener;
break;
default:
throw Error('Invalid resource context: ' + (id ? 'edit' : 'create') + resource);
throw Error('Invalid resource context: ' + type);
}
return promise.then(onInitSuccess, onInitFail);
@ -246,6 +262,10 @@
return lbaasv2API.createLoadBalancer(spec);
}
function createListener(spec) {
return lbaasv2API.createListener(spec);
}
function editLoadBalancer(spec) {
return lbaasv2API.editLoadBalancer(model.context.id, spec);
}
@ -375,6 +395,21 @@
model.spec.loadbalancer.name = name;
}
function onGetListeners(response) {
var existingNames = {};
angular.forEach(response.data.items, function nameExists(listener) {
existingNames[listener.name] = 1;
model.listenerPorts.push(listener.protocol_port);
});
var name;
var index = 0;
do {
index += 1;
name = interpolate(gettext('Listener %(index)s'), { index: index }, true);
} while (name in existingNames);
model.spec.listener.name = name;
}
function onGetSubnets(response) {
model.subnets.length = 0;
push.apply(model.subnets, response.data.items);
@ -426,7 +461,6 @@
function onGetLoadBalancer(response) {
var loadbalancer = response.data;
setLoadBalancerSpec(loadbalancer);
model.visibleResources.push('loadbalancer');
}
function onGetListener(response) {

View File

@ -98,6 +98,17 @@
return deferred.promise;
},
getListeners: function() {
var listeners = [
{ id: '1234', name: 'Listener 1', protocol_port: 80 },
{ id: '5678', name: 'Listener 2', protocol_port: 81 }
];
var deferred = $q.defer();
deferred.resolve({ data: { items: listeners } });
return deferred.promise;
},
getListener: function() {
var deferred = $q.defer();
deferred.resolve({ data: listenerResources });
@ -162,6 +173,9 @@
editLoadBalancer: function(id, spec) {
return spec;
},
createListener: function(spec) {
return spec;
},
editListener: function(id, spec) {
return spec;
}
@ -298,6 +312,10 @@
expect(model.certificates).toEqual([]);
});
it('has empty listener ports array', function() {
expect(model.listenerPorts).toEqual([]);
});
it('has array of listener protocols', function() {
expect(model.listenerProtocols).toEqual(['HTTP', 'TCP', 'TERMINATED_HTTPS']);
});
@ -340,8 +358,9 @@
expect(model.subnets.length).toBe(2);
expect(model.members.length).toBe(2);
expect(model.certificates.length).toBe(2);
expect(model.listenerPorts.length).toBe(0);
expect(model.spec).toBeDefined();
expect(model.spec.loadbalancer_id).toBeNull();
expect(model.spec.loadbalancer_id).toBeUndefined();
expect(model.spec.loadbalancer).toBeDefined();
expect(model.spec.listener).toBeDefined();
expect(model.spec.pool).toBeDefined();
@ -364,6 +383,43 @@
});
});
describe('Post initialize model (create listener)', function() {
beforeEach(function() {
model.initialize('listener', false, '1234');
scope.$apply();
});
it('should initialize model properties', function() {
expect(model.initializing).toBe(false);
expect(model.initialized).toBe(true);
expect(model.subnets.length).toBe(2);
expect(model.members.length).toBe(2);
expect(model.certificates.length).toBe(2);
expect(model.listenerPorts.length).toBe(2);
expect(model.spec).toBeDefined();
expect(model.spec.loadbalancer_id).toBe('1234');
expect(model.spec.loadbalancer).toBeDefined();
expect(model.spec.listener).toBeDefined();
expect(model.spec.pool).toBeDefined();
expect(model.spec.members).toEqual([]);
expect(model.spec.certificates).toEqual([]);
expect(model.spec.monitor).toBeDefined();
expect(model.certificatesError).toBe(false);
});
it('should initialize names', function() {
expect(model.spec.listener.name).toBe('Listener 3');
expect(model.spec.pool.name).toBe('Pool 1');
});
it('should initialize context properties', function() {
expect(model.context.resource).toBe('listener');
expect(model.context.id).toBeFalsy();
expect(model.context.submit).toBeDefined();
});
});
describe('Post initialize model (edit loadbalancer)', function() {
beforeEach(function() {
@ -375,14 +431,18 @@
expect(model.initializing).toBe(false);
expect(model.initialized).toBe(true);
expect(model.subnets.length).toBe(2);
expect(model.members).toEqual([]);
expect(model.members.length).toBe(0);
expect(model.certificates.length).toBe(0);
expect(model.listenerPorts.length).toBe(0);
expect(model.spec).toBeDefined();
expect(model.spec.loadbalancer_id).toBeNull();
expect(model.spec.loadbalancer_id).toBeUndefined();
expect(model.spec.loadbalancer).toBeDefined();
expect(model.spec.listener).toBeDefined();
expect(model.spec.pool).toBeDefined();
expect(model.spec.members).toEqual([]);
expect(model.spec.certificates).toEqual([]);
expect(model.spec.monitor).toBeDefined();
expect(model.certificatesError).toBe(false);
});
it('should initialize loadbalancer model spec properties', function() {
@ -592,7 +652,7 @@
});
it('sets load balancer ID to null', function() {
expect(model.spec.loadbalancer_id).toBeNull();
expect(model.spec.loadbalancer_id).toBeUndefined();
});
it('sets load balancer name to null', function() {
@ -754,6 +814,20 @@
});
});
describe('context (create listener)', function() {
beforeEach(function() {
model.initialize('listener', false, '1234');
scope.$apply();
});
it('should initialize context', function() {
expect(model.context.resource).toBe('listener');
expect(model.context.id).toBeFalsy();
expect(model.context.submit.name).toBe('createListener');
});
});
describe('context (edit listener)', function() {
beforeEach(function() {
@ -1014,6 +1088,214 @@
});
});
describe('Model submit function (create listener)', function() {
beforeEach(function() {
model.initialize('listener', false, '1234');
scope.$apply();
});
it('should set final spec properties', function() {
model.spec.listener.protocol = 'TCP';
model.spec.listener.port = 80;
model.spec.pool.name = 'pool name';
model.spec.pool.description = 'pool description';
model.spec.pool.method = 'LEAST_CONNECTIONS';
model.spec.members = [{
address: { ip: '1.2.3.4', subnet: '1' },
addresses: [{ ip: '1.2.3.4', subnet: '1' },
{ ip: '2.3.4.5', subnet: '2' }],
id: '1',
name: 'foo',
port: 80,
weight: 1
}, {
id: 'external-member-0',
address: '2.3.4.5',
subnet: null,
port: 80,
weight: 1
}, {
id: 'external-member-1',
address: null,
subnet: null,
port: 80,
weight: 1
}, {
id: 'external-member-2',
address: '3.4.5.6',
subnet: { id: '1' },
port: 80,
weight: 1
}];
model.spec.monitor.type = 'PING';
model.spec.monitor.interval = 1;
model.spec.monitor.retry = 1;
model.spec.monitor.timeout = 1;
model.spec.certificates = [{
id: 'container1',
name: 'foo',
expiration: '2015-03-26T21:10:45.417835'
}];
var finalSpec = model.submit();
expect(finalSpec.listener.name).toBe('Listener 3');
expect(finalSpec.listener.description).toBeUndefined();
expect(finalSpec.listener.protocol).toBe('TCP');
expect(finalSpec.listener.port).toBe(80);
expect(finalSpec.pool.name).toBe('pool name');
expect(finalSpec.pool.description).toBe('pool description');
expect(finalSpec.pool.protocol).toBe('TCP');
expect(finalSpec.pool.method).toBe('LEAST_CONNECTIONS');
expect(finalSpec.members.length).toBe(3);
expect(finalSpec.members[0].address).toBe('1.2.3.4');
expect(finalSpec.members[0].subnet).toBe('1');
expect(finalSpec.members[0].port).toBe(80);
expect(finalSpec.members[0].weight).toBe(1);
expect(finalSpec.members[0].id).toBe('1');
expect(finalSpec.members[0].addresses).toBeUndefined();
expect(finalSpec.members[0].name).toBeUndefined();
expect(finalSpec.members[0].allocatedMember).toBeUndefined();
expect(finalSpec.members[1].id).toBe('external-member-0');
expect(finalSpec.members[1].address).toBe('2.3.4.5');
expect(finalSpec.members[1].subnet).toBeUndefined();
expect(finalSpec.members[1].port).toBe(80);
expect(finalSpec.members[1].weight).toBe(1);
expect(finalSpec.members[1].allocatedMember).toBeUndefined();
expect(finalSpec.members[2].id).toBe('external-member-2');
expect(finalSpec.members[2].address).toBe('3.4.5.6');
expect(finalSpec.members[2].subnet).toBe('1');
expect(finalSpec.members[2].port).toBe(80);
expect(finalSpec.members[2].weight).toBe(1);
expect(finalSpec.members[2].allocatedMember).toBeUndefined();
expect(finalSpec.monitor.type).toBe('PING');
expect(finalSpec.monitor.interval).toBe(1);
expect(finalSpec.monitor.retry).toBe(1);
expect(finalSpec.monitor.timeout).toBe(1);
expect(finalSpec.certificates).toBeUndefined();
});
it('should set final spec certificates', function() {
model.spec.loadbalancer.ip = '1.2.3.4';
model.spec.loadbalancer.subnet = model.subnets[0];
model.spec.listener.protocol = 'TERMINATED_HTTPS';
model.spec.listener.port = 443;
model.spec.pool.method = 'LEAST_CONNECTIONS';
model.spec.certificates = [{
id: 'container1',
name: 'foo',
expiration: '2015-03-26T21:10:45.417835'
}];
var finalSpec = model.submit();
expect(finalSpec.listener.name).toBe('Listener 3');
expect(finalSpec.listener.description).toBeUndefined();
expect(finalSpec.listener.protocol).toBe('TERMINATED_HTTPS');
expect(finalSpec.listener.port).toBe(443);
expect(finalSpec.pool.protocol).toBe('HTTP');
expect(finalSpec.certificates).toEqual(['container1']);
});
it('should delete listener if any required property is not set', function() {
model.spec.loadbalancer.ip = '1.2.3.4';
model.spec.loadbalancer.subnet = model.subnets[0];
model.spec.listener.protocol = 'HTTP';
var finalSpec = model.submit();
expect(finalSpec.loadbalancer).toBeDefined();
expect(finalSpec.listener).toBeUndefined();
expect(finalSpec.pool).toBeUndefined();
});
it('should delete certificates if not using TERMINATED_HTTPS', function() {
model.spec.loadbalancer.ip = '1.2.3.4';
model.spec.loadbalancer.subnet = model.subnets[0];
model.spec.listener.protocol = 'HTTP';
model.spec.listener.port = 80;
model.spec.certificates = [{id: '1'}];
var finalSpec = model.submit();
expect(finalSpec.loadbalancer).toBeDefined();
expect(finalSpec.listener).toBeDefined();
expect(finalSpec.certificates).toBeUndefined();
});
it('should delete pool if any required property is not set', function() {
model.spec.loadbalancer.ip = '1.2.3.4';
model.spec.loadbalancer.subnet = model.subnets[0];
model.spec.listener.protocol = 'HTTP';
model.spec.listener.port = 80;
var finalSpec = model.submit();
expect(finalSpec.loadbalancer).toBeDefined();
expect(finalSpec.listener).toBeDefined();
expect(finalSpec.pool).toBeUndefined();
});
it('should delete members if none selected', function() {
model.spec.loadbalancer.ip = '1.2.3.4';
model.spec.loadbalancer.subnet = model.subnets[0];
model.spec.listener.protocol = 'HTTP';
model.spec.listener.port = 80;
model.spec.pool.method = 'LEAST_CONNECTIONS';
var finalSpec = model.submit();
expect(finalSpec.loadbalancer).toBeDefined();
expect(finalSpec.listener).toBeDefined();
expect(finalSpec.pool).toBeDefined();
expect(finalSpec.members).toBeUndefined();
});
it('should delete members if no members are valid', function() {
model.spec.loadbalancer.ip = '1.2.3.4';
model.spec.loadbalancer.subnet = model.subnets[0];
model.spec.listener.protocol = 'HTTP';
model.spec.listener.port = 80;
model.spec.pool.method = 'LEAST_CONNECTIONS';
model.spec.members = [{
id: 'foo',
address: '2.3.4.5',
weight: 1
}];
var finalSpec = model.submit();
expect(finalSpec.loadbalancer).toBeDefined();
expect(finalSpec.listener).toBeDefined();
expect(finalSpec.pool).toBeDefined();
expect(finalSpec.members).toBeUndefined();
});
it('should delete monitor if any required property not set', function() {
model.spec.loadbalancer.ip = '1.2.3.4';
model.spec.loadbalancer.subnet = model.subnets[0];
model.spec.listener.protocol = 'HTTP';
model.spec.listener.port = 80;
model.spec.pool.method = 'LEAST_CONNECTIONS';
model.spec.monitor.type = 'PING';
model.spec.monitor.interval = 1;
model.spec.monitor.retry = 1;
model.spec.monitor.timeout = null;
var finalSpec = model.submit();
expect(finalSpec.loadbalancer).toBeDefined();
expect(finalSpec.listener).toBeDefined();
expect(finalSpec.pool).toBeDefined();
expect(finalSpec.members).toBeUndefined();
expect(finalSpec.monitor).toBeUndefined();
});
});
describe('Model submit function (edit listener)', function() {
beforeEach(function() {