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')