diff options
Diffstat (limited to 'func/overlord/client.py')
-rwxr-xr-x | func/overlord/client.py | 295 |
1 files changed, 295 insertions, 0 deletions
diff --git a/func/overlord/client.py b/func/overlord/client.py new file mode 100755 index 0000000..3c60148 --- /dev/null +++ b/func/overlord/client.py @@ -0,0 +1,295 @@ +#!/usr/bin/python + +## +## func command line interface & client lib +## +## Copyright 2007, Red Hat, Inc +## Michael DeHaan <mdehaan@redhat.com> +## +AUTHORS +## +## This software may be freely redistributed under the terms of the GNU +## general public license. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software +## Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +## + +import optparse +import sys +import glob +import pprint + +from func.commonconfig import CMConfig +from func.config import read_config, CONFIG_FILE +import sslclient + +import command + +# =================================== +# defaults +# TO DO: some of this may want to come from config later + +DEFAULT_PORT = 51234 +FUNC_USAGE = "Usage: %s [ --help ] [ --verbose ] target.example.org module method arg1 [...]" + +# =================================== + +class CommandAutomagic(object): + """ + This allows a client object to act as if it were one machine, when in + reality it represents many. + """ + + def __init__(self, clientref, base): + self.base = base + self.clientref = clientref + + def __getattr__(self,name): + base2 = self.base[:] + base2.append(name) + return CommandAutomagic(self.clientref, base2) + + def __call__(self, *args): + if not self.base: + raise AttributeError("something wrong here") + if len(self.base) < 2: + raise AttributeError("no method called: %s" % ".".join(self.base)) + module = self.base[0] + method = ".".join(self.base[1:]) + return self.clientref.run(module,method,args) + +# =================================== + +class Client(object): + + def __init__(self, server_spec, port=DEFAULT_PORT, interactive=False, + verbose=False, noglobs=False, config=None): + """ + Constructor. + @server_spec -- something like "*.example.org" or "foosball" + @port -- is the port where all funcd processes should be contacted + @verbose -- whether to print unneccessary things + @noglobs -- specifies server_spec is not a glob, and run should return single values + @config -- optional config object + """ + self.config = config + if config is None: + self.config = read_config(CONFIG_FILE, CMConfig) + + self.server_spec = server_spec + self.port = port + self.verbose = verbose + self.interactive = interactive + self.noglobs = noglobs + self.servers = self.expand_servers(self.server_spec) + + # default cert/ca/key is the same as the certmaster ca - need to + # be able to change that on the cli + self.key = '%s/funcmaster.key' % self.config.cadir + self.cert = '%s/funcmaster.crt' % self.config.cadir + # yes, they're the same, that's the point + self.ca = '%s/funcmaster.crt' % self.config.cadir + + # ----------------------------------------------- + + def expand_servers(self,spec): + """ + Given a regex/blob of servers, expand to a list + of server ids. + """ + + if self.noglobs: + return [ "https://%s:%s" % (spec, self.port) ] + + all_hosts = [] + all_certs = [] + seperate_gloobs = spec.split(";") + for each_gloob in seperate_gloobs: + actual_gloob = "%s/%s.cert" % (self.config.certroot, each_gloob) + certs = glob.glob(actual_gloob) + for cert in certs: + all_certs.append(cert) + host = cert.replace(self.config.certroot,"")[1:-5] + all_hosts.append(host) + + all_urls = [] + for x in all_hosts: + all_urls.append("https://%s:%s" % (x, self.port)) + + if self.verbose and len(all_urls) == 0: + sys.stderr.write("no hosts matched\n") + + return all_urls + + # ----------------------------------------------- + + def __getattr__(self, name): + """ + This getattr allows manipulation of the object as if it were + a XMLRPC handle to a single machine, when in reality it is a handle + to an unspecified number of machines. + + So, it enables stuff like this: + + Client("*.example.org").yum.install("foo") + + # WARNING: any missing values in Client's source will yield + # strange errors with this engaged. Be aware of that. + """ + + return CommandAutomagic(self, [name]) + + # ----------------------------------------------- + + def run(self, module, method, args): + """ + Invoke a remote method on one or more servers. + Run returns a hash, the keys are server names, the values are the + returns. + + The returns may include exception objects. + If Client() was constructed with noglobs=True, the return is instead + just a single value, not a hash. + """ + + results = {} + + for server in self.servers: + + conn = sslclient.FuncServer(server, self.key, self.cert, self.ca ) + # conn = xmlrpclib.ServerProxy(server) + + if self.interactive: + sys.stderr.write("on %s running %s %s (%s)\n" % (server, + module, method, ",".join(args))) + + # FIXME: support userland command subclassing only if a module + # is present, otherwise run as follows. -- MPD + + try: + # thats some pretty code right there aint it? -akl + # we can't call "call" on s, since thats a rpc, so + # we call gettatr around it. + meth = "%s.%s" % (module, method) + retval = getattr(conn, meth)(*args[:]) + if self.interactive: + pprint.pprint(retval) + except Exception, e: + retval = e + if self.interactive: + sys.stderr.write("remote exception on %s: %s\n" % + (server, str(e))) + + if self.noglobs: + return retval + else: + left = server.rfind("/")+1 + right = server.rfind(":") + server_name = server[left:right] + results[server_name] = retval + + return results + + # ----------------------------------------------- + + def cli_return(self,results): + """ + As the return code list could return strings and exceptions + and all sorts of crazy stuff, reduce it down to a simple + integer return. It may not be useful but we need one. + """ + numbers = [] + for x in results.keys(): + # faults are the most important + if type(x) == Exception: + return -911 + # then pay attention to numbers + if type(x) == int: + numbers.append(x) + + # if there were no numbers, assume 0 + if len(numbers) == 0: + return 0 + + # if there were numbers, return the highest + # (presumably the worst error code + max = -9999 + for x in numbers: + if x > max: + max = x + return max + +# =================================================================== + +class Call(command.Command): + name = "call" + useage = "call nodule method name arg1 arg2..." + def addOptions(self): + self.parser.add_option("-v", "--verbose", dest="verbose", + action="store_true") + self.parser.add_option("-p", "--port", dest="port", + default=DEFAULT_PORT) + + def handleOptions(self, options): + self.options = options + + self.verbose = options.verbose + self.port = options.port + # I'm not really a fan of the "module methodname" approach + # but we'll keep it for now -akl + + def do(self, args): + + # I'm not really a fan of the "module methodname" approach + # but we'll keep it for now -akl + + self.server_spec = args[0] + self.module = args[1] + self.method = args[2] + self.method_args = args[3:] + + client = Client(self.server_spec,port=self.port,interactive=True, + verbose=self.verbose, config=self.config) + results = client.run(self.module, self.method, self.method_args) + + # TO DO: add multiplexer support + # probably as a higher level module. + + return client.cli_return(results) + +class FuncCommandLine(command.Command): + name = "client" + useage = "func is the commandline interface to a func minion" + + subCommandClasses = [Call] + + def __init__(self): + + command.Command.__init__(self) + + def do(self, args): + pass + + def addOptions(self): + self.parser.add_option('', '--version', action="store_true", + help="show version information") + self.parser.add_option("--list-minions", dest="list_minions", + action="store_true", help="list all available minions") + + def handleOptions(self, options): + if options.version: + #FIXME + print "version is NOT IMPLEMENTED YET" + if options.list_minions: + self.list_minions() + + sys.exit(0) # stop execution + + def list_minions(self): + print "Minions:" + gloob = "%s/%s.cert" % (self.config.certroot, "*") + certs = glob.glob(gloob) + for cert in certs: + host = cert.replace(self.config.certroot, "")[1:-5] + print " %s" % host |