#!/usr/bin/env python '''bcfg2-admin is a script that helps to administrate a bcfg2 deployment''' import getopt, difflib, logging, lxml.etree, os, popen2, re, socket, sys, ConfigParser import Bcfg2.Server.Core, Bcfg2.Logging, Bcfg2.tlslite.api import xml.sax.saxutils log = logging.getLogger('bcfg-admin') colors = ['steelblue1', 'chartreuse', 'gold', 'magenta', 'indianred1', 'limegreen', 'orange1', 'lightblue2', 'green1', 'blue1', 'yellow1', 'darkturquoise', 'gray66'] usage = ''' bcfg2-admin [options] fingerprint - print the server certificate fingerprint init - initialize the bcfg2 repository (this is interactive; only run once) pull - mine statistics for entry information minestruct - mine statistics for extra entries viz [--includehosts] [--includebundles] [--includekey] [-o output.png] [--raw] client add name= profile= uuid= password= address= secure= location= tidy - clean up unused files from repo compare - compare two configurations for differences ''' config = ''' [server] repository = %s structures = Bundler,Base generators = SSHbase,Cfg,Pkgmgr,Svcmgr,Rules [statistics] sendmailpath = /usr/sbin/sendmail [communication] protocol = xmlrpc/ssl password = %s key = %s/bcfg2.key [components] bcfg2 = %s ''' groups = ''' ''' clients = ''' ''' prompt = ''' please select which operating system your machine is running: a. Redhat/Fedora/RHEL/RHAS/Centos b. SUSE/SLES c. Mandrake d. Debian e. Ubuntu f. Solaris g. Gentoo h. FreeBSD ''' def err_exit(emsg): print emsg raise SystemExit, 1 #build bcfg2.conf file def initialize_repo(cfile): '''Setup a new repo''' repo = raw_input( "location of bcfg2 repository [/var/lib/bcfg2]: " ) if repo == '': repo = '/var/lib/bcfg2' password = '' while ( password == '' ): password = raw_input( "please provide the password used for communication verification: " ) #get the hostname server = "https://%s:6789" % socket.getfqdn() uri = raw_input( "please provide the server location[%s]: " % server) if uri == '': uri = server #guess path of ssl key file keypath = os.path.dirname(os.path.abspath(cfile)) open(cfile,"w").write(config % ( repo, password, keypath, uri )) #generate the ssl key print "Now we will generate the ssl key used for secure communitcation" os.popen('openssl req -x509 -nodes -days 1000 -newkey rsa:1024 -out %s/bcfg2.key -keyout %s/bcfg2.key' % (keypath, keypath)) try: os.chmod('%s/bcfg2.key'% keypath,'0600') except: pass #create the repo dirs for subdir in ['SSHbase', 'Cfg', 'Pkgmgr', 'Svcmgr', 'Rules', 'etc', 'Metadata', 'Base', 'Bundler']: path = "%s/%s" % (repo, subdir) newpath = '' for subdir in path.split('/'): newpath = newpath + subdir + '/' try: os.mkdir(newpath) except: pass #create the groups.xml file selection = '' while ( selection == '' ): print prompt selection = raw_input(" selection: ") if selection.lower() not in 'abcdefgh': selection = '' if selection.lower() == 'a': selection = 'redhat' elif selection.lower() == 'b': selection = 'suse' elif selection.lower() == 'c': selection = 'mandrake' elif selection.lower() == 'd': selection = 'debian' elif selection.lower() == 'e': selection = 'ubuntu' elif selection.lower() == 'f': selection = 'solaris' elif selection.lower() == 'g': selection = 'gentoo' elif selection.lower() == 'h': selection = 'freebsd' open("%s/Metadata/groups.xml"%repo, "w").write(groups%selection) #now the clients file open("%s/Metadata/clients.xml"%repo, "w").write(clients%socket.getfqdn()) print "Repository created successfuly in %s" % (repo) def update_file(path, diff): '''Update file at path using diff''' newdata = '\n'.join(difflib.restore(xml.sax.saxutils.unescape(diff).split('\n'), 1)) print "writing file, %s" % path open(path, 'w').write(newdata) def get_repo_path(cfile='/etc/bcfg2.conf'): '''return repository path''' cfp = ConfigParser.ConfigParser() cfp.read(cfile) return cfp.get('server', 'repository') def load_stats(repo, client): stats = lxml.etree.parse("%s/etc/statistics.xml" % (repo)) hostent = stats.xpath('//Node[@name="%s"]' % client) if not hostent: err_exit("Could not find stats for client %s" % (client)) return hostent[0] important = {'Package':['name', 'version'], 'Service':['name', 'status'], 'Directory':['name', 'owner', 'group', 'perms'], 'SymLink':['name', 'to'], 'ConfigFile':['name', 'owner', 'group', 'perms'], 'Permissions':['name', 'perms'], 'PostInstall':['name']} def compare(new, old): for child in new.getchildren(): equiv = old.xpath('%s[@name="%s"]' % (child.tag, child.get('name'))) if not important.has_key(child.tag): print "tag type %s not handled" % (child.tag) continue if len(equiv) == 0: print "didn't find matching %s %s" % (child.tag, child.get('name')) continue elif len(equiv) >= 1: if child.tag == 'ConfigFile': if child.text != equiv[0].text: print " %s %s contents differ" \ % (child.tag, child.get('name')) continue noattrmatch = [field for field in important[child.tag] if \ child.get(field) != equiv[0].get(field)] if not noattrmatch: new.remove(child) old.remove(equiv[0]) else: print " %s %s attributes %s do not match" % \ (child.tag, child.get('name'), noattrmatch) if len(old.getchildren()) == 0 and len(new.getchildren()) == 0: return True if new.tag == 'Independant': name = 'Base' else: name = new.get('name') both = [] oldl = ["%s %s" % (entry.tag, entry.get('name')) for entry in old] newl = ["%s %s" % (entry.tag, entry.get('name')) for entry in new] for entry in newl: if entry in oldl: both.append(entry) newl.remove(entry) oldl.remove(entry) for entry in both: print " %s differs (in bundle %s)" % (entry, name) for entry in oldl: print " %s only in old configuration (in bundle %s)" % (entry, name) for entry in newl: print " %s only in new configuration (in bundle %s)" % (entry, name) return False def do_compare(cargs): '''run file comparison''' if '-r' in cargs: cargs.remove('-r') (oldd, newd) = args (old, new) = [os.listdir(spot) for spot in args] for item in old: print "Entry:", item state = do_compare([oldd + '/' + item, newd + '/' + item]) new.remove(item) if state: print "Entry:", item, "good" else: print "Entry:", item, "bad" if new: print "new has extra entries", new return try: (old, new) = cargs except IndexError: print "Usage: bcfg2-admin compare " raise SystemExit try: new = lxml.etree.parse(new).getroot() except IOError: print "Failed to read %s" % (new) raise SystemExit, 1 try: old = lxml.etree.parse(old).getroot() except IOError: print "Failed to read %s" % (old) raise SystemExit, 1 for src in [new, old]: for bundle in src.findall('./Bundle'): if bundle.get('name')[-4:] == '.xml': bundle.set('name', bundle.get('name')[:-4]) rcs = [] for bundle in new.findall('./Bundle'): equiv = old.xpath('Bundle[@name="%s"]' % (bundle.get('name'))) if len(equiv) == 0: print "couldnt find matching bundle for %s" % bundle.get('name') continue if len(equiv) == 1: if compare(bundle, equiv[0]): new.remove(bundle) old.remove(equiv[0]) rcs.append(True) else: rcs.append(False) else: print "dunno what is going on for bundle %s" % (bundle.get('name')) i1 = new.find('./Independant') i2 = old.find('./Independant') if compare(i1, i2): new.remove(i1) old.remove(i2) else: rcs.append(False) return not False in rcs def do_fingerprint(cfile): '''calculate key fingerprint''' cf = ConfigParser.ConfigParser() cf.read([cfile]) keypath = cf.get('communication', 'key') x509 = Bcfg2.tlslite.api.X509() x509.parse(open(keypath).read()) print x509.getFingerprint() def do_pull(cfile, repopath, client, etype, ename): '''Make currently recorded client state correct for entry''' sdata = load_stats(repopath, client) if sdata.xpath('.//Statistics[@state="dirty"]'): state = 'dirty' else: state = 'clean' # need to pull entry out of statistics entry = sdata.xpath('.//Statistics[@state="%s"]/Bad/%s[@name="%s"]' % \ (state, etype, ename)) if not entry: err_exit("Could not find state data for entry; rerun bcfg2 on client system") diff = entry[0].get('current_diff') try: bcore = Bcfg2.Server.Core.Core({}, cfile) except Bcfg2.Server.Core.CoreInitError, msg: print "Core load failed because %s" % msg raise SystemExit, 1 [bcore.fam.Service() for x in range(10)] while bcore.fam.Service(): pass m = 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)] if len(glist) != 1: err_exit("Got wrong numbers of matching generators for entry:" \ + "%s" % ([g.__name__ for g in glist])) plugin = glist[0] try: plugin.AcceptEntry(m, 'ConfigFile', ename, diff) except Bcfg2.Server.Plugin.PluginExecutionError: err_exit("Configuration upload not supported by plugin %s" \ % (plugin.__name__)) # svn commit if running under svn def do_minestruct(repopath, argdata): '''Pull client entries into structure''' if len(argdata) != 1: err_exit("minestruct must be called with a client name") client = argdata[0] stats = load_stats(repopath, client) if len(stats.getchildren()) == 2: # client is dirty current = [ent for ent in stats.getchildren() if ent.get('state') == 'dirty'][0] else: current = stats.getchildren()[0] extra = current.find('Extra').getchildren() log.info("Found %d extra entries" % (len(extra))) log.info(["%s: %s" % (entry.tag, entry.get('name')) for entry in extra]) def do_tidy(repo, args): '''Clean up unused or unusable files from the repository''' hostmatcher = re.compile('.*\.H_(\S+)$') score = ([], []) # clean up unresolvable hosts in SSHbase for name in os.listdir("%s/SSHbase" % (repo)): if not hostmatcher.match(name): print "could not match name %s" % (name) else: hostname = hostmatcher.match(name).group(1) if hostname in score[0] + score[1]: continue try: socket.gethostbyname(hostname) score[0].append(hostname) except: print "could not resolve %s" % (hostname) score[1].append(hostname) for name in os.listdir("%s/SSHbase" % (repo)): if not hostmatcher.match(name): print "could not match name %s" % (name) else: if hostmatcher.match(name).group(1) in score[1]: if '-f' in args: os.unlink("%s/SSHbase/%s" % (repo, name)) else: answer = raw_input("Unlink file %s? [yN] " % name) if answer in ['y', 'Y']: os.unlink("%s/SSHbase/%s" % (repo, name)) # clean up file~ # clean up files without parsable names in Cfg def do_viz(repopath, myargs): '''Build visualization of groups file''' # First get options to the 'viz' subcommand try: opts, args = getopt.getopt(myargs, 'rhbko:', ['raw', 'includehosts', 'includebundles', 'includekey', 'outfile=']) except getopt.GetoptError, msg: print msg raise SystemExit, 1 options = [] for opt, arg in opts: if opt in ("-r", "--raw"): options.append("raw") elif opt in ("-h", "--includehosts"): options.append("hosts") elif opt in ("-b", "--includebundles"): options.append("bundles") elif opt in ("-k", "--includekey"): options.append("key") elif opt in ("-o", "--outfile"): options.append("outfile") outputfile = arg groupdata = lxml.etree.parse(repopath + '/Metadata/groups.xml') groupdata.xinclude() groups = groupdata.getroot() if 'raw' in options: dotpipe = popen2.Popen4("dd bs=4M 2>/dev/null") else: dotpipe = popen2.Popen4("dot -Tpng") categories = {'default':'grey83'} instances = {} egroups = groups.findall("Group") + groups.findall('.//Groups/Group') for group in egroups: if group.get('category', False): if not categories.has_key(group.get('category')): categories[group.get('category')] = colors.pop() try: dotpipe.tochild.write("digraph groups {\n") except: print "write to dot process failed. Is graphviz installed?" raise SystemExit, 1 dotpipe.tochild.write('\trankdir="LR";\n') if 'hosts' in options: clients = lxml.etree.parse(repopath + '/Metadata/clients.xml').getroot() for client in clients.findall('Client'): if instances.has_key(client.get('profile')): instances[client.get('profile')].append(client.get('name')) else: instances[client.get('profile')] = [client.get('name')] for profile, clist in instances.iteritems(): clist.sort() dotpipe.tochild.write('''\t"%s-instances" [ label="%s", shape="record" ];\n''' % (profile, '|'.join(clist))) dotpipe.tochild.write('''\t"%s-instances" -> "group-%s";\n''' % (profile, profile)) if 'bundles' in options: bundles = [] [bundles.append(bund.get('name')) for bund in groups.findall('.//Bundle') if bund.get('name') not in bundles] bundles.sort() for bundle in bundles: dotpipe.tochild.write('''\t"bundle-%s" [ label="%s", shape="septagon"];\n''' % (bundle, bundle)) gseen = [] for group in egroups: color = categories[group.get('category', 'default')] if group.get('profile', 'false') == 'true': style = "filled, bold" else: style = "filled" gseen.append(group.get('name')) dotpipe.tochild.write('\t"group-%s" [label="%s", style="%s", fillcolor=%s];\n' % (group.get('name'), group.get('name'), style, color)) if 'bundles' in options: for bundle in group.findall('Bundle'): dotpipe.tochild.write('\t"group-%s" -> "bundle-%s";\n' % (group.get('name'), bundle.get('name'))) for group in egroups: for parent in group.findall('Group'): if parent.get('name') not in gseen: dotpipe.tochild.write('\t"group-%s" [label="%s", style="filled", fillcolor="grey83"];\n' % (parent.get('name'), parent.get('name'))) gseen.append(parent.get("name")) dotpipe.tochild.write('\t"group-%s" -> "group-%s" ;\n' % (group.get('name'), parent.get('name'))) if 'key' in options: dotpipe.tochild.write("\tsubgraph cluster_key {\n") dotpipe.tochild.write('''\tstyle="filled";\n''') dotpipe.tochild.write('''\tcolor="lightblue";\n''') dotpipe.tochild.write('''\tBundle [ shape="septagon" ];\n''') dotpipe.tochild.write('''\tGroup [shape="ellipse"];\n''') dotpipe.tochild.write('''\tProfile [style="bold", shape="ellipse"];\n''') dotpipe.tochild.write('''\tHblock [label="Host1|Host2|Host3", shape="record"];\n''') for category in categories: dotpipe.tochild.write('''\t''' + category + ''' [label="''' + category + \ '''", shape="record", style="filled", fillcolor=''' + \ categories[category] + '''];\n''') dotpipe.tochild.write('''\tlabel="Key";\n''') dotpipe.tochild.write("\t}\n") dotpipe.tochild.write("}\n") dotpipe.tochild.close() data = dotpipe.fromchild.read() if 'outfile' in options: output = open(outputfile, 'w').write(data) else: print data def do_client(repopath, args): '''Do things with clients''' tree = lxml.etree.parse(repopath + '/Metadata/clients.xml') root = tree.getroot() if args[0] == 'add': # Adding a node print "Adding client..." element = lxml.etree.Element("Client") for i in args[1:]: attr, val = i.split('=', 1) if not(attr in ['name', 'profile', 'uuid', 'password', 'address', 'secure', 'location']): print "Attribute %s unknown" % attr raise SystemExit, 1 element.attrib[attr] = val root.append(element) elif args[0] in ['delete', 'remove']: # Removing a node print "Removing" tree.write(repopath + '/Metadata/clients.xml') print "Done" if __name__ == '__main__': Bcfg2.Logging.setup_logging('bcfg2-admin', to_console=False) # Some sensible defaults configfile = "/etc/bcfg2.conf" Repopath = "" try: opts, args = getopt.getopt(sys.argv[1:], 'hC:R:', ['help', 'configfile=', 'repopath=']) except getopt.GetoptError, msg: print msg raise SystemExit, 1 if not args: print usage raise SystemExit, 1 # First get the options... for opt, arg in opts: if opt in ("-h", "--help"): print usage raise SystemExit, 1 if opt in ("-C", "--configfile"): configfile = arg if opt in ("-R", "--repopath"): Repopath = arg # ...then do something with the other arguments if Repopath == '' and 'init' not in args: Repopath = get_repo_path(configfile) if len(args) < 1: print usage elif args[0] == "init": initialize_repo(configfile) elif args[0] == 'pull': if len(args) != 4: print usage raise SystemExit, 1 do_pull(configfile, Repopath, args[1], args[2], args[3]) elif args[0] == 'minestruct': do_minestruct(Repopath, args[1:]) elif args[0] == 'tidy': do_tidy(Repopath, args[1:]) elif args[0] == 'viz': do_viz(Repopath, args[1:]) elif args[0] == 'compare': do_compare(args[1:]) elif args[0] == 'fingerprint': do_fingerprint(configfile) elif args[0] == 'client': if len(args) < 4: print usage raise SystemExit, 1 do_client(Repopath, args[1:]) else: print usage