From 1126054cf332ef9445f2759d25cc615064a1a669 Mon Sep 17 00:00:00 2001 From: Travis Tripp Date: Thu, 29 Jan 2015 13:28:48 -0700 Subject: [PATCH] Glance Rest API for Angular Front End This is the glance direct / non-abstracted rest API for the angular front end. This initial patch will only implement the APIs needed to support the launch instance work. This was refactored out of change: 141273 All v1 / v2 work is being done separately in change: 150084 Based on the work done for Keystone here: https://review.openstack.org/#/c/150636 Partially Implements: blueprint launch-instance-redesign Co-Authored-By: Richard Jones Change-Id: I28735e242d610235695b1070a15f739e0d50b696 --- .../js/angular/services/hz.api.glance.js | 159 ++++++++++++++++ horizon/templates/horizon/_scripts.html | 1 + openstack_dashboard/api/glance.py | 6 +- openstack_dashboard/api/rest/__init__.py | 1 + openstack_dashboard/api/rest/glance.py | 176 ++++++++++++++++++ .../test/api_tests/glance_rest_tests.py | 153 +++++++++++++++ 6 files changed, 495 insertions(+), 1 deletion(-) create mode 100644 horizon/static/horizon/js/angular/services/hz.api.glance.js create mode 100644 openstack_dashboard/api/rest/glance.py create mode 100644 openstack_dashboard/test/api_tests/glance_rest_tests.py 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)