From 9e31f1dd76c29d99fb5a16f0a2d6752cf5ead1c9 Mon Sep 17 00:00:00 2001 From: Narayan Desai Date: Mon, 24 Mar 2008 04:15:41 +0000 Subject: Rework bcfg2-admin pull - forward port Cfg and SSHbase support - reimplement admin mode - add verbose flag, and implement initial interactive mode, also force mode git-svn-id: https://svn.mcs.anl.gov/repos/bcfg/trunk/bcfg2@4446 ce84e21b-d406-0410-9b95-82705330c041 --- src/lib/Server/Admin/Pull.py | 128 +++++++++++++++++++++++++------------------ 1 file changed, 76 insertions(+), 52 deletions(-) (limited to 'src/lib/Server/Admin/Pull.py') diff --git a/src/lib/Server/Admin/Pull.py b/src/lib/Server/Admin/Pull.py index 1071b5100..ede4271fb 100644 --- a/src/lib/Server/Admin/Pull.py +++ b/src/lib/Server/Admin/Pull.py @@ -1,89 +1,110 @@ -import binascii, lxml.etree, time +import binascii, difflib, getopt, lxml.etree, time, ConfigParser import Bcfg2.Server.Admin class Pull(Bcfg2.Server.Admin.Mode): '''Pull mode retrieves entries from clients and integrates the information into the repository''' - __shorthelp__ = 'bcfg2-admin pull ' + __shorthelp__ = 'bcfg2-admin pull [-v] [-f] [-I] ' __longhelp__ = __shorthelp__ + '\n\tIntegrate configuration information from clients into the server repository' - def __init__(self): - Bcfg2.Server.Admin.Mode.__init__(self) - + def __init__(self, configfile): + Bcfg2.Server.Admin.Mode.__init__(self, configfile) + cp = ConfigParser.ConfigParser() + cp.read([configfile]) + self.repo = cp.get('server', 'repository') + self.log = False + self.mode = 'interactive' + def __call__(self, args): Bcfg2.Server.Admin.Mode.__call__(self, args) - self.PullEntry(args[0], args[1], args[2]) + try: + opts, gargs = getopt.getopt(args, 'vfI') + except: + print self.__shorthelp__ + raise SystemExit(0) + for opt in opts: + if opt[0] == '-v': + self.log = True + elif opt[0] == '-f': + self.mode = 'force' + elif opt[0] == '-I': + self.mode == 'interactive' + self.PullEntry(gargs[0], gargs[1], gargs[2]) - def PullEntry(self, client, etype, ename): - '''Make currently recorded client state correct for entry''' - # FIXME Pull.py is _way_ too interactive + def BuildNewEntry(self, client, etype, ename): + '''construct a new full entry for given client/entry from statistics''' + new_entry = {'type':etype, 'name':ename} sdata = self.load_stats(client) - if sdata.xpath('.//Statistics[@state="dirty"]'): - state = 'dirty' - else: - state = 'clean' - # need to pull entry out of statistics - sxpath = ".//Statistics[@state='%s']/Bad/ConfigFile[@name='%s']/../.." % (state, ename) + # no entries if state != dirty + sxpath = ".//Statistics[@state='dirty']/Bad/ConfigFile[@name='%s']/../.." % \ + (ename) sentries = sdata.xpath(sxpath) if not len(sentries): self.errExit("Found %d entries for %s:%s:%s" % \ (len(sentries), client, etype, ename)) else: - print "Found %d entries for %s:%s:%s" % \ - (len(sentries), client, etype, ename) + if self.log: + print "Found %d entries for %s:%s:%s" % \ + (len(sentries), client, etype, ename) maxtime = max([time.strptime(stat.get('time')) for stat in sentries]) - print "Found entry from", time.strftime("%c", maxtime) + if self.log: + print "Found entry from", time.strftime("%c", maxtime) statblock = [stat for stat in sentries \ if time.strptime(stat.get('time')) == maxtime] entry = statblock[0].xpath('.//Bad/ConfigFile[@name="%s"]' % ename) if not entry: - self.errExit("Could not find state data for entry; rerun bcfg2 on client system") + self.errExit("Could not find state data for entry\n" \ + "rerun bcfg2 on client system") cfentry = entry[-1] badfields = [field for field in ['perms', 'owner', 'group'] \ if cfentry.get(field) != cfentry.get('current_' + field) and \ cfentry.get('current_' + field)] if badfields: - m_updates = dict([(field, cfentry.get('current_' + field)) \ - for field in badfields]) - print "got metadata_updates", m_updates - else: - m_updates = {} - - if 'current_bdiff' in cfentry.attrib: - data = False + for field in badfields: + new_entry[field] = cfentry.get('current_%s' % field) + # now metadata updates are in place + if 'current_bfile' in cfentry.attrib: + new_entry['text'] = binascii.a2b_base64(cfentry.get('current_bfile')) + elif 'current_bdiff' in cfentry.attrib: diff = binascii.a2b_base64(cfentry.get('current_bdiff')) - elif 'current_diff' in cfentry.attrib: - data = False - diff = cfentry.get('current_diff') - elif 'current_bfile' in cfentry.attrib: - data = binascii.a2b_base64(cfentry.get('current_bfile')) - diff = False + new_entry['text'] = '\n'.join(difflib.restore(diff.split('\n'), 1)) else: - if not m_updates: - self.errExit("having trouble processing entry. Entry is:\n" \ - + lxml.etree.tostring(cfentry)) - else: - data = False - diff = False + print "found no data::" + print lxml.etree.tostring(cfentry) + raise SystemExit(1) + return new_entry - if diff: - print "Located diff:\n %s" % diff - elif data: - print "Found full (binary) file data" - if m_updates: - print "Found metadata updates" + def Choose(self, choices): + '''Determine where to put pull data''' + if self.mode == 'interactive': + # FIXME improve bcfg2-admin pull interactive mode to add new entries + print "Plugin returned choice:" + if choices[0].all: + print " => global entry" + elif choices[0].group: + print " => group entry: %s (prio %d)" % (choices[0].group, choices[0].prio) + else: + print " => host entry: %s" % (choices[0].hostname) + if raw_input("Use this entry? [yN]: ") in ['y', 'Y']: + return choices[0] + return False + else: + # mode == 'force' + return choices[0] - if not diff and not data and not m_updates: - self.errExit("Failed to locate diff or full data or metadata updates\nStatistics entry was:\n%s" % lxml.etree.tostring(cfentry)) + def PullEntry(self, client, etype, ename): + '''Make currently recorded client state correct for entry''' + new_entry = self.BuildNewEntry(client, etype, ename) try: - bcore = Bcfg2.Server.Core.Core({}, self.configfile) + bcore = Bcfg2.Server.Core.Core(self.repo, [], ['Cfg', 'SSHbase', 'Metadata'], + 'foo', False) except Bcfg2.Server.Core.CoreInitError, msg: self.errExit("Core load failed because %s" % msg) - [bcore.fam.Service() for _ in range(10)] + [bcore.fam.Service() for _ in range(5)] while bcore.fam.Service(): pass - m = bcore.metadata.get_metadata(client) + meta = bcore.metadata.get_metadata(client) # find appropriate plugin in bcore glist = [gen for gen in bcore.generators if gen.Entries.get(etype, {}).has_key(ename)] @@ -92,8 +113,11 @@ class Pull(Bcfg2.Server.Admin.Mode): + "%s" % ([g.__name__ for g in glist])) plugin = glist[0] try: - plugin.AcceptEntry(m, 'ConfigFile', ename, diff, data, m_updates) + choices = plugin.AcceptChoices(new_entry, meta) + specific = self.Choose(choices) + if specific: + plugin.AcceptPullData(specific, new_entry, self.log) except Bcfg2.Server.Plugin.PluginExecutionError: self.errExit("Configuration upload not supported by plugin %s" \ % (plugin.__name__)) - # svn commit if running under svn + # FIXME svn commit if running under svn -- cgit v1.2.3-1-g7c22