Commit 51661642 authored by Justin Pomeroy's avatar Justin Pomeroy

Add associate and disassociate floating IP actions

This adds the load balancer actions for associating and
disassociating a floating IP address.

Partially-Implements: blueprint horizon-lbaas-v2-ui
Change-Id: Ie62cbaa6e4e6664a4d266f01557386d6d40cc2b1
parent 988bd51b
......@@ -21,6 +21,7 @@ from django.views import generic
from horizon import conf
from openstack_dashboard.api import network
from openstack_dashboard.api import neutron
from openstack_dashboard.api.rest import urls
from openstack_dashboard.api.rest import utils as rest_utils
......@@ -348,6 +349,21 @@ def update_member_list(request, **kwargs):
thread.start_new_thread(poll_loadbalancer_status, args)
def add_floating_ip_info(request, loadbalancers):
"""Add floating IP address info to each load balancer.
"""
floating_ips = network.tenant_floating_ip_list(request)
for lb in loadbalancers:
floating_ip = {}
associated_ip = next((fip for fip in floating_ips
if fip['fixed_ip'] == lb['vip_address']), None)
if associated_ip is not None:
floating_ip['id'] = associated_ip['id']
floating_ip['ip'] = associated_ip['ip']
lb['floating_ip'] = floating_ip
@urls.register
class LoadBalancers(generic.View):
"""API for load balancers.
......@@ -362,8 +378,11 @@ class LoadBalancers(generic.View):
The listing result is an object with property "items".
"""
tenant_id = request.user.project_id
result = neutronclient(request).list_loadbalancers(tenant_id=tenant_id)
return {'items': result.get('loadbalancers')}
loadbalancers = neutronclient(request).list_loadbalancers(
tenant_id=tenant_id).get('loadbalancers')
if request.GET.get('full') and network.floating_ip_supported(request):
add_floating_ip_info(request, loadbalancers)
return {'items': loadbalancers}
@rest_utils.ajax()
def post(self, request):
......@@ -409,8 +428,11 @@ class LoadBalancer(generic.View):
http://localhost/api/lbaas/loadbalancers/cc758c90-3d98-4ea1-af44-aab405c9c915
"""
lb = neutronclient(request).show_loadbalancer(loadbalancer_id)
return lb.get('loadbalancer')
loadbalancer = neutronclient(request).show_loadbalancer(
loadbalancer_id).get('loadbalancer')
if request.GET.get('full') and network.floating_ip_supported(request):
add_floating_ip_info(request, [loadbalancer])
return loadbalancer
@rest_utils.ajax()
def put(self, request, loadbalancer_id):
......
......@@ -60,13 +60,14 @@
* @name horizon.app.core.openstack-service-api.lbaasv2.getLoadBalancers
* @description
* Get a list of load balancers.
*
* @param {boolean} full
* The listing result is an object with property "items". Each item is
* a load balancer.
*/
function getLoadBalancers() {
return apiService.get('/api/lbaas/loadbalancers/')
function getLoadBalancers(full) {
var params = { full: full };
return apiService.get('/api/lbaas/loadbalancers/', { params: params })
.error(function () {
toastService.add('error', gettext('Unable to retrieve load balancers.'));
});
......@@ -77,11 +78,13 @@
* @description
* Get a single load balancer by ID
* @param {string} id
* @param {boolean} full
* Specifies the id of the load balancer to request.
*/
function getLoadBalancer(id) {
return apiService.get('/api/lbaas/loadbalancers/' + id)
function getLoadBalancer(id, full) {
var params = { full: full };
return apiService.get('/api/lbaas/loadbalancers/' + id, { params: params })
.error(function () {
toastService.add('error', gettext('Unable to retrieve load balancer.'));
});
......
......@@ -37,144 +37,110 @@
var tests = [
{
"func": "getLoadBalancers",
"method": "get",
"path": "/api/lbaas/loadbalancers/",
"error": "Unable to retrieve load balancers."
func: 'getLoadBalancers',
method: 'get',
path: '/api/lbaas/loadbalancers/',
error: 'Unable to retrieve load balancers.',
testInput: [ true ],
data: { params: { full: true } }
},
{
"func": "getLoadBalancer",
"method": "get",
"path": "/api/lbaas/loadbalancers/1234",
"error": "Unable to retrieve load balancer.",
"testInput": [
'1234'
]
func: 'getLoadBalancer',
method: 'get',
path: '/api/lbaas/loadbalancers/1234',
error: 'Unable to retrieve load balancer.',
testInput: [ '1234', true ],
data: { params: { full: true } }
},
{
"func": "deleteLoadBalancer",
"method": "delete",
"path": "/api/lbaas/loadbalancers/1234",
"error": "Unable to delete load balancer.",
"testInput": [
'1234'
]
func: 'deleteLoadBalancer',
method: 'delete',
path: '/api/lbaas/loadbalancers/1234',
error: 'Unable to delete load balancer.',
testInput: [ '1234' ]
},
{
"func": "getListeners",
"method": "get",
"path": "/api/lbaas/listeners/",
"data": {
"params": {
"loadbalancerId": "1234"
}
},
"error": "Unable to retrieve listeners.",
"testInput": [
"1234"
]
func: 'getListeners',
method: 'get',
path: '/api/lbaas/listeners/',
error: 'Unable to retrieve listeners.',
testInput: [ '1234' ],
data: { params: { loadbalancerId: '1234' } }
},
{
"func": "getListeners",
"method": "get",
"path": "/api/lbaas/listeners/",
"data": {},
"error": "Unable to retrieve listeners."
func: 'getListeners',
method: 'get',
path: '/api/lbaas/listeners/',
data: {},
error: 'Unable to retrieve listeners.'
},
{
"func": "getListener",
"method": "get",
"path": "/api/lbaas/listeners/1234",
"data": {
"params": {
"includeChildResources": true
}
},
"error": "Unable to retrieve listener.",
"testInput": [
'1234',
true
]
func: 'getListener',
method: 'get',
path: '/api/lbaas/listeners/1234',
data: { params: { includeChildResources: true } },
error: 'Unable to retrieve listener.',
testInput: [ '1234', true ]
},
{
"func": "getListener",
"method": "get",
"path": "/api/lbaas/listeners/1234",
"data": {},
"error": "Unable to retrieve listener.",
"testInput": [
'1234',
false
]
func: 'getListener',
method: 'get',
path: '/api/lbaas/listeners/1234',
data: {},
error: 'Unable to retrieve listener.',
testInput: [ '1234', false ]
},
{
"func": "getPool",
"method": "get",
"path": "/api/lbaas/pools/1234",
"error": "Unable to retrieve pool.",
"testInput": [
'1234'
]
func: 'getPool',
method: 'get',
path: '/api/lbaas/pools/1234',
error: 'Unable to retrieve pool.',
testInput: [ '1234' ]
},
{
"func": "getMembers",
"method": "get",
"path": "/api/lbaas/pools/1234/members/",
"error": "Unable to retrieve members.",
"testInput": [
'1234'
]
func: 'getMembers',
method: 'get',
path: '/api/lbaas/pools/1234/members/',
error: 'Unable to retrieve members.',
testInput: [ '1234' ]
},
{
"func": "getMember",
"method": "get",
"path": "/api/lbaas/pools/1234/members/5678",
"error": "Unable to retrieve member.",
"testInput": [
'1234',
'5678'
]
func: 'getMember',
method: 'get',
path: '/api/lbaas/pools/1234/members/5678',
error: 'Unable to retrieve member.',
testInput: [ '1234', '5678' ]
},
{
"func": "getHealthMonitor",
"method": "get",
"path": "/api/lbaas/healthmonitors/1234",
"error": "Unable to retrieve health monitor.",
"testInput": [
'1234'
]
func: 'getHealthMonitor',
method: 'get',
path: '/api/lbaas/healthmonitors/1234',
error: 'Unable to retrieve health monitor.',
testInput: [ '1234' ]
},
{
"func": "createLoadBalancer",
"method": "post",
"path": "/api/lbaas/loadbalancers/",
"error": "Unable to create load balancer.",
"data": { "name": "loadbalancer-1" },
"testInput": [
{ "name": "loadbalancer-1" }
]
func: 'createLoadBalancer',
method: 'post',
path: '/api/lbaas/loadbalancers/',
error: 'Unable to create load balancer.',
data: { name: 'loadbalancer-1' },
testInput: [ { name: 'loadbalancer-1' } ]
},
{
"func": "editLoadBalancer",
"method": "put",
"path": "/api/lbaas/loadbalancers/1234",
"error": "Unable to update load balancer.",
"data": { "name": "loadbalancer-1" },
"testInput": [
"1234",
{ "name": "loadbalancer-1" }
]
func: 'editLoadBalancer',
method: 'put',
path: '/api/lbaas/loadbalancers/1234',
error: 'Unable to update load balancer.',
data: { name: 'loadbalancer-1' },
testInput: [ '1234', { name: 'loadbalancer-1' } ]
},
{
"func": "editListener",
"method": "put",
"path": "/api/lbaas/listeners/1234",
"error": "Unable to update listener.",
"data": { "name": "listener-1" },
"testInput": [
"1234",
{ "name": "listener-1" }
]
func: 'editListener',
method: 'put',
path: '/api/lbaas/listeners/1234',
error: 'Unable to update listener.',
data: { name: 'listener-1' },
testInput: [ '1234', { name: 'listener-1' } ]
}
];
......
/*
* 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('AssociateFloatingIpModalController', AssociateFloatingIpModalController);
AssociateFloatingIpModalController.$inject = [
'$modalInstance',
'horizon.app.core.openstack-service-api.network',
'horizon.framework.util.i18n.gettext',
// Dependencies injected with resolve by $modal.open
'loadbalancer',
'floatingIps',
'floatingIpPools'
];
/**
* @ngdoc controller
* @name AssociateFloatingIpModalController
* @description
* Controller used by the modal service for associating a floating IP address to a
* load balancer.
*
* @param $modalInstance The angular bootstrap $modalInstance service.
* @param api The horizon network API service.
* @param gettext The horizon gettext function for translation.
* @param loadbalancer The load balancer to associate the floating IP with.
* @param floatingIps List of available floating IP addresses.
* @param floatingIpPools List of available floating IP pools.
*
* @returns The Associate Floating IP modal controller.
*/
function AssociateFloatingIpModalController(
$modalInstance, api, gettext, loadbalancer, floatingIps, floatingIpPools
) {
var ctrl = this;
var port = loadbalancer.vip_port_id + '_' + loadbalancer.vip_address;
ctrl.cancel = cancel;
ctrl.save = save;
ctrl.saving = false;
ctrl.options = initOptions();
ctrl.selected = ctrl.options.length === 1 ? ctrl.options[0] : null;
function save() {
ctrl.saving = true;
if (ctrl.selected.type === 'pool') {
allocateIpAddress(ctrl.selected.id);
} else {
associateIpAddress(ctrl.selected.id);
}
}
function cancel() {
$modalInstance.dismiss('cancel');
}
function onSuccess() {
$modalInstance.close();
}
function onFailure() {
ctrl.saving = false;
}
function initOptions() {
var options = [];
floatingIps.forEach(function addFloatingIp(ip) {
// Only show floating IPs that are not already associated with a fixed IP
if (!ip.fixed_ip) {
options.push({
id: ip.id,
name: ip.ip || ip.id,
type: 'ip',
group: gettext('Floating IP addresses')
});
}
});
floatingIpPools.forEach(function addFloatingIpPool(pool) {
options.push({
id: pool.id,
name: pool.name || pool.id,
type: 'pool',
group: gettext('Floating IP pools')
});
});
return options;
}
function allocateIpAddress(poolId) {
return api.allocateFloatingIp(poolId).then(getId).then(associateIpAddress);
}
function associateIpAddress(addressId) {
return api.associateFloatingIp(addressId, port).then(onSuccess, onFailure);
}
function getId(response) {
return response.data.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 Load Balancers Table Associate IP Controller', function() {
var ctrl, network, floatingIps, floatingIpPools, $controller, $modalInstance;
var associateFail = false;
beforeEach(module('horizon.framework.util.i18n'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(function() {
floatingIps = [{ id: 'ip1', ip: '1', fixed_ip: '1' },
{ id: 'ip2', ip: '2' }];
floatingIpPools = [{ id: 'pool1', name: 'pool' }];
});
beforeEach(module(function($provide) {
var fakePromise = function(response, returnPromise) {
return {
then: function(success, fail) {
if (fail && associateFail) {
return fail();
}
var res = success(response);
return returnPromise ? fakePromise(res) : res;
}
};
};
$provide.value('$modalInstance', {
close: angular.noop,
dismiss: angular.noop
});
$provide.value('loadbalancer', {
vip_port_id: 'port',
vip_address: 'address'
});
$provide.value('floatingIps', floatingIps);
$provide.value('floatingIpPools', floatingIpPools);
$provide.value('horizon.app.core.openstack-service-api.network', {
allocateFloatingIp: function() {
return fakePromise({ data: { id: 'foo' } }, true);
},
associateFloatingIp: function() {
return fakePromise();
}
});
}));
beforeEach(inject(function ($injector) {
network = $injector.get('horizon.app.core.openstack-service-api.network');
$controller = $injector.get('$controller');
$modalInstance = $injector.get('$modalInstance');
}));
it('should define controller properties', function() {
ctrl = $controller('AssociateFloatingIpModalController');
expect(ctrl.cancel).toBeDefined();
expect(ctrl.save).toBeDefined();
expect(ctrl.saving).toBe(false);
});
it('should initialize options', function() {
ctrl = $controller('AssociateFloatingIpModalController');
expect(ctrl.options.length).toBe(2);
expect(ctrl.options[0].id).toBe('ip2');
expect(ctrl.options[1].id).toBe('pool1');
});
it('should use ids instead of ip or name if not provided', function() {
delete floatingIps[1].ip;
delete floatingIpPools[0].name;
ctrl = $controller('AssociateFloatingIpModalController');
expect(ctrl.options.length).toBe(2);
expect(ctrl.options[0].name).toBe('ip2');
expect(ctrl.options[1].name).toBe('pool1');
});
it('should initialize selected option when only one option', function() {
floatingIps[1].fixed_ip = '2';
ctrl = $controller('AssociateFloatingIpModalController');
expect(ctrl.options.length).toBe(1);
expect(ctrl.selected).toBe(ctrl.options[0]);
});
it('should not initialize selected option when more than one option', function() {
ctrl = $controller('AssociateFloatingIpModalController');
expect(ctrl.options.length).toBe(2);
expect(ctrl.selected).toBeNull();
});
it('should associate floating IP if floating IP selected', function() {
ctrl = $controller('AssociateFloatingIpModalController');
ctrl.selected = ctrl.options[0];
spyOn(network, 'associateFloatingIp').and.callThrough();
spyOn($modalInstance, 'close');
ctrl.save();
expect(ctrl.saving).toBe(true);
expect(network.associateFloatingIp).toHaveBeenCalledWith('ip2', 'port_address');
expect($modalInstance.close).toHaveBeenCalled();
});
it('should allocate floating IP if floating IP pool selected', function() {
ctrl = $controller('AssociateFloatingIpModalController');
ctrl.selected = ctrl.options[1];
spyOn(network, 'allocateFloatingIp').and.callThrough();
spyOn(network, 'associateFloatingIp').and.callThrough();
spyOn($modalInstance, 'close');
ctrl.save();
expect(ctrl.saving).toBe(true);
expect(network.allocateFloatingIp).toHaveBeenCalledWith('pool1');
expect(network.associateFloatingIp).toHaveBeenCalledWith('foo', 'port_address');
expect($modalInstance.close).toHaveBeenCalled();
});
it('should dismiss modal if cancel clicked', function() {
ctrl = $controller('AssociateFloatingIpModalController');
spyOn($modalInstance, 'dismiss');
ctrl.cancel();
expect($modalInstance.dismiss).toHaveBeenCalledWith('cancel');
});
it('should not dismiss modal if save fails', function() {
ctrl = $controller('AssociateFloatingIpModalController');
ctrl.selected = ctrl.options[0];
associateFail = true;
spyOn($modalInstance, 'dismiss');
ctrl.save();
expect($modalInstance.dismiss).not.toHaveBeenCalled();
expect(ctrl.saving).toBe(false);
});
});
})();
<div class="modal-header">
<h3 class="modal-title">
<span translate>Associate Floating IP Address</span>
</h3>
</div>
<div class="modal-body">
<p translate>Select a floating IP address to associate with the load balancer or a floating IP pool in which to allocate a new floating IP address.</p>
<div ng-form="form">
<div class="row form-group">
<div class="col-sm-12 col-md-6">
<div class="form-field required">
<label translate class="on-top" for="floating-ip">Floating IP address or pool</label>
<select class="form-control input-sm" name="floating-ip" id="floating-ip"
ng-options="item.name group by item.group for item in modal.options"
ng-model="modal.selected" ng-required="true">
</select>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-sm btn-default" ng-click="modal.cancel()">
<span class="fa fa-close"></span>
<span translate>Cancel</span>
</button>
<button class="btn btn-sm btn-primary"
ng-click="modal.save()"
ng-disabled="form.$invalid || modal.saving">
<span class="fa" ng-class="modal.saving ? 'fa-spinner fa-spin' : 'fa-check'"></span>
<span translate>Associate</span>
</button>
</div>
/*
* 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')
.factory('horizon.dashboard.project.lbaasv2.loadbalancers.actions.associate-ip.modal.service',
modalService);
modalService.$inject = [
'$q',
'$modal',
'$route',
'horizon.da