2 ## func command line interface & client lib
4 ## Copyright 2007, Red Hat, Inc
5 ## Michael DeHaan <mdehaan@redhat.com>
8 ## This software may be freely redistributed under the terms of the GNU
9 ## general public license.
11 ## You should have received a copy of the GNU General Public License
12 ## along with this program; if not, write to the Free Software
13 ## Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20 from func
.commonconfig
import CMConfig
21 from func
.config
import read_config
, CONFIG_FILE
27 import func
.forkbomb
as forkbomb
28 import func
.jobthing
as jobthing
29 import func
.utils
as utils
30 from func
.CommonErrors
import *
32 # ===================================
34 # TO DO: some of this may want to come from config later
37 FUNC_USAGE
= "Usage: %s [ --help ] [ --verbose ] target.example.org module method arg1 [...]"
39 # ===================================
41 class CommandAutomagic(object):
43 This allows a client object to act as if it were one machine, when in
44 reality it represents many.
47 def __init__(self
, clientref
, base
, nforks
=1):
49 self
.clientref
= clientref
52 def __getattr__(self
,name
):
55 return CommandAutomagic(self
.clientref
, base2
, self
.nforks
)
57 def __call__(self
, *args
):
59 raise AttributeError("something wrong here")
60 if len(self
.base
) < 2:
61 raise AttributeError("no method called: %s" % ".".join(self
.base
))
63 method
= ".".join(self
.base
[1:])
64 return self
.clientref
.run(module
,method
,args
,nforks
=self
.nforks
)
68 group_class
= groups
.Groups()
69 return group_class
.get_groups()
72 def get_hosts_by_groupgoo(groups
, groupgoo
):
73 group_gloobs
= groupgoo
.split(':')
75 for group_gloob
in group_gloobs
:
76 if not group_gloob
[0] == "@":
78 if groups
.has_key(group_gloob
[1:]):
79 hosts
= hosts
+ groups
[group_gloob
[1:]]
81 print "group %s not defined" % group_gloob
84 # ===================================
85 # this is a module level def so we can use it and isServer() from
86 # other modules with a Client class
87 def expand_servers(spec
, port
=51234, noglobs
=None, verbose
=None, just_fqdns
=False):
89 Given a regex/blob of servers, expand to a list
94 # FIXME: we need to refactor expand_servers, it seems to do
95 # weird things, reload the config and groups config everytime it's
96 # called for one, which may or may not be bad... -akl
97 config
= read_config(CONFIG_FILE
, CMConfig
)
101 return [ "https://%s:%s" % (spec
, port
) ]
105 group_dict
= get_groups()
109 seperate_gloobs
= spec
.split(";")
111 new_hosts
= get_hosts_by_groupgoo(group_dict
, spec
)
113 seperate_gloobs
= spec
.split(";")
114 seperate_gloobs
= seperate_gloobs
+ new_hosts
115 for each_gloob
in seperate_gloobs
:
116 actual_gloob
= "%s/%s.cert" % (config
.certroot
, each_gloob
)
117 certs
= glob
.glob(actual_gloob
)
119 all_certs
.append(cert
)
120 host
= cert
.replace(config
.certroot
,"")[1:-5]
121 all_hosts
.append(host
)
126 all_urls
.append("https://%s:%s" % (x
, port
))
130 if verbose
and len(all_urls
) == 0:
131 sys
.stderr
.write("no hosts matched\n")
136 # does the hostnamegoo actually expand to anything?
137 def isServer(server_string
):
138 servers
= expand_servers(server_string
)
144 class Client(object):
146 def __init__(self
, server_spec
, port
=DEFAULT_PORT
, interactive
=False,
147 verbose
=False, noglobs
=False, nforks
=1, config
=None, async=False, init_ssl
=True):
150 @server_spec -- something like "*.example.org" or "foosball"
151 @port -- is the port where all funcd processes should be contacted
152 @verbose -- whether to print unneccessary things
153 @noglobs -- specifies server_spec is not a glob, and run should return single values
154 @config -- optional config object
158 self
.config
= read_config(CONFIG_FILE
, CMConfig
)
161 self
.server_spec
= server_spec
163 self
.verbose
= verbose
164 self
.interactive
= interactive
165 self
.noglobs
= noglobs
169 self
.servers
= expand_servers(self
.server_spec
, port
=self
.port
, noglobs
=self
.noglobs
,verbose
=self
.verbose
)
174 def setup_ssl(self
, client_key
=None, client_cert
=None, ca
=None):
176 # certmaster key, cert, ca
177 # funcd key, cert, ca
178 # raise FuncClientError
179 ol_key
= '%s/funcmaster.key' % self
.config
.cadir
180 ol_crt
= '%s/funcmaster.crt' % self
.config
.cadir
181 myname
= utils
.get_hostname()
182 # maybe /etc/pki/func is a variable somewhere?
183 fd_key
= '/etc/pki/func/%s.pem' % myname
184 fd_crt
= '/etc/pki/func/%s.cert' % myname
185 self
.ca
= '%s/funcmaster.crt' % self
.config
.cadir
186 if client_key
and client_cert
and ca
:
187 if (os
.access(client_key
, os
.R_OK
) and os
.access(client_cert
, os
.R_OK
)
188 and os
.access(ca
, os
.R_OK
)):
189 self
.key
= client_key
190 self
.cert
= client_cert
192 # otherwise fall through our defaults
193 elif os
.access(ol_key
, os
.R_OK
) and os
.access(ol_crt
, os
.R_OK
):
196 elif os
.access(fd_key
, os
.R_OK
) and os
.access(fd_crt
, os
.R_OK
):
200 raise Func_Client_Exception
, 'Cannot read ssl credentials: ssl, cert, ca'
205 def __getattr__(self
, name
):
207 This getattr allows manipulation of the object as if it were
208 a XMLRPC handle to a single machine, when in reality it is a handle
209 to an unspecified number of machines.
211 So, it enables stuff like this:
213 Client("*.example.org").yum.install("foo")
215 # WARNING: any missing values in Client's source will yield
216 # strange errors with this engaged. Be aware of that.
219 return CommandAutomagic(self
, [name
], self
.nforks
)
221 # -----------------------------------------------
223 def job_status(self
, jobid
):
225 Use this to acquire status from jobs when using run with async client handles
227 return jobthing
.job_status(jobid
, client_class
=Client
)
229 # -----------------------------------------------
231 def run(self
, module
, method
, args
, nforks
=1):
233 Invoke a remote method on one or more servers.
234 Run returns a hash, the keys are server names, the values are the
237 The returns may include exception objects.
238 If Client() was constructed with noglobs=True, the return is instead
239 just a single value, not a hash.
244 def process_server(bucketnumber
, buckets
, server
):
246 conn
= sslclient
.FuncServer(server
, self
.key
, self
.cert
, self
.ca
)
247 # conn = xmlrpclib.ServerProxy(server)
250 sys
.stderr
.write("on %s running %s %s (%s)\n" % (server
,
251 module
, method
, ",".join(args
)))
253 # FIXME: support userland command subclassing only if a module
254 # is present, otherwise run as follows. -- MPD
257 # thats some pretty code right there aint it? -akl
258 # we can't call "call" on s, since thats a rpc, so
259 # we call gettatr around it.
260 meth
= "%s.%s" % (module
, method
)
262 # async calling signature has an "imaginary" prefix
263 # so async.abc.def does abc.def as a background task.
264 # see Wiki docs for details
266 meth
= "async.%s" % meth
268 # this is the point at which we make the remote call.
269 retval
= getattr(conn
, meth
)(*args
[:])
274 (t
, v
, tb
) = sys
.exc_info()
275 retval
= utils
.nice_exception(t
,v
,tb
)
277 sys
.stderr
.write("remote exception on %s: %s\n" %
283 left
= server
.rfind("/")+1
284 right
= server
.rfind(":")
285 server_name
= server
[left
:right
]
286 return (server_name
, retval
)
289 if self
.nforks
> 1 or self
.async:
290 # using forkbomb module to distribute job over multiple threads
292 results
= forkbomb
.batch_run(self
.servers
, process_server
, nforks
)
294 results
= jobthing
.batch_run(self
.servers
, process_server
, nforks
)
296 # no need to go through the fork code, we can do this directly
298 for x
in self
.servers
:
299 (nkey
,nvalue
) = process_server(0, 0, x
)
300 results
[nkey
] = nvalue
302 # globbing is not being used, but still need to make sure
303 # URI is well formed.
304 expanded
= expand_servers(self
.server_spec
, port
=self
.port
, noglobs
=True, verbose
=self
.verbose
)[0]
305 results
= process_server(0, 0, expanded
)
309 # -----------------------------------------------
311 def cli_return(self
,results
):
313 As the return code list could return strings and exceptions
314 and all sorts of crazy stuff, reduce it down to a simple
315 integer return. It may not be useful but we need one.
318 for x
in results
.keys():
319 # faults are the most important
320 if type(x
) == Exception:
322 # then pay attention to numbers
326 # if there were no numbers, assume 0
327 if len(numbers
) == 0:
330 # if there were numbers, return the highest
331 # (presumably the worst error code