#!/usr/local/bin/python2.3
'''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 <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= secure= location=
tidy - clean up unused files from repo
compare <config1.xml> <config2.xml> - 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 = '''
<Groups version='3.0'>
<Group profile='true' public='false' name='basic'>
<Group name='%s'/>
</Group>
<Group name='ubuntu' toolset='debian'/>
<Group name='debian' toolset='debian'/>
<Group name='freebsd' toolset='freebsd'/>
<Group name='gentoo' toolset='gentoo'/>
<Group name='redhat' toolset='rh'/>
<Group name='suse' toolset='rh'/>
<Group name='mandrake' toolset='rh'/>
<Group name='solaris' toolset='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))
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 <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
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', '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
syntax highlighted by Code2HTML, v. 0.9.1