Add listener and pool to create load balancer workflow

This adds Listener Details and Pool Details tabs to the workflow for
creating a new load balancer

Partially-Implements: blueprint horizon-lbaas-v2-ui
Change-Id: Id6462ddae34e53ef46d1034068ab02ebc2094bd3
This commit is contained in:
Justin Pomeroy 2015-12-03 09:07:19 -06:00
parent 3b72651253
commit 32166ee5a8
17 changed files with 450 additions and 16 deletions

View File

@ -14,8 +14,13 @@
"""API over the neutron LBaaS v2 service.
"""
from six.moves import _thread as thread
from time import sleep
from django.views import generic
from horizon import conf
from openstack_dashboard.api import neutron
from openstack_dashboard.api.rest import urls
from openstack_dashboard.api.rest import utils as rest_utils
@ -23,6 +28,79 @@ from openstack_dashboard.api.rest import utils as rest_utils
neutronclient = neutron.neutronclient
def poll_loadbalancer_status(request, loadbalancer_id, callback,
from_state='PENDING_UPDATE', to_state='ACTIVE',
callback_kwargs=None):
"""Poll for the status of the load balancer.
Polls for the status of the load balancer and calls a function when the
status changes to a specified state.
:param request: django request object
:param loadbalancer_id: id of the load balancer to poll
:param callback: function to call when polling is complete
:param from_state: initial expected state of the load balancer
:param to_state: state to check for
:param callback_kwargs: kwargs to pass into the callback function
"""
interval = conf.HORIZON_CONFIG['ajax_poll_interval'] / 1000.0
status = from_state
while status == from_state:
sleep(interval)
lb = neutronclient(request).show_loadbalancer(
loadbalancer_id).get('loadbalancer')
status = lb['provisioning_status']
if status == to_state:
kwargs = {'loadbalancer_id': loadbalancer_id}
if callback_kwargs:
kwargs.update(callback_kwargs)
callback(request, **kwargs)
def create_listener(request, **kwargs):
"""Create a new listener.
"""
data = request.DATA
listenerSpec = {
'protocol': data['listener']['protocol'],
'protocol_port': data['listener']['port'],
'loadbalancer_id': kwargs['loadbalancer_id']
}
if data['listener'].get('name'):
listenerSpec['name'] = data['listener']['name']
if data['listener'].get('description'):
listenerSpec['description'] = data['listener']['description']
listener = neutronclient(request).create_listener(
{'listener': listenerSpec}).get('listener')
if data.get('pool'):
args = (request, kwargs['loadbalancer_id'], create_pool)
kwargs = {'callback_kwargs': {'listener_id': listener['id']}}
thread.start_new_thread(poll_loadbalancer_status, args, kwargs)
return listener
def create_pool(request, **kwargs):
"""Create a new pool.
"""
data = request.DATA
poolSpec = {
'protocol': data['pool']['protocol'],
'lb_algorithm': data['pool']['method'],
'listener_id': kwargs['listener_id']
}
if data['pool'].get('name'):
poolSpec['name'] = data['pool']['name']
if data['pool'].get('description'):
poolSpec['description'] = data['pool']['description']
return neutronclient(request).create_lbaas_pool(
{'pool': poolSpec}).get('pool')
@urls.register
class LoadBalancers(generic.View):
"""API for load balancers.
@ -59,6 +137,15 @@ class LoadBalancers(generic.View):
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

View File

@ -46,6 +46,8 @@ ADD_JS_FILES = [
'workflow.service.js'),
('dashboard/project/lbaasv2/loadbalancers/actions/create/details/'
'details.controller.js'),
('dashboard/project/lbaasv2/loadbalancers/actions/create/listener/'
'listener.controller.js')
]
ADD_JS_SPEC_FILES = [
@ -67,6 +69,8 @@ ADD_JS_SPEC_FILES = [
'workflow.service.spec.js'),
('dashboard/project/lbaasv2/loadbalancers/actions/create/details/'
'details.controller.spec.js'),
('dashboard/project/lbaasv2/loadbalancers/actions/create/listener/'
'listener.controller.spec.js')
]
ADD_SCSS_FILES = [

View File

@ -54,7 +54,7 @@
return [{
service: createModal,
template: {
url: basePath + 'loadbalancers/actions/create/action.template.html',
type: 'create',
text: gettext('Create Load Balancer')
}
}];

View File

@ -1,3 +0,0 @@
<action action-classes="'btn btn-default btn-sm pull-right'">
<span class="fa fa-plus">$text$</span>
</action>

View File

@ -21,7 +21,8 @@
.controller('CreateLoadBalancerDetailsController', CreateLoadBalancerDetailsController);
CreateLoadBalancerDetailsController.$inject = [
'horizon.dashboard.project.lbaasv2.patterns'
'horizon.dashboard.project.lbaasv2.patterns',
'horizon.framework.util.i18n.gettext'
];
/**
@ -31,10 +32,11 @@
* The `CreateLoadBalancerDetailsController` controller provides functions for
* configuring the details step of the Create Load Balancer Wizard.
* @param patterns The LBaaS v2 patterns constant.
* @param gettext The horizon gettext function for translation.
* @returns undefined
*/
function CreateLoadBalancerDetailsController(patterns) {
function CreateLoadBalancerDetailsController(patterns, gettext) {
var ctrl = this;

View File

@ -18,6 +18,7 @@
describe('Create Load Balancer Details Step', function() {
beforeEach(module('horizon.framework.util.i18n'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
describe('CreateLoadBalancerDetailsController', function() {

View File

@ -0,0 +1,44 @@
/*
* Copyright 2015 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('CreateListenerDetailsController', CreateListenerDetailsController);
CreateListenerDetailsController.$inject = [
'horizon.framework.util.i18n.gettext'
];
/**
* @ngdoc controller
* @name CreateListenerDetailsController
* @description
* The `CreateListenerDetailsController` controller provides functions for
* configuring the listener details step when creating a new listener.
* @param gettext The horizon gettext function for translation.
* @returns undefined
*/
function CreateListenerDetailsController(gettext) {
var ctrl = this;
// Error text for invalid fields
ctrl.listenerPortError = gettext('The port must be a number between 1 and 65535.');
}
})();

View File

@ -0,0 +1,37 @@
/*
* Copyright 2015 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 Listener Details Step', function() {
beforeEach(module('horizon.framework.util.i18n'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
describe('CreateListenerDetailsController', function() {
var ctrl;
beforeEach(inject(function($controller) {
ctrl = $controller('CreateListenerDetailsController');
}));
it('should define error messages for invalid fields', function() {
expect(ctrl.listenerPortError).toBeDefined();
});
});
});
})();

View File

@ -0,0 +1,3 @@
<h1 translate>Listener Details Help</h1>
<p translate>To create a listener, the port and protocol must be provided. If either of these properties are not provided, only the load balancer will be created.</p>

View File

@ -0,0 +1,62 @@
<div ng-controller="CreateListenerDetailsController as ctrl">
<h1 translate>Listener Details</h1>
<!--content-->
<div class="content">
<div translate class="subtitle">Provide the details for the new load balancer listener. The listener will only be created if values are provided for all fields marked as required.</div>
<div class="row form-group">
<div class="col-sm-12 col-md-6">
<div class="form-field listener-name">
<label translate class="on-top" for="listener-name">Name</label>
<input name="listener-name" id="listener-name"
type="text" class="form-control input-sm"
ng-model="model.spec.listener.name">
</div>
</div>
<div class="col-sm-12 col-md-6">
<div class="form-field listener-description">
<label translate class="on-top" for="listener-description">Description</label>
<input name="listener-description" id="listener-description"
type="text" class="form-control input-sm"
ng-model="model.spec.listener.description">
</div>
</div>
</div>
<div class="row form-group">
<div class="col-sm-6 col-md-3">
<div class="form-field required listener-protocol">
<label translate class="on-top" for="listener-protocol">Protocol</label>
<select class="form-control input-sm" name="listener-protocol"
id="listener-protocol"
ng-options="protocol for protocol in model.listenerProtocols"
ng-model="model.spec.listener.protocol">
</select>
</div>
</div>
<div class="col-sm-6 col-md-3">
<div class="form-field required listener-port"
ng-class="{ 'has-error': createLoadBalancerListenerForm['listener-port'].$invalid && createLoadBalancerListenerForm['listener-port'].$dirty }">
<label translate class="on-top" for="listener-port">Port</label>
<span class="fa fa-exclamation-triangle invalid"
ng-show="createLoadBalancerListenerForm['listener-port'].$invalid && createLoadBalancerListenerForm.$dirty"
popover="{$ ::ctrl.listenerPortError $}"
popover-placement="top" popover-append-to-body="true"
popover-trigger="hover"></span>
<input name="listener-port" id="listener-port"
type="number" class="form-control input-sm"
ng-model="model.spec.listener.port" ng-pattern="/^\d+$/" min="1" max="65535">
</div>
</div>
</div>
</div>
<!-- end content -->
</div>

View File

@ -24,7 +24,8 @@
createLoadBalancerModel.$inject = [
'$q',
'horizon.app.core.openstack-service-api.neutron',
'horizon.app.core.openstack-service-api.lbaasv2'
'horizon.app.core.openstack-service-api.lbaasv2',
'horizon.framework.util.i18n.gettext'
];
/**
@ -40,10 +41,11 @@
* @param $q The angular service for promises.
* @param neutronAPI The neutron 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) {
function createLoadBalancerModel($q, neutronAPI, lbaasv2API, gettext) {
var initPromise;
/**
@ -69,6 +71,9 @@
spec: null,
subnets: [],
listenerProtocols: ['TCP', 'HTTP', 'HTTPS'],
poolProtocols: ['TCP', 'HTTP', 'HTTPS'],
methods: ['ROUND_ROBIN', 'LEAST_CONNECTIONS', 'SOURCE_IP'],
/**
* api methods for UI controllers
@ -96,6 +101,18 @@
description: null,
ip: null,
subnet: null
},
listener: {
name: gettext('Listener 1'),
description: null,
protocol: null,
port: null
},
pool: {
name: gettext('Pool 1'),
description: null,
protocol: null,
method: null
}
};
@ -139,6 +156,16 @@
function createLoadBalancer() {
var finalSpec = angular.copy(model.spec);
// Listener requires protocol and port
if (!finalSpec.listener.protocol || !finalSpec.listener.port) {
delete finalSpec.listener;
}
// Pool requires protocol and method, and also the listener
if (!finalSpec.listener || !finalSpec.pool.protocol || !finalSpec.pool.method) {
delete finalSpec.pool;
}
// Delete null properties
angular.forEach(finalSpec, function(group, groupName) {
angular.forEach(group, function(value, key) {
@ -160,10 +187,9 @@
});
var name;
var index = 0;
var prefix = 'Load Balancer ';
do {
index += 1;
name = prefix + index;
name = interpolate(gettext('Load Balancer %(index)s'), { index: index }, true);
} while (name in existingNames);
model.spec.loadbalancer.name = name;
}

View File

@ -76,6 +76,18 @@
expect(model.subnets).toEqual([]);
});
it('has array of pool protocols', function() {
expect(model.poolProtocols).toEqual(['TCP', 'HTTP', 'HTTPS']);
});
it('has array of listener protocols', function() {
expect(model.listenerProtocols).toEqual(['TCP', 'HTTP', 'HTTPS']);
});
it('has array of methods', function() {
expect(model.methods).toEqual(['ROUND_ROBIN', 'LEAST_CONNECTIONS', 'SOURCE_IP']);
});
it('has an "initialize" function', function() {
expect(model.initialize).toBeDefined();
});
@ -98,10 +110,14 @@
expect(model.subnets.length).toBe(2);
expect(model.spec).toBeDefined();
expect(model.spec.loadbalancer).toBeDefined();
expect(model.spec.listener).toBeDefined();
expect(model.spec.pool).toBeDefined();
});
it('should initialize names', function() {
expect(model.spec.loadbalancer.name).toBe('Load Balancer 3');
expect(model.spec.listener.name).toBe('Listener 1');
expect(model.spec.pool.name).toBe('Pool 1');
});
});
@ -140,8 +156,10 @@
// 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(1);
expect(Object.keys(model.spec).length).toBe(3);
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);
});
it('sets load balancer name to null', function() {
@ -159,6 +177,38 @@
it('sets load balancer subnet to null', function() {
expect(model.spec.loadbalancer.subnet).toBeNull();
});
it('sets listener name to reasonable default', function() {
expect(model.spec.listener.name).toBe('Listener 1');
});
it('sets listener description to null', function() {
expect(model.spec.listener.description).toBeNull();
});
it('sets listener protocol to null', function() {
expect(model.spec.listener.protocol).toBeNull();
});
it('sets listener port to null', function() {
expect(model.spec.listener.port).toBeNull();
});
it('sets pool name to reasonable default', function() {
expect(model.spec.pool.name).toBe('Pool 1');
});
it('sets pool description to null', function() {
expect(model.spec.pool.description).toBeNull();
});
it('sets pool protocol to null', function() {
expect(model.spec.pool.protocol).toBeNull();
});
it('sets pool method to null', function() {
expect(model.spec.pool.method).toBeNull();
});
});
describe('Create Load Balancer', function() {
@ -171,12 +221,52 @@
it('should set final spec properties', 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.name = 'pool name';
model.spec.pool.description = 'pool description';
model.spec.pool.protocol = 'HTTP';
model.spec.pool.method = 'LEAST_CONNECTIONS';
var finalSpec = model.createLoadBalancer();
expect(finalSpec.loadbalancer.name).toBe('Load Balancer 3');
expect(finalSpec.loadbalancer.description).toBeUndefined();
expect(finalSpec.loadbalancer.ip).toBe('1.2.3.4');
expect(finalSpec.loadbalancer.subnet).toBe(model.subnets[0].id);
expect(finalSpec.listener.name).toBe('Listener 1');
expect(finalSpec.listener.description).toBeUndefined();
expect(finalSpec.listener.protocol).toBe('HTTPS');
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('HTTP');
expect(finalSpec.pool.method).toBe('LEAST_CONNECTIONS');
});
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 = 'HTTPS';
var finalSpec = model.createLoadBalancer();
expect(finalSpec.loadbalancer).toBeDefined();
expect(finalSpec.listener).toBeUndefined();
expect(finalSpec.pool).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 = 'HTTPS';
model.spec.listener.port = 80;
var finalSpec = model.createLoadBalancer();
expect(finalSpec.loadbalancer).toBeDefined();
expect(finalSpec.listener).toBeDefined();
expect(finalSpec.pool).toBeUndefined();
});
});

View File

@ -0,0 +1,3 @@
<h1 translate>Pool Details Help</h1>
<p translate>To create a pool, the protocol and method must be provided. If either of these properties are not provided then the pool will not be created.</p>

View File

@ -0,0 +1,58 @@
<div>
<h1 translate>Pool Details</h1>
<!--content-->
<div class="content">
<div translate class="subtitle">Provide the details for the new load balancer pool. The pool will only be created if values are provided for all fields marked as required.</div>
<div class="row form-group">
<div class="col-sm-12 col-md-6">
<div class="form-field pool-name">
<label translate class="on-top" for="pool-name">Name</label>
<input name="pool-name" id="pool-name"
type="text" class="form-control input-sm"
ng-model="model.spec.pool.name">
</div>
</div>
<div class="col-sm-12 col-md-6">
<div class="form-field pool-description">
<label translate class="on-top" for="pool-description">Description</label>
<input name="pool-description" id="pool-description"
type="text" class="form-control input-sm"
ng-model="model.spec.pool.description">
</div>
</div>
</div>
<div class="row form-group">
<div class="col-sm-6 col-md-3">
<div class="form-field required pool-protocol">
<label translate class="on-top" for="pool-protocol">Protocol</label>
<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">
</select>
</div>
</div>
<div class="col-sm-6 col-md-3">
<div class="form-field required pool-method">
<label translate class="on-top" for="pool-method">Method</label>
<select class="form-control input-sm" name="pool-method"
id="pool-method"
ng-options="method for method in model.methods"
ng-model="model.spec.pool.method">
</select>
</div>
</div>
</div>
</div>
<!-- end content -->
</div>

View File

@ -23,10 +23,11 @@
createLoadBalancerWorkflow.$inject = [
'horizon.dashboard.project.lbaasv2.basePath',
'horizon.app.core.workflow.factory'
'horizon.app.core.workflow.factory',
'horizon.framework.util.i18n.gettext'
];
function createLoadBalancerWorkflow(basePath, dashboardWorkflow) {
function createLoadBalancerWorkflow(basePath, dashboardWorkflow, gettext) {
return dashboardWorkflow({
title: gettext('Create Load Balancer'),
@ -37,6 +38,20 @@
templateUrl: basePath + 'loadbalancers/actions/create/details/details.html',
helpUrl: basePath + 'loadbalancers/actions/create/details/details.help.html',
formName: 'createLoadBalancerDetailsForm'
},
{
id: 'listener',
title: gettext('Listener Details'),
templateUrl: basePath + 'loadbalancers/actions/create/listener/listener.html',
helpUrl: basePath + 'loadbalancers/actions/create/listener/listener.help.html',
formName: 'createLoadBalancerListenerForm'
},
{
id: 'pool',
title: gettext('Pool Details'),
templateUrl: basePath + 'loadbalancers/actions/create/pool/pool.html',
helpUrl: basePath + 'loadbalancers/actions/create/pool/pool.help.html',
formName: 'createLoadBalancerPoolForm'
}
],

View File

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

View File

@ -1,6 +1,9 @@
<div class="content" ng-controller="LoadBalancerDetailController as ctrl">
<div class='page-header'>
<h1>{$ ::ctrl.loadbalancer.name $}</h1>
<ol class="breadcrumb">
<li><a href="project/ngloadbalancersv2/"><translate>Load Balancers</translate></a></li>
<li class="active">{$ ::ctrl.loadbalancer.name $}</li>
</ol>
<p ng-if="::ctrl.loadbalancer.description">{$ ::ctrl.loadbalancer.description $}</p>
</div>
<div class="detail-page">