Beispiel #1
0
    def __init__(self,
                 root,
                 cache_dir,
                 options,
                 conn=None,
                 cache_limit=1024,
                 cache_nodes=False,
                 cache_max_flush_threads=10,
                 secure_get=False):
        """Initialize the VOFS.

        cache_limit is in MB.
        The style here is to use dictionaries to contain information
        about the Node.  The full VOSpace path is used as the Key for
        most of these dictionaries."""

        self.cache_nodes = cache_nodes

        # Standard attributes of the Node
        # Where in the file system this Node is currently located
        self.loading_dir = {}

        # Command line options.
        self.opt = options

        # What is the 'root' of the VOSpace? (eg vos:MyVOSpace)
        self.root = root

        # VOSpace is a bit slow so we do some caching.
        self.cache = Cache(cache_dir,
                           cache_limit,
                           False,
                           VOFS.cacheTimeout,
                           maxFlushThreads=cache_max_flush_threads)

        # All communication with the VOSpace goes through this client
        # connection.
        try:
            # MJG
            self.client = vos.Client(rootNode=root, conn=conn)
#            ,
#                                     cadc_short_cut=True, secure_get=secure_get)
        except Exception as e:
            e = FuseOSError(getattr(e, 'errno', EIO))
            e.filename = root
            e.strerror = getattr(e, 'strerror', 'failed while making mount')
            raise e

        # Create a condition variable to get rid of those nasty sleeps
        self.condition = CacheCondition(lock=None, timeout=VOFS.cacheTimeout)
Beispiel #2
0
    def __init__(self, root, cache_dir, options, conn=None,
                 cache_limit=1024, cache_nodes=False,
                 cache_max_flush_threads=10, secure_get=False):
        """Initialize the VOFS.

        cache_limit is in MB.
        The style here is to use dictionaries to contain information
        about the Node.  The full VOSpace path is used as the Key for
        most of these dictionaries."""

        self.cache_nodes = cache_nodes

        # Standard attributes of the Node
        # Where in the file system this Node is currently located
        self.loading_dir = {}

        # Command line options.
        self.opt = options

        # What is the 'root' of the VOSpace? (eg vos:MyVOSpace)
        self.root = root

        # VOSpace is a bit slow so we do some caching.
        self.cache = Cache(cache_dir, cache_limit, False, VOFS.cacheTimeout,
                           maxFlushThreads=cache_max_flush_threads)

        # All communication with the VOSpace goes through this client
        # connection.
        try:
            self.client = vos.Client(root_node=root, conn=conn,
                                     transfer_shortcut=True, secure_get=secure_get)
        except Exception as e:
            e = FuseOSError(getattr(e, 'errno', EIO))
            e.filename = root
            e.strerror = getattr(e, 'strerror', 'failed while making mount')
            raise e

        # Create a condition variable to get rid of those nasty sleeps
        self.condition = CacheCondition(lock=None, timeout=VOFS.cacheTimeout)
Beispiel #3
0
class VOFS(Operations):
    cacheTimeout = 30.0
    """
    The VOFS filesystem operations class.  Requires the vos (VOSpace)
    python package.

    To use this you will also need a VOSpace account from the CADC.
    """
    # disable operations VOSpace doesn't support
    chown = None
    link = None
    mknode = None
    symlink = None
    getxattr = None
    listxattr = None
    removexattr = None

    def setxattr(self, path, name, value, options, position=0):
        logger.warning("Extended attributes not supported: {} {} {} {} {}".format(path, name, value, options, position))

    def __init__(self, root, cache_dir, options, conn=None,
                 cache_limit=1024, cache_nodes=False,
                 cache_max_flush_threads=10, secure_get=False):
        """Initialize the VOFS.

        cache_limit is in MB.
        The style here is to use dictionaries to contain information
        about the Node.  The full VOSpace path is used as the Key for
        most of these dictionaries."""

        self.cache_nodes = cache_nodes

        # Standard attributes of the Node
        # Where in the file system this Node is currently located
        self.loading_dir = {}

        # Command line options.
        self.opt = options

        # What is the 'root' of the VOSpace? (eg vos:MyVOSpace)
        self.root = root

        # VOSpace is a bit slow so we do some caching.
        self.cache = Cache(cache_dir, cache_limit, False, VOFS.cacheTimeout,
                           maxFlushThreads=cache_max_flush_threads)

        # All communication with the VOSpace goes through this client
        # connection.
        try:
            self.client = vos.Client(root_node=root, conn=conn) # , MJG
#                                     transfer_shortcut=True, secure_get=secure_get)
        except Exception as e:
            e = FuseOSError(getattr(e, 'errno', EIO))
            e.filename = root
            e.strerror = getattr(e, 'strerror', 'failed while making mount')
            raise e

        # Create a condition variable to get rid of those nasty sleeps
        self.condition = CacheCondition(lock=None, timeout=VOFS.cacheTimeout)

    def __call__(self, op, *args):
        logger.debug('-> {0} {1}'.format(op, repr(args)))
        ret = None
        try:
            if not hasattr(self, op):
                raise FuseOSError(EFAULT)
            ret = getattr(self, op)(*args)
            return ret
        except Exception as all_exceptions:
            ret = str(all_exceptions)
            errno = getattr(all_exceptions, 'errno', None)
            errno = errno is not None and errno or EAGAIN
            exception = FuseOSError(errno)
            raise exception
        finally:
            logger.debug('<- {0} {1}'.format(op, repr(ret)))

    @logExceptions()
    def access(self, path, mode):
        """Check if path is accessible.

        Only checks read access, mode is currently ignored"""
        logger.debug("Checking if -->{0}<-- is accessible".format(path))
        try:
            self.getNode(path)
        except:
            return -1
        return 0

    @logExceptions()
    def chmod(self, path, mode):
        """
        Set the read/write groups on the VOSpace node based on chmod style
        modes.

        This function is a bit funny as the VOSpace spec sets the name
        of the read and write groups instead of having mode setting as
        a separate action.  A chmod that adds group permission thus
        becomes a chgrp action.

        Here I use the logic that the new group will be inherited from
        the container group information.
        """
        logger.debug("Changing mode for %s to %d" % (path, mode))

        node = self.getNode(path)
        parent = self.getNode(os.path.dirname(path))

        if node.groupread == "NONE":
            node.groupread = parent.groupread
        if node.groupwrite == "NONE":
            node.groupwrite = parent.groupwrite
        # The 'node' object returned by getNode has a chmod method
        # that we now call to set the mod, since we set the group(s)
        # above.  NOTE: If the parrent doesn't have group then NONE is
        # passed up and the groupwrite and groupread will be set tovos:
        # the string NONE.
        if node.chmod(mode):
            # Now set the time of change/modification on the path...
            # TODO: This has to be broken. Attributes may come from Cache if
            # the file is modified. Even if they don't come from the cache,
            # the getAttr method calls getNode with force=True, which returns
            # a different Node object than "node". The st_ctime value will be
            # updated on the newly replaced node in self.node[path] but
            # not in node, then node is pushed to vospace without the st_time
            # change, and then it is pulled back, overwriting the change that
            # was made in self.node[path]. Plus modifying the mtime of the file
            # is not conventional Unix behaviour. The mtime of the parent
            # directory would be changed.
            self.getattr(path)['st_ctime'] = time.time()
            # if node.chmod returns false then no changes were made.
            self.client.update(node)
            self.getNode(path, force=True)

    @logExceptions()
    def create(self, path, flags, fi=None):
        """Create a node. Currently ignores the ownership mode
        @param path: the container/dataNode in VOSpace to be created
        @param flags: Read/Write settings (eg. 600)
        """

        logger.debug("Creating a node: {0} with flags {1}".format(path, str(flags)))

        # Create is handle by the client.
        # This should fail if the base path doesn't exist
        self.client.open(path, os.O_CREAT).close()

        node = self.getNode(path)
        parent = self.getNode(os.path.dirname(path))
        print node, parent
        # Force inheritance of group settings.
        node.groupread = parent.groupread
        node.groupwrite = parent.groupwrite
        if node.chmod(flags):
            # chmod returns True if the mode changed but doesn't do update.
            self.client.update(node)
            self.getNode(path, force=True)

        # now we can just open the file in the usual way and return the handle
        return self.open(path, os.O_WRONLY)

    def destroy(self, path):
        """Called on filesystem destruction. Path is always /

           Call the flushNodeQueue join() method which will block
           until any running and/or queued jobs are finished"""

        if self.cache.flushNodeQueue is None:
            raise CacheError("flushNodeQueue has not been initialized")
        self.cache.flushNodeQueue.join()
        self.cache.flushNodeQueue = None

    @logExceptions()
    def fsync(self, path, data_sync, file_id):
        if self.opt.readonly:
            logger.debug("File system is readonly, no sync allowed")
            return

        try:
            fh = HandleWrapper.file_handle(file_id)
        except KeyError:
            raise FuseOSError(EIO)

        if fh.read_only:
            raise FuseOSError(EPERM)

        fh.cache_file_handle.fsync()

    def get_node(self, path, force=False, limit=0):
        """Use the client and pull the node from VOSpace.

        Currently force=False is the default... so we never check
        VOSpace to see if the node metadata is different from what we
        have.  This doesn't keep the node metadata current but is
        faster if VOSpace is slow.
        @type limit: int or None
        @rtype : vos.Node
        @param path: the VOSpace node to get
        @param force: force retrieval (true) or provide cached version if available (false)?
        @param limit: Number of child nodes to retrieve per request, if limit  is None then get max returned by service.
        """

        # Pull the node meta data from VOSpace.
        logger.debug("requesting node {0} from VOSpace. Force: {1}".format(path, force))
        node = self.client.get_node(path, force=force, limit=limit)
        logger.debug("Got node {0}".format(node))
        return node

    getNode = get_node

    # @logExceptions()
    def getattr(self, path, file_id=None):
        """
        Build some attributes for this file, we have to make-up some stuff
        """
        # Try to get the attributes from the cache first. This will only return
        # a result if the files has been modified and not flushed to vospace.
        attr = self.cache.getAttr(path)
        return attr is not None and attr or self.get_node(path, limit=0, force=False).attr

    def init(self, path):
        """Called on filesystem initialization. (Path is always /)

        Here is where we start the worker threads for the queue that flushes nodes.
        """
        self.cache.flushNodeQueue = FlushNodeQueue(maxFlushThreads=self.cache.maxFlushThreads)

    # @logExceptions()
    def mkdir(self, path, mode):
        """Create a container node in the VOSpace at the correct location.

        set the mode after creation. """
        try:
            self.client.mkdir(path)
        except OSError as os_error:
            if "read-only mode" in str(os_error):
                raise FuseOSError(EPERM)
            raise FuseOSError(getattr(os_error, 'errno', EFAULT))
        # self.chmod(path, mode)

    # @logExceptions()
    def open(self, path, flags, *mode):
        """Open file with the desired modes

        Here we return a handle to the cached version of the file
        which is updated if older and out of sync with VOSpace.

        """

        logger.debug("Opening %s with flags %s" % (path, flag2mode(flags)))
        node = None

        # according to man for open(2), flags must contain one of O_RDWR,
        # O_WRONLY or O_RDONLY. Because O_RDONLY=0 and options other than
        # O_RDWR, O_WRONLY and O_RDONLY may be present,
        # readonly = (flags == O_RDONLY) and readonly = (flags | O_RDONLY)
        # won't work. The only way to detect if it's a read only is to check
        # whether the other two flags are absent.
        read_only = ((flags & (os.O_RDWR | os.O_WRONLY)) == 0)

        must_exist = not ((flags & os.O_CREAT) == os.O_CREAT)
        cache_file_attrs = self.cache.getAttr(path)
        if cache_file_attrs is None and not read_only:
            # file in the cache not in the process of being modified.
            # see if this node already exists in the cache; if not get info
            # from vospace
            try:
                node = self.get_node(path)
            except OSError as e:
                if e.errno == 404:
                    # file does not exist
                    if not flags & os.O_CREAT:
                        # file doesn't exist unless created
                        raise FuseOSError(ENOENT)
                else:
                    raise FuseOSError(e.errno)

        # check if this file is locked, if locked on vospace then don't open
        locked = False

        if node and node.props.get('islocked', False):
            logger.debug("%s is locked." % path)
            locked = True

        if not read_only and node and not locked:
            if node.type == "vos:DataNode":
                parent_node = self.get_node(os.path.dirname(path), force=False, limit=1)
                if parent_node.props.get('islocked', False):
                    logger.debug("%s is locked by parent node." % path)
                    locked = True
            elif node.type == "vos:LinkNode":
                try:
                    # sometimes target_nodes aren't internal... so then not
                    # locked
                    target_node = self.get_node(node.target, force=False, limit=1)
                    if target_node.props.get('islocked', False):
                        logger.debug("{0} target node is locked.".format(path))
                        locked = True
                    else:
                        target_parent_node = self.get_node(os.path.dirname(node.target), force=False, limit=1)
                        if target_parent_node.props.get('islocked', False):
                            logger.debug("{0} parent node is locked.".format(path))
                            locked = True
                except Exception as lock_exception:
                    logger.warn("Error while checking for lock: {0}".format(str(lock_exception)))
                    pass

        if locked and not read_only:
            # file is locked, cannot write
            e = OSError(EPERM)
            e.filename = path
            e.strerror = "Cannot write to locked file"
            logger.debug("{0}".format(e))
            raise e

        my_proxy = MyIOProxy(self, path)
        if node is not None:
            my_proxy.set_size(int(node.props.get('length')))
            my_proxy.set_md5(node.props.get('MD5'))

        logger.debug("IO Proxy initialized:{0}  in backing.".format(my_proxy))

        # new file in cache library or if no node information (node not in vospace).
        handle = self.cache.open(path, flags & os.O_WRONLY != 0, must_exist, my_proxy, self.cache_nodes)

        logger.debug("Creating file:{0}  in backing.".format(path))

        if flags & os.O_TRUNC != 0:
            handle.truncate(0)
        if node is not None:
            handle.setHeader(my_proxy.getSize(), my_proxy.get_md5())
        return HandleWrapper(handle, read_only).get_id()

    @logExceptions()
    def read(self, path, size=0, offset=0, file_id=None):
        """
        Read the required bytes from the file and return a buffer containing
        the bytes.
        """

        # Read from the requested file_handle, which was set during 'open'
        if file_id is None:
            raise FuseOSError(EIO)

        logger.debug("reading range: %s %d %d %d" % (path, size, offset, file_id))

        try:
            fh = HandleWrapper.file_handle(file_id)
        except KeyError:
            raise FuseOSError(EIO)

        return fh.cache_file_handle.read(size, offset)

    @logExceptions()
    def readlink(self, path):
        """
        Return a string representing the path to which the symbolic link
        points.

        path: filesystem location that is a link

        returns the file that path is a link to.

        Currently doesn't provide correct capabilty for VOSpace FS.
        """
        return self.get_node(path).name+"?link="+urllib.quote_plus(self.getNode(path).target)

    @logExceptions()
    def readdir(self, path, file_id):
        """Send a list of entries in this directory"""
        logger.debug("Getting directory list for {0}".format(path))
        # reading from VOSpace can be slow, we'll do this in a thread
        import thread
        with self.condition:
            if not self.loading_dir.get(path, False):
                self.loading_dir[path] = True
                thread.start_new_thread(self.load_dir, (path, ))

            while self.loading_dir.get(path, False):
                logger.debug("Waiting ... ")
                self.condition.wait()
        return ['.', '..'] + [e.name.encode('utf-8') for e in self.getNode(path,
                                                                           force=False,
                                                                           limit=None).node_list]

    @logExceptions()
    def load_dir(self, path):
        """Load the dirlist from VOSpace.

        This should always be run in a thread."""
        try:
            logger.debug("Starting getNodeList thread")
            node_list = self.getNode(path, force=True, limit=None).node_list
            logger.debug("Got listing {0} for {1}".format(node_list, path))
        finally:
            self.loading_dir[path] = False
            with self.condition:
                self.condition.notify_all()
        return

    @logExceptions()
    def flush(self, path, file_id):

        logger.debug("flushing {0}".format(file_id))
        fh = HandleWrapper.file_handle(file_id)

        try:
            fh.cache_file_handle.flush()
        except CacheRetry as ce:
            logger.critical(str(ce))
            logger.critical("Push to VOSpace reached FUSE timeout, continuing VOSpace push in background.")
            pass
        return 0

    @logExceptions()
    def release(self, path, file_id):
        """Close the file.

        @param path: vospace path to close reference to
        @param file_id: file_handle_id to close reference to
        """
        logger.debug("releasing file %d " % file_id)
        fh = HandleWrapper.file_handle(file_id)
        fh.cache_file_handle.release()
        if fh.cache_file_handle.fileModified:
            # This makes the node disappear from the nodeCache.
            with self.client.nodeCache.volatile(path):
                pass
        return fh.release()

    @logExceptions()
    def rename(self, src, dest):
        """Rename a data node into a new container"""
        logger.debug("Original %s -> %s" % (src, dest))
        try:
            logger.debug("Moving %s to %s" % (src, dest))
            result = self.client.move(src, dest)
            logger.debug(str(result))
            if result:
                self.cache.renameFile(src, dest)
                return 0
            return -1
        except Exception, e:
            logger.error("%s" % str(e))
            import re
            if re.search('NodeLocked', str(e)) is not None:
                raise OSError(EPERM)
            raise
Beispiel #4
0
class VOFS(Operations):
    cacheTimeout = 30.0
    """
    The VOFS filesystem operations class.  Requires the vos (VOSpace)
    python package.

    To use this you will also need a VOSpace account from the CADC.
    """
    # disable operations VOSpace doesn't support
    chown = None
    link = None
    mknode = None
    symlink = None
    getxattr = None
    listxattr = None
    removexattr = None

    def setxattr(self, path, name, value, options, position=0):
        logger.warning("Extended attributes not supported: {0} {1} {2} {3} {4}".format(path, name, value, options, position))

    def __init__(self, root, cache_dir, options, conn=None,
                 cache_limit=1024, cache_nodes=False,
                 cache_max_flush_threads=10, secure_get=False):
        """Initialize the VOFS.

        cache_limit is in MB.
        The style here is to use dictionaries to contain information
        about the Node.  The full VOSpace path is used as the Key for
        most of these dictionaries."""

        self.cache_nodes = cache_nodes

        # Standard attributes of the Node
        # Where in the file system this Node is currently located
        self.loading_dir = {}

        # Command line options.
        self.opt = options

        # What is the 'root' of the VOSpace? (eg vos:MyVOSpace)
        self.root = root

        # VOSpace is a bit slow so we do some caching.
        self.cache = Cache(cache_dir, cache_limit, False, VOFS.cacheTimeout,
                           maxFlushThreads=cache_max_flush_threads)

        # All communication with the VOSpace goes through this client
        # connection.
        try:
            self.client = vos.Client(root_node=root, conn=conn,
                                     transfer_shortcut=True, secure_get=secure_get)
        except Exception as e:
            e = FuseOSError(getattr(e, 'errno', EIO))
            e.filename = root
            e.strerror = getattr(e, 'strerror', 'failed while making mount')
            raise e

        # Create a condition variable to get rid of those nasty sleeps
        self.condition = CacheCondition(lock=None, timeout=VOFS.cacheTimeout)

    def __call__(self, op, *args):
        logger.debug('-> {0} {1}'.format(op, repr(args)))
        ret = None
        try:
            if not hasattr(self, op):
                raise FuseOSError(EFAULT)
            ret = getattr(self, op)(*args)
            return ret
        except Exception as all_exceptions:
            ret = str(all_exceptions)
            errno = getattr(all_exceptions, 'errno', None)
            errno = errno is not None and errno or EAGAIN
            exception = FuseOSError(errno)
            raise exception
        finally:
            logger.debug('<- {0} {1}'.format(op, repr(ret)))

    @logExceptions()
    def access(self, path, mode):
        """Check if path is accessible.

        Only checks read access, mode is currently ignored"""
        logger.debug("Checking if -->{0}<-- is accessible".format(path))
        try:
            self.getNode(path)
        except:
            return -1
        return 0

    @logExceptions()
    def chmod(self, path, mode):
        """
        Set the read/write groups on the VOSpace node based on chmod style
        modes.

        This function is a bit funny as the VOSpace spec sets the name
        of the read and write groups instead of having mode setting as
        a separate action.  A chmod that adds group permission thus
        becomes a chgrp action.

        Here I use the logic that the new group will be inherited from
        the container group information.
        """
        logger.debug("Changing mode for %s to %d" % (path, mode))

        node = self.getNode(path)
        parent = self.getNode(os.path.dirname(path))

        if node.groupread == "NONE":
            node.groupread = parent.groupread
        if node.groupwrite == "NONE":
            node.groupwrite = parent.groupwrite
        # The 'node' object returned by getNode has a chmod method
        # that we now call to set the mod, since we set the group(s)
        # above.  NOTE: If the parrent doesn't have group then NONE is
        # passed up and the groupwrite and groupread will be set tovos:
        # the string NONE.
        if node.chmod(mode):
            # Now set the time of change/modification on the path...
            # TODO: This has to be broken. Attributes may come from Cache if
            # the file is modified. Even if they don't come from the cache,
            # the getAttr method calls getNode with force=True, which returns
            # a different Node object than "node". The st_ctime value will be
            # updated on the newly replaced node in self.node[path] but
            # not in node, then node is pushed to vospace without the st_time
            # change, and then it is pulled back, overwriting the change that
            # was made in self.node[path]. Plus modifying the mtime of the file
            # is not conventional Unix behaviour. The mtime of the parent
            # directory would be changed.
            self.getattr(path)['st_ctime'] = time.time()
            # if node.chmod returns false then no changes were made.
            self.client.update(node)
            self.getNode(path, force=True)

    @logExceptions()
    def create(self, path, flags, fi=None):
        """Create a node. Currently ignores the ownership mode
        @param path: the container/dataNode in VOSpace to be created
        @param flags: Read/Write settings (eg. 600)
        """

        logger.debug("Creating a node: {0} with flags {1}".format(path, str(flags)))

        # Create is handle by the client.
        # This should fail if the base path doesn't exist
        self.client.open(path, os.O_CREAT).close()

        node = self.getNode(path)
        parent = self.getNode(os.path.dirname(path))
        print node, parent
        # Force inheritance of group settings.
        node.groupread = parent.groupread
        node.groupwrite = parent.groupwrite
        if node.chmod(flags):
            # chmod returns True if the mode changed but doesn't do update.
            self.client.update(node)
            self.getNode(path, force=True)

        # now we can just open the file in the usual way and return the handle
        return self.open(path, os.O_WRONLY)

    def destroy(self, path):
        """Called on filesystem destruction. Path is always /

           Call the flushNodeQueue join() method which will block
           until any running and/or queued jobs are finished"""

        if self.cache.flushNodeQueue is None:
            raise CacheError("flushNodeQueue has not been initialized")
        self.cache.flushNodeQueue.join()
        self.cache.flushNodeQueue = None

    @logExceptions()
    def fsync(self, path, data_sync, file_id):
        if self.opt.readonly:
            logger.debug("File system is readonly, no sync allowed")
            return

        try:
            fh = HandleWrapper.file_handle(file_id)
        except KeyError:
            raise FuseOSError(EIO)

        if fh.read_only:
            raise FuseOSError(EPERM)

        fh.cache_file_handle.fsync()

    def get_node(self, path, force=False, limit=0):
        """Use the client and pull the node from VOSpace.

        Currently force=False is the default... so we never check
        VOSpace to see if the node metadata is different from what we
        have.  This doesn't keep the node metadata current but is
        faster if VOSpace is slow.
        @type limit: int or None
        @rtype : vos.Node
        @param path: the VOSpace node to get
        @param force: force retrieval (true) or provide cached version if available (false)?
        @param limit: Number of child nodes to retrieve per request, if limit  is None then get max returned by service.
        """

        # Pull the node meta data from VOSpace.
        logger.debug("requesting node {0} from VOSpace. Force: {1}".format(path, force))
        node = self.client.get_node(path, force=force, limit=limit)
        logger.debug("Got node {0}".format(node))
        return node

    getNode = get_node

    # @logExceptions()
    def getattr(self, path, file_id=None):
        """
        Build some attributes for this file, we have to make-up some stuff
        """
        # Try to get the attributes from the cache first. This will only return
        # a result if the files has been modified and not flushed to vospace.
        attr = self.cache.getAttr(path)
        return attr is not None and attr or self.get_node(path, limit=0, force=False).attr

    def init(self, path):
        """Called on filesystem initialization. (Path is always /)

        Here is where we start the worker threads for the queue that flushes nodes.
        """
        self.cache.flushNodeQueue = FlushNodeQueue(maxFlushThreads=self.cache.maxFlushThreads)

    # @logExceptions()
    def mkdir(self, path, mode):
        """Create a container node in the VOSpace at the correct location.

        set the mode after creation. """
        try:
            self.client.mkdir(path)
        except OSError as os_error:
            if "read-only mode" in str(os_error):
                raise FuseOSError(EPERM)
            raise FuseOSError(getattr(os_error, 'errno', EFAULT))
        # self.chmod(path, mode)

    # @logExceptions()
    def open(self, path, flags, *mode):
        """Open file with the desired modes

        Here we return a handle to the cached version of the file
        which is updated if older and out of sync with VOSpace.

        """

        logger.debug("Opening %s with flags %s" % (path, flag2mode(flags)))
        node = None

        # according to man for open(2), flags must contain one of O_RDWR,
        # O_WRONLY or O_RDONLY. Because O_RDONLY=0 and options other than
        # O_RDWR, O_WRONLY and O_RDONLY may be present,
        # readonly = (flags == O_RDONLY) and readonly = (flags | O_RDONLY)
        # won't work. The only way to detect if it's a read only is to check
        # whether the other two flags are absent.
        read_only = ((flags & (os.O_RDWR | os.O_WRONLY)) == 0)

        must_exist = not ((flags & os.O_CREAT) == os.O_CREAT)
        cache_file_attrs = self.cache.getAttr(path)
        if cache_file_attrs is None and not read_only:
            # file in the cache not in the process of being modified.
            # see if this node already exists in the cache; if not get info
            # from vospace
            try:
                node = self.get_node(path)
            except OSError as e:
                if e.errno == 404:
                    # file does not exist
                    if not flags & os.O_CREAT:
                        # file doesn't exist unless created
                        raise FuseOSError(ENOENT)
                else:
                    raise FuseOSError(e.errno)

        # check if this file is locked, if locked on vospace then don't open
        locked = False

        if node and node.props.get('islocked', False):
            logger.debug("%s is locked." % path)
            locked = True

        if not read_only and node and not locked:
            if node.type == "vos:DataNode":
                parent_node = self.get_node(os.path.dirname(path), force=False, limit=1)
                if parent_node.props.get('islocked', False):
                    logger.debug("%s is locked by parent node." % path)
                    locked = True
            elif node.type == "vos:LinkNode":
                try:
                    # sometimes target_nodes aren't internal... so then not
                    # locked
                    target_node = self.get_node(node.target, force=False, limit=1)
                    if target_node.props.get('islocked', False):
                        logger.debug("{0} target node is locked.".format(path))
                        locked = True
                    else:
                        target_parent_node = self.get_node(os.path.dirname(node.target), force=False, limit=1)
                        if target_parent_node.props.get('islocked', False):
                            logger.debug("{0} parent node is locked.".format(path))
                            locked = True
                except Exception as lock_exception:
                    logger.warn("Error while checking for lock: {0}".format(str(lock_exception)))
                    pass

        if locked and not read_only:
            # file is locked, cannot write
            e = OSError(EPERM)
            e.filename = path
            e.strerror = "Cannot write to locked file"
            logger.debug("{0}".format(e))
            raise e

        my_proxy = MyIOProxy(self, path)
        if node is not None:
            my_proxy.set_size(int(node.props.get('length')))
            my_proxy.set_md5(node.props.get('MD5'))

        logger.debug("IO Proxy initialized:{0}  in backing.".format(my_proxy))

        # new file in cache library or if no node information (node not in vospace).
        handle = self.cache.open(path, flags & os.O_WRONLY != 0, must_exist, my_proxy, self.cache_nodes)

        logger.debug("Creating file:{0}  in backing.".format(path))

        if flags & os.O_TRUNC != 0:
            handle.truncate(0)
        if node is not None:
            handle.setHeader(my_proxy.getSize(), my_proxy.get_md5())
        return HandleWrapper(handle, read_only).get_id()

    @logExceptions()
    def read(self, path, size=0, offset=0, file_id=None):
        """
        Read the required bytes from the file and return a buffer containing
        the bytes.
        """

        # Read from the requested file_handle, which was set during 'open'
        if file_id is None:
            raise FuseOSError(EIO)

        logger.debug("reading range: %s %d %d %d" % (path, size, offset, file_id))

        try:
            fh = HandleWrapper.file_handle(file_id)
        except KeyError:
            raise FuseOSError(EIO)

        return fh.cache_file_handle.read(size, offset)

    @logExceptions()
    def readlink(self, path):
        """
        Return a string representing the path to which the symbolic link
        points.

        path: filesystem location that is a link

        returns the file that path is a link to.

        Currently doesn't provide correct capabilty for VOSpace FS.
        """
        return self.get_node(path).name+"?link="+urllib.quote_plus(self.getNode(path).target)

    @logExceptions()
    def readdir(self, path, file_id):
        """Send a list of entries in this directory"""
        logger.debug("Getting directory list for {0}".format(path))
        # reading from VOSpace can be slow, we'll do this in a thread
        import thread
        with self.condition:
            if not self.loading_dir.get(path, False):
                self.loading_dir[path] = True
                thread.start_new_thread(self.load_dir, (path, ))

            while self.loading_dir.get(path, False):
                logger.debug("Waiting ... ")
                self.condition.wait()
        return ['.', '..'] + [e.name.encode('utf-8') for e in self.getNode(path,
                                                                           force=False,
                                                                           limit=None).node_list]

    @logExceptions()
    def load_dir(self, path):
        """Load the dirlist from VOSpace.

        This should always be run in a thread."""
        try:
            logger.debug("Starting getNodeList thread")
            node_list = self.getNode(path, force=True, limit=None).node_list
            logger.debug("Got listing {0} for {1}".format(node_list, path))
        finally:
            self.loading_dir[path] = False
            with self.condition:
                self.condition.notify_all()
        return

    @logExceptions()
    def flush(self, path, file_id):

        logger.debug("flushing {0}".format(file_id))
        fh = HandleWrapper.file_handle(file_id)

        try:
            fh.cache_file_handle.flush()
        except CacheRetry as ce:
            logger.critical(str(ce))
            logger.critical("Push to VOSpace reached FUSE timeout, continuing VOSpace push in background.")
            pass
        return 0

    @logExceptions()
    def release(self, path, file_id):
        """Close the file.

        @param path: vospace path to close reference to
        @param file_id: file_handle_id to close reference to
        """
        logger.debug("releasing file %d " % file_id)
        fh = HandleWrapper.file_handle(file_id)
        fh.cache_file_handle.release()
        if fh.cache_file_handle.fileModified:
            # This makes the node disappear from the nodeCache.
            with self.client.nodeCache.volatile(path):
                pass
        return fh.release()

    @logExceptions()
    def rename(self, src, dest):
        """Rename a data node into a new container"""
        logger.debug("Original %s -> %s" % (src, dest))
        try:
            logger.debug("Moving %s to %s" % (src, dest))
            result = self.client.move(src, dest)
            logger.debug(str(result))
            if result:
                self.cache.renameFile(src, dest)
                return 0
            return -1
        except Exception, e:
            logger.error("%s" % str(e))
            import re
            if re.search('NodeLocked', str(e)) is not None:
                raise OSError(EPERM)
            raise
Beispiel #5
0
class VOFS(LoggingMixIn, Operations):
    cacheTimeout = 60
    """
    The VOFS filesystem opperations class.  Requires the vos (VOSpace)
    python package.

    To use this you will also need a VOSpace account from the CADC.
    """
    ### VOSpace doesn't support these so I've disabled these operations.
    chown = None
    link = None
    mknode = None
    rmdir = None
    symlink = None
    getxattr = None
    listxattr = None
    removexattr = None
    setxattr = None

    def __init__(self,
                 root,
                 cache_dir,
                 options,
                 conn=None,
                 cache_limit=1024,
                 cache_nodes=False,
                 cache_max_flush_threads=10,
                 secure_get=False):
        """Initialize the VOFS.

        cache_limit is in MB.
        The style here is to use dictionaries to contain information
        about the Node.  The full VOSpace path is used as the Key for
        most of these dictionaries."""

        self.cache_nodes = cache_nodes

        # Standard attribtutes of the Node
        # Where in the file system this Node is currently located
        self.loading_dir = {}

        # Command line options.
        self.opt = options

        # What is the 'root' of the VOSpace? (eg vos:MyVOSpace)
        self.root = root

        # VOSpace is a bit slow so we do some caching.
        self.cache = Cache(cache_dir, cache_limit, False, VOFS.cacheTimeout, \
                               maxFlushThreads=cache_max_flush_threads)

        # All communication with the VOSpace goes through this client
        # connection.
        try:
            self.client = vos.Client(rootNode=root, conn=conn)
        except Exception as e:
            e = FuseOSError(getattr(e, 'errno', EIO))
            e.filename = root
            e.strerror = getattr(e, 'strerror', 'failed while making mount')
            raise e

        # Create a condition variable to get rid of those nasty sleeps
        self.condition = CacheCondition(lock=None, timeout=VOFS.cacheTimeout)

    def __call__(self, op, path, *args):
        return super(VOFS, self).__call__(op, path, *args)

    #@logExceptions()
    def access(self, path, mode):
        """Check if path is accessible.

        Only checks read access, mode is currently ignored"""
        vos.logger.debug("Checking if -->%s<-- is accessible" % (path))
        try:
            self.getNode(path)
        except:
            return -1
        return 0

    #@logExceptions()
    def chmod(self, path, mode):
        """
        Set the read/write groups on the VOSpace node based on chmod style
        modes.

        This function is a bit funny as the VOSpace spec sets the name
        of the read and write groups instead of having mode setting as
        a separate action.  A chmod that adds group permission thus
        becomes a chgrp action.

        Here I use the logic that the new group will be inherited from
        the container group information.
        """
        vos.logger.debug("Changing mode for %s to %d" % (path, mode))

        node = self.getNode(path)
        parent = self.getNode(os.path.dirname(path))

        if node.groupread == "NONE":
            node.groupread = parent.groupread
        if node.groupwrite == "NONE":
            node.groupwrite = parent.groupwrite
        # The 'node' object returned by getNode has a chmod method
        # that we now call to set the mod, since we set the group(s)
        # above.  NOTE: If the parrent doesn't have group then NONE is
        # passed up and the groupwrite and groupread will be set tovos:
        # the string NONE.
        if node.chmod(mode):
            # Now set the time of change/modification on the path...
            # TODO: This has to be broken. Attributes may come from Cache if
            # the file is modified. Even if they don't come from the cache,
            # the getAttr method calls getNode with force=True, which returns
            # a different Node object than "node". The st_ctime value will be
            # updated on the newly replaced node in self.node[path] but
            # not in node, then node is pushed to vospace without the st_time
            # change, and then it is pulled back, overwriting the change that
            # was made in self.node[path]. Plus modifying the mtime of the file
            # is not conventional Unix behaviour. The mtime of the parent
            # directory would be changed.
            self.getattr(path)['st_ctime'] = time.time()
            ## if node.chmod returns false then no changes were made.
            try:
                self.client.update(node)
                self.getNode(path, force=True)
                # MJG: Any changes only currently come into effect when the
                # next mount happens
                vos.logger.debug("%s %s" %
                                 (mode, self.getattr(path)['st_mode']))
            except Exception as e:
                vos.logger.debug(str(e))
                vos.logger.debug(type(e))
                e = FuseOSError(getattr(e, 'errno', EIO))
                e.filename = path
                e.strerror = getattr(e, 'strerror',
                                     'failed to chmod on %s' % (path))
                raise e

    #@logExceptions()
    def create(self, path, flags):
        """Create a node. Currently ignores the ownership mode"""

        vos.logger.debug("Creating a node: %s with flags %s" %
                         (path, str(flags)))

        # Create is handle by the client.
        # This should fail if the basepath doesn't exist
        try:
            self.client.open(path, os.O_CREAT).close()

            node = self.getNode(path)
            parent = self.getNode(os.path.dirname(path))

            # Force inheritance of group settings.
            node.groupread = parent.groupread
            node.groupwrite = parent.groupwrite
            if node.chmod(flags):
                # chmod returns True if the mode changed but doesn't do update.
                self.client.update(node)
                node = self.getNode(path, force=True)

        except Exception as e:
            vos.logger.error(str(e))
            vos.logger.error("Error trying to create Node %s" % (path))
            f = FuseOSError(getattr(e, 'errno', EIO))
            f.strerror = getattr(e, 'strerror', 'failed to create %s' % (path))
            raise f

        ## now we can just open the file in the usual way and return the handle
        return self.open(path, os.O_WRONLY)

    def destroy(self, path):
        """Called on filesystem destruction. Path is always /

           Call the flushNodeQueue join() method which will block
           until any running and/or queued jobs are finished"""

        if self.cache.flushNodeQueue is None:
            raise CacheError("flushNodeQueue has not been initialized")
        self.cache.flushNodeQueue.join()
        self.cache.flushNodeQueue = None

    #@logExceptions()
    def fsync(self, path, datasync, id):
        if self.opt.readonly:
            vos.logger.debug("File system is readonly, no sync allowed")
            return

        try:
            fh = HandleWrapper.findHandle(id)
        except KeyError:
            raise FuseOSError(EIO)

        if fh.readOnly:
            raise FuseOSError(EPERM)

        fh.cacheFileHandle.fsync()

    def getNode(self, path, force=False, limit=0):
        """Use the client and pull the node from VOSpace.

        Currently force=False is the default... so we never check
        VOSpace to see if the node metadata is different from what we
        have.  This doesn't keep the node metadata current but is
        faster if VOSpace is slow.
        """

        vos.logger.debug("force? -> %s path -> %s" % (force, path))

        ## Pull the node meta data from VOSpace.
        try:
            vos.logger.debug("requesting node %s from VOSpace" % (path))
            node = self.client.getNode(path, force=force, limit=limit)
        except Exception as e:
            vos.logger.debug(str(e))
            vos.logger.debug(type(e))
            ex = FuseOSError(getattr(e, 'errno', ENOENT))
            ex.filename = path
            ex.strerror = getattr(e, 'strerror', 'Error getting %s' % (path))
            vos.logger.debug("failing with errno = %d" % ex.errno)
            raise ex

        return node

    #@logExceptions()
    def getattr(self, path, id=None):
        """
        Build some attributes for this file, we have to make-up some stuff
        """

        vos.logger.debug("getting attributes of %s" % (path))
        # Try to get the attributes from the cache first. This will only return
        # a result if the files has been modified and not flushed to vospace.
        cacheFileAttrs = self.cache.getAttr(path)
        if cacheFileAttrs is not None:
            return cacheFileAttrs

        return self.getNode(path, limit=0, force=False).attr

    def init(self, path):
        """Called on filesystem initialization. (Path is always /)

           Here is where we start the worker threads for the queue
           that flushes nodes."""

        self.cache.flushNodeQueue = \
            FlushNodeQueue(maxFlushThreads=self.cache.maxFlushThreads)

    #@logExceptions()
    def mkdir(self, path, mode):
        """Create a container node in the VOSpace at the correct location.

        set the mode after creation. """
        #try:
        if 1 == 1:
            parentNode = self.getNode(os.path.dirname(path),
                                      force=False,
                                      limit=1)
            if parentNode and parentNode.props.get('islocked', False):
                vos.logger.debug("Parent node of %s is locked." % path)
                raise FuseOSError(EPERM)
            self.client.mkdir(path)
            self.chmod(path, mode)
        #except Exception as e:
        #   vos.logger.error(str(e))
        #   ex=FuseOSError(e.errno)
        #   ex.filename=path
        #   ex.strerror=e.strerror
        #   raise ex
        return

    #@logExceptions()
    def open(self, path, flags, *mode):
        """Open file with the desired modes

        Here we return a handle to the cached version of the file
        which is updated if older and out of synce with VOSpace.

        """

        vos.logger.debug("Opening %s with flags %s" % (path, flag2mode(flags)))
        node = None

        # according to man for open(2), flags must contain one of O_RDWR,
        # O_WRONLY or O_RDONLY. Because O_RDONLY=0 and options other than
        # O_RDWR, O_WRONLY and O_RDONLY may be present,
        # readonly = (flags == O_RDONLY) and readonly = (flags | # O_RDONLY)
        # won't work. The only way to detect if it's a read only is to check
        # whether the other two flags are absent.
        readOnly = ((flags & (os.O_RDWR | os.O_WRONLY)) == 0)

        mustExist = not ((flags & os.O_CREAT) == os.O_CREAT)
        cacheFileAttrs = self.cache.getAttr(path)
        if cacheFileAttrs is None and not readOnly:
            # file in the cache not in the process of being modified.
            # see if this node already exists in the cache; if not get info
            # from vospace
            try:
                node = self.getNode(path)
            except IOError as e:
                if e.errno == 404:
                    # file does not exist
                    if not flags & os.O_CREAT:
                        # file doesn't exist unless created
                        raise FuseOSError(ENOENT)
                else:
                    raise FuseOSError(e.errno)

        ### check if this file is locked, if locked on vospace then don't open
        locked = False

        if node and node.props.get('islocked', False):
            vos.logger.debug("%s is locked." % path)
            locked = True

        if not readOnly and node and not locked:
            if node.type == "vos:DataNode":
                parentNode = self.getNode(os.path.dirname(path),
                                          force=False,
                                          limit=1)
                if parentNode.props.get('islocked', False):
                    vos.logger.debug("%s is locked by parent node." % path)
                    locked = True
            elif node.type == "vos:LinkNode":
                try:
                    # sometimes targetNodes aren't internal... so then not
                    # locked
                    targetNode = self.getNode(node.target,
                                              force=False,
                                              limit=1)
                    if targetNode.props.get('islocked', False):
                        vos.logger.debug("%s target node is locked." % path)
                        locked = True
                    else:
                        targetParentNode = self.getNode(os.path.dirname(
                            node.target),
                                                        force=False,
                                                        limit=1)
                        if targetParentNode.props.get('islocked', False):
                            vos.logger.debug(
                                "%s is locked by target parent node." % path)
                            locked = True
                except Exception as e:
                    vos.logger.error("Got an error while checking for lock: " +
                                     str(e))
                    pass

        if locked and not readOnly:
            # file is locked, cannot write
            e = FuseOSError(ENOENT)
            e.strerror = "Cannot create locked file"
            vos.logger.debug("Cannot create locked file: %s", path)
            raise e

        myProxy = MyIOProxy(self, path)
        if node is not None:
            myProxy.setSize(int(node.props.get('length')))
            myProxy.setMD5(node.props.get('MD5'))

        # new file in cache library or if no node information (node not in
        # vospace).
        handle = self.cache.open(path, flags & os.O_WRONLY != 0, mustExist,
                                 myProxy, self.cache_nodes)
        if flags & os.O_TRUNC != 0:
            handle.truncate(0)
        if node is not None:
            handle.setHeader(myProxy.getSize(), myProxy.getMD5())
        return HandleWrapper(handle, readOnly).getId()

    #@logExceptions()
    def read(self, path, size=0, offset=0, id=None):
        """
        Read the required bytes from the file and return a buffer containing
        the bytes.
        """

        ## Read from the requested filehandle, which was set during 'open'
        if id is None:
            raise FuseOSError(EIO)

        vos.logger.debug("reading range: %s %d %d %d" %
                         (path, size, offset, id))

        try:
            fh = HandleWrapper.findHandle(id)
        except KeyError:
            raise FuseOSError(EIO)

        try:
            return fh.cacheFileHandle.read(size, offset)
        except CacheRetry:
            e = FuseOSError(EAGAIN)
            e.strerror = "Timeout waiting for file read"
            vos.logger.debug("Timeout Waiting for file read: %s", path)
            raise e

    #@logExceptions()
    def readlink(self, path):
        """
        Return a string representing the path to which the symbolic link
        points.

        path: filesystem location that is a link

        returns the file that path is a link to.

        Currently doesn't provide correct capabilty for VOSpace FS.
        """
        return self.getNode(path).name + "?link=" + urllib.quote_plus(
            self.getNode(path).target)

    #@logExceptions()
    def readdir(self, path, id):
        """Send a list of entries in this directory"""
        vos.logger.debug("Getting directory list for %s " % (path))
        ## reading from VOSpace can be slow, we'll do this in a thread
        import thread
        with self.condition:
            if not self.loading_dir.get(path, False):
                self.loading_dir[path] = True
                thread.start_new_thread(self.load_dir, (path, ))

            while self.loading_dir.get(path, False):
                vos.logger.debug("Waiting ... ")
                try:
                    self.condition.wait()
                except CacheRetry:
                    e = FuseOSError(EAGAIN)
                    e.strerror = "Timeout waiting for directory listing"
                    vos.logger.debug("Timeout Waiting for directory read: %s",
                                     path)
                    raise e
        return ['.', '..'] + [
            e.name.encode('utf-8')
            for e in self.getNode(path, force=False, limit=None).getNodeList()
        ]

    #@logExceptions()
    def load_dir(self, path):
        """Load the dirlist from VOSpace.
        This should always be run in a thread."""
        try:
            vos.logger.debug("Starting getNodeList thread")
            self.getNode(path, force=True, limit=None).getNodeList()
            vos.logger.debug("Got listing for %s" % (path))
        finally:
            self.loading_dir[path] = False
            with self.condition:
                self.condition.notify_all()
        return

    #@logExceptions()
    def release(self, path, id):
        """Close the file"""
        vos.logger.debug("releasing file %d " % id)
        try:
            fh = HandleWrapper.findHandle(id)
        except KeyError:
            raise FuseOSError(EIO)

        try:
            while True:
                try:
                    fh.cacheFileHandle.release()
                    break
                except CacheRetry:
                    vos.logger.debug("Timeout Waiting for file release: %s",
                                     fh.cacheFileHandle.path)
            if fh.cacheFileHandle.fileModified:
                # This makes the node disapear from the nodeCache.
                with self.client.nodeCache.volatile(path):
                    pass
            fh.release()
        except Exception, e:
            #unexpected problem
            raise FuseOSError(EIO)
        return
Beispiel #6
0
class VOFS(LoggingMixIn, Operations):
    cacheTimeout = 60
    """
    The VOFS filesystem opperations class.  Requires the vos (VOSpace)
    python package.

    To use this you will also need a VOSpace account from the CADC.
    """
    ### VOSpace doesn't support these so I've disabled these operations.
    chown = None
    link = None
    mknode = None
    rmdir = None
    symlink = None
    getxattr = None
    listxattr = None
    removexattr = None
    setxattr = None

    def __init__(self, root, cache_dir, options, conn=None,
                 cache_limit=1024, cache_nodes=False,
                 cache_max_flush_threads=10, secure_get=False):
        """Initialize the VOFS.

        cache_limit is in MB.
        The style here is to use dictionaries to contain information
        about the Node.  The full VOSpace path is used as the Key for
        most of these dictionaries."""

        self.cache_nodes = cache_nodes

        # Standard attribtutes of the Node
        # Where in the file system this Node is currently located
        self.loading_dir = {}

        # Command line options.
        self.opt = options

        # What is the 'root' of the VOSpace? (eg vos:MyVOSpace)
        self.root = root

        # VOSpace is a bit slow so we do some caching.
        self.cache = Cache(cache_dir, cache_limit, False, VOFS.cacheTimeout, \
                               maxFlushThreads=cache_max_flush_threads)

        # All communication with the VOSpace goes through this client
        # connection.
        try:
            self.client = vos.Client(rootNode=root, conn=conn,
                    cadc_short_cut=True, secure_get=secure_get)
        except Exception as e:
            e = FuseOSError(getattr(e, 'errno', EIO))
            e.filename = root
            e.strerror = getattr(e, 'strerror', 'failed while making mount')
            raise e

        # Create a condition variable to get rid of those nasty sleeps
        self.condition = CacheCondition(lock=None, timeout=VOFS.cacheTimeout)

    def __call__(self, op, path, *args):
        return super(VOFS, self).__call__(op, path, *args)

    #@logExceptions()
    def access(self, path, mode):
        """Check if path is accessible.

        Only checks read access, mode is currently ignored"""
        vos.logger.debug("Checking if -->%s<-- is accessible" % (path))
        try:
            self.getNode(path)
        except:
            return -1
        return 0

    #@logExceptions()
    def chmod(self, path, mode):
        """
        Set the read/write groups on the VOSpace node based on chmod style
        modes.

        This function is a bit funny as the VOSpace spec sets the name
        of the read and write groups instead of having mode setting as
        a separate action.  A chmod that adds group permission thus
        becomes a chgrp action.

        Here I use the logic that the new group will be inherited from
        the container group information.
        """
        vos.logger.debug("Changing mode for %s to %d" % (path, mode))

        node = self.getNode(path)
        parent = self.getNode(os.path.dirname(path))

        if node.groupread == "NONE":
            node.groupread = parent.groupread
        if node.groupwrite == "NONE":
            node.groupwrite = parent.groupwrite
        # The 'node' object returned by getNode has a chmod method
        # that we now call to set the mod, since we set the group(s)
        # above.  NOTE: If the parrent doesn't have group then NONE is
        # passed up and the groupwrite and groupread will be set tovos:
        # the string NONE.
        if node.chmod(mode):
            # Now set the time of change/modification on the path...
            # TODO: This has to be broken. Attributes may come from Cache if
            # the file is modified. Even if they don't come from the cache,
            # the getAttr method calls getNode with force=True, which returns
            # a different Node object than "node". The st_ctime value will be
            # updated on the newly replaced node in self.node[path] but
            # not in node, then node is pushed to vospace without the st_time
            # change, and then it is pulled back, overwriting the change that
            # was made in self.node[path]. Plus modifying the mtime of the file
            # is not conventional Unix behaviour. The mtime of the parent
            # directory would be changed.
            self.getattr(path)['st_ctime'] = time.time()
            ## if node.chmod returns false then no changes were made.
            try:
                self.client.update(node)
                self.getNode(path, force=True)
            except Exception as e:
                vos.logger.debug(str(e))
                vos.logger.debug(type(e))
                e = FuseOSError(getattr(e, 'errno', EIO))
                e.filename = path
                e.strerror = getattr(e, 'strerror', 'failed to chmod on %s' %
                        (path))
                raise e

    #@logExceptions()
    def create(self, path, flags):
        """Create a node. Currently ignores the ownership mode"""

        vos.logger.debug("Creating a node: %s with flags %s" %
                (path, str(flags)))

        # Create is handle by the client.
        # This should fail if the basepath doesn't exist
        try:
            self.client.open(path, os.O_CREAT).close()

            node = self.getNode(path)
            parent = self.getNode(os.path.dirname(path))

            # Force inheritance of group settings.
            node.groupread = parent.groupread
            node.groupwrite = parent.groupwrite
            if node.chmod(flags):
                # chmod returns True if the mode changed but doesn't do update.
                self.client.update(node)
                node = self.getNode(path, force=True)

        except Exception as e:
            vos.logger.error(str(e))
            vos.logger.error("Error trying to create Node %s" % (path))
            f = FuseOSError(getattr(e, 'errno', EIO))
            f.strerror = getattr(e, 'strerror', 'failed to create %s' % (path))
            raise f

        ## now we can just open the file in the usual way and return the handle
        return self.open(path, os.O_WRONLY)

    def destroy(self, path):
        """Called on filesystem destruction. Path is always /

           Call the flushNodeQueue join() method which will block
           until any running and/or queued jobs are finished"""

        if self.cache.flushNodeQueue is None:
            raise CacheError("flushNodeQueue has not been initialized")
        self.cache.flushNodeQueue.join()
        self.cache.flushNodeQueue = None

    #@logExceptions()
    def fsync(self, path, datasync, id):
        if self.opt.readonly:
            vos.logger.debug("File system is readonly, no sync allowed")
            return

        try:
            fh = HandleWrapper.findHandle(id)
        except KeyError:
            raise FuseOSError(EIO)

        if fh.readOnly:
            raise FuseOSError(EPERM)

        fh.cacheFileHandle.fsync()

    def getNode(self, path, force=False, limit=0):
        """Use the client and pull the node from VOSpace.

        Currently force=False is the default... so we never check
        VOSpace to see if the node metadata is different from what we
        have.  This doesn't keep the node metadata current but is
        faster if VOSpace is slow.
        """

        vos.logger.debug("force? -> %s path -> %s" % (force, path))

        ## Pull the node meta data from VOSpace.
        try:
            vos.logger.debug("requesting node %s from VOSpace" % (path))
            node = self.client.getNode(path, force=force, limit=limit)
        except Exception as e:
            vos.logger.debug(str(e))
            vos.logger.debug(type(e))
            ex = FuseOSError(getattr(e, 'errno', ENOENT))
            ex.filename = path
            ex.strerror = getattr(e, 'strerror', 'Error getting %s' % (path))
            vos.logger.debug("failing with errno = %d" % ex.errno)
            raise ex

        return node

    #@logExceptions()
    def getattr(self, path, id=None):
        """
        Build some attributes for this file, we have to make-up some stuff
        """

        vos.logger.debug("getting attributes of %s" % (path))
        # Try to get the attributes from the cache first. This will only return
        # a result if the files has been modified and not flushed to vospace.
        cacheFileAttrs = self.cache.getAttr(path)
        if cacheFileAttrs is not None:
            return cacheFileAttrs

        return self.getNode(path, limit=0, force=False).attr

    def init(self, path):
        """Called on filesystem initialization. (Path is always /)

           Here is where we start the worker threads for the queue
           that flushes nodes."""

        self.cache.flushNodeQueue = \
            FlushNodeQueue(maxFlushThreads=self.cache.maxFlushThreads)

    #@logExceptions()
    def mkdir(self, path, mode):
        """Create a container node in the VOSpace at the correct location.

        set the mode after creation. """
        #try:
        if 1 == 1:
            parentNode = self.getNode(os.path.dirname(path), force=False,
                    limit=1)
            if parentNode and parentNode.props.get('islocked', False):
                vos.logger.debug("Parent node of %s is locked." % path)
                raise FuseOSError(EPERM)
            self.client.mkdir(path)
            self.chmod(path, mode)
        #except Exception as e:
        #   vos.logger.error(str(e))
        #   ex=FuseOSError(e.errno)
        #   ex.filename=path
        #   ex.strerror=e.strerror
        #   raise ex
        return

    #@logExceptions()
    def open(self, path, flags, *mode):
        """Open file with the desired modes

        Here we return a handle to the cached version of the file
        which is updated if older and out of synce with VOSpace.

        """

        vos.logger.debug("Opening %s with flags %s" % (path, flag2mode(flags)))
        node = None

        # according to man for open(2), flags must contain one of O_RDWR,
        # O_WRONLY or O_RDONLY. Because O_RDONLY=0 and options other than
        # O_RDWR, O_WRONLY and O_RDONLY may be present,
        # readonly = (flags == O_RDONLY) and readonly = (flags | # O_RDONLY)
        # won't work. The only way to detect if it's a read only is to check
        # whether the other two flags are absent.
        readOnly = ((flags & (os.O_RDWR | os.O_WRONLY)) == 0)

        mustExist = not ((flags & os.O_CREAT) == os.O_CREAT)
        cacheFileAttrs = self.cache.getAttr(path)
        if cacheFileAttrs is None and not readOnly:
            # file in the cache not in the process of being modified.
            # see if this node already exists in the cache; if not get info
            # from vospace
            try:
                node = self.getNode(path)
            except IOError as e:
                if e.errno == 404:
                    # file does not exist
                    if not flags & os.O_CREAT:
                        # file doesn't exist unless created
                        raise FuseOSError(ENOENT)
                else:
                    raise FuseOSError(e.errno)

        ### check if this file is locked, if locked on vospace then don't open
        locked = False

        if node and node.props.get('islocked', False):
            vos.logger.debug("%s is locked." % path)
            locked = True

        if not readOnly and node and not locked:
            if node.type == "vos:DataNode":
                parentNode = self.getNode(os.path.dirname(path),
                        force=False, limit=1)
                if parentNode.props.get('islocked', False):
                    vos.logger.debug("%s is locked by parent node." % path)
                    locked = True
            elif node.type == "vos:LinkNode":
                try:
                    # sometimes targetNodes aren't internal... so then not
                    # locked
                    targetNode = self.getNode(node.target, force=False,
                            limit=1)
                    if targetNode.props.get('islocked', False):
                        vos.logger.debug("%s target node is locked." % path)
                        locked = True
                    else:
                        targetParentNode = self.getNode(os.path.dirname(
                                node.target), force=False, limit=1)
                        if targetParentNode.props.get('islocked', False):
                            vos.logger.debug(
                                    "%s is locked by target parent node." %
                                    path)
                            locked = True
                except Exception as e:
                    vos.logger.error("Got an error while checking for lock: " +
                            str(e))
                    pass

        if locked and not readOnly:
            # file is locked, cannot write
            e = FuseOSError(ENOENT)
            e.strerror = "Cannot create locked file"
            vos.logger.debug("Cannot create locked file: %s", path)
            raise e

        myProxy = MyIOProxy(self, path)
        if node is not None:
            myProxy.setSize(int(node.props.get('length')))
            myProxy.setMD5(node.props.get('MD5'))

        # new file in cache library or if no node information (node not in
        # vospace).
        handle = self.cache.open(path, flags & os.O_WRONLY != 0, mustExist,
                myProxy, self.cache_nodes)
        if flags & os.O_TRUNC != 0:
            handle.truncate(0)
        if node is not None:
            handle.setHeader(myProxy.getSize(), myProxy.getMD5())
        return HandleWrapper(handle, readOnly).getId()

    #@logExceptions()
    def read(self, path, size=0, offset=0, id=None):
        """
        Read the required bytes from the file and return a buffer containing
        the bytes.
        """

        ## Read from the requested filehandle, which was set during 'open'
        if id is None:
            raise FuseOSError(EIO)

        vos.logger.debug("reading range: %s %d %d %d" %
                (path, size, offset, id))

        try:
            fh = HandleWrapper.findHandle(id)
        except KeyError:
            raise FuseOSError(EIO)

        try:
            return fh.cacheFileHandle.read(size, offset)
        except CacheRetry:
            e = FuseOSError(EAGAIN)
            e.strerror = "Timeout waiting for file read"
            vos.logger.debug("Timeout Waiting for file read: %s", path)
            raise e

    #@logExceptions()
    def readlink(self, path):
        """
        Return a string representing the path to which the symbolic link
        points.

        path: filesystem location that is a link

        returns the file that path is a link to.

        Currently doesn't provide correct capabilty for VOSpace FS.
        """
        return self.getNode(path).name+"?link="+urllib.quote_plus(self.getNode(path).target)

    #@logExceptions()
    def readdir(self, path, id):
        """Send a list of entries in this directory"""
        vos.logger.debug("Getting directory list for %s " % (path))
        ## reading from VOSpace can be slow, we'll do this in a thread
        import thread
        with self.condition:
            if not self.loading_dir.get(path, False):
                self.loading_dir[path] = True
                thread.start_new_thread(self.load_dir, (path, ))

            while self.loading_dir.get(path, False):
                vos.logger.debug("Waiting ... ")
                try:
                    self.condition.wait()
                except CacheRetry:
                    e = FuseOSError(EAGAIN)
                    e.strerror = "Timeout waiting for directory listing"
                    vos.logger.debug("Timeout Waiting for directory read: %s",
                            path)
                    raise e
        return ['.', '..'] + [e.name.encode('utf-8') for e in self.getNode(
                path, force=False, limit=None).getNodeList()]

    #@logExceptions()
    def load_dir(self, path):
        """Load the dirlist from VOSpace.
        This should always be run in a thread."""
        try:
            vos.logger.debug("Starting getNodeList thread")
            self.getNode(path, force=True,
                    limit=None).getNodeList()
            vos.logger.debug("Got listing for %s" % (path))
        finally:
            self.loading_dir[path] = False
            with self.condition:
                self.condition.notify_all()
        return

    #@logExceptions()
    def release(self, path, id):
        """Close the file"""
        vos.logger.debug("releasing file %d " % id)
        try:
            fh = HandleWrapper.findHandle(id)
        except KeyError:
            raise FuseOSError(EIO)

        try:
            while True:
                try:
                    fh.cacheFileHandle.release()
                    break
                except CacheRetry:
                    vos.logger.debug("Timeout Waiting for file release: %s",
                            fh.cacheFileHandle.path)
            if fh.cacheFileHandle.fileModified:
                # This makes the node disapear from the nodeCache.
                with self.client.nodeCache.volatile(path):
                    pass
            fh.release()
        except Exception, e:
            #unexpected problem
            raise FuseOSError(EIO)
        return