diff options
Diffstat (limited to 'src/sbin/bcfg2-admin')
-rwxr-xr-x | src/sbin/bcfg2-admin | 643 |
1 files changed, 31 insertions, 612 deletions
diff --git a/src/sbin/bcfg2-admin b/src/sbin/bcfg2-admin index dab67bf96..d7ad02d3a 100755 --- a/src/sbin/bcfg2-admin +++ b/src/sbin/bcfg2-admin @@ -2,588 +2,27 @@ '''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 binascii, time +import Bcfg2.Server.Core, Bcfg2.Logging log = logging.getLogger('bcfg-admin') -colors = ['steelblue1', 'chartreuse', 'gold', 'magenta', 'indianred1', 'limegreen', - 'orange1', 'lightblue2', 'green1', 'blue1', 'yellow1', 'darkturquoise', - 'gray66'] +import Bcfg2.Server.Admin -usage = ''' -bcfg2-admin [options] -fingerprint - print the server certificate fingerprint -init - initialize the bcfg2 repository - (this is interactive; only run once) -pull <client> <entry type> <entry name> - - mine statistics for entry information -minestruct <client> - - 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 <config1.xml> <config2.xml> - - compare two configurations for differences -''' +def mode_import(modename): + '''Load Bcfg2.Server.Admin.<mode>''' + modname = modename.capitalize() + mod = getattr(__import__("Bcfg2.Server.Admin.%s" % + (modname)).Server.Admin, modname) + return getattr(mod, modname) -config = ''' -[server] -repository = %s -structures = Bundler,Base -generators = SSHbase,Cfg,Pkgmgr,Rules - -[statistics] -sendmailpath = /usr/sbin/sendmail -database_engine = sqlite3 -# 'postgresql', 'mysql', 'mysql_old', 'sqlite3' or 'ado_mssql'. -database_name = -# Or path to database file if using sqlite3. -#<repository>/etc/brpt.sqlite is default path if left empty -database_user = -# Not used with sqlite3. -database_password = -# Not used with sqlite3. -database_host = -# Not used with sqlite3. -database_port = -# Set to empty string for default. Not used with sqlite3. -web_debug = True - - -[communication] -protocol = xmlrpc/ssl -password = %s -key = %s/bcfg2.key - -[components] -bcfg2 = %s -''' - -groups = ''' -<Groups version='3.0'> - <Group profile='true' public='false' default='true' name='basic'> - <Group name='%s'/> - </Group> - <Group name='ubuntu'/> - <Group name='debian'/> - <Group name='freebsd'/> - <Group name='gentoo'/> - <Group name='redhat'/> - <Group name='suse'/> - <Group name='mandrake'/> - <Group name='solaris'/> -</Groups> -''' -clients = ''' -<Clients version="3.0"> - <Client profile="basic" pingable="Y" pingtime="0" name="%s"/> -</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)) - - try: - open(cfile,"w").write(config % ( repo, password, keypath, uri )) - except: - err_exit("Failed to write bcfg2 config file to %s" % cfile) - - #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', '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 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 <old> <new>" - 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 - sxpath = ".//Statistics[@state='%s']/Bad/ConfigFile[@name='%s']/../.." % (state, ename) - sentries = sdata.xpath(sxpath) - print "Found %d entries for %s:%s:%s" % \ - (len(sentries), client, etype, ename) - if not len(sentries): - raise SystemExit, 1 - maxtime = max([time.strptime(stat.get('time')) for stat in sentries]) - 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: - err_exit("Could not find state data for entry; 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 - 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 - else: - if not m_updates: - print "having trouble processing entry. Entry is:" - print lxml.etree.tostring(cfentry) - raise SystemExit, 1 - else: - data = False - diff = False - - if diff: - print "Located diff:\n %s" % diff - elif data: - print "Found full (binary) file data" - if m_updates: - print "Found metadata updates" - - if not diff and not data and not m_updates: - err_exit("Failed to locate diff or full data or metadata updates\nStatistics entry was:\n%s" % lxml.etree.tostring(cfentry)) - - 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, data, m_updates) - 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) + Bcfg2.Logging.setup_logging('bcfg2-admin', to_console=True) # Some sensible defaults configfile = "/etc/bcfg2.conf" - Repopath = "" try: - opts, args = getopt.getopt(sys.argv[1:], 'hC:R:', ['help', 'configfile=', 'repopath=']) + opts, args = getopt.getopt(sys.argv[1:], 'hC:', ['help', 'configfile=']) except getopt.GetoptError, msg: print msg raise SystemExit(1) @@ -592,51 +31,31 @@ if __name__ == '__main__': print usage raise SystemExit(1) + help = False # First get the options... for opt, arg in opts: if opt in ("-h", "--help"): - print usage - raise SystemExit(1) + help = True 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) + modes = [x.lower() for x in Bcfg2.Server.Admin.__all__] + modes.remove('mode') + if help or len(args) < 1 or args[0] == 'help': + if len(args) > 1: + args = [args[1], args[0]] + else: + for mod in [mode_import(mode) for mode in modes]: + print mod.__shorthelp__ + raise SystemExit(0) + if args[0] in modes: + modname = args[0].capitalize() + try: + mode_cls = mode_import(modname) + except ImportError, e: + log.error("Failed to load admin mod %s: %s" % (modname, e)) + mode = mode_cls(configfile) + mode(args[1:]) + else: + print "unknown mode %s" % args[0] - elif args[0] == 'client': - if len(args) < 4: - print usage - raise SystemExit(1) - do_client(Repopath, args[1:]) - else: - print usage |