# vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2010 OpenStack, LLC # 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. """ A simple filesystem-backed store """ import logging import os import urlparse from glance.common import exception import glance.store logger = logging.getLogger('glance.store.filesystem') class ChunkedFile(object): """ We send this back to the Glance API server as something that can iterate over a large file """ CHUNKSIZE = 65536 def __init__(self, filepath): self.filepath = filepath self.fp = open(self.filepath, 'rb') def __iter__(self): """Return an iterator over the image file""" try: while True: chunk = self.fp.read(ChunkedFile.CHUNKSIZE) if chunk: yield chunk else: break finally: self.close() def close(self): """Close the internal file pointer""" if self.fp: self.fp.close() self.fp = None class FilesystemBackend(glance.store.Backend): @classmethod def get(cls, parsed_uri, expected_size=None, options=None): """ Filesystem-based backend file:///path/to/file.tar.gz.0 """ filepath = parsed_uri.path if not os.path.exists(filepath): raise exception.NotFound("Image file %s not found" % filepath) else: logger.debug("Found image at %s. Returning in ChunkedFile.", filepath) return ChunkedFile(filepath) @classmethod def delete(cls, parsed_uri): """ Removes a file from the filesystem backend. :param parsed_uri: Parsed pieces of URI in form of:: file:///path/to/filename.ext :raises NotFound if file does not exist :raises NotAuthorized if cannot delete because of permissions """ fn = parsed_uri.path if os.path.exists(fn): try: logger.debug("Deleting image at %s", fn) os.unlink(fn) except OSError: raise exception.NotAuthorized("You cannot delete file %s" % fn) else: raise exception.NotFound("Image file %s does not exist" % fn) @classmethod def add(cls, id, data, options): """ 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 `//`, where is the value of options['filesystem_store_datadir'] and is the supplied image ID. :param id: The opaque image identifier :param data: The image data to write, as a file-like object :param options: Conf mapping :retval Tuple with (location, size) The location that was written, with file:// scheme prepended and the size in bytes of the data written """ datadir = options['filesystem_store_datadir'] if not os.path.exists(datadir): logger.info("Directory to write image files does not exist " "(%s). Creating.", datadir) os.makedirs(datadir) filepath = os.path.join(datadir, str(id)) if os.path.exists(filepath): raise exception.Duplicate("Image file %s already exists!" % filepath) bytes_written = 0 with open(filepath, 'wb') as f: while True: buf = data.read(ChunkedFile.CHUNKSIZE) if not buf: break bytes_written += len(buf) f.write(buf) logger.debug("Wrote %(bytes_written)d bytes to %(filepath)s" % locals()) return ('file://%s' % filepath, bytes_written) @classmethod def add_options(cls, parser): """ Adds specific configuration options for this store :param parser: An optparse.OptionParser object :retval None """ parser.add_option('--filesystem-store-datadir', metavar="DIR", default="/var/lib/glance/images/", help="Location to write image data. This directory " "should be writeable by the user that runs the " "glance-api program. Default: %default")