Strict ImageRef validation to UUID only

Currently imageRef in server create, rebuild and rescue
operation can be accepted as random url which contains image
UUID and fetch the UUID from that.

As /images proxy APIs are deprecated, and ImageRef in
server creation etc are UUID only and valid against glance.

This patch makes imageRef handling as UUID only and
return 400 if non UUID are requested.

NOTE- Previously nova use to allow the empty string which was
      ok in case of boot from volume.
      We will keep the same behavior of allowing empty string in case of
      boot from volume only and 400 in all other case.

Closes-Bug: #1607229

Change-Id: I49f4da62c1b5b3fd8c5f67039ae113f76722b26c
This commit is contained in:
ghanshyam 2016-07-28 16:11:47 +09:00 committed by Ghanshyam Mann
parent e3211934d4
commit cbd3ec476f
16 changed files with 197 additions and 199 deletions

View File

@ -2262,8 +2262,10 @@ image_status:
type: string type: string
imageRef: imageRef:
description: | description: |
The image reference, as a UUID or full URL, for the image to use for your server The UUID of the image to use for your server instance.
instance. This is not required in case of boot from volume.
In all other cases it is required and must be a valid UUID
otherwise API will return 400.
in: body in: body
required: true required: true
type: string type: string

View File

@ -4,7 +4,7 @@
"accessIPv6": "80fe::", "accessIPv6": "80fe::",
"name" : "new-server-test", "name" : "new-server-test",
"description" : "new-server-description", "description" : "new-server-description",
"imageRef" : "http://glance.openstack.example.com/images/70a599e0-31e7-49b7-b260-868f441e862b", "imageRef" : "70a599e0-31e7-49b7-b260-868f441e862b",
"flavorRef" : "http://openstack.example.com/flavors/1", "flavorRef" : "http://openstack.example.com/flavors/1",
"metadata" : { "metadata" : {
"My Server Name" : "Apache1" "My Server Name" : "Apache1"

View File

@ -20,7 +20,6 @@ import re
from oslo_log import log as logging from oslo_log import log as logging
from oslo_utils import strutils from oslo_utils import strutils
from oslo_utils import uuidutils
import six import six
import six.moves.urllib.parse as urlparse import six.moves.urllib.parse as urlparse
import webob import webob
@ -548,41 +547,3 @@ def is_all_tenants(search_opts):
# The empty string is considered enabling all_tenants # The empty string is considered enabling all_tenants
all_tenants = 'all_tenants' in search_opts all_tenants = 'all_tenants' in search_opts
return all_tenants return all_tenants
def image_uuid_from_href(image_href, key):
"""If the image href was generated by nova api, strip image_href down to an
id and use the default glance connection params
:param image_href: URL or UUID for an image.
:param key: The request body key for the image_href value. This is used in
error messages.
:returns: The parsed image UUID.
:raises: webob.exc.HTTPBadRequest if the image_href does not have a valid
image UUID in it.
"""
if not image_href:
# NOTE(mriedem): This error message is copied from how our jsonschema
# validation error looks.
msg = _("Invalid input for field/attribute %(path)s. "
"Value: %(value)s. %(message)s") % {
'path': key, 'value': image_href,
'message': (
'Invalid image reference format. Specify the image '
'reference by UUID or full URL.')
}
raise exc.HTTPBadRequest(explanation=msg)
image_uuid = image_href.split('/').pop()
if not uuidutils.is_uuid_like(image_uuid):
msg = _("Invalid input for field/attribute %(path)s. "
"Value: %(value)s. %(message)s") % {
'path': key, 'value': image_href,
'message': (
'Invalid image reference format. Specify the image '
'reference by UUID or full URL.')
}
raise exc.HTTPBadRequest(explanation=msg)
return image_uuid

View File

@ -55,9 +55,8 @@ class RescueController(wsgi.Controller):
instance = common.get_instance(self.compute_api, context, id) instance = common.get_instance(self.compute_api, context, id)
rescue_image_ref = None rescue_image_ref = None
if body['rescue'] and 'rescue_image_ref' in body['rescue']: if body['rescue']:
rescue_image_ref = common.image_uuid_from_href( rescue_image_ref = body['rescue'].get('rescue_image_ref')
body['rescue']['rescue_image_ref'], 'rescue_image_ref')
try: try:
self.compute_api.rescue(context, instance, self.compute_api.rescue(context, instance,

View File

@ -22,7 +22,7 @@ rescue = {
'type': ['object', 'null'], 'type': ['object', 'null'],
'properties': { 'properties': {
'adminPass': parameter_types.admin_password, 'adminPass': parameter_types.admin_password,
'rescue_image_ref': parameter_types.image_ref, 'rescue_image_ref': parameter_types.image_id,
}, },
'additionalProperties': False, 'additionalProperties': False,
}, },

View File

@ -25,7 +25,12 @@ base_create = {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
'name': parameter_types.name, 'name': parameter_types.name,
'imageRef': parameter_types.image_ref, # NOTE(gmann): In case of boot from volume, imageRef was
# allowed as the empty string also So keeping the same
# behavior and allow empty string in case of boot from
# volume only. Python code make sure empty string is
# not alowed for other cases.
'imageRef': parameter_types.image_id_or_empty_string,
'flavorRef': parameter_types.flavor_ref, 'flavorRef': parameter_types.flavor_ref,
'adminPass': parameter_types.admin_password, 'adminPass': parameter_types.admin_password,
'metadata': parameter_types.metadata, 'metadata': parameter_types.metadata,
@ -108,7 +113,7 @@ base_rebuild = {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
'name': parameter_types.name, 'name': parameter_types.name,
'imageRef': parameter_types.image_ref, 'imageRef': parameter_types.image_id,
'adminPass': parameter_types.admin_password, 'adminPass': parameter_types.admin_password,
'metadata': parameter_types.metadata, 'metadata': parameter_types.metadata,
'preserve_ephemeral': parameter_types.boolean, 'preserve_ephemeral': parameter_types.boolean,

View File

@ -864,8 +864,7 @@ class ServersController(wsgi.Controller):
if not image_href and create_kwargs.get('block_device_mapping'): if not image_href and create_kwargs.get('block_device_mapping'):
return '' return ''
elif image_href: elif image_href:
return common.image_uuid_from_href(six.text_type(image_href), return image_href
'imageRef')
else: else:
msg = _("Missing imageRef attribute") msg = _("Missing imageRef attribute")
raise exc.HTTPBadRequest(explanation=msg) raise exc.HTTPBadRequest(explanation=msg)
@ -899,7 +898,6 @@ class ServersController(wsgi.Controller):
rebuild_dict = body['rebuild'] rebuild_dict = body['rebuild']
image_href = rebuild_dict["imageRef"] image_href = rebuild_dict["imageRef"]
image_href = common.image_uuid_from_href(image_href, 'imageRef')
password = self._get_server_admin_password(rebuild_dict) password = self._get_server_admin_password(rebuild_dict)

View File

@ -269,6 +269,14 @@ image_id = {
} }
image_id_or_empty_string = {
'oneOf': [
{'type': 'string', 'format': 'uuid'},
{'type': 'string', 'maxLength': 0}
]
}
volume_id = { volume_id = {
'type': 'string', 'format': 'uuid' 'type': 'string', 'format': 'uuid'
} }
@ -294,11 +302,6 @@ admin_password = {
} }
image_ref = {
'type': 'string',
}
flavor_ref = { flavor_ref = {
'type': ['string', 'integer'], 'minLength': 1 'type': ['string', 'integer'], 'minLength': 1
} }

View File

@ -4,7 +4,7 @@
"accessIPv6": "%(access_ip_v6)s", "accessIPv6": "%(access_ip_v6)s",
"name" : "new-server-test", "name" : "new-server-test",
"description" : "new-server-description", "description" : "new-server-description",
"imageRef" : "%(glance_host)s/images/%(image_id)s", "imageRef" : "%(image_id)s",
"flavorRef" : "%(host)s/flavors/1", "flavorRef" : "%(host)s/flavors/1",
"metadata" : { "metadata" : {
"My Server Name" : "Apache1" "My Server Name" : "Apache1"

View File

@ -120,24 +120,11 @@ class _IntegratedTestBase(test.TestCase):
def get_invalid_image(self): def get_invalid_image(self):
return str(uuid.uuid4()) return str(uuid.uuid4())
def _get_any_image_href(self):
image = self.api.get_images()[0]
LOG.debug("Image: %s" % image)
if self._image_ref_parameter in image:
image_href = image[self._image_ref_parameter]
else:
image_href = image['id']
image_href = 'http://fake.server/%s' % image_href
return image_href
def _build_minimal_create_server_request(self): def _build_minimal_create_server_request(self):
server = {} server = {}
image_href = self._get_any_image_href()
# We now have a valid imageId # We now have a valid imageId
server[self._image_ref_parameter] = image_href server[self._image_ref_parameter] = self.api.get_images()[0]['id']
# Set a valid flavorId # Set a valid flavorId
flavor = self.api.get_flavors()[0] flavor = self.api.get_flavors()[0]
@ -181,19 +168,11 @@ class _IntegratedTestBase(test.TestCase):
def _build_server(self, flavor_id): def _build_server(self, flavor_id):
server = {} server = {}
image_href = self._get_any_image_href()
image = self.api.get_images()[0] image = self.api.get_images()[0]
LOG.debug("Image: %s" % image) LOG.debug("Image: %s" % image)
if self._image_ref_parameter in image:
image_href = image[self._image_ref_parameter]
else:
image_href = image['id']
image_href = 'http://fake.server/%s' % image_href
# We now have a valid imageId # We now have a valid imageId
server[self._image_ref_parameter] = image_href server[self._image_ref_parameter] = image['id']
# Set a valid flavorId # Set a valid flavorId
flavor = self.api.get_flavor(flavor_id) flavor = self.api.get_flavor(flavor_id)
@ -243,19 +222,8 @@ class InstanceHelperMixin(object):
flavor_id=None): flavor_id=None):
server = {} server = {}
if image_uuid:
image_href = 'http://fake.server/%s' % image_uuid
else:
image = api.get_images()[0]
if 'imageRef' in image:
image_href = image['imageRef']
else:
image_href = image['id']
image_href = 'http://fake.server/%s' % image_href
# We now have a valid imageId # We now have a valid imageId
server['imageRef'] = image_href server['imageRef'] = image_uuid or api.get_images()[0]['id']
if not flavor_id: if not flavor_id:
# Set a valid flavorId # Set a valid flavorId

View File

@ -148,6 +148,44 @@ class BlockDeviceMappingTestV21(test.TestCase):
params = {block_device_mapping.ATTRIBUTE_NAME: self.bdm} params = {block_device_mapping.ATTRIBUTE_NAME: self.bdm}
self._test_create(params, no_image=True) self._test_create(params, no_image=True)
@mock.patch.object(compute_api.API, '_validate_bdm')
@mock.patch.object(compute_api.API, '_get_bdm_image_metadata')
def test_create_instance_with_bdms_and_empty_imageRef(
self, mock_bdm_image_metadata, mock_validate_bdm):
mock_bdm_image_metadata.return_value = {}
mock_validate_bdm.return_value = True
old_create = compute_api.API.create
def create(*args, **kwargs):
self.assertThat(
block_device.BlockDeviceDict(self.bdm[0]),
matchers.DictMatches(kwargs['block_device_mapping'][0])
)
return old_create(*args, **kwargs)
self.stub_out('nova.compute.api.API.create', create)
params = {block_device_mapping.ATTRIBUTE_NAME: self.bdm,
'imageRef': ''}
self._test_create(params)
def test_create_instance_with_imageRef_as_full_url(self):
bdm = [{'device_name': 'foo'}]
image_href = ('http://localhost/v2/fake/images/'
'76fa36fc-c930-4bf3-8c8a-ea2a2420deb6')
params = {block_device_mapping.ATTRIBUTE_NAME: bdm,
'imageRef': image_href}
self.assertRaises(exception.ValidationError,
self._test_create, params)
def test_create_instance_with_non_uuid_imageRef(self):
bdm = [{'device_name': 'foo'}]
params = {block_device_mapping.ATTRIBUTE_NAME: bdm,
'imageRef': '123123abcd'}
self.assertRaises(exception.ValidationError,
self._test_create, params)
def test_create_instance_with_device_name_not_string(self): def test_create_instance_with_device_name_not_string(self):
self.bdm[0]['device_name'] = 123 self.bdm[0]['device_name'] = 123
old_create = compute_api.API.create old_create = compute_api.API.create

View File

@ -140,6 +140,51 @@ class BlockDeviceMappingTestV21(test.TestCase):
self.mox.ReplayAll() self.mox.ReplayAll()
self._test_create(params, no_image=True) self._test_create(params, no_image=True)
@mock.patch.object(compute_api.API, '_validate_bdm')
@mock.patch.object(compute_api.API, '_get_bdm_image_metadata')
def test_create_instance_with_imageRef_as_empty_string(
self, mock_bdm_image_metadata, mock_validate_bdm):
volume = {
'id': uuids.volume_id,
'status': 'active',
'volume_image_metadata':
{'test_key': 'test_value'}
}
mock_bdm_image_metadata.return_value = volume
mock_validate_bdm.return_value = True
params = {'block_device_mapping': self.bdm,
'imageRef': ''}
old_create = compute_api.API.create
def create(*args, **kwargs):
self.assertEqual(kwargs['block_device_mapping'], self.bdm)
return old_create(*args, **kwargs)
self.stub_out('nova.compute.api.API.create', create)
self._test_create(params)
def test_create_instance_with_imageRef_as_full_url(self):
bdm = [{
'volume_id': self.volume_id,
'device_name': 'vda'
}]
image_href = ('http://localhost/v2/fake/images/'
'76fa36fc-c930-4bf3-8c8a-ea2a2420deb6')
params = {'block_device_mapping': bdm,
'imageRef': image_href}
self.assertRaises(exception.ValidationError,
self._test_create, params)
def test_create_instance_with_non_uuid_imageRef(self):
bdm = [{
'volume_id': self.volume_id,
'device_name': 'vda'
}]
params = {'block_device_mapping': bdm,
'imageRef': 'bad-format'}
self.assertRaises(exception.ValidationError,
self._test_create, params)
def test_create_instance_with_volumes_disabled(self): def test_create_instance_with_volumes_disabled(self):
bdm = [{'device_name': 'foo'}] bdm = [{'device_name': 'foo'}]
params = {'block_device_mapping': bdm} params = {'block_device_mapping': bdm}

View File

@ -43,7 +43,6 @@ def fake_compute_get(*args, **kwargs):
class RescueTestV21(test.NoDBTestCase): class RescueTestV21(test.NoDBTestCase):
image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
image_href = 'http://localhost/v2/fake/images/%s' % image_uuid
def setUp(self): def setUp(self):
super(RescueTestV21, self).setUp() super(RescueTestV21, self).setUp()
@ -139,11 +138,26 @@ class RescueTestV21(test.NoDBTestCase):
self.controller._rescue, self.controller._rescue,
self.fake_req, UUID, body=body) self.fake_req, UUID, body=body)
@mock.patch('nova.compute.api.API.rescue') def test_rescue_with_bad_image_specified(self):
def test_rescue_with_bad_image_specified(self, mock_compute_api_rescue):
body = {"rescue": {"adminPass": "ABC123", body = {"rescue": {"adminPass": "ABC123",
"rescue_image_ref": "img-id"}} "rescue_image_ref": "img-id"}}
self.assertRaises(webob.exc.HTTPBadRequest, self.assertRaises(exception.ValidationError,
self.controller._rescue,
self.fake_req, UUID, body=body)
def test_rescue_with_imageRef_as_full_url(self):
image_href = ('http://localhost/v2/fake/images/'
'76fa36fc-c930-4bf3-8c8a-ea2a2420deb6')
body = {"rescue": {"adminPass": "ABC123",
"rescue_image_ref": image_href}}
self.assertRaises(exception.ValidationError,
self.controller._rescue,
self.fake_req, UUID, body=body)
def test_rescue_with_imageRef_as_empty_string(self):
body = {"rescue": {"adminPass": "ABC123",
"rescue_image_ref": ''}}
self.assertRaises(exception.ValidationError,
self.controller._rescue, self.controller._rescue,
self.fake_req, UUID, body=body) self.fake_req, UUID, body=body)
@ -151,7 +165,7 @@ class RescueTestV21(test.NoDBTestCase):
def test_rescue_with_image_specified(self, mock_compute_api_rescue): def test_rescue_with_image_specified(self, mock_compute_api_rescue):
instance = fake_compute_get() instance = fake_compute_get()
body = {"rescue": {"adminPass": "ABC123", body = {"rescue": {"adminPass": "ABC123",
"rescue_image_ref": self.image_href}} "rescue_image_ref": self.image_uuid}}
resp_json = self.controller._rescue(self.fake_req, UUID, body=body) resp_json = self.controller._rescue(self.fake_req, UUID, body=body)
self.assertEqual("ABC123", resp_json['adminPass']) self.assertEqual("ABC123", resp_json['adminPass'])

View File

@ -338,15 +338,6 @@ class ServerActionsControllerTestV21(test.TestCase):
self.assertEqual(info['image_href_in_call'], self.image_uuid) self.assertEqual(info['image_href_in_call'], self.image_uuid)
def test_rebuild_instance_with_image_href_uses_uuid(self): def test_rebuild_instance_with_image_href_uses_uuid(self):
info = dict(image_href_in_call=None)
def rebuild(self2, context, instance, image_href, *args, **kwargs):
info['image_href_in_call'] = image_href
self.stub_out('nova.db.instance_get',
fakes.fake_instance_get(vm_state=vm_states.ACTIVE))
self.stubs.Set(compute_api.API, 'rebuild', rebuild)
# proper local hrefs must start with 'http://localhost/v2/' # proper local hrefs must start with 'http://localhost/v2/'
body = { body = {
'rebuild': { 'rebuild': {
@ -354,8 +345,9 @@ class ServerActionsControllerTestV21(test.TestCase):
}, },
} }
self.controller._action_rebuild(self.req, FAKE_UUID, body=body) self.assertRaises(exception.ValidationError,
self.assertEqual(info['image_href_in_call'], self.image_uuid) self.controller._action_rebuild,
self.req, FAKE_UUID, body=body)
def test_rebuild_accepted_minimum_pass_disabled(self): def test_rebuild_accepted_minimum_pass_disabled(self):
# run with enable_instance_password disabled to verify adminPass # run with enable_instance_password disabled to verify adminPass
@ -517,7 +509,7 @@ class ServerActionsControllerTestV21(test.TestCase):
"imageRef": "foo", "imageRef": "foo",
}, },
} }
self.assertRaises(webob.exc.HTTPBadRequest, self.assertRaises(exception.ValidationError,
self.controller._action_rebuild, self.controller._action_rebuild,
self.req, FAKE_UUID, body=body) self.req, FAKE_UUID, body=body)

View File

@ -1571,7 +1571,6 @@ class ServersControllerDeleteTest(ControllerTest):
class ServersControllerRebuildInstanceTest(ControllerTest): class ServersControllerRebuildInstanceTest(ControllerTest):
image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
image_href = 'http://localhost/v2/fake/images/%s' % image_uuid
def setUp(self): def setUp(self):
super(ServersControllerRebuildInstanceTest, self).setUp() super(ServersControllerRebuildInstanceTest, self).setUp()
@ -1594,7 +1593,7 @@ class ServersControllerRebuildInstanceTest(ControllerTest):
self.body = { self.body = {
'rebuild': { 'rebuild': {
'name': 'new_name', 'name': 'new_name',
'imageRef': self.image_href, 'imageRef': self.image_uuid,
'metadata': { 'metadata': {
'open': 'stack', 'open': 'stack',
}, },
@ -1604,6 +1603,29 @@ class ServersControllerRebuildInstanceTest(ControllerTest):
self.req.method = 'POST' self.req.method = 'POST'
self.req.headers["content-type"] = "application/json" self.req.headers["content-type"] = "application/json"
def test_rebuild_server_with_image_not_uuid(self):
self.body['rebuild']['imageRef'] = 'not-uuid'
self.assertRaises(exception.ValidationError,
self.controller._action_rebuild,
self.req, FAKE_UUID,
body=self.body)
def test_rebuild_server_with_image_as_full_url(self):
image_href = ('http://localhost/v2/fake/images/'
'76fa36fc-c930-4bf3-8c8a-ea2a2420deb6')
self.body['rebuild']['imageRef'] = image_href
self.assertRaises(exception.ValidationError,
self.controller._action_rebuild,
self.req, FAKE_UUID,
body=self.body)
def test_rebuild_server_with_image_as_empty_string(self):
self.body['rebuild']['imageRef'] = ''
self.assertRaises(exception.ValidationError,
self.controller._action_rebuild,
self.req, FAKE_UUID,
body=self.body)
def test_rebuild_instance_name_with_spaces_in_the_middle(self): def test_rebuild_instance_name_with_spaces_in_the_middle(self):
self.body['rebuild']['name'] = 'abc def' self.body['rebuild']['name'] = 'abc def'
self.req.body = jsonutils.dump_as_bytes(self.body) self.req.body = jsonutils.dump_as_bytes(self.body)
@ -1751,7 +1773,7 @@ class ServersControllerRebuildInstanceTest(ControllerTest):
def test_rebuild_bad_personality(self): def test_rebuild_bad_personality(self):
body = { body = {
"rebuild": { "rebuild": {
"imageRef": self.image_href, "imageRef": self.image_uuid,
"personality": [{ "personality": [{
"path": "/path/to/file", "path": "/path/to/file",
"contents": "INVALID b64", "contents": "INVALID b64",
@ -1766,7 +1788,7 @@ class ServersControllerRebuildInstanceTest(ControllerTest):
def test_rebuild_personality(self): def test_rebuild_personality(self):
body = { body = {
"rebuild": { "rebuild": {
"imageRef": self.image_href, "imageRef": self.image_uuid,
"personality": [{ "personality": [{
"path": "/path/to/file", "path": "/path/to/file",
"contents": base64.b64encode("Test String"), "contents": base64.b64encode("Test String"),
@ -2437,10 +2459,9 @@ class ServersControllerCreateTest(test.TestCase):
self.assertRaises(webob.exc.HTTPBadRequest, self._test_create_instance, self.assertRaises(webob.exc.HTTPBadRequest, self._test_create_instance,
flavor=1324) flavor=1324)
def test_create_server_bad_image_href(self): def test_create_server_bad_image_uuid(self):
image_href = 1
self.body['server']['min_count'] = 1 self.body['server']['min_count'] = 1
self.body['server']['imageRef'] = image_href, self.body['server']['imageRef'] = 1,
self.req.body = jsonutils.dump_as_bytes(self.body) self.req.body = jsonutils.dump_as_bytes(self.body)
self.assertRaises(exception.ValidationError, self.assertRaises(exception.ValidationError,
self.controller.create, self.controller.create,
@ -2505,23 +2526,24 @@ class ServersControllerCreateTest(test.TestCase):
"Flavor's disk is too small for requested image."): "Flavor's disk is too small for requested image."):
self.controller.create(self.req, body=self.body) self.controller.create(self.req, body=self.body)
def test_create_instance_image_ref_is_bookmark(self): def test_create_instance_with_image_non_uuid(self):
image_href = 'http://localhost/fake/images/%s' % self.image_uuid self.body['server']['imageRef'] = 'not-uuid'
self.body['server']['imageRef'] = image_href self.assertRaises(exception.ValidationError,
self.req.body = jsonutils.dump_as_bytes(self.body) self.controller.create,
res = self.controller.create(self.req, body=self.body).obj self.req, body=self.body)
server = res['server'] def test_create_instance_with_image_as_full_url(self):
self.assertEqual(FAKE_UUID, server['id']) image_href = ('http://localhost/v2/fake/images/'
'76fa36fc-c930-4bf3-8c8a-ea2a2420deb6')
def test_create_instance_image_ref_is_invalid(self):
image_uuid = 'this_is_not_a_valid_uuid'
image_href = 'http://localhost/fake/images/%s' % image_uuid
flavor_ref = 'http://localhost/fake/flavors/3'
self.body['server']['imageRef'] = image_href self.body['server']['imageRef'] = image_href
self.body['server']['flavorRef'] = flavor_ref self.assertRaises(exception.ValidationError,
self.req.body = jsonutils.dump_as_bytes(self.body) self.controller.create,
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, self.req, body=self.body)
def test_create_instance_with_image_as_empty_string(self):
self.body['server']['imageRef'] = ''
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create,
self.req, body=self.body) self.req, body=self.body)
def test_create_instance_no_key_pair(self): def test_create_instance_no_key_pair(self):
@ -2682,10 +2704,7 @@ class ServersControllerCreateTest(test.TestCase):
# test with admin passwords disabled See lp bug 921814 # test with admin passwords disabled See lp bug 921814
self.flags(enable_instance_password=False) self.flags(enable_instance_password=False)
# proper local hrefs must start with 'http://localhost/v2/'
self.flags(enable_instance_password=False) self.flags(enable_instance_password=False)
image_href = 'http://localhost/v2/fake/images/%s' % self.image_uuid
self.body['server']['imageRef'] = image_href
self.req.body = jsonutils.dump_as_bytes(self.body) self.req.body = jsonutils.dump_as_bytes(self.body)
res = self.controller.create(self.req, body=self.body).obj res = self.controller.create(self.req, body=self.body).obj
@ -2694,50 +2713,36 @@ class ServersControllerCreateTest(test.TestCase):
self.assertEqual(FAKE_UUID, server['id']) self.assertEqual(FAKE_UUID, server['id'])
def test_create_instance_name_too_long(self): def test_create_instance_name_too_long(self):
# proper local hrefs must start with 'http://localhost/v2/'
image_href = 'http://localhost/v2/images/%s' % self.image_uuid
self.body['server']['name'] = 'X' * 256 self.body['server']['name'] = 'X' * 256
self.body['server']['imageRef'] = image_href
self.req.body = jsonutils.dump_as_bytes(self.body) self.req.body = jsonutils.dump_as_bytes(self.body)
self.assertRaises(exception.ValidationError, self.controller.create, self.assertRaises(exception.ValidationError, self.controller.create,
self.req, body=self.body) self.req, body=self.body)
def test_create_instance_name_with_spaces_in_the_middle(self): def test_create_instance_name_with_spaces_in_the_middle(self):
# proper local hrefs must start with 'http://localhost/v2/'
image_href = 'http://localhost/v2/images/%s' % self.image_uuid
self.body['server']['name'] = 'abc def' self.body['server']['name'] = 'abc def'
self.body['server']['imageRef'] = image_href
self.req.body = jsonutils.dump_as_bytes(self.body) self.req.body = jsonutils.dump_as_bytes(self.body)
self.controller.create(self.req, body=self.body) self.controller.create(self.req, body=self.body)
def test_create_instance_name_with_leading_trailing_spaces(self): def test_create_instance_name_with_leading_trailing_spaces(self):
# proper local hrefs must start with 'http://localhost/v2/'
image_href = 'http://localhost/v2/images/%s' % self.image_uuid
self.body['server']['name'] = ' abc def ' self.body['server']['name'] = ' abc def '
self.body['server']['imageRef'] = image_href
self.req.body = jsonutils.dump_as_bytes(self.body) self.req.body = jsonutils.dump_as_bytes(self.body)
self.assertRaises(exception.ValidationError, self.assertRaises(exception.ValidationError,
self.controller.create, self.req, body=self.body) self.controller.create, self.req, body=self.body)
def test_create_instance_name_with_leading_trailing_spaces_in_compat_mode( def test_create_instance_name_with_leading_trailing_spaces_in_compat_mode(
self): self):
# proper local hrefs must start with 'http://localhost/v2/'
image_href = 'http://localhost/v2/images/%s' % self.image_uuid
self.body['server']['name'] = ' abc def ' self.body['server']['name'] = ' abc def '
self.body['server']['imageRef'] = image_href
self.req.body = jsonutils.dump_as_bytes(self.body) self.req.body = jsonutils.dump_as_bytes(self.body)
self.req.set_legacy_v2() self.req.set_legacy_v2()
self.controller.create(self.req, body=self.body) self.controller.create(self.req, body=self.body)
def test_create_instance_name_all_blank_spaces(self): def test_create_instance_name_all_blank_spaces(self):
# proper local hrefs must start with 'http://localhost/v2/'
image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
image_href = 'http://localhost/v2/images/%s' % image_uuid
flavor_ref = 'http://localhost/fake/flavors/3' flavor_ref = 'http://localhost/fake/flavors/3'
body = { body = {
'server': { 'server': {
'name': ' ' * 64, 'name': ' ' * 64,
'imageRef': image_href, 'imageRef': image_uuid,
'flavorRef': flavor_ref, 'flavorRef': flavor_ref,
'metadata': { 'metadata': {
'hello': 'world', 'hello': 'world',
@ -2754,9 +2759,6 @@ class ServersControllerCreateTest(test.TestCase):
self.controller.create, req, body=body) self.controller.create, req, body=body)
def test_create_az_with_leading_trailing_spaces(self): def test_create_az_with_leading_trailing_spaces(self):
# proper local hrefs must start with 'http://localhost/v2/'
image_href = 'http://localhost/v2/images/%s' % self.image_uuid
self.body['server']['imageRef'] = image_href
self.body['server']['availability_zone'] = ' zone1 ' self.body['server']['availability_zone'] = ' zone1 '
self.req.body = jsonutils.dump_as_bytes(self.body) self.req.body = jsonutils.dump_as_bytes(self.body)
self.assertRaises(exception.ValidationError, self.assertRaises(exception.ValidationError,
@ -2764,10 +2766,7 @@ class ServersControllerCreateTest(test.TestCase):
def test_create_az_with_leading_trailing_spaces_in_compat_mode( def test_create_az_with_leading_trailing_spaces_in_compat_mode(
self): self):
# proper local hrefs must start with 'http://localhost/v2/'
image_href = 'http://localhost/v2/images/%s' % self.image_uuid
self.body['server']['name'] = ' abc def ' self.body['server']['name'] = ' abc def '
self.body['server']['imageRef'] = image_href
self.body['server']['availability_zones'] = ' zone1 ' self.body['server']['availability_zones'] = ' zone1 '
self.req.body = jsonutils.dump_as_bytes(self.body) self.req.body = jsonutils.dump_as_bytes(self.body)
self.req.set_legacy_v2() self.req.set_legacy_v2()
@ -2776,9 +2775,6 @@ class ServersControllerCreateTest(test.TestCase):
self.controller.create(self.req, body=self.body) self.controller.create(self.req, body=self.body)
def test_create_instance(self): def test_create_instance(self):
# proper local hrefs must start with 'http://localhost/v2/'
image_href = 'http://localhost/v2/images/%s' % self.image_uuid
self.body['server']['imageRef'] = image_href
self.req.body = jsonutils.dump_as_bytes(self.body) self.req.body = jsonutils.dump_as_bytes(self.body)
res = self.controller.create(self.req, body=self.body).obj res = self.controller.create(self.req, body=self.body).obj
@ -2793,14 +2789,12 @@ class ServersControllerCreateTest(test.TestCase):
self.stubs.Set(keypairs.Keypairs, 'server_create', self.stubs.Set(keypairs.Keypairs, 'server_create',
fake_keypair_server_create) fake_keypair_server_create)
# proper local hrefs must start with 'http://localhost/v2/'
image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
image_href = 'http://localhost/v2/images/%s' % image_uuid
flavor_ref = 'http://localhost/123/flavors/3' flavor_ref = 'http://localhost/123/flavors/3'
body = { body = {
'server': { 'server': {
'name': 'server_test', 'name': 'server_test',
'imageRef': image_href, 'imageRef': image_uuid,
'flavorRef': flavor_ref, 'flavorRef': flavor_ref,
'metadata': { 'metadata': {
'hello': 'world', 'hello': 'world',
@ -2818,9 +2812,6 @@ class ServersControllerCreateTest(test.TestCase):
def test_create_instance_pass_disabled(self): def test_create_instance_pass_disabled(self):
self.flags(enable_instance_password=False) self.flags(enable_instance_password=False)
# proper local hrefs must start with 'http://localhost/v2/'
image_href = 'http://localhost/v2/images/%s' % self.image_uuid
self.body['server']['imageRef'] = image_href
self.req.body = jsonutils.dump_as_bytes(self.body) self.req.body = jsonutils.dump_as_bytes(self.body)
res = self.controller.create(self.req, body=self.body).obj res = self.controller.create(self.req, body=self.body).obj
@ -2837,8 +2828,6 @@ class ServersControllerCreateTest(test.TestCase):
'cpuset': None, 'cpuset': None,
'memsize': 0, 'memsize': 0,
'memtotal': 0}) 'memtotal': 0})
image_href = 'http://localhost/v2/images/%s' % self.image_uuid
self.body['server']['imageRef'] = image_href
self.req.body = jsonutils.dump_as_bytes(self.body) self.req.body = jsonutils.dump_as_bytes(self.body)
self.assertRaises(webob.exc.HTTPBadRequest, self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create, self.req, body=self.body) self.controller.create, self.req, body=self.body)
@ -2855,8 +2844,6 @@ class ServersControllerCreateTest(test.TestCase):
def test_create_instance_too_much_metadata(self): def test_create_instance_too_much_metadata(self):
self.flags(quota_metadata_items=1) self.flags(quota_metadata_items=1)
image_href = 'http://localhost/v2/images/%s' % self.image_uuid
self.body['server']['imageRef'] = image_href
self.body['server']['metadata']['vote'] = 'fiddletown' self.body['server']['metadata']['vote'] = 'fiddletown'
self.req.body = jsonutils.dump_as_bytes(self.body) self.req.body = jsonutils.dump_as_bytes(self.body)
self.assertRaises(webob.exc.HTTPForbidden, self.assertRaises(webob.exc.HTTPForbidden,
@ -2864,8 +2851,6 @@ class ServersControllerCreateTest(test.TestCase):
def test_create_instance_metadata_key_too_long(self): def test_create_instance_metadata_key_too_long(self):
self.flags(quota_metadata_items=1) self.flags(quota_metadata_items=1)
image_href = 'http://localhost/v2/images/%s' % self.image_uuid
self.body['server']['imageRef'] = image_href
self.body['server']['metadata'] = {('a' * 260): '12345'} self.body['server']['metadata'] = {('a' * 260): '12345'}
self.req.body = jsonutils.dump_as_bytes(self.body) self.req.body = jsonutils.dump_as_bytes(self.body)
@ -2874,8 +2859,6 @@ class ServersControllerCreateTest(test.TestCase):
def test_create_instance_metadata_value_too_long(self): def test_create_instance_metadata_value_too_long(self):
self.flags(quota_metadata_items=1) self.flags(quota_metadata_items=1)
image_href = 'http://localhost/v2/images/%s' % self.image_uuid
self.body['server']['imageRef'] = image_href
self.body['server']['metadata'] = {'key1': ('a' * 260)} self.body['server']['metadata'] = {'key1': ('a' * 260)}
self.req.body = jsonutils.dump_as_bytes(self.body) self.req.body = jsonutils.dump_as_bytes(self.body)
self.assertRaises(exception.ValidationError, self.assertRaises(exception.ValidationError,
@ -2883,8 +2866,6 @@ class ServersControllerCreateTest(test.TestCase):
def test_create_instance_metadata_key_blank(self): def test_create_instance_metadata_key_blank(self):
self.flags(quota_metadata_items=1) self.flags(quota_metadata_items=1)
image_href = 'http://localhost/v2/images/%s' % self.image_uuid
self.body['server']['imageRef'] = image_href
self.body['server']['metadata'] = {'': 'abcd'} self.body['server']['metadata'] = {'': 'abcd'}
self.req.body = jsonutils.dump_as_bytes(self.body) self.req.body = jsonutils.dump_as_bytes(self.body)
self.assertRaises(exception.ValidationError, self.assertRaises(exception.ValidationError,
@ -2892,8 +2873,6 @@ class ServersControllerCreateTest(test.TestCase):
def test_create_instance_metadata_not_dict(self): def test_create_instance_metadata_not_dict(self):
self.flags(quota_metadata_items=1) self.flags(quota_metadata_items=1)
image_href = 'http://localhost/v2/images/%s' % self.image_uuid
self.body['server']['imageRef'] = image_href
self.body['server']['metadata'] = 'string' self.body['server']['metadata'] = 'string'
self.req.body = jsonutils.dump_as_bytes(self.body) self.req.body = jsonutils.dump_as_bytes(self.body)
self.assertRaises(exception.ValidationError, self.assertRaises(exception.ValidationError,
@ -2901,8 +2880,6 @@ class ServersControllerCreateTest(test.TestCase):
def test_create_instance_metadata_key_not_string(self): def test_create_instance_metadata_key_not_string(self):
self.flags(quota_metadata_items=1) self.flags(quota_metadata_items=1)
image_href = 'http://localhost/v2/images/%s' % self.image_uuid
self.body['server']['imageRef'] = image_href
self.body['server']['metadata'] = {1: 'test'} self.body['server']['metadata'] = {1: 'test'}
self.req.body = jsonutils.dump_as_bytes(self.body) self.req.body = jsonutils.dump_as_bytes(self.body)
self.assertRaises(exception.ValidationError, self.assertRaises(exception.ValidationError,
@ -2910,8 +2887,6 @@ class ServersControllerCreateTest(test.TestCase):
def test_create_instance_metadata_value_not_string(self): def test_create_instance_metadata_value_not_string(self):
self.flags(quota_metadata_items=1) self.flags(quota_metadata_items=1)
image_href = 'http://localhost/v2/images/%s' % self.image_uuid
self.body['server']['imageRef'] = image_href
self.body['server']['metadata'] = {'test': ['a', 'list']} self.body['server']['metadata'] = {'test': ['a', 'list']}
self.req.body = jsonutils.dump_as_bytes(self.body) self.req.body = jsonutils.dump_as_bytes(self.body)
self.assertRaises(exception.ValidationError, self.assertRaises(exception.ValidationError,
@ -2923,8 +2898,6 @@ class ServersControllerCreateTest(test.TestCase):
self._test_create_extra, params) self._test_create_extra, params)
def test_create_instance_invalid_key_name(self): def test_create_instance_invalid_key_name(self):
image_href = 'http://localhost/v2/images/2'
self.body['server']['imageRef'] = image_href
self.body['server']['key_name'] = 'nonexistentkey' self.body['server']['key_name'] = 'nonexistentkey'
self.req.body = jsonutils.dump_as_bytes(self.body) self.req.body = jsonutils.dump_as_bytes(self.body)
self.assertRaises(webob.exc.HTTPBadRequest, self.assertRaises(webob.exc.HTTPBadRequest,
@ -2939,18 +2912,14 @@ class ServersControllerCreateTest(test.TestCase):
self._check_admin_password_len(res["server"]) self._check_admin_password_len(res["server"])
def test_create_instance_invalid_flavor_href(self): def test_create_instance_invalid_flavor_href(self):
image_href = 'http://localhost/v2/images/2'
flavor_ref = 'http://localhost/v2/flavors/asdf' flavor_ref = 'http://localhost/v2/flavors/asdf'
self.body['server']['imageRef'] = image_href
self.body['server']['flavorRef'] = flavor_ref self.body['server']['flavorRef'] = flavor_ref
self.req.body = jsonutils.dump_as_bytes(self.body) self.req.body = jsonutils.dump_as_bytes(self.body)
self.assertRaises(webob.exc.HTTPBadRequest, self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create, self.req, body=self.body) self.controller.create, self.req, body=self.body)
def test_create_instance_invalid_flavor_id_int(self): def test_create_instance_invalid_flavor_id_int(self):
image_href = 'http://localhost/v2/images/2'
flavor_ref = -1 flavor_ref = -1
self.body['server']['imageRef'] = image_href
self.body['server']['flavorRef'] = flavor_ref self.body['server']['flavorRef'] = flavor_ref
self.req.body = jsonutils.dump_as_bytes(self.body) self.req.body = jsonutils.dump_as_bytes(self.body)
self.assertRaises(webob.exc.HTTPBadRequest, self.assertRaises(webob.exc.HTTPBadRequest,
@ -2964,22 +2933,12 @@ class ServersControllerCreateTest(test.TestCase):
self.controller.create, self.req, body=self.body) self.controller.create, self.req, body=self.body)
def test_create_instance_bad_flavor_href(self): def test_create_instance_bad_flavor_href(self):
image_href = 'http://localhost/v2/images/2'
flavor_ref = 'http://localhost/v2/flavors/17' flavor_ref = 'http://localhost/v2/flavors/17'
self.body['server']['imageRef'] = image_href
self.body['server']['flavorRef'] = flavor_ref self.body['server']['flavorRef'] = flavor_ref
self.req.body = jsonutils.dump_as_bytes(self.body) self.req.body = jsonutils.dump_as_bytes(self.body)
self.assertRaises(webob.exc.HTTPBadRequest, self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create, self.req, body=self.body) self.controller.create, self.req, body=self.body)
def test_create_instance_bad_href(self):
image_href = 'asdf'
self.body['server']['imageRef'] = image_href
self.req.body = jsonutils.dump_as_bytes(self.body)
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create, self.req, body=self.body)
def test_create_instance_local_href(self): def test_create_instance_local_href(self):
self.req.body = jsonutils.dump_as_bytes(self.body) self.req.body = jsonutils.dump_as_bytes(self.body)
res = self.controller.create(self.req, body=self.body).obj res = self.controller.create(self.req, body=self.body).obj
@ -3340,9 +3299,6 @@ class ServersControllerCreateTest(test.TestCase):
class ServersControllerCreateTestV219(ServersControllerCreateTest): class ServersControllerCreateTestV219(ServersControllerCreateTest):
def _create_instance_req(self, set_desc, desc=None): def _create_instance_req(self, set_desc, desc=None):
# proper local hrefs must start with 'http://localhost/v2/'
image_href = 'http://localhost/v2/images/%s' % self.image_uuid
self.body['server']['imageRef'] = image_href
if set_desc: if set_desc:
self.body['server']['description'] = desc self.body['server']['description'] = desc
self.req.body = jsonutils.dump_as_bytes(self.body) self.req.body = jsonutils.dump_as_bytes(self.body)

View File

@ -0,0 +1,17 @@
---
upgrade:
- imageRef input to the REST API is now restricted
to be UUID or an empty string only. imageRef
input while create, rebuild and rescue server etc
must be a valid UUID now. Previously, a random
image ref url containing image UUID was accepted.
But now all the reference of imageRef must be a
valid UUID (with below exception) otherwise API
will return 400.
Exception- In case boot server from volume.
Previously empty string was allowed in imageRef
and which is ok in case of boot from volume.
Nova will keep the same behavior and allow empty
string in case of boot from volume only and 400
in all other case.