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