Merge "Implement scheduler hints for API v2"

This commit is contained in:
Jenkins 2013-05-25 16:07:42 +00:00 committed by Gerrit Code Review
commit e3136a37a6
5 changed files with 201 additions and 4 deletions
cinder
api
tests/api/contrib
volume

@ -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]

@ -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:

@ -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,

@ -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)

@ -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)