source: fedd/fedd_create.py @ 65f6442

compt_changesinfo-ops
Last change on this file since 65f6442 was 65f6442, checked in by Ted Faber <faber@…>, 8 years ago

Move to lib creddy. Closes #22

  • Property mode set to 100755
File size: 10.5 KB
Line 
1#!/usr/bin/env python
2
3import sys, os
4import re
5import subprocess
6
7import ABAC
8import Creddy
9
10from string import join, ascii_letters
11from random import choice
12
13from federation.proof import proof
14from federation.fedid import fedid, generate_fedid
15from federation.remote_service import service_caller
16from federation.client_lib import client_opts, exit_with_fault, RPCException, \
17        wrangle_standard_options, do_rpc, get_experiment_names, save_certfile,\
18        get_abac_certs, log_authentication
19from federation.util import abac_split_cert, abac_context_to_creds
20from federation import topdl
21
22from xml.parsers.expat import ExpatError
23
24class fedd_create_opts(client_opts):
25    """
26    Extra options that create needs.  Help entries should explain them.
27    """
28    def __init__(self):
29        client_opts.__init__(self)
30        self.add_option("--experiment_cert", dest="out_certfile",
31                action="callback", callback=self.expand_file,
32                type="string", help="output certificate file")
33        self.add_option("--experiment_name", dest="exp_name",
34                type="string", help="Suggested experiment name")
35        self.add_option("--file", dest="file", action="callback",
36                callback=self.expand_file, type="str",
37                help="experiment description file")
38        self.add_option("--project", action="store", dest="project", 
39                type="string",
40                help="Project to export from master")
41        self.add_option("--master", dest="master",
42                help="Master testbed in the federation (pseudo project export)")
43        self.add_option("--service", dest="service", action="append",
44                type="string", default=[],
45                help="Service description name:exporters:importers:attrs")
46        self.add_option("--map", dest="map", action="append",
47                type="string", default=[],
48                help="Explicit map from testbed label to URI - " + \
49                        "deter:https://users.isi.deterlab/net:13232")
50        self.add_option('--gen_cert', action='store_true', dest='gen_cert',
51                default=False,
52                help='generate a cert to which to delegate rights')
53        self.add_option('--delegate', action='store_true', dest='generate',
54                help='Delegate rights to a generated cert (default)')
55        self.add_option('--no-delegate', action='store_true', dest='generate',
56                help='Do not delegate rights to a generated cert')
57
58        self.set_defaults(delegate=True)
59
60def parse_service(svc):
61    """
62    Pasre a service entry into a hash representing a service entry in a
63    message.  The format is:
64        svc_name:exporter(s):importer(s):attr=val,attr=val
65    The svc_name is teh service name, exporter is the exporting testbeds
66    (comma-separated) importer is the importing testbeds (if any) and the rest
67    are attr=val pairs that will become attributes of the service.  These
68    include parameters and other such stuff.
69    """
70
71    terms = svc.split(':')
72    svcd = { }
73    if len(terms) < 2 or len(terms[0]) == 0 or len(terms[1]) == 0:
74        sys.exit("Bad service description '%s': Not enough terms" % svc)
75   
76    svcd['name'] = terms[0]
77    svcd['export'] = terms[1].split(",")
78    if len(terms) > 2 and len(terms[2]) > 0:
79        svcd['import'] = terms[2].split(",")
80    if len(terms) > 3 and len(terms[3]) > 0:
81        svcd['fedAttr'] = [ ]
82        for t in terms[3].split(";"):
83            i = t.find("=")
84            if i != -1 :
85                svcd['fedAttr'].append(
86                        {'attribute': t[0:i], 'value': t[i+1:]})
87            else:
88                sys.exit("Bad service attribute '%s': no equals sign" % t)
89    return svcd
90
91def project_export_service(master, project):
92    """
93    Output the dict representation of a project_export service for this project
94    and master.
95    """
96    return {
97            'name': 'project_export', 
98            'export': [master], 
99            'importall': True,
100            'fedAttr': [ 
101                { 'attribute': 'project', 'value': project },
102                ],
103            }
104
105def delegate(fedid, cert, dir, name=None, debug=False):
106    '''
107    Make the creddy call to create an attribute delegating rights to the new
108    experiment.  The cert parameter points to a conventional cert & key combo,
109    which we split out into tempfiles, which we delete on return.  The return
110    value if the filename in which the certificate was stored.
111    '''
112    certfile = keyfile = None
113    expid = "%s" % fedid
114
115    # Trim the "fedid:"
116    if expid.startswith("fedid:"): expid = expid[6:]
117
118    try:
119        keyfile, certfile = abac_split_cert(cert)
120
121        rv = 0
122        if name: 
123            fn ='%s/%s_attr.der' % (dir, name) 
124            id_fn = '%s/%s_id.pem' % (dir, name)
125        else: 
126            fn = '%s/%s_attr.der' % (dir, expid)
127            id_fn = '%s/%s_id.pem' % (dir, expid)
128
129        try:
130            cid = Creddy.ID(certfile)
131            cid.load_privkey(keyfile)
132            cattr = Creddy.Attribute(cid, 'acting_for', 3600 * 24 * 365 * 10)
133            cattr.principal("%s" % expid)
134            cattr.bake()
135            cattr.write_name(fn)
136        except RuntimeError:
137            print >>sys.stderr, "Cannot create ABAC delegation. " + \
138                    "Did you run cert_to_fedid.py on your X.509 cert?"
139            return []
140
141        context = ABAC.Context()
142        if context.load_id_file(certfile) != ABAC.ABAC_CERT_SUCCESS or \
143                context.load_attribute_file(fn) != ABAC.ABAC_CERT_SUCCESS:
144            print >>sys.stderr, "Cannot load delegation into ABAC context. " + \
145                    "Did you run cert_to_fedid.py on your X.509 cert?"
146            return []
147        ids, attrs = abac_context_to_creds(context)
148
149        return ids + attrs
150
151
152    finally:
153        if keyfile: os.unlink(keyfile)
154        if certfile: os.unlink(certfile)
155
156
157# Main line
158service_re = re.compile('^\\s*#\\s*SERVICE:\\s*([\\S]+)')
159parser = fedd_create_opts()
160(opts, args) = parser.parse_args()
161
162svcs = []
163# Option processing
164try:
165    cert, fid, url = wrangle_standard_options(opts)
166except RuntimeError, e:
167    sys.exit("%s" %e)
168
169if opts.file:
170
171    top = None
172    # Try the file as a topdl description
173    try:
174        top = topdl.topology_from_xml(filename=opts.file, top='experiment')
175    except EnvironmentError:
176        # Can't read the file, fail now
177        sys.exit("Cannot read description file (%s)" %opts.file)
178    except ExpatError:
179        # The file is not topdl, fall and assume it's ns2
180        pass
181
182    if top is None:
183        try:
184            lines = [ line for line in open(opts.file, 'r')]
185            exp_desc = "".join(lines)
186            # Parse all the strings that we can pull out of the file using the
187            # service_re.
188            svcs.extend([parse_service(service_re.match(l).group(1)) \
189                    for l in lines if service_re.match(l)])
190        except EnvironmentError:
191            sys.exit("Cannot read description file (%s)" %opts.file)
192else:
193    sys.exit("Must specify an experiment description (--file)")
194
195out_certfile = opts.out_certfile
196
197# If there is no abac directory in which to store a delegated credential, don't
198# delegate.
199if not opts.abac_dir and opts.delegate:
200    opts.delegate = False
201
202# Load ABAC certs
203if opts.abac_dir:
204    try:
205        acerts = get_abac_certs(opts.abac_dir)
206    except EnvironmentError, e:
207        sys.exit('%s: %s' % (e.filename, e.strerror))
208else:
209    acerts = None
210
211# Fill in services
212if opts.master and opts.project:
213    svcs.append(project_export_service(opts.master, opts.project))
214svcs.extend([ parse_service(s) for s in opts.service])
215
216# Create a testbed map if one is specified
217tbmap = { }
218for m in opts.map:
219    i = m.find(":")
220    if i != -1: tbmap[m[0:i]] = m[i+1:]
221    else: sys.exit("Bad mapping argument: %s" %m )
222
223
224if not svcs:
225    print >>sys.stderr, "Warning:Neither master/project nor services requested"
226
227# Construct the New experiment request
228msg = { }
229
230
231# Generate a certificate if requested and put it into the message
232if opts.gen_cert:
233    expid, expcert = generate_fedid(opts.exp_name or 'dummy')
234    msg['experimentAccess'] = { 'X509': expcert }
235else:
236    expid = expcert = None
237
238if opts.exp_name:
239    msg['experimentID'] = { 'localname': opts.exp_name }
240
241if acerts:
242    msg['credential'] = acerts
243
244# ZSI will not properly construct an empty message.  If nothing has been added
245# to msg, pick a random localname to ensure the message is created
246if not msg:
247    msg['experimentID'] = { 
248            'localname': join([choice(ascii_letters) for i in range(0,8)],""),
249            }
250
251
252if opts.debug > 1: print >>sys.stderr, msg
253
254# The New call
255try:
256    resp_dict = do_rpc(msg, 
257            url, opts.transport, cert, opts.trusted, 
258            serialize_only=opts.serialize_only,
259            tracefile=opts.tracefile,
260            caller=service_caller('New'), responseBody="NewResponseBody")
261except RPCException, e:
262    exit_with_fault(e, 'New (create)', opts)
263except RuntimeError, e:
264    sys.exit("Error processing RPC: %s" % e)
265
266if opts.debug > 1: print >>sys.stderr, resp_dict
267
268proof = proof.from_dict(resp_dict.get('proof', {}))
269if proof and opts.auth_log:
270    log_authentication(opts.auth_log, 'New (create)', 'succeeded', proof)
271# Save the experiment ID certificate if we need it
272try:
273    save_certfile(opts.out_certfile, resp_dict.get('experimentAccess', None))
274except EnvironmentError, e:
275    sys.exit('Could not write to %s: %s' %  (opts.out_certfile, e))
276
277e_fedid, e_local = get_experiment_names(resp_dict.get('experimentID', None))
278if not e_fedid and not e_local and opts.serialize_only:
279    e_local = "serialize"
280
281# If delegation is requested and we have a target, make the delegation, and add
282# the credential to acerts.
283if e_fedid and opts.delegate:
284    try:
285        creds = delegate(e_fedid, cert, opts.abac_dir, name=opts.exp_name)
286        if creds:
287            acerts.extend(creds)
288    except EnvironmentError, e:
289        sys.exit("Cannot delegate rights %s: %s" % (e.filename, e.strerror));
290
291# Construct the Create message
292if top: 
293    msg = { 'experimentdescription' : { 'topdldescription': top.to_dict() }, }
294else: 
295    msg = { 'experimentdescription': { 'ns2description': exp_desc }, }
296
297if svcs:
298    msg['service'] = svcs
299
300if e_fedid:
301    msg['experimentID'] = { 'fedid': e_fedid }
302elif e_local:
303    msg['experimentID'] = { 'localname': e_local }
304else:
305    sys.exit("New did not return an experiment ID??")
306
307if acerts:
308    msg['credential'] = acerts
309
310if tbmap:
311    msg['testbedmap'] = [ { 'testbed': t, 'uri': u } for t, u in tbmap.items() ]
312
313if opts.debug > 1: print >>sys.stderr, msg
314
315# make the call
316try:
317    resp_dict = do_rpc(msg, 
318            url, opts.transport, cert, opts.trusted, 
319            serialize_only=opts.serialize_only,
320            tracefile=opts.tracefile,
321            caller=service_caller('Create', max_retries=1), responseBody="CreateResponseBody")
322except RPCException, e:
323    exit_with_fault(e, 'Create', opts)
324except RuntimeError, e:
325    sys.exit("Error processing RPC: %s" % e)
326
327if opts.debug > 1: print >>sys.stderr, resp_dict
328
329# output
330e_fedid, e_local = get_experiment_names(resp_dict.get('experimentID', None))
331st = resp_dict.get('experimentStatus', None)
332
333if e_local: print "localname: %s" % e_local
334if e_fedid: print "fedid: %s" % e_fedid
335if st: print "status: %s" % st
336proof = proof.from_dict(resp_dict.get('proof', {}))
337if proof and opts.auth_log:
338    log_authentication(opts.auth_log, 'Create', 'succeeded', proof)
Note: See TracBrowser for help on using the repository browser.