Merge branch 'stable-2.16' into stable-2.17
[ganeti-github.git] / lib / asyncnotifier.py
1 #
2 #
3
4 # Copyright (C) 2009 Google Inc.
5 # All rights reserved.
6 #
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions are
9 # met:
10 #
11 # 1. Redistributions of source code must retain the above copyright notice,
12 # this list of conditions and the following disclaimer.
13 #
14 # 2. Redistributions in binary form must reproduce the above copyright
15 # notice, this list of conditions and the following disclaimer in the
16 # documentation and/or other materials provided with the distribution.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
19 # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
20 # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
22 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30
31 """Asynchronous pyinotify implementation"""
32
33
34 import asyncore
35 import logging
36
37 try:
38 # pylint: disable=E0611
39 from pyinotify import pyinotify
40 except ImportError:
41 import pyinotify
42
43 from ganeti import daemon
44 from ganeti import errors
45
46
47 # We contributed the AsyncNotifier class back to python-pyinotify, and it's
48 # part of their codebase since version 0.8.7. This code can be removed once
49 # we'll be ready to depend on python-pyinotify >= 0.8.7
50 class AsyncNotifier(asyncore.file_dispatcher):
51 """An asyncore dispatcher for inotify events.
52
53 """
54 # pylint: disable=W0622,W0212
55 def __init__(self, watch_manager, default_proc_fun=None, map=None):
56 """Initializes this class.
57
58 This is a a special asyncore file_dispatcher that actually wraps a
59 pyinotify Notifier, making it asyncronous.
60
61 """
62 if default_proc_fun is None:
63 default_proc_fun = pyinotify.ProcessEvent()
64
65 self.notifier = pyinotify.Notifier(watch_manager, default_proc_fun)
66
67 # here we need to steal the file descriptor from the notifier, so we can
68 # use it in the global asyncore select, and avoid calling the
69 # check_events() function of the notifier (which doesn't allow us to select
70 # together with other file descriptors)
71 self.fd = self.notifier._fd
72 asyncore.file_dispatcher.__init__(self, self.fd, map)
73
74 def handle_read(self):
75 self.notifier.read_events()
76 self.notifier.process_events()
77
78
79 class ErrorLoggingAsyncNotifier(AsyncNotifier,
80 daemon.GanetiBaseAsyncoreDispatcher):
81 """An asyncnotifier that can survive errors in the callbacks.
82
83 We define this as a separate class, since we don't want to make AsyncNotifier
84 diverge from what we contributed upstream.
85
86 """
87
88
89 class FileEventHandlerBase(pyinotify.ProcessEvent):
90 """Base class for file event handlers.
91
92 @ivar watch_manager: Inotify watch manager
93
94 """
95 def __init__(self, watch_manager):
96 """Initializes this class.
97
98 @type watch_manager: pyinotify.WatchManager
99 @param watch_manager: inotify watch manager
100
101 """
102 # pylint: disable=W0231
103 # no need to call the parent's constructor
104 self.watch_manager = watch_manager
105
106 def process_default(self, event):
107 logging.error("Received unhandled inotify event: %s", event)
108
109 def AddWatch(self, filename, mask):
110 """Adds a file watch.
111
112 @param filename: Path to file
113 @param mask: Inotify event mask
114 @return: Result
115
116 """
117 result = self.watch_manager.add_watch(filename, mask)
118
119 ret = result.get(filename, -1)
120 if ret <= 0:
121 raise errors.InotifyError("Could not add inotify watcher (error code %s);"
122 " increasing fs.inotify.max_user_watches sysctl"
123 " might be necessary" % ret)
124
125 return result[filename]
126
127 def RemoveWatch(self, handle):
128 """Removes a handle from the watcher.
129
130 @param handle: Inotify handle
131 @return: Whether removal was successful
132
133 """
134 result = self.watch_manager.rm_watch(handle)
135
136 return result[handle]
137
138
139 class SingleFileEventHandler(FileEventHandlerBase):
140 """Handle modify events for a single file.
141
142 """
143 def __init__(self, watch_manager, callback, filename):
144 """Constructor for SingleFileEventHandler
145
146 @type watch_manager: pyinotify.WatchManager
147 @param watch_manager: inotify watch manager
148 @type callback: function accepting a boolean
149 @param callback: function to call when an inotify event happens
150 @type filename: string
151 @param filename: config file to watch
152
153 """
154 FileEventHandlerBase.__init__(self, watch_manager)
155
156 self._callback = callback
157 self._filename = filename
158
159 self._watch_handle = None
160
161 def enable(self):
162 """Watch the given file.
163
164 """
165 if self._watch_handle is not None:
166 return
167
168 # Different Pyinotify versions have the flag constants at different places,
169 # hence not accessing them directly
170 mask = (pyinotify.EventsCodes.ALL_FLAGS["IN_MODIFY"] |
171 pyinotify.EventsCodes.ALL_FLAGS["IN_IGNORED"])
172
173 self._watch_handle = self.AddWatch(self._filename, mask)
174
175 def disable(self):
176 """Stop watching the given file.
177
178 """
179 if self._watch_handle is not None and self.RemoveWatch(self._watch_handle):
180 self._watch_handle = None
181
182 # pylint: disable=C0103
183 # this overrides a method in pyinotify.ProcessEvent
184 def process_IN_IGNORED(self, event):
185 # Since we monitor a single file rather than the directory it resides in,
186 # when that file is replaced with another one (which is what happens when
187 # utils.WriteFile, the most normal way of updating files in ganeti, is
188 # called) we're going to receive an IN_IGNORED event from inotify, because
189 # of the file removal (which is contextual with the replacement). In such a
190 # case we'll need to create a watcher for the "new" file. This can be done
191 # by the callback by calling "enable" again on us.
192 logging.debug("Received 'ignored' inotify event for %s", event.path)
193 self._watch_handle = None
194 self._callback(False)
195
196 # pylint: disable=C0103
197 # this overrides a method in pyinotify.ProcessEvent
198 def process_IN_MODIFY(self, event):
199 # This gets called when the monitored file is modified. Note that this
200 # doesn't usually happen in Ganeti, as most of the time we're just
201 # replacing any file with a new one, at filesystem level, rather than
202 # actually changing it. (see utils.WriteFile)
203 logging.debug("Received 'modify' inotify event for %s", event.path)
204 self._callback(True)