diff options
-rw-r--r-- | src/lib/Server/Plugin.py | 137 | ||||
-rw-r--r-- | testsuite/TestPlugin.py | 122 |
2 files changed, 259 insertions, 0 deletions
diff --git a/src/lib/Server/Plugin.py b/src/lib/Server/Plugin.py index dea30e105..4ad48838b 100644 --- a/src/lib/Server/Plugin.py +++ b/src/lib/Server/Plugin.py @@ -7,6 +7,12 @@ from lxml.etree import XML, XMLSyntaxError logger = logging.getLogger('Bcfg2.Plugin') +default_file_metadata = {'owner': 'root', 'group': 'root', 'perms': '644'} + +info_regex = re.compile( \ + '^owner:(\s)*(?P<owner>\w+)$|group:(\s)*(?P<group>\w+)$|' + + 'perms:(\s)*(?P<perms>\w+)$') + class PluginInitError(Exception): '''Error raised in cases of Plugin initialization errors''' pass @@ -378,3 +384,134 @@ class PrioDir(Plugin, XMLDirectoryBacked): [entry.append(copy.deepcopy(item)) for item in data['__children__']] [entry.attrib.__setitem__(key, data[key]) for key in data.keys() \ if not key.startswith('__')] + +# new unified EntrySet backend + +class SpecificityError(Exception): + '''Thrown in case of filename parse failure''' + pass + +class Specificity: + + def __init__(self, reg, fname): + self.hostname = None + self.all = False + self.group = None + self.prio = 0 + data = reg.match(fname) + if not data: + raise SpecificityError(fname) + if data.group('hostname'): + self.hostname = data.group('hostname') + elif data.group('group'): + self.group = data.group('group') + self.prio = int(data.group('prio')) + else: + self.all = True + +class EntrySet: + '''Entry sets deal with the host- and group-specific entries''' + def __init__(self, basename, path, props, entry_type): + self.path = path + self.entry_type = entry_type + self.entries = {} + self.properties = props + self.metadata = default_file_metadata.copy() + self.infoxml = None + pattern = '(.*/)?%s(\.((H_(?P<hostname>\S+))|' % basename + pattern += '(G(?P<prio>\d+)_(?P<group>\S+))))?$' + self.specific = re.compile(pattern) + + def handle_event(self, event): + '''Handle FAM events for the TemplateSet''' + action = event.code2str() + + if event.filename in ['info', 'info.xml']: + if action in ['exists', 'created', 'changed']: + self.update_metadata(event) + elif action == 'deleted': + self.reset_metadata(event) + return + + if action in ['exists', 'created']: + self.entry_init(event) + elif action == 'changed': + self.entries[event.filename].handle_event(event) + elif action == 'deleted': + del self.entries[event.filename] + + def entry_init(self, event): + '''handle template and info file creation''' + if event.filename in self.entries: + logger.warn("Got duplicate add for %s" % event.filename) + else: + fpath = "%s/%s" % (self.path, event.filename) + spec = Specificity(self.specific, event.filename) + self.entries[event.filename] = self.entry_type(fpath, + self.properties, + spec) + self.entries[event.filename].handle_event(event) + + def update_metadata(self, event): + '''process info and info.xml files for the templates''' + fpath = "%s/%s" % (self.path, event.filename) + if event.filename == 'info.xml': + if not self.infoxml: + self.infoxml = XMLSrc(fpath, True) + self.infoxml.HandleEvent(event) + elif event.filename == 'info': + for line in open(fpath).readlines(): + match = info_regex.match(line) + if not match: + logger.warning("Failed to match line: %s"%line) + continue + else: + mgd = match.groupdict() + if mgd['owner']: + self.metadata['owner'] = mgd['owner'] + elif mgd['group']: + self.metadata['group'] = mgd['group'] + elif mgd['perms']: + self.metadata['perms'] = mgd['perms'] + if len(self.metadata['perms']) == 3: + self.metadata['perms'] = "0%s" % (self.metadata['perms']) + + def reset_metadata(self, event): + '''reset metadata to defaults if info or info.xml removed''' + if event.filename == 'info.xml': + self.infoxml = None + elif event.filename == 'info': + self.metadata = default_file_metadata.copy() + + def group_sortfunc(self, x, y): + '''sort groups by their priority''' + return cmp(x.specific.prio, y.specific.prio) + + def bind_entry(self, entry, metadata): + '''Return the appropriate interpreted template from the set of available templates''' + if not self.infoxml: + for key in self.metadata: + entry.set(key, self.metadata[key]) + else: + mdata = {} + self.infoxml.pnode.Match(metadata, mdata) + [entry.attrib.__setitem__(key, value) \ + for (key, value) in mdata['Info'][None].iteritems()] + + hspec = [ent for ent in self.entries.values() if + ent.specific.hostname == metadata.hostname] + if hspec: + return hspec[0].bind_entry(entry, metadata) + + gspec = [ent for ent in self.entries.values() if + ent.specific.group in metadata.groups] + if gspec: + gspec.sort(self.group_sortfunc) + return gspec[-1].bind_entry(entry, metadata) + + aspec = [ent for ent in self.entries.values() if ent.specific.all] + if aspec: + return aspec[0].bind_entry(entry, metadata) + + raise PluginExecutionError + diff --git a/testsuite/TestPlugin.py b/testsuite/TestPlugin.py new file mode 100644 index 000000000..66d51a694 --- /dev/null +++ b/testsuite/TestPlugin.py @@ -0,0 +1,122 @@ +import os, Bcfg2.Server.Core, gamin, lxml.etree +from Bcfg2.Server.Plugin import EntrySet + +class es_testtype(object): + def __init__(self, name, properties, specific): + self.name = name + self.properties = properties + self.specific = specific + self.handled = 0 + self.built = 0 + + def handle_event(self, event): + self.handled += 1 + + def bind_entry(self, entry, metadata): + entry.set('bound', '1') + entry.set('name', self.name) + self.built += 1 + +class metadata(object): + def __init__(self, hostname): + self.hostname = hostname + self.groups = ['base', 'debian'] + +#FIXME add test_specific + +class test_entry_set(object): + def __init__(self): + self.dirname = '/tmp/estest-%d' % os.getpid() + os.path.isdir(self.dirname) or os.mkdir(self.dirname) + self.metadata = metadata('testhost') + self.es = EntrySet('template', self.dirname, None, es_testtype) + self.e = Bcfg2.Server.Core.GaminEvent(1, 'template', + gamin.GAMExists) + def test_init(self): + es = self.es + e = self.e + e.action = 'exists' + es.handle_event(e) + es.handle_event(e) + assert len(es.entries) == 1 + assert es.entries.values()[0].handled == 2 + e.action = 'changed' + es.handle_event(e) + assert es.entries.values()[0].handled == 3 + + + def test_info(self): + '''test info and info.xml handling''' + es = self.es + e = self.e + dirname = self.dirname + metadata = self.metadata + + # test 'info' handling + assert es.metadata['group'] == 'root' + self.mk_info(dirname) + e.filename = 'info' + e.action = 'exists' + es.handle_event(e) + assert es.metadata['group'] == 'sys' + e.action = 'deleted' + es.handle_event(e) + assert es.metadata['group'] == 'root' + + # test 'info.xml' handling + assert es.infoxml == None + self.mk_info_xml(dirname) + e.filename = 'info.xml' + e.action = 'exists' + es.handle_event(e) + assert es.infoxml + e.action = 'deleted' + es.handle_event(e) + assert es.infoxml == None + + + def test_file_building(self): + '''test file building''' + self.test_init() + ent = lxml.etree.Element('foo') + self.es.bind_entry(ent, self.metadata) + print self.es.entries.values()[0] + assert self.es.entries.values()[0].built == 1 + + + def test_host_specific_file_building(self): + '''add a host-specific template and build it''' + self.e.filename = 'template.H_%s' % self.metadata.hostname + self.e.action = 'exists' + self.es.handle_event(self.e) + assert len(self.es.entries) == 1 + ent = lxml.etree.Element('foo') + self.es.bind_entry(ent, self.metadata) + # FIXME need to test that it built the _right_ file here + + + + def test_deletion(self): + '''test deletion of files''' + self.test_init() + self.e.filename = 'template' + self.e.action = 'deleted' + self.es.handle_event(self.e) + assert len(self.es.entries) == 0 + + # TODO - how to clean up the temp dir & files after tests done? + + def mk_info(self, dir): + i = open("%s/info" % dir, 'w') + i.write('owner: root\n') + i.write('group: sys\n') + i.write('perms: 0600\n') + i.close + + def mk_info_xml(self, dir): + i = open("%s/info.xml" % dir, 'w') + i.write('<FileInfo><Info owner="root" group="other" perms="0600" /></FileInfo>\n') + i.close + + + |