From 51661642c068b2a2c3a5fc47ff81da6d7312fef5 Mon Sep 17 00:00:00 2001 From: Justin Pomeroy Date: Tue, 16 Feb 2016 22:17:42 -0600 Subject: [PATCH] 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 --- neutron_lbaas_dashboard/api/rest/lbaasv2.py | 30 ++- .../openstack-service-api/lbaasv2.service.js | 13 +- .../lbaasv2.service.spec.js | 190 +++++++----------- .../actions/associate-ip/modal.controller.js | 119 +++++++++++ .../associate-ip/modal.controller.spec.js | 149 ++++++++++++++ .../actions/associate-ip/modal.html | 33 +++ .../actions/associate-ip/modal.service.js | 126 ++++++++++++ .../associate-ip/modal.service.spec.js | 130 ++++++++++++ .../actions/disassociate-ip/modal.service.js | 99 +++++++++ .../disassociate-ip/modal.service.spec.js | 124 ++++++++++++ .../actions/row-actions.service.js | 34 +++- .../actions/row-actions.service.spec.js | 6 +- .../loadbalancers/detail.controller.js | 2 +- .../loadbalancers/detail.controller.spec.js | 2 +- .../project/lbaasv2/loadbalancers/detail.html | 6 +- .../lbaasv2/loadbalancers/table.controller.js | 2 +- .../project/lbaasv2/loadbalancers/table.html | 8 +- 17 files changed, 940 insertions(+), 133 deletions(-) create mode 100644 neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/associate-ip/modal.controller.js create mode 100644 neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/associate-ip/modal.controller.spec.js create mode 100644 neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/associate-ip/modal.html create mode 100644 neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/associate-ip/modal.service.js create mode 100644 neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/associate-ip/modal.service.spec.js create mode 100644 neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/disassociate-ip/modal.service.js create mode 100644 neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/disassociate-ip/modal.service.spec.js diff --git a/neutron_lbaas_dashboard/api/rest/lbaasv2.py b/neutron_lbaas_dashboard/api/rest/lbaasv2.py index 743432ea..25fed39f 100644 --- a/neutron_lbaas_dashboard/api/rest/lbaasv2.py +++ b/neutron_lbaas_dashboard/api/rest/lbaasv2.py @@ -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): diff --git a/neutron_lbaas_dashboard/static/app/core/openstack-service-api/lbaasv2.service.js b/neutron_lbaas_dashboard/static/app/core/openstack-service-api/lbaasv2.service.js index 1ade241b..5f19e85c 100644 --- a/neutron_lbaas_dashboard/static/app/core/openstack-service-api/lbaasv2.service.js +++ b/neutron_lbaas_dashboard/static/app/core/openstack-service-api/lbaasv2.service.js @@ -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.')); }); diff --git a/neutron_lbaas_dashboard/static/app/core/openstack-service-api/lbaasv2.service.spec.js b/neutron_lbaas_dashboard/static/app/core/openstack-service-api/lbaasv2.service.spec.js index 77834be1..240e527f 100644 --- a/neutron_lbaas_dashboard/static/app/core/openstack-service-api/lbaasv2.service.spec.js +++ b/neutron_lbaas_dashboard/static/app/core/openstack-service-api/lbaasv2.service.spec.js @@ -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' } ] } ]; diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/associate-ip/modal.controller.js b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/associate-ip/modal.controller.js new file mode 100644 index 00000000..f8f7692a --- /dev/null +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/associate-ip/modal.controller.js @@ -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; + } + } +})(); diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/associate-ip/modal.controller.spec.js b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/associate-ip/modal.controller.spec.js new file mode 100644 index 00000000..243032ea --- /dev/null +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/associate-ip/modal.controller.spec.js @@ -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); + }); + + }); + +})(); diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/associate-ip/modal.html b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/associate-ip/modal.html new file mode 100644 index 00000000..20e214ae --- /dev/null +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/associate-ip/modal.html @@ -0,0 +1,33 @@ + + + diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/associate-ip/modal.service.js b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/associate-ip/modal.service.js new file mode 100644 index 00000000..a8f273a1 --- /dev/null +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/associate-ip/modal.service.js @@ -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; + } + + } +})(); diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/associate-ip/modal.service.spec.js b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/associate-ip/modal.service.spec.js new file mode 100644 index 00000000..0ea8308b --- /dev/null +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/associate-ip/modal.service.spec.js @@ -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(); + }); + + }); + +})(); diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/disassociate-ip/modal.service.js b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/disassociate-ip/modal.service.js new file mode 100644 index 00000000..23370121 --- /dev/null +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/disassociate-ip/modal.service.js @@ -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(); + } + + } +})(); diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/disassociate-ip/modal.service.spec.js b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/disassociate-ip/modal.service.spec.js new file mode 100644 index 00000000..772d7d53 --- /dev/null +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/disassociate-ip/modal.service.spec.js @@ -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(); + }); + + }); +})(); diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/row-actions.service.js b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/row-actions.service.js index 1aa15882..e8b6a8e2 100644 --- a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/row-actions.service.js +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/row-actions.service.js @@ -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'), diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/row-actions.service.spec.js b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/row-actions.service.spec.js index 2632f041..8211f19f 100644 --- a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/row-actions.service.spec.js +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/actions/row-actions.service.spec.js @@ -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() { diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/detail.controller.js b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/detail.controller.js index d4572f2f..a4139727 100644 --- a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/detail.controller.js +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/detail.controller.js @@ -53,7 +53,7 @@ //////////////////////////////// function init() { - api.getLoadBalancer($routeParams.loadbalancerId).success(success); + api.getLoadBalancer($routeParams.loadbalancerId, true).success(success); } function success(response) { diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/detail.controller.spec.js b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/detail.controller.spec.js index 3a39fc5f..73a03b43 100644 --- a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/detail.controller.spec.js +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/detail.controller.spec.js @@ -55,7 +55,7 @@ it('should invoke lbaasv2 apis', function() { createController(); - expect(lbaasv2API.getLoadBalancer).toHaveBeenCalledWith('1234'); + expect(lbaasv2API.getLoadBalancer).toHaveBeenCalledWith('1234', true); }); }); diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/detail.html b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/detail.html index 9d09e18e..11c3a934 100644 --- a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/detail.html +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/detail.html @@ -32,7 +32,11 @@
Admin State Up
-
{$ ::ctrl.loadbalancer.admin_state_up | yesno $}
+
{$ ctrl.loadbalancer.admin_state_up | yesno $}
+
+
+
Floating IP Address
+
{$ ctrl.loadbalancer.floating_ip.ip || 'None' | translate $}
Load Balancer ID
diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/table.controller.js b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/table.controller.js index a8db3734..170d8078 100644 --- a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/table.controller.js +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/table.controller.js @@ -57,7 +57,7 @@ //////////////////////////////// function init() { - api.getLoadBalancers().success(success); + api.getLoadBalancers(true).success(success); } function success(response) { diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/table.html b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/table.html index 1e87c127..c14406a3 100644 --- a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/table.html +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/loadbalancers/table.html @@ -117,6 +117,10 @@
Provider
{$ ::item.provider $}
+
+
Floating IP Address
+
{$ item.floating_ip.ip || 'None' | translate $}
+
Admin State Up
{$ ::item.admin_state_up | yesno $}
@@ -127,11 +131,11 @@
Subnet ID
-
{$ ::item.vip_subnet_id $}
+
{$ ::item.vip_subnet_id $}
Port ID
-
{$ ::item.vip_port_id $}
+
{$ ::item.vip_port_id $}