--- /dev/null
+# 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]
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:
LOG = logging.getLogger(__name__)
-
-
+SCHEDULER_HINTS_NAMESPACE =\
+ "http://docs.openstack.org/block-service/ext/scheduler-hints/api/v2"
FLAGS = flags.FLAGS
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 = {}
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
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,
--- /dev/null
+# 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)
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
'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)