diff --git a/horizon/static/horizon/js/angular/services/hz.api.neutron.js b/horizon/static/horizon/js/angular/services/hz.api.neutron.js new file mode 100644 index 0000000000..e55889d179 --- /dev/null +++ b/horizon/static/horizon/js/angular/services/hz.api.neutron.js @@ -0,0 +1,204 @@ +/** + * Copyright 2015 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'; + + /** + * @ngdoc service + * @name hz.api.NeutronAPI + * @description Provides access to Neutron APIs. + */ + function NeutronAPI(apiService) { + + // Networks + + /** + * @name hz.api.neturonAPI.getNetworks + * @description + * Get a list of networks for a tenant. + * + * The listing result is an object with property "items". Each item is + * a network. + */ + this.getNetworks = function() { + return apiService.get('/api/neutron/networks/') + .error(function () { + horizon.alert('error', gettext('Unable to retrieve networks.')); + }); + }; + + /** + * @name hz.api.neutronAPI.createNetwork + * @description + * Create a new network. + * @returns The new network object on success. + * + * @param {Object} newNetwork + * The network to create. Required. + * + * Example new network object + * { + * "network": { + * "name": "myNewNetwork", + * "admin_state_up": true, + * "net_profile_id" : "asdsarafssdaser", + * "shared": true, + * "tenant_id": "4fd44f30292945e481c7b8a0c8908869 + * } + * } + * + * Description of properties on the network object + * + * @property {string} newNetwork.name + * The name of the new network. Optional. + * + * @property {boolean} newNetwork.admin_state_up + * The administrative state of the network, which is up (true) or + * down (false). Optional. + * + * @property {string} newNetwork.net_profile_id + * The network profile id. Optional. + * + * @property {boolean} newNetwork.shared + * Indicates whether this network is shared across all tenants. + * By default, only adminstative users can change this value. Optional. + * + * @property {string} newNetwork.tenant_id + * The UUID of the tenant that will own the network. This tenant can + * be different from the tenant that makes the create network request. + * However, only administative users can specify a tenant ID other than + * their own. You cannot change this value through authorization + * policies. Optional. + * + */ + this.createNetwork = function(newNetwork) { + return apiService.post('/api/neutron/networks/', newNetwork) + .error(function () { + horizon.alert('error', gettext('Unable to create the network.')); + }); + }; + + // Subnets + + /** + * @name hz.api.neutronAPI.getSubnets + * @description + * Get a list of subnets for a network. + * + * The listing result is an object with property "items". Each item is + * a subnet. + * + * @param {string} network_id + * The network id to retrieve subnets for. Required. + */ + this.getSubnets = function(network_id) { + return apiService.get('/api/neutron/subnets/', network_id) + .error(function () { + horizon.alert('error', gettext('Unable to retrieve subnets.')); + }); + }; + + /** + * @name hz.api.neutronAPI.createSubnet + * @description + * Create a Subnet for given Network. + * @returns The JSON representation of Subnet on success. + * + * @param {Object} newSubnet + * The subnet to create. + * + * Example new subnet object + * { + * "subnet": { + * "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", + * "ip_version": 4, + * "cidr": "192.168.199.0/24", + * "name": "mySubnet", + * "tenant_id": "4fd44f30292945e481c7b8a0c8908869, + * "allocation_pools": [ + * { + * "start": "192.168.199.2", + * "end": "192.168.199.254" + * } + * ], + * "gateway_ip": "192.168.199.1", + * "id": "abce", + * "enable_dhcp": true, + * } + * } + * + * Description of properties on the subnet object + * @property {string} newSubnet.network_id + * The id of the attached network. Required. + * + * @property {number} newSubnet.ip_version + * The IP version, which is 4 or 6. Required. + * + * @property {string} newSubnet.cidr + * The CIDR. Required. + * + * @property {string} newSubnet.name + * The name of the new subnet. Optional. + * + * @property {string} newSubnet.tenant_id + * The ID of the tenant who owns the network. Only administrative users + * can specify a tenant ID other than their own. Optional. + * + * @property {string|Array} newSubnet.allocation_pools + * The start and end addresses for the allocation pools. Optional. + * + * @property {string} newSubnet.gateway_ip + * The gateway IP address. Optional. + * + * @property {string} newSubnet.id + * The ID of the subnet. Optional. + * + * @property {boolean} newSubnet.enable_dhcp + * Set to true if DHCP is enabled and false if DHCP is disabled. Optional. + * + */ + this.createSubnet = function(newSubnet) { + return apiService.post('/api/neutron/subnets/', newSubnet) + .error(function () { + horizon.alert('error', gettext('Unable to create the subnet.')); + }); + }; + + // Ports + + /** + * @name hz.api.neutronAPI.getPorts + * @description + * Get a list of ports for a network. + * + * The listing result is an object with property "items". Each item is + * a port. + * + * @param {string} network_id + * The network id to retrieve ports for. Required. + */ + this.getPorts = function(network_id) { + return apiService.get('/api/neutron/ports/', network_id) + .error(function () { + horizon.alert('error', gettext('Unable to retrieve ports.')); + }); + }; + + } + + angular.module('hz.api') + .service('neutronAPI', ['apiService', NeutronAPI]); +}()); diff --git a/horizon/templates/horizon/_scripts.html b/horizon/templates/horizon/_scripts.html index 03a7d5c7e9..089842544d 100644 --- a/horizon/templates/horizon/_scripts.html +++ b/horizon/templates/horizon/_scripts.html @@ -26,6 +26,7 @@ + diff --git a/openstack_dashboard/api/base.py b/openstack_dashboard/api/base.py index f9ebf83c70..932d64a7ef 100644 --- a/openstack_dashboard/api/base.py +++ b/openstack_dashboard/api/base.py @@ -170,6 +170,9 @@ class APIDictWrapper(object): def __repr__(self): return "<%s: %s>" % (self.__class__.__name__, self._apidict) + def to_dict(self): + return self._apidict + class Quota(object): """Wrapper for individual limits in a quota.""" diff --git a/openstack_dashboard/api/neutron.py b/openstack_dashboard/api/neutron.py index d6f13892a2..21dcd2d80d 100644 --- a/openstack_dashboard/api/neutron.py +++ b/openstack_dashboard/api/neutron.py @@ -94,6 +94,11 @@ class Network(NeutronAPIDictWrapper): apiresource['__'.join(key.split(':'))] = apiresource[key] super(Network, self).__init__(apiresource) + def to_dict(self): + d = dict(super(NeutronAPIDictWrapper, self).to_dict()) + d['subnets'] = [s.to_dict() for s in d['subnets']] + return d + class Subnet(NeutronAPIDictWrapper): """Wrapper for neutron subnets.""" diff --git a/openstack_dashboard/api/rest/__init__.py b/openstack_dashboard/api/rest/__init__.py index 91b0a4c181..37a80858aa 100644 --- a/openstack_dashboard/api/rest/__init__.py +++ b/openstack_dashboard/api/rest/__init__.py @@ -27,5 +27,6 @@ import config #flake8: noqa import glance #flake8: noqa import keystone #flake8: noqa import network #flake8: noqa +import neutron #flake8: noqa import nova #flake8: noqa import policy #flake8: noqa diff --git a/openstack_dashboard/api/rest/neutron.py b/openstack_dashboard/api/rest/neutron.py new file mode 100644 index 0000000000..9150954c06 --- /dev/null +++ b/openstack_dashboard/api/rest/neutron.py @@ -0,0 +1,136 @@ +# +# (c) Copyright 2015 Hewlett-Packard Development Company, L.P. +# +# 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. +"""API over the neutron service. +""" + +from django.views import generic + +from openstack_dashboard import api +from openstack_dashboard.api.rest import utils as rest_utils + +from openstack_dashboard.api.rest import urls + + +@urls.register +class Networks(generic.View): + """API for Neutron Networks + http://developer.openstack.org/api-ref-networking-v2.html + """ + url_regex = r'neutron/networks/$' + + @rest_utils.ajax() + def get(self, request): + """Get a list of networks for a project + + The listing result is an object with property "items". Each item is + a network. + """ + tenant_id = request.user.tenant_id + result = api.neutron.network_list_for_tenant(request, tenant_id) + return{'items': [n.to_dict() for n in result]} + + @rest_utils.ajax(data_required=True) + def post(self, request): + """Create a network + :param admin_state_up (optional): The administrative state of the + network, which is up (true) or down (false). + :param name (optional): The network name. A request body is optional: + If you include it, it can specify this optional attribute. + :param net_profile_id (optional): network profile id + :param shared (optional): Indicates whether this network is shared + across all tenants. By default, only administrative users can + change this value. + :param tenant_id (optional): Admin-only. The UUID of the tenant that + will own the network. This tenant can be different from the + tenant that makes the create network request. However, only + administrative users can specify a tenant ID other than their + own. You cannot change this value through authorization + policies. + + :return: JSON representation of a Network + """ + if not api.neutron.is_port_profiles_supported(): + request.DATA.pop("net_profile_id", None) + new_network = api.neutron.network_create(request, **request.DATA) + return rest_utils.CreatedResponse( + '/api/neutron/networks/%s' % new_network.id, + new_network.to_dict() + ) + + +@urls.register +class Subnets(generic.View): + """API for Neutron SubNets + http://developer.openstack.org/api-ref-networking-v2.html#subnets + """ + url_regex = r'neutron/subnets/$' + + @rest_utils.ajax() + def get(self, request): + """Get a list of subnets for a project + + The listing result is an object with property "items". Each item is + a subnet. + + """ + result = api.neutron.subnet_list(request, **request.GET) + return{'items': [n.to_dict() for n in result]} + + @rest_utils.ajax(data_required=True) + def post(self, request): + """Create a Subnet for a given Network + + :param name (optional): The subnet name. + :param network_id: The ID of the attached network. + :param tenant_id (optional): The ID of the tenant who owns the network. + Only administrative users can specify a tenant ID other than + their own. + :param allocation_pools (optional): The start and end addresses for the + allocation pools. + :param gateway_ip (optional): The gateway IP address. + :param ip_version: The IP version, which is 4 or 6. + :param cidr: The CIDR. + :param id (optional): The ID of the subnet. + :param enable_dhcp (optional): Set to true if DHCP is enabled and false + if DHCP is disabled. + + :return: JSON representation of a Subnet + + """ + new_subnet = api.neutron.subnet_create(request, **request.DATA) + return rest_utils.CreatedResponse( + '/api/neutron/subnets/%s' % new_subnet.id, + new_subnet.to_dict() + ) + + +@urls.register +class Ports(generic.View): + """API for Neutron Ports + http://developer.openstack.org/api-ref-networking-v2.html#ports + """ + url_regex = r'neutron/ports/$' + + @rest_utils.ajax() + def get(self, request): + """Get a list of ports for a network + + The listing result is an object with property "items". Each item is + a subnet. + """ + # see + # https://github.com/openstack/neutron/blob/master/neutron/api/v2/attributes.py + result = api.neutron.port_list(request, **request.GET) + return{'items': [n.to_dict() for n in result]} diff --git a/openstack_dashboard/api/rest/utils.py b/openstack_dashboard/api/rest/utils.py index a08a01bf81..b676600456 100644 --- a/openstack_dashboard/api/rest/utils.py +++ b/openstack_dashboard/api/rest/utils.py @@ -121,8 +121,11 @@ def ajax(authenticated=True, data_required=False): # exception was raised with a specific HTTP status if hasattr(e, 'http_status'): http_status = e.http_status - else: + elif hasattr(e, 'code'): http_status = e.code + else: + log.exception('HTTP exception with no status/code') + return JSONResponse(str(e), 500) return JSONResponse(str(e), http_status) except Exception as e: log.exception('error invoking apiclient') diff --git a/openstack_dashboard/test/api_tests/neutron_rest_tests.py b/openstack_dashboard/test/api_tests/neutron_rest_tests.py new file mode 100644 index 0000000000..8a064b2e41 --- /dev/null +++ b/openstack_dashboard/test/api_tests/neutron_rest_tests.py @@ -0,0 +1,134 @@ +# +# (c) Copyright 2015 Hewlett-Packard Development Company, L.P. +# +# 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. +import json + +import mock + +from openstack_dashboard.api.rest import neutron +from openstack_dashboard.test.test_data import neutron_data +from openstack_dashboard.test.test_data.utils import TestData # noqa + +from openstack_dashboard.test import helpers as test + + +TEST = TestData(neutron_data.data) + + +class NeutronNetworksTestCase(test.TestCase): + @classmethod + def setUpClass(cls): + cls._networks = [mock_factory(n) + for n in TEST.api_networks.list()] + + @mock.patch.object(neutron.api, 'neutron') + def test_get_list_for_tenant(self, client): + request = self.mock_rest_request() + networks = self._networks + client.network_list_for_tenant.return_value = networks + response = neutron.Networks().get(request) + self.assertStatusCode(response, 200) + self.assertItemsCollectionEqual(response, TEST.api_networks.list()) + client.network_list_for_tenant.assert_called_once_with( + request, request.user.tenant_id) + + @mock.patch.object(neutron.api, 'neutron') + def test_create(self, client): + self._test_create( + '{"name": "mynetwork"}', + {'name': 'mynetwork'} + ) + + @mock.patch.object(neutron.api, 'neutron') + def test_create_with_bogus_param(self, client): + self._test_create( + '{"name": "mynetwork","bilbo":"baggins"}', + {'name': 'mynetwork'} + ) + + @mock.patch.object(neutron.api, 'neutron') + def _test_create(self, supplied_body, expected_call, client): + request = self.mock_rest_request(body=supplied_body) + client.network_create.return_value = self._networks[0] + response = neutron.Networks().post(request) + self.assertStatusCode(response, 201) + self.assertEqual(response['location'], + '/api/neutron/networks/' + + str(TEST.api_networks.first().get("id"))) + self.assertEqual(response.content, + json.dumps(TEST.api_networks.first())) + + +class NeutronSubnetsTestCase(test.TestCase): + @classmethod + def setUpClass(cls): + cls._networks = [mock_factory(n) + for n in TEST.api_networks.list()] + cls._subnets = [mock_factory(n) + for n in TEST.api_subnets.list()] + + @mock.patch.object(neutron.api, 'neutron') + def test_get(self, client): + request = self.mock_rest_request( + GET={"network_id": self._networks[0].id}) + client.subnet_list.return_value = [self._subnets[0]] + response = neutron.Subnets().get(request) + self.assertStatusCode(response, 200) + client.subnet_list.assert_called_once_with( + request, network_id=TEST.api_networks.first().get("id")) + + @mock.patch.object(neutron.api, 'neutron') + def test_create(self, client): + request = self.mock_rest_request( + body='{"network_id": "%s",' + ' "ip_version": "4",' + ' "cidr": "192.168.199.0/24"}' % self._networks[0].id) + client.subnet_create.return_value = self._subnets[0] + response = neutron.Subnets().post(request) + self.assertStatusCode(response, 201) + self.assertEqual(response['location'], + '/api/neutron/subnets/' + + str(TEST.api_subnets.first().get("id"))) + self.assertEqual(response.content, + json.dumps(TEST.api_subnets.first())) + + +class NeutronPortsTestCase(test.TestCase): + @classmethod + def setUpClass(cls): + cls._networks = [mock_factory(n) + for n in TEST.api_networks.list()] + cls._ports = [mock_factory(n) + for n in TEST.api_ports.list()] + + @mock.patch.object(neutron.api, 'neutron') + def test_get(self, client): + request = self.mock_rest_request( + GET={"network_id": self._networks[0].id}) + client.port_list.return_value = [self._ports[0]] + response = neutron.Ports().get(request) + self.assertStatusCode(response, 200) + client.port_list.assert_called_once_with( + request, network_id=TEST.api_networks.first().get("id")) + + +def mock_obj_to_dict(r): + return mock.Mock(**{'to_dict.return_value': r}) + + +def mock_factory(r): + """mocks all the attributes as well as the to_dict """ + mocked = mock_obj_to_dict(r) + mocked.configure_mock(**r) + return mocked diff --git a/openstack_dashboard/test/helpers.py b/openstack_dashboard/test/helpers.py index 255169a76d..50d2afb14b 100644 --- a/openstack_dashboard/test/helpers.py +++ b/openstack_dashboard/test/helpers.py @@ -19,8 +19,10 @@ import collections import copy from functools import wraps # noqa +import json import os + from ceilometerclient.v2 import client as ceilometer_client from cinderclient import client as cinder_client from django.conf import settings @@ -278,6 +280,10 @@ class TestCase(horizon_helpers.TestCase): expected_code, response.content)) + def assertItemsCollectionEqual(self, response, items_list): + self.assertEqual(response.content, + '{"items": ' + json.dumps(items_list) + "}") + @staticmethod def mock_rest_request(**args): mock_args = {