summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2/Server/FileMonitor/__init__.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/Bcfg2/Server/FileMonitor/__init__.py')
-rw-r--r--src/lib/Bcfg2/Server/FileMonitor/__init__.py232
1 files changed, 207 insertions, 25 deletions
diff --git a/src/lib/Bcfg2/Server/FileMonitor/__init__.py b/src/lib/Bcfg2/Server/FileMonitor/__init__.py
index 1b12ab703..ec21a744c 100644
--- a/src/lib/Bcfg2/Server/FileMonitor/__init__.py
+++ b/src/lib/Bcfg2/Server/FileMonitor/__init__.py
@@ -1,4 +1,49 @@
-"""Bcfg2.Server.FileMonitor provides the support for monitoring files."""
+""" Bcfg2.Server.FileMonitor provides the support for monitoring
+files. The FAM acts as a dispatcher for events: An event is detected
+on a file (e.g., the file content is changed), and then that event is
+dispatched to the ``HandleEvent`` method of an object that knows how
+to handle the event. Consequently,
+:func:`Bcfg2.Server.FileMonitor.FileMonitor.AddMonitor` takes two
+arguments: the path to monitor, and the object that handles events
+detected on that event.
+
+``HandleEvent`` is called with a single argument, the
+:class:`Bcfg2.Server.FileMonitor.Event` object to be handled.
+
+Assumptions
+-----------
+
+The FAM API Bcfg2 uses is based on the API of SGI's `File Alteration
+Monitor <http://oss.sgi.com/projects/fam/>`_ (also called "FAM").
+Consequently, a few assumptions apply:
+
+* When a file or directory is monitored for changes, we call that a
+ "monitor"; other backends my use the term "watch," but for
+ consistency we will use "monitor."
+* Monitors can be set on files or directories.
+* A monitor set on a directory monitors all files within that
+ directory, non-recursively. If the object that requested the
+ monitor wishes to monitor recursively, it must implement that
+ itself.
+* Setting a monitor immediately produces "exists" and "endExist"
+ events for the monitored file or directory and all files or
+ directories contained within it (non-recursively).
+* An event on a file or directory that is monitored directly yields
+ the full path to the file or directory.
+* An event on a file or directory that is *only* contained within a
+ monitored directory yields the relative path to the file or
+ directory within the monitored parent. It is the responsibility of
+ the handler to reconstruct full paths as necessary.
+* Each monitor that is set must have a unique ID that identifies it,
+ in order to make it possible to reconstruct full paths as
+ necessary. This ID will be stored in
+ :attr:`Bcfg2.Server.FileMonitor.FileMonitor.handles`. It may be any
+ hashable value; some FAM backends use monotonically increasing
+ integers, while others use the path to the monitor.
+
+Base Classes
+------------
+"""
import os
import sys
@@ -10,14 +55,45 @@ LOGGER = logging.getLogger(__name__)
class Event(object):
- """ Base class for all FAM events """
+ """ Base class for all FAM events. """
+
def __init__(self, request_id, filename, code):
+ """
+ :param request_id: The handler ID of the monitor that produced
+ this event
+ :type request_id: Varies
+ :param filename: The file or directory on which the event was
+ detected. An event on a file or directory
+ that is monitored directly yields the full
+ path to the file or directory; an event on a
+ file or directory that is *only* contained
+ within a monitored directory yields the
+ relative path to the file or directory within
+ the monitored parent.
+ :type filename: string
+ :param code: The :ref:`event code
+ <development-fam-event-codes>` produced. I.e.,
+ the type of event.
+ :type code: string
+ """
+ #: The handler ID of the monitor that produced this event
self.requestID = request_id
+
+ #: The file or directory on which the event was detected. An
+ #: event on a file or directory that is monitored directly
+ #: yields the full path to the file or directory; an event on
+ #: a file or directory that is *only* contained within a
+ #: monitored directory yields the relative path to the file or
+ #: directory within the monitored parent.
self.filename = filename
+
+ #: The :ref:`event code <development-fam-event-codes>`
+ #: produced. I.e., the type of event.
self.action = code
def code2str(self):
- """return static code for event"""
+ """ Return the :ref:`event code <development-fam-event-codes>`
+ for this event. This is just an alias for :attr:`action`. """
return self.action
def __str__(self):
@@ -29,15 +105,48 @@ class Event(object):
class FileMonitor(object):
- """File Monitor baseclass."""
+ """ The base class that all FAM implementions must inherit.
+
+ The simplest instance of a FileMonitor subclass needs only to add
+ monitor objects to :attr:`handles` and received events to
+ :attr:`events`; the basic interface will handle the rest. """
+
+ #: The relative priority of this FAM backend. Better backends
+ #: should have higher priorities.
+ __priority__ = -1
+
def __init__(self, ignore=None, debug=False):
- object.__init__(self)
+ """
+ :param ignore: A list of filename globs describing events that
+ should be ignored (i.e., not processed by any
+ object)
+ :type ignore: list of strings (filename globs)
+ :param debug: Produce debugging information about the events
+ received and handled.
+ :type debug: bool
+
+ .. -----
+ .. autoattribute:: __priority__
+ """
+ #: Whether or not to produce debug logging
self.debug = debug
+
+ #: A dict that records which objects handle which events.
+ #: Keys are monitor handle IDs and values are objects whose
+ #: ``HandleEvent`` method will be called to handle an event
self.handles = dict()
+
+ #: Queue of events to handle
self.events = []
+
if ignore is None:
ignore = []
+ #: List of filename globs to ignore events for. For events
+ #: that include the full path, both the full path and the bare
+ #: filename will be checked against ``ignore``.
self.ignore = ignore
+
+ #: Whether or not the FAM has been started. See :func:`start`.
self.started = False
def __str__(self):
@@ -49,17 +158,34 @@ class FileMonitor(object):
self.fileno())
def start(self):
- """ start threads or anything else that needs to be done after
- the server forks and daemonizes """
+ """ Start threads or anything else that needs to be done after
+ the server forks and daemonizes. Note that monitors may (and
+ almost certainly will) be added before ``start()`` is called,
+ so if a backend depends on being started to add monitors,
+ those requests will need to be enqueued and added after
+ ``start()``. See
+ :class:`Bcfg2.Server.FileMonitor.Inotify.Inotify` for an
+ example of this. """
self.started = True
def debug_log(self, msg):
- """ log a debug message """
+ """ Log a debug message.
+
+ :param msg: The message to log iff :attr:`debug` is set."""
if self.debug:
LOGGER.info(msg)
def should_ignore(self, event):
- """ returns true if an event should be ignored """
+ """ Returns True if an event should be ignored, False
+ otherwise. For events that include the full path, both the
+ full path and the bare filename will be checked against
+ :attr:`ignore`. If the event is ignored, a debug message will
+ be logged with :func:`debug_log`.
+
+ :param event: Check if this event matches :attr:`ignore`
+ :type event: Bcfg2.Server.FileMonitor.Event
+ :returns: bool - Whether not to ignore the event
+ """
for pattern in self.ignore:
if (fnmatch.fnmatch(event.filename, pattern) or
fnmatch.fnmatch(os.path.split(event.filename)[-1], pattern)):
@@ -68,20 +194,36 @@ class FileMonitor(object):
return False
def pending(self):
- """ returns True if there are pending events """
+ """ Returns True if there are pending events (i.e., events in
+ :attr:`events` that have not been processed), False
+ otherwise. """
return bool(self.events)
def get_event(self):
- """ get the oldest pending event """
+ """ Get the oldest pending event in :attr:`events`.
+
+ :returns: :class:`Bcfg2.Server.FileMonitor.Event`
+ """
return self.events.pop(0)
def fileno(self):
- """ get the file descriptor of the file monitor thread """
+ """ Get the file descriptor of the file monitor thread.
+
+ :returns: int - The FD number
+ """
return 0
def handle_one_event(self, event):
- """ handle the given event by dispatching it to the object
- that handles events for the path """
+ """ Handle the given event by dispatching it to the object
+ that handles it. This is only called by
+ :func:`handle_event_set`, so if a backend overrides that
+ method it does not necessarily need to implement this
+ function.
+
+ :param event: The event to handle.
+ :type event: Bcfg2.Server.FileMonitor.Event
+ :returns: None
+ """
if not self.started:
self.start()
if self.should_ignore(event):
@@ -101,15 +243,22 @@ class FileMonitor(object):
(event.code2str(), event.filename, err))
def handle_event_set(self, lock=None):
- """ Handle all pending events """
+ """ Handle all pending events.
+
+ :param lock: A thread lock to use while handling events. If
+ None, then no thread locking will be performed.
+ This can possibly lead to race conditions in
+ event handling, although it's unlikely to cause
+ any real problems.
+ :type lock: threading.Lock
+ :returns: None
+ """
if not self.started:
self.start()
- count = 1
- event = self.get_event()
+ count = 0
start = time()
if lock:
lock.acquire()
- self.handle_one_event(event)
while self.pending():
self.handle_one_event(self.get_event())
count += 1
@@ -119,8 +268,19 @@ class FileMonitor(object):
LOGGER.info("Handled %d events in %.03fs" % (count, (end - start)))
def handle_events_in_interval(self, interval):
- """ handle events for the specified period of time (in
- seconds) """
+ """ Handle events for the specified period of time (in
+ seconds). This call will block for ``interval`` seconds and
+ handle all events received during that period by calling
+ :func:`handle_event_set`.
+
+ :param interval: The interval, in seconds, during which events
+ should be handled. Any events that are
+ already pending when
+ :func:`handle_events_in_interval` is called
+ will also be handled.
+ :type interval: int
+ :returns: None
+ """
if not self.started:
self.start()
end = time() + interval
@@ -132,17 +292,38 @@ class FileMonitor(object):
sleep(0.5)
def shutdown(self):
- """ shutdown the monitor """
+ """ Handle any tasks required to shut down the monitor. """
self.started = False
def AddMonitor(self, path, obj, handleID=None):
- """ watch the specified path, alerting obj to events """
+ """ Monitor the specified path, alerting obj to events. This
+ method must be overridden by a subclass of
+ :class:`Bcfg2.Server.FileMonitor.FileMonitor`.
+
+ :param path: The path to monitor
+ :type path: string
+ :param obj: The object whose ``HandleEvent`` method will be
+ called when an event is produced.
+ :type obj: Varies
+ :param handleID: The handle ID to use for the monitor. This
+ is useful when requests to add a monitor must
+ be enqueued and the actual monitors added
+ after :func:`start` is called.
+ :type handleID: Varies
+ :returns: Varies - The handler ID for the newly created
+ monitor
+ """
raise NotImplementedError
+#: A dict of all available FAM backends. Keys are the human-readable
+#: names of the backends, which are used in bcfg2.conf to select a
+#: backend; values are the backend classes. In addition, the
+#: ``default`` key will be set to the best FAM backend as determined
+#: by :attr:`Bcfg2.Server.FileMonitor.FileMonitor.__priority__`
available = dict() # pylint: disable=C0103
-# todo: loading the monitor drivers should be automatic
+# TODO: loading the monitor drivers should be automatic
from Bcfg2.Server.FileMonitor.Pseudo import Pseudo
available['pseudo'] = Pseudo
@@ -162,9 +343,10 @@ try:
from Bcfg2.Server.FileMonitor.Inotify import Inotify
available['inotify'] = Inotify
except ImportError:
- pass
+ pass
-for fdrv in sorted(available.keys(), key=lambda k: available[k].__priority__):
+for fdrv in reversed(sorted(available.keys(),
+ key=lambda k: available[k].__priority__)):
if fdrv in available:
available['default'] = available[fdrv]
break