Adds POST /images work that saves image data to a store backend
This commit is contained in:
parent
c566d9b77d
commit
cff6301bc1
@ -46,11 +46,6 @@ class ClientConnectionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BadInputError(Exception):
|
||||
"""Error resulting from a client sending bad input to a server"""
|
||||
pass
|
||||
|
||||
|
||||
class ImageBodyIterator(object):
|
||||
|
||||
"""
|
||||
@ -142,17 +137,18 @@ class BaseClient(object):
|
||||
if status_code == httplib.OK:
|
||||
return res
|
||||
elif status_code == httplib.UNAUTHORIZED:
|
||||
raise exception.NotAuthorized
|
||||
raise exception.NotAuthorized(res.body)
|
||||
elif status_code == httplib.FORBIDDEN:
|
||||
raise exception.NotAuthorized
|
||||
raise exception.NotAuthorized(res.body)
|
||||
elif status_code == httplib.NOT_FOUND:
|
||||
raise exception.NotFound
|
||||
raise exception.NotFound(res.body)
|
||||
elif status_code == httplib.CONFLICT:
|
||||
raise exception.Duplicate
|
||||
raise exception.Duplicate(res.body)
|
||||
elif status_code == httplib.BAD_REQUEST:
|
||||
raise BadInputError
|
||||
raise exception.BadInputError(res.body)
|
||||
else:
|
||||
raise Exception("Unknown error occurred! %d" % status_code)
|
||||
raise Exception("Unknown error occurred! %d (%s)"
|
||||
% (status_code, res.body))
|
||||
|
||||
except (socket.error, IOError), e:
|
||||
raise ClientConnectionError("Unable to connect to "
|
||||
@ -235,17 +231,33 @@ class Client(BaseClient):
|
||||
"""
|
||||
Tells Glance about an image's metadata as well
|
||||
as optionally the image_data itself
|
||||
|
||||
:param image_meta: Mapping of information about the
|
||||
image
|
||||
:param image_data: Optional string of raw image data
|
||||
or file-like object that can be
|
||||
used to read the image data
|
||||
|
||||
:retval The newly-stored image's metadata.
|
||||
"""
|
||||
if not image_data and 'location' not in image_meta.keys():
|
||||
raise exception.Invalid("You must either specify a location "
|
||||
"for the image or supply the actual "
|
||||
"image data when adding an image to "
|
||||
"Glance")
|
||||
body = image_data
|
||||
headers = util.image_meta_to_http_headers(image_meta)
|
||||
if image_data:
|
||||
if hasattr(image_data, 'read'):
|
||||
# TODO(jaypipes): This is far from efficient. Implement
|
||||
# chunked transfer encoding if size is not in image_meta
|
||||
body = image_data.read()
|
||||
else:
|
||||
body = image_data
|
||||
headers['content-type'] = 'application/octet-stream'
|
||||
else:
|
||||
body = None
|
||||
|
||||
res = self.do_request("POST", "/images", body, headers)
|
||||
# Registry returns a JSONified dict(image=image_info)
|
||||
data = json.loads(res.read())
|
||||
return data['image']['id']
|
||||
|
||||
|
@ -70,6 +70,11 @@ class Invalid(Error):
|
||||
pass
|
||||
|
||||
|
||||
class BadInputError(Exception):
|
||||
"""Error resulting from a client sending bad input to a server"""
|
||||
pass
|
||||
|
||||
|
||||
def wrap_exception(f):
|
||||
def _wrap(*args, **kw):
|
||||
try:
|
||||
|
@ -173,3 +173,5 @@ DEFINE_string('sql_connection',
|
||||
'sqlite:///%s/glance.sqlite' % os.path.abspath("./"),
|
||||
'connection string for sql database')
|
||||
DEFINE_bool('verbose', False, 'show debug output')
|
||||
DEFINE_string('default_store', 'file',
|
||||
'Default storage backend. Default: "file"')
|
||||
|
@ -23,6 +23,10 @@ Glance API Server
|
||||
Configuration Options
|
||||
---------------------
|
||||
|
||||
`default_store`: When no x-image-meta-store header is sent for a
|
||||
`POST /images` request, this store will be used
|
||||
for storing the image data. Default: 'file'
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
@ -39,7 +43,9 @@ from glance.common import flags
|
||||
from glance.common import wsgi
|
||||
from glance.store import (get_from_backend,
|
||||
delete_from_backend,
|
||||
get_store_from_location)
|
||||
get_store_from_location,
|
||||
get_backend_class,
|
||||
UnsupportedBackend)
|
||||
from glance import registry
|
||||
from glance import util
|
||||
|
||||
@ -174,9 +180,6 @@ class Controller(wsgi.Controller):
|
||||
|
||||
:param request: The WSGI/Webob Request object
|
||||
|
||||
:see The `id_type` configuration option (default: uuid) determines
|
||||
the type of identifier that Glance generates for an image
|
||||
|
||||
:raises HTTPBadRequest if no x-image-meta-location is missing
|
||||
and the request body is not application/octet-stream
|
||||
image data.
|
||||
@ -195,7 +198,7 @@ class Controller(wsgi.Controller):
|
||||
"mime-encoded as application/"
|
||||
"octet-stream.", request=req)
|
||||
else:
|
||||
if 'x-image-meta-store' in headers_keys:
|
||||
if 'x-image-meta-store' in header_keys:
|
||||
image_store = req.headers['x-image-meta-store']
|
||||
image_status = 'pending' # set to available when stored...
|
||||
image_in_body = True
|
||||
@ -204,7 +207,15 @@ class Controller(wsgi.Controller):
|
||||
image_store = get_store_from_location(image_location)
|
||||
image_status = 'available'
|
||||
|
||||
# If image is the request body, validate that the requested
|
||||
# or default store is capable of storing the image data...
|
||||
if not image_store:
|
||||
image_store = FLAGS.default_store
|
||||
if image_in_body:
|
||||
store = self.get_store_or_400(req, image_store)
|
||||
|
||||
image_meta = util.get_image_meta_from_headers(req)
|
||||
|
||||
image_meta['status'] = image_status
|
||||
image_meta['store'] = image_store
|
||||
|
||||
@ -212,15 +223,19 @@ class Controller(wsgi.Controller):
|
||||
image_meta = registry.add_image_metadata(image_meta)
|
||||
|
||||
if image_in_body:
|
||||
#store = stores.get_store()
|
||||
#store.add_image(req.body)
|
||||
try:
|
||||
location = store.add(image_meta['id'], req.body)
|
||||
except exception.Duplicate, e:
|
||||
return HTTPConflict(str(e), request=req)
|
||||
image_meta['status'] = 'available'
|
||||
registries.update_image(image_meta)
|
||||
image_meta['location'] = location
|
||||
registry.update_image_metadata(image_meta['id'], image_meta)
|
||||
|
||||
return dict(image=image_meta)
|
||||
|
||||
except exception.Duplicate:
|
||||
return HTTPConflict()
|
||||
return HTTPConflict("An image with identifier %s already exists"
|
||||
% image_meta['id'], request=req)
|
||||
except exception.Invalid:
|
||||
return HTTPBadRequest()
|
||||
|
||||
@ -275,6 +290,25 @@ class Controller(wsgi.Controller):
|
||||
request=request,
|
||||
content_type='text/plain')
|
||||
|
||||
def get_store_or_400(self, request, store_name):
|
||||
"""
|
||||
Grabs the storage backend for the supplied store name
|
||||
or raises an HTTPBadRequest (400) response
|
||||
|
||||
:param request: The WSGI/Webob Request object
|
||||
:param id: The opaque image identifier
|
||||
|
||||
:raises HTTPNotFound if image does not exist
|
||||
"""
|
||||
try:
|
||||
return get_backend_class(store_name)
|
||||
except UnsupportedBackend:
|
||||
raise HTTPBadRequest(body='Requested store %s not available '
|
||||
'for storage on this Glance node'
|
||||
% store_name,
|
||||
request=request,
|
||||
content_type='text/plain')
|
||||
|
||||
|
||||
class API(wsgi.Router):
|
||||
|
||||
|
@ -66,7 +66,7 @@ def get_backend_class(backend):
|
||||
try:
|
||||
return BACKENDS[backend]
|
||||
except KeyError:
|
||||
raise UnsupportedBackend("No backend found for '%s'" % scheme)
|
||||
raise UnsupportedBackend("No backend found for '%s'" % backend)
|
||||
|
||||
|
||||
def get_from_backend(uri, **kwargs):
|
||||
|
@ -26,6 +26,11 @@ from glance.common import exception
|
||||
from glance.common import flags
|
||||
import glance.store
|
||||
|
||||
|
||||
flags.DEFINE_string('filesystem_store_datadir', '/var/lib/glance/images/',
|
||||
'Location to write image data. '
|
||||
'Default: /var/lib/glance/images/')
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
|
||||
|
||||
@ -94,4 +99,30 @@ class FilesystemBackend(glance.store.Backend):
|
||||
except OSError:
|
||||
raise exception.NotAuthorized("You cannot delete file %s" % fn)
|
||||
else:
|
||||
raise exception.NotFound("Image file %s does not exist" % fn)
|
||||
raise exception.NotFound("Image file %s does not exist" % fn)
|
||||
|
||||
@classmethod
|
||||
def add(cls, id, data):
|
||||
"""
|
||||
Stores image data to disk and returns a location that the image was
|
||||
written to. By default, the backend writes the image data to a file
|
||||
`/<DATADIR>/<ID>`, where <DATADIR> is the value of
|
||||
FLAGS.filesystem_store_datadir and <ID> is the supplied image ID.
|
||||
|
||||
:param id: The opaque image identifier
|
||||
:param data: The image data to write
|
||||
|
||||
:retval The location that was written, with file:// scheme prepended
|
||||
"""
|
||||
|
||||
filepath = os.path.join(FLAGS.filesystem_store_datadir, str(id))
|
||||
|
||||
if os.path.exists(filepath):
|
||||
raise exception.Duplicate("Image file %s already exists!"
|
||||
% filepath)
|
||||
|
||||
f = open(filepath, 'wb')
|
||||
f.write(data)
|
||||
f.close()
|
||||
|
||||
return 'file://%s' % filepath
|
||||
|
@ -37,7 +37,7 @@ import glance.store.swift
|
||||
import glance.registry.db.sqlalchemy.api
|
||||
|
||||
|
||||
FAKE_FILESYSTEM_ROOTDIR = os.path.join('//tmp', 'glance-tests')
|
||||
FAKE_FILESYSTEM_ROOTDIR = os.path.join('/tmp', 'glance-tests')
|
||||
|
||||
|
||||
def stub_out_http_backend(stubs):
|
||||
@ -356,6 +356,22 @@ def stub_out_registry_db_image_api(stubs):
|
||||
return values
|
||||
|
||||
def image_update(self, _context, image_id, values):
|
||||
|
||||
props = []
|
||||
|
||||
if 'properties' in values.keys():
|
||||
for k,v in values['properties'].iteritems():
|
||||
p = {}
|
||||
p['key'] = k
|
||||
p['value'] = v
|
||||
p['deleted'] = False
|
||||
p['created_at'] = datetime.datetime.utcnow()
|
||||
p['updated_at'] = datetime.datetime.utcnow()
|
||||
p['deleted_at'] = None
|
||||
props.append(p)
|
||||
|
||||
values['properties'] = props
|
||||
|
||||
image = self.image_get(_context, image_id)
|
||||
image.update(values)
|
||||
return image
|
||||
|
@ -22,11 +22,14 @@ import stubout
|
||||
import webob
|
||||
|
||||
from glance import server
|
||||
from glance.common import flags
|
||||
from glance.registry import server as rserver
|
||||
from tests import stubs
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
|
||||
class TestImageController(unittest.TestCase):
|
||||
|
||||
class TestRegistryAPI(unittest.TestCase):
|
||||
def setUp(self):
|
||||
"""Establish a clean test environment"""
|
||||
self.stubs = stubout.StubOutForTesting()
|
||||
@ -230,7 +233,71 @@ class TestImageController(unittest.TestCase):
|
||||
self.assertEquals(res.status_int,
|
||||
webob.exc.HTTPNotFound.code)
|
||||
|
||||
|
||||
class TestGlanceAPI(unittest.TestCase):
|
||||
def setUp(self):
|
||||
"""Establish a clean test environment"""
|
||||
self.stubs = stubout.StubOutForTesting()
|
||||
stubs.stub_out_registry_and_store_server(self.stubs)
|
||||
stubs.stub_out_registry_db_image_api(self.stubs)
|
||||
stubs.stub_out_filesystem_backend()
|
||||
self.orig_filesystem_store_datadir = FLAGS.filesystem_store_datadir
|
||||
FLAGS.filesystem_store_datadir = stubs.FAKE_FILESYSTEM_ROOTDIR
|
||||
|
||||
def tearDown(self):
|
||||
"""Clear the test environment"""
|
||||
FLAGS.filesystem_store_datadir = self.orig_filesystem_store_datadir
|
||||
stubs.clean_out_fake_filesystem_backend()
|
||||
self.stubs.UnsetAll()
|
||||
|
||||
def test_add_image_no_location_no_image_as_body(self):
|
||||
"""Tests raises BadRequest for no body and no loc header"""
|
||||
fixture_headers = {'x-image-meta-store': 'file',
|
||||
'x-image-meta-name': 'fake image #3'}
|
||||
|
||||
req = webob.Request.blank("/images")
|
||||
req.method = 'POST'
|
||||
for k,v in fixture_headers.iteritems():
|
||||
req.headers[k] = v
|
||||
res = req.get_response(server.API())
|
||||
self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code)
|
||||
|
||||
def test_add_image_bad_store(self):
|
||||
"""Tests raises BadRequest for invalid store header"""
|
||||
fixture_headers = {'x-image-meta-store': 'bad',
|
||||
'x-image-meta-name': 'fake image #3'}
|
||||
|
||||
req = webob.Request.blank("/images")
|
||||
req.method = 'POST'
|
||||
for k,v in fixture_headers.iteritems():
|
||||
req.headers[k] = v
|
||||
|
||||
req.headers['Content-Type'] = 'application/octet-stream'
|
||||
req.body = "chunk00000remainder"
|
||||
res = req.get_response(server.API())
|
||||
self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code)
|
||||
|
||||
def test_add_image_basic_file_store(self):
|
||||
"""Tests raises BadRequest for invalid store header"""
|
||||
fixture_headers = {'x-image-meta-store': 'file',
|
||||
'x-image-meta-name': 'fake image #3'}
|
||||
|
||||
req = webob.Request.blank("/images")
|
||||
req.method = 'POST'
|
||||
for k,v in fixture_headers.iteritems():
|
||||
req.headers[k] = v
|
||||
|
||||
req.headers['Content-Type'] = 'application/octet-stream'
|
||||
req.body = "chunk00000remainder"
|
||||
res = req.get_response(server.API())
|
||||
self.assertEquals(res.status_int, 200)
|
||||
|
||||
res_body = json.loads(res.body)['image']
|
||||
self.assertEquals(res_body['location'],
|
||||
'file:///tmp/glance-tests/3')
|
||||
|
||||
def test_image_meta(self):
|
||||
"""Test for HEAD /images/<ID>"""
|
||||
expected_headers = {'x-image-meta-id': 2,
|
||||
'x-image-meta-name': 'fake image #2'}
|
||||
req = webob.Request.blank("/images/2")
|
||||
|
@ -16,6 +16,7 @@
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
import os
|
||||
import stubout
|
||||
import StringIO
|
||||
import unittest
|
||||
@ -24,9 +25,12 @@ import webob
|
||||
|
||||
from glance import client
|
||||
from glance.registry import client as rclient
|
||||
from glance.common import flags
|
||||
from glance.common import exception
|
||||
from tests import stubs
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
|
||||
|
||||
class TestBadClients(unittest.TestCase):
|
||||
|
||||
@ -212,7 +216,7 @@ class TestRegistryClient(unittest.TestCase):
|
||||
'location': "file:///tmp/glance-tests/2",
|
||||
}
|
||||
|
||||
self.assertRaises(client.BadInputError,
|
||||
self.assertRaises(exception.BadInputError,
|
||||
self.client.add_image,
|
||||
fixture)
|
||||
|
||||
@ -279,10 +283,13 @@ class TestClient(unittest.TestCase):
|
||||
stubs.stub_out_registry_db_image_api(self.stubs)
|
||||
stubs.stub_out_registry_and_store_server(self.stubs)
|
||||
stubs.stub_out_filesystem_backend()
|
||||
self.orig_filesystem_store_datadir = FLAGS.filesystem_store_datadir
|
||||
FLAGS.filesystem_store_datadir = stubs.FAKE_FILESYSTEM_ROOTDIR
|
||||
self.client = client.Client()
|
||||
|
||||
def tearDown(self):
|
||||
"""Clear the test environment"""
|
||||
FLAGS.filesystem_store_datadir = self.orig_filesystem_store_datadir
|
||||
stubs.clean_out_fake_filesystem_backend()
|
||||
self.stubs.UnsetAll()
|
||||
|
||||
@ -480,6 +487,85 @@ class TestClient(unittest.TestCase):
|
||||
|
||||
self.assertEquals(data['status'], 'available')
|
||||
|
||||
def test_add_image_with_image_data_as_string(self):
|
||||
"""Tests can add image by passing image data as string"""
|
||||
fixture = {'name': 'fake public image',
|
||||
'is_public': True,
|
||||
'type': 'kernel',
|
||||
'size': 19,
|
||||
'properties': {'distro': 'Ubuntu 10.04 LTS'}
|
||||
}
|
||||
|
||||
image_data_fixture = r"chunk0000remainder"
|
||||
|
||||
new_id = self.client.add_image(fixture, image_data_fixture)
|
||||
|
||||
self.assertEquals(3, new_id)
|
||||
|
||||
new_meta, new_image_chunks = self.client.get_image(3)
|
||||
|
||||
new_image_data = ""
|
||||
for image_chunk in new_image_chunks:
|
||||
new_image_data += image_chunk
|
||||
|
||||
self.assertEquals(image_data_fixture, new_image_data)
|
||||
for k,v in fixture.iteritems():
|
||||
self.assertEquals(v, new_meta[k])
|
||||
|
||||
def test_add_image_with_image_data_as_file(self):
|
||||
"""Tests can add image by passing image data as file"""
|
||||
fixture = {'name': 'fake public image',
|
||||
'is_public': True,
|
||||
'type': 'kernel',
|
||||
'size': 19,
|
||||
'properties': {'distro': 'Ubuntu 10.04 LTS'}
|
||||
}
|
||||
|
||||
image_data_fixture = r"chunk0000remainder"
|
||||
|
||||
tmp_image_filepath = '/tmp/rubbish-image'
|
||||
|
||||
if os.path.exists(tmp_image_filepath):
|
||||
os.unlink(tmp_image_filepath)
|
||||
|
||||
tmp_file = open(tmp_image_filepath, 'wb')
|
||||
tmp_file.write(image_data_fixture)
|
||||
tmp_file.close()
|
||||
|
||||
new_id = self.client.add_image(fixture, open(tmp_image_filepath))
|
||||
|
||||
self.assertEquals(3, new_id)
|
||||
|
||||
if os.path.exists(tmp_image_filepath):
|
||||
os.unlink(tmp_image_filepath)
|
||||
|
||||
new_meta, new_image_chunks = self.client.get_image(3)
|
||||
|
||||
new_image_data = ""
|
||||
for image_chunk in new_image_chunks:
|
||||
new_image_data += image_chunk
|
||||
|
||||
self.assertEquals(image_data_fixture, new_image_data)
|
||||
for k,v in fixture.iteritems():
|
||||
self.assertEquals(v, new_meta[k])
|
||||
|
||||
def test_add_image_with_bad_store(self):
|
||||
"""Tests BadRequest raised when supplying bad store name in meta"""
|
||||
fixture = {'name': 'fake public image',
|
||||
'is_public': True,
|
||||
'type': 'kernel',
|
||||
'size': 19,
|
||||
'store': 'bad',
|
||||
'properties': {'distro': 'Ubuntu 10.04 LTS'}
|
||||
}
|
||||
|
||||
image_data_fixture = r"chunk0000remainder"
|
||||
|
||||
self.assertRaises(exception.BadInputError,
|
||||
self.client.add_image,
|
||||
fixture,
|
||||
image_data_fixture)
|
||||
|
||||
def test_update_image(self):
|
||||
"""Tests that the /images PUT registry API updates the image"""
|
||||
fixture = {'name': 'fake public image #2',
|
||||
|
Loading…
x
Reference in New Issue
Block a user