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
This commit is contained in:
Justin Pomeroy 2016-02-16 22:17:42 -06:00
parent 988bd51b2e
commit 51661642c0
17 changed files with 940 additions and 133 deletions

View File

@ -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):

View File

@ -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.'));
});

View File

@ -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' } ]
}
];

View File

@ -0,0 +1,119 @@
/*
* 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;
}
}
})();

View File

@ -0,0 +1,149 @@
/*
* 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);
});
});
})();

View File

@ -0,0 +1,33 @@
<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>

View File

@ -0,0 +1,126 @@
/*
* 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.dashboard.project.lbaasv2.basePath',
'horizon.app.core.openstack-service-api.policy',
'horizon.app.core.openstack-service-api.network',
'horizon.framework.util.q.extensions',
'horizon.framework.widgets.toast.service',
'horizon.framework.util.i18n.gettext'
];
/**
* @ngdoc service
* @ngname horizon.dashboard.project.lbaasv2.loadbalancers.actions.associate-ip.modal.service
*
* @description
* Provides the service for the Load Balancer Associate Floating IP action.
*
* @param $q The angular service for promises.
* @param $modal The angular bootstrap $modal service.
* @param $route The angular $route service.
* @param basePath The LBaaS v2 module base path.
* @param policy The horizon policy service.
* @param network The horizon network API service.
* @param qExtensions Horizon extensions to the $q service.
* @param toastService The horizon toast service.
* @param gettext The horizon gettext function for translation.
*
* @returns The Associate Floating IP modal service.
*/
function modalService(
$q,
$modal,
$route,
basePath,
policy,
network,
qExtensions,
toastService,
gettext
) {
var service = {
perform: open,
allowed: allowed
};
return service;
////////////
function allowed(item) {
return $q.all([
qExtensions.booleanAsPromise(item.floating_ip && !item.floating_ip.ip),
// This rule is made up and should therefore always pass. At some point there will
// likely be a valid rule similar to this that we will want to use.
policy.ifAllowed({ rules: [['neutron', 'loadbalancer_associate_floating_ip']] })
]);
}
/**
* @ngdoc method
* @name open
*
* @description
* Open the modal.
*
* @param item The row item from the table action.
* @returns undefined
*/
function open(item) {
var spec = {
backdrop: 'static',
controller: 'AssociateFloatingIpModalController as modal',
templateUrl: basePath + 'loadbalancers/actions/associate-ip/modal.html',
resolve: {
loadbalancer: function() {
return item;
},
floatingIps: function() {
return network.getFloatingIps().then(getResponseItems);
},
floatingIpPools: function() {
return network.getFloatingIpPools().then(getResponseItems);
}
}
};
$modal.open(spec).result.then(onModalClose);
}
function onModalClose() {
toastService.add('success', gettext('Associating floating IP with load balancer.'));
$route.reload();
}
function getResponseItems(response) {
return response.data.items;
}
}
})();

View File

@ -0,0 +1,130 @@
/*
* 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 Service', function() {
var service, policy, $scope, $route, item, $modal, toast;
function allowed(item) {
spyOn(policy, 'ifAllowed').and.returnValue(true);
var promise = service.allowed(item);
var allowed;
promise.then(function() {
allowed = true;
}, function() {
allowed = false;
});
$scope.$apply();
expect(policy.ifAllowed).toHaveBeenCalledWith(
{rules: [['neutron', 'loadbalancer_associate_floating_ip']]});
return allowed;
}
beforeEach(module('horizon.framework.util'));
beforeEach(module('horizon.framework.conf'));
beforeEach(module('horizon.framework.widgets'));
beforeEach(module('horizon.app.core.openstack-service-api'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(function() {
item = { id: '1', name: 'First', floating_ip: {} };
});
beforeEach(module(function($provide) {
var fakePromise = function(response) {
return {
then: function(func) {
return func(response);
}
};
};
$provide.value('$modal', {
open: function() {
return {
result: fakePromise()
};
}
});
$provide.value('horizon.app.core.openstack-service-api.network', {
getFloatingIps: function() {
return fakePromise({ data: { items: 'foo' } });
},
getFloatingIpPools: function() {
return fakePromise({ data: { items: 'bar' } });
}
});
}));
beforeEach(inject(function ($injector) {
policy = $injector.get('horizon.app.core.openstack-service-api.policy');
toast = $injector.get('horizon.framework.widgets.toast.service');
$scope = $injector.get('$rootScope').$new();
$route = $injector.get('$route');
$modal = $injector.get('$modal');
service = $injector.get(
'horizon.dashboard.project.lbaasv2.loadbalancers.actions.associate-ip.modal.service');
}));
it('should have the "allowed" and "perform" functions', function() {
expect(service.allowed).toBeDefined();
expect(service.perform).toBeDefined();
});
it('should check policy to allow the action', function() {
expect(allowed(item)).toBe(true);
});
it('should not allow action if floating IP already associated', function() {
item.floating_ip.ip = 'foo';
expect(allowed(item)).toBe(false);
});
it('should open the modal', function() {
spyOn($modal, 'open').and.callThrough();
service.perform(item);
$scope.$apply();
expect($modal.open.calls.count()).toBe(1);
});
it('should resolve data for passing into the modal', function() {
spyOn($modal, 'open').and.callThrough();
service.perform(item);
$scope.$apply();
var resolve = $modal.open.calls.argsFor(0)[0].resolve;
expect(resolve).toBeDefined();
expect(resolve.loadbalancer).toBeDefined();
expect(resolve.loadbalancer()).toEqual(item);
expect(resolve.floatingIps).toBeDefined();
expect(resolve.floatingIps()).toBe('foo');
expect(resolve.floatingIpPools).toBeDefined();
expect(resolve.floatingIpPools()).toBe('bar');
});
it('should show message and reload page upon closing modal', function() {
spyOn(toast, 'add');
spyOn($route, 'reload');
service.perform(item);
$scope.$apply();
expect(toast.add).toHaveBeenCalledWith('success',
'Associating floating IP with load balancer.');
expect($route.reload).toHaveBeenCalled();
});
});
})();

View File

@ -0,0 +1,99 @@
/*
* 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.disassociate-ip.modal.service',
modalService);
modalService.$inject = [
'$q',
'$route',
'horizon.framework.widgets.modal.deleteModalService',
'horizon.app.core.openstack-service-api.network',
'horizon.app.core.openstack-service-api.policy',
'horizon.framework.util.q.extensions',
'horizon.framework.util.i18n.gettext'
];
/**
* @ngDoc factory
* @name horizon.dashboard.project.lbaasv2.loadbalancers.actions.disassociate-ip.modal.service
* @description
* Brings up the disassociate floating IP confirmation modal dialog.
* On submit, dsiassociates the floating IP address from the load balancer.
* On cancel, does nothing.
* @param $q The angular service for promises.
* @param $route The angular $route service.
* @param deleteModal The horizon delete modal service.
* @param network The horizon network API service.
* @param policy The horizon policy service.
* @param qExtensions Horizon extensions to the $q service.
* @param gettext The horizon gettext function for translation.
* @returns The load balancers table row delete service.
*/
function modalService($q, $route, deleteModal, network, policy, qExtensions, gettext) {
var loadbalancer;
var context = {
labels: {
title: gettext('Confirm Disassociate Floating IP Address'),
/* eslint-disable max-len */
message: gettext('You are about to disassociate the floating IP address from load balancer "%s". Please confirm.'),
/* eslint-enable max-len */
submit: gettext('Disassociate'),
success: gettext('Disassociated floating IP address from load balancer: %s.'),
error: gettext('Unable to disassociate floating IP address from load balancer: %s.')
},
deleteEntity: disassociate
};
var service = {
perform: perform,
allowed: allowed
};
return service;
//////////////
function perform(item) {
loadbalancer = item;
deleteModal.open({ $emit: actionComplete }, [item], context);
}
function allowed(item) {
return $q.all([
qExtensions.booleanAsPromise(item.floating_ip && !!item.floating_ip.ip),
// This rule is made up and should therefore always pass. At some point there will
// likely be a valid rule similar to this that we will want to use.
policy.ifAllowed({ rules: [['neutron', 'loadbalancer_disassociate_floating_ip']] })
]);
}
function disassociate() {
return network.disassociateFloatingIp(loadbalancer.floating_ip.id);
}
function actionComplete() {
$route.reload();
}
}
})();

View File

@ -0,0 +1,124 @@
/*
* 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 Disassociate IP Service', function() {
var service, policy, modal, network, $scope, $route, item;
function allowed(item) {
spyOn(policy, 'ifAllowed').and.returnValue(true);
var promise = service.allowed(item);
var allowed;
promise.then(function() {
allowed = true;
}, function() {
allowed = false;
});
$scope.$apply();
expect(policy.ifAllowed).toHaveBeenCalledWith(
{rules: [['neutron', 'loadbalancer_disassociate_floating_ip']]});
return allowed;
}
beforeEach(module('horizon.framework.util'));
beforeEach(module('horizon.framework.conf'));
beforeEach(module('horizon.framework.widgets'));
beforeEach(module('horizon.app.core.openstack-service-api'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(function() {
item = { id: '1', name: 'First', floating_ip: { id: 'ip1', ip: '1' } };
});
beforeEach(module(function($provide) {
var fakePromise = {
then: function(func) {
func();
}
};
$provide.value('$modal', {
open: function() {
return {
result: fakePromise
};
}
});
$provide.value('horizon.app.core.openstack-service-api.network', {
disassociateFloatingIp: function() {
return fakePromise;
}
});
}));
beforeEach(inject(function ($injector) {
policy = $injector.get('horizon.app.core.openstack-service-api.policy');
network = $injector.get('horizon.app.core.openstack-service-api.network');
modal = $injector.get('horizon.framework.widgets.modal.deleteModalService');
$scope = $injector.get('$rootScope').$new();
$route = $injector.get('$route');
service = $injector.get(
'horizon.dashboard.project.lbaasv2.loadbalancers.actions.disassociate-ip.modal.service');
}));
it('should have the "allowed" and "perform" functions', function() {
expect(service.allowed).toBeDefined();
expect(service.perform).toBeDefined();
});
it('should check policy to allow action', function() {
expect(allowed(item)).toBe(true);
});
it('should not allow action if floating IP not associated', function() {
delete item.floating_ip.ip;
expect(allowed(item)).toBe(false);
});
it('should open the delete modal', function() {
spyOn(modal, 'open');
service.perform(item);
$scope.$apply();
expect(modal.open.calls.count()).toBe(1);
var args = modal.open.calls.argsFor(0);
expect(args.length).toBe(3);
expect(args[0]).toEqual({ $emit: jasmine.any(Function) });
expect(args[1]).toEqual([jasmine.objectContaining({ id: '1' })]);
expect(args[2]).toEqual(jasmine.objectContaining({
labels: jasmine.any(Object),
deleteEntity: jasmine.any(Function)
}));
expect(args[2].labels.title).toBe('Confirm Disassociate Floating IP Address');
});
it('should pass function to modal that disassociates the IP address', function() {
spyOn(modal, 'open').and.callThrough();
spyOn(network, 'disassociateFloatingIp').and.callThrough();
service.perform(item);
$scope.$apply();
expect(network.disassociateFloatingIp.calls.count()).toBe(1);
expect(network.disassociateFloatingIp).toHaveBeenCalledWith('ip1');
});
it('should reload page after action completes', function() {
spyOn($route, 'reload');
service.perform(item);
$scope.$apply();
expect($route.reload).toHaveBeenCalled();
});
});
})();

View File

@ -26,7 +26,10 @@
'$route',
'horizon.dashboard.project.lbaasv2.workflow.modal',
'horizon.dashboard.project.lbaasv2.loadbalancers.actions.delete',
'horizon.dashboard.project.lbaasv2.loadbalancers.actions.associate-ip.modal.service',
'horizon.dashboard.project.lbaasv2.loadbalancers.actions.disassociate-ip.modal.service',
'horizon.app.core.openstack-service-api.policy',
'horizon.app.core.openstack-service-api.network',
'horizon.framework.util.q.extensions',
'horizon.framework.util.i18n.gettext'
];
@ -42,14 +45,27 @@
* @param $route The angular $route service.
* @param workflowModal The LBaaS workflow modal service.
* @param deleteService The load balancer delete service.
* @param associateIp The associate floating IP modal service.
* @param disassociateIp The disassociate floating IP modal service.
* @param policy The horizon policy service.
* @param network The horizon network API service.
* @param qExtensions Horizon extensions to the $q service.
* @param gettext The horizon gettext function for translation.
* @returns Load balancers table batch actions service object.
* @returns Load balancers table row actions service object.
*/
function tableRowActions($q, $route, workflowModal, deleteService, policy, qExtensions, gettext) {
function tableRowActions(
$q,
$route,
workflowModal,
deleteService,
associateIp,
disassociateIp,
policy,
network,
qExtensions,
gettext
) {
var edit = workflowModal.init({
controller: 'EditLoadBalancerWizardController',
message: gettext('The load balancer has been updated.'),
@ -71,7 +87,17 @@
template: {
text: gettext('Edit')
}
}, {
},{
service: associateIp,
template: {
text: gettext('Associate Floating IP')
}
},{
service: disassociateIp,
template: {
text: gettext('Disassociate Floating IP')
}
},{
service: deleteService,
template: {
text: gettext('Delete Load Balancer'),

View File

@ -69,9 +69,11 @@
}));
it('should define correct table row actions', function() {
expect(actions.length).toBe(2);
expect(actions.length).toBe(4);
expect(actions[0].template.text).toBe('Edit');
expect(actions[1].template.text).toBe('Delete Load Balancer');
expect(actions[1].template.text).toBe('Associate Floating IP');
expect(actions[2].template.text).toBe('Disassociate Floating IP');
expect(actions[3].template.text).toBe('Delete Load Balancer');
});
it('should allow editing an ACTIVE load balancer', function() {

View File

@ -53,7 +53,7 @@
////////////////////////////////
function init() {
api.getLoadBalancer($routeParams.loadbalancerId).success(success);
api.getLoadBalancer($routeParams.loadbalancerId, true).success(success);
}
function success(response) {

View File

@ -55,7 +55,7 @@
it('should invoke lbaasv2 apis', function() {
createController();
expect(lbaasv2API.getLoadBalancer).toHaveBeenCalledWith('1234');
expect(lbaasv2API.getLoadBalancer).toHaveBeenCalledWith('1234', true);
});
});

View File

@ -32,7 +32,11 @@
</div>
<div>
<dt translate>Admin State Up</dt>
<dd>{$ ::ctrl.loadbalancer.admin_state_up | yesno $}</dd>
<dd>{$ ctrl.loadbalancer.admin_state_up | yesno $}</dd>
</div>
<div ng-if="ctrl.loadbalancer.floating_ip !== undefined">
<dt translate>Floating IP Address</dt>
<dd>{$ ctrl.loadbalancer.floating_ip.ip || 'None' | translate $}</dd>
</div>
<div>
<dt translate>Load Balancer ID</dt>

View File

@ -57,7 +57,7 @@
////////////////////////////////
function init() {
api.getLoadBalancers().success(success);
api.getLoadBalancers(true).success(success);
}
function success(response) {

View File

@ -117,6 +117,10 @@
<dt translate>Provider</dt>
<dd>{$ ::item.provider $}</dd>
</dl>
<dl class="col-sm-2" ng-if="item.floating_ip !== undefined">
<dt translate>Floating IP Address</dt>
<dd>{$ item.floating_ip.ip || 'None' | translate $}</dd>
</dl>
<dl class="col-sm-2">
<dt translate>Admin State Up</dt>
<dd>{$ ::item.admin_state_up | yesno $}</dd>
@ -127,11 +131,11 @@
</dl>
<dl class="col-sm-2">
<dt translate>Subnet ID</dt>
<dd>{$ ::item.vip_subnet_id $}</dd>
<dd><a target="_self" ng-href="project/networks/subnets/{$ ::item.vip_subnet_id $}/detail">{$ ::item.vip_subnet_id $}</a></dd>
</dl>
<dl class="col-sm-2">
<dt translate>Port ID</dt>
<dd>{$ ::item.vip_port_id $}</dd>
<dd><a target="_self" ng-href="project/networks/ports/{$ ::item.vip_port_id $}/detail">{$ ::item.vip_port_id $}</a></dd>
</dl>
</div>