diff options
Diffstat (limited to 'func/overlord/client.py')
-rwxr-xr-x | func/overlord/client.py | 336 |
1 files changed, 336 insertions, 0 deletions
diff --git a/func/overlord/client.py b/func/overlord/client.py new file mode 100755 index 0000000..cf1009c --- /dev/null +++ b/func/overlord/client.py @@ -0,0 +1,336 @@ +## +## 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 sys +import glob +import os + +from func.commonconfig import CMConfig +from func.config import read_config, CONFIG_FILE + +import sslclient + +import command +import groups +import func.forkbomb as forkbomb +import func.jobthing as jobthing +import func.utils as utils +from func.CommonErrors import * + +# =================================== +# 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, nforks=1): + self.base = base + self.clientref = clientref + self.nforks = nforks + + def __getattr__(self,name): + base2 = self.base[:] + base2.append(name) + return CommandAutomagic(self.clientref, base2, self.nforks) + + 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,nforks=self.nforks) + + +def get_groups(): + group_class = groups.Groups() + return group_class.get_groups() + + +def get_hosts_by_groupgoo(groups, groupgoo): + group_gloobs = groupgoo.split(':') + hosts = [] + for group_gloob in group_gloobs: + if not group_gloob[0] == "@": + continue + if groups.has_key(group_gloob[1:]): + hosts = hosts + groups[group_gloob[1:]] + else: + print "group %s not defined" % group_gloob + return hosts + +# =================================== +# this is a module level def so we can use it and isServer() from +# other modules with a Client class +def expand_servers(spec, port=51234, noglobs=None, verbose=None, just_fqdns=False): + """ + Given a regex/blob of servers, expand to a list + of server ids. + """ + + + # FIXME: we need to refactor expand_servers, it seems to do + # weird things, reload the config and groups config everytime it's + # called for one, which may or may not be bad... -akl + config = read_config(CONFIG_FILE, CMConfig) + + if noglobs: + if not just_fqdns: + return [ "https://%s:%s" % (spec, port) ] + else: + return spec + + group_dict = get_groups() + + all_hosts = [] + all_certs = [] + seperate_gloobs = spec.split(";") + + new_hosts = get_hosts_by_groupgoo(group_dict, spec) + + seperate_gloobs = spec.split(";") + seperate_gloobs = seperate_gloobs + new_hosts + for each_gloob in seperate_gloobs: + actual_gloob = "%s/%s.cert" % (config.certroot, each_gloob) + certs = glob.glob(actual_gloob) + for cert in certs: + all_certs.append(cert) + host = cert.replace(config.certroot,"")[1:-5] + all_hosts.append(host) + + all_urls = [] + for x in all_hosts: + if not just_fqdns: + all_urls.append("https://%s:%s" % (x, port)) + else: + all_urls.append(x) + + if verbose and len(all_urls) == 0: + sys.stderr.write("no hosts matched\n") + + return all_urls + + +# does the hostnamegoo actually expand to anything? +def isServer(server_string): + servers = expand_servers(server_string) + if len(servers) > 0: + return True + return False + + +class Client(object): + + def __init__(self, server_spec, port=DEFAULT_PORT, interactive=False, + verbose=False, noglobs=False, nforks=1, config=None, async=False, init_ssl=True): + """ + 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.nforks = nforks + self.async = async + + self.servers = expand_servers(self.server_spec, port=self.port, noglobs=self.noglobs,verbose=self.verbose) + + if init_ssl: + self.setup_ssl() + + def setup_ssl(self, client_key=None, client_cert=None, ca=None): + # defaults go: + # certmaster key, cert, ca + # funcd key, cert, ca + # raise FuncClientError + ol_key = '%s/funcmaster.key' % self.config.cadir + ol_crt = '%s/funcmaster.crt' % self.config.cadir + myname = utils.get_hostname() + # maybe /etc/pki/func is a variable somewhere? + fd_key = '/etc/pki/func/%s.pem' % myname + fd_crt = '/etc/pki/func/%s.cert' % myname + self.ca = '%s/funcmaster.crt' % self.config.cadir + if client_key and client_cert and ca: + if (os.access(client_key, os.R_OK) and os.access(client_cert, os.R_OK) + and os.access(ca, os.R_OK)): + self.key = client_key + self.cert = client_cert + self.ca = ca + # otherwise fall through our defaults + elif os.access(ol_key, os.R_OK) and os.access(ol_crt, os.R_OK): + self.key = ol_key + self.cert = ol_crt + elif os.access(fd_key, os.R_OK) and os.access(fd_crt, os.R_OK): + self.key = fd_key + self.cert = fd_crt + else: + raise Func_Client_Exception, 'Cannot read ssl credentials: ssl, cert, ca' + + + + + 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], self.nforks) + + # ----------------------------------------------- + + def job_status(self, jobid): + """ + Use this to acquire status from jobs when using run with async client handles + """ + return jobthing.job_status(jobid, client_class=Client) + + # ----------------------------------------------- + + def run(self, module, method, args, nforks=1): + """ + 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 = {} + + def process_server(bucketnumber, buckets, server): + + 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) + + # async calling signature has an "imaginary" prefix + # so async.abc.def does abc.def as a background task. + # see Wiki docs for details + if self.async: + meth = "async.%s" % meth + + # this is the point at which we make the remote call. + retval = getattr(conn, meth)(*args[:]) + + if self.interactive: + print retval + except Exception, e: + (t, v, tb) = sys.exc_info() + retval = utils.nice_exception(t,v,tb) + 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] + return (server_name, retval) + + if not self.noglobs: + if self.nforks > 1 or self.async: + # using forkbomb module to distribute job over multiple threads + if not self.async: + results = forkbomb.batch_run(self.servers, process_server, nforks) + else: + results = jobthing.batch_run(self.servers, process_server, nforks) + else: + # no need to go through the fork code, we can do this directly + results = {} + for x in self.servers: + (nkey,nvalue) = process_server(0, 0, x) + results[nkey] = nvalue + else: + # globbing is not being used, but still need to make sure + # URI is well formed. + expanded = expand_servers(self.server_spec, port=self.port, noglobs=True, verbose=self.verbose)[0] + results = process_server(0, 0, expanded) + + 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 |