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 @@
+
+
+
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.
+
+
+
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 $}