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 = {