diff options
Diffstat (limited to 'glance/store/filesystem.py')
-rw-r--r-- | glance/store/filesystem.py | 301 |
1 files changed, 301 insertions, 0 deletions
diff --git a/glance/store/filesystem.py b/glance/store/filesystem.py new file mode 100644 index 0000000..dd9d8ba --- /dev/null +++ b/glance/store/filesystem.py @@ -0,0 +1,301 @@ +# Copyright 2010 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. + +""" +A simple filesystem-backed store +""" + +import errno +import hashlib +import os +import urlparse + +from oslo.config import cfg + +from glance.common import exception +from glance.common import utils +from glance.openstack.common import jsonutils +import glance.openstack.common.log as logging +import glance.store +import glance.store.base +import glance.store.location + +LOG = logging.getLogger(__name__) + +filesystem_opts = [ + cfg.StrOpt('filesystem_store_datadir', + help=_('Directory to which the Filesystem backend ' + 'store writes images.')), + cfg.StrOpt('filesystem_store_metadata_file', + help=_("The path to a file which contains the " + "metadata to be returned with any location " + "associated with this store. The file must " + "contain a valid JSON dict."))] + +CONF = cfg.CONF +CONF.register_opts(filesystem_opts) + + +class StoreLocation(glance.store.location.StoreLocation): + + """Class describing a Filesystem URI""" + + def process_specs(self): + self.scheme = self.specs.get('scheme', 'file') + self.path = self.specs.get('path') + + def get_uri(self): + return "file://%s" % self.path + + def parse_uri(self, uri): + """ + Parse URLs. This method fixes an issue where credentials specified + in the URL are interpreted differently in Python 2.6.1+ than prior + versions of Python. + """ + pieces = urlparse.urlparse(uri) + assert pieces.scheme in ('file', 'filesystem') + self.scheme = pieces.scheme + path = (pieces.netloc + pieces.path).strip() + if path == '': + reason = _("No path specified in URI: %s") % uri + LOG.debug(reason) + raise exception.BadStoreUri('No path specified') + self.path = path + + +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: + if self.fp: + 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 Store(glance.store.base.Store): + + def get_schemes(self): + return ('file', 'filesystem') + + def configure_add(self): + """ + Configure the Store to use the stored configuration options + Any store that needs special configuration should implement + this method. If the store was not able to successfully configure + itself, it should raise `exception.BadStoreConfiguration` + """ + self.datadir = CONF.filesystem_store_datadir + if self.datadir is None: + reason = (_("Could not find %s in configuration options.") % + 'filesystem_store_datadir') + LOG.error(reason) + raise exception.BadStoreConfiguration(store_name="filesystem", + reason=reason) + + if not os.path.exists(self.datadir): + msg = _("Directory to write image files does not exist " + "(%s). Creating.") % self.datadir + LOG.info(msg) + try: + os.makedirs(self.datadir) + except (IOError, OSError): + if os.path.exists(self.datadir): + # NOTE(markwash): If the path now exists, some other + # process must have beat us in the race condition. But it + # doesn't hurt, so we can safely ignore the error. + return + reason = _("Unable to create datadir: %s") % self.datadir + LOG.error(reason) + raise exception.BadStoreConfiguration(store_name="filesystem", + reason=reason) + + @staticmethod + def _resolve_location(location): + filepath = location.store_location.path + + if not os.path.exists(filepath): + raise exception.NotFound(_("Image file %s not found") % filepath) + + filesize = os.path.getsize(filepath) + return filepath, filesize + + def _get_metadata(self): + if CONF.filesystem_store_metadata_file is None: + return {} + + try: + with open(CONF.filesystem_store_metadata_file, 'r') as fptr: + metadata = jsonutils.load(fptr) + glance.store.check_location_metadata(metadata) + return metadata + except glance.store.BackendException as bee: + LOG.error(_('The JSON in the metadata file %s could not be used: ' + '%s An empty dictionary will be returned ' + 'to the client.') + % (CONF.filesystem_store_metadata_file, str(bee))) + return {} + except IOError as ioe: + LOG.error(_('The path for the metadata file %s could not be ' + 'opened: %s An empty dictionary will be returned ' + 'to the client.') + % (CONF.filesystem_store_metadata_file, ioe)) + return {} + except Exception as ex: + LOG.exception(_('An error occurred processing the storage systems ' + 'meta data file: %s. An empty dictionary will be ' + 'returned to the client.') % str(ex)) + return {} + + def get(self, location): + """ + Takes a `glance.store.location.Location` object that indicates + where to find the image file, and returns a tuple of generator + (for reading the image file) and image_size + + :param location `glance.store.location.Location` object, supplied + from glance.store.location.get_location_from_uri() + :raises `glance.exception.NotFound` if image does not exist + """ + filepath, filesize = self._resolve_location(location) + msg = _("Found image at %s. Returning in ChunkedFile.") % filepath + LOG.debug(msg) + return (ChunkedFile(filepath), filesize) + + def get_size(self, location): + """ + Takes a `glance.store.location.Location` object that indicates + where to find the image file and returns the image size + + :param location `glance.store.location.Location` object, supplied + from glance.store.location.get_location_from_uri() + :raises `glance.exception.NotFound` if image does not exist + :rtype int + """ + filepath, filesize = self._resolve_location(location) + msg = _("Found image at %s.") % filepath + LOG.debug(msg) + return filesize + + def delete(self, location): + """ + Takes a `glance.store.location.Location` object that indicates + where to find the image file to delete + + :location `glance.store.location.Location` object, supplied + from glance.store.location.get_location_from_uri() + + :raises NotFound if image does not exist + :raises Forbidden if cannot delete because of permissions + """ + loc = location.store_location + fn = loc.path + if os.path.exists(fn): + try: + LOG.debug(_("Deleting image at %(fn)s"), {'fn': fn}) + os.unlink(fn) + except OSError: + raise exception.Forbidden(_("You cannot delete file %s") % fn) + else: + raise exception.NotFound(_("Image file %s does not exist") % fn) + + def add(self, image_id, image_file, image_size): + """ + Stores an image file with supplied identifier to the backend + storage system and returns a tuple containing information + about the stored image. + + :param image_id: The opaque image identifier + :param image_file: The image data to write, as a file-like object + :param image_size: The size of the image data to write, in bytes + + :retval tuple of URL in backing store, bytes written, checksum + and a dictionary with storage system specific information + :raises `glance.common.exception.Duplicate` if the image already + existed + + :note By default, the backend writes the image data to a file + `/<DATADIR>/<ID>`, where <DATADIR> is the value of + the filesystem_store_datadir configuration option and <ID> + is the supplied image ID. + """ + + filepath = os.path.join(self.datadir, str(image_id)) + + if os.path.exists(filepath): + raise exception.Duplicate(_("Image file %s already exists!") + % filepath) + + checksum = hashlib.md5() + bytes_written = 0 + try: + with open(filepath, 'wb') as f: + for buf in utils.chunkreadable(image_file, + ChunkedFile.CHUNKSIZE): + bytes_written += len(buf) + checksum.update(buf) + f.write(buf) + except IOError as e: + if e.errno != errno.EACCES: + self._delete_partial(filepath, image_id) + exceptions = {errno.EFBIG: exception.StorageFull(), + errno.ENOSPC: exception.StorageFull(), + errno.EACCES: exception.StorageWriteDenied()} + raise exceptions.get(e.errno, e) + except Exception: + self._delete_partial(filepath, image_id) + raise + + checksum_hex = checksum.hexdigest() + metadata = self._get_metadata() + + LOG.debug(_("Wrote %(bytes_written)d bytes to %(filepath)s with " + "checksum %(checksum_hex)s"), + {'bytes_written': bytes_written, + 'filepath': filepath, + 'checksum_hex': checksum_hex}) + return ('file://%s' % filepath, bytes_written, checksum_hex, metadata) + + @staticmethod + def _delete_partial(filepath, id): + try: + os.unlink(filepath) + except Exception as e: + msg = _('Unable to remove partial image data for image %s: %s') + LOG.error(msg % (id, e)) |