summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2/Server/Plugin
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/Bcfg2/Server/Plugin')
-rw-r--r--src/lib/Bcfg2/Server/Plugin/base.py20
-rw-r--r--src/lib/Bcfg2/Server/Plugin/helpers.py187
-rw-r--r--src/lib/Bcfg2/Server/Plugin/interfaces.py19
3 files changed, 145 insertions, 81 deletions
diff --git a/src/lib/Bcfg2/Server/Plugin/base.py b/src/lib/Bcfg2/Server/Plugin/base.py
index f7bc08717..c825a57b5 100644
--- a/src/lib/Bcfg2/Server/Plugin/base.py
+++ b/src/lib/Bcfg2/Server/Plugin/base.py
@@ -87,6 +87,10 @@ class Plugin(Debuggable):
#: alphabetically by their name.
sort_order = 500
+ #: Whether or not to automatically create a data directory for
+ #: this plugin
+ create = True
+
#: List of names of methods to be exposed as XML-RPC functions
__rmi__ = Debuggable.__rmi__
@@ -97,15 +101,21 @@ class Plugin(Debuggable):
:param datastore: The path to the Bcfg2 repository on the
filesystem
:type datastore: string
- :raises: :class:`Bcfg2.Server.Plugin.exceptions.PluginInitError`
+ :raises: :exc:`OSError` if adding a file monitor failed;
+ :class:`Bcfg2.Server.Plugin.exceptions.PluginInitError`
+ on other errors
.. autoattribute:: Bcfg2.Server.Plugin.base.Debuggable.__rmi__
"""
+ Debuggable.__init__(self, name=self.name)
self.Entries = {}
self.core = core
self.data = os.path.join(datastore, self.name)
+ if self.create and not os.path.exists(self.data):
+ self.logger.warning("%s: %s does not exist, creating" %
+ (self.name, self.data))
+ os.makedirs(self.data)
self.running = True
- Debuggable.__init__(self, name=self.name)
@classmethod
def init_repo(cls, repo):
@@ -125,5 +135,11 @@ class Plugin(Debuggable):
self.debug_log("Shutting down %s plugin" % self.name)
self.running = False
+ def set_debug(self, debug):
+ for entry in self.Entries.values():
+ if isinstance(entry, Debuggable):
+ entry.set_debug(debug)
+ return Debuggable.set_debug(self, debug)
+
def __str__(self):
return "%s Plugin" % self.__class__.__name__
diff --git a/src/lib/Bcfg2/Server/Plugin/helpers.py b/src/lib/Bcfg2/Server/Plugin/helpers.py
index 0b81077a3..81dc1d736 100644
--- a/src/lib/Bcfg2/Server/Plugin/helpers.py
+++ b/src/lib/Bcfg2/Server/Plugin/helpers.py
@@ -25,15 +25,15 @@ except ImportError:
HAS_DJANGO = False
#: A dict containing default metadata for Path entries from bcfg2.conf
-DEFAULT_FILE_METADATA = Bcfg2.Options.OptionParser(dict(
- configfile=Bcfg2.Options.CFILE,
- owner=Bcfg2.Options.MDATA_OWNER,
- group=Bcfg2.Options.MDATA_GROUP,
- mode=Bcfg2.Options.MDATA_MODE,
- secontext=Bcfg2.Options.MDATA_SECONTEXT,
- important=Bcfg2.Options.MDATA_IMPORTANT,
- paranoid=Bcfg2.Options.MDATA_PARANOID,
- sensitive=Bcfg2.Options.MDATA_SENSITIVE))
+DEFAULT_FILE_METADATA = Bcfg2.Options.OptionParser(
+ dict(configfile=Bcfg2.Options.CFILE,
+ owner=Bcfg2.Options.MDATA_OWNER,
+ group=Bcfg2.Options.MDATA_GROUP,
+ mode=Bcfg2.Options.MDATA_MODE,
+ secontext=Bcfg2.Options.MDATA_SECONTEXT,
+ important=Bcfg2.Options.MDATA_IMPORTANT,
+ paranoid=Bcfg2.Options.MDATA_PARANOID,
+ sensitive=Bcfg2.Options.MDATA_SENSITIVE))
DEFAULT_FILE_METADATA.parse([Bcfg2.Options.CFILE.cmd, Bcfg2.Options.CFILE])
del DEFAULT_FILE_METADATA['args']
del DEFAULT_FILE_METADATA['configfile']
@@ -41,15 +41,15 @@ del DEFAULT_FILE_METADATA['configfile']
LOGGER = logging.getLogger(__name__)
#: a compiled regular expression for parsing info and :info files
-INFO_REGEX = re.compile('owner:(\s)*(?P<owner>\S+)|' +
- 'group:(\s)*(?P<group>\S+)|' +
- 'mode:(\s)*(?P<mode>\w+)|' +
- 'secontext:(\s)*(?P<secontext>\S+)|' +
- 'paranoid:(\s)*(?P<paranoid>\S+)|' +
- 'sensitive:(\s)*(?P<sensitive>\S+)|' +
- 'encoding:(\s)*(?P<encoding>\S+)|' +
- 'important:(\s)*(?P<important>\S+)|' +
- 'mtime:(\s)*(?P<mtime>\w+)|')
+INFO_REGEX = re.compile(r'owner:\s*(?P<owner>\S+)|' +
+ r'group:\s*(?P<group>\S+)|' +
+ r'mode:\s*(?P<mode>\w+)|' +
+ r'secontext:\s*(?P<secontext>\S+)|' +
+ r'paranoid:\s*(?P<paranoid>\S+)|' +
+ r'sensitive:\s*(?P<sensitive>\S+)|' +
+ r'encoding:\s*(?P<encoding>\S+)|' +
+ r'important:\s*(?P<important>\S+)|' +
+ r'mtime:\s*(?P<mtime>\w+)')
def bind_info(entry, metadata, infoxml=None, default=DEFAULT_FILE_METADATA):
@@ -193,7 +193,7 @@ class PluginDatabaseModel(object):
app_label = "Server"
-class FileBacked(object):
+class FileBacked(Debuggable):
""" This object caches file data in memory. FileBacked objects are
principally meant to be used as a part of
:class:`Bcfg2.Server.Plugin.helpers.DirectoryBacked`. """
@@ -206,7 +206,7 @@ class FileBacked(object):
changes
:type fam: Bcfg2.Server.FileMonitor.FileMonitor
"""
- object.__init__(self)
+ Debuggable.__init__(self)
#: A string containing the raw data in this file
self.data = ''
@@ -231,10 +231,10 @@ class FileBacked(object):
self.Index()
except IOError:
err = sys.exc_info()[1]
- LOGGER.error("Failed to read file %s: %s" % (self.name, err))
+ self.logger.error("Failed to read file %s: %s" % (self.name, err))
except:
err = sys.exc_info()[1]
- LOGGER.error("Failed to parse file %s: %s" % (self.name, err))
+ self.logger.error("Failed to parse file %s: %s" % (self.name, err))
def Index(self):
""" Index() is called by :func:`HandleEvent` every time the
@@ -246,7 +246,7 @@ class FileBacked(object):
return "%s: %s" % (self.__class__.__name__, self.name)
-class DirectoryBacked(object):
+class DirectoryBacked(Debuggable):
""" DirectoryBacked objects represent a directory that contains
files, represented by objects of the type listed in
:attr:`__child__`, and other directories recursively. It monitors
@@ -280,7 +280,7 @@ class DirectoryBacked(object):
.. -----
.. autoattribute:: __child__
"""
- object.__init__(self)
+ Debuggable.__init__(self)
self.data = os.path.normpath(data)
self.fam = fam
@@ -299,11 +299,29 @@ class DirectoryBacked(object):
self.handles = {}
# Monitor everything in the plugin's directory
+ if not os.path.exists(self.data):
+ self.logger.warning("%s does not exist, creating" % self.data)
+ os.makedirs(self.data)
self.add_directory_monitor('')
+ def set_debug(self, debug):
+ for entry in self.entries.values():
+ if isinstance(entry, Debuggable):
+ entry.set_debug(debug)
+ return Debuggable.set_debug(self, debug)
+
def __getitem__(self, key):
return self.entries[key]
+ def __len__(self):
+ return len(self.entries)
+
+ def __delitem__(self, key):
+ del self.entries[key]
+
+ def __setitem__(self, key, val):
+ self.entries[key] = val
+
def __iter__(self):
return iter(list(self.entries.items()))
@@ -320,7 +338,7 @@ class DirectoryBacked(object):
dirpathname = os.path.join(self.data, relative)
if relative not in self.handles.values():
if not os.path.isdir(dirpathname):
- LOGGER.error("%s is not a directory" % dirpathname)
+ self.logger.error("%s is not a directory" % dirpathname)
return
reqid = self.fam.AddMonitor(dirpathname, self)
self.handles[reqid] = relative
@@ -365,8 +383,8 @@ class DirectoryBacked(object):
return
if event.requestID not in self.handles:
- LOGGER.warn("Got %s event with unknown handle (%s) for %s" %
- (action, event.requestID, event.filename))
+ self.logger.warn("Got %s event with unknown handle (%s) for %s" %
+ (action, event.requestID, event.filename))
return
# Clean up path names
@@ -376,7 +394,7 @@ class DirectoryBacked(object):
event.filename = event.filename[len(self.data) + 1:]
if self.ignore and self.ignore.search(event.filename):
- LOGGER.debug("Ignoring event %s" % event.filename)
+ self.logger.debug("Ignoring event %s" % event.filename)
return
# Calculate the absolute and relative paths this event refers to
@@ -411,19 +429,20 @@ class DirectoryBacked(object):
# class doesn't support canceling, so at least let
# the user know that a restart might be a good
# idea.
- LOGGER.warn("Directory properties for %s changed, please "
- " consider restarting the server" % abspath)
+ self.logger.warn("Directory properties for %s changed, "
+ "please consider restarting the server" %
+ abspath)
else:
# Got a "changed" event for a directory that we
# didn't know about. Go ahead and treat it like a
# "created" event, but log a warning, because this
# is unexpected.
- LOGGER.warn("Got %s event for unexpected dir %s" %
- (action, abspath))
+ self.logger.warn("Got %s event for unexpected dir %s" %
+ (action, abspath))
self.add_directory_monitor(relpath)
else:
- LOGGER.warn("Got unknown dir event %s %s %s" %
- (event.requestID, event.code2str(), abspath))
+ self.logger.warn("Got unknown dir event %s %s %s" %
+ (event.requestID, event.code2str(), abspath))
elif self.patterns.search(event.filename):
if action in ['exists', 'created']:
self.add_entry(relpath, event)
@@ -435,16 +454,15 @@ class DirectoryBacked(object):
# know about. Go ahead and treat it like a
# "created" event, but log a warning, because this
# is unexpected.
- LOGGER.warn("Got %s event for unexpected file %s" %
- (action,
- abspath))
+ self.logger.warn("Got %s event for unexpected file %s" %
+ (action, abspath))
self.add_entry(relpath, event)
else:
- LOGGER.warn("Got unknown file event %s %s %s" %
- (event.requestID, event.code2str(), abspath))
+ self.logger.warn("Got unknown file event %s %s %s" %
+ (event.requestID, event.code2str(), abspath))
else:
- LOGGER.warn("Could not process filename %s; ignoring" %
- event.filename)
+ self.logger.warn("Could not process filename %s; ignoring" %
+ event.filename)
class XMLFileBacked(FileBacked):
@@ -459,7 +477,11 @@ class XMLFileBacked(FileBacked):
#: behavior, set ``__identifier__`` to ``None``.
__identifier__ = 'name'
- def __init__(self, filename, fam=None, should_monitor=False):
+ #: If ``create`` is set, then it overrides the ``create`` argument
+ #: to the constructor.
+ create = None
+
+ def __init__(self, filename, fam=None, should_monitor=False, create=None):
"""
:param filename: The full path to the file to cache and monitor
:type filename: string
@@ -474,6 +496,13 @@ class XMLFileBacked(FileBacked):
:class:`Bcfg2.Server.Plugin.helpers.XMLDirectoryBacked`
object).
:type should_monitor: bool
+ :param create: Create the file if it doesn't exist.
+ ``create`` can be either an
+ :class:`lxml.etree._Element` object, which will
+ be used as initial content, or a string, which
+ will be used as the name of the (empty) tag
+ that will be the initial content of the file.
+ :type create: lxml.etree._Element or string
.. -----
.. autoattribute:: __identifier__
@@ -497,6 +526,21 @@ class XMLFileBacked(FileBacked):
#: "Extra" files included in this file by XInclude.
self.extras = []
+ #: Extra FAM monitors set by this object for files included by
+ #: XInclude.
+ self.extra_monitors = []
+
+ if ((create is not None or self.create not in [None, False]) and
+ not os.path.exists(self.name)):
+ toptag = create or self.create
+ self.logger.warning("%s does not exist, creating" % self.name)
+ if hasattr(toptag, "getroottree"):
+ el = toptag
+ else:
+ el = lxml.etree.Element(toptag)
+ el.getroottree().write(self.name, xml_declaration=False,
+ pretty_print=True)
+
#: Whether or not to monitor this file for changes.
self.should_monitor = should_monitor
if fam and should_monitor:
@@ -528,17 +572,19 @@ class XMLFileBacked(FileBacked):
if not extras:
msg = "%s: %s does not exist, skipping" % (self.name, name)
if el.findall('./%sfallback' % Bcfg2.Server.XI_NAMESPACE):
- LOGGER.debug(msg)
+ self.logger.debug(msg)
else:
- LOGGER.warning(msg)
+ self.logger.warning(msg)
parent = el.getparent()
parent.remove(el)
for extra in extras:
if extra != self.name and extra not in self.extras:
- self.add_monitor(extra)
+ self.extras.append(extra)
lxml.etree.SubElement(parent, xinclude, href=extra)
self._follow_xincludes(fname=extra)
+ if extra not in self.extra_monitors:
+ self.add_monitor(extra)
def Index(self):
self.xdata = lxml.etree.XML(self.data, base_url=self.name,
@@ -550,7 +596,8 @@ class XMLFileBacked(FileBacked):
self.xdata.getroottree().xinclude()
except lxml.etree.XIncludeError:
err = sys.exc_info()[1]
- LOGGER.error("XInclude failed on %s: %s" % (self.name, err))
+ self.logger.error("XInclude failed on %s: %s" % (self.name,
+ err))
self.entries = self.xdata.getchildren()
if self.__identifier__ is not None:
@@ -566,7 +613,7 @@ class XMLFileBacked(FileBacked):
:type fpath: string
:returns: None
"""
- self.extras.append(fpath)
+ self.extra_monitors.append(fpath)
if self.fam and self.should_monitor:
self.fam.AddMonitor(fpath, self)
@@ -755,14 +802,14 @@ class InfoNode (INode):
Client="lambda m, e: '%(name)s' == m.hostname and predicate(m, e)",
Group="lambda m, e: '%(name)s' in m.groups and predicate(m, e)",
Path="lambda m, e: ('%(name)s' == e.get('name') or " +
- "'%(name)s' == e.get('realname')) and " +
- "predicate(m, e)")
+ "'%(name)s' == e.get('realname')) and " +
+ "predicate(m, e)")
nraw = dict(
Client="lambda m, e: '%(name)s' != m.hostname and predicate(m, e)",
Group="lambda m, e: '%(name)s' not in m.groups and predicate(m, e)",
Path="lambda m, e: '%(name)s' != e.get('name') and " +
- "'%(name)s' != e.get('realname') and " +
- "predicate(m, e)")
+ "'%(name)s' != e.get('realname') and " +
+ "predicate(m, e)")
containers = ['Group', 'Client', 'Path']
@@ -776,8 +823,8 @@ class XMLSrc(XMLFileBacked):
__cacheobj__ = dict
__priority_required__ = True
- def __init__(self, filename, fam=None, should_monitor=False):
- XMLFileBacked.__init__(self, filename, fam, should_monitor)
+ def __init__(self, filename, fam=None, should_monitor=False, create=None):
+ XMLFileBacked.__init__(self, filename, fam, should_monitor, create)
self.items = {}
self.cache = None
self.pnode = None
@@ -789,7 +836,7 @@ class XMLSrc(XMLFileBacked):
data = open(self.name).read()
except IOError:
msg = "Failed to read file %s: %s" % (self.name, sys.exc_info()[1])
- LOGGER.error(msg)
+ self.logger.error(msg)
raise PluginExecutionError(msg)
self.items = {}
try:
@@ -797,7 +844,7 @@ class XMLSrc(XMLFileBacked):
except lxml.etree.XMLSyntaxError:
msg = "Failed to parse file %s: %s" % (self.name,
sys.exc_info()[1])
- LOGGER.error(msg)
+ self.logger.error(msg)
raise PluginExecutionError(msg)
self.pnode = self.__node__(xdata, self.items)
self.cache = None
@@ -807,7 +854,7 @@ class XMLSrc(XMLFileBacked):
if self.__priority_required__:
msg = "Got bogus priority %s for file %s" % \
(xdata.get('priority'), self.name)
- LOGGER.error(msg)
+ self.logger.error(msg)
raise PluginExecutionError(msg)
del xdata, data
@@ -817,8 +864,8 @@ class XMLSrc(XMLFileBacked):
if self.cache is None or self.cache[0] != metadata:
cache = (metadata, self.__cacheobj__())
if self.pnode is None:
- LOGGER.error("Cache method called early for %s; "
- "forcing data load" % self.name)
+ self.logger.error("Cache method called early for %s; "
+ "forcing data load" % self.name)
self.HandleEvent()
return
self.pnode.Match(metadata, cache[1])
@@ -842,7 +889,7 @@ class XMLDirectoryBacked(DirectoryBacked):
#: Only track and include files whose names (not paths) match this
#: compiled regex.
- patterns = re.compile('^.*\.xml$')
+ patterns = re.compile(r'^.*\.xml$')
#: The type of child objects to create for files contained within
#: the directory that is tracked. Default is
@@ -1111,7 +1158,7 @@ class EntrySet(Debuggable):
#: file is encountered that does not match the ``basename``
#: argument passed to the constructor or ``ignore``, then a
#: warning will be produced.
- ignore = re.compile("^(\.#.*|.*~|\\..*\\.(sw[px])|.*\\.genshi_include)$")
+ ignore = re.compile(r'^(\.#.*|.*~|\..*\.(sw[px])|.*\.genshi_include)$')
# The ``basename`` argument passed to the constructor will be
#: processed as a string that contains a regular expression (i.e.,
@@ -1174,8 +1221,8 @@ class EntrySet(Debuggable):
base_pat = basename
else:
base_pat = re.escape(basename)
- pattern = '(.*/)?%s(\.((H_(?P<hostname>\S+))|' % base_pat
- pattern += '(G(?P<prio>\d+)_(?P<group>\S+))))?$'
+ pattern = r'(.*/)?' + base_pat + \
+ r'(\.((H_(?P<hostname>\S+))|(G(?P<prio>\d+)_(?P<group>\S+))))?$'
#: ``specific`` is a regular expression that is used to
#: determine the specificity of a file in this entry set. It
@@ -1254,8 +1301,8 @@ class EntrySet(Debuggable):
self.entry_init(event)
else:
if event.filename not in self.entries:
- LOGGER.warning("Got %s event for unknown file %s" %
- (action, event.filename))
+ self.logger.warning("Got %s event for unknown file %s" %
+ (action, event.filename))
if action == 'changed':
# received a bogus changed event; warn, but treat
# it like a created event
@@ -1291,7 +1338,7 @@ class EntrySet(Debuggable):
entry_type = self.entry_type
if event.filename in self.entries:
- LOGGER.warn("Got duplicate add for %s" % event.filename)
+ self.logger.warn("Got duplicate add for %s" % event.filename)
else:
fpath = os.path.join(self.path, event.filename)
try:
@@ -1299,8 +1346,8 @@ class EntrySet(Debuggable):
specific=specific)
except SpecificityError:
if not self.ignore.match(event.filename):
- LOGGER.error("Could not process filename %s; ignoring" %
- fpath)
+ self.logger.error("Could not process filename %s; ignoring"
+ % fpath)
return
self.entries[event.filename] = entry_type(fpath, spec,
self.encoding)
@@ -1365,8 +1412,8 @@ class EntrySet(Debuggable):
for line in open(fpath).readlines():
match = INFO_REGEX.match(line)
if not match:
- LOGGER.warning("Failed to match line in %s: %s" % (fpath,
- line))
+ self.logger.warning("Failed to match line in %s: %s" %
+ (fpath, line))
continue
else:
mgd = match.groupdict()
@@ -1450,8 +1497,6 @@ class GroupSpool(Plugin, Generator):
def __init__(self, core, datastore):
Plugin.__init__(self, core, datastore)
Generator.__init__(self)
- if self.data[-1] == '/':
- self.data = self.data[:-1]
#: See :class:`Bcfg2.Server.Plugins.interfaces.Generator` for
#: details on the Entries attribute.
diff --git a/src/lib/Bcfg2/Server/Plugin/interfaces.py b/src/lib/Bcfg2/Server/Plugin/interfaces.py
index cb996b1ca..222b94fe3 100644
--- a/src/lib/Bcfg2/Server/Plugin/interfaces.py
+++ b/src/lib/Bcfg2/Server/Plugin/interfaces.py
@@ -286,6 +286,8 @@ class Statistics(Plugin):
you should avoid using Statistics and use
:class:`ThreadedStatistics` instead."""
+ create = False
+
def process_statistics(self, client, xdata):
""" Process the given XML statistics data for the specified
client.
@@ -337,12 +339,11 @@ class ThreadedStatistics(Statistics, Threaded, threading.Thread):
pending_data = []
try:
while not self.work_queue.empty():
- (metadata, data) = self.work_queue.get_nowait()
- pending_data.append(
- (metadata.hostname,
- lxml.etree.tostring(
- data,
- xml_declaration=False).decode("UTF-8")))
+ (metadata, xdata) = self.work_queue.get_nowait()
+ data = \
+ lxml.etree.tostring(xdata,
+ xml_declaration=False).decode("UTF-8")
+ pending_data.append((metadata.hostname, data))
except Empty:
pass
@@ -409,7 +410,7 @@ class ThreadedStatistics(Statistics, Threaded, threading.Thread):
def run(self):
if not self._load():
return
- while not self.terminate.isSet() and self.work_queue != None:
+ while not self.terminate.isSet() and self.work_queue is not None:
try:
(client, xdata) = self.work_queue.get(block=True, timeout=2)
except Empty:
@@ -419,7 +420,7 @@ class ThreadedStatistics(Statistics, Threaded, threading.Thread):
self.logger.error("ThreadedStatistics: %s" % err)
continue
self.handle_statistic(client, xdata)
- if self.work_queue != None and not self.work_queue.empty():
+ if self.work_queue is not None and not self.work_queue.empty():
self._save()
def process_statistics(self, metadata, data):
@@ -527,6 +528,8 @@ class GoalValidator(object):
class Version(Plugin):
""" Version plugins interact with various version control systems. """
+ create = False
+
#: The path to the VCS metadata file or directory, relative to the
#: base of the Bcfg2 repository. E.g., for Subversion this would
#: be ".svn"