# # # Copyright (C) 2009 Google Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301, USA. """Asynchronous pyinotify implementation""" import asyncore import logging try: # pylint: disable=E0611 from pyinotify import pyinotify except ImportError: import pyinotify from ganeti import daemon from ganeti import errors # We contributed the AsyncNotifier class back to python-pyinotify, and it's # part of their codebase since version 0.8.7. This code can be removed once # we'll be ready to depend on python-pyinotify >= 0.8.7 class AsyncNotifier(asyncore.file_dispatcher): """An asyncore dispatcher for inotify events. """ # pylint: disable=W0622,W0212 def __init__(self, watch_manager, default_proc_fun=None, map=None): """Initializes this class. This is a a special asyncore file_dispatcher that actually wraps a pyinotify Notifier, making it asyncronous. """ if default_proc_fun is None: default_proc_fun = pyinotify.ProcessEvent() self.notifier = pyinotify.Notifier(watch_manager, default_proc_fun) # here we need to steal the file descriptor from the notifier, so we can # use it in the global asyncore select, and avoid calling the # check_events() function of the notifier (which doesn't allow us to select # together with other file descriptors) self.fd = self.notifier._fd asyncore.file_dispatcher.__init__(self, self.fd, map) def handle_read(self): self.notifier.read_events() self.notifier.process_events() 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 (error code %s);" " increasing fs.inotify.max_user_watches sysctl" " might be necessary" % 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)