#!/usr/bin/python '''bcfg2-admin is a script that helps to administrate a bcfg2 deployment''' import difflib, logging, lxml.etree, os, re, socket, sys, ConfigParser import Bcfg2.Server.Core, Bcfg2.Logging log = logging.getLogger('bcfg-admin') usage = ''' bcfg2-admin [options] init - initialize the bcfg2 repository( this is interactive; only run once ) mineentry - mine statistics for entry information minestruct - mine statistics for extra entries ''' 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 = /etc/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 ''' def err_exit(msg): print msg raise SystemExit, 1 #build bcfg2.conf file def initialize_repo(): '''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 open("/etc/bcfg2.conf","w").write(config % ( repo, password, 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 /etc/bcfg2.key -keyout /etc/bcfg2.key') try: os.chmod('/etc/bcfg2.key','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 'abcdefg': 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' 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(diff.split('\n'), 1)) print "writing file, %s" % path open(path, 'w').write(newdata) def get_repo_path(): '''return repository path''' cfile = '/etc/bcfg2.conf' cfp = ConfigParser.ConfigParser() cfp.read(cfile) return cfp.get('server', 'repository') def load_stats(client): repo = get_repo_path() 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] def do_pull(client, etype, ename): '''Make currently recorded client state correct for entry''' cfile = '/etc/bcfg2.conf' sdata = load_stats(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") # fail for unsupported etypes if etype not in ['ConfigFile', 'Package']: err_exit("Unsupported entry type %s" % (etype)) 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 location in repo if etype == 'ConfigFile': rversion = lxml.etree.Element('ConfigFile', name=ename) bcore.Bind(rversion, m) current = rversion.text diff = entry[0].get('current_diff')[1:-1] basefile = [frag for frag in \ bcore.plugins['Cfg'].entries[ename].fragments \ if frag.applies(m)][-1] if ".H_%s" % (m.hostname) in basefile.name: answer = raw_input("Found host-specific file %s; Should it be updated (n/Y): ") if answer in 'Yy': update_file(basefile.name, diff) else: raise SystemExit, 1 else: # there are two possibilities msg = "Should this change apply to this host of all hosts effected by file %s? (N/y): " % (basefile.name) choice = raw_input(msg) if choice in 'Yy': newname = basefile.name else: # figure out host-specific filename if '.G_' in basefile.name: idx = basefile.name.find(".G_") newname = basefile.name[:idx] + ".H_%s" % (m.hostname) else: newname = basefile.name + ".H_%s" % (m.hostname) print "This file will be installed as file %s" % newname if raw_input("Should it be installed? (N/y): ") in 'Yy': update_file(newname, diff) else: err_exit("Don't support entry type %s yet" % etype) # svn commit if running under svn def do_minestruct(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(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(args): '''Clean up unused or unusable files from the repository''' hostmatcher = re.compile('.*\.H_(\S+)$') repo = get_repo_path() 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 if __name__ == '__main__': Bcfg2.Logging.setup_logging('bcfg2-admin', to_console=True) if sys.argv[1] == "init": initialize_repo() elif sys.argv[1] == 'pull': if len(sys.argv) != 5: print usage raise SystemExit, 1 do_pull(sys.argv[2], sys.argv[3], sys.argv[4]) elif sys.argv[1] == 'minestruct': do_minestruct(sys.argv[2:]) elif sys.argv[1] == 'tidy': do_tidy(sys.argv[2:]) else: print usage