+
+
+class ErrorLoggingAsyncNotifier(AsyncNotifier,
+ daemon.GanetiBaseAsyncoreDispatcher):
+ """An asyncnotifier that can survive errors in the callbacks.
+
+ We define this as a separate class, since we don't want to make AsyncNotifier
+ diverge from what we contributed upstream.
+
+ """
+
+
+class FileEventHandlerBase(pyinotify.ProcessEvent):
+ """Base class for file event handlers.
+
+ @ivar watch_manager: Inotify watch manager
+
+ """
+ def __init__(self, watch_manager):
+ """Initializes this class.
+
+ @type watch_manager: pyinotify.WatchManager
+ @param watch_manager: inotify watch manager
+
+ """
+ # pylint: disable=W0231
+ # no need to call the parent's constructor
+ self.watch_manager = watch_manager
+
+ def process_default(self, event):
+ logging.error("Received unhandled inotify event: %s", event)
+
+ def AddWatch(self, filename, mask):
+ """Adds a file watch.
+
+ @param filename: Path to file
+ @param mask: Inotify event mask
+ @return: Result
+
+ """
+ result = self.watch_manager.add_watch(filename, mask)
+
+ ret = result.get(filename, -1)
+ if ret <= 0:
+ raise errors.InotifyError("Could not add inotify watcher (%s)" % ret)
+
+ return result[filename]
+
+ def RemoveWatch(self, handle):
+ """Removes a handle from the watcher.
+
+ @param handle: Inotify handle
+ @return: Whether removal was successful
+
+ """
+ result = self.watch_manager.rm_watch(handle)
+
+ return result[handle]
+
+
+class SingleFileEventHandler(FileEventHandlerBase):
+ """Handle modify events for a single file.
+
+ """
+ def __init__(self, watch_manager, callback, filename):
+ """Constructor for SingleFileEventHandler
+
+ @type watch_manager: pyinotify.WatchManager
+ @param watch_manager: inotify watch manager
+ @type callback: function accepting a boolean
+ @param callback: function to call when an inotify event happens
+ @type filename: string
+ @param filename: config file to watch
+
+ """
+ FileEventHandlerBase.__init__(self, watch_manager)
+
+ self._callback = callback
+ self._filename = filename
+
+ self._watch_handle = None
+
+ def enable(self):
+ """Watch the given file.
+
+ """
+ if self._watch_handle is not None:
+ return
+
+ # Different Pyinotify versions have the flag constants at different places,
+ # hence not accessing them directly
+ mask = (pyinotify.EventsCodes.ALL_FLAGS["IN_MODIFY"] |
+ pyinotify.EventsCodes.ALL_FLAGS["IN_IGNORED"])
+
+ self._watch_handle = self.AddWatch(self._filename, mask)
+
+ def disable(self):
+ """Stop watching the given file.
+
+ """
+ if self._watch_handle is not None and self.RemoveWatch(self._watch_handle):
+ self._watch_handle = None
+
+ # pylint: disable=C0103
+ # this overrides a method in pyinotify.ProcessEvent
+ def process_IN_IGNORED(self, event):
+ # Since we monitor a single file rather than the directory it resides in,
+ # when that file is replaced with another one (which is what happens when
+ # utils.WriteFile, the most normal way of updating files in ganeti, is
+ # called) we're going to receive an IN_IGNORED event from inotify, because
+ # of the file removal (which is contextual with the replacement). In such a
+ # case we'll need to create a watcher for the "new" file. This can be done
+ # by the callback by calling "enable" again on us.
+ logging.debug("Received 'ignored' inotify event for %s", event.path)
+ self._watch_handle = None
+ self._callback(False)
+
+ # pylint: disable=C0103
+ # this overrides a method in pyinotify.ProcessEvent
+ def process_IN_MODIFY(self, event):
+ # This gets called when the monitored file is modified. Note that this
+ # doesn't usually happen in Ganeti, as most of the time we're just
+ # replacing any file with a new one, at filesystem level, rather than
+ # actually changing it. (see utils.WriteFile)
+ logging.debug("Received 'modify' inotify event for %s", event.path)
+ self._callback(True)