source: fedd/federation/client_lib.py @ a3bbb4a

compt_changesinfo-ops
Last change on this file since a3bbb4a was e83f2f2, checked in by Ted Faber <faber@…>, 13 years ago

Move proofs around. Lots of changes, including fault handling.

  • Property mode set to 100755
File size: 10.3 KB
Line 
1#!/usr/local/bin/python
2
3import sys
4import pwd
5import os
6import os.path
7
8from string import join
9from datetime import datetime
10
11
12from fedid import fedid
13from util import fedd_ssl_context, file_expanding_opts
14from remote_service import service_caller
15from service_error import service_error
16
17from optparse import OptionParser, OptionValueError
18
19
20class client_opts(file_expanding_opts):
21    """
22    Standatd set of options that all clients talking to fedd can probably use.
23    Client code usually specializes this.
24    """
25
26    def __init__(self):
27        file_expanding_opts.__init__(self,
28                usage="%prog [opts] (--help for details)",
29                version="0.1")
30
31        self.add_option("--cert", action="callback", dest="cert", 
32                callback=self.expand_file,
33                type="string", help="my certificate file")
34        self.add_option("--auth_log", action="callback", dest="auth_log",
35                callback=self.expand_file, default=None,
36                type="string", help="Log authentication decisions to this file")
37        self.add_option("--abac", action="callback", dest="abac_dir",
38                callback=self.expand_file,
39                type="string", default=os.path.expanduser('~/.abac'), 
40                help="Directory with abac certs")
41        self.add_option('--no_abac', action='store_const', const=None, 
42                dest='abac_dir', help='Do not use abac authorization')
43        self.add_option( "--debug", action="count", dest="debug", 
44                default=0, help="Set debug.  Repeat for more information")
45        self.add_option("--serialize_only", action="store_true", 
46                dest="serialize_only", default=False,
47                help="Print the SOAP request that would be sent and exit")
48        self.add_option("--trusted", action="callback", dest="trusted",
49                callback=self.expand_file,
50                type="string", help="Trusted certificates (required)")
51        self.add_option("--url", action="store", dest="url",
52                type="string",default=None, help="URL to connect to")
53        self.add_option("--transport", action="store", type="choice",
54                choices=("xmlrpc", "soap"), default="soap",
55                help="Transport for request (xmlrpc|soap) (Default: %default)")
56        self.add_option("--trace", action="store_const", dest="tracefile", 
57                const=sys.stderr, help="Print SOAP exchange to stderr")
58
59def log_authentication(fn, action, outcome, proof):
60    f = open(fn, 'a')
61    print >>f, '%s %s at %s' % (action, outcome, datetime.now())
62    if isinstance(proof, list):
63        for p in proof:
64            print >>f, p.to_xml()
65    else:
66        print >>f, proof.to_xml()
67    f.close()
68
69
70def exit_with_fault(exc, action, opts, out=sys.stderr):
71    """
72    Print an error message and exit.  exc is the RPCException that caused the
73    failure.
74    """
75    codestr = ""
76
77    if exc.errstr: codestr = "Error: %s" % exc.errstr
78    else: codestr = ""
79
80    if exc.code:
81        if isinstance(exc.code, int): code = exc.code
82        else: code = -1
83
84        if len(codestr) > 0 : codestr += " (%d)" % code
85        else: codestr = "Error Code: %d" % code
86    else:
87        code = -1
88
89    if exc.code == service_error.access and opts.auth_log:
90        try:
91            log_authentication(opts.auth_log, action, 'failed', exc.proof)
92        except EnvironmentError, e:
93            print >>sys.stderr, "Failed to log to %s: %s" % \
94                    (e.filename, e.strerror)
95
96    print>>out, codestr
97    print>>out, "Description: %s" % exc.desc
98    sys.exit(code)
99
100class RPCException(RuntimeError):
101    """
102    An error during the RPC exchange.  It unifies errors from both SOAP and
103    XMLPRC calls.
104    """
105    def __init__(self, desc=None, code=None, errstr=None, proof=None):
106        RuntimeError.__init__(self)
107        self.desc = desc
108        if isinstance(code, int): self.code = code
109        else: self.code = -1
110        self.errstr = errstr
111        self.proof = proof
112
113class CertificateMismatchError(RuntimeError): pass
114
115
116def get_user_cert():
117    for c in ("~/.ssl/fedid.pem", "~/.ssl/emulab.pem"):
118        cert = os.path.expanduser(c)
119        if os.access(cert, os.R_OK):
120            break
121        else:
122            cert = None
123    return cert
124
125def get_abac_certs(dir):
126    '''
127    Return a list of the contents of the files in dir.  These should be abac
128    certificates, but that isn't checked.
129    '''
130    rv = [ ]
131    if dir and os.path.isdir(dir):
132        for fn in ["%s/%s" % (dir, p) for p in os.listdir(dir) \
133                if os.path.isfile("%s/%s" % (dir,p))]:
134            f = open(fn, 'r')
135            rv.append(f.read())
136            f.close()
137    return rv
138
139def wrangle_standard_options(opts):
140    """
141    Look for the certificate to use for the call (check for the standard emulab
142    file and any passed in.  Make sure a present cert file can be read.  Make
143    sure that any trusted cert file can be read and if the debug level is set,
144    set the tracefile attribute as well.  If any of these tests fail, raise a
145    RuntimeError.  Otherwise return the certificate file, the fedid, and the
146    fedd url.
147    """
148    default_url="https://localhost:23235"
149
150    if opts.debug > 0: opts.tracefile=sys.stderr
151
152    if opts.trusted:
153        if ( not os.access(opts.trusted, os.R_OK) ) :
154            raise RuntimeError("Cannot read trusted certificates (%s)" \
155                    % opts.trusted)
156
157    cert = get_user_cert()
158    if opts.cert: cert = opts.cert
159
160    if cert is None:
161        raise RuntimeError("No certificate given (--cert) or found")
162
163    if os.access(cert, os.R_OK):
164        fid = fedid(file=cert)
165    else:
166        raise RuntimeError("Cannot read certificate (%s)" % cert)
167
168    if opts.url: url = opts.url
169    elif 'FEDD_URL' in os.environ: url = os.environ['FEDD_URL']
170    else: url = default_url
171
172    if opts.abac_dir:
173        if not os.access(opts.abac_dir, os.F_OK):
174            raise RuntimeError("No ABAC directory: %s" % opts.abac_dir)
175        elif not os.path.isdir(opts.abac_dir):
176            raise RuntimeError("ABAC directory not a directory: %s" \
177                    % opts.abac_dir)
178        elif not os.access(opts.abac_dir, os.W_OK):
179            raise RuntimeError("Cannot write to ABAC directory: %s" \
180                    % opts.abac_dir)
181
182
183
184    return (cert, fid, url)
185
186def save_certfile(out_certfile, ea, check_cert=None):
187    """
188    if the experiment authority section in ea has a certificate and the
189    out_certfile parameter has a place to put it, save the cert to the file.
190    EnvronmentError s can come from the file operations.  If check_cert is
191    given, the certificate in ea is compared with it and if they are not equal,
192    a CertificateMismatchError is raised.
193    """
194    if out_certfile and ea and 'X509' in ea:
195        out_cert = ea['X509']
196        if check_cert and check_cert != out_cert:
197            raise CertificateMismatchError()
198        f = open(out_certfile, "w")
199        f.write(out_cert)
200        f.close()
201
202
203def do_rpc(req_dict, url, transport, cert, trusted, tracefile=None,
204        serialize_only=False, caller=None, responseBody=None):
205    """
206    The work of sending and parsing the RPC as either XMLRPC or SOAP
207    """
208
209    if caller is None: 
210        raise RuntimeError("Must provide caller to do_rpc")
211
212    context = None
213    while context == None:
214        try:
215            context = fedd_ssl_context(cert, trusted)
216        except Exception, e:
217            # Yes, doing this on message type is not ideal.  The string
218            # comes from OpenSSL, so check there is this stops working.
219            if str(e) == "bad decrypt": 
220                print >>sys.stderr, "Bad Passphrase given."
221            else: raise
222
223    if transport == "soap":
224        if serialize_only:
225            print caller.serialize_soap(req_dict) 
226            return { }
227        else:
228            try:
229                resp = caller.call_soap_service(url, req_dict, 
230                        context=context, tracefile=tracefile)
231            except service_error, e:
232                raise RPCException(desc=e.desc, code=e.code, 
233                        errstr=e.code_string(), proof=e.proof)
234    elif transport == "xmlrpc":
235        if serialize_only:
236            ser = dumps((req_dict,))
237            print ser
238            return { }
239        else:
240            try:
241                resp = caller.call_xmlrpc_service(url, req_dict, 
242                        context=context, tracefile=tracefile)
243            except service_error, e:
244                raise RPCException(desc=e.desc, code=e.code, 
245                        errstr=e.code_string())
246    else:
247        raise RuntimeError("Unknown RPC transport: %s" % transport)
248
249    if responseBody:
250        if responseBody in resp: return resp[responseBody]
251        else: raise RuntimeError("No body in response??")
252    else:
253        return resp
254
255def get_experiment_names(eid):
256    """
257    eid is the experimentID entry in one of a dict representing a fedd message.
258    Pull out the fedid and localname from that hash and return them as fedid,
259    localid)
260    """
261    fedid = None
262    local = None
263    for id in eid or []:
264        for k in id.keys():
265            if k =='fedid':
266                fedid = id[k]
267            elif k =='localname':
268                local = id[k]
269
270    return (fedid, local)
271
272class info_format:
273    def __init__(self, out=sys.stdout):
274        self.out = out
275        self.key = {
276                'vis': 'vis', 
277                'vtopo': 'vtopo',
278                'federant': 'federant',
279                'experimentdescription': 'experimentdescription',
280                'id': 'experimentID',
281                'status': 'experimentStatus',
282                'log': 'allocationLog',
283                'embedding': 'embedding',
284            }
285        self.formatter = {
286                'vis':  self.print_vis_or_vtopo('vis', self.out),
287                'vtopo':  self.print_vis_or_vtopo('vtopo', self.out),
288                'federant':  self.print_xml,
289                'experimentdescription':  self.print_xml,
290                'id': self.print_id,
291                'status': self.print_string,
292                'log': self.print_string,
293                'embedding':  self.print_xml,
294            }
295
296    def print_string(self, d):
297        """
298        Print the string to the class output.
299        """
300        print >>self.out, d
301
302    def print_id(self, d):
303        """
304        d is an array of ID dicts.  Print each one to the class output.
305        """
306        for id in d or []:
307            for k, i in id.items():
308                print >>self.out, "%s: %s" % (k, i)
309
310    def print_xml(self, d):
311        """
312        Very simple ugly xml formatter of the kinds of dicts that come back
313        from services.
314        """
315        if isinstance(d, dict):
316            for k, v in d.items():
317                print >>self.out, "<%s>" % k
318                self.print_xml(v)
319                print >>self.out, "</%s>" % k
320        elif isinstance(d, list):
321            for x in d:
322                self.print_xml(x)
323        else:
324            print >>self.out, d
325
326
327    class print_vis_or_vtopo:
328        """
329        Print the retrieved data is a simple xml representation of the dict.
330        """
331        def __init__(self, top, out=sys.stdout):
332            self.xml = top
333            self.out = out
334
335        def __call__(self, d, out=sys.stdout):
336            str = "<%s>\n" % self.xml
337            for t in ('node', 'lan'):
338                if d.has_key(t): 
339                    for x in d[t]:
340                        str += "<%s>" % t
341                        for k in x.keys():
342                            str += "<%s>%s</%s>" % (k, x[k],k)
343                        str += "</%s>\n" % t
344            str+= "</%s>" % self.xml
345            print >>self.out, str
346
347    def __call__(self, d, r):
348        """
349        Print the data of type r (one of the keys of key) to the class output.
350        """
351        k = self.key.get(r, None)
352        if k:
353            if k in d: self.formatter[r](d[k])
354            else: raise RuntimeError("Bad response: no %s" %k)
355        else:
356            raise RuntimeError("Don't understand datatype %s" %r)
357
Note: See TracBrowser for help on using the repository browser.