Merge branch 'master' into 11898-no-distinct
[arvados.git] / services / fuse / arvados_fuse / __init__.py
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 """FUSE driver for Arvados Keep
6
7 Architecture:
8
9 There is one `Operations` object per mount point.  It is the entry point for all
10 read and write requests from the llfuse module.
11
12 The operations object owns an `Inodes` object.  The inodes object stores the
13 mapping from numeric inode (used throughout the file system API to uniquely
14 identify files) to the Python objects that implement files and directories.
15
16 The `Inodes` object owns an `InodeCache` object.  The inode cache records the
17 memory footprint of file system objects and when they are last used.  When the
18 cache limit is exceeded, the least recently used objects are cleared.
19
20 File system objects inherit from `fresh.FreshBase` which manages the object lifecycle.
21
22 File objects inherit from `fusefile.File`.  Key methods are `readfrom` and `writeto`
23 which implement actual reads and writes.
24
25 Directory objects inherit from `fusedir.Directory`.  The directory object wraps
26 a Python dict which stores the mapping from filenames to directory entries.
27 Directory contents can be accessed through the Python operators such as `[]`
28 and `in`.  These methods automatically check if the directory is fresh (up to
29 date) or stale (needs update) and will call `update` if necessary before
30 returing a result.
31
32 The general FUSE operation flow is as follows:
33
34 - The request handler is called with either an inode or file handle that is the
35   subject of the operation.
36
37 - Look up the inode using the Inodes table or the file handle in the
38   filehandles table to get the file system object.
39
40 - For methods that alter files or directories, check that the operation is
41   valid and permitted using _check_writable().
42
43 - Call the relevant method on the file system object.
44
45 - Return the result.
46
47 The FUSE driver supports the Arvados event bus.  When an event is received for
48 an object that is live in the inode cache, that object is immediately updated.
49
50 """
51
52 import os
53 import sys
54 import llfuse
55 import errno
56 import stat
57 import threading
58 import arvados
59 import pprint
60 import arvados.events
61 import re
62 import apiclient
63 import json
64 import logging
65 import time
66 import _strptime
67 import calendar
68 import threading
69 import itertools
70 import ciso8601
71 import collections
72 import functools
73 import arvados.keep
74
75 import Queue
76
77 # Default _notify_queue has a limit of 1000 items, but it really needs to be
78 # unlimited to avoid deadlocks, see https://arvados.org/issues/3198#note-43 for
79 # details.
80
81 if hasattr(llfuse, 'capi'):
82     # llfuse < 0.42
83     llfuse.capi._notify_queue = Queue.Queue()
84 else:
85     # llfuse >= 0.42
86     llfuse._notify_queue = Queue.Queue()
87
88 LLFUSE_VERSION_0 = llfuse.__version__.startswith('0')
89
90 from fusedir import sanitize_filename, Directory, CollectionDirectory, TmpCollectionDirectory, MagicDirectory, TagsDirectory, ProjectDirectory, SharedDirectory, CollectionDirectoryBase
91 from fusefile import StringFile, FuseArvadosFile
92
93 _logger = logging.getLogger('arvados.arvados_fuse')
94
95 # Uncomment this to enable llfuse debug logging.
96 # log_handler = logging.StreamHandler()
97 # llogger = logging.getLogger('llfuse')
98 # llogger.addHandler(log_handler)
99 # llogger.setLevel(logging.DEBUG)
100
101 class Handle(object):
102     """Connects a numeric file handle to a File or Directory object that has
103     been opened by the client."""
104
105     def __init__(self, fh, obj):
106         self.fh = fh
107         self.obj = obj
108         self.obj.inc_use()
109
110     def release(self):
111         self.obj.dec_use()
112
113     def flush(self):
114         pass
115
116
117 class FileHandle(Handle):
118     """Connects a numeric file handle to a File  object that has
119     been opened by the client."""
120
121     def flush(self):
122         if self.obj.writable():
123             return self.obj.flush()
124
125
126 class DirectoryHandle(Handle):
127     """Connects a numeric file handle to a Directory object that has
128     been opened by the client."""
129
130     def __init__(self, fh, dirobj, entries):
131         super(DirectoryHandle, self).__init__(fh, dirobj)
132         self.entries = entries
133
134
135 class InodeCache(object):
136     """Records the memory footprint of objects and when they are last used.
137
138     When the cache limit is exceeded, the least recently used objects are
139     cleared.  Clearing the object means discarding its contents to release
140     memory.  The next time the object is accessed, it must be re-fetched from
141     the server.  Note that the inode cache limit is a soft limit; the cache
142     limit may be exceeded if necessary to load very large objects, it may also
143     be exceeded if open file handles prevent objects from being cleared.
144
145     """
146
147     def __init__(self, cap, min_entries=4):
148         self._entries = collections.OrderedDict()
149         self._by_uuid = {}
150         self.cap = cap
151         self._total = 0
152         self.min_entries = min_entries
153
154     def total(self):
155         return self._total
156
157     def _remove(self, obj, clear):
158         if clear:
159             if obj.in_use():
160                 _logger.debug("InodeCache cannot clear inode %i, in use", obj.inode)
161                 return
162             if obj.has_ref(True):
163                 obj.kernel_invalidate()
164                 _logger.debug("InodeCache sent kernel invalidate inode %i", obj.inode)
165                 return
166             obj.clear()
167
168         # The llfuse lock is released in del_entry(), which is called by
169         # Directory.clear().  While the llfuse lock is released, it can happen
170         # that a reentrant call removes this entry before this call gets to it.
171         # Ensure that the entry is still valid before trying to remove it.
172         if obj.inode not in self._entries:
173             return
174
175         self._total -= obj.cache_size
176         del self._entries[obj.inode]
177         if obj.cache_uuid:
178             self._by_uuid[obj.cache_uuid].remove(obj)
179             if not self._by_uuid[obj.cache_uuid]:
180                 del self._by_uuid[obj.cache_uuid]
181             obj.cache_uuid = None
182         if clear:
183             _logger.debug("InodeCache cleared inode %i total now %i", obj.inode, self._total)
184
185     def cap_cache(self):
186         if self._total > self.cap:
187             for ent in self._entries.values():
188                 if self._total < self.cap or len(self._entries) < self.min_entries:
189                     break
190                 self._remove(ent, True)
191
192     def manage(self, obj):
193         if obj.persisted():
194             obj.cache_size = obj.objsize()
195             self._entries[obj.inode] = obj
196             obj.cache_uuid = obj.uuid()
197             if obj.cache_uuid:
198                 if obj.cache_uuid not in self._by_uuid:
199                     self._by_uuid[obj.cache_uuid] = [obj]
200                 else:
201                     if obj not in self._by_uuid[obj.cache_uuid]:
202                         self._by_uuid[obj.cache_uuid].append(obj)
203             self._total += obj.objsize()
204             _logger.debug("InodeCache touched inode %i (size %i) (uuid %s) total now %i", obj.inode, obj.objsize(), obj.cache_uuid, self._total)
205             self.cap_cache()
206
207     def touch(self, obj):
208         if obj.persisted():
209             if obj.inode in self._entries:
210                 self._remove(obj, False)
211             self.manage(obj)
212
213     def unmanage(self, obj):
214         if obj.persisted() and obj.inode in self._entries:
215             self._remove(obj, True)
216
217     def find_by_uuid(self, uuid):
218         return self._by_uuid.get(uuid, [])
219
220     def clear(self):
221         self._entries.clear()
222         self._by_uuid.clear()
223         self._total = 0
224
225 class Inodes(object):
226     """Manage the set of inodes.  This is the mapping from a numeric id
227     to a concrete File or Directory object"""
228
229     def __init__(self, inode_cache, encoding="utf-8"):
230         self._entries = {}
231         self._counter = itertools.count(llfuse.ROOT_INODE)
232         self.inode_cache = inode_cache
233         self.encoding = encoding
234         self.deferred_invalidations = []
235
236     def __getitem__(self, item):
237         return self._entries[item]
238
239     def __setitem__(self, key, item):
240         self._entries[key] = item
241
242     def __iter__(self):
243         return self._entries.iterkeys()
244
245     def items(self):
246         return self._entries.items()
247
248     def __contains__(self, k):
249         return k in self._entries
250
251     def touch(self, entry):
252         entry._atime = time.time()
253         self.inode_cache.touch(entry)
254
255     def add_entry(self, entry):
256         entry.inode = next(self._counter)
257         if entry.inode == llfuse.ROOT_INODE:
258             entry.inc_ref()
259         self._entries[entry.inode] = entry
260         self.inode_cache.manage(entry)
261         return entry
262
263     def del_entry(self, entry):
264         if entry.ref_count == 0:
265             self.inode_cache.unmanage(entry)
266             del self._entries[entry.inode]
267             with llfuse.lock_released:
268                 entry.finalize()
269             self.invalidate_inode(entry.inode)
270             entry.inode = None
271         else:
272             entry.dead = True
273             _logger.debug("del_entry on inode %i with refcount %i", entry.inode, entry.ref_count)
274
275     def invalidate_inode(self, inode):
276         llfuse.invalidate_inode(inode)
277
278     def invalidate_entry(self, inode, name):
279         llfuse.invalidate_entry(inode, name.encode(self.encoding))
280
281     def clear(self):
282         self.inode_cache.clear()
283
284         for k,v in self._entries.items():
285             try:
286                 v.finalize()
287             except Exception as e:
288                 _logger.exception("Error during finalize of inode %i", k)
289
290         self._entries.clear()
291
292
293 def catch_exceptions(orig_func):
294     """Catch uncaught exceptions and log them consistently."""
295
296     @functools.wraps(orig_func)
297     def catch_exceptions_wrapper(self, *args, **kwargs):
298         try:
299             return orig_func(self, *args, **kwargs)
300         except llfuse.FUSEError:
301             raise
302         except EnvironmentError as e:
303             raise llfuse.FUSEError(e.errno)
304         except arvados.errors.KeepWriteError as e:
305             _logger.error("Keep write error: " + str(e))
306             raise llfuse.FUSEError(errno.EIO)
307         except arvados.errors.NotFoundError as e:
308             _logger.error("Block not found error: " + str(e))
309             raise llfuse.FUSEError(errno.EIO)
310         except:
311             _logger.exception("Unhandled exception during FUSE operation")
312             raise llfuse.FUSEError(errno.EIO)
313
314     return catch_exceptions_wrapper
315
316
317 class Operations(llfuse.Operations):
318     """This is the main interface with llfuse.
319
320     The methods on this object are called by llfuse threads to service FUSE
321     events to query and read from the file system.
322
323     llfuse has its own global lock which is acquired before calling a request handler,
324     so request handlers do not run concurrently unless the lock is explicitly released
325     using 'with llfuse.lock_released:'
326
327     """
328
329     def __init__(self, uid, gid, api_client, encoding="utf-8", inode_cache=None, num_retries=4, enable_write=False):
330         super(Operations, self).__init__()
331
332         self._api_client = api_client
333
334         if not inode_cache:
335             inode_cache = InodeCache(cap=256*1024*1024)
336         self.inodes = Inodes(inode_cache, encoding=encoding)
337         self.uid = uid
338         self.gid = gid
339         self.enable_write = enable_write
340
341         # dict of inode to filehandle
342         self._filehandles = {}
343         self._filehandles_counter = itertools.count(0)
344
345         # Other threads that need to wait until the fuse driver
346         # is fully initialized should wait() on this event object.
347         self.initlock = threading.Event()
348
349         # If we get overlapping shutdown events (e.g., fusermount -u
350         # -z and operations.destroy()) llfuse calls forget() on inodes
351         # that have already been deleted. To avoid this, we make
352         # forget() a no-op if called after destroy().
353         self._shutdown_started = threading.Event()
354
355         self.num_retries = num_retries
356
357         self.read_counter = arvados.keep.Counter()
358         self.write_counter = arvados.keep.Counter()
359         self.read_ops_counter = arvados.keep.Counter()
360         self.write_ops_counter = arvados.keep.Counter()
361
362         self.events = None
363
364     def init(self):
365         # Allow threads that are waiting for the driver to be finished
366         # initializing to continue
367         self.initlock.set()
368
369     @catch_exceptions
370     def destroy(self):
371         self._shutdown_started.set()
372         if self.events:
373             self.events.close()
374             self.events = None
375
376         # Different versions of llfuse require and forbid us to
377         # acquire the lock here. See #8345#note-37, #10805#note-9.
378         if LLFUSE_VERSION_0 and llfuse.lock.acquire():
379             # llfuse < 0.42
380             self.inodes.clear()
381             llfuse.lock.release()
382         else:
383             # llfuse >= 0.42
384             self.inodes.clear()
385
386     def access(self, inode, mode, ctx):
387         return True
388
389     def listen_for_events(self):
390         self.events = arvados.events.subscribe(
391             self._api_client,
392             [["event_type", "in", ["create", "update", "delete"]]],
393             self.on_event)
394
395     @catch_exceptions
396     def on_event(self, ev):
397         if 'event_type' not in ev or ev["event_type"] not in ("create", "update", "delete"):
398             return
399         with llfuse.lock:
400             properties = ev.get("properties") or {}
401             old_attrs = properties.get("old_attributes") or {}
402             new_attrs = properties.get("new_attributes") or {}
403
404             for item in self.inodes.inode_cache.find_by_uuid(ev["object_uuid"]):
405                 item.invalidate()
406                 if ev.get("object_kind") == "arvados#collection":
407                     pdh = new_attrs.get("portable_data_hash")
408                     # new_attributes.modified_at currently lacks
409                     # subsecond precision (see #6347) so use event_at
410                     # which should always be the same.
411                     stamp = ev.get("event_at")
412                     if (stamp and pdh and item.writable() and
413                         item.collection is not None and
414                         item.collection.modified() and
415                         new_attrs.get("is_trashed") is not True):
416                         item.update(to_record_version=(stamp, pdh))
417
418             oldowner = old_attrs.get("owner_uuid")
419             newowner = ev.get("object_owner_uuid")
420             for parent in (
421                     self.inodes.inode_cache.find_by_uuid(oldowner) +
422                     self.inodes.inode_cache.find_by_uuid(newowner)):
423                 parent.child_event(ev)
424
425     @catch_exceptions
426     def getattr(self, inode, ctx=None):
427         if inode not in self.inodes:
428             raise llfuse.FUSEError(errno.ENOENT)
429
430         e = self.inodes[inode]
431
432         entry = llfuse.EntryAttributes()
433         entry.st_ino = inode
434         entry.generation = 0
435         entry.entry_timeout = 60 if e.allow_dirent_cache else 0
436         entry.attr_timeout = 60 if e.allow_attr_cache else 0
437
438         entry.st_mode = stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH
439         if isinstance(e, Directory):
440             entry.st_mode |= stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH | stat.S_IFDIR
441         else:
442             entry.st_mode |= stat.S_IFREG
443             if isinstance(e, FuseArvadosFile):
444                 entry.st_mode |= stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
445
446         if self.enable_write and e.writable():
447             entry.st_mode |= stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH
448
449         entry.st_nlink = 1
450         entry.st_uid = self.uid
451         entry.st_gid = self.gid
452         entry.st_rdev = 0
453
454         entry.st_size = e.size()
455
456         entry.st_blksize = 512
457         entry.st_blocks = (entry.st_size/512)+1
458         if hasattr(entry, 'st_atime_ns'):
459             # llfuse >= 0.42
460             entry.st_atime_ns = int(e.atime() * 1000000000)
461             entry.st_mtime_ns = int(e.mtime() * 1000000000)
462             entry.st_ctime_ns = int(e.mtime() * 1000000000)
463         else:
464             # llfuse < 0.42
465             entry.st_atime = int(e.atime)
466             entry.st_mtime = int(e.mtime)
467             entry.st_ctime = int(e.mtime)
468
469         return entry
470
471     @catch_exceptions
472     def setattr(self, inode, attr, fields=None, fh=None, ctx=None):
473         entry = self.getattr(inode)
474
475         if fh is not None and fh in self._filehandles:
476             handle = self._filehandles[fh]
477             e = handle.obj
478         else:
479             e = self.inodes[inode]
480
481         if fields is None:
482             # llfuse < 0.42
483             update_size = attr.st_size is not None
484         else:
485             # llfuse >= 0.42
486             update_size = fields.update_size
487         if update_size and isinstance(e, FuseArvadosFile):
488             with llfuse.lock_released:
489                 e.arvfile.truncate(attr.st_size)
490                 entry.st_size = e.arvfile.size()
491
492         return entry
493
494     @catch_exceptions
495     def lookup(self, parent_inode, name, ctx=None):
496         name = unicode(name, self.inodes.encoding)
497         inode = None
498
499         if name == '.':
500             inode = parent_inode
501         else:
502             if parent_inode in self.inodes:
503                 p = self.inodes[parent_inode]
504                 self.inodes.touch(p)
505                 if name == '..':
506                     inode = p.parent_inode
507                 elif isinstance(p, Directory) and name in p:
508                     inode = p[name].inode
509
510         if inode != None:
511             _logger.debug("arv-mount lookup: parent_inode %i name '%s' inode %i",
512                       parent_inode, name, inode)
513             self.inodes[inode].inc_ref()
514             return self.getattr(inode)
515         else:
516             _logger.debug("arv-mount lookup: parent_inode %i name '%s' not found",
517                       parent_inode, name)
518             raise llfuse.FUSEError(errno.ENOENT)
519
520     @catch_exceptions
521     def forget(self, inodes):
522         if self._shutdown_started.is_set():
523             return
524         for inode, nlookup in inodes:
525             ent = self.inodes[inode]
526             _logger.debug("arv-mount forget: inode %i nlookup %i ref_count %i", inode, nlookup, ent.ref_count)
527             if ent.dec_ref(nlookup) == 0 and ent.dead:
528                 self.inodes.del_entry(ent)
529
530     @catch_exceptions
531     def open(self, inode, flags, ctx=None):
532         if inode in self.inodes:
533             p = self.inodes[inode]
534         else:
535             raise llfuse.FUSEError(errno.ENOENT)
536
537         if isinstance(p, Directory):
538             raise llfuse.FUSEError(errno.EISDIR)
539
540         if ((flags & os.O_WRONLY) or (flags & os.O_RDWR)) and not p.writable():
541             raise llfuse.FUSEError(errno.EPERM)
542
543         fh = next(self._filehandles_counter)
544         self._filehandles[fh] = FileHandle(fh, p)
545         self.inodes.touch(p)
546
547         # Normally, we will have received an "update" event if the
548         # parent collection is stale here. However, even if the parent
549         # collection hasn't changed, the manifest might have been
550         # fetched so long ago that the signatures on the data block
551         # locators have expired. Calling checkupdate() on all
552         # ancestors ensures the signatures will be refreshed if
553         # necessary.
554         while p.parent_inode in self.inodes:
555             if p == self.inodes[p.parent_inode]:
556                 break
557             p = self.inodes[p.parent_inode]
558             self.inodes.touch(p)
559             p.checkupdate()
560
561         _logger.debug("arv-mount open inode %i flags %x fh %i", inode, flags, fh)
562
563         return fh
564
565     @catch_exceptions
566     def read(self, fh, off, size):
567         _logger.debug("arv-mount read fh %i off %i size %i", fh, off, size)
568         self.read_ops_counter.add(1)
569
570         if fh in self._filehandles:
571             handle = self._filehandles[fh]
572         else:
573             raise llfuse.FUSEError(errno.EBADF)
574
575         self.inodes.touch(handle.obj)
576
577         r = handle.obj.readfrom(off, size, self.num_retries)
578         if r:
579             self.read_counter.add(len(r))
580         return r
581
582     @catch_exceptions
583     def write(self, fh, off, buf):
584         _logger.debug("arv-mount write %i %i %i", fh, off, len(buf))
585         self.write_ops_counter.add(1)
586
587         if fh in self._filehandles:
588             handle = self._filehandles[fh]
589         else:
590             raise llfuse.FUSEError(errno.EBADF)
591
592         if not handle.obj.writable():
593             raise llfuse.FUSEError(errno.EPERM)
594
595         self.inodes.touch(handle.obj)
596
597         w = handle.obj.writeto(off, buf, self.num_retries)
598         if w:
599             self.write_counter.add(w)
600         return w
601
602     @catch_exceptions
603     def release(self, fh):
604         if fh in self._filehandles:
605             _logger.debug("arv-mount release fh %i", fh)
606             try:
607                 self._filehandles[fh].flush()
608             except Exception:
609                 raise
610             finally:
611                 self._filehandles[fh].release()
612                 del self._filehandles[fh]
613         self.inodes.inode_cache.cap_cache()
614
615     def releasedir(self, fh):
616         self.release(fh)
617
618     @catch_exceptions
619     def opendir(self, inode, ctx=None):
620         _logger.debug("arv-mount opendir: inode %i", inode)
621
622         if inode in self.inodes:
623             p = self.inodes[inode]
624         else:
625             raise llfuse.FUSEError(errno.ENOENT)
626
627         if not isinstance(p, Directory):
628             raise llfuse.FUSEError(errno.ENOTDIR)
629
630         fh = next(self._filehandles_counter)
631         if p.parent_inode in self.inodes:
632             parent = self.inodes[p.parent_inode]
633         else:
634             raise llfuse.FUSEError(errno.EIO)
635
636         # update atime
637         self.inodes.touch(p)
638
639         self._filehandles[fh] = DirectoryHandle(fh, p, [('.', p), ('..', parent)] + list(p.items()))
640         return fh
641
642     @catch_exceptions
643     def readdir(self, fh, off):
644         _logger.debug("arv-mount readdir: fh %i off %i", fh, off)
645
646         if fh in self._filehandles:
647             handle = self._filehandles[fh]
648         else:
649             raise llfuse.FUSEError(errno.EBADF)
650
651         e = off
652         while e < len(handle.entries):
653             if handle.entries[e][1].inode in self.inodes:
654                 yield (handle.entries[e][0].encode(self.inodes.encoding), self.getattr(handle.entries[e][1].inode), e+1)
655             e += 1
656
657     @catch_exceptions
658     def statfs(self, ctx=None):
659         st = llfuse.StatvfsData()
660         st.f_bsize = 128 * 1024
661         st.f_blocks = 0
662         st.f_files = 0
663
664         st.f_bfree = 0
665         st.f_bavail = 0
666
667         st.f_ffree = 0
668         st.f_favail = 0
669
670         st.f_frsize = 0
671         return st
672
673     def _check_writable(self, inode_parent):
674         if not self.enable_write:
675             raise llfuse.FUSEError(errno.EROFS)
676
677         if inode_parent in self.inodes:
678             p = self.inodes[inode_parent]
679         else:
680             raise llfuse.FUSEError(errno.ENOENT)
681
682         if not isinstance(p, Directory):
683             raise llfuse.FUSEError(errno.ENOTDIR)
684
685         if not p.writable():
686             raise llfuse.FUSEError(errno.EPERM)
687
688         return p
689
690     @catch_exceptions
691     def create(self, inode_parent, name, mode, flags, ctx=None):
692         _logger.debug("arv-mount create: parent_inode %i '%s' %o", inode_parent, name, mode)
693
694         p = self._check_writable(inode_parent)
695         p.create(name)
696
697         # The file entry should have been implicitly created by callback.
698         f = p[name]
699         fh = next(self._filehandles_counter)
700         self._filehandles[fh] = FileHandle(fh, f)
701         self.inodes.touch(p)
702
703         f.inc_ref()
704         return (fh, self.getattr(f.inode))
705
706     @catch_exceptions
707     def mkdir(self, inode_parent, name, mode, ctx=None):
708         _logger.debug("arv-mount mkdir: parent_inode %i '%s' %o", inode_parent, name, mode)
709
710         p = self._check_writable(inode_parent)
711         p.mkdir(name)
712
713         # The dir entry should have been implicitly created by callback.
714         d = p[name]
715
716         d.inc_ref()
717         return self.getattr(d.inode)
718
719     @catch_exceptions
720     def unlink(self, inode_parent, name, ctx=None):
721         _logger.debug("arv-mount unlink: parent_inode %i '%s'", inode_parent, name)
722         p = self._check_writable(inode_parent)
723         p.unlink(name)
724
725     @catch_exceptions
726     def rmdir(self, inode_parent, name, ctx=None):
727         _logger.debug("arv-mount rmdir: parent_inode %i '%s'", inode_parent, name)
728         p = self._check_writable(inode_parent)
729         p.rmdir(name)
730
731     @catch_exceptions
732     def rename(self, inode_parent_old, name_old, inode_parent_new, name_new, ctx=None):
733         _logger.debug("arv-mount rename: old_parent_inode %i '%s' new_parent_inode %i '%s'", inode_parent_old, name_old, inode_parent_new, name_new)
734         src = self._check_writable(inode_parent_old)
735         dest = self._check_writable(inode_parent_new)
736         dest.rename(name_old, name_new, src)
737
738     @catch_exceptions
739     def flush(self, fh):
740         if fh in self._filehandles:
741             self._filehandles[fh].flush()
742
743     def fsync(self, fh, datasync):
744         self.flush(fh)
745
746     def fsyncdir(self, fh, datasync):
747         self.flush(fh)