Pushing changes as part of certmaster split
[certmaster.git] / certmaster / overlord / client.py
1 ##
2 ## func command line interface & client lib
3 ##
4 ## Copyright 2007, Red Hat, Inc
5 ## Michael DeHaan <mdehaan@redhat.com>
6 ## +AUTHORS
7 ##
8 ## This software may be freely redistributed under the terms of the GNU
9 ## general public license.
10 ##
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.
14 ##
15
16 import sys
17 import glob
18 import os
19
20 from func.commonconfig import CMConfig
21 from func.config import read_config, CONFIG_FILE
22
23 import sslclient
24
25 import command
26 import groups
27 import func.forkbomb as forkbomb
28 import func.jobthing as jobthing
29 import func.utils as utils
30 from func.CommonErrors import *
31
32 # ===================================
33 # defaults
34 # TO DO: some of this may want to come from config later
35
36 DEFAULT_PORT = 51234
37 FUNC_USAGE = "Usage: %s [ --help ] [ --verbose ] target.example.org module method arg1 [...]"
38
39 # ===================================
40
41 class CommandAutomagic(object):
42 """
43 This allows a client object to act as if it were one machine, when in
44 reality it represents many.
45 """
46
47 def __init__(self, clientref, base, nforks=1):
48 self.base = base
49 self.clientref = clientref
50 self.nforks = nforks
51
52 def __getattr__(self,name):
53 base2 = self.base[:]
54 base2.append(name)
55 return CommandAutomagic(self.clientref, base2, self.nforks)
56
57 def __call__(self, *args):
58 if not self.base:
59 raise AttributeError("something wrong here")
60 if len(self.base) < 2:
61 raise AttributeError("no method called: %s" % ".".join(self.base))
62 module = self.base[0]
63 method = ".".join(self.base[1:])
64 return self.clientref.run(module,method,args,nforks=self.nforks)
65
66
67 def get_groups():
68 group_class = groups.Groups()
69 return group_class.get_groups()
70
71
72 def get_hosts_by_groupgoo(groups, groupgoo):
73 group_gloobs = groupgoo.split(':')
74 hosts = []
75 for group_gloob in group_gloobs:
76 if not group_gloob[0] == "@":
77 continue
78 if groups.has_key(group_gloob[1:]):
79 hosts = hosts + groups[group_gloob[1:]]
80 else:
81 print "group %s not defined" % group_gloob
82 return hosts
83
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):
88 """
89 Given a regex/blob of servers, expand to a list
90 of server ids.
91 """
92
93
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)
98
99 if noglobs:
100 if not just_fqdns:
101 return [ "https://%s:%s" % (spec, port) ]
102 else:
103 return spec
104
105 group_dict = get_groups()
106
107 all_hosts = []
108 all_certs = []
109 seperate_gloobs = spec.split(";")
110
111 new_hosts = get_hosts_by_groupgoo(group_dict, spec)
112
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)
118 for cert in certs:
119 all_certs.append(cert)
120 host = cert.replace(config.certroot,"")[1:-5]
121 all_hosts.append(host)
122
123 all_urls = []
124 for x in all_hosts:
125 if not just_fqdns:
126 all_urls.append("https://%s:%s" % (x, port))
127 else:
128 all_urls.append(x)
129
130 if verbose and len(all_urls) == 0:
131 sys.stderr.write("no hosts matched\n")
132
133 return all_urls
134
135
136 # does the hostnamegoo actually expand to anything?
137 def isServer(server_string):
138 servers = expand_servers(server_string)
139 if len(servers) > 0:
140 return True
141 return False
142
143
144 class Client(object):
145
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):
148 """
149 Constructor.
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
155 """
156 self.config = config
157 if config is None:
158 self.config = read_config(CONFIG_FILE, CMConfig)
159
160
161 self.server_spec = server_spec
162 self.port = port
163 self.verbose = verbose
164 self.interactive = interactive
165 self.noglobs = noglobs
166 self.nforks = nforks
167 self.async = async
168
169 self.servers = expand_servers(self.server_spec, port=self.port, noglobs=self.noglobs,verbose=self.verbose)
170
171 if init_ssl:
172 self.setup_ssl()
173
174 def setup_ssl(self, client_key=None, client_cert=None, ca=None):
175 # defaults go:
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
191 self.ca = ca
192 # otherwise fall through our defaults
193 elif os.access(ol_key, os.R_OK) and os.access(ol_crt, os.R_OK):
194 self.key = ol_key
195 self.cert = ol_crt
196 elif os.access(fd_key, os.R_OK) and os.access(fd_crt, os.R_OK):
197 self.key = fd_key
198 self.cert = fd_crt
199 else:
200 raise Func_Client_Exception, 'Cannot read ssl credentials: ssl, cert, ca'
201
202
203
204
205 def __getattr__(self, name):
206 """
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.
210
211 So, it enables stuff like this:
212
213 Client("*.example.org").yum.install("foo")
214
215 # WARNING: any missing values in Client's source will yield
216 # strange errors with this engaged. Be aware of that.
217 """
218
219 return CommandAutomagic(self, [name], self.nforks)
220
221 # -----------------------------------------------
222
223 def job_status(self, jobid):
224 """
225 Use this to acquire status from jobs when using run with async client handles
226 """
227 return jobthing.job_status(jobid, client_class=Client)
228
229 # -----------------------------------------------
230
231 def run(self, module, method, args, nforks=1):
232 """
233 Invoke a remote method on one or more servers.
234 Run returns a hash, the keys are server names, the values are the
235 returns.
236
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.
240 """
241
242 results = {}
243
244 def process_server(bucketnumber, buckets, server):
245
246 conn = sslclient.FuncServer(server, self.key, self.cert, self.ca )
247 # conn = xmlrpclib.ServerProxy(server)
248
249 if self.interactive:
250 sys.stderr.write("on %s running %s %s (%s)\n" % (server,
251 module, method, ",".join(args)))
252
253 # FIXME: support userland command subclassing only if a module
254 # is present, otherwise run as follows. -- MPD
255
256 try:
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)
261
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
265 if self.async:
266 meth = "async.%s" % meth
267
268 # this is the point at which we make the remote call.
269 retval = getattr(conn, meth)(*args[:])
270
271 if self.interactive:
272 print retval
273 except Exception, e:
274 (t, v, tb) = sys.exc_info()
275 retval = utils.nice_exception(t,v,tb)
276 if self.interactive:
277 sys.stderr.write("remote exception on %s: %s\n" %
278 (server, str(e)))
279
280 if self.noglobs:
281 return retval
282 else:
283 left = server.rfind("/")+1
284 right = server.rfind(":")
285 server_name = server[left:right]
286 return (server_name, retval)
287
288 if not self.noglobs:
289 if self.nforks > 1 or self.async:
290 # using forkbomb module to distribute job over multiple threads
291 if not self.async:
292 results = forkbomb.batch_run(self.servers, process_server, nforks)
293 else:
294 results = jobthing.batch_run(self.servers, process_server, nforks)
295 else:
296 # no need to go through the fork code, we can do this directly
297 results = {}
298 for x in self.servers:
299 (nkey,nvalue) = process_server(0, 0, x)
300 results[nkey] = nvalue
301 else:
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)
306
307 return results
308
309 # -----------------------------------------------
310
311 def cli_return(self,results):
312 """
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.
316 """
317 numbers = []
318 for x in results.keys():
319 # faults are the most important
320 if type(x) == Exception:
321 return -911
322 # then pay attention to numbers
323 if type(x) == int:
324 numbers.append(x)
325
326 # if there were no numbers, assume 0
327 if len(numbers) == 0:
328 return 0
329
330 # if there were numbers, return the highest
331 # (presumably the worst error code
332 max = -9999
333 for x in numbers:
334 if x > max:
335 max = x
336 return max