Merge "Implement scheduler hints for API v2"
This commit is contained in:
commit
e3136a37a6
cinder
api
tests/api/contrib
volume
63
cinder/api/contrib/scheduler_hints.py
Normal file
63
cinder/api/contrib/scheduler_hints.py
Normal file
@ -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,
|
||||
|
101
cinder/tests/api/contrib/test_scheduler_hints.py
Normal file
101
cinder/tests/api/contrib/test_scheduler_hints.py
Normal file
@ -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)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user