Add members tab to create load balancer workflow

This adds the members tab to the create load balancer workflow and
allows adding multiple members (nova servers) to the pool.

Partially-Implements: blueprint horizon-lbaas-v2-ui
Change-Id: I469dd9ff4d9aef9b91316699d1edec278957f091
This commit is contained in:
Justin Pomeroy 2015-12-08 13:35:43 -06:00
parent 32166ee5a8
commit 6c4828d8f6
22 changed files with 783 additions and 40 deletions

View File

@ -97,9 +97,45 @@ def create_pool(request, **kwargs):
poolSpec['name'] = data['pool']['name']
if data['pool'].get('description'):
poolSpec['description'] = data['pool']['description']
return neutronclient(request).create_lbaas_pool(
pool = neutronclient(request).create_lbaas_pool(
{'pool': poolSpec}).get('pool')
if data.get('members'):
args = (request, kwargs['loadbalancer_id'], add_member)
kwargs = {'callback_kwargs': {'pool_id': pool['id'],
'index': 0}}
thread.start_new_thread(poll_loadbalancer_status, args, kwargs)
return pool
def add_member(request, **kwargs):
"""Add a member to a pool.
"""
data = request.DATA
members = data['members']
index = kwargs['index']
member = members[index]
memberSpec = {
'address': member['address'],
'protocol_port': member['port'],
'subnet_id': member['subnet']
}
if member.get('weight'):
memberSpec['weight'] = member['weight']
member = neutronclient(request).create_lbaas_member(
kwargs['pool_id'], {'member': memberSpec}).get('member')
index += 1
if len(members) > index:
args = (request, kwargs['loadbalancer_id'], add_member)
kwargs = {'callback_kwargs': {'pool_id': kwargs['pool_id'],
'index': index}}
thread.start_new_thread(poll_loadbalancer_status, args, kwargs)
return member
@urls.register
class LoadBalancers(generic.View):

View File

@ -47,7 +47,11 @@ ADD_JS_FILES = [
('dashboard/project/lbaasv2/loadbalancers/actions/create/details/'
'details.controller.js'),
('dashboard/project/lbaasv2/loadbalancers/actions/create/listener/'
'listener.controller.js')
'listener.controller.js'),
('dashboard/project/lbaasv2/loadbalancers/actions/create/pool/'
'pool.controller.js'),
('dashboard/project/lbaasv2/loadbalancers/actions/create/members/'
'members.controller.js')
]
ADD_JS_SPEC_FILES = [
@ -70,7 +74,11 @@ ADD_JS_SPEC_FILES = [
('dashboard/project/lbaasv2/loadbalancers/actions/create/details/'
'details.controller.spec.js'),
('dashboard/project/lbaasv2/loadbalancers/actions/create/listener/'
'listener.controller.spec.js')
'listener.controller.spec.js'),
('dashboard/project/lbaasv2/loadbalancers/actions/create/pool/'
'pool.controller.spec.js'),
('dashboard/project/lbaasv2/loadbalancers/actions/create/members/'
'members.controller.spec.js')
]
ADD_SCSS_FILES = [

View File

@ -50,7 +50,7 @@ module.exports = function (config) {
// Sets up module to process templates.
ngHtml2JsPreprocessor: {
prependPrefix: '/static/',
prependPrefix: '/',
moduleName: 'templates'
},

View File

@ -33,8 +33,11 @@
.constant('horizon.dashboard.project.lbaasv2.patterns', {
ipv4: '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$',
ipv6: '^((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?$'
});
})
/* eslint-enable max-len */
.constant('horizon.dashboard.project.lbaasv2.popovers', {
ipAddresses: '<ul><li ng-repeat="addr in member.addresses">{$ addr.ip $}</li></ul>'
});
config.$inject = [
'$provide',

View File

@ -45,3 +45,25 @@
margin-top: 12px;
}
}
transfer-table {
.member-weight,
.member-port {
display: inline-block;
width: 6em;
}
.member-address {
width: 20em;
}
span.invalid {
vertical-align: top;
margin: 8px 0px 0px 5px;
}
}
.addresses-popover + .popover {
ul {
list-style-type: disc;
padding-left: 10px;
}
}

View File

@ -0,0 +1,96 @@
/*
* 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.loadbalancers')
.controller('AddMembersController', AddMembersController);
AddMembersController.$inject = [
'$scope',
'$compile',
'horizon.dashboard.project.lbaasv2.popovers',
'horizon.framework.util.i18n.gettext'
];
/**
* @ngdoc controller
* @name AddMembersController
* @description
* The `AddMembersController` controller provides functions for adding members to a pool.
* @param $scope The angular scope object.
* @param $compile The angular compile service.
* @param popovers LBaaS v2 popover templates constant.
* @param gettext The horizon gettext function for translation.
* @returns undefined
*/
function AddMembersController($scope, $compile, popoverTemplates, gettext) {
var ctrl = this;
// Error text for invalid fields
ctrl.portError = gettext('The port must be a number between 1 and 65535.');
ctrl.weightError = gettext('The weight must be a number between 1 and 256.');
// Table widget properties
ctrl.tableData = {
available: $scope.model.members,
allocated: $scope.model.spec.members,
displayedAvailable: [],
displayedAllocated: []
};
ctrl.tableLimits = {
maxAllocation: -1
};
ctrl.tableHelp = {
availHelpText: '',
noneAllocText: gettext('Select members from the available members below'),
noneAvailText: gettext('No available members')
};
// Functions to control the IP address popover
ctrl.showAddressPopover = showAddressPopover;
ctrl.hideAddressPopover = hideAddressPopover;
ctrl.addressPopoverTarget = addressPopoverTarget;
//////////
function showAddressPopover(event, member) {
var element = angular.element(event.target);
var scope = $scope.$new(true);
scope.member = member;
element.popover({
content: $compile(popoverTemplates.ipAddresses)(scope),
html: true,
placement: 'top',
title: interpolate(gettext('IP Addresses (%(count)s)'),
{ count: member.addresses.length }, true)
});
element.popover('show');
}
function hideAddressPopover(event) {
var element = angular.element(event.target);
element.popover('hide');
}
function addressPopoverTarget(member) {
return interpolate(gettext('%(ip)s...'), { ip: member.address.ip }, true);
}
}
})();

View File

@ -0,0 +1,141 @@
/*
* 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('Create Load Balancer Add Members Step', function() {
var members = [{
id: '1',
name: 'foo',
description: 'bar',
weight: 1,
port: 80,
address: { ip: '1.2.3.4', subnet: '1' },
addresses: [{ ip: '1.2.3.4', subnet: '1' },
{ ip: '2.3.4.5', subnet: '2' }]
}];
beforeEach(module('horizon.framework.util.i18n'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
describe('AddMembersController', function() {
var ctrl, scope;
beforeEach(inject(function($controller) {
scope = {
model: {
spec: {
members: []
},
members: members
}
};
ctrl = $controller('AddMembersController', { $scope: scope });
}));
it('should define error messages for invalid fields', function() {
expect(ctrl.portError).toBeDefined();
expect(ctrl.weightError).toBeDefined();
});
it('should define transfer table properties', function() {
expect(ctrl.tableData).toBeDefined();
expect(ctrl.tableLimits).toBeDefined();
expect(ctrl.tableHelp).toBeDefined();
});
it('should have available members', function() {
expect(ctrl.tableData.available).toBeDefined();
expect(ctrl.tableData.available.length).toBe(1);
expect(ctrl.tableData.available[0].id).toBe('1');
});
it('should not have allocated members', function() {
expect(ctrl.tableData.allocated).toEqual([]);
});
it('should allow adding multiple members', function() {
expect(ctrl.tableLimits.maxAllocation).toBe(-1);
});
it('should properly format address popover target', function() {
var target = ctrl.addressPopoverTarget(members[0]);
expect(target).toBe('1.2.3.4...');
});
});
describe('Add Members Template', function() {
var $scope, $element, popoverContent;
beforeEach(module('templates'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(inject(function($injector) {
var $compile = $injector.get('$compile');
var $templateCache = $injector.get('$templateCache');
var basePath = $injector.get('horizon.dashboard.project.lbaasv2.basePath');
var popoverTemplates = $injector.get('horizon.dashboard.project.lbaasv2.popovers');
var markup = $templateCache.get(
basePath + 'loadbalancers/actions/create/members/members.html');
$scope = $injector.get('$rootScope').$new();
$scope.model = {
spec: {
members: []
},
members: members
};
$element = $compile(markup)($scope);
var popoverScope = $injector.get('$rootScope').$new();
popoverScope.member = members[0];
popoverContent = $compile(popoverTemplates.ipAddresses)(popoverScope);
}));
it('should show IP addresses popover on hover', function() {
var ctrl = $element.scope().ctrl;
ctrl.tableData.displayedAvailable = members;
$scope.$apply();
var popoverElement = $element.find('span.addresses-popover');
expect(popoverElement.length).toBe(1);
$.fn.popover = angular.noop;
spyOn($.fn, 'popover');
spyOn(ctrl, 'showAddressPopover').and.callThrough();
popoverElement.trigger('mouseover');
expect(ctrl.showAddressPopover).toHaveBeenCalledWith(
jasmine.objectContaining({type: 'mouseover'}), members[0]);
expect($.fn.popover.calls.count()).toBe(2);
expect($.fn.popover.calls.argsFor(0)[0]).toEqual({
content: popoverContent,
html: true,
placement: 'top',
title: 'IP Addresses (2)'
});
expect($.fn.popover.calls.argsFor(1)[0]).toBe('show');
spyOn(ctrl, 'hideAddressPopover').and.callThrough();
popoverElement.trigger('mouseleave');
expect(ctrl.hideAddressPopover)
.toHaveBeenCalledWith(jasmine.objectContaining({type: 'mouseleave'}));
expect($.fn.popover.calls.count()).toBe(3);
expect($.fn.popover.calls.argsFor(2)[0]).toBe('hide');
});
});
});
})();

View File

@ -0,0 +1,3 @@
<h1 translate>Pool Members Help</h1>
<p translate>Add members to the load balancer pool. The Available Members table contains existing instances that can be added to the pool.</p>

View File

@ -0,0 +1,191 @@
<div ng-controller="AddMembersController as ctrl">
<h1 translate>Pool Members</h1>
<!--content-->
<div class="content">
<div translate class="subtitle">Add members to the load balancer pool. All required listener and pool details must also be provided.</div>
<transfer-table tr-model="ctrl.tableData"
limits="::ctrl.tableLimits"
help-text="::ctrl.tableHelp">
<!-- Allocated-->
<allocated>
<table st-table="ctrl.tableData.displayedAllocated"
st-safe-src="ctrl.tableData.allocated" hz-table
class="table-striped table-rsp table-detail modern form-group">
<thead>
<tr>
<th class="expander"></th>
<th class="rsp-p2" translate>Name</th>
<th class="rsp-p3" translate>Description</th>
<th class="rsp-p1"
ng-class="{ 'required': ctrl.tableData.displayedAllocated.length > 0 }">
<label translate>IP Address</label>
</th>
<th class="rsp-p1" translate>Weight</th>
<th class="rsp-p1"
ng-class="{ 'required': ctrl.tableData.displayedAllocated.length > 0 }">
<label translate>Port</label>
</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-if="ctrl.tableData.allocated.length === 0">
<td colspan="0">
<div class="no-rows-help">
{$ ::trCtrl.helpText.noneAllocText $}
</div>
</td>
</tr>
<tr ng-repeat-start="row in ctrl.tableData.displayedAllocated track by row.id">
<td class="expander">
<i class="fa fa-chevron-right"
hz-expand-detail
duration="200">
</i>
</td>
<td class="rsp-p2">{$ ::row.name $}</td>
<td class="rsp-p3">{$ ::row.description | noValue $}</td>
<td class="rsp-p1">
<span ng-if="row.addresses.length === 1">{$ row.address.ip $}</span>
<div ng-if="row.addresses.length > 1"
class="form-field member-address">
<select class="form-control input-sm"
ng-options="addr.ip for addr in row.addresses"
ng-model="row.address" ng-required="true">
</select>
</div>
</td>
<td class="rsp-p1">
<div class="form-field member-weight"
ng-class="{ 'has-error': createLoadBalancerMembersForm['{$ ::row.id $}-weight'].$invalid && createLoadBalancerMembersForm['{$ ::row.id $}-weight'].$dirty }">
<input name="{$ ::row.id $}-weight" type="number" class="form-control input-sm"
ng-model="row.weight" ng-pattern="/^\d+$/" min="1" max="256">
</div>
<span class="fa fa-exclamation-triangle invalid"
ng-show="createLoadBalancerMembersForm['{$ ::row.id $}-weight'].$invalid && createLoadBalancerMembersForm.$dirty"
popover="{$ ::ctrl.weightError $}"
popover-placement="top" popover-append-to-body="true"
popover-trigger="hover"></span>
</td>
<td class="rsp-p1">
<div class="form-field member-port"
ng-class="{ 'has-error': createLoadBalancerMembersForm['{$ ::row.id $}-port'].$invalid && createLoadBalancerMembersForm['{$ ::row.id $}-port'].$dirty }">
<input name="{$ ::row.id $}-port" type="number" class="form-control input-sm"
ng-model="row.port" ng-pattern="/^\d+$/" min="1" max="65535"
ng-required="true">
</div>
<span class="fa fa-exclamation-triangle invalid"
ng-show="createLoadBalancerMembersForm['{$ ::row.id $}-port'].$invalid && createLoadBalancerMembersForm.$dirty"
popover="{$ ::ctrl.portError $}"
popover-placement="top" popover-append-to-body="true"
popover-trigger="hover"></span>
</td>
<td class="action-col">
<action-list>
<action action-classes="'btn btn-sm btn-default'"
callback="trCtrl.deallocate" item="row">
<span class="fa fa-minus"></span>
</action>
</action-list>
</td>
</tr>
<tr ng-repeat-end class="detail-row">
<td class="detail" colspan="0">
<div class="row">
<dl class="rsp-alt-p2 col-sm-2">
<dt translate>Name</dt>
<dd>{$ ::row.name $}</dd>
</dl>
<dl class="rsp-alt-p3 col-sm-2">
<dt translate>Description</dt>
<dd>{$ ::row.description | noValue $}</dd>
</dl>
</div>
</td>
</tr>
</tbody>
</table>
</allocated>
<!-- Available -->
<available>
<table st-table="ctrl.tableData.displayedAvailable"
st-safe-src="ctrl.tableData.available"
hz-table class="table-striped table-rsp table-detail modern">
<thead>
<tr>
<th class="search-header" colspan="0">
<hz-search-bar group-classes="input-group-sm" icon-classes="fa-search">
</hz-search-bar>
</th>
</tr>
<tr>
<th class="expander"></th>
<th st-sort="name" st-sort-default class="rsp-p2" translate>Name</th>
<th class="rsp-p3" translate>Description</th>
<th class="rsp-p1" translate>IP Address</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-if="trCtrl.numAvailable() === 0">
<td colspan="0">
<div class="no-rows-help">
{$ ::trCtrl.helpText.noneAvailText $}
</div>
</td>
</tr>
<tr ng-repeat-start="row in ctrl.tableData.displayedAvailable track by row.id"
ng-if="!trCtrl.allocatedIds[row.id]">
<td class="expander">
<i class="fa fa-chevron-right"
hz-expand-detail
duration="200">
</i>
</td>
<td class="rsp-p2">{$ ::row.name $}</td>
<td class="rsp-p3">{$ ::row.description | noValue $}</td>
<td class="rsp-p1">
<span ng-if="row.addresses.length === 1">{$ row.address.ip $}</span>
<!-- The current version of the popover directive doesn't seem to allow HTML content (0.11.0) -->
<span ng-if="row.addresses.length > 1"
class="addresses-popover"
ng-mouseover="ctrl.showAddressPopover($event, row)"
ng-mouseleave="ctrl.hideAddressPopover($event)">
{$ ctrl.addressPopoverTarget(row) $}
</span>
</td>
<td class="action-col">
<action-list>
<action action-classes="'btn btn-sm btn-default'"
callback="trCtrl.allocate" item="row">
<span class="fa fa-plus"></span>
</action>
</action-list>
</td>
</tr>
<tr ng-repeat-end class="detail-row">
<td class="detail" colspan="0">
<div class="row">
<dl class="rsp-alt-p2 col-sm-2">
<dt translate>Name</dt>
<dd>{$ ::row.name $}</dd>
</dl>
<dl class="rsp-alt-p3 col-sm-2">
<dt translate>Description</dt>
<dd>{$ ::row.description | noValue $}</dd>
</dl>
</div>
</td>
</tr>
</tbody>
</table>
</available>
</transfer-table> <!-- End Transfer Table -->
</div> <!-- end content -->
</div>

View File

@ -24,7 +24,6 @@
modalService.$inject = [
'$modal',
'$location',
'$window',
'horizon.framework.widgets.toast.service',
'horizon.framework.util.i18n.gettext',
'horizon.app.core.openstack-service-api.policy'
@ -39,14 +38,13 @@
*
* @param $modal The angular bootstrap $modal service.
* @param $location The angular $location service.
* @param $window The angular reference to the browser window object.
* @param toastService The horizon toast service.
* @param gettext The horizon gettext function for translation.
* @param policy The horizon policy service.
* @returns The modal service for the create load balancer workflow.
*/
function modalService($modal, $location, $window, toastService, gettext, policy) {
function modalService($modal, $location, toastService, gettext, policy) {
var service = {
allowed: allowed,
@ -77,7 +75,7 @@
var modal = $modal.open(spec);
modal.result.then(function(response) {
toastService.add('success', gettext('A new load balancer is being created.'));
$location.path($window.WEBROOT + 'project/ngloadbalancersv2/detail/' + response.data.id);
$location.path('project/ngloadbalancersv2/detail/' + response.data.id);
});
}

View File

@ -98,7 +98,7 @@
modalService.perform();
expect(toastService.add).toHaveBeenCalledWith('success', jasmine.any(String));
expect($location.path).toHaveBeenCalledWith('/project/ngloadbalancersv2/detail/1');
expect($location.path).toHaveBeenCalledWith('project/ngloadbalancersv2/detail/1');
});
});

View File

@ -16,6 +16,8 @@
(function () {
'use strict';
var push = Array.prototype.push;
angular
.module('horizon.dashboard.project.lbaasv2.loadbalancers')
.factory('horizon.dashboard.project.lbaasv2.loadbalancers.actions.create.model',
@ -24,6 +26,7 @@
createLoadBalancerModel.$inject = [
'$q',
'horizon.app.core.openstack-service-api.neutron',
'horizon.app.core.openstack-service-api.nova',
'horizon.app.core.openstack-service-api.lbaasv2',
'horizon.framework.util.i18n.gettext'
];
@ -40,13 +43,14 @@
*
* @param $q The angular service for promises.
* @param neutronAPI The neutron service API.
* @param novaAPI The nova service API.
* @param lbaasv2API The LBaaS V2 service API.
* @param gettext The horizon gettext function for translation.
* @returns The model service for the create load balancer workflow.
*/
function createLoadBalancerModel($q, neutronAPI, lbaasv2API, gettext) {
var initPromise;
function createLoadBalancerModel($q, neutronAPI, novaAPI, lbaasv2API, gettext) {
var initPromise, ports;
/**
* @ngdoc model api object
@ -71,6 +75,7 @@
spec: null,
subnets: [],
members: [],
listenerProtocols: ['TCP', 'HTTP', 'HTTPS'],
poolProtocols: ['TCP', 'HTTP', 'HTTPS'],
methods: ['ROUND_ROBIN', 'LEAST_CONNECTIONS', 'SOURCE_IP'],
@ -113,7 +118,8 @@
description: null,
protocol: null,
method: null
}
},
members: []
};
if (model.initializing) {
@ -123,7 +129,9 @@
promise = $q.all([
lbaasv2API.getLoadBalancers().then(onGetLoadBalancers),
neutronAPI.getSubnets().then(onGetSubnets)
neutronAPI.getSubnets().then(onGetSubnets),
neutronAPI.getPorts().then(onGetPorts),
novaAPI.getServers().then(onGetServers)
]);
promise.then(onInitSuccess, onInitFail);
@ -133,6 +141,7 @@
}
function onInitSuccess() {
initMemberAddresses();
model.initializing = false;
model.initialized = true;
}
@ -166,9 +175,24 @@
delete finalSpec.pool;
}
// Members require a pool, address, subnet, and port but the wizard requires the address,
// subnet, and port so we can assume those exist here.
if (!finalSpec.pool || finalSpec.members.length === 0) {
delete finalSpec.members;
}
angular.forEach(finalSpec.members, function cleanMember(member) {
delete member.id;
delete member.addresses;
delete member.name;
delete member.description;
member.subnet = member.address.subnet;
member.address = member.address.ip;
});
// Delete null properties
angular.forEach(finalSpec, function(group, groupName) {
angular.forEach(group, function(value, key) {
angular.forEach(finalSpec, function deleteNullsForGroup(group, groupName) {
angular.forEach(group, function deleteNullValue(value, key) {
if (value === null) {
delete finalSpec[groupName][key];
}
@ -195,9 +219,43 @@
}
function onGetSubnets(response) {
model.subnets = [];
angular.forEach(response.data.items, function(subnet) {
model.subnets.push(subnet);
model.subnets.length = 0;
push.apply(model.subnets, response.data.items);
}
function onGetServers(response) {
model.members.length = 0;
var members = [];
angular.forEach(response.data.items, function(server) {
members.push({
id: server.id,
name: server.name,
description: server.description,
weight: 1
});
});
push.apply(model.members, members);
}
function onGetPorts(response) {
ports = response.data.items;
}
function initMemberAddresses() {
angular.forEach(model.members, function(member) {
var memberPorts = ports.filter(function(port) {
return port.device_id === member.id;
});
member.addresses = [];
angular.forEach(memberPorts, function(port) {
angular.forEach(port.fixed_ips, function(ip) {
member.addresses.push({
ip: ip.ip_address,
subnet: ip.subnet_id
});
});
});
member.address = member.addresses[0];
});
}

View File

@ -19,6 +19,7 @@
describe('LBaaS v2 Create Load Balancer Workflow Model Service', function() {
var model, $q, scope;
beforeEach(module('horizon.framework.util.i18n'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(module(function($provide) {
@ -44,6 +45,31 @@
var deferred = $q.defer();
deferred.resolve({ data: { items: subnets } });
return deferred.promise;
},
getPorts: function() {
var ports = [ { device_id: '1',
fixed_ips: [{ ip_address: '1.2.3.4', subnet_id: '1' },
{ ip_address: '2.3.4.5', subnet_id: '2' }] },
{ device_id: '2',
fixed_ips: [{ ip_address: '3.4.5.6', subnet_id: '1' },
{ ip_address: '4.5.6.7', subnet_id: '2' }] } ];
var deferred = $q.defer();
deferred.resolve({ data: { items: ports } });
return deferred.promise;
}
});
$provide.value('horizon.app.core.openstack-service-api.nova', {
getServers: function() {
var servers = [ { id: '1', name: 'server-1' },
{ id: '2', name: 'server-2' } ];
var deferred = $q.defer();
deferred.resolve({ data: { items: servers } });
return deferred.promise;
}
});
@ -76,6 +102,10 @@
expect(model.subnets).toEqual([]);
});
it('has empty members array', function() {
expect(model.members).toEqual([]);
});
it('has array of pool protocols', function() {
expect(model.poolProtocols).toEqual(['TCP', 'HTTP', 'HTTPS']);
});
@ -108,10 +138,12 @@
expect(model.initializing).toBe(false);
expect(model.initialized).toBe(true);
expect(model.subnets.length).toBe(2);
expect(model.members.length).toBe(2);
expect(model.spec).toBeDefined();
expect(model.spec.loadbalancer).toBeDefined();
expect(model.spec.listener).toBeDefined();
expect(model.spec.pool).toBeDefined();
expect(model.spec.members).toEqual([]);
});
it('should initialize names', function() {
@ -156,7 +188,7 @@
// This is here to ensure that as people add/change spec properties, they don't forget
// to implement tests for them.
it('has the right number of properties', function() {
expect(Object.keys(model.spec).length).toBe(3);
expect(Object.keys(model.spec).length).toBe(4);
expect(Object.keys(model.spec.loadbalancer).length).toBe(4);
expect(Object.keys(model.spec.listener).length).toBe(4);
expect(Object.keys(model.spec.pool).length).toBe(4);
@ -227,6 +259,16 @@
model.spec.pool.description = 'pool description';
model.spec.pool.protocol = 'HTTP';
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',
description: 'bar',
port: 80,
weight: 1
}];
var finalSpec = model.createLoadBalancer();
@ -242,6 +284,15 @@
expect(finalSpec.pool.description).toBe('pool description');
expect(finalSpec.pool.protocol).toBe('HTTP');
expect(finalSpec.pool.method).toBe('LEAST_CONNECTIONS');
expect(finalSpec.members.length).toBe(1);
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].addresses).toBeUndefined();
expect(finalSpec.members[0].id).toBeUndefined();
expect(finalSpec.members[0].name).toBeUndefined();
expect(finalSpec.members[0].description).toBeUndefined();
});
it('should delete listener if any required property is not set', function() {
@ -268,6 +319,22 @@
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 = 'HTTPS';
model.spec.listener.port = 80;
model.spec.pool.protocol = 'HTTP';
model.spec.pool.method = 'LEAST_CONNECTIONS';
var finalSpec = model.createLoadBalancer();
expect(finalSpec.loadbalancer).toBeDefined();
expect(finalSpec.listener).toBeDefined();
expect(finalSpec.pool).toBeDefined();
expect(finalSpec.members).toBeUndefined();
});
});
});

View File

@ -0,0 +1,57 @@
/*
* 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.loadbalancers')
.controller('CreatePoolDetailsController', CreatePoolDetailsController);
CreatePoolDetailsController.$inject = [
'$scope'
];
/**
* @ngdoc controller
* @name CreatePoolDetailsController
* @description
* The `CreatePoolDetailsController` controller provides functions for configuring
* pool details.
* @param $scope The angular scope object.
* @returns undefined
*/
function CreatePoolDetailsController($scope) {
var ctrl = this;
ctrl.protocolChange = protocolChange;
//////////
function protocolChange(protocol) {
var port = '';
if (protocol === 'HTTP') {
port = 80;
} else if (protocol === 'HTTPS') {
port = 443;
}
$scope.model.members.forEach(function setPort(member) {
member.port = port;
});
}
}
})();

View File

@ -0,0 +1,62 @@
/*
* 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('Create Pool Details Step', function() {
var ctrl;
var members = [{port: ''}, {port: ''}];
beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(inject(function($controller) {
var scope = {
model: {
members: members
}
};
ctrl = $controller('CreatePoolDetailsController', { $scope: scope });
}));
it('should define protocolChange function', function() {
expect(ctrl.protocolChange).toBeDefined();
});
it('should update member ports on protocol change to HTTP', function() {
ctrl.protocolChange('HTTP');
members.forEach(function(member) {
expect(member.port).toBe(80);
});
});
it('should update member ports on protocol change to HTTPS', function() {
ctrl.protocolChange('HTTPS');
members.forEach(function(member) {
expect(member.port).toBe(443);
});
});
it('should update member ports on protocol change to TCP', function() {
ctrl.protocolChange('TCP');
members.forEach(function(member) {
expect(member.port).toBe('');
});
});
});
})();

View File

@ -1,4 +1,4 @@
<div>
<div ng-controller="CreatePoolDetailsController as ctrl">
<h1 translate>Pool Details</h1>
<!--content-->
@ -35,7 +35,8 @@
<select class="form-control input-sm" name="pool-protocol"
id="pool-protocol"
ng-options="protocol for protocol in model.poolProtocols"
ng-model="model.spec.pool.protocol">
ng-model="model.spec.pool.protocol"
ng-change="ctrl.protocolChange(model.spec.pool.protocol)">
</select>
</div>
</div>

View File

@ -52,6 +52,13 @@
templateUrl: basePath + 'loadbalancers/actions/create/pool/pool.html',
helpUrl: basePath + 'loadbalancers/actions/create/pool/pool.help.html',
formName: 'createLoadBalancerPoolForm'
},
{
id: 'members',
title: gettext('Pool Members'),
templateUrl: basePath + 'loadbalancers/actions/create/members/members.html',
helpUrl: basePath + 'loadbalancers/actions/create/members/members.help.html',
formName: 'createLoadBalancerMembersForm'
}
],

View File

@ -41,12 +41,13 @@
it('should have steps defined', function () {
expect(createLoadBalancerWorkflow.steps).toBeDefined();
expect(createLoadBalancerWorkflow.steps.length).toBe(3);
expect(createLoadBalancerWorkflow.steps.length).toBe(4);
var forms = [
'createLoadBalancerDetailsForm',
'createLoadBalancerListenerForm',
'createLoadBalancerPoolForm'
'createLoadBalancerPoolForm',
'createLoadBalancerMembersForm'
];
forms.forEach(function(expectedForm, idx) {

View File

@ -22,8 +22,7 @@
LoadBalancerDetailController.$inject = [
'horizon.app.core.openstack-service-api.lbaasv2',
'$routeParams',
'$window'
'$routeParams'
];
/**
@ -35,15 +34,13 @@
*
* @param api The LBaaS v2 API service.
* @param $routeParams The angular $routeParams service.
* @param $window The angular reference to the browser window object.
* @returns undefined
*/
function LoadBalancerDetailController(api, $routeParams, $window) {
function LoadBalancerDetailController(api, $routeParams) {
var ctrl = this;
ctrl.loadbalancer = {};
ctrl.webroot = $window.webroot;
var loadbalancerId = $routeParams.loadbalancerId;

View File

@ -39,13 +39,13 @@
<div>
<dt translate>Subnet ID</dt>
<dd>
<a target="_self" ng-href="{$ ::ctrl.webroot $}project/networks/subnets/{$ ::ctrl.loadbalancer.vip_subnet_id $}/detail">{$ ::ctrl.loadbalancer.vip_subnet_id $}</a>
<a target="_self" ng-href="project/networks/subnets/{$ ::ctrl.loadbalancer.vip_subnet_id $}/detail">{$ ::ctrl.loadbalancer.vip_subnet_id $}</a>
</dd>
</div>
<div>
<dt translate>Port ID</dt>
<dd>
<a target="_self" ng-href="{$ ::ctrl.webroot $}project/networks/ports/{$ ::ctrl.loadbalancer.vip_port_id $}/detail">{$ ::ctrl.loadbalancer.vip_port_id $}</a>
<a target="_self" ng-href="project/networks/ports/{$ ::ctrl.loadbalancer.vip_port_id $}/detail">{$ ::ctrl.loadbalancer.vip_port_id $}</a>
</dd>
</div>
</dl>

View File

@ -21,8 +21,6 @@
.controller('LoadBalancersTableController', LoadBalancersTableController);
LoadBalancersTableController.$inject = [
'$scope',
'$window',
'horizon.app.core.openstack-service-api.lbaasv2',
'horizon.dashboard.project.lbaasv2.loadbalancers.actions.batchActions'
];
@ -34,20 +32,17 @@
* @description
* Controller for the LBaaS v2 load balancers table. Serves as the focal point for table actions.
*
* @param $scope The angular $scope object.
* @param $window The angular reference to the browser window object.
* @param api The LBaaS V2 service API.
* @param batchActions The load balancer batch actions service.
* @returns undefined
*/
function LoadBalancersTableController($scope, $window, api, batchActions) {
function LoadBalancersTableController(api, batchActions) {
var ctrl = this;
ctrl.items = [];
ctrl.src = [];
ctrl.checked = {};
ctrl.webroot = $window.webroot;
ctrl.batchActions = batchActions;
init();

View File

@ -18,7 +18,7 @@
Table-batch-actions:
This is where batch actions like searching, creating, and deleting.
-->
<th colspan="100" class="search-header">
<th colspan="0" class="search-header">
<hz-search-bar group-classes="input-group-sm" icon-classes="fa-search">
<actions allowed="table.batchActions.actions" type="batch"></actions>
</hz-search-bar>
@ -71,7 +71,7 @@
duration="200">
</span>
</td>
<td class="rsp-p1"><a ng-href="{$ ::ctrl.webroot $}project/ngloadbalancersv2/detail/{$ ::item.id $}">{$ item.name $}</a></td>
<td class="rsp-p1"><a ng-href="project/ngloadbalancersv2/detail/{$ ::item.id $}">{$ item.name $}</a></td>
<td class="rsp-p1">{$ item.description | noValue $}</td>
<td class="rsp-p1">{$ item.operating_status | operatingStatus $}</td>
<td class="rsp-p1">{$ item.provisioning_status | provisioningStatus $}</td>
@ -86,7 +86,7 @@
Can be toggled using the chevron button.
Ensure colspan is greater or equal to number of column-headers.
-->
<td class="detail" colspan="100">
<td class="detail" colspan="0">
<!--
The responsive columns that disappear typically should reappear here
with the same responsive priority that they disappear.