diff options
Diffstat (limited to 'src/lib/Bcfg2/Server/Plugin')
-rw-r--r-- | src/lib/Bcfg2/Server/Plugin/base.py | 20 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Plugin/helpers.py | 187 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Plugin/interfaces.py | 19 |
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" |