source: fedd/federation/client_lib.py @ e8f2d4c

Last change on this file since e8f2d4c was 311ece3, checked in by Ted Faber <faber@…>, 12 years ago

Make auth log more xml-like

  • Property mode set to 100755
File size: 10.4 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 deter 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, '<comment>%s %s at %s</comment>' % (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            try:
175                os.mkdir(opts.abac_dir, 0700)
176            except OSError, e:
177                raise RuntimeError("No ABAC directory (could not create): %s" \
178                        % opts.abac_dir)
179
180        if not os.path.isdir(opts.abac_dir):
181            raise RuntimeError("ABAC directory not a directory: %s" \
182                    % opts.abac_dir)
183        elif not os.access(opts.abac_dir, os.W_OK):
184            raise RuntimeError("Cannot write to ABAC directory: %s" \
185                    % opts.abac_dir)
186
187
188
189    return (cert, fid, url)
190
191def save_certfile(out_certfile, ea, check_cert=None):
192    """
193    if the experiment authority section in ea has a certificate and the
194    out_certfile parameter has a place to put it, save the cert to the file.
195    EnvronmentError s can come from the file operations.  If check_cert is
196    given, the certificate in ea is compared with it and if they are not equal,
197    a CertificateMismatchError is raised.
198    """
199    if out_certfile and ea and 'X509' in ea:
200        out_cert = ea['X509']
201        if check_cert and check_cert != out_cert:
202            raise CertificateMismatchError()
203        f = open(out_certfile, "w")
204        f.write(out_cert)
205        f.close()
206
207
208def do_rpc(req_dict, url, transport, cert, trusted, tracefile=None,
209        serialize_only=False, caller=None, responseBody=None):
210    """
211    The work of sending and parsing the RPC as either XMLRPC or SOAP
212    """
213
214    if caller is None: 
215        raise RuntimeError("Must provide caller to do_rpc")
216
217    context = None
218    while context == None:
219        try:
220            context = fedd_ssl_context(cert, trusted)
221        except Exception, e:
222            # Yes, doing this on message type is not ideal.  The string
223            # comes from OpenSSL, so check there is this stops working.
224            if str(e) == "bad decrypt": 
225                print >>sys.stderr, "Bad Passphrase given."
226            else: raise
227
228    if transport == "soap":
229        if serialize_only:
230            print caller.serialize_soap(req_dict) 
231            return { }
232        else:
233            try:
234                resp = caller.call_soap_service(url, req_dict, 
235                        context=context, tracefile=tracefile)
236            except service_error, e:
237                raise RPCException(desc=e.desc, code=e.code, 
238                        errstr=e.code_string(), proof=e.proof)
239    elif transport == "xmlrpc":
240        if serialize_only:
241            ser = dumps((req_dict,))
242            print ser
243            return { }
244        else:
245            try:
246                resp = caller.call_xmlrpc_service(url, req_dict, 
247                        context=context, tracefile=tracefile)
248            except service_error, e:
249                raise RPCException(desc=e.desc, code=e.code, 
250                        errstr=e.code_string())
251    else:
252        raise RuntimeError("Unknown RPC transport: %s" % transport)
253
254    if responseBody:
255        if responseBody in resp: return resp[responseBody]
256        else: raise RuntimeError("No body in response??")
257    else:
258        return resp
259
260def get_experiment_names(eid):
261    """
262    eid is the experimentID entry in one of a dict representing a fedd message.
263    Pull out the fedid and localname from that hash and return them as fedid,
264    localid)
265    """
266    fedid = None
267    local = None
268    for id in eid or []:
269        for k in id.keys():
270            if k =='fedid':
271                fedid = id[k]
272            elif k =='localname':
273                local = id[k]
274
275    return (fedid, local)
276
277class info_format:
278    def __init__(self, out=sys.stdout):
279        self.out = out
280        self.key = {
281                'vis': 'vis', 
282                'vtopo': 'vtopo',
283                'federant': 'federant',
284                'experimentdescription': 'experimentdescription',
285                'id': 'experimentID',
286                'status': 'experimentStatus',
287                'log': 'allocationLog',
288                'embedding': 'embedding',
289            }
290        self.formatter = {
291                'vis':  self.print_vis_or_vtopo('vis', self.out),
292                'vtopo':  self.print_vis_or_vtopo('vtopo', self.out),
293                'federant':  self.print_xml,
294                'experimentdescription':  self.print_xml,
295                'id': self.print_id,
296                'status': self.print_string,
297                'log': self.print_string,
298                'embedding':  self.print_xml,
299            }
300
301    def print_string(self, d):
302        """
303        Print the string to the class output.
304        """
305        print >>self.out, d
306
307    def print_id(self, d):
308        """
309        d is an array of ID dicts.  Print each one to the class output.
310        """
311        for id in d or []:
312            for k, i in id.items():
313                print >>self.out, "%s: %s" % (k, i)
314
315    def print_xml(self, d):
316        """
317        Very simple ugly xml formatter of the kinds of dicts that come back
318        from services.
319        """
320        if isinstance(d, dict):
321            for k, v in d.items():
322                print >>self.out, "<%s>" % k
323                self.print_xml(v)
324                print >>self.out, "</%s>" % k
325        elif isinstance(d, list):
326            for x in d:
327                self.print_xml(x)
328        else:
329            print >>self.out, d
330
331
332    class print_vis_or_vtopo:
333        """
334        Print the retrieved data is a simple xml representation of the dict.
335        """
336        def __init__(self, top, out=sys.stdout):
337            self.xml = top
338            self.out = out
339
340        def __call__(self, d, out=sys.stdout):
341            str = "<%s>\n" % self.xml
342            for t in ('node', 'lan'):
343                if d.has_key(t): 
344                    for x in d[t]:
345                        str += "<%s>" % t
346                        for k in x.keys():
347                            str += "<%s>%s</%s>" % (k, x[k],k)
348                        str += "</%s>\n" % t
349            str+= "</%s>" % self.xml
350            print >>self.out, str
351
352    def __call__(self, d, r):
353        """
354        Print the data of type r (one of the keys of key) to the class output.
355        """
356        k = self.key.get(r, None)
357        if k:
358            if k in d: self.formatter[r](d[k])
359            else: raise RuntimeError("Bad response: no %s" %k)
360        else:
361            raise RuntimeError("Don't understand datatype %s" %r)
362
Note: See TracBrowser for help on using the repository browser.