Make ConfdInotifyEventHandler a library function
[ganeti-local] / lib / asyncnotifier.py
1 #
2 #
3
4 # Copyright (C) 2009 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """Asynchronous pyinotify implementation"""
23
24
25 import asyncore
26 import logging
27
28 try:
29   # pylint: disable-msg=E0611
30   from pyinotify import pyinotify
31 except ImportError:
32   import pyinotify
33
34 from ganeti import errors
35
36 # We contributed the AsyncNotifier class back to python-pyinotify, and it's
37 # part of their codebase since version 0.8.7. This code can be removed once
38 # we'll be ready to depend on python-pyinotify >= 0.8.7
39 class AsyncNotifier(asyncore.file_dispatcher):
40   """An asyncore dispatcher for inotify events.
41
42   """
43   # pylint: disable-msg=W0622,W0212
44   def __init__(self, watch_manager, default_proc_fun=None, map=None):
45     """Initializes this class.
46
47     This is a a special asyncore file_dispatcher that actually wraps a
48     pyinotify Notifier, making it asyncronous.
49
50     """
51     if default_proc_fun is None:
52       default_proc_fun = pyinotify.ProcessEvent()
53
54     self.notifier = pyinotify.Notifier(watch_manager, default_proc_fun)
55
56     # here we need to steal the file descriptor from the notifier, so we can
57     # use it in the global asyncore select, and avoid calling the
58     # check_events() function of the notifier (which doesn't allow us to select
59     # together with other file descriptors)
60     self.fd = self.notifier._fd
61     asyncore.file_dispatcher.__init__(self, self.fd, map)
62
63   def handle_read(self):
64     self.notifier.read_events()
65     self.notifier.process_events()
66
67
68 class SingleFileEventHandler(pyinotify.ProcessEvent):
69   """Handle modify events for a single file.
70
71   """
72
73   def __init__(self, watch_manager, callback, filename):
74     """Constructor for SingleFileEventHandler
75
76     @type watch_manager: pyinotify.WatchManager
77     @param watch_manager: inotify watch manager
78     @type callback: function accepting a boolean
79     @param callback: function to call when an inotify event happens
80     @type filename: string
81     @param filename: config file to watch
82
83     """
84     # pylint: disable-msg=W0231
85     # no need to call the parent's constructor
86     self.watch_manager = watch_manager
87     self.callback = callback
88     self.mask = pyinotify.EventsCodes.ALL_FLAGS["IN_IGNORED"] | \
89                 pyinotify.EventsCodes.ALL_FLAGS["IN_MODIFY"]
90     self.file = filename
91     self.watch_handle = None
92
93   def enable(self):
94     """Watch the given file
95
96     """
97     if self.watch_handle is None:
98       result = self.watch_manager.add_watch(self.file, self.mask)
99       if not self.file in result or result[self.file] <= 0:
100         raise errors.InotifyError("Could not add inotify watcher")
101       else:
102         self.watch_handle = result[self.file]
103
104   def disable(self):
105     """Stop watching the given file
106
107     """
108     if self.watch_handle is not None:
109       result = self.watch_manager.rm_watch(self.watch_handle)
110       if result[self.watch_handle]:
111         self.watch_handle = None
112
113   # pylint: disable-msg=C0103
114   # this overrides a method in pyinotify.ProcessEvent
115   def process_IN_IGNORED(self, event):
116     # Due to the fact that we monitor just for the cluster config file (rather
117     # than for the whole data dir) when the file is replaced with another one
118     # (which is what happens normally in ganeti) we're going to receive an
119     # IN_IGNORED event from inotify, because of the file removal (which is
120     # contextual with the replacement). In such a case we need to create
121     # another watcher for the "new" file.
122     logging.debug("Received 'ignored' inotify event for %s", event.path)
123     self.watch_handle = None
124
125     try:
126       # Since the kernel believes the file we were interested in is gone, it's
127       # not going to notify us of any other events, until we set up, here, the
128       # new watch. This is not a race condition, though, since we're anyway
129       # going to realod the file after setting up the new watch.
130       self.callback(False)
131     except: # pylint: disable-msg=W0702
132       # we need to catch any exception here, log it, but proceed, because even
133       # if we failed handling a single request, we still want our daemon to
134       # proceed.
135       logging.error("Unexpected exception", exc_info=True)
136
137   # pylint: disable-msg=C0103
138   # this overrides a method in pyinotify.ProcessEvent
139   def process_IN_MODIFY(self, event):
140     # This gets called when the config file is modified. Note that this doesn't
141     # usually happen in Ganeti, as the config file is normally replaced by a
142     # new one, at filesystem level, rather than actually modified (see
143     # utils.WriteFile)
144     logging.debug("Received 'modify' inotify event for %s", event.path)
145
146     try:
147       self.callback(True)
148     except: # pylint: disable-msg=W0702
149       # we need to catch any exception here, log it, but proceed, because even
150       # if we failed handling a single request, we still want our daemon to
151       # proceed.
152       logging.error("Unexpected exception", exc_info=True)
153
154   def process_default(self, event):
155     logging.error("Received unhandled inotify event: %s", event)