summaryrefslogtreecommitdiffstats
path: root/src/sbin/bcfg2-admin
diff options
context:
space:
mode:
authorNarayan Desai <desai@mcs.anl.gov>2007-12-29 04:57:53 +0000
committerNarayan Desai <desai@mcs.anl.gov>2007-12-29 04:57:53 +0000
commit3610288cbcbf4d1adedefa03166cd77ee15aad96 (patch)
tree1588fb78ae1c6e51e4e4ea2dab46a006a3ece0d3 /src/sbin/bcfg2-admin
parent6bf7875bc299a1f81061782c8646c90972e06e5a (diff)
downloadbcfg2-3610288cbcbf4d1adedefa03166cd77ee15aad96.tar.gz
bcfg2-3610288cbcbf4d1adedefa03166cd77ee15aad96.tar.bz2
bcfg2-3610288cbcbf4d1adedefa03166cd77ee15aad96.zip
Refactor of bcfg2-admin (all modes moved to discrete modules in Bcfg2.Server.Admin
git-svn-id: https://svn.mcs.anl.gov/repos/bcfg/trunk/bcfg2@4125 ce84e21b-d406-0410-9b95-82705330c041
Diffstat (limited to 'src/sbin/bcfg2-admin')
-rwxr-xr-xsrc/sbin/bcfg2-admin643
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