summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/lib/Server/Plugin.py137
-rw-r--r--testsuite/TestPlugin.py122
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
+
+
+