From 062bc25b68aeafe175d5bdb3a2f54233794a5f78 Mon Sep 17 00:00:00 2001 From: Justin Pomeroy Date: Tue, 16 Feb 2016 16:08:51 -0600 Subject: [PATCH] Add REST API for working with floating IPs This adds the REST APIs for certain floating IP actions such as listing floating IP addresses and pools, allocating new floating IPs and associating and disassociating floating IPs. Related to blueprint horizon-lbaas-v2-ui Change-Id: I80e550a04b0a0f2ad05203180781013ba5501472 --- openstack_dashboard/api/rest/network.py | 76 ++++++++++ .../openstack-service-api/network.service.js | 132 ++++++++++++++++++ .../network.service.spec.js | 89 ++++++++++++ .../test/api_tests/network_rest_tests.py | 71 ++++++++++ 4 files changed, 368 insertions(+) create mode 100644 openstack_dashboard/static/app/core/openstack-service-api/network.service.js create mode 100644 openstack_dashboard/static/app/core/openstack-service-api/network.service.spec.js diff --git a/openstack_dashboard/api/rest/network.py b/openstack_dashboard/api/rest/network.py index 4dd602015a..8a20fbb4e6 100644 --- a/openstack_dashboard/api/rest/network.py +++ b/openstack_dashboard/api/rest/network.py @@ -1,5 +1,6 @@ # Copyright 2015, Hewlett-Packard Development Company, L.P. +# 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. @@ -44,3 +45,78 @@ class SecurityGroups(generic.View): security_groups = api.network.security_group_list(request) return {'items': [sg.to_dict() for sg in security_groups]} + + +@urls.register +class FloatingIP(generic.View): + """API for a single floating IP address. + """ + url_regex = r'network/floatingip/$' + + @rest_utils.ajax(data_required=True) + def post(self, request): + """Allocate a new floating IP address. + + :param pool_id: The ID of the floating IP address pool in which to + allocate the new address. + + :return: JSON representation of the new floating IP address + """ + pool = request.DATA['pool_id'] + result = api.network.tenant_floating_ip_allocate(request, pool) + return result.to_dict() + + @rest_utils.ajax(data_required=True) + def patch(self, request): + """Associate or disassociate a floating IP address. + + :param address_id: The ID of the floating IP address to associate + or disassociate. + :param port_id: The ID of the port to associate. + """ + address = request.DATA['address_id'] + port = request.DATA.get('port_id') + if port is None: + api.network.floating_ip_disassociate(request, address) + else: + api.network.floating_ip_associate(request, address, port) + + +@urls.register +class FloatingIPs(generic.View): + """API for floating IP addresses. + """ + url_regex = r'network/floatingips/$' + + @rest_utils.ajax() + def get(self, request): + """Get a list of floating IP addresses. + + The listing result is an object with property "items". Each item is + an extension. + + Example: + http://localhost/api/network/floatingips + """ + result = api.network.tenant_floating_ip_list(request) + return {'items': [ip.to_dict() for ip in result]} + + +@urls.register +class FloatingIPPools(generic.View): + """API for floating IP pools. + """ + url_regex = r'network/floatingippools/$' + + @rest_utils.ajax() + def get(self, request): + """Get a list of floating IP pools. + + The listing result is an object with property "items". Each item is + an extension. + + Example: + http://localhost/api/network/floatingippools + """ + result = api.network.floating_ip_pools_list(request) + return {'items': [p.to_dict() for p in result]} diff --git a/openstack_dashboard/static/app/core/openstack-service-api/network.service.js b/openstack_dashboard/static/app/core/openstack-service-api/network.service.js new file mode 100644 index 0000000000..a394a98902 --- /dev/null +++ b/openstack_dashboard/static/app/core/openstack-service-api/network.service.js @@ -0,0 +1,132 @@ +/** + * 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.app.core.openstack-service-api') + .factory('horizon.app.core.openstack-service-api.network', networkAPI); + + networkAPI.$inject = [ + 'horizon.framework.util.http.service', + 'horizon.framework.widgets.toast.service' + ]; + + /** + * @ngdoc service + * @name horizon.app.core.openstack-service-api.network + * @description Provides access to APIs that are common to nova network + * and neutron. + */ + function networkAPI(apiService, toastService) { + var service = { + getFloatingIps: getFloatingIps, + getFloatingIpPools: getFloatingIpPools, + allocateFloatingIp: allocateFloatingIp, + associateFloatingIp: associateFloatingIp, + disassociateFloatingIp: disassociateFloatingIp + }; + + return service; + + ///////////// + + // Floating IPs + + /** + * @name horizon.app.core.openstack-service-api.networkAPI.getFloatingIps + * @description + * Get a list of floating IP addresses. + * + * The listing result is an object with property "items". Each item is + * a floating IP address. + */ + function getFloatingIps() { + return apiService.get('/api/network/floatingips/') + .error(function () { + toastService.add('error', gettext('Unable to retrieve floating IPs.')); + }); + } + + /** + * @name horizon.app.core.openstack-service-api.networkAPI.getFloatingIpPools + * @description + * Get a list of floating IP pools. + * + * The listing result is an object with property "items". Each item is + * a floating IP address. + */ + function getFloatingIpPools() { + return apiService.get('/api/network/floatingippools/') + .error(function () { + toastService.add('error', gettext('Unable to retrieve floating IP pools.')); + }); + } + + /** + * @name horizon.app.core.openstack-service-api.networkAPI.allocateFloatingIp + * @description + * Allocate a floating IP address within a pool. + * + * @param {string} poolId + * The Id of the pool in which to allocate the new floating IP address. + * + * Returns the new floating IP address on success. + */ + function allocateFloatingIp(poolId) { + return apiService.post('/api/network/floatingip/', { pool_id: poolId }) + .error(function () { + toastService.add('error', gettext('Unable to allocate new floating IP address.')); + }); + } + + /** + * @name horizon.app.core.openstack-service-api.networkAPI.associateFloatingIp + * @description + * Associate a floating IP address with a port. + * + * @param {string} addressId + * The Id of the floating IP address to associate. + * + * @param {string} portId + * The Id of the port to associate. + */ + function associateFloatingIp(addressId, portId) { + var params = { address_id: addressId, port_id: portId }; + return apiService.patch('/api/network/floatingip/', params) + .error(function () { + toastService.add('error', gettext('Unable to associate floating IP address.')); + }); + } + + /** + * @name horizon.app.core.openstack-service-api.networkAPI.disassociateFloatingIp + * @description + * Disassociate a floating IP address. + * + * @param {string} addressId + * The Id of the floating IP address to disassociate. + */ + function disassociateFloatingIp(addressId) { + return apiService.patch('/api/network/floatingip/', { address_id: addressId }) + .error(function () { + toastService.add('error', gettext('Unable to disassociate floating IP address.')); + }); + } + + } + +}()); diff --git a/openstack_dashboard/static/app/core/openstack-service-api/network.service.spec.js b/openstack_dashboard/static/app/core/openstack-service-api/network.service.spec.js new file mode 100644 index 0000000000..913795121a --- /dev/null +++ b/openstack_dashboard/static/app/core/openstack-service-api/network.service.spec.js @@ -0,0 +1,89 @@ +/** + * 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('Network API', function() { + var testCall, service; + var apiService = {}; + var toastService = {}; + + beforeEach(function() { + module('horizon.mock.openstack-service-api', function($provide, initServices) { + testCall = initServices($provide, apiService, toastService); + }); + + module('horizon.app.core.openstack-service-api'); + + inject(['horizon.app.core.openstack-service-api.network', function(networkAPI) { + service = networkAPI; + }]); + }); + + it('defines the service', function() { + expect(service).toBeDefined(); + }); + + var tests = [ + { + func: 'getFloatingIps', + method: 'get', + path: '/api/network/floatingips/', + error: 'Unable to retrieve floating IPs.' + }, + { + func: 'getFloatingIpPools', + method: 'get', + path: '/api/network/floatingippools/', + error: 'Unable to retrieve floating IP pools.' + }, + { + func: 'allocateFloatingIp', + method: 'post', + path: '/api/network/floatingip/', + data: { pool_id: 'pool' }, + error: 'Unable to allocate new floating IP address.', + testInput: [ 'pool' ] + }, + { + func: 'associateFloatingIp', + method: 'patch', + path: '/api/network/floatingip/', + data: { address_id: 'address', port_id: 'port' }, + error: 'Unable to associate floating IP address.', + testInput: [ 'address', 'port' ] + }, + { + func: 'disassociateFloatingIp', + method: 'patch', + path: '/api/network/floatingip/', + data: { address_id: 'address' }, + error: 'Unable to disassociate floating IP address.', + testInput: [ 'address' ] + } + ]; + + // Iterate through the defined tests and apply as Jasmine specs. + angular.forEach(tests, function(params) { + it('defines the ' + params.func + ' call properly', function() { + var callParams = [apiService, service, toastService, params]; + testCall.apply(this, callParams); + }); + }); + + }); + +})(); diff --git a/openstack_dashboard/test/api_tests/network_rest_tests.py b/openstack_dashboard/test/api_tests/network_rest_tests.py index 2520ae4751..40e43cc1df 100644 --- a/openstack_dashboard/test/api_tests/network_rest_tests.py +++ b/openstack_dashboard/test/api_tests/network_rest_tests.py @@ -1,4 +1,5 @@ # Copyright 2015, Hewlett-Packard Development Company, L.P. +# 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. @@ -31,3 +32,73 @@ class RestNetworkApiSecurityGroupTests(test.TestCase): self.assertEqual(response.json, {"items": [{"name": "default"}]}) client.security_group_list.assert_called_once_with(request) + + +class RestNetworkApiFloatingIpTests(test.TestCase): + + @mock.patch.object(network.api, 'network') + def test_floating_ip_list(self, client): + request = self.mock_rest_request() + client.tenant_floating_ip_list.return_value = ([ + mock.Mock(**{'to_dict.return_value': {'ip': '1.2.3.4'}}), + mock.Mock(**{'to_dict.return_value': {'ip': '2.3.4.5'}}) + ]) + + response = network.FloatingIPs().get(request) + self.assertStatusCode(response, 200) + self.assertEqual(response.json, + {'items': [{'ip': '1.2.3.4'}, {'ip': '2.3.4.5'}]}) + client.tenant_floating_ip_list.assert_called_once_with(request) + + @mock.patch.object(network.api, 'network') + def test_floating_ip_pool_list(self, client): + request = self.mock_rest_request() + client.floating_ip_pools_list.return_value = ([ + mock.Mock(**{'to_dict.return_value': {'name': '1'}}), + mock.Mock(**{'to_dict.return_value': {'name': '2'}}) + ]) + + response = network.FloatingIPPools().get(request) + self.assertStatusCode(response, 200) + self.assertEqual(response.json, + {'items': [{'name': '1'}, {'name': '2'}]}) + client.floating_ip_pools_list.assert_called_once_with(request) + + @mock.patch.object(network.api, 'network') + def test_allocate_floating_ip(self, client): + request = self.mock_rest_request( + body='{"pool_id": "pool"}' + ) + client.tenant_floating_ip_allocate.return_value = ( + mock.Mock(**{'to_dict.return_value': {'ip': '1.2.3.4'}}) + ) + + response = network.FloatingIP().post(request) + self.assertStatusCode(response, 200) + self.assertEqual(response.json, + {'ip': '1.2.3.4'}) + client.tenant_floating_ip_allocate.assert_called_once_with(request, + 'pool') + + @mock.patch.object(network.api, 'network') + def test_associate_floating_ip(self, client): + request = self.mock_rest_request( + body='{"address_id": "address", "port_id": "port"}' + ) + + response = network.FloatingIP().patch(request) + self.assertStatusCode(response, 204) + client.floating_ip_associate.assert_called_once_with(request, + 'address', + 'port') + + @mock.patch.object(network.api, 'network') + def test_disassociate_floating_ip(self, client): + request = self.mock_rest_request( + body='{"address_id": "address"}' + ) + + response = network.FloatingIP().patch(request) + self.assertStatusCode(response, 204) + client.floating_ip_disassociate.assert_called_once_with(request, + 'address')