diff options
27 files changed, 1888 insertions, 1250 deletions
diff --git a/gluster/swift/__init__.py b/gluster/swift/__init__.py index 4af47a2..4c41618 100644 --- a/gluster/swift/__init__.py +++ b/gluster/swift/__init__.py @@ -44,6 +44,6 @@ class PkgInfo(object):  ###  ### Change the Package version here  ### -_pkginfo = PkgInfo('1.8.0', '7', 'glusterfs-openstack-swift', False) +_pkginfo = PkgInfo('1.9.0', '0', 'glusterfs-openstack-swift', False)  __version__ = _pkginfo.pretty_version  __canonical_version__ = _pkginfo.canonical_version diff --git a/gluster/swift/account/server.py b/gluster/swift/account/server.py index ca718c3..a2a20af 100644 --- a/gluster/swift/account/server.py +++ b/gluster/swift/account/server.py @@ -24,7 +24,8 @@ from gluster.swift.common.DiskDir import DiskAccount  class AccountController(server.AccountController): -    def _get_account_broker(self, drive, part, account): + +    def _get_account_broker(self, drive, part, account, **kwargs):          """          Overriden to provide the GlusterFS specific broker that talks to          Gluster for the information related to servicing a given request @@ -35,7 +36,7 @@ class AccountController(server.AccountController):          :param account: account name          :returns: DiskDir object          """ -        return DiskAccount(self.root, drive, account, self.logger) +        return DiskAccount(self.root, drive, account, self.logger, **kwargs)  def app_factory(global_conf, **local_conf): diff --git a/gluster/swift/common/DiskDir.py b/gluster/swift/common/DiskDir.py index 556907f..eb0b292 100644 --- a/gluster/swift/common/DiskDir.py +++ b/gluster/swift/common/DiskDir.py @@ -150,7 +150,8 @@ class DiskCommon(object):      """      Common fields and methods shared between DiskDir and DiskAccount classes.      """ -    def __init__(self, root, drive, account, logger): +    def __init__(self, root, drive, account, logger, pending_timeout=None, +                 stale_reads_ok=False):          # WARNING: The following four fields are referenced as fields by our          # callers outside of this module, do not remove.          # Create a dummy db_file in Glusterfs.RUN_DIR @@ -161,8 +162,8 @@ class DiskCommon(object):                  file(_db_file, 'w+')          self.db_file = _db_file          self.metadata = {} -        self.pending_timeout = 0 -        self.stale_reads_ok = False +        self.pending_timeout = pending_timeout or 10 +        self.stale_reads_ok = stale_reads_ok          # The following fields are common          self.root = root          assert logger is not None @@ -287,8 +288,8 @@ class DiskDir(DiskCommon):      """      def __init__(self, path, drive, account, container, logger, -                 uid=DEFAULT_UID, gid=DEFAULT_GID): -        super(DiskDir, self).__init__(path, drive, account, logger) +                 uid=DEFAULT_UID, gid=DEFAULT_GID, **kwargs): +        super(DiskDir, self).__init__(path, drive, account, logger, **kwargs)          self.uid = int(uid)          self.gid = int(gid) @@ -530,8 +531,9 @@ class DiskAccount(DiskCommon):              .update_metadata()      """ -    def __init__(self, root, drive, account, logger): -        super(DiskAccount, self).__init__(root, drive, account, logger) +    def __init__(self, root, drive, account, logger, **kwargs): +        super(DiskAccount, self).__init__(root, drive, account, logger, +                                          **kwargs)          # Since accounts should always exist (given an account maps to a          # gluster volume directly, and the mount has already been checked at diff --git a/gluster/swift/common/exceptions.py b/gluster/swift/common/exceptions.py index ba2364e..010ea24 100644 --- a/gluster/swift/common/exceptions.py +++ b/gluster/swift/common/exceptions.py @@ -44,7 +44,3 @@ class AlreadyExistsAsDir(GlusterfsException):  class AlreadyExistsAsFile(GlusterfsException):      pass - - -class DiskFileNoSpace(GlusterfsException): -    pass diff --git a/gluster/swift/common/fs_utils.py b/gluster/swift/common/fs_utils.py index e624da1..b2935d0 100644 --- a/gluster/swift/common/fs_utils.py +++ b/gluster/swift/common/fs_utils.py @@ -19,7 +19,6 @@ import errno  import stat  import random  import os.path as os_path    # noqa -from eventlet import tpool  from eventlet import sleep  from gluster.swift.common.exceptions import FileOrDirNotFoundError, \      NotDirectoryError, GlusterFileSystemOSError, GlusterFileSystemIOError @@ -243,7 +242,7 @@ def do_rename(old_path, new_path):  def do_fsync(fd):      try: -        tpool.execute(os.fsync, fd) +        os.fsync(fd)      except OSError as err:          raise GlusterFileSystemOSError(              err.errno, '%s, os.fsync("%s")' % (err.strerror, fd)) diff --git a/gluster/swift/common/ring.py b/gluster/swift/common/ring.py index f4df8da..f8c268a 100644 --- a/gluster/swift/common/ring.py +++ b/gluster/swift/common/ring.py @@ -91,6 +91,29 @@ class Ring(ring.Ring):          """          return self._get_part_nodes(part) +    def get_part(self, account, container=None, obj=None): +        """ +        Get the partition for an account/container/object. + +        :param account: account name +        :param container: container name +        :param obj: object name +        :returns: the partition number +        """ +        if account.startswith(reseller_prefix): +            account = account.replace(reseller_prefix, '', 1) + +        # Save the account name in the table +        # This makes part be the index of the location of the account +        # in the list +        try: +            part = self.account_list.index(account) +        except ValueError: +            self.account_list.append(account) +            part = self.account_list.index(account) + +        return part +      def get_nodes(self, account, container=None, obj=None):          """          Get the partition and nodes for an account/container/object. @@ -117,18 +140,7 @@ class Ring(ring.Ring):                  hardware description          ======  ===============================================================          """ -        if account.startswith(reseller_prefix): -            account = account.replace(reseller_prefix, '', 1) - -        # Save the account name in the table -        # This makes part be the index of the location of the account -        # in the list -        try: -            part = self.account_list.index(account) -        except ValueError: -            self.account_list.append(account) -            part = self.account_list.index(account) - +        part = self.get_part(account, container, obj)          return part, self._get_part_nodes(part)      def get_more_nodes(self, part): @@ -141,4 +153,4 @@ class Ring(ring.Ring):          See :func:`get_nodes` for a description of the node dicts.          Should never be called in the swift UFO environment, so yield nothing          """ -        yield self.false_node +        return [] diff --git a/gluster/swift/container/server.py b/gluster/swift/container/server.py index 780a300..e832248 100644 --- a/gluster/swift/container/server.py +++ b/gluster/swift/container/server.py @@ -33,7 +33,7 @@ class ContainerController(server.ContainerController):      directly).      """ -    def _get_container_broker(self, drive, part, account, container): +    def _get_container_broker(self, drive, part, account, container, **kwargs):          """          Overriden to provide the GlusterFS specific broker that talks to          Gluster for the information related to servicing a given request @@ -45,7 +45,8 @@ class ContainerController(server.ContainerController):          :param container: container name          :returns: DiskDir object, a duck-type of DatabaseBroker          """ -        return DiskDir(self.root, drive, account, container, self.logger) +        return DiskDir(self.root, drive, account, container, self.logger, +                       **kwargs)      def account_update(self, req, account, container, broker):          """ diff --git a/gluster/swift/common/DiskFile.py b/gluster/swift/obj/diskfile.py index d64726b..ce69b6d 100644 --- a/gluster/swift/common/DiskFile.py +++ b/gluster/swift/obj/diskfile.py @@ -22,11 +22,12 @@ import logging  from hashlib import md5  from eventlet import sleep  from contextlib import contextmanager -from swift.common.utils import TRUE_VALUES, fallocate -from swift.common.exceptions import DiskFileNotExist, DiskFileError +from swift.common.utils import TRUE_VALUES, drop_buffer_cache, ThreadPool +from swift.common.exceptions import DiskFileNotExist, DiskFileError, \ +    DiskFileNoSpace, DiskFileDeviceUnavailable -from gluster.swift.common.exceptions import GlusterFileSystemOSError, \ -    DiskFileNoSpace +from gluster.swift.common.exceptions import GlusterFileSystemOSError +from gluster.swift.common.Glusterfs import mount  from gluster.swift.common.fs_utils import do_fstat, do_open, do_close, \      do_unlink, do_chown, os_path, do_fsync, do_fchown, do_stat  from gluster.swift.common.utils import read_metadata, write_metadata, \ @@ -37,13 +38,15 @@ from gluster.swift.common.utils import X_CONTENT_LENGTH, X_CONTENT_TYPE, \      FILE_TYPE, DEFAULT_UID, DEFAULT_GID, DIR_NON_OBJECT, DIR_OBJECT  from ConfigParser import ConfigParser, NoSectionError, NoOptionError -from swift.obj.server import DiskFile +from swift.obj.diskfile import DiskFile as SwiftDiskFile +from swift.obj.diskfile import DiskWriter as SwiftDiskWriter  # FIXME: Hopefully we'll be able to move to Python 2.7+ where O_CLOEXEC will  # be back ported. See http://www.python.org/dev/peps/pep-0433/  O_CLOEXEC = 02000000  DEFAULT_DISK_CHUNK_SIZE = 65536 +DEFAULT_BYTES_PER_SYNC = (512 * 1024 * 1024)  # keep these lower-case  DISALLOWED_HEADERS = set('content-length content-type deleted etag'.split()) @@ -235,16 +238,10 @@ if _fs_conf.read(os.path.join('/etc/swift', 'fs.conf')):              in TRUE_VALUES      except (NoSectionError, NoOptionError):          _relaxed_writes = False -    try: -        _preallocate = _fs_conf.get('DEFAULT', 'preallocate', "no") \ -            in TRUE_VALUES -    except (NoSectionError, NoOptionError): -        _preallocate = False  else:      _mkdir_locking = False      _use_put_mount = False      _relaxed_writes = False -    _preallocate = False  if _mkdir_locking:      make_directory = _make_directory_locked @@ -275,7 +272,153 @@ def _adjust_metadata(metadata):      return metadata -class Gluster_DiskFile(DiskFile): +class DiskWriter(SwiftDiskWriter): +    """ +    Encapsulation of the write context for servicing PUT REST API +    requests. Serves as the context manager object for DiskFile's writer() +    method. + +    We just override the put() method for Gluster. +    """ +    def put(self, metadata, extension='.data'): +        """ +        Finalize writing the file on disk, and renames it from the temp file +        to the real location.  This should be called after the data has been +        written to the temp file. + +        :param metadata: dictionary of metadata to be written +        :param extension: extension to be used when making the file +        """ +        # Our caller will use '.data' here; we just ignore it since we map the +        # URL directly to the file system. + +        assert self.tmppath is not None +        metadata = _adjust_metadata(metadata) +        df = self.disk_file + +        if dir_is_object(metadata): +            if not df.data_file: +                # Does not exist, create it +                data_file = os.path.join(df._obj_path, df._obj) +                _, df.metadata = self.threadpool.force_run_in_thread( +                    df._create_dir_object, data_file, metadata) +                df.data_file = os.path.join(df._container_path, data_file) +            elif not df.is_dir: +                # Exists, but as a file +                raise DiskFileError('DiskFile.put(): directory creation failed' +                                    ' since the target, %s, already exists as' +                                    ' a file' % df.data_file) +            return + +        if df._is_dir: +            # A pre-existing directory already exists on the file +            # system, perhaps gratuitously created when another +            # object was created, or created externally to Swift +            # REST API servicing (UFO use case). +            raise DiskFileError('DiskFile.put(): file creation failed since' +                                ' the target, %s, already exists as a' +                                ' directory' % df.data_file) + +        def finalize_put(): +            # Write out metadata before fsync() to ensure it is also forced to +            # disk. +            write_metadata(self.fd, metadata) + +            if not _relaxed_writes: +                # We call fsync() before calling drop_cache() to lower the +                # amount of redundant work the drop cache code will perform on +                # the pages (now that after fsync the pages will be all +                # clean). +                do_fsync(self.fd) +                # From the Department of the Redundancy Department, make sure +                # we call drop_cache() after fsync() to avoid redundant work +                # (pages all clean). +                drop_buffer_cache(self.fd, 0, self.upload_size) + +            # At this point we know that the object's full directory path +            # exists, so we can just rename it directly without using Swift's +            # swift.common.utils.renamer(), which makes the directory path and +            # adds extra stat() calls. +            data_file = os.path.join(df.put_datadir, df._obj) +            while True: +                try: +                    os.rename(self.tmppath, data_file) +                except OSError as err: +                    if err.errno in (errno.ENOENT, errno.EIO): +                        # FIXME: Why either of these two error conditions is +                        # happening is unknown at this point. This might be a +                        # FUSE issue of some sort or a possible race +                        # condition. So let's sleep on it, and double check +                        # the environment after a good nap. +                        _random_sleep() +                        # Tease out why this error occurred. The man page for +                        # rename reads: +                        #   "The link named by tmppath does not exist; or, a +                        #    directory component in data_file does not exist; +                        #    or, tmppath or data_file is an empty string." +                        assert len(self.tmppath) > 0 and len(data_file) > 0 +                        tpstats = do_stat(self.tmppath) +                        tfstats = do_fstat(self.fd) +                        assert tfstats +                        if not tpstats or tfstats.st_ino != tpstats.st_ino: +                            # Temporary file name conflict +                            raise DiskFileError( +                                'DiskFile.put(): temporary file, %s, was' +                                ' already renamed (targeted for %s)' % ( +                                    self.tmppath, data_file)) +                        else: +                            # Data file target name now has a bad path! +                            dfstats = do_stat(self.put_datadir) +                            if not dfstats: +                                raise DiskFileError( +                                    'DiskFile.put(): path to object, %s, no' +                                    ' longer exists (targeted for %s)' % ( +                                        df.put_datadir, +                                        data_file)) +                            else: +                                is_dir = stat.S_ISDIR(dfstats.st_mode) +                                if not is_dir: +                                    raise DiskFileError( +                                        'DiskFile.put(): path to object, %s,' +                                        ' no longer a directory (targeted for' +                                        ' %s)' % (df.put_datadir, +                                                  data_file)) +                                else: +                                    # Let's retry since everything looks okay +                                    logging.warn( +                                        "DiskFile.put(): os.rename('%s','%s')" +                                        " initially failed (%s) but a" +                                        " stat('%s') following that succeeded:" +                                        " %r" % ( +                                            self.tmppath, data_file, +                                            str(err), df.put_datadir, +                                            dfstats)) +                                    continue +                    else: +                        raise GlusterFileSystemOSError( +                            err.errno, "%s, os.rename('%s', '%s')" % ( +                                err.strerror, self.tmppath, data_file)) +                else: +                    # Success! +                    break +            # Close here so the calling context does not have to perform this +            # in a thread. +            do_close(self.fd) + +        self.threadpool.force_run_in_thread(finalize_put) + +        # Avoid the unlink() system call as part of the mkstemp context +        # cleanup +        self.tmppath = None + +        df.metadata = metadata +        df._filter_metadata() + +        # Mark that it actually exists now +        df.data_file = os.path.join(df.datadir, df._obj) + + +class DiskFile(SwiftDiskFile):      """      Manage object files on disk. @@ -294,6 +437,12 @@ class Gluster_DiskFile(DiskFile):      :param logger: logger object for writing out log file messages      :param keep_data_fp: if True, don't close the fp, otherwise close it      :param disk_chunk_Size: size of chunks on file reads +    :param bytes_per_sync: number of bytes between fdatasync calls +    :param iter_hook: called when __iter__ returns a chunk +    :param threadpool: thread pool in which to do blocking operations +    :param obj_dir: ignored +    :param mount_check: check the target device is a mount point and not on the +                        root volume      :param uid: user ID disk object should assume (file or directory)      :param gid: group ID disk object should assume (file or directory)      """ @@ -301,9 +450,16 @@ class Gluster_DiskFile(DiskFile):      def __init__(self, path, device, partition, account, container, obj,                   logger, keep_data_fp=False,                   disk_chunk_size=DEFAULT_DISK_CHUNK_SIZE, -                 uid=DEFAULT_UID, gid=DEFAULT_GID, iter_hook=None): +                 bytes_per_sync=DEFAULT_BYTES_PER_SYNC, iter_hook=None, +                 threadpool=None, obj_dir='objects', mount_check=False, +                 disallowed_metadata_keys=None, uid=DEFAULT_UID, +                 gid=DEFAULT_GID): +        if mount_check and not mount(path, device): +            raise DiskFileDeviceUnavailable()          self.disk_chunk_size = disk_chunk_size +        self.bytes_per_sync = bytes_per_sync          self.iter_hook = iter_hook +        self.threadpool = threadpool or ThreadPool(nthreads=0)          obj = obj.strip(os.path.sep)          if os.path.sep in obj: @@ -326,7 +482,6 @@ class Gluster_DiskFile(DiskFile):          else:              self.put_datadir = self.datadir          self._is_dir = False -        self.tmppath = None          self.logger = logger          self.metadata = {}          self.meta_file = None @@ -365,7 +520,7 @@ class Gluster_DiskFile(DiskFile):              create_object_metadata(data_file)              self.metadata = read_metadata(data_file) -        self.filter_metadata() +        self._filter_metadata()          if not self._is_dir and keep_data_fp:              # The caller has an assumption that the "fp" field of this @@ -390,22 +545,20 @@ class Gluster_DiskFile(DiskFile):              do_close(self.fp)              self.fp = None -    def is_deleted(self): -        """ -        Check if the file is deleted. - -        :returns: True if the file doesn't exist or has been flagged as -                  deleted. -        """ -        return not self.data_file +    def _filter_metadata(self): +        if X_TYPE in self.metadata: +            self.metadata.pop(X_TYPE) +        if X_OBJECT_TYPE in self.metadata: +            self.metadata.pop(X_OBJECT_TYPE)      def _create_dir_object(self, dir_path, metadata=None):          """          Create a directory object at the specified path. No check is made to -        see if the directory object already exists, that is left to the -        caller (this avoids a potentially duplicate stat() system call). +        see if the directory object already exists, that is left to the caller +        (this avoids a potentially duplicate stat() system call). -        The "dir_path" must be relative to its container, self._container_path. +        The "dir_path" must be relative to its container, +        self._container_path.          The "metadata" object is an optional set of metadata to apply to the          newly created directory object. If not present, no initial metadata is @@ -455,233 +608,8 @@ class Gluster_DiskFile(DiskFile):              child = stack.pop() if stack else None          return True, newmd -    def put_metadata(self, metadata, tombstone=False): -        """ -        Short hand for putting metadata to .meta and .ts files. - -        :param metadata: dictionary of metadata to be written -        :param tombstone: whether or not we are writing a tombstone -        """ -        if tombstone: -            # We don't write tombstone files. So do nothing. -            return -        assert self.data_file is not None, \ -            "put_metadata: no file to put metadata into" -        metadata = _adjust_metadata(metadata) -        write_metadata(self.data_file, metadata) -        self.metadata = metadata -        self.filter_metadata() - -    def put(self, fd, metadata, extension='.data'): -        """ -        Finalize writing the file on disk, and renames it from the temp file -        to the real location.  This should be called after the data has been -        written to the temp file. - -        :param fd: file descriptor of the temp file -        :param metadata: dictionary of metadata to be written -        :param extension: extension to be used when making the file -        """ -        # Our caller will use '.data' here; we just ignore it since we map the -        # URL directly to the file system. - -        metadata = _adjust_metadata(metadata) - -        if dir_is_object(metadata): -            if not self.data_file: -                # Does not exist, create it -                data_file = os.path.join(self._obj_path, self._obj) -                _, self.metadata = self._create_dir_object(data_file, metadata) -                self.data_file = os.path.join(self._container_path, data_file) -            elif not self.is_dir: -                # Exists, but as a file -                raise DiskFileError('DiskFile.put(): directory creation failed' -                                    ' since the target, %s, already exists as' -                                    ' a file' % self.data_file) -            return - -        if self._is_dir: -            # A pre-existing directory already exists on the file -            # system, perhaps gratuitously created when another -            # object was created, or created externally to Swift -            # REST API servicing (UFO use case). -            raise DiskFileError('DiskFile.put(): file creation failed since' -                                ' the target, %s, already exists as a' -                                ' directory' % self.data_file) - -        # Write out metadata before fsync() to ensure it is also forced to -        # disk. -        write_metadata(fd, metadata) - -        if not _relaxed_writes: -            do_fsync(fd) -            if X_CONTENT_LENGTH in metadata: -                # Don't bother doing this before fsync in case the OS gets any -                # ideas to issue partial writes. -                fsize = int(metadata[X_CONTENT_LENGTH]) -                self.drop_cache(fd, 0, fsize) - -        # At this point we know that the object's full directory path exists, -        # so we can just rename it directly without using Swift's -        # swift.common.utils.renamer(), which makes the directory path and -        # adds extra stat() calls. -        data_file = os.path.join(self.put_datadir, self._obj) -        while True: -            try: -                os.rename(self.tmppath, data_file) -            except OSError as err: -                if err.errno in (errno.ENOENT, errno.EIO): -                    # FIXME: Why either of these two error conditions is -                    # happening is unknown at this point. This might be a FUSE -                    # issue of some sort or a possible race condition. So -                    # let's sleep on it, and double check the environment -                    # after a good nap. -                    _random_sleep() -                    # Tease out why this error occurred. The man page for -                    # rename reads: -                    #   "The link named by tmppath does not exist; or, a -                    #    directory component in data_file does not exist; -                    #    or, tmppath or data_file is an empty string." -                    assert len(self.tmppath) > 0 and len(data_file) > 0 -                    tpstats = do_stat(self.tmppath) -                    tfstats = do_fstat(fd) -                    assert tfstats -                    if not tpstats or tfstats.st_ino != tpstats.st_ino: -                        # Temporary file name conflict -                        raise DiskFileError('DiskFile.put(): temporary file,' -                                            ' %s, was already renamed' -                                            ' (targeted for %s)' % ( -                                                self.tmppath, data_file)) -                    else: -                        # Data file target name now has a bad path! -                        dfstats = do_stat(self.put_datadir) -                        if not dfstats: -                            raise DiskFileError('DiskFile.put(): path to' -                                                ' object, %s, no longer exists' -                                                ' (targeted for %s)' % ( -                                                    self.put_datadir, -                                                    data_file)) -                        else: -                            is_dir = stat.S_ISDIR(dfstats.st_mode) -                            if not is_dir: -                                raise DiskFileError('DiskFile.put(): path to' -                                                    ' object, %s, no longer a' -                                                    ' directory (targeted for' -                                                    ' %s)' % (self.put_datadir, -                                                              data_file)) -                            else: -                                # Let's retry since everything looks okay -                                logging.warn("DiskFile.put(): os.rename('%s'," -                                             "'%s') initially failed (%s) but" -                                             " a stat('%s') following that" -                                             " succeeded: %r" % ( -                                                 self.tmppath, data_file, -                                                 str(err), self.put_datadir, -                                                 dfstats)) -                                continue -                else: -                    raise GlusterFileSystemOSError( -                        err.errno, "%s, os.rename('%s', '%s')" % ( -                            err.strerror, self.tmppath, data_file)) -            else: -                # Success! -                break - -        # Avoid the unlink() system call as part of the mkstemp context cleanup -        self.tmppath = None - -        self.metadata = metadata -        self.filter_metadata() - -        # Mark that it actually exists now -        self.data_file = os.path.join(self.datadir, self._obj) - -    def unlinkold(self, timestamp): -        """ -        Remove any older versions of the object file.  Any file that has an -        older timestamp than timestamp will be deleted. - -        :param timestamp: timestamp to compare with each file -        """ -        if not self.metadata or self.metadata[X_TIMESTAMP] >= timestamp: -            return - -        assert self.data_file, \ -            "Have metadata, %r, but no data_file" % self.metadata - -        if self._is_dir: -            # Marker, or object, directory. -            # -            # Delete from the filesystem only if it contains -            # no objects.  If it does contain objects, then just -            # remove the object metadata tag which will make this directory a -            # fake-filesystem-only directory and will be deleted -            # when the container or parent directory is deleted. -            metadata = read_metadata(self.data_file) -            if dir_is_object(metadata): -                metadata[X_OBJECT_TYPE] = DIR_NON_OBJECT -                write_metadata(self.data_file, metadata) -            rmobjdir(self.data_file) - -        else: -            # Delete file object -            do_unlink(self.data_file) - -        # Garbage collection of non-object directories. -        # Now that we deleted the file, determine -        # if the current directory and any parent -        # directory may be deleted. -        dirname = os.path.dirname(self.data_file) -        while dirname and dirname != self._container_path: -            # Try to remove any directories that are not -            # objects. -            if not rmobjdir(dirname): -                # If a directory with objects has been -                # found, we can stop garabe collection -                break -            else: -                dirname = os.path.dirname(dirname) - -        self.metadata = {} -        self.data_file = None - -    def get_data_file_size(self): -        """ -        Returns the os_path.getsize for the file.  Raises an exception if this -        file does not match the Content-Length stored in the metadata, or if -        self.data_file does not exist. - -        :returns: file size as an int -        :raises DiskFileError: on file size mismatch. -        :raises DiskFileNotExist: on file not existing (including deleted) -        """ -        #Marker directory. -        if self._is_dir: -            return 0 -        try: -            file_size = 0 -            if self.data_file: -                file_size = os_path.getsize(self.data_file) -                if X_CONTENT_LENGTH in self.metadata: -                    metadata_size = int(self.metadata[X_CONTENT_LENGTH]) -                    if file_size != metadata_size: -                        self.metadata[X_CONTENT_LENGTH] = file_size -                        write_metadata(self.data_file, self.metadata) - -                return file_size -        except OSError as err: -            if err.errno != errno.ENOENT: -                raise -        raise DiskFileNotExist('Data File does not exist.') - -    def filter_metadata(self): -        if X_TYPE in self.metadata: -            self.metadata.pop(X_TYPE) -        if X_OBJECT_TYPE in self.metadata: -            self.metadata.pop(X_OBJECT_TYPE) -      @contextmanager -    def mkstemp(self, size=None): +    def writer(self, size=None):          """          Contextmanager to make a temporary file, optionally of a specified          initial size. @@ -706,9 +634,7 @@ class Gluster_DiskFile(DiskFile):              except GlusterFileSystemOSError as gerr:                  if gerr.errno == errno.ENOSPC:                      # Raise DiskFileNoSpace to be handled by upper layers -                    excp = DiskFileNoSpace() -                    excp.drive = os.path.basename(self.device_path) -                    raise excp +                    raise DiskFileNoSpace()                  if gerr.errno == errno.EEXIST:                      # Retry with a different random number.                      continue @@ -750,30 +676,117 @@ class Gluster_DiskFile(DiskFile):                                  ' create a temporary file without running'                                  ' into a name conflict after 1,000 attempts'                                  ' for: %s' % (data_file,)) - -        self.tmppath = tmppath - +        dw = None          try:              # Ensure it is properly owned before we make it available.              do_fchown(fd, self.uid, self.gid) -            if _preallocate and size: -                # For XFS, fallocate() turns off speculative pre-allocation -                # until a write is issued either to the last block of the file -                # before the EOF or beyond the EOF. This means that we are -                # less likely to fragment free space with pre-allocated -                # extents that get truncated back to the known file size. -                # However, this call also turns holes into allocated but -                # unwritten extents, so that allocation occurs before the -                # write, not during XFS writeback. This effectively defeats -                # any allocation optimizations the filesystem can make at -                # writeback time. -                fallocate(fd, size) -            yield fd +            # NOTE: we do not perform the fallocate() call at all. We ignore +            # it completely. +            dw = DiskWriter(self, fd, tmppath, self.threadpool) +            yield dw          finally:              try: -                do_close(fd) +                if dw.fd: +                    do_close(dw.fd)              except OSError:                  pass -            if self.tmppath: -                tmppath, self.tmppath = self.tmppath, None -                do_unlink(tmppath) +            if dw.tmppath: +                do_unlink(dw.tmppath) + +    def put_metadata(self, metadata, tombstone=False): +        """ +        Short hand for putting metadata to .meta and .ts files. + +        :param metadata: dictionary of metadata to be written +        :param tombstone: whether or not we are writing a tombstone +        """ +        if tombstone: +            # We don't write tombstone files. So do nothing. +            return +        assert self.data_file is not None, \ +            "put_metadata: no file to put metadata into" +        metadata = _adjust_metadata(metadata) +        self.threadpool.run_in_thread(write_metadata, self.data_file, metadata) +        self.metadata = metadata +        self._filter_metadata() + +    def unlinkold(self, timestamp): +        """ +        Remove any older versions of the object file.  Any file that has an +        older timestamp than timestamp will be deleted. + +        :param timestamp: timestamp to compare with each file +        """ +        if not self.metadata or self.metadata[X_TIMESTAMP] >= timestamp: +            return + +        assert self.data_file, \ +            "Have metadata, %r, but no data_file" % self.metadata + +        def _unlinkold(): +            if self._is_dir: +                # Marker, or object, directory. +                # +                # Delete from the filesystem only if it contains no objects. +                # If it does contain objects, then just remove the object +                # metadata tag which will make this directory a +                # fake-filesystem-only directory and will be deleted when the +                # container or parent directory is deleted. +                metadata = read_metadata(self.data_file) +                if dir_is_object(metadata): +                    metadata[X_OBJECT_TYPE] = DIR_NON_OBJECT +                    write_metadata(self.data_file, metadata) +                rmobjdir(self.data_file) +            else: +                # Delete file object +                do_unlink(self.data_file) + +            # Garbage collection of non-object directories.  Now that we +            # deleted the file, determine if the current directory and any +            # parent directory may be deleted. +            dirname = os.path.dirname(self.data_file) +            while dirname and dirname != self._container_path: +                # Try to remove any directories that are not objects. +                if not rmobjdir(dirname): +                    # If a directory with objects has been found, we can stop +                    # garabe collection +                    break +                else: +                    dirname = os.path.dirname(dirname) + +        self.threadpool.run_in_thread(_unlinkold) + +        self.metadata = {} +        self.data_file = None + +    def get_data_file_size(self): +        """ +        Returns the os_path.getsize for the file.  Raises an exception if this +        file does not match the Content-Length stored in the metadata, or if +        self.data_file does not exist. + +        :returns: file size as an int +        :raises DiskFileError: on file size mismatch. +        :raises DiskFileNotExist: on file not existing (including deleted) +        """ +        #Marker directory. +        if self._is_dir: +            return 0 +        try: +            file_size = 0 +            if self.data_file: +                def _old_getsize(): +                    file_size = os_path.getsize(self.data_file) +                    if X_CONTENT_LENGTH in self.metadata: +                        metadata_size = int(self.metadata[X_CONTENT_LENGTH]) +                        if file_size != metadata_size: +                            # FIXME - bit rot detection? +                            self.metadata[X_CONTENT_LENGTH] = file_size +                            write_metadata(self.data_file, self.metadata) +                    return file_size +                file_size = self.threadpool.run_in_thread(_old_getsize) +                return file_size +        except OSError as err: +            if err.errno != errno.ENOENT: +                raise +        raise DiskFileNotExist('Data File does not exist.') diff --git a/gluster/swift/obj/server.py b/gluster/swift/obj/server.py index b3747ab..bdd7687 100644 --- a/gluster/swift/obj/server.py +++ b/gluster/swift/obj/server.py @@ -13,20 +13,15 @@  # See the License for the specific language governing permissions and  # limitations under the License. -""" Object Server for Gluster Swift UFO """ +""" Object Server for Gluster for Swift """  # Simply importing this monkey patches the constraint handling to fit our  # needs -from swift.obj import server -import gluster.swift.common.utils          # noqa  import gluster.swift.common.constraints    # noqa -from swift.common.utils import public, timing_stats -from gluster.swift.common.DiskFile import Gluster_DiskFile -from gluster.swift.common.exceptions import DiskFileNoSpace -from swift.common.swob import HTTPInsufficientStorage -# Monkey patch the object server module to use Gluster's DiskFile definition -server.DiskFile = Gluster_DiskFile +from swift.obj import server + +from gluster.swift.obj.diskfile import DiskFile  class ObjectController(server.ObjectController): @@ -37,6 +32,18 @@ class ObjectController(server.ObjectController):      operations directly).      """ +    def _diskfile(self, device, partition, account, container, obj, **kwargs): +        """Utility method for instantiating a DiskFile.""" +        kwargs.setdefault('mount_check', self.mount_check) +        kwargs.setdefault('bytes_per_sync', self.bytes_per_sync) +        kwargs.setdefault('disk_chunk_size', self.disk_chunk_size) +        kwargs.setdefault('threadpool', self.threadpools[device]) +        kwargs.setdefault('obj_dir', server.DATADIR) +        kwargs.setdefault('disallowed_metadata_keys', +                          server.DISALLOWED_HEADERS) +        return DiskFile(self.devices, device, partition, account, +                        container, obj, self.logger, **kwargs) +      def container_update(self, op, account, container, obj, request,                           headers_out, objdevice):          """ @@ -56,15 +63,6 @@ class ObjectController(server.ObjectController):          """          return -    @public -    @timing_stats() -    def PUT(self, request): -        try: -            return server.ObjectController.PUT(self, request) -        except DiskFileNoSpace as err: -            drive = err.drive -            return HTTPInsufficientStorage(drive=drive, request=request) -  def app_factory(global_conf, **local_conf):      """paste.deploy app factory for creating WSGI object server apps""" diff --git a/glusterfs-openstack-swift.spec b/glusterfs-openstack-swift.spec index 71439f0..fd6a0a1 100644 --- a/glusterfs-openstack-swift.spec +++ b/glusterfs-openstack-swift.spec @@ -39,11 +39,11 @@ BuildRequires: python-setuptools  Requires : memcached  Requires : openssl  Requires : python -Requires : openstack-swift >= 1.8.0 -Requires : openstack-swift-account >= 1.8.0 -Requires : openstack-swift-container >= 1.8.0 -Requires : openstack-swift-object >= 1.8.0 -Requires : openstack-swift-proxy >= 1.8.0 +Requires : openstack-swift >= 1.9.1 +Requires : openstack-swift-account >= 1.9.1 +Requires : openstack-swift-container >= 1.9.1 +Requires : openstack-swift-object >= 1.9.1 +Requires : openstack-swift-proxy >= 1.9.1  Obsoletes: glusterfs-swift-plugin  Obsoletes: glusterfs-swift  Obsoletes: glusterfs-ufo diff --git a/test/unit/__init__.py b/test/unit/__init__.py index e90553f..04895b4 100644 --- a/test/unit/__init__.py +++ b/test/unit/__init__.py @@ -1,10 +1,9 @@  """ Swift tests """ -import sys  import os  import copy -import logging  import errno +import logging  from sys import exc_info  from contextlib import contextmanager  from collections import defaultdict @@ -13,13 +12,98 @@ from eventlet.green import socket  from tempfile import mkdtemp  from shutil import rmtree  from test import get_config -from ConfigParser import MissingSectionHeaderError -from StringIO import StringIO -from swift.common.utils import readconf, config_true_value -from logging import Handler +from swift.common.utils import config_true_value  from hashlib import md5 -from eventlet import sleep, spawn, Timeout +from eventlet import sleep, Timeout  import logging.handlers +from httplib import HTTPException + + +class DebugLogger(object): +    """A simple stdout logger for eventlet wsgi.""" + +    def write(self, *args): +        print args + + +class FakeRing(object): + +    def __init__(self, replicas=3, max_more_nodes=0): +        # 9 total nodes (6 more past the initial 3) is the cap, no matter if +        # this is set higher, or R^2 for R replicas +        self.replicas = replicas +        self.max_more_nodes = max_more_nodes +        self.devs = {} + +    def set_replicas(self, replicas): +        self.replicas = replicas +        self.devs = {} + +    @property +    def replica_count(self): +        return self.replicas + +    def get_part(self, account, container=None, obj=None): +        return 1 + +    def get_nodes(self, account, container=None, obj=None): +        devs = [] +        for x in xrange(self.replicas): +            devs.append(self.devs.get(x)) +            if devs[x] is None: +                self.devs[x] = devs[x] = \ +                    {'ip': '10.0.0.%s' % x, +                     'port': 1000 + x, +                     'device': 'sd' + (chr(ord('a') + x)), +                     'zone': x % 3, +                     'region': x % 2, +                     'id': x} +        return 1, devs + +    def get_part_nodes(self, part): +        return self.get_nodes('blah')[1] + +    def get_more_nodes(self, part): +        # replicas^2 is the true cap +        for x in xrange(self.replicas, min(self.replicas + self.max_more_nodes, +                                           self.replicas * self.replicas)): +            yield {'ip': '10.0.0.%s' % x, +                   'port': 1000 + x, +                   'device': 'sda', +                   'zone': x % 3, +                   'region': x % 2, +                   'id': x} + + +class FakeMemcache(object): + +    def __init__(self): +        self.store = {} + +    def get(self, key): +        return self.store.get(key) + +    def keys(self): +        return self.store.keys() + +    def set(self, key, value, time=0): +        self.store[key] = value +        return True + +    def incr(self, key, time=0): +        self.store[key] = self.store.setdefault(key, 0) + 1 +        return self.store[key] + +    @contextmanager +    def soft_lock(self, key, timeout=0, retries=5): +        yield True + +    def delete(self, key): +        try: +            del self.store[key] +        except Exception: +            pass +        return True  def readuntil2crlfs(fd): @@ -28,6 +112,8 @@ def readuntil2crlfs(fd):      crlfs = 0      while crlfs < 2:          c = fd.read(1) +        if not c: +            raise ValueError("didn't get two CRLFs; just got %r" % rv)          rv = rv + c          if c == '\r' and lc != '\n':              crlfs = 0 @@ -137,6 +223,7 @@ class FakeLogger(object):      def exception(self, *args, **kwargs):          self.log_dict['exception'].append((args, kwargs, str(exc_info()[1]))) +        print 'FakeLogger Exception: %s' % self.log_dict      # mock out the StatsD logging methods:      increment = _store_in('increment') @@ -279,7 +366,7 @@ def fake_http_connect(*code_iter, **kwargs):      class FakeConn(object):          def __init__(self, status, etag=None, body='', timestamp='1', -                     expect_status=None): +                     expect_status=None, headers=None):              self.status = status              if expect_status is None:                  self.expect_status = self.status @@ -292,6 +379,7 @@ def fake_http_connect(*code_iter, **kwargs):              self.received = 0              self.etag = etag              self.body = body +            self.headers = headers or {}              self.timestamp = timestamp          def getresponse(self): @@ -323,9 +411,12 @@ def fake_http_connect(*code_iter, **kwargs):                         'x-timestamp': self.timestamp,                         'last-modified': self.timestamp,                         'x-object-meta-test': 'testing', +                       'x-delete-at': '9876543210',                         'etag': etag, -                       'x-works': 'yes', -                       'x-account-container-count': kwargs.get('count', 12345)} +                       'x-works': 'yes'} +            if self.status // 100 == 2: +                headers['x-account-container-count'] = \ +                    kwargs.get('count', 12345)              if not self.timestamp:                  del headers['x-timestamp']              try: @@ -335,8 +426,7 @@ def fake_http_connect(*code_iter, **kwargs):                  pass              if 'slow' in kwargs:                  headers['content-length'] = '4' -            if 'headers' in kwargs: -                headers.update(kwargs['headers']) +            headers.update(self.headers)              return headers.items()          def read(self, amt=None): @@ -360,6 +450,11 @@ def fake_http_connect(*code_iter, **kwargs):      timestamps_iter = iter(kwargs.get('timestamps') or ['1'] * len(code_iter))      etag_iter = iter(kwargs.get('etags') or [None] * len(code_iter)) +    if isinstance(kwargs.get('headers'), list): +        headers_iter = iter(kwargs['headers']) +    else: +        headers_iter = iter([kwargs.get('headers', {})] * len(code_iter)) +      x = kwargs.get('missing_container', [False] * len(code_iter))      if not isinstance(x, (tuple, list)):          x = [x] * len(code_iter) @@ -384,6 +479,7 @@ def fake_http_connect(*code_iter, **kwargs):          else:              expect_status = status          etag = etag_iter.next() +        headers = headers_iter.next()          timestamp = timestamps_iter.next()          if status <= 0: @@ -393,6 +489,6 @@ def fake_http_connect(*code_iter, **kwargs):          else:              body = body_iter.next()          return FakeConn(status, etag, body=body, timestamp=timestamp, -                        expect_status=expect_status) +                        expect_status=expect_status, headers=headers)      return connect diff --git a/test/unit/common/data/README.rings b/test/unit/common/data/README.rings index 6457501..4ff802e 100644 --- a/test/unit/common/data/README.rings +++ b/test/unit/common/data/README.rings @@ -1,3 +1,3 @@  The unit tests expect certain ring data built using the following command: -    ../../../../bin/gluster-swift-gen-builders test iops
\ No newline at end of file +    ../../../../bin/gluster-swift-gen-builders test iops diff --git a/test/unit/common/data/account.builder b/test/unit/common/data/account.builder Binary files differindex 090ba4b..c3c0a33 100644 --- a/test/unit/common/data/account.builder +++ b/test/unit/common/data/account.builder diff --git a/test/unit/common/data/account.ring.gz b/test/unit/common/data/account.ring.gz Binary files differindex 6d4c854..dc34c31 100644 --- a/test/unit/common/data/account.ring.gz +++ b/test/unit/common/data/account.ring.gz diff --git a/test/unit/common/data/backups/1365124498.account.builder b/test/unit/common/data/backups/1365124498.account.builder Binary files differdeleted file mode 100644 index 090ba4b..0000000 --- a/test/unit/common/data/backups/1365124498.account.builder +++ /dev/null diff --git a/test/unit/common/data/backups/1365124498.container.builder b/test/unit/common/data/backups/1365124498.container.builder Binary files differdeleted file mode 100644 index 733d27d..0000000 --- a/test/unit/common/data/backups/1365124498.container.builder +++ /dev/null diff --git a/test/unit/common/data/backups/1365124498.object.builder b/test/unit/common/data/backups/1365124498.object.builder Binary files differdeleted file mode 100644 index ff877ec..0000000 --- a/test/unit/common/data/backups/1365124498.object.builder +++ /dev/null diff --git a/test/unit/common/data/backups/1365124499.object.builder b/test/unit/common/data/backups/1365124499.object.builder Binary files differdeleted file mode 100644 index 8b8cd6c..0000000 --- a/test/unit/common/data/backups/1365124499.object.builder +++ /dev/null diff --git a/test/unit/common/data/container.builder b/test/unit/common/data/container.builder Binary files differindex 733d27d..22b9b80 100644 --- a/test/unit/common/data/container.builder +++ b/test/unit/common/data/container.builder diff --git a/test/unit/common/data/container.ring.gz b/test/unit/common/data/container.ring.gz Binary files differindex 592b84b..269a1eb 100644 --- a/test/unit/common/data/container.ring.gz +++ b/test/unit/common/data/container.ring.gz diff --git a/test/unit/common/data/object.builder b/test/unit/common/data/object.builder Binary files differindex 8b8cd6c..b5bdda9 100644 --- a/test/unit/common/data/object.builder +++ b/test/unit/common/data/object.builder diff --git a/test/unit/common/data/object.ring.gz b/test/unit/common/data/object.ring.gz Binary files differindex d2f7192..1c8199a 100644 --- a/test/unit/common/data/object.ring.gz +++ b/test/unit/common/data/object.ring.gz diff --git a/test/unit/common/test_diskdir.py b/test/unit/common/test_diskdir.py index bbdb168..f32c3ad 100644 --- a/test/unit/common/test_diskdir.py +++ b/test/unit/common/test_diskdir.py @@ -272,8 +272,8 @@ class TestDiskCommon(unittest.TestCase):                              self.fake_accounts[0], self.fake_logger)          assert dc.metadata == {}          assert dc.db_file == dd._db_file -        assert dc.pending_timeout == 0 -        assert dc.stale_reads_ok == False +        assert dc.pending_timeout == 10 +        assert dc.stale_reads_ok is False          assert dc.root == self.td          assert dc.logger == self.fake_logger          assert dc.account == self.fake_accounts[0] @@ -290,8 +290,8 @@ class TestDiskCommon(unittest.TestCase):          dc._dir_exists_read_metadata()          assert dc.metadata == fake_md, repr(dc.metadata)          assert dc.db_file == dd._db_file -        assert dc.pending_timeout == 0 -        assert dc.stale_reads_ok == False +        assert dc.pending_timeout == 10 +        assert dc.stale_reads_ok is False          assert dc.root == self.td          assert dc.logger == self.fake_logger          assert dc.account == self.fake_accounts[0] @@ -303,8 +303,8 @@ class TestDiskCommon(unittest.TestCase):          dc._dir_exists_read_metadata()          assert dc.metadata == {}          assert dc.db_file == dd._db_file -        assert dc.pending_timeout == 0 -        assert dc.stale_reads_ok == False +        assert dc.pending_timeout == 10 +        assert dc.stale_reads_ok is False          assert dc.root == self.td          assert dc.logger == self.fake_logger          assert dc.account == "dne0" diff --git a/test/unit/common/test_ring.py b/test/unit/common/test_ring.py index 32dd7bb..de32c7b 100644 --- a/test/unit/common/test_ring.py +++ b/test/unit/common/test_ring.py @@ -51,6 +51,10 @@ class TestRing(unittest.TestCase):          for node in self.ring.get_more_nodes(0):              assert node['device'] == 'volume_not_in_ring' +    def test_second_device_part(self): +        part = self.ring.get_part('iops') +        assert part == 0 +      def test_second_device_with_reseller_prefix(self):          part, node = self.ring.get_nodes('AUTH_iops')          assert node[0]['device'] == 'iops' diff --git a/test/unit/common/test_diskfile.py b/test/unit/obj/test_diskfile.py index 410f113..4686a19 100644 --- a/test/unit/common/test_diskfile.py +++ b/test/unit/obj/test_diskfile.py @@ -13,7 +13,7 @@  # See the License for the specific language governing permissions and  # limitations under the License. -""" Tests for gluster.swift.common.DiskFile """ +""" Tests for gluster.swift.obj.diskfile """  import os  import stat @@ -26,20 +26,20 @@ from mock import patch  from hashlib import md5  import gluster.swift.common.utils -import gluster.swift.common.DiskFile +import gluster.swift.obj.diskfile  from swift.common.utils import normalize_timestamp -from gluster.swift.common.DiskFile import Gluster_DiskFile -from swift.common.exceptions import DiskFileNotExist, DiskFileError +from gluster.swift.obj.diskfile import DiskFile +from swift.common.exceptions import DiskFileNotExist, DiskFileError, \ +    DiskFileNoSpace  from gluster.swift.common.utils import DEFAULT_UID, DEFAULT_GID, X_TYPE, \      X_OBJECT_TYPE, DIR_OBJECT -from test_utils import _initxattr, _destroyxattr +from test.unit.common.test_utils import _initxattr, _destroyxattr  from test.unit import FakeLogger -from gluster.swift.common.exceptions import * -  _metadata = {}  def _mock_read_metadata(filename): +    global _metadata      if filename in _metadata:          md = _metadata[filename]      else: @@ -47,9 +47,11 @@ def _mock_read_metadata(filename):      return md  def _mock_write_metadata(filename, metadata): +    global _metadata      _metadata[filename] = metadata  def _mock_clear_metadata(): +    global _metadata      _metadata = {} @@ -58,7 +60,7 @@ class MockException(Exception):  def _mock_rmobjdir(p): -    raise MockException("gluster.swift.common.DiskFile.rmobjdir() called") +    raise MockException("gluster.swift.obj.diskfile.rmobjdir() called")  def _mock_do_fsync(fd):      return @@ -72,36 +74,35 @@ def _mock_renamer(a, b):  class TestDiskFile(unittest.TestCase): -    """ Tests for gluster.swift.common.DiskFile """ +    """ Tests for gluster.swift.obj.diskfile """      def setUp(self):          self.lg = FakeLogger()          _initxattr()          _mock_clear_metadata() -        self._saved_df_wm = gluster.swift.common.DiskFile.write_metadata -        self._saved_df_rm = gluster.swift.common.DiskFile.read_metadata -        gluster.swift.common.DiskFile.write_metadata = _mock_write_metadata -        gluster.swift.common.DiskFile.read_metadata = _mock_read_metadata +        self._saved_df_wm = gluster.swift.obj.diskfile.write_metadata +        self._saved_df_rm = gluster.swift.obj.diskfile.read_metadata +        gluster.swift.obj.diskfile.write_metadata = _mock_write_metadata +        gluster.swift.obj.diskfile.read_metadata = _mock_read_metadata          self._saved_ut_wm = gluster.swift.common.utils.write_metadata          self._saved_ut_rm = gluster.swift.common.utils.read_metadata          gluster.swift.common.utils.write_metadata = _mock_write_metadata          gluster.swift.common.utils.read_metadata = _mock_read_metadata -        self._saved_do_fsync = gluster.swift.common.DiskFile.do_fsync -        gluster.swift.common.DiskFile.do_fsync = _mock_do_fsync +        self._saved_do_fsync = gluster.swift.obj.diskfile.do_fsync +        gluster.swift.obj.diskfile.do_fsync = _mock_do_fsync      def tearDown(self):          self.lg = None          _destroyxattr() -        gluster.swift.common.DiskFile.write_metadata = self._saved_df_wm -        gluster.swift.common.DiskFile.read_metadata = self._saved_df_rm +        gluster.swift.obj.diskfile.write_metadata = self._saved_df_wm +        gluster.swift.obj.diskfile.read_metadata = self._saved_df_rm          gluster.swift.common.utils.write_metadata = self._saved_ut_wm          gluster.swift.common.utils.read_metadata = self._saved_ut_rm -        gluster.swift.common.DiskFile.do_fsync = self._saved_do_fsync +        gluster.swift.obj.diskfile.do_fsync = self._saved_do_fsync      def test_constructor_no_slash(self):          assert not os.path.exists("/tmp/foo") -        gdf = Gluster_DiskFile("/tmp/foo", "vol0", "p57", "ufo47", "bar", -                               "z", self.lg) +        gdf = DiskFile("/tmp/foo", "vol0", "p57", "ufo47", "bar", "z", self.lg)          assert gdf._obj == "z"          assert gdf._obj_path == ""          assert gdf.name == "bar" @@ -126,8 +127,8 @@ class TestDiskFile(unittest.TestCase):      def test_constructor_leadtrail_slash(self):          assert not os.path.exists("/tmp/foo") -        gdf = Gluster_DiskFile("/tmp/foo", "vol0", "p57", "ufo47", "bar", -                               "/b/a/z/", self.lg) +        gdf = DiskFile("/tmp/foo", "vol0", "p57", "ufo47", "bar", "/b/a/z/", +                       self.lg)          assert gdf._obj == "z"          assert gdf._obj_path == "b/a"          assert gdf.name == "bar/b/a" @@ -152,8 +153,7 @@ class TestDiskFile(unittest.TestCase):                  'ETag': etag,                  'X-Timestamp': ts,                  'Content-Type': 'application/octet-stream'} -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", -                                   "z", self.lg) +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar", "z", self.lg)              assert gdf._obj == "z"              assert gdf.data_file == the_file              assert not gdf._is_dir @@ -181,8 +181,7 @@ class TestDiskFile(unittest.TestCase):              exp_md = ini_md.copy()              del exp_md['X-Type']              del exp_md['X-Object-Type'] -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", -                                   "z", self.lg) +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar", "z", self.lg)              assert gdf._obj == "z"              assert gdf.data_file == the_file              assert not gdf._is_dir @@ -205,8 +204,7 @@ class TestDiskFile(unittest.TestCase):              os.makedirs(the_path)              with open(the_file, "wb") as fd:                  fd.write("1234") -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", -                                   "z", self.lg) +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar", "z", self.lg)              assert gdf._obj == "z"              assert gdf.data_file == the_file              assert not gdf._is_dir @@ -232,8 +230,8 @@ class TestDiskFile(unittest.TestCase):              exp_md = ini_md.copy()              del exp_md['X-Type']              del exp_md['X-Object-Type'] -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", -                                   "d", self.lg, keep_data_fp=True) +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar", "d", self.lg, +                           keep_data_fp=True)              assert gdf._obj == "d"              assert gdf.data_file == the_dir              assert gdf._is_dir @@ -250,8 +248,8 @@ class TestDiskFile(unittest.TestCase):              os.makedirs(the_path)              with open(the_file, "wb") as fd:                  fd.write("1234") -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", -                                   "z", self.lg, keep_data_fp=True) +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar", "z", self.lg, +                           keep_data_fp=True)              assert gdf._obj == "z"              assert gdf.data_file == the_file              assert not gdf._is_dir @@ -261,20 +259,19 @@ class TestDiskFile(unittest.TestCase):      def test_constructor_chunk_size(self):          assert not os.path.exists("/tmp/foo") -        gdf = Gluster_DiskFile("/tmp/foo", "vol0", "p57", "ufo47", "bar", -                               "z", self.lg, disk_chunk_size=8192) +        gdf = DiskFile("/tmp/foo", "vol0", "p57", "ufo47", "bar", "z", self.lg, +                       disk_chunk_size=8192)          assert gdf.disk_chunk_size == 8192      def test_constructor_iter_hook(self):          assert not os.path.exists("/tmp/foo") -        gdf = Gluster_DiskFile("/tmp/foo", "vol0", "p57", "ufo47", "bar", -                               "z", self.lg, iter_hook='hook') +        gdf = DiskFile("/tmp/foo", "vol0", "p57", "ufo47", "bar", "z", self.lg, +                       iter_hook='hook')          assert gdf.iter_hook == 'hook'      def test_close(self):          assert not os.path.exists("/tmp/foo") -        gdf = Gluster_DiskFile("/tmp/foo", "vol0", "p57", "ufo47", "bar", -                               "z", self.lg) +        gdf = DiskFile("/tmp/foo", "vol0", "p57", "ufo47", "bar", "z", self.lg)          # Should be a no-op, as by default is_dir is False, but fp is None          gdf.close() @@ -285,22 +282,21 @@ class TestDiskFile(unittest.TestCase):          assert gdf.fp == "123"          gdf._is_dir = False -        saved_dc = gluster.swift.common.DiskFile.do_close +        saved_dc = gluster.swift.obj.diskfile.do_close          self.called = False          def our_do_close(fp):              self.called = True -        gluster.swift.common.DiskFile.do_close = our_do_close +        gluster.swift.obj.diskfile.do_close = our_do_close          try:              gdf.close()              assert self.called              assert gdf.fp is None          finally: -            gluster.swift.common.DiskFile.do_close = saved_dc +            gluster.swift.obj.diskfile.do_close = saved_dc      def test_is_deleted(self):          assert not os.path.exists("/tmp/foo") -        gdf = Gluster_DiskFile("/tmp/foo", "vol0", "p57", "ufo47", "bar", -                               "z", self.lg) +        gdf = DiskFile("/tmp/foo", "vol0", "p57", "ufo47", "bar", "z", self.lg)          assert gdf.is_deleted()          gdf.data_file = "/tmp/foo/bar"          assert not gdf.is_deleted() @@ -311,8 +307,8 @@ class TestDiskFile(unittest.TestCase):          the_dir = "dir"          try:              os.makedirs(the_cont) -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", -                                   os.path.join(the_dir, "z"), self.lg) +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar", +                           os.path.join(the_dir, "z"), self.lg)              # Not created, dir object path is different, just checking              assert gdf._obj == "z"              gdf._create_dir_object(the_dir) @@ -328,8 +324,8 @@ class TestDiskFile(unittest.TestCase):          the_dir = "dir"          try:              os.makedirs(the_cont) -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", -                                   os.path.join(the_dir, "z"), self.lg) +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar", +                           os.path.join(the_dir, "z"), self.lg)              # Not created, dir object path is different, just checking              assert gdf._obj == "z"              dir_md = {'Content-Type': 'application/directory', @@ -349,19 +345,18 @@ class TestDiskFile(unittest.TestCase):              os.makedirs(the_path)              with open(the_dir, "wb") as fd:                  fd.write("1234") -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", -                                   "dir/z", self.lg) +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar", "dir/z", self.lg)              # Not created, dir object path is different, just checking              assert gdf._obj == "z"              def _mock_do_chown(p, u, g):                  assert u == DEFAULT_UID                  assert g == DEFAULT_GID -            dc = gluster.swift.common.DiskFile.do_chown -            gluster.swift.common.DiskFile.do_chown = _mock_do_chown +            dc = gluster.swift.obj.diskfile.do_chown +            gluster.swift.obj.diskfile.do_chown = _mock_do_chown              self.assertRaises(DiskFileError,                      gdf._create_dir_object,                      the_dir) -            gluster.swift.common.DiskFile.do_chown = dc +            gluster.swift.obj.diskfile.do_chown = dc              self.assertFalse(os.path.isdir(the_dir))              self.assertFalse(the_dir in _metadata)          finally: @@ -375,19 +370,18 @@ class TestDiskFile(unittest.TestCase):              os.makedirs(the_path)              with open(the_dir, "wb") as fd:                  fd.write("1234") -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", -                                   "dir/z", self.lg) +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar", "dir/z", self.lg)              # Not created, dir object path is different, just checking              assert gdf._obj == "z"              def _mock_do_chown(p, u, g):                  assert u == DEFAULT_UID                  assert g == DEFAULT_GID -            dc = gluster.swift.common.DiskFile.do_chown -            gluster.swift.common.DiskFile.do_chown = _mock_do_chown +            dc = gluster.swift.obj.diskfile.do_chown +            gluster.swift.obj.diskfile.do_chown = _mock_do_chown              self.assertRaises(DiskFileError,                      gdf._create_dir_object,                      the_dir) -            gluster.swift.common.DiskFile.do_chown = dc +            gluster.swift.obj.diskfile.do_chown = dc              self.assertFalse(os.path.isdir(the_dir))              self.assertFalse(the_dir in _metadata)          finally: @@ -399,8 +393,7 @@ class TestDiskFile(unittest.TestCase):          the_dir = os.path.join(the_path, "z")          try:              os.makedirs(the_dir) -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", -                                   "z", self.lg) +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar", "z", self.lg)              md = { 'Content-Type': 'application/octet-stream', 'a': 'b' }              gdf.put_metadata(md.copy())              assert gdf.metadata == md, "gdf.metadata = %r, md = %r" % (gdf.metadata, md) @@ -410,8 +403,7 @@ class TestDiskFile(unittest.TestCase):      def test_put_w_tombstone(self):          assert not os.path.exists("/tmp/foo") -        gdf = Gluster_DiskFile("/tmp/foo", "vol0", "p57", "ufo47", "bar", -                               "z", self.lg) +        gdf = DiskFile("/tmp/foo", "vol0", "p57", "ufo47", "bar", "z", self.lg)          assert gdf.metadata == {}          gdf.put_metadata({'x': '1'}, tombstone=True) @@ -425,8 +417,7 @@ class TestDiskFile(unittest.TestCase):              os.makedirs(the_path)              with open(the_file, "wb") as fd:                  fd.write("1234") -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", -                                   "z", self.lg) +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar", "z", self.lg)              newmd = gdf.metadata.copy()              newmd['X-Object-Meta-test'] = '1234'              gdf.put_metadata(newmd) @@ -443,7 +434,7 @@ class TestDiskFile(unittest.TestCase):              os.makedirs(the_path)              with open(the_file, "wb") as fd:                  fd.write("1234") -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar",                                     "z", self.lg)              newmd = gdf.metadata.copy()              newmd['Content-Type'] = '' @@ -460,7 +451,7 @@ class TestDiskFile(unittest.TestCase):          the_dir = os.path.join(the_path, "dir")          try:              os.makedirs(the_dir) -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar",                                     "dir", self.lg)              newmd = gdf.metadata.copy()              newmd['X-Object-Meta-test'] = '1234' @@ -476,7 +467,7 @@ class TestDiskFile(unittest.TestCase):          the_dir = os.path.join(the_path, "dir")          try:              os.makedirs(the_dir) -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar",                                     "dir", self.lg)              newmd = gdf.metadata.copy()              newmd['X-Object-Meta-test'] = '1234' @@ -492,14 +483,15 @@ class TestDiskFile(unittest.TestCase):          the_dir = os.path.join(the_cont, "dir")          try:              os.makedirs(the_cont) -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar",                                     "dir", self.lg)              assert gdf.metadata == {}              newmd = {                  'ETag': 'etag',                  'X-Timestamp': 'ts',                  'Content-Type': 'application/directory'} -            gdf.put(None, newmd, extension='.dir') +            with gdf.writer() as dw: +                dw.put(newmd, extension='.dir')              assert gdf.data_file == the_dir              for key,val in newmd.items():                  assert gdf.metadata[key] == val @@ -515,7 +507,7 @@ class TestDiskFile(unittest.TestCase):          the_dir = os.path.join(the_path, "dir")          try:              os.makedirs(the_dir) -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar",                                     "dir", self.lg)              origmd = gdf.metadata.copy()              origfmd = _metadata[the_dir] @@ -524,13 +516,14 @@ class TestDiskFile(unittest.TestCase):              # how this can happen normally.              newmd['Content-Type'] = ''              newmd['X-Object-Meta-test'] = '1234' -            try: -                gdf.put(None, newmd, extension='.data') -            except DiskFileError: -                pass -            else: -                self.fail("Expected to encounter" -                          " 'already-exists-as-dir' exception") +            with gdf.writer() as dw: +                try: +                    dw.put(newmd, extension='.data') +                except DiskFileError: +                    pass +                else: +                    self.fail("Expected to encounter" +                              " 'already-exists-as-dir' exception")              assert gdf.metadata == origmd              assert _metadata[the_dir] == origfmd          finally: @@ -541,7 +534,7 @@ class TestDiskFile(unittest.TestCase):          the_cont = os.path.join(td, "vol0", "bar")          try:              os.makedirs(the_cont) -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar",                                     "z", self.lg)              assert gdf._obj == "z"              assert gdf._obj_path == "" @@ -560,11 +553,11 @@ class TestDiskFile(unittest.TestCase):                  'Content-Length': '5',                  } -            with gdf.mkstemp() as fd: -                assert gdf.tmppath is not None -                tmppath = gdf.tmppath -                os.write(fd, body) -                gdf.put(fd, metadata) +            with gdf.writer() as dw: +                assert dw.tmppath is not None +                tmppath = dw.tmppath +                dw.write(body) +                dw.put(metadata)              assert gdf.data_file == os.path.join(td, "vol0", "bar", "z")              assert os.path.exists(gdf.data_file) @@ -578,7 +571,7 @@ class TestDiskFile(unittest.TestCase):          the_cont = os.path.join(td, "vol0", "bar")          try:              os.makedirs(the_cont) -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar",                                     "z", self.lg)              assert gdf._obj == "z"              assert gdf._obj_path == "" @@ -601,13 +594,14 @@ class TestDiskFile(unittest.TestCase):              with mock.patch("os.open", mock_open):                  try: -                    with gdf.mkstemp() as fd: -                        assert gdf.tmppath is not None -                        tmppath = gdf.tmppath -                        os.write(fd, body) -                        gdf.put(fd, metadata) +                    with gdf.writer() as dw: +                        assert dw.tmppath is not None +                        dw.write(body) +                        dw.put(metadata)                  except DiskFileNoSpace:                      pass +                else: +                    self.fail("Expected exception DiskFileNoSpace")          finally:              shutil.rmtree(td) @@ -616,7 +610,7 @@ class TestDiskFile(unittest.TestCase):          the_file = os.path.join(the_obj_path, "z")          td = tempfile.mkdtemp()          try: -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar",                                     the_file, self.lg)              assert gdf._obj == "z"              assert gdf._obj_path == the_obj_path @@ -635,11 +629,11 @@ class TestDiskFile(unittest.TestCase):                  'Content-Length': '5',                  } -            with gdf.mkstemp() as fd: -                assert gdf.tmppath is not None -                tmppath = gdf.tmppath -                os.write(fd, body) -                gdf.put(fd, metadata) +            with gdf.writer() as dw: +                assert dw.tmppath is not None +                tmppath = dw.tmppath +                dw.write(body) +                dw.put(metadata)              assert gdf.data_file == os.path.join(td, "vol0", "bar", "b", "a", "z")              assert os.path.exists(gdf.data_file) @@ -649,32 +643,32 @@ class TestDiskFile(unittest.TestCase):      def test_unlinkold_no_metadata(self):          assert not os.path.exists("/tmp/foo") -        gdf = Gluster_DiskFile("/tmp/foo", "vol0", "p57", "ufo47", "bar", +        gdf = DiskFile("/tmp/foo", "vol0", "p57", "ufo47", "bar",                                 "z", self.lg)          assert gdf.metadata == {} -        _saved_rmobjdir = gluster.swift.common.DiskFile.rmobjdir -        gluster.swift.common.DiskFile.rmobjdir = _mock_rmobjdir +        _saved_rmobjdir = gluster.swift.obj.diskfile.rmobjdir +        gluster.swift.obj.diskfile.rmobjdir = _mock_rmobjdir          try:              gdf.unlinkold(None)          except MockException as exp:              self.fail(str(exp))          finally: -            gluster.swift.common.DiskFile.rmobjdir = _saved_rmobjdir +            gluster.swift.obj.diskfile.rmobjdir = _saved_rmobjdir      def test_unlinkold_same_timestamp(self):          assert not os.path.exists("/tmp/foo") -        gdf = Gluster_DiskFile("/tmp/foo", "vol0", "p57", "ufo47", "bar", +        gdf = DiskFile("/tmp/foo", "vol0", "p57", "ufo47", "bar",                                 "z", self.lg)          assert gdf.metadata == {}          gdf.metadata['X-Timestamp'] = 1 -        _saved_rmobjdir = gluster.swift.common.DiskFile.rmobjdir -        gluster.swift.common.DiskFile.rmobjdir = _mock_rmobjdir +        _saved_rmobjdir = gluster.swift.obj.diskfile.rmobjdir +        gluster.swift.obj.diskfile.rmobjdir = _mock_rmobjdir          try:              gdf.unlinkold(1)          except MockException as exp:              self.fail(str(exp))          finally: -            gluster.swift.common.DiskFile.rmobjdir = _saved_rmobjdir +            gluster.swift.obj.diskfile.rmobjdir = _saved_rmobjdir      def test_unlinkold_file(self):          td = tempfile.mkdtemp() @@ -684,7 +678,7 @@ class TestDiskFile(unittest.TestCase):              os.makedirs(the_path)              with open(the_file, "wb") as fd:                  fd.write("1234") -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar",                                     "z", self.lg)              assert gdf._obj == "z"              assert gdf.data_file == the_file @@ -705,7 +699,7 @@ class TestDiskFile(unittest.TestCase):              os.makedirs(the_path)              with open(the_file, "wb") as fd:                  fd.write("1234") -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar",                                     "z", self.lg)              assert gdf._obj == "z"              assert gdf.data_file == the_file @@ -729,7 +723,7 @@ class TestDiskFile(unittest.TestCase):              os.makedirs(the_path)              with open(the_file, "wb") as fd:                  fd.write("1234") -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar",                                     "z", self.lg)              assert gdf._obj == "z"              assert gdf.data_file == the_file @@ -766,7 +760,7 @@ class TestDiskFile(unittest.TestCase):          the_dir = os.path.join(the_path, "d")          try:              os.makedirs(the_dir) -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar",                                     "d", self.lg, keep_data_fp=True)              assert gdf.data_file == the_dir              assert gdf._is_dir @@ -786,7 +780,7 @@ class TestDiskFile(unittest.TestCase):              os.makedirs(the_path)              with open(the_file, "wb") as fd:                  fd.write("1234") -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar",                                     "z", self.lg)              assert gdf._obj == "z"              assert gdf.data_file == the_file @@ -795,7 +789,7 @@ class TestDiskFile(unittest.TestCase):          finally:              shutil.rmtree(td) -    def test_get_data_file_size(self): +    def test_get_data_file_size_md_restored(self):          td = tempfile.mkdtemp()          the_path = os.path.join(td, "vol0", "bar")          the_file = os.path.join(the_path, "z") @@ -803,7 +797,7 @@ class TestDiskFile(unittest.TestCase):              os.makedirs(the_path)              with open(the_file, "wb") as fd:                  fd.write("1234") -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar",                                     "z", self.lg)              assert gdf._obj == "z"              assert gdf.data_file == the_file @@ -817,10 +811,10 @@ class TestDiskFile(unittest.TestCase):      def test_get_data_file_size_dne(self):          assert not os.path.exists("/tmp/foo") -        gdf = Gluster_DiskFile("/tmp/foo", "vol0", "p57", "ufo47", "bar", +        gdf = DiskFile("/tmp/foo", "vol0", "p57", "ufo47", "bar",                                 "/b/a/z/", self.lg)          try: -            s = gdf.get_data_file_size() +            gdf.get_data_file_size()          except DiskFileNotExist:              pass          else: @@ -834,14 +828,14 @@ class TestDiskFile(unittest.TestCase):              os.makedirs(the_path)              with open(the_file, "wb") as fd:                  fd.write("1234") -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar",                                     "z", self.lg)              assert gdf._obj == "z"              assert gdf.data_file == the_file              assert not gdf._is_dir              gdf.data_file = gdf.data_file + ".dne"              try: -                s = gdf.get_data_file_size() +                gdf.get_data_file_size()              except DiskFileNotExist:                  pass              else: @@ -857,7 +851,7 @@ class TestDiskFile(unittest.TestCase):              os.makedirs(the_path)              with open(the_file, "wb") as fd:                  fd.write("1234") -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar",                                     "z", self.lg)              assert gdf._obj == "z"              assert gdf.data_file == the_file @@ -871,7 +865,7 @@ class TestDiskFile(unittest.TestCase):                  with patch("os.path.getsize", _mock_getsize_eaccess_err):                      try: -                        s = gdf.get_data_file_size() +                        gdf.get_data_file_size()                      except OSError as err:                          assert err.errno == errno.EACCES                      else: @@ -887,7 +881,7 @@ class TestDiskFile(unittest.TestCase):          the_dir = os.path.join(the_path, "d")          try:              os.makedirs(the_dir) -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar",                                     "d", self.lg, keep_data_fp=True)              assert gdf._obj == "d"              assert gdf.data_file == the_dir @@ -898,42 +892,42 @@ class TestDiskFile(unittest.TestCase):      def test_filter_metadata(self):          assert not os.path.exists("/tmp/foo") -        gdf = Gluster_DiskFile("/tmp/foo", "vol0", "p57", "ufo47", "bar", +        gdf = DiskFile("/tmp/foo", "vol0", "p57", "ufo47", "bar",                                 "z", self.lg)          assert gdf.metadata == {} -        gdf.filter_metadata() +        gdf._filter_metadata()          assert gdf.metadata == {}          gdf.metadata[X_TYPE] = 'a'          gdf.metadata[X_OBJECT_TYPE] = 'b'          gdf.metadata['foobar'] = 'c' -        gdf.filter_metadata() +        gdf._filter_metadata()          assert X_TYPE not in gdf.metadata          assert X_OBJECT_TYPE not in gdf.metadata          assert 'foobar' in gdf.metadata -    def test_mkstemp(self): +    def test_writer(self):          td = tempfile.mkdtemp() -        the_path = os.path.join(td, "vol0", "bar") -        the_dir = os.path.join(the_path, "dir")          try: -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar",                                     "dir/z", self.lg)              saved_tmppath = '' -            with gdf.mkstemp() as fd: +            saved_fd = None +            with gdf.writer() as dw:                  assert gdf.datadir == os.path.join(td, "vol0", "bar", "dir")                  assert os.path.isdir(gdf.datadir) -                saved_tmppath = gdf.tmppath +                saved_tmppath = dw.tmppath                  assert os.path.dirname(saved_tmppath) == gdf.datadir                  assert os.path.basename(saved_tmppath)[:3] == '.z.'                  assert os.path.exists(saved_tmppath) -                os.write(fd, "123") +                dw.write("123") +                saved_fd = dw.fd              # At the end of previous with block a close on fd is called.              # Calling os.close on the same fd will raise an OSError              # exception and we must catch it.              try: -                os.close(fd) -            except OSError as err: +                os.close(saved_fd) +            except OSError:                  pass              else:                  self.fail("Exception expected") @@ -941,44 +935,40 @@ class TestDiskFile(unittest.TestCase):          finally:              shutil.rmtree(td) -    def test_mkstemp_err_on_close(self): +    def test_writer_err_on_close(self):          td = tempfile.mkdtemp() -        the_path = os.path.join(td, "vol0", "bar") -        the_dir = os.path.join(the_path, "dir")          try: -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar",                                     "dir/z", self.lg)              saved_tmppath = '' -            with gdf.mkstemp() as fd: +            with gdf.writer() as dw:                  assert gdf.datadir == os.path.join(td, "vol0", "bar", "dir")                  assert os.path.isdir(gdf.datadir) -                saved_tmppath = gdf.tmppath +                saved_tmppath = dw.tmppath                  assert os.path.dirname(saved_tmppath) == gdf.datadir                  assert os.path.basename(saved_tmppath)[:3] == '.z.'                  assert os.path.exists(saved_tmppath) -                os.write(fd, "123") +                dw.write("123")                  # Closing the fd prematurely should not raise any exceptions. -                os.close(fd) +                os.close(dw.fd)              assert not os.path.exists(saved_tmppath)          finally:              shutil.rmtree(td) -    def test_mkstemp_err_on_unlink(self): +    def test_writer_err_on_unlink(self):          td = tempfile.mkdtemp() -        the_path = os.path.join(td, "vol0", "bar") -        the_dir = os.path.join(the_path, "dir")          try: -            gdf = Gluster_DiskFile(td, "vol0", "p57", "ufo47", "bar", +            gdf = DiskFile(td, "vol0", "p57", "ufo47", "bar",                                     "dir/z", self.lg)              saved_tmppath = '' -            with gdf.mkstemp() as fd: +            with gdf.writer() as dw:                  assert gdf.datadir == os.path.join(td, "vol0", "bar", "dir")                  assert os.path.isdir(gdf.datadir) -                saved_tmppath = gdf.tmppath +                saved_tmppath = dw.tmppath                  assert os.path.dirname(saved_tmppath) == gdf.datadir                  assert os.path.basename(saved_tmppath)[:3] == '.z.'                  assert os.path.exists(saved_tmppath) -                os.write(fd, "123") +                dw.write("123")                  os.unlink(saved_tmppath)              assert not os.path.exists(saved_tmppath)          finally: diff --git a/test/unit/proxy/test_server.py b/test/unit/proxy/test_server.py index 638e6b4..a74d70d 100644 --- a/test/unit/proxy/test_server.py +++ b/test/unit/proxy/test_server.py @@ -37,29 +37,29 @@ import unittest  from nose import SkipTest  import urlparse  import signal -from contextlib import contextmanager +from contextlib import contextmanager, nested, closing  from gzip import GzipFile  from shutil import rmtree  import time  from urllib import quote  from hashlib import md5  from tempfile import mkdtemp -import random -import eventlet -from eventlet import sleep, spawn, Timeout, util, wsgi, listen +import mock +from eventlet import sleep, spawn, wsgi, listen  import simplejson  import gluster.swift.common.Glusterfs as gfs  gfs.RUN_DIR = mkdtemp() -from test.unit import connect_tcp, readuntil2crlfs, FakeLogger, fake_http_connect +from test.unit import connect_tcp, readuntil2crlfs, FakeLogger, \ +    fake_http_connect, FakeRing, FakeMemcache  from gluster.swift.proxy.server import server as proxy_server  from gluster.swift.obj import server as object_server  from gluster.swift.account import server as account_server  from gluster.swift.container import server as container_server  from swift.common import ring -from swift.common.exceptions import ChunkReadTimeout +from swift.common.exceptions import ChunkReadTimeout, SegmentError  from swift.common.constraints import MAX_META_NAME_LENGTH, \      MAX_META_VALUE_LENGTH, MAX_META_COUNT, MAX_META_OVERALL_SIZE, \      MAX_FILE_SIZE, MAX_ACCOUNT_NAME_LENGTH, MAX_CONTAINER_NAME_LENGTH @@ -77,7 +77,10 @@ from swift.common.swob import Request, Response, HTTPNotFound, \  logging.getLogger().addHandler(logging.StreamHandler(sys.stdout)) +STATIC_TIME = time.time()  _request_instances = 0 +_test_coros = _test_servers = _test_sockets = _orig_container_listing_limit = \ +    _testdir = None  def request_init(self, *args, **kwargs): @@ -92,6 +95,7 @@ def request_del(self):          self._orig_del()      _request_instances -= 1 +  def setup():      utils.HASH_PATH_SUFFIX = 'endcap'      global _testdir, _test_servers, _test_sockets, \ @@ -109,6 +113,8 @@ def setup():      rmtree(_testdir)      mkdirs(os.path.join(_testdir, 'sda1'))      mkdirs(os.path.join(_testdir, 'sda1', 'tmp')) +    mkdirs(os.path.join(_testdir, 'sdb1')) +    mkdirs(os.path.join(_testdir, 'sdb1', 'tmp'))      mkdirs(os.path.join(_testdir, 'a'))      mkdirs(os.path.join(_testdir, 'a', 'tmp'))      _orig_container_listing_limit = \ @@ -126,24 +132,39 @@ def setup():      obj2lis = listen(('localhost', 0))      _test_sockets = \          (prolis, acc1lis, acc2lis, con1lis, con2lis, obj1lis, obj2lis) -    pickle.dump(ring.RingData([[0, 1, 0, 1], [1, 0, 1, 0]], -                [{'id': 0, 'zone': 0, 'device': 'sda1', 'ip': '127.0.0.1', -                  'port': acc1lis.getsockname()[1]}, -                 {'id': 1, 'zone': 1, 'device': 'a', 'ip': '127.0.0.1', -                  'port': acc2lis.getsockname()[1]}], 30), -                GzipFile(os.path.join(_testdir, 'account.ring.gz'), 'wb')) -    pickle.dump(ring.RingData([[0, 1, 0, 1], [1, 0, 1, 0]], -                [{'id': 0, 'zone': 0, 'device': 'sda1', 'ip': '127.0.0.1', -                  'port': con1lis.getsockname()[1]}, -                 {'id': 1, 'zone': 1, 'device': 'a', 'ip': '127.0.0.1', -                  'port': con2lis.getsockname()[1]}], 30), -                GzipFile(os.path.join(_testdir, 'container.ring.gz'), 'wb')) -    pickle.dump(ring.RingData([[0, 1, 0, 1], [1, 0, 1, 0]], -                [{'id': 0, 'zone': 0, 'device': 'sda1', 'ip': '127.0.0.1', -                  'port': obj1lis.getsockname()[1]}, -                 {'id': 1, 'zone': 1, 'device': 'a', 'ip': '127.0.0.1', -                  'port': obj2lis.getsockname()[1]}], 30), -                GzipFile(os.path.join(_testdir, 'object.ring.gz'), 'wb')) +    with closing(GzipFile(os.path.join(_testdir, 'account.ring.gz'), 'wb')) \ +            as f: +        pickle.dump(ring.RingData([[0, 1, 0, 1], [1, 0, 1, 0]], +                    [{'id': 0, 'zone': 0, 'device': 'sda1', 'ip': '127.0.0.1', +                      'port': acc1lis.getsockname()[1]}, +                     {'id': 1, 'zone': 1, 'device': 'sdb1', 'ip': '127.0.0.1', +                      'port': acc2lis.getsockname()[1]}, +                     # Gluster volume mapping to device +                     {'id': 1, 'zone': 1, 'device': 'a', 'ip': '127.0.0.1', +                      'port': acc2lis.getsockname()[1]}], 30), +                    f) +    with closing(GzipFile(os.path.join(_testdir, 'container.ring.gz'), 'wb')) \ +             as f: +        pickle.dump(ring.RingData([[0, 1, 0, 1], [1, 0, 1, 0]], +                    [{'id': 0, 'zone': 0, 'device': 'sda1', 'ip': '127.0.0.1', +                      'port': con1lis.getsockname()[1]}, +                     {'id': 1, 'zone': 1, 'device': 'sdb1', 'ip': '127.0.0.1', +                      'port': con2lis.getsockname()[1]}, +                     # Gluster volume mapping to device +                     {'id': 1, 'zone': 1, 'device': 'a', 'ip': '127.0.0.1', +                      'port': con2lis.getsockname()[1]}], 30), +                    f) +    with closing(GzipFile(os.path.join(_testdir, 'object.ring.gz'), 'wb')) \ +            as f: +        pickle.dump(ring.RingData([[0, 1, 0, 1], [1, 0, 1, 0]], +                    [{'id': 0, 'zone': 0, 'device': 'sda1', 'ip': '127.0.0.1', +                      'port': obj1lis.getsockname()[1]}, +                     {'id': 1, 'zone': 1, 'device': 'sdb1', 'ip': '127.0.0.1', +                      'port': obj2lis.getsockname()[1]}, +                     # Gluster volume mapping to device +                     {'id': 1, 'zone': 1, 'device': 'a', 'ip': '127.0.0.1', +                      'port': obj2lis.getsockname()[1]}], 30), +                    f)      prosrv = proxy_server.Application(conf, FakeMemcacheReturnsNone())      acc1srv = account_server.AccountController(conf)      acc2srv = account_server.AccountController(conf) @@ -153,7 +174,10 @@ def setup():      obj2srv = object_server.ObjectController(conf)      _test_servers = \          (prosrv, acc1srv, acc2srv, con1srv, con2srv, obj1srv, obj2srv) -    nl = NullLogger() +    # Use DebugLogger() when trying to figure out what failed in the spawned +    # servers. +    from test.unit import DebugLogger +    nl = DebugLogger()      prospa = spawn(wsgi.server, prolis, prosrv, nl)      acc1spa = spawn(wsgi.server, acc1lis, acc1srv, nl)      acc2spa = spawn(wsgi.server, acc2lis, acc2srv, nl) @@ -163,7 +187,7 @@ def setup():      obj2spa = spawn(wsgi.server, obj2lis, obj2srv, nl)      _test_coros = \          (prospa, acc1spa, acc2spa, con1spa, con2spa, obj1spa, obj2spa) -    # Create account +    # Gluster: ensure account exists      ts = normalize_timestamp(time.time())      partition, nodes = prosrv.account_ring.get_nodes('a')      for node in nodes: @@ -175,10 +199,9 @@ def setup():                                                           'x-trans-id': 'test'})          resp = conn.getresponse() -        # For GlusterFS the volume should have already -        # been created since accounts map to volumes. -        # Expect a 202 instead of a 201 as in OpenStack Swift's -        # proxy unit test. +        # For GlusterFS the volume should have already been created since +        # accounts map to volumes.  Expect a 202 instead of a 201 as for +        # OpenStack Swift's proxy unit test the account is explicitly created.          assert(resp.status == 202)      # Create container      sock = connect_tcp(('localhost', prolis.getsockname()[1])) @@ -189,7 +212,7 @@ def setup():      fd.flush()      headers = readuntil2crlfs(fd)      exp = 'HTTP/1.1 201' -    assert(headers[:len(exp)] == exp) +    assert headers[:len(exp)] == exp, "Expected '%s', encountered '%s'" % (exp, headers[:len(exp)])  def teardown(): @@ -214,72 +237,6 @@ def sortHeaderNames(headerNames):      return ', '.join(headers) -class FakeRing(object): - -    def __init__(self, replicas=3): -        # 9 total nodes (6 more past the initial 3) is the cap, no matter if -        # this is set higher, or R^2 for R replicas -        self.replicas = replicas -        self.max_more_nodes = 0 -        self.devs = {} - -    def set_replicas(self, replicas): -        self.replicas = replicas -        self.devs = {} - -    def get_nodes(self, account, container=None, obj=None): -        devs = [] -        for x in xrange(self.replicas): -            devs.append(self.devs.get(x)) -            if devs[x] is None: -                self.devs[x] = devs[x] = \ -                    {'ip': '10.0.0.%s' % x, -                     'port': 1000 + x, -                     'device': 'sd' + (chr(ord('a') + x)), -                     'id': x} -        return 1, devs - -    def get_part_nodes(self, part): -        return self.get_nodes('blah')[1] - -    def get_more_nodes(self, nodes): -        # replicas^2 is the true cap -        for x in xrange(self.replicas, min(self.replicas + self.max_more_nodes, -                                           self.replicas * self.replicas)): -            yield {'ip': '10.0.0.%s' % x, 'port': 1000 + x, 'device': 'sda'} - - -class FakeMemcache(object): - -    def __init__(self): -        self.store = {} - -    def get(self, key): -        return self.store.get(key) - -    def keys(self): -        return self.store.keys() - -    def set(self, key, value, time=0): -        self.store[key] = value -        return True - -    def incr(self, key, time=0): -        self.store[key] = self.store.setdefault(key, 0) + 1 -        return self.store[key] - -    @contextmanager -    def soft_lock(self, key, timeout=0, retries=5): -        yield True - -    def delete(self, key): -        try: -            del self.store[key] -        except Exception: -            pass -        return True - -  class FakeMemcacheReturnsNone(FakeMemcache):      def get(self, key): @@ -326,11 +283,31 @@ class TestController(unittest.TestCase):                                         object_ring=FakeRing())          self.controller = swift.proxy.controllers.Controller(app) +        class FakeReq(object): +            def __init__(self): +                self.url = "/foo/bar" +                self.method = "METHOD" + +            def as_referer(self): +                return self.method + ' ' + self.url +          self.account = 'some_account'          self.container = 'some_container' +        self.request = FakeReq()          self.read_acl = 'read_acl'          self.write_acl = 'write_acl' +    def test_transfer_headers(self): +        src_headers = {'x-remove-base-meta-owner': 'x', +                       'x-base-meta-size': '151M', +                       'new-owner': 'Kun'} +        dst_headers = {'x-base-meta-owner': 'Gareth', +                       'x-base-meta-size': '150M'} +        self.controller.transfer_headers(src_headers, dst_headers) +        expected_headers = {'x-base-meta-owner': '', +                            'x-base-meta-size': '151M'} +        self.assertEquals(dst_headers, expected_headers) +      def check_account_info_return(self, partition, nodes, is_none=False):          if is_none:              p, n = None, None @@ -369,7 +346,7 @@ class TestController(unittest.TestCase):          with save_globals():              set_http_connect(200)              partition, nodes, count = \ -                self.controller.account_info(self.account) +                self.controller.account_info(self.account, self.request)              set_http_connect(201, raise_timeout_exc=True)              self.controller._make_request(                  nodes, partition, 'POST', '/', '', '', @@ -380,13 +357,15 @@ class TestController(unittest.TestCase):          with save_globals():              set_http_connect(200)              partition, nodes, count = \ -                self.controller.account_info(self.account) +                self.controller.account_info(self.account, self.request)              self.check_account_info_return(partition, nodes)              self.assertEquals(count, 12345) +            # Test the internal representation in memcache +            # 'container_count' changed from int to str              cache_key = get_account_memcache_key(self.account)              container_info = {'status': 200, -                              'container_count': 12345, +                              'container_count': '12345',                                'total_object_count': None,                                'bytes': None,                                'meta': {}} @@ -395,7 +374,7 @@ class TestController(unittest.TestCase):              set_http_connect()              partition, nodes, count = \ -                self.controller.account_info(self.account) +                self.controller.account_info(self.account, self.request)              self.check_account_info_return(partition, nodes)              self.assertEquals(count, 12345) @@ -404,22 +383,24 @@ class TestController(unittest.TestCase):          with save_globals():              set_http_connect(404, 404, 404)              partition, nodes, count = \ -                self.controller.account_info(self.account) +                self.controller.account_info(self.account, self.request)              self.check_account_info_return(partition, nodes, True)              self.assertEquals(count, None) +            # Test the internal representation in memcache +            # 'container_count' changed from 0 to None              cache_key = get_account_memcache_key(self.account) -            container_info = {'status': 404, -                              'container_count': 0, -                              'total_object_count': None, -                              'bytes': None, -                              'meta': {}} -            self.assertEquals(container_info, +            account_info = {'status': 404, +                            'container_count': None, # internally keep None +                            'total_object_count': None, +                            'bytes': None, +                            'meta': {}} +            self.assertEquals(account_info,                                self.memcache.get(cache_key))              set_http_connect()              partition, nodes, count = \ -                self.controller.account_info(self.account) +                self.controller.account_info(self.account, self.request)              self.check_account_info_return(partition, nodes, True)              self.assertEquals(count, None) @@ -428,71 +409,27 @@ class TestController(unittest.TestCase):          def test(*status_list):              set_http_connect(*status_list)              partition, nodes, count = \ -                self.controller.account_info(self.account) +                self.controller.account_info(self.account, self.request)              self.assertEqual(len(self.memcache.keys()), 0)              self.check_account_info_return(partition, nodes, True)              self.assertEquals(count, None)          with save_globals(): -            test(503, 404, 404) -            test(404, 404, 503) +            # We cache if we have two 404 responses - fail if only one +            test(503, 503, 404) +            test(504, 404, 503)              test(404, 507, 503)              test(503, 503, 503) -    def test_account_info_account_autocreate(self): +    def test_account_info_no_account(self):          with save_globals():              self.memcache.store = {} -            set_http_connect(404, 404, 404, 201, 201, 201) -            partition, nodes, count = \ -                self.controller.account_info(self.account, autocreate=False) -            self.check_account_info_return(partition, nodes, is_none=True) -            self.assertEquals(count, None) - -            self.memcache.store = {} -            set_http_connect(404, 404, 404, 201, 201, 201) +            set_http_connect(404, 404, 404)              partition, nodes, count = \ -                self.controller.account_info(self.account) +                self.controller.account_info(self.account, self.request)              self.check_account_info_return(partition, nodes, is_none=True)              self.assertEquals(count, None) -            self.memcache.store = {} -            set_http_connect(404, 404, 404, 201, 201, 201) -            partition, nodes, count = \ -                self.controller.account_info(self.account, autocreate=True) -            self.check_account_info_return(partition, nodes) -            self.assertEquals(count, 0) - -            self.memcache.store = {} -            set_http_connect(404, 404, 404, 503, 201, 201) -            partition, nodes, count = \ -                self.controller.account_info(self.account, autocreate=True) -            self.check_account_info_return(partition, nodes) -            self.assertEquals(count, 0) - -            self.memcache.store = {} -            set_http_connect(404, 404, 404, 503, 201, 503) -            exc = None -            partition, nodes, count = \ -                self.controller.account_info(self.account, autocreate=True) -            self.check_account_info_return(partition, nodes, is_none=True) -            self.assertEquals(None, count) - -            self.memcache.store = {} -            set_http_connect(404, 404, 404, 403, 403, 403) -            exc = None -            partition, nodes, count = \ -                self.controller.account_info(self.account, autocreate=True) -            self.check_account_info_return(partition, nodes, is_none=True) -            self.assertEquals(None, count) - -            self.memcache.store = {} -            set_http_connect(404, 404, 404, 409, 409, 409) -            exc = None -            partition, nodes, count = \ -                self.controller.account_info(self.account, autocreate=True) -            self.check_account_info_return(partition, nodes, is_none=True) -            self.assertEquals(None, count) -      def check_container_info_return(self, ret, is_none=False):          if is_none:              partition, nodes, read_acl, write_acl = None, None, None, None @@ -506,27 +443,26 @@ class TestController(unittest.TestCase):          self.assertEqual(write_acl, ret['write_acl'])      def test_container_info_invalid_account(self): -        def account_info(self, account, autocreate=False): +        def account_info(self, account, request, autocreate=False):              return None, None          with save_globals():              swift.proxy.controllers.Controller.account_info = account_info              ret = self.controller.container_info(self.account, -                                                 self.container) +                                                 self.container, +                                                 self.request)              self.check_container_info_return(ret, True)      # tests if 200 is cached and used      def test_container_info_200(self): -        def account_info(self, account, autocreate=False): -            return True, True, 0          with save_globals():              headers = {'x-container-read': self.read_acl,                         'x-container-write': self.write_acl} -            swift.proxy.controllers.Controller.account_info = account_info -            set_http_connect(200, headers=headers) -            ret = self.controller.container_info(self.account, -                                                 self.container) +            set_http_connect(200, # account_info is found +                             200, headers=headers) # container_info is found +            ret = self.controller.container_info( +                self.account, self.container, self.request)              self.check_container_info_return(ret)              cache_key = get_container_memcache_key(self.account, @@ -536,20 +472,20 @@ class TestController(unittest.TestCase):              self.assertEquals(200, cache_value.get('status'))              set_http_connect() -            ret = self.controller.container_info(self.account, -                                                 self.container) +            ret = self.controller.container_info( +                self.account, self.container, self.request)              self.check_container_info_return(ret)      # tests if 404 is cached and used      def test_container_info_404(self): -        def account_info(self, account, autocreate=False): +        def account_info(self, account, request):              return True, True, 0          with save_globals(): -            swift.proxy.controllers.Controller.account_info = account_info -            set_http_connect(404, 404, 404) -            ret = self.controller.container_info(self.account, -                                                 self.container) +            set_http_connect(503, 204, # account_info found +                             504, 404, 404) # container_info 'NotFound' +            ret = self.controller.container_info( +                self.account, self.container, self.request)              self.check_container_info_return(ret, True)              cache_key = get_container_memcache_key(self.account, @@ -559,22 +495,39 @@ class TestController(unittest.TestCase):              self.assertEquals(404, cache_value.get('status'))              set_http_connect() -            ret = self.controller.container_info(self.account, -                                                 self.container) +            ret = self.controller.container_info( +                self.account, self.container, self.request) +            self.check_container_info_return(ret, True) + +            set_http_connect(503, 404, 404)# account_info 'NotFound' +            ret = self.controller.container_info( +                self.account, self.container, self.request) +            self.check_container_info_return(ret, True) + +            cache_key = get_container_memcache_key(self.account, +                                                   self.container) +            cache_value = self.memcache.get(cache_key) +            self.assertTrue(isinstance(cache_value, dict)) +            self.assertEquals(404, cache_value.get('status')) + +            set_http_connect() +            ret = self.controller.container_info( +                self.account, self.container, self.request)              self.check_container_info_return(ret, True)      # tests if some http status codes are not cached      def test_container_info_no_cache(self):          def test(*status_list):              set_http_connect(*status_list) -            ret = self.controller.container_info(self.account, -                                                 self.container) +            ret = self.controller.container_info( +                self.account, self.container, self.request)              self.assertEqual(len(self.memcache.keys()), 0)              self.check_container_info_return(ret, True)          with save_globals(): -            test(503, 404, 404) -            test(404, 404, 503) +            # We cache if we have two 404 responses - fail if only one +            test(503, 503, 404) +            test(504, 404, 503)              test(404, 507, 503)              test(503, 503, 503) @@ -629,7 +582,7 @@ class TestProxyServer(unittest.TestCase):              req = Request.blank('/v1/a')              req.environ['swift.authorize'] = authorize              app.update_request(req) -            resp = app.handle_request(req) +            app.handle_request(req)          self.assert_(called[0])      def test_calls_authorize_deny(self): @@ -645,7 +598,7 @@ class TestProxyServer(unittest.TestCase):          req = Request.blank('/v1/a')          req.environ['swift.authorize'] = authorize          app.update_request(req) -        resp = app.handle_request(req) +        app.handle_request(req)          self.assert_(called[0])      def test_negative_content_length(self): @@ -697,25 +650,34 @@ class TestProxyServer(unittest.TestCase):          exp_timings = {}          self.assertEquals(baseapp.node_timings, exp_timings) -        proxy_server.time = lambda: times.pop(0) -        try: -            times = [time.time()] -            exp_timings = {'127.0.0.1': (0.1, -                                         times[0] + baseapp.timing_expiry)} +        times = [time.time()] +        exp_timings = {'127.0.0.1': (0.1, times[0] + baseapp.timing_expiry)} +        with mock.patch('swift.proxy.server.time', lambda: times.pop(0)):              baseapp.set_node_timing({'ip': '127.0.0.1'}, 0.1) -            self.assertEquals(baseapp.node_timings, exp_timings) -        finally: -            proxy_server.time = time.time +        self.assertEquals(baseapp.node_timings, exp_timings) -        proxy_server.shuffle = lambda l: l -        try: -            nodes = [{'ip': '127.0.0.1'}, {'ip': '127.0.0.2'}, {'ip': '127.0.0.3'}] +        nodes = [{'ip': '127.0.0.1'}, {'ip': '127.0.0.2'}, {'ip': '127.0.0.3'}] +        with mock.patch('swift.proxy.server.shuffle', lambda l: l):              res = baseapp.sort_nodes(nodes) -            exp_sorting = [{'ip': '127.0.0.2'}, {'ip': '127.0.0.3'}, -                           {'ip': '127.0.0.1'}] -            self.assertEquals(res, exp_sorting) -        finally: -            proxy_server.shuffle = random.shuffle +        exp_sorting = [{'ip': '127.0.0.2'}, {'ip': '127.0.0.3'}, +                       {'ip': '127.0.0.1'}] +        self.assertEquals(res, exp_sorting) + +    def test_node_affinity(self): +        baseapp = proxy_server.Application({'sorting_method': 'affinity', +                                            'read_affinity': 'r1=1'}, +                                           FakeMemcache(), +                                           container_ring=FakeRing(), +                                           object_ring=FakeRing(), +                                           account_ring=FakeRing()) + +        nodes = [{'region': 2, 'zone': 1, 'ip': '127.0.0.1'}, +                 {'region': 1, 'zone': 2, 'ip': '127.0.0.2'}] +        with mock.patch('swift.proxy.server.shuffle', lambda x: x): +            app_sorted = baseapp.sort_nodes(nodes) +            exp_sorted = [{'region': 1, 'zone': 2, 'ip': '127.0.0.2'}, +                          {'region': 2, 'zone': 1, 'ip': '127.0.0.1'}] +            self.assertEquals(exp_sorted, app_sorted)  class TestObjectController(unittest.TestCase): @@ -757,46 +719,6 @@ class TestObjectController(unittest.TestCase):              res = method(req)              self.assertEquals(res.status_int, expected) -    def test_illegal_object_name(self): -        prolis = _test_sockets[0] -        prosrv = _test_servers[0] - -        # Create a container -        sock = connect_tcp(('localhost', prolis.getsockname()[1])) -        fd = sock.makefile() -        fd.write('PUT /v1/a/illegal_name HTTP/1.1\r\nHost: localhost\r\n' -                 'Connection: close\r\nX-Storage-Token: t\r\n' -                 'Content-Length: 0\r\n\r\n') -        fd.flush() -        headers = readuntil2crlfs(fd) -        exp = 'HTTP/1.1 201' -        self.assertEquals(headers[:len(exp)], exp) - -        # Create a file obj -        fakedata = 'a' * 1024 -        sock = connect_tcp(('localhost', prolis.getsockname()[1])) -        fd = sock.makefile() -        fd.write('PUT /v1/a/illegal_name/file/ HTTP/1.1\r\nHost: localhost\r\n' -                 'Connection: close\r\nX-Storage-Token: t\r\n' -                 'Content-Length: %s\r\n' -                 'Content-Type: application/octect-stream\r\n' -                 '\r\n%s' % (str(len(fakedata)), fakedata)) -        fd.flush() -        headers = readuntil2crlfs(fd) -        exp = 'HTTP/1.1 400' -        self.assertEquals(headers[:len(exp)], exp) - -        # Delete continer -        sock = connect_tcp(('localhost', prolis.getsockname()[1])) -        fd = sock.makefile() -        fd.write('DELETE /v1/a/illegal_name HTTP/1.1\r\nHost: localhost\r\n' -                 'Connection: close\r\nX-Storage-Token: t\r\n' -                 'Content-Length: 0\r\n\r\n') -        fd.flush() -        headers = readuntil2crlfs(fd) -        exp = 'HTTP/1.1 204' -        self.assertEquals(headers[:len(exp)], exp) -      def test_GET_newest_large_file(self):          calls = [0] @@ -884,6 +806,246 @@ class TestObjectController(unittest.TestCase):              res = controller.PUT(req)              self.assertTrue(res.status.startswith('201 ')) +    def test_PUT_respects_write_affinity(self): +        written_to = [] + +        def test_connect(ipaddr, port, device, partition, method, path, +                         headers=None, query_string=None): +            if path == '/a/c/o.jpg': +                written_to.append((ipaddr, port, device)) + +        with save_globals(): +            def is_r0(node): +                return node['region'] == 0 + +            self.app.object_ring.max_more_nodes = 100 +            self.app.write_affinity_is_local_fn = is_r0 +            self.app.write_affinity_node_count = lambda r: 3 + +            controller = \ +                proxy_server.ObjectController(self.app, 'a', 'c', 'o.jpg') +            set_http_connect(200, 200, 201, 201, 201, +                             give_connect=test_connect) +            req = Request.blank('/a/c/o.jpg', {}) +            req.content_length = 1 +            req.body = 'a' +            self.app.memcache.store = {} +            res = controller.PUT(req) +            self.assertTrue(res.status.startswith('201 ')) + +        self.assertEqual(3, len(written_to)) +        for ip, port, device in written_to: +            # this is kind of a hokey test, but in FakeRing, the port is even +            # when the region is 0, and odd when the region is 1, so this test +            # asserts that we only wrote to nodes in region 0. +            self.assertEqual(0, port % 2) + +    def test_PUT_respects_write_affinity_with_507s(self): +        written_to = [] + +        def test_connect(ipaddr, port, device, partition, method, path, +                         headers=None, query_string=None): +            if path == '/a/c/o.jpg': +                written_to.append((ipaddr, port, device)) + +        with save_globals(): +            def is_r0(node): +                return node['region'] == 0 + +            self.app.object_ring.max_more_nodes = 100 +            self.app.write_affinity_is_local_fn = is_r0 +            self.app.write_affinity_node_count = lambda r: 3 + +            controller = \ +                proxy_server.ObjectController(self.app, 'a', 'c', 'o.jpg') +            controller.error_limit( +                self.app.object_ring.get_part_nodes(1)[0], 'test') +            set_http_connect(200, 200,       # account, container +                             201, 201, 201,  # 3 working backends +                             give_connect=test_connect) +            req = Request.blank('/a/c/o.jpg', {}) +            req.content_length = 1 +            req.body = 'a' +            self.app.memcache.store = {} +            res = controller.PUT(req) +            self.assertTrue(res.status.startswith('201 ')) + +        self.assertEqual(3, len(written_to)) +        # this is kind of a hokey test, but in FakeRing, the port is even when +        # the region is 0, and odd when the region is 1, so this test asserts +        # that we wrote to 2 nodes in region 0, then went to 1 non-r0 node. +        self.assertEqual(0, written_to[0][1] % 2)  # it's (ip, port, device) +        self.assertEqual(0, written_to[1][1] % 2) +        self.assertNotEqual(0, written_to[2][1] % 2) + +    def test_PUT_message_length_using_content_length(self): +        prolis = _test_sockets[0] +        sock = connect_tcp(('localhost', prolis.getsockname()[1])) +        fd = sock.makefile() +        obj = 'j' * 20 +        fd.write('PUT /v1/a/c/o.content-length HTTP/1.1\r\n' +                 'Host: localhost\r\n' +                 'Connection: close\r\n' +                 'X-Storage-Token: t\r\n' +                 'Content-Length: %s\r\n' +                 'Content-Type: application/octet-stream\r\n' +                 '\r\n%s' % (str(len(obj)), obj)) +        fd.flush() +        headers = readuntil2crlfs(fd) +        exp = 'HTTP/1.1 201' +        self.assertEqual(headers[:len(exp)], exp) + +    def test_PUT_message_length_using_transfer_encoding(self): +        prolis = _test_sockets[0] +        sock = connect_tcp(('localhost', prolis.getsockname()[1])) +        fd = sock.makefile() +        fd.write('PUT /v1/a/c/o.chunked HTTP/1.1\r\n' +                 'Host: localhost\r\n' +                 'Connection: close\r\n' +                 'X-Storage-Token: t\r\n' +                 'Content-Type: application/octet-stream\r\n' +                 'Transfer-Encoding: chunked\r\n\r\n' +                 '2\r\n' +                 'oh\r\n' +                 '4\r\n' +                 ' say\r\n' +                 '4\r\n' +                 ' can\r\n' +                 '4\r\n' +                 ' you\r\n' +                 '4\r\n' +                 ' see\r\n' +                 '3\r\n' +                 ' by\r\n' +                 '4\r\n' +                 ' the\r\n' +                 '8\r\n' +                 ' dawns\'\n\r\n' +                 '0\r\n\r\n') +        fd.flush() +        headers = readuntil2crlfs(fd) +        exp = 'HTTP/1.1 201' +        self.assertEqual(headers[:len(exp)], exp) + +    def test_PUT_message_length_using_both(self): +        prolis = _test_sockets[0] +        sock = connect_tcp(('localhost', prolis.getsockname()[1])) +        fd = sock.makefile() +        fd.write('PUT /v1/a/c/o.chunked HTTP/1.1\r\n' +                 'Host: localhost\r\n' +                 'Connection: close\r\n' +                 'X-Storage-Token: t\r\n' +                 'Content-Type: application/octet-stream\r\n' +                 'Content-Length: 33\r\n' +                 'Transfer-Encoding: chunked\r\n\r\n' +                 '2\r\n' +                 'oh\r\n' +                 '4\r\n' +                 ' say\r\n' +                 '4\r\n' +                 ' can\r\n' +                 '4\r\n' +                 ' you\r\n' +                 '4\r\n' +                 ' see\r\n' +                 '3\r\n' +                 ' by\r\n' +                 '4\r\n' +                 ' the\r\n' +                 '8\r\n' +                 ' dawns\'\n\r\n' +                 '0\r\n\r\n') +        fd.flush() +        headers = readuntil2crlfs(fd) +        exp = 'HTTP/1.1 201' +        self.assertEqual(headers[:len(exp)], exp) + +    def test_PUT_bad_message_length(self): +        prolis = _test_sockets[0] +        sock = connect_tcp(('localhost', prolis.getsockname()[1])) +        fd = sock.makefile() +        fd.write('PUT /v1/a/c/o.chunked HTTP/1.1\r\n' +                 'Host: localhost\r\n' +                 'Connection: close\r\n' +                 'X-Storage-Token: t\r\n' +                 'Content-Type: application/octet-stream\r\n' +                 'Content-Length: 33\r\n' +                 'Transfer-Encoding: gzip\r\n\r\n' +                 '2\r\n' +                 'oh\r\n' +                 '4\r\n' +                 ' say\r\n' +                 '4\r\n' +                 ' can\r\n' +                 '4\r\n' +                 ' you\r\n' +                 '4\r\n' +                 ' see\r\n' +                 '3\r\n' +                 ' by\r\n' +                 '4\r\n' +                 ' the\r\n' +                 '8\r\n' +                 ' dawns\'\n\r\n' +                 '0\r\n\r\n') +        fd.flush() +        headers = readuntil2crlfs(fd) +        exp = 'HTTP/1.1 400' +        self.assertEqual(headers[:len(exp)], exp) + +    def test_PUT_message_length_unsup_xfr_encoding(self): +        prolis = _test_sockets[0] +        sock = connect_tcp(('localhost', prolis.getsockname()[1])) +        fd = sock.makefile() +        fd.write('PUT /v1/a/c/o.chunked HTTP/1.1\r\n' +                 'Host: localhost\r\n' +                 'Connection: close\r\n' +                 'X-Storage-Token: t\r\n' +                 'Content-Type: application/octet-stream\r\n' +                 'Content-Length: 33\r\n' +                 'Transfer-Encoding: gzip,chunked\r\n\r\n' +                 '2\r\n' +                 'oh\r\n' +                 '4\r\n' +                 ' say\r\n' +                 '4\r\n' +                 ' can\r\n' +                 '4\r\n' +                 ' you\r\n' +                 '4\r\n' +                 ' see\r\n' +                 '3\r\n' +                 ' by\r\n' +                 '4\r\n' +                 ' the\r\n' +                 '8\r\n' +                 ' dawns\'\n\r\n' +                 '0\r\n\r\n') +        fd.flush() +        headers = readuntil2crlfs(fd) +        exp = 'HTTP/1.1 501' +        self.assertEqual(headers[:len(exp)], exp) + +    def test_PUT_message_length_too_large(self): +        swift.proxy.controllers.obj.MAX_FILE_SIZE = 10 +        try: +            prolis = _test_sockets[0] +            sock = connect_tcp(('localhost', prolis.getsockname()[1])) +            fd = sock.makefile() +            fd.write('PUT /v1/a/c/o.chunked HTTP/1.1\r\n' +                     'Host: localhost\r\n' +                     'Connection: close\r\n' +                     'X-Storage-Token: t\r\n' +                     'Content-Type: application/octet-stream\r\n' +                     'Content-Length: 33\r\n\r\n' +                     'oh say can you see by the dawns\'\n') +            fd.flush() +            headers = readuntil2crlfs(fd) +            exp = 'HTTP/1.1 413' +            self.assertEqual(headers[:len(exp)], exp) +        finally: +            swift.proxy.controllers.obj.MAX_FILE_SIZE = MAX_FILE_SIZE +      def test_expirer_DELETE_on_versioned_object(self):          test_errors = [] @@ -914,7 +1076,7 @@ class TestObjectController(unittest.TestCase):                                  headers={'X-If-Delete-At': 1},                                  environ={'REQUEST_METHOD': 'DELETE'})              self.app.update_request(req) -            res = controller.DELETE(req) +            controller.DELETE(req)              self.assertEquals(test_errors, [])      def test_GET_manifest_no_segments(self): @@ -1084,8 +1246,8 @@ class TestObjectController(unittest.TestCase):                  200,    # GET listing1                  200,    # GET seg01                  200,    # GET seg02 -                headers={"X-Static-Large-Object": "True", -                         'content-type': 'text/html; swift_bytes=4'}, +                headers=[{}, {}, {"X-Static-Large-Object": "True", +                         'content-type': 'text/html; swift_bytes=4'}, {}, {}],                  body_iter=response_bodies,                  give_connect=capture_requested_paths) @@ -1104,6 +1266,106 @@ class TestObjectController(unittest.TestCase):                   ['GET', '/a/d1/seg01', {}],                   ['GET', '/a/d2/seg02', {}]]) +    def test_GET_slo_multipart_manifest(self): +        listing = [{"hash": "98568d540134639be4655198a36614a4", +                    "last_modified": "2012-11-08T04:05:37.866820", +                    "bytes": 2, +                    "name": "/d1/seg01", +                    "content_type": "application/octet-stream"}, +                   {"hash": "d526f1c8ef6c1e4e980e2b8471352d23", +                    "last_modified": "2012-11-08T04:05:37.846710", +                    "bytes": 2, +                    "name": "/d2/seg02", +                    "content_type": "application/octet-stream"}] +        json_listing = simplejson.dumps(listing) +        response_bodies = ( +            '',                           # HEAD /a +            '',                           # HEAD /a/c +            json_listing)    # GET manifest +        with save_globals(): +            controller = proxy_server.ObjectController( +                self.app, 'a', 'c', 'manifest') + +            requested = [] + +            def capture_requested_paths(ipaddr, port, device, partition, +                                        method, path, headers=None, +                                        query_string=None): +                qs_dict = dict(urlparse.parse_qsl(query_string or '')) +                requested.append([method, path, qs_dict]) + +            set_http_connect( +                200,    # HEAD /a +                200,    # HEAD /a/c +                200,    # GET listing1 +                headers={"X-Static-Large-Object": "True", +                         'content-type': 'text/html; swift_bytes=4'}, +                body_iter=response_bodies, +                give_connect=capture_requested_paths) + +            req = Request.blank('/a/c/manifest?multipart-manifest=get') +            resp = controller.GET(req) +            self.assertEqual(resp.status_int, 200) +            self.assertEqual(resp.body, json_listing) +            self.assertEqual(resp.content_type, 'application/json') +            self.assertEqual(resp.charset.lower(), 'utf-8') + +            self.assertEqual( +                requested, +                [['HEAD', '/a', {}], +                 ['HEAD', '/a/c', {}], +                 ['GET', '/a/c/manifest', {'multipart-manifest': 'get'}]]) + +    def test_GET_slo_multipart_manifest_from_copy(self): +        listing = [{"hash": "98568d540134639be4655198a36614a4", +                    "last_modified": "2012-11-08T04:05:37.866820", +                    "bytes": 2, +                    "name": "/d1/seg01", +                    "content_type": "application/octet-stream"}, +                   {"hash": "d526f1c8ef6c1e4e980e2b8471352d23", +                    "last_modified": "2012-11-08T04:05:37.846710", +                    "bytes": 2, +                    "name": "/d2/seg02", +                    "content_type": "application/octet-stream"}] +        json_listing = simplejson.dumps(listing) +        response_bodies = ( +            '',                           # HEAD /a +            '',                           # HEAD /a/c +            json_listing)    # GET manifest +        with save_globals(): +            controller = proxy_server.ObjectController( +                self.app, 'a', 'c', 'manifest') + +            requested = [] + +            def capture_requested_paths(ipaddr, port, device, partition, +                                        method, path, headers=None, +                                        query_string=None): +                qs_dict = dict(urlparse.parse_qsl(query_string or '')) +                requested.append([method, path, qs_dict]) + +            set_http_connect( +                200,    # HEAD /a +                200,    # HEAD /a/c +                200,    # GET listing1 +                headers={"X-Static-Large-Object": "True", +                         'content-type': 'text/html; swift_bytes=4'}, +                body_iter=response_bodies, +                give_connect=capture_requested_paths) + +            req = Request.blank('/a/c/manifest?multipart-manifest=get', +                                headers={'x-copy-from': '/a/c/manifest'}) +            resp = controller.GET(req) +            self.assertEqual(resp.status_int, 200) +            self.assertEqual(resp.body, json_listing) +            self.assertEqual(resp.content_type, 'text/html') + +            self.assertEqual( +                requested, +                [['HEAD', '/a', {}], +                 ['HEAD', '/a/c', {}], +                 ['GET', '/a/c/manifest', {'multipart-manifest': 'get'}]]) +      def test_GET_bad_etag_manifest_slo(self):          listing = [{"hash": "98568d540134639be4655198a36614a4",                      "last_modified": "2012-11-08T04:05:37.866820", @@ -1140,16 +1402,18 @@ class TestObjectController(unittest.TestCase):                  200,    # GET listing1                  200,    # GET seg01                  200,    # GET seg02 -                headers={"X-Static-Large-Object": "True", -                         'content-type': 'text/html; swift_bytes=4'}, +                headers=[{}, {}, {"X-Static-Large-Object": "True", +                         'content-type': 'text/html; swift_bytes=4'}, {}, {}],                  body_iter=response_bodies,                  give_connect=capture_requested_paths)              req = Request.blank('/a/c/manifest')              resp = controller.GET(req)              self.assertEqual(resp.status_int, 200) -            self.assertEqual(resp.body, 'Aa')  # dropped connection              self.assertEqual(resp.content_length, 4)  # content incomplete              self.assertEqual(resp.content_type, 'text/html') +            self.assertRaises(SegmentError, lambda: resp.body) +            # dropped connection, exception is caught by eventlet as it is +            # iterating over response              self.assertEqual(                  requested, @@ -1159,6 +1423,94 @@ class TestObjectController(unittest.TestCase):                   ['GET', '/a/d1/seg01', {}],                   ['GET', '/a/d2/seg02', {}]]) +    def test_GET_nested_slo(self): +        listing = [{"hash": "98568d540134639be4655198a36614a4", +                    "last_modified": "2012-11-08T04:05:37.866820", +                    "bytes": 2, +                    "name": "/d1/seg01", +                    "content_type": "application/octet-stream"}, +                   {"hash": "8681fb3ada2715c8754706ee5f23d4f8", +                    "last_modified": "2012-11-08T04:05:37.846710", +                    "bytes": 4, +                    "name": "/d2/sub_manifest", +                    "content_type": "application/octet-stream"}, +                   {"hash": "419af6d362a14b7a789ba1c7e772bbae", +                    "last_modified": "2012-11-08T04:05:37.866820", +                    "bytes": 2, +                    "name": "/d1/seg04", +                    "content_type": "application/octet-stream"}] + +        sub_listing = [{"hash": "d526f1c8ef6c1e4e980e2b8471352d23", +                        "last_modified": "2012-11-08T04:05:37.866820", +                        "bytes": 2, +                        "name": "/d1/seg02", +                        "content_type": "application/octet-stream"}, +                       {"hash": "e4c8f1de1c0855c7c2be33196d3c3537", +                        "last_modified": "2012-11-08T04:05:37.846710", +                        "bytes": 2, +                        "name": "/d2/seg03", +                        "content_type": "application/octet-stream"}] + +        response_bodies = ( +            '',                           # HEAD /a +            '',                           # HEAD /a/c +            simplejson.dumps(listing),    # GET manifest +            'Aa',                         # GET seg01 +            simplejson.dumps(sub_listing),  # GET sub_manifest +            'Bb',                         # GET seg02 +            'Cc',                         # GET seg03 +            'Dd')                         # GET seg04 +        with save_globals(): +            controller = proxy_server.ObjectController( +                self.app, 'a', 'c', 'manifest') + +            requested = [] + +            def capture_requested_paths(ipaddr, port, device, partition, +                                        method, path, headers=None, +                                        query_string=None): +                qs_dict = dict(urlparse.parse_qsl(query_string or '')) +                requested.append([method, path, qs_dict]) + +            slob_headers = {"X-Static-Large-Object": "True", +                            'content-type': 'text/html; swift_bytes=4'} +            set_http_connect( +                200,    # HEAD /a +                200,    # HEAD /a/c +                200,    # GET listing1 +                200,    # GET seg01 +                200,    # GET sub listing1 +                200,    # GET seg02 +                200,    # GET seg03 +                200,    # GET seg04 +                headers=[{}, {}, slob_headers, {}, slob_headers, {}, {}, {}], +                body_iter=response_bodies, +                give_connect=capture_requested_paths) +            req = Request.blank('/a/c/manifest') +            resp = controller.GET(req) +            self.assertEqual(resp.status_int, 200) +            self.assertEqual(resp.content_length, 8) +            self.assertEqual(resp.content_type, 'text/html') + +            self.assertEqual( +                requested, +                [['HEAD', '/a', {}], +                 ['HEAD', '/a/c', {}], +                 ['GET', '/a/c/manifest', {}]]) +            # iterating over body will retrieve manifest and sub manifest's +            # objects +            self.assertEqual(resp.body, 'AaBbCcDd') +            self.assertEqual( +                requested, +                [['HEAD', '/a', {}], +                 ['HEAD', '/a/c', {}], +                 ['GET', '/a/c/manifest', {}], +                 ['GET', '/a/d1/seg01', {}], +                 ['GET', '/a/d2/sub_manifest', {}], +                 ['GET', '/a/d1/seg02', {}], +                 ['GET', '/a/d2/seg03', {}], +                 ['GET', '/a/d1/seg04', {}]]) +      def test_GET_bad_404_manifest_slo(self):          listing = [{"hash": "98568d540134639be4655198a36614a4",                      "last_modified": "2012-11-08T04:05:37.866820", @@ -1200,16 +1552,18 @@ class TestObjectController(unittest.TestCase):                  200,    # GET listing1                  200,    # GET seg01                  404,    # GET seg02 -                headers={"X-Static-Large-Object": "True", -                         'content-type': 'text/html; swift_bytes=4'}, +                headers=[{}, {}, {"X-Static-Large-Object": "True", +                         'content-type': 'text/html; swift_bytes=4'}, {}, {}],                  body_iter=response_bodies,                  give_connect=capture_requested_paths)              req = Request.blank('/a/c/manifest')              resp = controller.GET(req)              self.assertEqual(resp.status_int, 200) -            self.assertEqual(resp.body, 'Aa')  # dropped connection              self.assertEqual(resp.content_length, 6)  # content incomplete              self.assertEqual(resp.content_type, 'text/html') +            self.assertRaises(SegmentError, lambda: resp.body) +            # dropped connection, exception is caught by eventlet as it is +            # iterating over response              self.assertEqual(                  requested, @@ -1308,10 +1662,10 @@ class TestObjectController(unittest.TestCase):          try:              with open(os.path.join(swift_dir, 'mime.types'), 'w') as fp:                  fp.write('foo/bar foo\n') -            ba = proxy_server.Application({'swift_dir': swift_dir}, -                                          FakeMemcache(), FakeLogger(), -                                          FakeRing(), FakeRing(), -                                          FakeRing()) +            proxy_server.Application({'swift_dir': swift_dir}, +                                     FakeMemcache(), FakeLogger(), +                                     FakeRing(), FakeRing(), +                                     FakeRing())              self.assertEquals(proxy_server.mimetypes.guess_type('blah.foo')[0],                                'foo/bar')              self.assertEquals(proxy_server.mimetypes.guess_type('blah.jpg')[0], @@ -1715,6 +2069,53 @@ class TestObjectController(unittest.TestCase):              res = controller.POST(req)              self.assertEquals(res.status_int, 400) +    def test_PUT_not_autodetect_content_type(self): +        with save_globals(): +            controller = proxy_server.ObjectController( +                self.app, 'a', 'c', 'o.html') + +            headers = {'Content-Type': 'something/right', 'Content-Length': 0} +            it_worked = [] + +            def verify_content_type(ipaddr, port, device, partition, +                                    method, path, headers=None, +                                    query_string=None): +                if path == '/a/c/o.html': +                    it_worked.append( +                        headers['Content-Type'].startswith('something/right')) + +            set_http_connect(204, 204, 201, 201, 201, +                             give_connect=verify_content_type) +            req = Request.blank('/a/c/o.html', {}, headers=headers) +            self.app.update_request(req) +            res = controller.PUT(req) +            self.assertNotEquals(it_worked, []) +            self.assertTrue(all(it_worked)) + +    def test_PUT_autodetect_content_type(self): +        with save_globals(): +            controller = proxy_server.ObjectController( +                self.app, 'a', 'c', 'o.html') + +            headers = {'Content-Type': 'something/wrong', 'Content-Length': 0, +                       'X-Detect-Content-Type': 'True'} +            it_worked = [] + +            def verify_content_type(ipaddr, port, device, partition, +                                    method, path, headers=None, +                                    query_string=None): +                if path == '/a/c/o.html': +                    it_worked.append( +                        headers['Content-Type'].startswith('text/html')) + +            set_http_connect(204, 204, 201, 201, 201, +                             give_connect=verify_content_type) +            req = Request.blank('/a/c/o.html', {}, headers=headers) +            self.app.update_request(req) +            res = controller.PUT(req) +            self.assertNotEquals(it_worked, []) +            self.assertTrue(all(it_worked)) +      def test_client_timeout(self):          with save_globals():              self.app.account_ring.get_nodes('account') @@ -1886,12 +2287,13 @@ class TestObjectController(unittest.TestCase):                                                                    'container',                                                                    'object')                  collected_nodes = [] -                for node in controller.iter_nodes(partition, nodes, -                                                  self.app.object_ring): +                for node in controller.iter_nodes(self.app.object_ring, +                                                  partition):                      collected_nodes.append(node)                  self.assertEquals(len(collected_nodes), 5)                  self.app.object_ring.max_more_nodes = 20 +                self.app.request_node_count = lambda r: 20                  controller = proxy_server.ObjectController(self.app, 'account',                                                             'container',                                                             'object') @@ -1899,8 +2301,8 @@ class TestObjectController(unittest.TestCase):                                                                    'container',                                                                    'object')                  collected_nodes = [] -                for node in controller.iter_nodes(partition, nodes, -                                                  self.app.object_ring): +                for node in controller.iter_nodes(self.app.object_ring, +                                                  partition):                      collected_nodes.append(node)                  self.assertEquals(len(collected_nodes), 9) @@ -1914,8 +2316,8 @@ class TestObjectController(unittest.TestCase):                                                                    'container',                                                                    'object')                  collected_nodes = [] -                for node in controller.iter_nodes(partition, nodes, -                                                  self.app.object_ring): +                for node in controller.iter_nodes(self.app.object_ring, +                                                  partition):                      collected_nodes.append(node)                  self.assertEquals(len(collected_nodes), 5)                  self.assertEquals( @@ -1933,14 +2335,78 @@ class TestObjectController(unittest.TestCase):                                                                    'container',                                                                    'object')                  collected_nodes = [] -                for node in controller.iter_nodes(partition, nodes, -                                                  self.app.object_ring): +                for node in controller.iter_nodes(self.app.object_ring, +                                                  partition):                      collected_nodes.append(node)                  self.assertEquals(len(collected_nodes), 5)                  self.assertEquals(self.app.logger.log_dict['warning'], [])              finally:                  self.app.object_ring.max_more_nodes = 0 +    def test_iter_nodes_calls_sort_nodes(self): +        with mock.patch.object(self.app, 'sort_nodes') as sort_nodes: +            controller = proxy_server.ObjectController(self.app, 'a', 'c', 'o') +            for node in controller.iter_nodes(self.app.object_ring, 0): +                pass +            sort_nodes.assert_called_once_with( +                self.app.object_ring.get_part_nodes(0)) + +    def test_iter_nodes_skips_error_limited(self): +        with mock.patch.object(self.app, 'sort_nodes', lambda n: n): +            controller = proxy_server.ObjectController(self.app, 'a', 'c', 'o') +            first_nodes = list(controller.iter_nodes(self.app.object_ring, 0)) +            second_nodes = list(controller.iter_nodes(self.app.object_ring, 0)) +            self.assertTrue(first_nodes[0] in second_nodes) + +            controller.error_limit(first_nodes[0], 'test') +            second_nodes = list(controller.iter_nodes(self.app.object_ring, 0)) +            self.assertTrue(first_nodes[0] not in second_nodes) + +    def test_iter_nodes_gives_extra_if_error_limited_inline(self): +        with nested( +                mock.patch.object(self.app, 'sort_nodes', lambda n: n), +                mock.patch.object(self.app, 'request_node_count', +                                  lambda r: 6), +                mock.patch.object(self.app.object_ring, 'max_more_nodes', 99)): +            controller = proxy_server.ObjectController(self.app, 'a', 'c', 'o') +            first_nodes = list(controller.iter_nodes(self.app.object_ring, 0)) +            second_nodes = [] +            for node in controller.iter_nodes(self.app.object_ring, 0): +                if not second_nodes: +                    controller.error_limit(node, 'test') +                second_nodes.append(node) +            self.assertEquals(len(first_nodes), 6) +            self.assertEquals(len(second_nodes), 7) + +    def test_iter_nodes_with_custom_node_iter(self): +        controller = proxy_server.ObjectController(self.app, 'a', 'c', 'o') +        node_list = [dict(id=n) for n in xrange(10)] +        with nested( +                mock.patch.object(self.app, 'sort_nodes', lambda n: n), +                mock.patch.object(self.app, 'request_node_count', +                                  lambda r: 3)): +            got_nodes = list(controller.iter_nodes(self.app.object_ring, 0, +                                                   node_iter=iter(node_list))) +        self.assertEqual(node_list[:3], got_nodes) + +        with nested( +                mock.patch.object(self.app, 'sort_nodes', lambda n: n), +                mock.patch.object(self.app, 'request_node_count', +                                  lambda r: 1000000)): +            got_nodes = list(controller.iter_nodes(self.app.object_ring, 0, +                                                   node_iter=iter(node_list))) +        self.assertEqual(node_list, got_nodes) + +    def test_best_response_sets_headers(self): +        controller = proxy_server.ObjectController(self.app, 'account', +                                                   'container', 'object') +        req = Request.blank('/a/c/o', environ={'REQUEST_METHOD': 'GET'}) +        resp = controller.best_response(req, [200] * 3, ['OK'] * 3, [''] * 3, +                                        'Object', headers=[{'X-Test': '1'}, +                                                           {'X-Test': '2'}, +                                                           {'X-Test': '3'}]) +        self.assertEquals(resp.headers['X-Test'], '1') +      def test_best_response_sets_etag(self):          controller = proxy_server.ObjectController(self.app, 'account',                                                     'container', 'object') @@ -2039,36 +2505,50 @@ class TestObjectController(unittest.TestCase):              set_http_connect(404, 404, 404)              #                acct acct acct +            # make sure to use a fresh request without cached env +            req = Request.blank('/a/c/o', environ={'REQUEST_METHOD': 'DELETE'})              resp = getattr(controller, 'DELETE')(req)              self.assertEquals(resp.status_int, 404)              set_http_connect(503, 404, 404)              #                acct acct acct +            # make sure to use a fresh request without cached env +            req = Request.blank('/a/c/o', environ={'REQUEST_METHOD': 'DELETE'})              resp = getattr(controller, 'DELETE')(req)              self.assertEquals(resp.status_int, 404)              set_http_connect(503, 503, 404)              #                acct acct acct +            # make sure to use a fresh request without cached env +            req = Request.blank('/a/c/o', environ={'REQUEST_METHOD': 'DELETE'})              resp = getattr(controller, 'DELETE')(req)              self.assertEquals(resp.status_int, 404)              set_http_connect(503, 503, 503)              #                acct acct acct +            # make sure to use a fresh request without cached env +            req = Request.blank('/a/c/o', environ={'REQUEST_METHOD': 'DELETE'})              resp = getattr(controller, 'DELETE')(req)              self.assertEquals(resp.status_int, 404)              set_http_connect(200, 200, 204, 204, 204)              #                acct cont obj  obj  obj +            # make sure to use a fresh request without cached env +            req = Request.blank('/a/c/o', environ={'REQUEST_METHOD': 'DELETE'})              resp = getattr(controller, 'DELETE')(req)              self.assertEquals(resp.status_int, 204)              set_http_connect(200, 404, 404, 404)              #                acct cont cont cont +            # make sure to use a fresh request without cached env +            req = Request.blank('/a/c/o', environ={'REQUEST_METHOD': 'DELETE'})              resp = getattr(controller, 'DELETE')(req)              self.assertEquals(resp.status_int, 404)              set_http_connect(200, 503, 503, 503)              #                acct cont cont cont +            # make sure to use a fresh request without cached env +            req = Request.blank('/a/c/o', environ={'REQUEST_METHOD': 'DELETE'})              resp = getattr(controller, 'DELETE')(req)              self.assertEquals(resp.status_int, 404) @@ -2078,6 +2558,8 @@ class TestObjectController(unittest.TestCase):              set_http_connect(200)              #                acct [isn't actually called since everything              #                      is error limited] +            # make sure to use a fresh request without cached env +            req = Request.blank('/a/c/o', environ={'REQUEST_METHOD': 'DELETE'})              resp = getattr(controller, 'DELETE')(req)              self.assertEquals(resp.status_int, 404) @@ -2089,6 +2571,8 @@ class TestObjectController(unittest.TestCase):              set_http_connect(200, 200)              #                acct cont [isn't actually called since              #                           everything is error limited] +            # make sure to use a fresh request without cached env +            req = Request.blank('/a/c/o', environ={'REQUEST_METHOD': 'DELETE'})              resp = getattr(controller, 'DELETE')(req)              self.assertEquals(resp.status_int, 404) @@ -2363,6 +2847,7 @@ class TestObjectController(unittest.TestCase):              self.assertEquals(resp.headers.get('x-object-meta-test'),                                'testing')              self.assertEquals(resp.headers.get('x-object-meta-ours'), 'okay') +            self.assertEquals(resp.headers.get('x-delete-at'), '9876543210')              # copy-from object is too large to fit in target object              req = Request.blank('/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, @@ -2494,6 +2979,7 @@ class TestObjectController(unittest.TestCase):              self.assertEquals(resp.headers.get('x-object-meta-test'),                                'testing')              self.assertEquals(resp.headers.get('x-object-meta-ours'), 'okay') +            self.assertEquals(resp.headers.get('x-delete-at'), '9876543210')              req = Request.blank('/a/c/o', environ={'REQUEST_METHOD': 'COPY'},                                  headers={'Destination': '/c/o'}) @@ -2531,6 +3017,30 @@ class TestObjectController(unittest.TestCase):              self.assertEquals(resp.headers['x-copied-from-last-modified'],                                '3') +    def test_COPY_delete_at(self): +        with save_globals(): +            given_headers = {} + +            def fake_connect_put_node(nodes, part, path, headers, +                                      logger_thread_locals): +                given_headers.update(headers) + +            controller = proxy_server.ObjectController(self.app, 'a', +                                                       'c', 'o') +            controller._connect_put_node = fake_connect_put_node +            set_http_connect(200, 200, 200, 200, 200, 201, 201, 201) +            self.app.memcache.store = {} +            req = Request.blank('/a/c/o', environ={'REQUEST_METHOD': 'COPY'}, +                                headers={'Destination': '/c/o'}) + +            self.app.update_request(req) +            controller.COPY(req) +            self.assertEquals(given_headers.get('X-Delete-At'), '9876543210') +            self.assertTrue('X-Delete-At-Host' in given_headers) +            self.assertTrue('X-Delete-At-Device' in given_headers) +            self.assertTrue('X-Delete-At-Partition' in given_headers) +            self.assertTrue('X-Delete-At-Container' in given_headers) +      def test_chunked_put(self):          class ChunkedFile(): @@ -2837,53 +3347,6 @@ class TestObjectController(unittest.TestCase):          body = fd.read()          self.assertEquals(body, 'oh hai123456789abcdef') -    def test_put_put(self): -        (prolis, acc1lis, acc2lis, con1lis, con2lis, obj1lis, -         obj2lis) = _test_sockets -        sock = connect_tcp(('localhost', prolis.getsockname()[1])) -        fd = sock.makefile() -        fd.write('PUT /v1/a/c/o/putput HTTP/1.1\r\nHost: localhost\r\n' -                 'Connection: close\r\nX-Auth-Token: t\r\n' -                 'Content-Length:27\r\n\r\n' -                 'abcdefghijklmnopqrstuvwxyz\n\r\n\r\n') -        fd.flush() -        headers = readuntil2crlfs(fd) -        exp = 'HTTP/1.1 201' -        self.assertEquals(headers[:len(exp)], exp) -        # Ensure we get what we put -        sock = connect_tcp(('localhost', prolis.getsockname()[1])) -        fd = sock.makefile() -        fd.write('GET /v1/a/c/o/putput HTTP/1.1\r\nHost: localhost\r\n' -                 'Connection: close\r\nX-Auth-Token: t\r\n\r\n') -        fd.flush() -        headers = readuntil2crlfs(fd) -        exp = 'HTTP/1.1 200' -        self.assertEquals(headers[:len(exp)], exp) -        body = fd.read() -        self.assertEquals(body, 'abcdefghijklmnopqrstuvwxyz\n') - -        sock = connect_tcp(('localhost', prolis.getsockname()[1])) -        fd = sock.makefile() -        fd.write('PUT /v1/a/c/o/putput HTTP/1.1\r\nHost: localhost\r\n' -                 'Connection: close\r\nX-Auth-Token: t\r\n' -                 'Content-Length:27\r\n\r\n' -                 'ABCDEFGHIJKLMNOPQRSTUVWXYZ\n\r\n\r\n') -        fd.flush() -        headers = readuntil2crlfs(fd) -        exp = 'HTTP/1.1 201' -        self.assertEquals(headers[:len(exp)], exp) -        # Ensure we get what we put -        sock = connect_tcp(('localhost', prolis.getsockname()[1])) -        fd = sock.makefile() -        fd.write('GET /v1/a/c/o/putput HTTP/1.1\r\nHost: localhost\r\n' -                 'Connection: close\r\nX-Auth-Token: t\r\n\r\n') -        fd.flush() -        headers = readuntil2crlfs(fd) -        exp = 'HTTP/1.1 200' -        self.assertEquals(headers[:len(exp)], exp) -        body = fd.read() -        self.assertEquals(body, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ\n') -      def test_version_manifest(self):          raise SkipTest("Not until we support versioned objects")          versions_to_create = 3 @@ -3662,7 +4125,7 @@ class TestObjectController(unittest.TestCase):              req = Request.blank('/a/c/o')              req.environ['swift.authorize'] = authorize              self.app.update_request(req) -            res = controller.GET(req) +            controller.GET(req)          self.assert_(called[0])      def test_HEAD_calls_authorize(self): @@ -3678,7 +4141,7 @@ class TestObjectController(unittest.TestCase):              req = Request.blank('/a/c/o', {'REQUEST_METHOD': 'HEAD'})              req.environ['swift.authorize'] = authorize              self.app.update_request(req) -            res = controller.HEAD(req) +            controller.HEAD(req)          self.assert_(called[0])      def test_POST_calls_authorize(self): @@ -3696,7 +4159,7 @@ class TestObjectController(unittest.TestCase):                                  headers={'Content-Length': '5'}, body='12345')              req.environ['swift.authorize'] = authorize              self.app.update_request(req) -            res = controller.POST(req) +            controller.POST(req)          self.assert_(called[0])      def test_POST_as_copy_calls_authorize(self): @@ -3713,7 +4176,7 @@ class TestObjectController(unittest.TestCase):                                  headers={'Content-Length': '5'}, body='12345')              req.environ['swift.authorize'] = authorize              self.app.update_request(req) -            res = controller.POST(req) +            controller.POST(req)          self.assert_(called[0])      def test_PUT_calls_authorize(self): @@ -3730,7 +4193,7 @@ class TestObjectController(unittest.TestCase):                                  headers={'Content-Length': '5'}, body='12345')              req.environ['swift.authorize'] = authorize              self.app.update_request(req) -            res = controller.PUT(req) +            controller.PUT(req)          self.assert_(called[0])      def test_COPY_calls_authorize(self): @@ -3747,7 +4210,7 @@ class TestObjectController(unittest.TestCase):                                  headers={'Destination': 'c/o'})              req.environ['swift.authorize'] = authorize              self.app.update_request(req) -            res = controller.COPY(req) +            controller.COPY(req)          self.assert_(called[0])      def test_POST_converts_delete_after_to_delete_at(self): @@ -3838,6 +4301,7 @@ class TestObjectController(unittest.TestCase):              self.assertTrue('X-Delete-At-Host' in given_headers)              self.assertTrue('X-Delete-At-Device' in given_headers)              self.assertTrue('X-Delete-At-Partition' in given_headers) +            self.assertTrue('X-Delete-At-Container' in given_headers)              t = str(int(time.time() + 100)) + '.1'              req = Request.blank('/a/c/o', {}, @@ -3933,6 +4397,7 @@ class TestObjectController(unittest.TestCase):              self.assertTrue('X-Delete-At-Host' in given_headers)              self.assertTrue('X-Delete-At-Device' in given_headers)              self.assertTrue('X-Delete-At-Partition' in given_headers) +            self.assertTrue('X-Delete-At-Container' in given_headers)              t = str(int(time.time() + 100)) + '.1'              req = Request.blank('/a/c/o', {}, @@ -4023,7 +4488,6 @@ class TestObjectController(unittest.TestCase):                  return {                      'cors': {                          'allow_origin': 'http://foo.bar:8080 https://foo.bar', -                        'allow_headers': 'x-foo',                          'max_age': '999',                      }                  } @@ -4046,9 +4510,6 @@ class TestObjectController(unittest.TestCase):                  len(resp.headers['access-control-allow-methods'].split(', ')),                  7)              self.assertEquals('999', resp.headers['access-control-max-age']) -            self.assertEquals( -                'x-auth-token, x-foo', -                sortHeaderNames(resp.headers['access-control-allow-headers']))              req = Request.blank(                  '/a/c/o.jpg',                  {'REQUEST_METHOD': 'OPTIONS'}, @@ -4083,7 +4544,6 @@ class TestObjectController(unittest.TestCase):                  return {                      'cors': {                          'allow_origin': '*', -                        'allow_headers': 'x-foo',                          'max_age': '999',                      }                  } @@ -4106,9 +4566,6 @@ class TestObjectController(unittest.TestCase):                  len(resp.headers['access-control-allow-methods'].split(', ')),                  7)              self.assertEquals('999', resp.headers['access-control-max-age']) -            self.assertEquals( -                'x-auth-token, x-foo', -                sortHeaderNames(resp.headers['access-control-allow-headers']))      def test_CORS_valid(self):          with save_globals(): @@ -4153,9 +4610,9 @@ class TestObjectController(unittest.TestCase):      def _gather_x_container_headers(self, controller_call, req, *connect_args,                                      **kwargs): -        header_list = kwargs.pop('header_list', ['X-Container-Partition', +        header_list = kwargs.pop('header_list', ['X-Container-Device',                                                   'X-Container-Host', -                                                 'X-Container-Device']) +                                                 'X-Container-Partition'])          seen_headers = []          def capture_headers(ipaddr, port, device, partition, method, @@ -4176,7 +4633,7 @@ class TestObjectController(unittest.TestCase):              # don't care about the account/container HEADs, so chuck              # the first two requests              return sorted(seen_headers[2:], -                          key=lambda d: d.get(header_list[0]) or 'Z') +                          key=lambda d: d.get(header_list[0]) or 'z')      def test_PUT_x_container_headers_with_equal_replicas(self):          req = Request.blank('/a/c/o', environ={'REQUEST_METHOD': 'PUT'}, @@ -4187,13 +4644,13 @@ class TestObjectController(unittest.TestCase):              200, 200, 201, 201, 201)   # HEAD HEAD PUT PUT PUT          self.assertEqual(seen_headers, [                  {'X-Container-Host': '10.0.0.0:1000', -                 'X-Container-Partition': 1, +                 'X-Container-Partition': '1',                   'X-Container-Device': 'sda'},                  {'X-Container-Host': '10.0.0.1:1001', -                 'X-Container-Partition': 1, +                 'X-Container-Partition': '1',                   'X-Container-Device': 'sdb'},                  {'X-Container-Host': '10.0.0.2:1002', -                 'X-Container-Partition': 1, +                 'X-Container-Partition': '1',                   'X-Container-Device': 'sdc'}])      def test_PUT_x_container_headers_with_fewer_container_replicas(self): @@ -4208,10 +4665,10 @@ class TestObjectController(unittest.TestCase):          self.assertEqual(seen_headers, [                  {'X-Container-Host': '10.0.0.0:1000', -                 'X-Container-Partition': 1, +                 'X-Container-Partition': '1',                   'X-Container-Device': 'sda'},                  {'X-Container-Host': '10.0.0.1:1001', -                 'X-Container-Partition': 1, +                 'X-Container-Partition': '1',                   'X-Container-Device': 'sdb'},                  {'X-Container-Host': None,                   'X-Container-Partition': None, @@ -4229,13 +4686,13 @@ class TestObjectController(unittest.TestCase):          self.assertEqual(seen_headers, [                  {'X-Container-Host': '10.0.0.0:1000,10.0.0.3:1003', -                 'X-Container-Partition': 1, +                 'X-Container-Partition': '1',                   'X-Container-Device': 'sda,sdd'},                  {'X-Container-Host': '10.0.0.1:1001', -                 'X-Container-Partition': 1, +                 'X-Container-Partition': '1',                   'X-Container-Device': 'sdb'},                  {'X-Container-Host': '10.0.0.2:1002', -                 'X-Container-Partition': 1, +                 'X-Container-Partition': '1',                   'X-Container-Device': 'sdc'}])      def test_POST_x_container_headers_with_more_container_replicas(self): @@ -4251,13 +4708,13 @@ class TestObjectController(unittest.TestCase):          self.assertEqual(seen_headers, [                  {'X-Container-Host': '10.0.0.0:1000,10.0.0.3:1003', -                 'X-Container-Partition': 1, +                 'X-Container-Partition': '1',                   'X-Container-Device': 'sda,sdd'},                  {'X-Container-Host': '10.0.0.1:1001', -                 'X-Container-Partition': 1, +                 'X-Container-Partition': '1',                   'X-Container-Device': 'sdb'},                  {'X-Container-Host': '10.0.0.2:1002', -                 'X-Container-Partition': 1, +                 'X-Container-Partition': '1',                   'X-Container-Device': 'sdc'}])      def test_DELETE_x_container_headers_with_more_container_replicas(self): @@ -4271,66 +4728,87 @@ class TestObjectController(unittest.TestCase):              200, 200, 200, 200, 200)   # HEAD HEAD DELETE DELETE DELETE          self.assertEqual(seen_headers, [ -                {'X-Container-Host': '10.0.0.0:1000,10.0.0.3:1003', -                 'X-Container-Partition': 1, -                 'X-Container-Device': 'sda,sdd'}, -                {'X-Container-Host': '10.0.0.1:1001', -                 'X-Container-Partition': 1, -                 'X-Container-Device': 'sdb'}, -                {'X-Container-Host': '10.0.0.2:1002', -                 'X-Container-Partition': 1, -                 'X-Container-Device': 'sdc'}]) - +            {'X-Container-Host': '10.0.0.0:1000,10.0.0.3:1003', +             'X-Container-Partition': '1', +             'X-Container-Device': 'sda,sdd'}, +            {'X-Container-Host': '10.0.0.1:1001', +             'X-Container-Partition': '1', +             'X-Container-Device': 'sdb'}, +            {'X-Container-Host': '10.0.0.2:1002', +             'X-Container-Partition': '1', +             'X-Container-Device': 'sdc'} +        ]) + +    @mock.patch('time.time', new=lambda: STATIC_TIME)      def test_PUT_x_delete_at_with_fewer_container_replicas(self):          self.app.container_ring.set_replicas(2) +        delete_at_timestamp = int(time.time()) + 100000 +        delete_at_container = str( +            delete_at_timestamp / +            self.app.expiring_objects_container_divisor * +            self.app.expiring_objects_container_divisor)          req = Request.blank('/a/c/o', environ={'REQUEST_METHOD': 'PUT'},                              headers={'Content-Type': 'application/stuff',                                       'Content-Length': '0', -                                     'X-Delete-At': int(time.time()) + 100000}) +                                     'X-Delete-At': str(delete_at_timestamp)})          controller = proxy_server.ObjectController(self.app, 'a', 'c', 'o')          seen_headers = self._gather_x_container_headers(              controller.PUT, req,              200, 200, 201, 201, 201,   # HEAD HEAD PUT PUT PUT              header_list=('X-Delete-At-Host', 'X-Delete-At-Device', -                         'X-Delete-At-Partition')) +                         'X-Delete-At-Partition', 'X-Delete-At-Container'))          self.assertEqual(seen_headers, [ -                {'X-Delete-At-Host': '10.0.0.0:1000', -                 'X-Delete-At-Partition': 1, -                 'X-Delete-At-Device': 'sda'}, -                {'X-Delete-At-Host': '10.0.0.1:1001', -                 'X-Delete-At-Partition': 1, -                 'X-Delete-At-Device': 'sdb'}, -                {'X-Delete-At-Host': None, -                 'X-Delete-At-Partition': None, -                 'X-Delete-At-Device': None}]) - +            {'X-Delete-At-Host': '10.0.0.0:1000', +             'X-Delete-At-Container': delete_at_container, +             'X-Delete-At-Partition': '1', +             'X-Delete-At-Device': 'sda'}, +            {'X-Delete-At-Host': '10.0.0.1:1001', +             'X-Delete-At-Container': delete_at_container, +             'X-Delete-At-Partition': '1', +             'X-Delete-At-Device': 'sdb'}, +            {'X-Delete-At-Host': None, +             'X-Delete-At-Container': None, +             'X-Delete-At-Partition': None, +             'X-Delete-At-Device': None} +        ]) + +    @mock.patch('time.time', new=lambda: STATIC_TIME)      def test_PUT_x_delete_at_with_more_container_replicas(self):          self.app.container_ring.set_replicas(4)          self.app.expiring_objects_account = 'expires'          self.app.expiring_objects_container_divisor = 60 +        delete_at_timestamp = int(time.time()) + 100000 +        delete_at_container = str( +            delete_at_timestamp / +            self.app.expiring_objects_container_divisor * +            self.app.expiring_objects_container_divisor)          req = Request.blank('/a/c/o', environ={'REQUEST_METHOD': 'PUT'},                              headers={'Content-Type': 'application/stuff',                                       'Content-Length': 0, -                                     'X-Delete-At': int(time.time()) + 100000}) +                                     'X-Delete-At': str(delete_at_timestamp)})          controller = proxy_server.ObjectController(self.app, 'a', 'c', 'o')          seen_headers = self._gather_x_container_headers(              controller.PUT, req,              200, 200, 201, 201, 201,   # HEAD HEAD PUT PUT PUT              header_list=('X-Delete-At-Host', 'X-Delete-At-Device', -                         'X-Delete-At-Partition')) +                         'X-Delete-At-Partition', 'X-Delete-At-Container'))          self.assertEqual(seen_headers, [ -                {'X-Delete-At-Host': '10.0.0.0:1000,10.0.0.3:1003', -                 'X-Delete-At-Partition': 1, -                 'X-Delete-At-Device': 'sda,sdd'}, -                {'X-Delete-At-Host': '10.0.0.1:1001', -                 'X-Delete-At-Partition': 1, -                 'X-Delete-At-Device': 'sdb'}, -                {'X-Delete-At-Host': '10.0.0.2:1002', -                 'X-Delete-At-Partition': 1, -                 'X-Delete-At-Device': 'sdc'}]) +            {'X-Delete-At-Host': '10.0.0.0:1000,10.0.0.3:1003', +             'X-Delete-At-Container': delete_at_container, +             'X-Delete-At-Partition': '1', +             'X-Delete-At-Device': 'sda,sdd'}, +            {'X-Delete-At-Host': '10.0.0.1:1001', +             'X-Delete-At-Container': delete_at_container, +             'X-Delete-At-Partition': '1', +             'X-Delete-At-Device': 'sdb'}, +            {'X-Delete-At-Host': '10.0.0.2:1002', +             'X-Delete-At-Container': delete_at_container, +             'X-Delete-At-Partition': '1', +             'X-Delete-At-Device': 'sdc'} +        ])  class TestContainerController(unittest.TestCase): @@ -4342,6 +4820,17 @@ class TestContainerController(unittest.TestCase):                                              container_ring=FakeRing(),                                              object_ring=FakeRing()) +    def test_transfer_headers(self): +        src_headers = {'x-remove-versions-location': 'x', +                       'x-container-read': '*:user'} +        dst_headers = {'x-versions-location': 'backup'} +        controller = swift.proxy.controllers.ContainerController(self.app, +                                                                 'a', 'c') +        controller.transfer_headers(src_headers, dst_headers) +        expected_headers = {'x-versions-location': '', +                            'x-container-read': '*:user'} +        self.assertEqual(dst_headers, expected_headers) +      def assert_status_map(self, method, statuses, expected,                            raise_exc=False, missing_container=False):          with save_globals(): @@ -4364,12 +4853,12 @@ class TestContainerController(unittest.TestCase):              res = method(req)              self.assertEquals(res.status_int, expected) -    def test_HEAD(self): +    def test_HEAD_GET(self):          with save_globals(): -            controller = proxy_server.ContainerController(self.app, 'account', -                                                          'container') +            controller = proxy_server.ContainerController(self.app, 'a', 'c') -            def test_status_map(statuses, expected, **kwargs): +            def test_status_map(statuses, expected, +                                c_expected=None, a_expected=None, **kwargs):                  set_http_connect(*statuses, **kwargs)                  self.app.memcache.store = {}                  req = Request.blank('/a/c', {}) @@ -4380,12 +4869,60 @@ class TestContainerController(unittest.TestCase):                  if expected < 400:                      self.assert_('x-works' in res.headers)                      self.assertEquals(res.headers['x-works'], 'yes') -            test_status_map((200, 200, 404, 404), 200) -            test_status_map((200, 200, 500, 404), 200) -            test_status_map((200, 304, 500, 404), 304) -            test_status_map((200, 404, 404, 404), 404) -            test_status_map((200, 404, 404, 500), 404) -            test_status_map((200, 500, 500, 500), 503) +                if c_expected: +                    self.assertTrue('swift.container/a/c' in res.environ) +                    self.assertEquals(res.environ['swift.container/a/c']['status'], +                                      c_expected) +                else: +                    self.assertTrue('swift.container/a/c' not in res.environ) +                if a_expected: +                    self.assertTrue('swift.account/a' in res.environ) +                    self.assertEquals(res.environ['swift.account/a']['status'], +                                      a_expected) +                else: +                    self.assertTrue('swift.account/a' not in res.environ) + +                set_http_connect(*statuses, **kwargs) +                self.app.memcache.store = {} +                req = Request.blank('/a/c', {}) +                self.app.update_request(req) +                res = controller.GET(req) +                self.assertEquals(res.status[:len(str(expected))], +                                  str(expected)) +                if expected < 400: +                    self.assert_('x-works' in res.headers) +                    self.assertEquals(res.headers['x-works'], 'yes') +                if c_expected: +                    self.assertTrue('swift.container/a/c' in res.environ) +                    self.assertEquals(res.environ['swift.container/a/c']['status'], +                                      c_expected) +                else: +                    self.assertTrue('swift.container/a/c' not in res.environ) +                if a_expected: +                    self.assertTrue('swift.account/a' in res.environ) +                    self.assertEquals(res.environ['swift.account/a']['status'], +                                      a_expected) +                else: +                    self.assertTrue('swift.account/a' not in res.environ) +            # In all the following tests cache 200 for account +            # return and ache vary for container +            # return 200 and cache 200 for and container +            test_status_map((200, 200, 404, 404), 200, 200, 200) +            test_status_map((200, 200, 500, 404), 200, 200, 200) +            # return 304 dont cache container +            test_status_map((200, 304, 500, 404), 304, None, 200) +            # return 404 and cache 404 for container +            test_status_map((200, 404, 404, 404), 404, 404, 200) +            test_status_map((200, 404, 404, 500), 404, 404, 200) +            # return 503, dont cache container +            test_status_map((200, 500, 500, 500), 503, None, 200) +            self.assertFalse(self.app.account_autocreate) + +            # In all the following tests cache 404 for account +            # return 404 (as account is not found) and dont cache container +            test_status_map((404, 404, 404), 404, None, 404) +            self.app.account_autocreate = True # This should make no difference +            test_status_map((404, 404, 404), 404, None, 404)      def test_PUT(self):          with save_globals(): @@ -4401,10 +4938,73 @@ class TestContainerController(unittest.TestCase):                  res = controller.PUT(req)                  expected = str(expected)                  self.assertEquals(res.status[:len(expected)], expected) + +            test_status_map((200, 201, 201, 201), 201, missing_container=True) +            test_status_map((200, 201, 201, 500), 201, missing_container=True) +            test_status_map((200, 204, 404, 404), 404, missing_container=True) +            test_status_map((200, 204, 500, 404), 503, missing_container=True) +            self.assertFalse(self.app.account_autocreate) +            test_status_map((404, 404, 404), 404, missing_container=True) +            self.app.account_autocreate = True +            #fail to retrieve account info +            test_status_map( +                (503, 503, 503), # account_info fails on 503 +                 404, missing_container=True) +            # account fail after creation +            test_status_map( +                (404, 404, 404, # account_info fails on 404 +                 201, 201, 201, # PUT account +                 404, 404, 404), # account_info fail +                 404, missing_container=True) +            test_status_map( +                (503, 503, 404, # account_info fails on 404 +                 503, 503, 503, # PUT account +                 503, 503, 404), # account_info fail +                 404, missing_container=True) +            #put fails +            test_status_map( +                (404, 404, 404, # account_info fails on 404 +                 201, 201, 201, # PUT account +                 200, # account_info success +                 503, 503, 201), # put container fail +                 503, missing_container=True) +            # all goes according to plan +            test_status_map( +                (404, 404, 404, # account_info fails on 404 +                 201, 201, 201, # PUT account +                 200, # account_info success +                 201, 201, 201), # put container success +                 201, missing_container=True) +            test_status_map( +                (503, 404, 404, # account_info fails on 404 +                 503, 201, 201, # PUT account +                 503, 200, # account_info success +                 503, 201, 201), # put container success +                 201, missing_container=True) + +    def test_POST(self): +        with save_globals(): +            controller = proxy_server.ContainerController(self.app, 'account', +                                                          'container') + +            def test_status_map(statuses, expected, **kwargs): +                set_http_connect(*statuses, **kwargs) +                self.app.memcache.store = {} +                req = Request.blank('/a/c', {}) +                req.content_length = 0 +                self.app.update_request(req) +                res = controller.POST(req) +                expected = str(expected) +                self.assertEquals(res.status[:len(expected)], expected) +              test_status_map((200, 201, 201, 201), 201, missing_container=True)              test_status_map((200, 201, 201, 500), 201, missing_container=True)              test_status_map((200, 204, 404, 404), 404, missing_container=True)              test_status_map((200, 204, 500, 404), 503, missing_container=True) +            self.assertFalse(self.app.account_autocreate) +            test_status_map((404, 404, 404), 404, missing_container=True) +            self.app.account_autocreate = True +            test_status_map((404, 404, 404), 404, missing_container=True)      def test_PUT_max_containers_per_account(self):          with save_globals(): @@ -4475,14 +5075,20 @@ class TestContainerController(unittest.TestCase):                  self.assertEquals(resp.status_int, 200)                  set_http_connect(404, 404, 404, 200, 200, 200) +                # Make sure it is a blank request wthout env caching +                req = Request.blank('/a/c', environ={'REQUEST_METHOD': meth})                  resp = getattr(controller, meth)(req)                  self.assertEquals(resp.status_int, 404)                  set_http_connect(503, 404, 404) +                # Make sure it is a blank request wthout env caching +                req = Request.blank('/a/c', environ={'REQUEST_METHOD': meth})                  resp = getattr(controller, meth)(req)                  self.assertEquals(resp.status_int, 404)                  set_http_connect(503, 404, raise_exc=True) +                # Make sure it is a blank request wthout env caching +                req = Request.blank('/a/c', environ={'REQUEST_METHOD': meth})                  resp = getattr(controller, meth)(req)                  self.assertEquals(resp.status_int, 404) @@ -4490,6 +5096,8 @@ class TestContainerController(unittest.TestCase):                      dev['errors'] = self.app.error_suppression_limit + 1                      dev['last_error'] = time.time()                  set_http_connect(200, 200, 200, 200, 200, 200) +                # Make sure it is a blank request wthout env caching +                req = Request.blank('/a/c', environ={'REQUEST_METHOD': meth})                  resp = getattr(controller, meth)(req)                  self.assertEquals(resp.status_int, 404) @@ -4506,7 +5114,7 @@ class TestContainerController(unittest.TestCase):                  if self.allow_lock:                      yield True                  else: -                    raise MemcacheLockError() +                    raise NotImplementedError          with save_globals():              controller = proxy_server.ContainerController(self.app, 'account', @@ -4567,279 +5175,6 @@ class TestContainerController(unittest.TestCase):              self.assert_status_map(controller.DELETE,                                     (200, 404, 404, 404), 404) -    def test_DELETE_container_that_does_not_exist(self): -        prolis = _test_sockets[0] -        # Create a container -        sock = connect_tcp(('localhost', prolis.getsockname()[1])) -        fd = sock.makefile() -        fd.write('PUT /v1/a/aaabbbccc HTTP/1.1\r\nHost: localhost\r\n' -                 'Connection: close\r\nX-Storage-Token: t\r\n' -                 'Content-Length: 0\r\n\r\n') -        fd.flush() -        headers = readuntil2crlfs(fd) -        exp = 'HTTP/1.1 201' -        self.assertEquals(headers[:len(exp)], exp) - -        # Delete container -        sock = connect_tcp(('localhost', prolis.getsockname()[1])) -        fd = sock.makefile() -        fd.write('DELETE /v1/a/aaabbbccc HTTP/1.1\r\nHost: localhost\r\n' -                 'Connection: close\r\nX-Storage-Token: t\r\n' -                 'Content-Length: 0\r\n\r\n') -        fd.flush() -        headers = readuntil2crlfs(fd) -        exp = 'HTTP/1.1 204' -        self.assertEquals(headers[:len(exp)], exp) - -        # Delete again -        sock = connect_tcp(('localhost', prolis.getsockname()[1])) -        fd = sock.makefile() -        fd.write('DELETE /v1/a/aaabbbccc HTTP/1.1\r\nHost: localhost\r\n' -                 'Connection: close\r\nX-Storage-Token: t\r\n' -                 'Content-Length: 0\r\n\r\n') -        fd.flush() -        headers = readuntil2crlfs(fd) -        exp = 'HTTP/1.1 404' -        self.assertEquals(headers[:len(exp)], exp) - -    def test_dir_object_not_lost(self): -        prolis = _test_sockets[0] -        prosrv = _test_servers[0] - -        # Create a container -        sock = connect_tcp(('localhost', prolis.getsockname()[1])) -        fd = sock.makefile() -        fd.write('PUT /v1/a/dir_obj_test HTTP/1.1\r\nHost: localhost\r\n' -                 'Connection: close\r\nX-Storage-Token: t\r\n' -                 'Content-Length: 0\r\n\r\n') -        fd.flush() -        headers = readuntil2crlfs(fd) -        exp = 'HTTP/1.1 201' -        self.assertEquals(headers[:len(exp)], exp) - -        # Create a dir obj A -        dir_list = ['a', 'a/b', 'a/b/c'] - -        for dir_obj in dir_list: -            sock = connect_tcp(('localhost', prolis.getsockname()[1])) -            fd = sock.makefile() -            fd.write('PUT /v1/a/dir_obj_test/%s HTTP/1.1\r\nHost: localhost\r\n' -                     'Connection: close\r\nX-Storage-Token: t\r\n' -                     'Content-Length: 0\r\n' -                     'Content-type: application/directory\r\n\r\n' % dir_obj) -            fd.flush() -            headers = readuntil2crlfs(fd) -            exp = 'HTTP/1.1 201' -            self.assertEquals(headers[:len(exp)], exp) - -        # Check we see all the objects we created -        req = Request.blank('/v1/a/dir_obj_test', -                            environ={'REQUEST_METHOD': 'GET'}) -        res = req.get_response(prosrv) -        obj_list = res.body.split('\n') -        for dir_obj in dir_list: -            self.assertTrue(dir_obj in obj_list) - -        # Now let's create a file obj -        fakedata = 'a' * 1024 -        sock = connect_tcp(('localhost', prolis.getsockname()[1])) -        fd = sock.makefile() -        fd.write('PUT /v1/a/dir_obj_test/a/b/c/file1 HTTP/1.1\r\nHost: localhost\r\n' -                 'Connection: close\r\nX-Storage-Token: t\r\n' -                 'Content-Length: %s\r\n' -                 'Content-Type: application/octect-stream\r\n' -                 '\r\n%s' % (str(len(fakedata)), fakedata)) -        fd.flush() -        headers = readuntil2crlfs(fd) -        exp = 'HTTP/1.1 201' -        self.assertEquals(headers[:len(exp)], exp) - -        # Now check we get all dir objs and the file obj -        req = Request.blank('/v1/a/dir_obj_test', -                            environ={'REQUEST_METHOD': 'GET'}) -        res = req.get_response(prosrv) -        obj_list = res.body.split('\n') -        for dir_obj in dir_list: -            self.assertTrue(dir_obj in obj_list) -        self.assertTrue('a/b/c/file1' in obj_list) - -        # Delete dir objects, file should still be available -        for dir_obj in dir_list: -            sock = connect_tcp(('localhost', prolis.getsockname()[1])) -            fd = sock.makefile() -            fd.write('DELETE /v1/a/dir_obj_test/%s HTTP/1.1\r\nHost: localhost\r\n' -                     'Connection: close\r\nX-Storage-Token: t\r\n' -                     '\r\n' % dir_obj) -            fd.flush() -            headers = readuntil2crlfs(fd) -            exp = 'HTTP/1.1 204' -            self.assertEquals(headers[:len(exp)], exp) - -        # Now check file is still available -        req = Request.blank('/v1/a/dir_obj_test', -                            environ={'REQUEST_METHOD': 'GET'}) -        res = req.get_response(prosrv) -        obj_list = res.body.split('\n') -        for dir_obj in dir_list: -            self.assertFalse(dir_obj in obj_list) -        self.assertTrue('a/b/c/file1' in obj_list) - -        # Delete file -        sock = connect_tcp(('localhost', prolis.getsockname()[1])) -        fd = sock.makefile() -        fd.write('DELETE /v1/a/dir_obj_test/a/b/c/file1 HTTP/1.1\r\nHost: localhost\r\n' -                 'Connection: close\r\nX-Storage-Token: t\r\n' -                 '\r\n') -        fd.flush() -        headers = readuntil2crlfs(fd) -        exp = 'HTTP/1.1 204' -        self.assertEquals(headers[:len(exp)], exp) - -        # Delete continer -        sock = connect_tcp(('localhost', prolis.getsockname()[1])) -        fd = sock.makefile() -        fd.write('DELETE /v1/a/dir_obj_test HTTP/1.1\r\nHost: localhost\r\n' -                 'Connection: close\r\nX-Storage-Token: t\r\n' -                 'Content-Length: 0\r\n\r\n') -        fd.flush() -        headers = readuntil2crlfs(fd) -        exp = 'HTTP/1.1 204' -        self.assertEquals(headers[:len(exp)], exp) - -    def test_container_lists_dir_and_file_objects(self): -        prolis = _test_sockets[0] -        prosrv = _test_servers[0] - -        # Create a container -        sock = connect_tcp(('localhost', prolis.getsockname()[1])) -        fd = sock.makefile() -        fd.write('PUT /v1/a/list_test HTTP/1.1\r\nHost: localhost\r\n' -                 'Connection: close\r\nX-Storage-Token: t\r\n' -                 'Content-Length: 0\r\n\r\n') -        fd.flush() -        headers = readuntil2crlfs(fd) -        exp = 'HTTP/1.1 201' -        self.assertEquals(headers[:len(exp)], exp) - -        # Create a file obj -        fakedata = 'a' * 1024 -        sock = connect_tcp(('localhost', prolis.getsockname()[1])) -        fd = sock.makefile() -        fd.write('PUT /v1/a/list_test/a/b/c/file1 HTTP/1.1\r\nHost: localhost\r\n' -                 'Connection: close\r\nX-Storage-Token: t\r\n' -                 'Content-Length: %s\r\n' -                 'Content-Type: application/octect-stream\r\n' -                 '\r\n%s' % (str(len(fakedata)), fakedata)) -        fd.flush() -        headers = readuntil2crlfs(fd) -        exp = 'HTTP/1.1 201' -        self.assertEquals(headers[:len(exp)], exp) - -        # Create a second file obj -        sock = connect_tcp(('localhost', prolis.getsockname()[1])) -        fd = sock.makefile() -        fd.write('PUT /v1/a/list_test/a/b/c/file2 HTTP/1.1\r\nHost: localhost\r\n' -                 'Connection: close\r\nX-Storage-Token: t\r\n' -                 'Content-Length: %s\r\n' -                 'Content-Type: application/octect-stream\r\n' -                 '\r\n%s' % (str(len(fakedata)), fakedata)) -        fd.flush() -        headers = readuntil2crlfs(fd) -        exp = 'HTTP/1.1 201' -        self.assertEquals(headers[:len(exp)], exp) - -        # Create a third file obj -        sock = connect_tcp(('localhost', prolis.getsockname()[1])) -        fd = sock.makefile() -        fd.write('PUT /v1/a/list_test/file3 HTTP/1.1\r\nHost: localhost\r\n' -                 'Connection: close\r\nX-Storage-Token: t\r\n' -                 'Content-Length: %s\r\n' -                 'Content-Type: application/octect-stream\r\n' -                 '\r\n%s' % (str(len(fakedata)), fakedata)) -        fd.flush() -        headers = readuntil2crlfs(fd) -        exp = 'HTTP/1.1 201' -        self.assertEquals(headers[:len(exp)], exp) - -        # Create a dir obj -        sock = connect_tcp(('localhost', prolis.getsockname()[1])) -        fd = sock.makefile() -        fd.write('PUT /v1/a/list_test/a/b/c/dir1 HTTP/1.1\r\nHost: localhost\r\n' -                 'Connection: close\r\nX-Storage-Token: t\r\n' -                 'Content-Length: 0\r\n' -                 'Content-type: application/directory\r\n\r\n') -        fd.flush() -        headers = readuntil2crlfs(fd) -        exp = 'HTTP/1.1 201' -        self.assertEquals(headers[:len(exp)], exp) - -        # Path tests -        req = Request.blank('/v1/a/list_test?path=', -                            environ={'REQUEST_METHOD': 'GET'}) -        res = req.get_response(prosrv) -        obj_list = res.body.split('\n') -        self.assertFalse('a/b/c/file1' in obj_list) -        self.assertFalse('a/b/c/file2' in obj_list) -        self.assertFalse('a/b/c/dir1' in obj_list) -        self.assertTrue('file3' in obj_list) - -        req = Request.blank('/v1/a/list_test?path=a/b/c', -                            environ={'REQUEST_METHOD': 'GET'}) -        res = req.get_response(prosrv) -        obj_list = res.body.split('\n') -        self.assertTrue('a/b/c/file1' in obj_list) -        self.assertTrue('a/b/c/file2' in obj_list) -        self.assertTrue('a/b/c/dir1' in obj_list) -        self.assertFalse('file3' in obj_list) - -        # Try to delete, but expect failure since the -        # container is not empty -        sock = connect_tcp(('localhost', prolis.getsockname()[1])) -        fd = sock.makefile() -        fd.write('DELETE /v1/a/list_test HTTP/1.1\r\nHost: localhost\r\n' -                 'Connection: close\r\nX-Storage-Token: t\r\n' -                 'Content-Length: 0\r\n\r\n') -        fd.flush() -        headers = readuntil2crlfs(fd) -        exp = 'HTTP/1.1 409' -        self.assertEquals(headers[:len(exp)], exp) - -        # Get object list -        req = Request.blank('/v1/a/list_test', -                            environ={'REQUEST_METHOD': 'GET'}) -        res = req.get_response(prosrv) -        obj_list = res.body.split('\n') -        self.assertTrue('a/b/c/file1' in obj_list) -        self.assertTrue('a/b/c/file2' in obj_list) -        self.assertTrue('a/b/c/dir1' in obj_list) -        self.assertTrue('file3' in obj_list) -        self.assertEqual(res.headers['x-container-object-count'], '4') - -        # Now let's delete the objects -        for obj in obj_list: -            if not obj: -                continue -            sock = connect_tcp(('localhost', prolis.getsockname()[1])) -            fd = sock.makefile() -            fd.write('DELETE /v1/a/list_test/%s HTTP/1.1\r\nHost: localhost\r\n' -                     'Connection: close\r\nX-Storage-Token: t\r\n' -                     '\r\n' % obj) -            fd.flush() -            headers = readuntil2crlfs(fd) -            exp = 'HTTP/1.1 204' -            self.assertEquals(headers[:len(exp)], exp) - -        # Delete continer which has stale directies a/b/c -        sock = connect_tcp(('localhost', prolis.getsockname()[1])) -        fd = sock.makefile() -        fd.write('DELETE /v1/a/list_test HTTP/1.1\r\nHost: localhost\r\n' -                 'Connection: close\r\nX-Storage-Token: t\r\n' -                 'Content-Length: 0\r\n\r\n') -        fd.flush() -        headers = readuntil2crlfs(fd) -        exp = 'HTTP/1.1 204' -        self.assertEquals(headers[:len(exp)], exp) -      def test_response_get_accept_ranges_header(self):          with save_globals():              set_http_connect(200, 200, body='{}') @@ -4902,7 +5237,7 @@ class TestContainerController(unittest.TestCase):                  req = Request.blank('/a/c', environ={'REQUEST_METHOD': method},                                      headers={test_header: test_value})                  self.app.update_request(req) -                res = getattr(controller, method)(req) +                getattr(controller, method)(req)                  self.assertEquals(test_errors, [])      def test_PUT_bad_metadata(self): @@ -5009,7 +5344,7 @@ class TestContainerController(unittest.TestCase):                                  headers={'X-Container-Read': '.r:*'})              req.environ['swift.clean_acl'] = clean_acl              self.app.update_request(req) -            res = controller.POST(req) +            controller.POST(req)          self.assert_(called[0])          called[0] = False          with save_globals(): @@ -5020,7 +5355,7 @@ class TestContainerController(unittest.TestCase):                                  headers={'X-Container-Write': '.r:*'})              req.environ['swift.clean_acl'] = clean_acl              self.app.update_request(req) -            res = controller.POST(req) +            controller.POST(req)          self.assert_(called[0])      def test_PUT_calls_clean_acl(self): @@ -5037,7 +5372,7 @@ class TestContainerController(unittest.TestCase):                                  headers={'X-Container-Read': '.r:*'})              req.environ['swift.clean_acl'] = clean_acl              self.app.update_request(req) -            res = controller.PUT(req) +            controller.PUT(req)          self.assert_(called[0])          called[0] = False          with save_globals(): @@ -5048,7 +5383,7 @@ class TestContainerController(unittest.TestCase):                                  headers={'X-Container-Write': '.r:*'})              req.environ['swift.clean_acl'] = clean_acl              self.app.update_request(req) -            res = controller.PUT(req) +            controller.PUT(req)          self.assert_(called[0])      def test_GET_no_content(self): @@ -5059,6 +5394,7 @@ class TestContainerController(unittest.TestCase):              req = Request.blank('/a/c')              self.app.update_request(req)              res = controller.GET(req) +            self.assertEquals(res.environ['swift.container/a/c']['status'], 204)              self.assertEquals(res.content_length, 0)              self.assertTrue('transfer-encoding' not in res.headers) @@ -5076,6 +5412,7 @@ class TestContainerController(unittest.TestCase):              req.environ['swift.authorize'] = authorize              self.app.update_request(req)              res = controller.GET(req) +        self.assertEquals(res.environ['swift.container/a/c']['status'], 201)          self.assert_(called[0])      def test_HEAD_calls_authorize(self): @@ -5091,7 +5428,7 @@ class TestContainerController(unittest.TestCase):              req = Request.blank('/a/c', {'REQUEST_METHOD': 'HEAD'})              req.environ['swift.authorize'] = authorize              self.app.update_request(req) -            res = controller.HEAD(req) +            controller.HEAD(req)          self.assert_(called[0])      def test_OPTIONS(self): @@ -5124,7 +5461,6 @@ class TestContainerController(unittest.TestCase):                  return {                      'cors': {                          'allow_origin': 'http://foo.bar:8080 https://foo.bar', -                        'allow_headers': 'x-foo',                          'max_age': '999',                      }                  } @@ -5147,9 +5483,6 @@ class TestContainerController(unittest.TestCase):                  len(resp.headers['access-control-allow-methods'].split(', ')),                  6)              self.assertEquals('999', resp.headers['access-control-max-age']) -            self.assertEquals( -                'x-auth-token, x-foo', -                sortHeaderNames(resp.headers['access-control-allow-headers']))              req = Request.blank(                  '/a/c',                  {'REQUEST_METHOD': 'OPTIONS'}, @@ -5185,7 +5518,6 @@ class TestContainerController(unittest.TestCase):                  return {                      'cors': {                          'allow_origin': '*', -                        'allow_headers': 'x-foo',                          'max_age': '999',                      }                  } @@ -5208,8 +5540,20 @@ class TestContainerController(unittest.TestCase):                  len(resp.headers['access-control-allow-methods'].split(', ')),                  6)              self.assertEquals('999', resp.headers['access-control-max-age']) + +            req = Request.blank( +                '/a/c/o.jpg', +                {'REQUEST_METHOD': 'OPTIONS'}, +                headers={'Origin': 'https://bar.baz', +                         'Access-Control-Request-Headers': +                         'x-foo, x-bar, x-auth-token', +                         'Access-Control-Request-Method': 'GET'} +            ) +            req.content_length = 0 +            resp = controller.OPTIONS(req) +            self.assertEquals(200, resp.status_int)              self.assertEquals( -                'x-auth-token, x-foo', +                sortHeaderNames('x-foo, x-bar, x-auth-token'),                  sortHeaderNames(resp.headers['access-control-allow-headers']))      def test_CORS_valid(self): @@ -5288,15 +5632,16 @@ class TestContainerController(unittest.TestCase):              controller.PUT, req,              200, 201, 201, 201)    # HEAD PUT PUT PUT          self.assertEqual(seen_headers, [ -                {'X-Account-Host': '10.0.0.0:1000', -                 'X-Account-Partition': 1, -                 'X-Account-Device': 'sda'}, -                {'X-Account-Host': '10.0.0.1:1001', -                 'X-Account-Partition': 1, -                 'X-Account-Device': 'sdb'}, -                {'X-Account-Host': None, -                 'X-Account-Partition': None, -                 'X-Account-Device': None}]) +            {'X-Account-Host': '10.0.0.0:1000', +             'X-Account-Partition': '1', +             'X-Account-Device': 'sda'}, +            {'X-Account-Host': '10.0.0.1:1001', +             'X-Account-Partition': '1', +             'X-Account-Device': 'sdb'}, +            {'X-Account-Host': None, +             'X-Account-Partition': None, +             'X-Account-Device': None} +        ])      def test_PUT_x_account_headers_with_more_account_replicas(self):          self.app.account_ring.set_replicas(4) @@ -5307,15 +5652,16 @@ class TestContainerController(unittest.TestCase):              controller.PUT, req,              200, 201, 201, 201)    # HEAD PUT PUT PUT          self.assertEqual(seen_headers, [ -                {'X-Account-Host': '10.0.0.0:1000,10.0.0.3:1003', -                 'X-Account-Partition': 1, -                 'X-Account-Device': 'sda,sdd'}, -                {'X-Account-Host': '10.0.0.1:1001', -                 'X-Account-Partition': 1, -                 'X-Account-Device': 'sdb'}, -                {'X-Account-Host': '10.0.0.2:1002', -                 'X-Account-Partition': 1, -                 'X-Account-Device': 'sdc'}]) +            {'X-Account-Host': '10.0.0.0:1000,10.0.0.3:1003', +             'X-Account-Partition': '1', +             'X-Account-Device': 'sda,sdd'}, +            {'X-Account-Host': '10.0.0.1:1001', +             'X-Account-Partition': '1', +             'X-Account-Device': 'sdb'}, +            {'X-Account-Host': '10.0.0.2:1002', +             'X-Account-Partition': '1', +             'X-Account-Device': 'sdc'} +        ])      def test_DELETE_x_account_headers_with_fewer_account_replicas(self):          self.app.account_ring.set_replicas(2) @@ -5326,15 +5672,16 @@ class TestContainerController(unittest.TestCase):              controller.DELETE, req,              200, 204, 204, 204)    # HEAD DELETE DELETE DELETE          self.assertEqual(seen_headers, [ -                {'X-Account-Host': '10.0.0.0:1000', -                 'X-Account-Partition': 1, -                 'X-Account-Device': 'sda'}, -                {'X-Account-Host': '10.0.0.1:1001', -                 'X-Account-Partition': 1, -                 'X-Account-Device': 'sdb'}, -                {'X-Account-Host': None, -                 'X-Account-Partition': None, -                 'X-Account-Device': None}]) +            {'X-Account-Host': '10.0.0.0:1000', +             'X-Account-Partition': '1', +             'X-Account-Device': 'sda'}, +            {'X-Account-Host': '10.0.0.1:1001', +             'X-Account-Partition': '1', +             'X-Account-Device': 'sdb'}, +            {'X-Account-Host': None, +             'X-Account-Partition': None, +             'X-Account-Device': None} +        ])      def test_DELETE_x_account_headers_with_more_account_replicas(self):          self.app.account_ring.set_replicas(4) @@ -5345,15 +5692,16 @@ class TestContainerController(unittest.TestCase):              controller.DELETE, req,              200, 204, 204, 204)    # HEAD DELETE DELETE DELETE          self.assertEqual(seen_headers, [ -                {'X-Account-Host': '10.0.0.0:1000,10.0.0.3:1003', -                 'X-Account-Partition': 1, -                 'X-Account-Device': 'sda,sdd'}, -                {'X-Account-Host': '10.0.0.1:1001', -                 'X-Account-Partition': 1, -                 'X-Account-Device': 'sdb'}, -                {'X-Account-Host': '10.0.0.2:1002', -                 'X-Account-Partition': 1, -                 'X-Account-Device': 'sdc'}]) +            {'X-Account-Host': '10.0.0.0:1000,10.0.0.3:1003', +             'X-Account-Partition': '1', +             'X-Account-Device': 'sda,sdd'}, +            {'X-Account-Host': '10.0.0.1:1001', +             'X-Account-Partition': '1', +             'X-Account-Device': 'sdb'}, +            {'X-Account-Host': '10.0.0.2:1002', +             'X-Account-Partition': '1', +             'X-Account-Device': 'sdc'} +        ])  class TestAccountController(unittest.TestCase): @@ -5364,18 +5712,24 @@ class TestAccountController(unittest.TestCase):                                              container_ring=FakeRing(),                                              object_ring=FakeRing) -    def assert_status_map(self, method, statuses, expected): +    def assert_status_map(self, method, statuses, expected, env_expected=None):          with save_globals():              set_http_connect(*statuses)              req = Request.blank('/a', {})              self.app.update_request(req)              res = method(req)              self.assertEquals(res.status_int, expected) +            if env_expected: +                self.assertEquals(res.environ['swift.account/a']['status'], +                                  env_expected)              set_http_connect(*statuses)              req = Request.blank('/a/', {})              self.app.update_request(req)              res = method(req)              self.assertEquals(res.status_int, expected) +            if env_expected: +                self.assertEquals(res.environ['swift.account/a']['status'], +                                  env_expected)      def test_OPTIONS(self):          with save_globals(): @@ -5420,83 +5774,102 @@ class TestAccountController(unittest.TestCase):      def test_GET(self):          with save_globals():              controller = proxy_server.AccountController(self.app, 'account') -            self.assert_status_map(controller.GET, (200, 200, 200), 200) -            self.assert_status_map(controller.GET, (200, 200, 503), 200) -            self.assert_status_map(controller.GET, (200, 503, 503), 200) -            self.assert_status_map(controller.GET, (204, 204, 204), 204) -            self.assert_status_map(controller.GET, (204, 204, 503), 204) -            self.assert_status_map(controller.GET, (204, 503, 503), 204) -            self.assert_status_map(controller.GET, (204, 204, 200), 204) -            self.assert_status_map(controller.GET, (204, 200, 200), 204) -            self.assert_status_map(controller.GET, (404, 404, 404), 404) -            self.assert_status_map(controller.GET, (404, 404, 200), 200) -            self.assert_status_map(controller.GET, (404, 200, 200), 200) -            self.assert_status_map(controller.GET, (404, 404, 503), 404) +            # GET returns after the first successful call to an Account Server +            self.assert_status_map(controller.GET, (200,), 200, 200) +            self.assert_status_map(controller.GET, (503, 200), 200, 200) +            self.assert_status_map(controller.GET, (503, 503, 200), 200, 200) +            self.assert_status_map(controller.GET, (204,), 204, 204) +            self.assert_status_map(controller.GET, (503, 204), 204, 204) +            self.assert_status_map(controller.GET, (503, 503, 204), 204, 204) +            self.assert_status_map(controller.GET, (404, 200), 200, 200) +            self.assert_status_map(controller.GET, (404, 404, 200), 200, 200) +            self.assert_status_map(controller.GET, (404, 503, 204), 204, 204) +            # If Account servers fail, if autocreate = False, return majority +            # response +            self.assert_status_map(controller.GET, (404, 404, 404), 404, 404) +            self.assert_status_map(controller.GET, (404, 404, 503), 404, 404)              self.assert_status_map(controller.GET, (404, 503, 503), 503) -            self.assert_status_map(controller.GET, (404, 204, 503), 204)              self.app.memcache = FakeMemcacheReturnsNone() -            self.assert_status_map(controller.GET, (404, 404, 404), 404) +            self.assert_status_map(controller.GET, (404, 404, 404), 404, 404)      def test_GET_autocreate(self):          with save_globals():              controller = proxy_server.AccountController(self.app, 'account')              self.app.memcache = FakeMemcacheReturnsNone() +            self.assertFalse(self.app.account_autocreate) +            # Repeat the test for autocreate = False and 404 by all +            self.assert_status_map(controller.GET, +                                   (404, 404, 404), 404)              self.assert_status_map(controller.GET, -                                   (404, 404, 404, 201, 201, 201, 204), 404) +                                   (404, 503, 404), 404) +            # When autocreate is True, if none of the nodes respond 2xx +            # And quorum of the nodes responded 404, +            # ALL nodes are asked to create the account +            # If successful, the GET request is repeated.              controller.app.account_autocreate = True              self.assert_status_map(controller.GET, -                                   (404, 404, 404, 201, 201, 201, 204), 204) +                                   (404, 404, 404), 204)              self.assert_status_map(controller.GET, -                                   (404, 404, 404, 403, 403, 403, 403), 403) +                                   (404, 503, 404), 204) + +            # We always return 503 if no majority between 4xx, 3xx or 2xx found              self.assert_status_map(controller.GET, -                                   (404, 404, 404, 409, 409, 409, 409), 409) +                                   (500, 500, 400), 503)      def test_HEAD(self): +        # Same behaviour as GET          with save_globals():              controller = proxy_server.AccountController(self.app, 'account') -            self.assert_status_map(controller.HEAD, (200, 200, 200), 200) -            self.assert_status_map(controller.HEAD, (200, 200, 503), 200) -            self.assert_status_map(controller.HEAD, (200, 503, 503), 200) -            self.assert_status_map(controller.HEAD, (204, 204, 204), 204) -            self.assert_status_map(controller.HEAD, (204, 204, 503), 204) -            self.assert_status_map(controller.HEAD, (204, 503, 503), 204) -            self.assert_status_map(controller.HEAD, (204, 204, 200), 204) -            self.assert_status_map(controller.HEAD, (204, 200, 200), 204) -            self.assert_status_map(controller.HEAD, (404, 404, 404), 404) -            self.assert_status_map(controller.HEAD, (404, 404, 200), 200) -            self.assert_status_map(controller.HEAD, (404, 200, 200), 200) -            self.assert_status_map(controller.HEAD, (404, 404, 503), 404) +            self.assert_status_map(controller.HEAD, (200,), 200, 200) +            self.assert_status_map(controller.HEAD, (503, 200), 200, 200) +            self.assert_status_map(controller.HEAD, (503, 503, 200), 200, 200) +            self.assert_status_map(controller.HEAD, (204,), 204, 204) +            self.assert_status_map(controller.HEAD, (503, 204), 204, 204) +            self.assert_status_map(controller.HEAD, (204, 503, 503), 204, 204) +            self.assert_status_map(controller.HEAD, (204,), 204, 204) +            self.assert_status_map(controller.HEAD, (404, 404, 404), 404, 404) +            self.assert_status_map(controller.HEAD, (404, 404, 200), 200, 200) +            self.assert_status_map(controller.HEAD, (404, 200), 200, 200) +            self.assert_status_map(controller.HEAD, (404, 404, 503), 404, 404)              self.assert_status_map(controller.HEAD, (404, 503, 503), 503) -            self.assert_status_map(controller.HEAD, (404, 204, 503), 204) +            self.assert_status_map(controller.HEAD, (404, 503, 204), 204, 204)      def test_HEAD_autocreate(self): +        # Same behaviour as GET          with save_globals():              controller = proxy_server.AccountController(self.app, 'account')              self.app.memcache = FakeMemcacheReturnsNone() +            self.assertFalse(self.app.account_autocreate)              self.assert_status_map(controller.HEAD, -                                   (404, 404, 404, 201, 201, 201, 204), 404) +                                   (404, 404, 404), 404)              controller.app.account_autocreate = True              self.assert_status_map(controller.HEAD, -                                   (404, 404, 404, 201, 201, 201, 204), 204) +                                   (404, 404, 404), 204)              self.assert_status_map(controller.HEAD, -                                   (404, 404, 404, 403, 403, 403, 403), 403) +                                   (500, 404, 404), 204) +            # We always return 503 if no majority between 4xx, 3xx or 2xx found              self.assert_status_map(controller.HEAD, -                                   (404, 404, 404, 409, 409, 409, 409), 409) +                                   (500, 500, 400), 503)      def test_POST_autocreate(self):          with save_globals():              controller = proxy_server.AccountController(self.app, 'account')              self.app.memcache = FakeMemcacheReturnsNone() +            # first test with autocreate being False +            self.assertFalse(self.app.account_autocreate)              self.assert_status_map(controller.POST, -                                   (404, 404, 404, 201, 201, 201), 404) +                (404, 404, 404), 404) +            # next turn it on and test account being created than updated              controller.app.account_autocreate = True              self.assert_status_map(controller.POST, -                                   (404, 404, 404, 201, 201, 201), 201) +                (404, 404, 404, 202, 202, 202, 201, 201, 201), 201) +                # account_info  PUT account  POST account              self.assert_status_map(controller.POST, -                                   (404, 404, 404, 403, 403, 403, 403), 403) +                (404, 404, 503, 201, 201, 503,  204, 204, 504), 204) +            # what if create fails              self.assert_status_map(controller.POST, -                                   (404, 404, 404, 409, 409, 409, 409), 409) +                (404, 404, 404, 403, 403, 403, 400, 400, 400), 400)      def test_connection_refused(self):          self.app.account_ring.get_nodes('account') @@ -5616,7 +5989,7 @@ class TestAccountController(unittest.TestCase):                  req = Request.blank('/a/c', environ={'REQUEST_METHOD': method},                                      headers={test_header: test_value})                  self.app.update_request(req) -                res = getattr(controller, method)(req) +                getattr(controller, method)(req)                  self.assertEquals(test_errors, [])      def test_PUT_bad_metadata(self): @@ -5730,6 +6103,92 @@ class TestAccountController(unittest.TestCase):              test_status_map((201, 500, 500), 503)              test_status_map((204, 500, 404), 503) +    def test_DELETE_with_query_string(self): +        # Extra safety in case someone typos a query string for an +        # account-level DELETE request that was really meant to be caught by +        # some middleware. +        with save_globals(): +            controller = proxy_server.AccountController(self.app, 'account') + +            def test_status_map(statuses, expected, **kwargs): +                set_http_connect(*statuses, **kwargs) +                self.app.memcache.store = {} +                req = Request.blank('/a?whoops', {'REQUEST_METHOD': 'DELETE'}) +                req.content_length = 0 +                self.app.update_request(req) +                res = controller.DELETE(req) +                expected = str(expected) +                self.assertEquals(res.status[:len(expected)], expected) +            test_status_map((201, 201, 201), 400) +            self.app.allow_account_management = True +            test_status_map((201, 201, 201), 400) +            test_status_map((201, 201, 500), 400) +            test_status_map((201, 500, 500), 400) +            test_status_map((204, 500, 404), 400) + + +class TestAccountControllerFakeGetResponse(unittest.TestCase): +    """ +    Test all the faked-out GET responses for accounts that don't exist. They +    have to match the responses for empty accounts that really exist. +    """ +    def setUp(self): +        self.app = proxy_server.Application(None, FakeMemcache(), +                                            account_ring=FakeRing(), +                                            container_ring=FakeRing(), +                                            object_ring=FakeRing) +        self.app.memcache = FakeMemcacheReturnsNone() +        self.controller = proxy_server.AccountController(self.app, 'acc') +        self.controller.app.account_autocreate = True + +    def test_GET_autocreate_accept_json(self): +        with save_globals(): +            set_http_connect(404)  # however many backends we ask, they all 404 +            req = Request.blank('/a', headers={'Accept': 'application/json'}) + +            resp = self.controller.GET(req) +            self.assertEqual(200, resp.status_int) +            self.assertEqual('application/json; charset=utf-8', +                             resp.headers['Content-Type']) +            self.assertEqual("[]", resp.body) + +    def test_GET_autocreate_format_json(self): +        with save_globals(): +            set_http_connect(404)  # however many backends we ask, they all 404 +            req = Request.blank('/a?format=json') + +            resp = self.controller.GET(req) +            self.assertEqual(200, resp.status_int) +            self.assertEqual('application/json; charset=utf-8', +                             resp.headers['Content-Type']) +            self.assertEqual("[]", resp.body) + +    def test_GET_autocreate_accept_xml(self): +        with save_globals(): +            set_http_connect(404)  # however many backends we ask, they all 404 +            req = Request.blank('/a', headers={"Accept": "text/xml"}) + +            resp = self.controller.GET(req) +            self.assertEqual(200, resp.status_int) +            self.assertEqual('text/xml; charset=utf-8', +                             resp.headers['Content-Type']) +            empty_xml_listing = ('<?xml version="1.0" encoding="UTF-8"?>\n' +                                 '<account name="acc">\n</account>') +            self.assertEqual(empty_xml_listing, resp.body) + +    def test_GET_autocreate_format_xml(self): +        with save_globals(): +            set_http_connect(404)  # however many backends we ask, they all 404 +            req = Request.blank('/a?format=xml') + +            resp = self.controller.GET(req) +            self.assertEqual(200, resp.status_int) +            self.assertEqual('application/xml; charset=utf-8', +                             resp.headers['Content-Type']) +            empty_xml_listing = ('<?xml version="1.0" encoding="UTF-8"?>\n' +                                 '<account name="acc">\n</account>') +            self.assertEqual(empty_xml_listing, resp.body) +  class FakeObjectController(object): @@ -5744,13 +6203,14 @@ class FakeObjectController(object):          self.node_timeout = 1          self.rate_limit_after_segment = 3          self.rate_limit_segments_per_sec = 2 +        self.GETorHEAD_base_args = []      def exception(self, *args):          self.exception_args = args          self.exception_info = sys.exc_info()      def GETorHEAD_base(self, *args): -        self.GETorHEAD_base_args = args +        self.GETorHEAD_base_args.append(args)          req = args[0]          path = args[4]          body = data = path[-1] * int(path[-1]) @@ -5762,8 +6222,8 @@ class FakeObjectController(object):          resp = Response(app_iter=iter(body))          return resp -    def iter_nodes(self, partition, nodes, ring): -        for node in nodes: +    def iter_nodes(self, ring, partition): +        for node in ring.get_part_nodes(partition):              yield node          for node in ring.get_more_nodes(partition):              yield node @@ -5801,7 +6261,8 @@ class TestSegmentedIterable(unittest.TestCase):          segit = SegmentedIterable(self.controller, 'lc', [{'name':                                    'o1'}])          segit._load_next_segment() -        self.assertEquals(self.controller.GETorHEAD_base_args[4], '/a/lc/o1') +        self.assertEquals( +            self.controller.GETorHEAD_base_args[0][4], '/a/lc/o1')          data = ''.join(segit.segment_iter)          self.assertEquals(data, '1') @@ -5809,11 +6270,13 @@ class TestSegmentedIterable(unittest.TestCase):          segit = SegmentedIterable(self.controller, 'lc', [{'name':                                    'o1'}, {'name': 'o2'}])          segit._load_next_segment() -        self.assertEquals(self.controller.GETorHEAD_base_args[4], '/a/lc/o1') +        self.assertEquals( +            self.controller.GETorHEAD_base_args[-1][4], '/a/lc/o1')          data = ''.join(segit.segment_iter)          self.assertEquals(data, '1')          segit._load_next_segment() -        self.assertEquals(self.controller.GETorHEAD_base_args[4], '/a/lc/o2') +        self.assertEquals( +            self.controller.GETorHEAD_base_args[-1][4], '/a/lc/o2')          data = ''.join(segit.segment_iter)          self.assertEquals(data, '22') @@ -5835,46 +6298,109 @@ class TestSegmentedIterable(unittest.TestCase):              for _ in xrange(3):                  segit._load_next_segment()              self.assertEquals([], sleep_calls) -            self.assertEquals(self.controller.GETorHEAD_base_args[4], +            self.assertEquals(self.controller.GETorHEAD_base_args[-1][4],                                '/a/lc/o3')              # Loading of next (4th) segment starts rate-limiting.              segit._load_next_segment()              self.assertAlmostEqual(0.5, sleep_calls[0], places=2) -            self.assertEquals(self.controller.GETorHEAD_base_args[4], +            self.assertEquals(self.controller.GETorHEAD_base_args[-1][4],                                '/a/lc/o4')              sleep_calls = []              segit._load_next_segment()              self.assertAlmostEqual(0.5, sleep_calls[0], places=2) -            self.assertEquals(self.controller.GETorHEAD_base_args[4], +            self.assertEquals(self.controller.GETorHEAD_base_args[-1][4], +                              '/a/lc/o5') +        finally: +            swift.proxy.controllers.obj.sleep = orig_sleep + +    def test_load_next_segment_range_req_rate_limiting(self): +        sleep_calls = [] + +        def _stub_sleep(sleepy_time): +            sleep_calls.append(sleepy_time) +        orig_sleep = swift.proxy.controllers.obj.sleep +        try: +            swift.proxy.controllers.obj.sleep = _stub_sleep +            segit = SegmentedIterable( +                self.controller, 'lc', [ +                    {'name': 'o0', 'bytes': 5}, {'name': 'o1', 'bytes': 5}, +                    {'name': 'o2', 'bytes': 1}, {'name': 'o3'}, {'name': 'o4'}, +                    {'name': 'o5'}, {'name': 'o6'}]) + +            # this tests for a range request which skips over the whole first +            # segment, after that 3 segments will be read in because the +            # rate_limit_after_segment == 3, then sleeping starts +            segit_iter = segit.app_iter_range(10, None) +            segit_iter.next() +            for _ in xrange(2): +                # this is set to 2 instead of 3 because o2 was loaded after +                # o0 and o1 were skipped. +                segit._load_next_segment() +            self.assertEquals([], sleep_calls) +            self.assertEquals(self.controller.GETorHEAD_base_args[-1][4], +                              '/a/lc/o4') + +            # Loading of next (5th) segment starts rate-limiting. +            segit._load_next_segment() +            self.assertAlmostEqual(0.5, sleep_calls[0], places=2) +            self.assertEquals(self.controller.GETorHEAD_base_args[-1][4],                                '/a/lc/o5') + +            sleep_calls = [] +            segit._load_next_segment() +            self.assertAlmostEqual(0.5, sleep_calls[0], places=2) +            self.assertEquals(self.controller.GETorHEAD_base_args[-1][4], +                              '/a/lc/o6')          finally:              swift.proxy.controllers.obj.sleep = orig_sleep      def test_load_next_segment_with_two_segments_skip_first(self):          segit = SegmentedIterable(self.controller, 'lc', [{'name':                                    'o1'}, {'name': 'o2'}]) -        segit.segment = 0 +        segit.ratelimit_index = 0          segit.listing.next()          segit._load_next_segment() -        self.assertEquals(self.controller.GETorHEAD_base_args[4], '/a/lc/o2') +        self.assertEquals(self.controller.GETorHEAD_base_args[-1][4], '/a/lc/o2')          data = ''.join(segit.segment_iter)          self.assertEquals(data, '22')      def test_load_next_segment_with_seek(self): -        segit = SegmentedIterable(self.controller, 'lc', [{'name': -                                  'o1'}, {'name': 'o2'}]) -        segit.segment = 0 +        segit = SegmentedIterable(self.controller, 'lc', +                                  [{'name': 'o1', 'bytes': 1}, +                                   {'name': 'o2', 'bytes': 2}]) +        segit.ratelimit_index = 0          segit.listing.next()          segit.seek = 1          segit._load_next_segment() -        self.assertEquals(self.controller.GETorHEAD_base_args[4], '/a/lc/o2') -        self.assertEquals(str(self.controller.GETorHEAD_base_args[0].range), +        self.assertEquals(self.controller.GETorHEAD_base_args[-1][4], '/a/lc/o2') +        self.assertEquals(str(self.controller.GETorHEAD_base_args[-1][0].range),                            'bytes=1-')          data = ''.join(segit.segment_iter)          self.assertEquals(data, '2') +    def test_fetching_only_what_you_need(self): +        segit = SegmentedIterable(self.controller, 'lc', +                                  [{'name': 'o7', 'bytes': 7}, +                                   {'name': 'o8', 'bytes': 8}, +                                   {'name': 'o9', 'bytes': 9}]) + +        body = ''.join(segit.app_iter_range(10, 20)) +        self.assertEqual('8888899999', body) + +        GoH_args = self.controller.GETorHEAD_base_args +        self.assertEquals(2, len(GoH_args)) + +        # Either one is fine, as they both indicate "from byte 3 to (the last) +        # byte 8". +        self.assert_(str(GoH_args[0][0].range) in ['bytes=3-', 'bytes=3-8']) + +        # This one must ask only for the bytes it needs; otherwise we waste +        # bandwidth pulling bytes from the object server and then throwing +        # them out +        self.assertEquals(str(GoH_args[1][0].range), 'bytes=0-4') +      def test_load_next_segment_with_get_error(self):          def local_GETorHEAD_base(*args): @@ -10,7 +10,7 @@ setenv = VIRTUAL_ENV={envdir}           NOSE_OPENSTACK_SHOW_ELAPSED=1           NOSE_OPENSTACK_STDOUT=1  deps = -  https://launchpad.net/swift/grizzly/1.8.0/+download/swift-1.8.0.tar.gz +  https://launchpad.net/swift/havana/1.9.1/+download/swift-1.9.1.tar.gz    -r{toxinidir}/tools/test-requires  changedir = {toxinidir}/test/unit  commands = nosetests -v --exe --with-xunit --with-coverage --cover-package gluster --cover-erase --cover-xml --cover-html --cover-branches {posargs}  | 
