diff --git a/horizon/static/horizon/js/angular/services/hz.api.glance.js b/horizon/static/horizon/js/angular/services/hz.api.glance.js
new file mode 100644
index 0000000000..66b0224c07
--- /dev/null
+++ b/horizon/static/horizon/js/angular/services/hz.api.glance.js
@@ -0,0 +1,159 @@
+/*
+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.
+*/
+(function () {
+ 'use strict';
+
+ /**
+ * @ngdoc service
+ * @name hz.api.glanceAPI
+ * @description Provides direct pass through to Glance with NO abstraction.
+ */
+ function GlanceAPI(apiService) {
+
+ // Images
+
+ /**
+ * @name hz.api.glanceAPI.getImage
+ * @description
+ * Get a single image by ID
+ * @param {string} id
+ * Specifies the id of the image to request.
+ */
+ this.getImage = function(id) {
+ return apiService.get('/api/glance/images/' + id)
+ .error(function () {
+ horizon.alert('error', gettext('Unable to retrieve image.'));
+ });
+ };
+
+
+ /**
+ * @name hz.api.glanceAPI.getImages
+ * @description
+ * Get a list of images.
+ *
+ * The listing result is an object with property "items". Each item is
+ * an image.
+ *
+ * @param {Object} params
+ * Query parameters. Optional.
+ *
+ * @param {boolean} params.paginate
+ * True to paginate automatically.
+ *
+ * @param {string} params.marker
+ * Specifies the image of the last-seen image.
+ *
+ * The typical pattern of limit and marker is to make an
+ * initial limited request and then to use the last
+ * image from the response as the marker parameter
+ * in a subsequent limited request. With paginate, limit
+ * is automatically set.
+ *
+ * @param {string} params.sort_dir
+ * The sort direction ('asc' or 'desc').
+ *
+ * @param {string} params.sort_key
+ * The field to sort on (for example, 'created_at').
+ * Default is created_at.
+ *
+ * @param {string} params.other
+ * Any additional request parameters will be passed through the API as
+ * filters. For example "name" : "fedora" would filter on the fedora name.
+ */
+ this.getImages = function(params) {
+ var config = (params) ? { 'params' : params} : {};
+ return apiService.get('/api/glance/images/', config)
+ .error(function () {
+ horizon.alert('error', gettext('Unable to retrieve images.'));
+ });
+ };
+
+ // Metadata Definitions - Namespaces
+
+ /**
+ * @name hz.api.glanceAPI.getNamespaces
+ * @description
+ * Get a list of metadata definition namespaces.
+ *
+ * http://docs.openstack.org/developer/glance/metadefs-concepts.html
+ *
+ * The listing result is an object with property "items". Each item is
+ * an namespace.
+ *
+ * @description
+ * Get a list of namespaces.
+ *
+ * The listing result is an object with property "items". Each item is
+ * a namespace.
+ *
+ * @param {Object} params
+ * Query parameters. Optional.
+ *
+ * @param {boolean} params.paginate
+ * True to paginate automatically.
+ *
+ * @param {string} params.marker
+ * Specifies the namespace of the last-seen namespace.
+ *
+ * The typical pattern of limit and marker is to make an
+ * initial limited request and then to use the last
+ * namespace from the response as the marker parameter
+ * in a subsequent limited request. With paginate, limit
+ * is automatically set.
+ *
+ * @param {string} params.sort_dir
+ * The sort direction ('asc' or 'desc').
+ *
+ * @param {string} params.sort_key
+ * The field to sort on (for example, 'created_at').
+ * Default is namespace.
+ *
+ * @param {string} params.other
+ * Any additional request parameters will be passed through the API as
+ * filters.
+ */
+ this.getNamespaces = function(params) {
+ var config = (params) ? { 'params' : params} : {};
+ return apiService.get('/api/glance/metadefs/namespaces/', config)
+ .error(function () {
+ horizon.alert('error', gettext('Unable to retrieve namespaces.'));
+ });
+ };
+
+ /**
+ * @name hz.api.glanceAPI.getImages
+ * @description
+ * Get a specific namespace.
+ *
+ * http://docs.openstack.org/developer/glance/metadefs-concepts.html
+ */
+ this.getNamespace = function(namespace) {
+ return apiService.get('/api/glance/metadefs/namespaces/' + namespace)
+ .error(function () {
+ horizon.alert('error', gettext('Unable to retrieve namespace.'));
+ });
+ };
+
+ }
+
+ // Register it with the API module so that anybody using the
+ // API module will have access to the Glance APIs.
+
+ angular.module('hz.api')
+ .service('glanceAPI', ['apiService', GlanceAPI]);
+
+}());
diff --git a/horizon/templates/horizon/_scripts.html b/horizon/templates/horizon/_scripts.html
index d89b255a63..ff2fc58d5b 100644
--- a/horizon/templates/horizon/_scripts.html
+++ b/horizon/templates/horizon/_scripts.html
@@ -21,6 +21,7 @@
+
diff --git a/openstack_dashboard/api/glance.py b/openstack_dashboard/api/glance.py
index 16298071f8..d0cabb4a5f 100644
--- a/openstack_dashboard/api/glance.py
+++ b/openstack_dashboard/api/glance.py
@@ -168,6 +168,9 @@ class BaseGlanceMetadefAPIResourceWrapper(base.APIResourceWrapper):
result[attr] = getattr(self, attr)
return json.dumps(result, indent=indent)
+ def to_dict(self):
+ return self._apiresource
+
class Namespace(BaseGlanceMetadefAPIResourceWrapper):
@@ -222,7 +225,8 @@ def metadefs_namespace_list(request,
typically at first deployment is done in a single transaction
giving them a potentially unpredictable sort result when using
create_at.
- :param filters: specifies addition fields to filter on such as name.
+ :param filters: specifies addition fields to filter on such as
+ resource_types.
:returns A tuple of three values:
1) Current page results
2) A boolean of whether or not there are previous page(s).
diff --git a/openstack_dashboard/api/rest/__init__.py b/openstack_dashboard/api/rest/__init__.py
index dbec207251..354b009011 100644
--- a/openstack_dashboard/api/rest/__init__.py
+++ b/openstack_dashboard/api/rest/__init__.py
@@ -22,4 +22,5 @@ in https://wiki.openstack.org/wiki/APIChangeGuidelines.
"""
# import REST API modules here
+import glance #flake8: noqa
import keystone #flake8: noqa
diff --git a/openstack_dashboard/api/rest/glance.py b/openstack_dashboard/api/rest/glance.py
new file mode 100644
index 0000000000..a152fbe7ec
--- /dev/null
+++ b/openstack_dashboard/api/rest/glance.py
@@ -0,0 +1,176 @@
+
+# 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 for the glance 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
+
+
+CLIENT_KEYWORDS = {'marker', 'sort_dir', 'sort_key', 'paginate'}
+
+
+def _parse_filters_kwargs(request):
+ """REST request parameters are separated appropriately.
+
+ Glance client processes some keywords separately
+ from filters and takes them as separate inputs.
+ This potentially may not be needed when Glance
+ v2 support is brought into Horizon via a separate effort.
+ """
+ filters = {}
+ kwargs = {}
+ for param in request.GET:
+ if param in CLIENT_KEYWORDS:
+ kwargs[param] = request.GET[param]
+ else:
+ filters[param] = request.GET[param]
+ return filters, kwargs
+
+
+@urls.register
+class Image(generic.View):
+ """API for retrieving a single image
+ """
+ url_regex = r'glance/images/(?P.+|default)$'
+
+ @rest_utils.ajax()
+ def get(self, request, image_id):
+ """Get a specific image
+
+ http://localhost/api/glance/images/cc758c90-3d98-4ea1-af44-aab405c9c915
+ """
+ return api.glance.image_get(request, image_id).to_dict()
+
+
+@urls.register
+class Images(generic.View):
+ """API for Glance images.
+ """
+ url_regex = r'glance/images/$'
+
+ @rest_utils.ajax()
+ def get(self, request):
+ """Get a list of images.
+
+ The listing result is an object with property "items". Each item is
+ an image.
+
+ Example GET:
+ http://localhost/api/glance/images?sort_dir=desc&sort_key=name&name=cirros-0.3.2-x86_64-uec #flake8: noqa
+
+ The following get parameters may be passed in the GET
+ request:
+
+ :param paginate: If true will perform pagination based on settings.
+ :param marker: Specifies the namespace of the last-seen image.
+ The typical pattern of limit and marker is to make an
+ initial limited request and then to use the last
+ namespace from the response as the marker parameter
+ in a subsequent limited request. With paginate, limit
+ is automatically set.
+ :param sort_dir: The sort direction ('asc' or 'desc').
+ :param sort_key: The field to sort on (for example, 'created_at').
+ Default is created_at.
+
+ Any additional request parameters will be passed through the API as
+ filters. There are v1/v2 complications which are being addressed as a
+ separate work stream: https://review.openstack.org/#/c/150084/
+ """
+
+ filters, kwargs = _parse_filters_kwargs(request)
+
+ images, has_more_data, has_prev_data = api.glance.image_list_detailed(
+ request, filters=filters, **kwargs)
+
+ return {
+ 'items': [i.to_dict() for i in images],
+ 'has_more_data': has_more_data,
+ 'has_prev_data': has_prev_data,
+ }
+
+
+@urls.register
+class MetadefsNamespace(generic.View):
+ """API for Glance Metadata Definitions.
+
+ http://docs.openstack.org/developer/glance/metadefs-concepts.html
+ """
+ url_regex = r'glance/metadefs/namespaces/(?P.+|default)$'
+
+ @rest_utils.ajax()
+ def get(self, request, namespace):
+ """Get a specific metadata definition namespaces.
+
+ Returns the namespace. GET params are passed through.
+
+ Example GET:
+ http://localhost/api/glance/metadefs/namespaces/OS::Compute::Watchdog
+ """
+ return api.glance.metadefs_namespace_get(request, namespace)
+
+
+@urls.register
+class MetadefsNamespaces(generic.View):
+ """API for Single Glance Metadata Definitions.
+
+ http://docs.openstack.org/developer/glance/metadefs-concepts.html
+ """
+ url_regex = r'glance/metadefs/namespaces/$'
+
+ @rest_utils.ajax()
+ def get(self, request):
+ """Get a list of metadata definition namespaces.
+
+ The listing result is an object with property "items". Each item is
+ a namespace.
+
+ Example GET:
+ http://localhost/api/glance/metadefs/namespaces?resource_types=OS::Nova::Flavor&sort_dir=desc&marker=OS::Compute::Watchdog&paginate=False&sort_key=namespace #flake8: noqa
+
+ The following get parameters may be passed in the GET
+ request:
+
+ :param paginate: If true will perform pagination based on settings.
+ :param marker: Specifies the namespace of the last-seen namespace.
+ The typical pattern of limit and marker is to make an
+ initial limited request and then to use the last
+ namespace from the response as the marker parameter
+ in a subsequent limited request. With paginate, limit
+ is automatically set.
+ :param sort_dir: The sort direction ('asc' or 'desc').
+ :param sort_key: The field to sort on (for example, 'created_at').
+ Default is namespace. The way base namespaces are loaded into
+ glance typically at first deployment is done in a single
+ transaction giving them a potentially unpredictable sort result
+ when using create_at.
+
+ Any additional request parameters will be passed through the API as
+ filters.
+ """
+
+ filters, kwargs = _parse_filters_kwargs(request)
+
+ namespaces, has_more, has_prev = api.glance.metadefs_namespace_list(
+ request, filters=filters, **kwargs)
+
+ return {
+ 'items': [n.to_dict() for n in namespaces],
+ 'has_more_data': has_more,
+ 'has_prev_data': has_prev,
+ }
diff --git a/openstack_dashboard/test/api_tests/glance_rest_tests.py b/openstack_dashboard/test/api_tests/glance_rest_tests.py
new file mode 100644
index 0000000000..c23f5c863e
--- /dev/null
+++ b/openstack_dashboard/test/api_tests/glance_rest_tests.py
@@ -0,0 +1,153 @@
+# Copyright 2015, Rackspace, US, Inc.
+# 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 mock
+
+from openstack_dashboard.api.rest import glance
+from openstack_dashboard.test.api_tests import rest_test_utils # noqa
+from openstack_dashboard.test import helpers as test
+
+
+class ImagesRestTestCase(test.APITestCase):
+ def assertStatusCode(self, response, expected_code):
+ if response.status_code == expected_code:
+ return
+ self.fail('status code %r != %r: %s' % (response.status_code,
+ expected_code,
+ response.content))
+
+ @mock.patch.object(glance.api, 'glance')
+ def test_image_get_single(self, gc):
+ request = rest_test_utils.construct_request()
+ gc.image_get.return_value.to_dict.return_value = {'name': '1'}
+
+ response = glance.Image().get(request, "1")
+ self.assertStatusCode(response, 200)
+ gc.image_get.assert_called_once_with(request, "1")
+
+ @mock.patch.object(glance.api, 'glance')
+ def test_image_get_list_detailed(self, gc):
+ kwargs = {
+ 'sort_dir': 'desc',
+ 'sort_key': 'namespace',
+ 'marker': 1,
+ 'paginate': False,
+ }
+ filters = {'name': 'fedora'}
+ request = rest_test_utils.construct_request(
+ **{'GET': dict(kwargs, **filters)})
+ gc.image_list_detailed.return_value = ([
+ mock.Mock(**{'to_dict.return_value': {'name': 'fedora'}}),
+ mock.Mock(**{'to_dict.return_value': {'name': 'cirros'}})
+ ], False, False)
+
+ response = glance.Images().get(request)
+ self.assertStatusCode(response, 200)
+ self.assertEqual(response.content,
+ '{"items": [{"name": "fedora"}, {"name": "cirros"}]'
+ ', "has_more_data": false, "has_prev_data": false}')
+ gc.image_list_detailed.assert_called_once_with(request,
+ filters=filters,
+ **kwargs)
+
+ @mock.patch.object(glance.api, 'glance')
+ def test_namespace_get_list(self, gc):
+ request = rest_test_utils.construct_request(**{'GET': {}})
+ gc.metadefs_namespace_list.return_value = ([
+ mock.Mock(**{'to_dict.return_value': {'namespace': '1'}}),
+ mock.Mock(**{'to_dict.return_value': {'namespace': '2'}})
+ ], False, False)
+
+ response = glance.MetadefsNamespaces().get(request)
+ self.assertStatusCode(response, 200)
+ self.assertEqual(response.content,
+ '{"items": [{"namespace": "1"}, {"namespace": "2"}]'
+ ', "has_more_data": false, "has_prev_data": false}')
+ gc.metadefs_namespace_list.assert_called_once_with(request,
+ filters={},
+ **{})
+
+ @mock.patch.object(glance.api, 'glance')
+ def test_namespace_get_list_kwargs_and_filters(self, gc):
+ kwargs = {
+ 'sort_dir': 'desc',
+ 'sort_key': 'namespace',
+ 'marker': 1,
+ 'paginate': False,
+ }
+ filters = {'resource_types': 'type'}
+ request = rest_test_utils.construct_request(
+ **{'GET': dict(kwargs, **filters)})
+ gc.metadefs_namespace_list.return_value = ([
+ mock.Mock(**{'to_dict.return_value': {'namespace': '1'}}),
+ mock.Mock(**{'to_dict.return_value': {'namespace': '2'}})
+ ], False, False)
+
+ response = glance.MetadefsNamespaces().get(request)
+ self.assertStatusCode(response, 200)
+ self.assertEqual(response.content,
+ '{"items": [{"namespace": "1"}, {"namespace": "2"}]'
+ ', "has_more_data": false, "has_prev_data": false}')
+ gc.metadefs_namespace_list.assert_called_once_with(request,
+ filters=filters,
+ **kwargs)
+
+ @mock.patch.object(glance.api, 'glance')
+ def test_namespace_get_namespace(self, gc):
+ kwargs = {'resource_type': ['OS::Nova::Flavor']}
+ request = rest_test_utils.construct_request(**{'GET': dict(kwargs)})
+ gc.metadefs_namespace_get.return_value\
+ .to_dict.return_value = {'namespace': '1'}
+
+ response = glance.MetadefsNamespace().get(request, "1")
+ self.assertStatusCode(response, 200)
+ gc.metadefs_namespace_get.assert_called_once_with(request,
+ "1")
+
+ def test_parse_filters_keywords(self):
+ kwargs = {
+ 'sort_dir': '1',
+ 'sort_key': '2',
+ }
+ filters = {
+ 'filter1': '1',
+ 'filter2': '2',
+ }
+
+ # Combined
+ request_params = dict(kwargs)
+ request_params.update(filters)
+ request = rest_test_utils.construct_request(
+ **{'GET': dict(request_params)})
+ output_filters, output_kwargs = glance._parse_filters_kwargs(request)
+ self.assertDictEqual(kwargs, output_kwargs)
+ self.assertDictEqual(filters, output_filters)
+
+ # Empty Filters
+ request = rest_test_utils.construct_request(**{'GET': dict(kwargs)})
+ output_filters, output_kwargs = glance._parse_filters_kwargs(request)
+ self.assertDictEqual(kwargs, output_kwargs)
+ self.assertDictEqual({}, output_filters)
+
+ # Emtpy keywords
+ request = rest_test_utils.construct_request(**{'GET': dict(filters)})
+ output_filters, output_kwargs = glance._parse_filters_kwargs(request)
+ self.assertDictEqual({}, output_kwargs)
+ self.assertDictEqual(filters, output_filters)
+
+ # Empty both
+ request = rest_test_utils.construct_request(**{'GET': dict()})
+ output_filters, output_kwargs = glance._parse_filters_kwargs(request)
+ self.assertDictEqual({}, output_kwargs)
+ self.assertDictEqual({}, output_filters)