Removing all ssh setup code from the core
[ganeti-local] / lib / asyncnotifier.py
index e90fb7c..e60c260 100644 (file)
 """Asynchronous pyinotify implementation"""
 
 
-import pyinotify
 import asyncore
+import logging
 
+try:
+  # pylint: disable-msg=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-msg=W0622,W0212
+  def __init__(self, watch_manager, default_proc_fun=None, map=None):
+    """Initializes this class.
 
-  def __init__(self, watch_manager,
-               default_proc_fun=None,
-               map=None):
-    """
-    Constructor for AsyncNotifier, a special asyncore file_dispatcher that
-    actually wraps a pyinotify Notifier, making it asyncronous.
+    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()
+      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
@@ -53,3 +65,85 @@ class AsyncNotifier(asyncore.file_dispatcher):
     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 SingleFileEventHandler(pyinotify.ProcessEvent):
+  """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
+
+    """
+    # pylint: disable-msg=W0231
+    # no need to call the parent's constructor
+    self.watch_manager = watch_manager
+    self.callback = callback
+    self.mask = pyinotify.EventsCodes.ALL_FLAGS["IN_IGNORED"] | \
+                pyinotify.EventsCodes.ALL_FLAGS["IN_MODIFY"]
+    self.file = filename
+    self.watch_handle = None
+
+  def enable(self):
+    """Watch the given file
+
+    """
+    if self.watch_handle is None:
+      result = self.watch_manager.add_watch(self.file, self.mask)
+      if not self.file in result or result[self.file] <= 0:
+        raise errors.InotifyError("Could not add inotify watcher")
+      else:
+        self.watch_handle = result[self.file]
+
+  def disable(self):
+    """Stop watching the given file
+
+    """
+    if self.watch_handle is not None:
+      result = self.watch_manager.rm_watch(self.watch_handle)
+      if result[self.watch_handle]:
+        self.watch_handle = None
+
+  # pylint: disable-msg=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-msg=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)
+
+  def process_default(self, event):
+    logging.error("Received unhandled inotify event: %s", event)