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

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
parent 73f1e373
......@@ -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 = []
......
......@@ -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
......
......@@ -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',
......
/*
* 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);
}
}
})();
/*
* 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');
});
});
})();
/*
* 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);
}
})();
/*
* 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');
});
});
})();
......@@ -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();
......
......@@ -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() {
......
......@@ -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>
......
/*
* 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;
}
}
}
})();
/*
* 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);
});
});
})();
......@@ -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.');