Merge branch '12032-project-trash' refs #12032
[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             obj.kernel_invalidate()
163             if obj.has_ref(True):
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             entry.inode = None
270         else:
271             entry.dead = True
272             _logger.debug("del_entry on inode %i with refcount %i", entry.inode, entry.ref_count)
273
274     def invalidate_inode(self, entry):
275         if entry.has_ref(False):
276             # Only necessary if the kernel has previously done a lookup on this
277             # inode and hasn't yet forgotten about it.
278             llfuse.invalidate_inode(entry.inode)
279
280     def invalidate_entry(self, entry, name):
281         if entry.has_ref(False):
282             # Only necessary if the kernel has previously done a lookup on this
283             # inode and hasn't yet forgotten about it.
284             llfuse.invalidate_entry(entry.inode, name.encode(self.encoding))
285
286     def clear(self):
287         self.inode_cache.clear()
288
289         for k,v in self._entries.items():
290             try:
291                 v.finalize()
292             except Exception as e:
293                 _logger.exception("Error during finalize of inode %i", k)
294
295         self._entries.clear()
296
297
298 def catch_exceptions(orig_func):
299     """Catch uncaught exceptions and log them consistently."""
300
301     @functools.wraps(orig_func)
302     def catch_exceptions_wrapper(self, *args, **kwargs):
303         try:
304             return orig_func(self, *args, **kwargs)
305         except llfuse.FUSEError:
306             raise
307         except EnvironmentError as e:
308             raise llfuse.FUSEError(e.errno)
309         except arvados.errors.KeepWriteError as e:
310             _logger.error("Keep write error: " + str(e))
311             raise llfuse.FUSEError(errno.EIO)
312         except arvados.errors.NotFoundError as e:
313             _logger.error("Block not found error: " + str(e))
314             raise llfuse.FUSEError(errno.EIO)
315         except:
316             _logger.exception("Unhandled exception during FUSE operation")
317             raise llfuse.FUSEError(errno.EIO)
318
319     return catch_exceptions_wrapper
320
321
322 class Operations(llfuse.Operations):
323     """This is the main interface with llfuse.
324
325     The methods on this object are called by llfuse threads to service FUSE
326     events to query and read from the file system.
327
328     llfuse has its own global lock which is acquired before calling a request handler,
329     so request handlers do not run concurrently unless the lock is explicitly released
330     using 'with llfuse.lock_released:'
331
332     """
333
334     def __init__(self, uid, gid, api_client, encoding="utf-8", inode_cache=None, num_retries=4, enable_write=False):
335         super(Operations, self).__init__()
336
337         self._api_client = api_client
338
339         if not inode_cache:
340             inode_cache = InodeCache(cap=256*1024*1024)
341         self.inodes = Inodes(inode_cache, encoding=encoding)
342         self.uid = uid
343         self.gid = gid
344         self.enable_write = enable_write
345
346         # dict of inode to filehandle
347         self._filehandles = {}
348         self._filehandles_counter = itertools.count(0)
349
350         # Other threads that need to wait until the fuse driver
351         # is fully initialized should wait() on this event object.
352         self.initlock = threading.Event()
353
354         # If we get overlapping shutdown events (e.g., fusermount -u
355         # -z and operations.destroy()) llfuse calls forget() on inodes
356         # that have already been deleted. To avoid this, we make
357         # forget() a no-op if called after destroy().
358         self._shutdown_started = threading.Event()
359
360         self.num_retries = num_retries
361
362         self.read_counter = arvados.keep.Counter()
363         self.write_counter = arvados.keep.Counter()
364         self.read_ops_counter = arvados.keep.Counter()
365         self.write_ops_counter = arvados.keep.Counter()
366
367         self.events = None
368
369     def init(self):
370         # Allow threads that are waiting for the driver to be finished
371         # initializing to continue
372         self.initlock.set()
373
374     @catch_exceptions
375     def destroy(self):
376         self._shutdown_started.set()
377         if self.events:
378             self.events.close()
379             self.events = None
380
381         # Different versions of llfuse require and forbid us to
382         # acquire the lock here. See #8345#note-37, #10805#note-9.
383         if LLFUSE_VERSION_0 and llfuse.lock.acquire():
384             # llfuse < 0.42
385             self.inodes.clear()
386             llfuse.lock.release()
387         else:
388             # llfuse >= 0.42
389             self.inodes.clear()
390
391     def access(self, inode, mode, ctx):
392         return True
393
394     def listen_for_events(self):
395         self.events = arvados.events.subscribe(
396             self._api_client,
397             [["event_type", "in", ["create", "update", "delete"]]],
398             self.on_event)
399
400     @catch_exceptions
401     def on_event(self, ev):
402         if 'event_type' not in ev or ev["event_type"] not in ("create", "update", "delete"):
403             return
404         with llfuse.lock:
405             properties = ev.get("properties") or {}
406             old_attrs = properties.get("old_attributes") or {}
407             new_attrs = properties.get("new_attributes") or {}
408
409             for item in self.inodes.inode_cache.find_by_uuid(ev["object_uuid"]):
410                 item.invalidate()
411                 if ev.get("object_kind") == "arvados#collection":
412                     pdh = new_attrs.get("portable_data_hash")
413                     # new_attributes.modified_at currently lacks
414                     # subsecond precision (see #6347) so use event_at
415                     # which should always be the same.
416                     stamp = ev.get("event_at")
417                     if (stamp and pdh and item.writable() and
418                         item.collection is not None and
419                         item.collection.modified() and
420                         new_attrs.get("is_trashed") is not True):
421                         item.update(to_record_version=(stamp, pdh))
422
423             oldowner = old_attrs.get("owner_uuid")
424             newowner = ev.get("object_owner_uuid")
425             for parent in (
426                     self.inodes.inode_cache.find_by_uuid(oldowner) +
427                     self.inodes.inode_cache.find_by_uuid(newowner)):
428                 parent.child_event(ev)
429
430     @catch_exceptions
431     def getattr(self, inode, ctx=None):
432         if inode not in self.inodes:
433             raise llfuse.FUSEError(errno.ENOENT)
434
435         e = self.inodes[inode]
436
437         entry = llfuse.EntryAttributes()
438         entry.st_ino = inode
439         entry.generation = 0
440         entry.entry_timeout = 0
441         entry.attr_timeout = e.time_to_next_poll() if e.allow_attr_cache else 0
442
443         entry.st_mode = stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH
444         if isinstance(e, Directory):
445             entry.st_mode |= stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH | stat.S_IFDIR
446         else:
447             entry.st_mode |= stat.S_IFREG
448             if isinstance(e, FuseArvadosFile):
449                 entry.st_mode |= stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
450
451         if self.enable_write and e.writable():
452             entry.st_mode |= stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH
453
454         entry.st_nlink = 1
455         entry.st_uid = self.uid
456         entry.st_gid = self.gid
457         entry.st_rdev = 0
458
459         entry.st_size = e.size()
460
461         entry.st_blksize = 512
462         entry.st_blocks = (entry.st_size/512)+1
463         if hasattr(entry, 'st_atime_ns'):
464             # llfuse >= 0.42
465             entry.st_atime_ns = int(e.atime() * 1000000000)
466             entry.st_mtime_ns = int(e.mtime() * 1000000000)
467             entry.st_ctime_ns = int(e.mtime() * 1000000000)
468         else:
469             # llfuse < 0.42
470             entry.st_atime = int(e.atime)
471             entry.st_mtime = int(e.mtime)
472             entry.st_ctime = int(e.mtime)
473
474         return entry
475
476     @catch_exceptions
477     def setattr(self, inode, attr, fields=None, fh=None, ctx=None):
478         entry = self.getattr(inode)
479
480         if fh is not None and fh in self._filehandles:
481             handle = self._filehandles[fh]
482             e = handle.obj
483         else:
484             e = self.inodes[inode]
485
486         if fields is None:
487             # llfuse < 0.42
488             update_size = attr.st_size is not None
489         else:
490             # llfuse >= 0.42
491             update_size = fields.update_size
492         if update_size and isinstance(e, FuseArvadosFile):
493             with llfuse.lock_released:
494                 e.arvfile.truncate(attr.st_size)
495                 entry.st_size = e.arvfile.size()
496
497         return entry
498
499     @catch_exceptions
500     def lookup(self, parent_inode, name, ctx=None):
501         name = unicode(name, self.inodes.encoding)
502         inode = None
503
504         if name == '.':
505             inode = parent_inode
506         else:
507             if parent_inode in self.inodes:
508                 p = self.inodes[parent_inode]
509                 self.inodes.touch(p)
510                 if name == '..':
511                     inode = p.parent_inode
512                 elif isinstance(p, Directory) and name in p:
513                     inode = p[name].inode
514
515         if inode != None:
516             _logger.debug("arv-mount lookup: parent_inode %i name '%s' inode %i",
517                       parent_inode, name, inode)
518             self.inodes[inode].inc_ref()
519             return self.getattr(inode)
520         else:
521             _logger.debug("arv-mount lookup: parent_inode %i name '%s' not found",
522                       parent_inode, name)
523             raise llfuse.FUSEError(errno.ENOENT)
524
525     @catch_exceptions
526     def forget(self, inodes):
527         if self._shutdown_started.is_set():
528             return
529         for inode, nlookup in inodes:
530             ent = self.inodes[inode]
531             _logger.debug("arv-mount forget: inode %i nlookup %i ref_count %i", inode, nlookup, ent.ref_count)
532             if ent.dec_ref(nlookup) == 0 and ent.dead:
533                 self.inodes.del_entry(ent)
534
535     @catch_exceptions
536     def open(self, inode, flags, ctx=None):
537         if inode in self.inodes:
538             p = self.inodes[inode]
539         else:
540             raise llfuse.FUSEError(errno.ENOENT)
541
542         if isinstance(p, Directory):
543             raise llfuse.FUSEError(errno.EISDIR)
544
545         if ((flags & os.O_WRONLY) or (flags & os.O_RDWR)) and not p.writable():
546             raise llfuse.FUSEError(errno.EPERM)
547
548         fh = next(self._filehandles_counter)
549         self._filehandles[fh] = FileHandle(fh, p)
550         self.inodes.touch(p)
551
552         # Normally, we will have received an "update" event if the
553         # parent collection is stale here. However, even if the parent
554         # collection hasn't changed, the manifest might have been
555         # fetched so long ago that the signatures on the data block
556         # locators have expired. Calling checkupdate() on all
557         # ancestors ensures the signatures will be refreshed if
558         # necessary.
559         while p.parent_inode in self.inodes:
560             if p == self.inodes[p.parent_inode]:
561                 break
562             p = self.inodes[p.parent_inode]
563             self.inodes.touch(p)
564             p.checkupdate()
565
566         _logger.debug("arv-mount open inode %i flags %x fh %i", inode, flags, fh)
567
568         return fh
569
570     @catch_exceptions
571     def read(self, fh, off, size):
572         _logger.debug("arv-mount read fh %i off %i size %i", fh, off, size)
573         self.read_ops_counter.add(1)
574
575         if fh in self._filehandles:
576             handle = self._filehandles[fh]
577         else:
578             raise llfuse.FUSEError(errno.EBADF)
579
580         self.inodes.touch(handle.obj)
581
582         r = handle.obj.readfrom(off, size, self.num_retries)
583         if r:
584             self.read_counter.add(len(r))
585         return r
586
587     @catch_exceptions
588     def write(self, fh, off, buf):
589         _logger.debug("arv-mount write %i %i %i", fh, off, len(buf))
590         self.write_ops_counter.add(1)
591
592         if fh in self._filehandles:
593             handle = self._filehandles[fh]
594         else:
595             raise llfuse.FUSEError(errno.EBADF)
596
597         if not handle.obj.writable():
598             raise llfuse.FUSEError(errno.EPERM)
599
600         self.inodes.touch(handle.obj)
601
602         w = handle.obj.writeto(off, buf, self.num_retries)
603         if w:
604             self.write_counter.add(w)
605         return w
606
607     @catch_exceptions
608     def release(self, fh):
609         if fh in self._filehandles:
610             _logger.debug("arv-mount release fh %i", fh)
611             try:
612                 self._filehandles[fh].flush()
613             except Exception:
614                 raise
615             finally:
616                 self._filehandles[fh].release()
617                 del self._filehandles[fh]
618         self.inodes.inode_cache.cap_cache()
619
620     def releasedir(self, fh):
621         self.release(fh)
622
623     @catch_exceptions
624     def opendir(self, inode, ctx=None):
625         _logger.debug("arv-mount opendir: inode %i", inode)
626
627         if inode in self.inodes:
628             p = self.inodes[inode]
629         else:
630             raise llfuse.FUSEError(errno.ENOENT)
631
632         if not isinstance(p, Directory):
633             raise llfuse.FUSEError(errno.ENOTDIR)
634
635         fh = next(self._filehandles_counter)
636         if p.parent_inode in self.inodes:
637             parent = self.inodes[p.parent_inode]
638         else:
639             raise llfuse.FUSEError(errno.EIO)
640
641         # update atime
642         self.inodes.touch(p)
643
644         self._filehandles[fh] = DirectoryHandle(fh, p, [('.', p), ('..', parent)] + list(p.items()))
645         return fh
646
647     @catch_exceptions
648     def readdir(self, fh, off):
649         _logger.debug("arv-mount readdir: fh %i off %i", fh, off)
650
651         if fh in self._filehandles:
652             handle = self._filehandles[fh]
653         else:
654             raise llfuse.FUSEError(errno.EBADF)
655
656         e = off
657         while e < len(handle.entries):
658             if handle.entries[e][1].inode in self.inodes:
659                 yield (handle.entries[e][0].encode(self.inodes.encoding), self.getattr(handle.entries[e][1].inode), e+1)
660             e += 1
661
662     @catch_exceptions
663     def statfs(self, ctx=None):
664         st = llfuse.StatvfsData()
665         st.f_bsize = 128 * 1024
666         st.f_blocks = 0
667         st.f_files = 0
668
669         st.f_bfree = 0
670         st.f_bavail = 0
671
672         st.f_ffree = 0
673         st.f_favail = 0
674
675         st.f_frsize = 0
676         return st
677
678     def _check_writable(self, inode_parent):
679         if not self.enable_write:
680             raise llfuse.FUSEError(errno.EROFS)
681
682         if inode_parent in self.inodes:
683             p = self.inodes[inode_parent]
684         else:
685             raise llfuse.FUSEError(errno.ENOENT)
686
687         if not isinstance(p, Directory):
688             raise llfuse.FUSEError(errno.ENOTDIR)
689
690         if not p.writable():
691             raise llfuse.FUSEError(errno.EPERM)
692
693         return p
694
695     @catch_exceptions
696     def create(self, inode_parent, name, mode, flags, ctx=None):
697         _logger.debug("arv-mount create: parent_inode %i '%s' %o", inode_parent, name, mode)
698
699         p = self._check_writable(inode_parent)
700         p.create(name)
701
702         # The file entry should have been implicitly created by callback.
703         f = p[name]
704         fh = next(self._filehandles_counter)
705         self._filehandles[fh] = FileHandle(fh, f)
706         self.inodes.touch(p)
707
708         f.inc_ref()
709         return (fh, self.getattr(f.inode))
710
711     @catch_exceptions
712     def mkdir(self, inode_parent, name, mode, ctx=None):
713         _logger.debug("arv-mount mkdir: parent_inode %i '%s' %o", inode_parent, name, mode)
714
715         p = self._check_writable(inode_parent)
716         p.mkdir(name)
717
718         # The dir entry should have been implicitly created by callback.
719         d = p[name]
720
721         d.inc_ref()
722         return self.getattr(d.inode)
723
724     @catch_exceptions
725     def unlink(self, inode_parent, name, ctx=None):
726         _logger.debug("arv-mount unlink: parent_inode %i '%s'", inode_parent, name)
727         p = self._check_writable(inode_parent)
728         p.unlink(name)
729
730     @catch_exceptions
731     def rmdir(self, inode_parent, name, ctx=None):
732         _logger.debug("arv-mount rmdir: parent_inode %i '%s'", inode_parent, name)
733         p = self._check_writable(inode_parent)
734         p.rmdir(name)
735
736     @catch_exceptions
737     def rename(self, inode_parent_old, name_old, inode_parent_new, name_new, ctx=None):
738         _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)
739         src = self._check_writable(inode_parent_old)
740         dest = self._check_writable(inode_parent_new)
741         dest.rename(name_old, name_new, src)
742
743     @catch_exceptions
744     def flush(self, fh):
745         if fh in self._filehandles:
746             self._filehandles[fh].flush()
747
748     def fsync(self, fh, datasync):
749         self.flush(fh)
750
751     def fsyncdir(self, fh, datasync):
752         self.flush(fh)