From 463aa30b3d713921fc178f4e125742a7aad8bd01 Mon Sep 17 00:00:00 2001 From: Nikolaj Starodubtsev Date: Thu, 18 Apr 2013 16:42:35 +0400 Subject: [PATCH] Implement scheduler hints for API v2 We've done this implementation because we need to use scheduler hint in cinder with some specific filters. So, most part of code have been imported from nova. bp scheduler-hints docimpact Change-Id: I4c8a78ade4ff668d79e7aa6d0d358029754e3d90 --- cinder/api/contrib/scheduler_hints.py | 63 +++++++++++ cinder/api/openstack/wsgi.py | 10 ++ cinder/api/v2/volumes.py | 23 +++- .../tests/api/contrib/test_scheduler_hints.py | 101 ++++++++++++++++++ cinder/volume/api.py | 8 +- 5 files changed, 201 insertions(+), 4 deletions(-) create mode 100644 cinder/api/contrib/scheduler_hints.py create mode 100644 cinder/tests/api/contrib/test_scheduler_hints.py diff --git a/cinder/api/contrib/scheduler_hints.py b/cinder/api/contrib/scheduler_hints.py new file mode 100644 index 000000000..17838e51d --- /dev/null +++ b/cinder/api/contrib/scheduler_hints.py @@ -0,0 +1,63 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack Foundation +# +# 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 webob.exc + +from cinder.api import extensions +from cinder.api.openstack import wsgi +from cinder.api.v2 import volumes +from cinder.openstack.common import log as logging + +LOG = logging.getLogger(__name__) + + +class SchedulerHintsController(wsgi.Controller): + + @staticmethod + def _extract_scheduler_hints(body): + hints = {} + + attr = '%s:scheduler_hints' % Scheduler_hints.alias + try: + if attr in body: + hints.update(body[attr]) + except ValueError: + msg = _("Malformed scheduler_hints attribute") + raise webob.exc.HTTPBadRequest(reason=msg) + + return hints + + @wsgi.extends + def create(self, req, body): + hints = self._extract_scheduler_hints(body) + + if 'volume' in body: + body['volume']['scheduler_hints'] = hints + yield + + +class Scheduler_hints(extensions.ExtensionDescriptor): + """Pass arbitrary key/value pairs to the scheduler.""" + + name = "SchedulerHints" + alias = "OS-SCH-HNT" + namespace = volumes.SCHEDULER_HINTS_NAMESPACE + updated = "2013-04-18T00:00:00+00:00" + + def get_controller_extensions(self): + controller = SchedulerHintsController() + ext = extensions.ControllerExtension(self, 'volumes', controller) + return [ext] diff --git a/cinder/api/openstack/wsgi.py b/cinder/api/openstack/wsgi.py index cc882c826..810b77914 100644 --- a/cinder/api/openstack/wsgi.py +++ b/cinder/api/openstack/wsgi.py @@ -179,6 +179,16 @@ class XMLDeserializer(TextDeserializer): listnames) return result + def find_first_child_named_in_namespace(self, parent, namespace, name): + """Search a nodes children for the first child with a given name.""" + for node in parent.childNodes: + if (node.localName == name and + node.namespaceURI and + node.namespaceURI == namespace): + return node + return None + + def find_first_child_named(self, parent, name): """Search a nodes children for the first child with a given name""" for node in parent.childNodes: diff --git a/cinder/api/v2/volumes.py b/cinder/api/v2/volumes.py index bf46449d4..720b7cbe1 100644 --- a/cinder/api/v2/volumes.py +++ b/cinder/api/v2/volumes.py @@ -32,8 +32,8 @@ from cinder.volume import volume_types LOG = logging.getLogger(__name__) - - +SCHEDULER_HINTS_NAMESPACE =\ + "http://docs.openstack.org/block-service/ext/scheduler-hints/api/v2" FLAGS = flags.FLAGS @@ -92,6 +92,20 @@ class CommonDeserializer(wsgi.MetadataXMLDeserializer): metadata_deserializer = common.MetadataXMLDeserializer() + def _extract_scheduler_hints(self, volume_node): + """Marshal the scheduler hints attribute of a parsed request.""" + node = self.find_first_child_named_in_namespace(volume_node, + SCHEDULER_HINTS_NAMESPACE, "scheduler_hints") + if node: + scheduler_hints = {} + for child in self.extract_elements(node): + scheduler_hints.setdefault(child.nodeName, []) + value = self.extract_text(child).strip() + scheduler_hints[child.nodeName].append(value) + return scheduler_hints + else: + return None + def _extract_volume(self, node): """Marshal the volume attribute of a parsed request.""" volume = {} @@ -107,6 +121,10 @@ class CommonDeserializer(wsgi.MetadataXMLDeserializer): if metadata_node is not None: volume['metadata'] = self.extract_metadata(metadata_node) + scheduler_hints = self._extract_scheduler_hints(volume_node) + if scheduler_hints: + volume['scheduler_hints'] = scheduler_hints + return volume @@ -280,6 +298,7 @@ class VolumeController(wsgi.Controller): kwargs['image_id'] = image_uuid kwargs['availability_zone'] = volume.get('availability_zone', None) + kwargs['scheduler_hints'] = volume.get('scheduler_hints', None) new_volume = self.volume_api.create(context, size, diff --git a/cinder/tests/api/contrib/test_scheduler_hints.py b/cinder/tests/api/contrib/test_scheduler_hints.py new file mode 100644 index 000000000..4edc9d00c --- /dev/null +++ b/cinder/tests/api/contrib/test_scheduler_hints.py @@ -0,0 +1,101 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 OpenStack Foundation +# All Rights Reserved. +# +# 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 datetime + +import cinder +from cinder.api.openstack import wsgi +from cinder.openstack.common import jsonutils +from cinder import test +from cinder.tests.api import fakes +from cinder.tests.api.v2 import stubs + +UUID = fakes.FAKE_UUID + + +class SchedulerHintsTestCase(test.TestCase): + + def setUp(self): + super(SchedulerHintsTestCase, self).setUp() + self.fake_instance = stubs.stub_volume(1, uuid=UUID) + self.fake_instance['created_at'] =\ + datetime.datetime(2013, 1, 1, 1, 1, 1) + self.flags( + osapi_volume_extension=[ + 'cinder.api.contrib.select_extensions'], + osapi_volume_ext_list=['Scheduler_hints']) + self.app = fakes.wsgi_app() + + def test_create_server_without_hints(self): + + @wsgi.response(202) + def fake_create(*args, **kwargs): + self.assertNotIn('scheduler_hints', kwargs['body']) + return self.fake_instance + + self.stubs.Set(cinder.api.v2.volumes.VolumeController, 'create', + fake_create) + + req = fakes.HTTPRequest.blank('/v2/fake/volumes') + req.method = 'POST' + req.content_type = 'application/json' + body = {'id': id, + 'volume_type_id': 'cedef40a-ed67-4d10-800e-17455edce175', + 'volume_id': '1', + } + req.body = jsonutils.dumps(body) + res = req.get_response(self.app) + self.assertEqual(202, res.status_int) + + def test_create_server_with_hints(self): + + @wsgi.response(202) + def fake_create(*args, **kwargs): + self.assertIn('scheduler_hints', kwargs['body']) + self.assertEqual(kwargs['body']['scheduler_hints'], {"a": "b"}) + return self.fake_instance + + self.stubs.Set(cinder.api.v2.volumes.VolumeController, 'create', + fake_create) + + req = fakes.HTTPRequest.blank('/v2/fake/volumes') + req.method = 'POST' + req.content_type = 'application/json' + body = {'id': id, + 'volume_type_id': 'cedef40a-ed67-4d10-800e-17455edce175', + 'volume_id': '1', + 'scheduler_hints': {'a': 'b'}, + } + + req.body = jsonutils.dumps(body) + res = req.get_response(self.app) + self.assertEqual(202, res.status_int) + + def test_create_server_bad_hints(self): + req = fakes.HTTPRequest.blank('/v2/fake/volumes') + req.method = 'POST' + req.content_type = 'application/json' + body = {'volume': { + 'id': id, + 'volume_type_id': 'cedef40a-ed67-4d10-800e-17455edce175', + 'volume_id': '1', + 'scheduler_hints': 'a', } + } + + req.body = jsonutils.dumps(body) + res = req.get_response(self.app) + self.assertEqual(400, res.status_int) diff --git a/cinder/volume/api.py b/cinder/volume/api.py index 042ae6802..68550b5e3 100644 --- a/cinder/volume/api.py +++ b/cinder/volume/api.py @@ -87,7 +87,8 @@ class API(base.Base): def create(self, context, size, name, description, snapshot=None, image_id=None, volume_type=None, metadata=None, - availability_zone=None, source_volume=None): + availability_zone=None, source_volume=None, + scheduler_hints=None): exclusive_options = (snapshot, image_id, source_volume) exclusive_options_set = sum(1 for option in @@ -223,7 +224,10 @@ class API(base.Base): 'image_id': image_id, 'source_volid': volume['source_volid']} - filter_properties = {} + if scheduler_hints: + filter_properties = {'scheduler_hints': scheduler_hints} + else: + filter_properties = {} self._cast_create_volume(context, request_spec, filter_properties) -- 2.45.2