source: fedd/federation/desktop_access.py @ 0608d96

Last change on this file since 0608d96 was 0608d96, checked in by Ted Faber <faber@…>, 5 years ago

Linux nat

  • Property mode set to 100644
File size: 23.0 KB
Line 
1#!/usr/local/bin/python
2
3import os,sys
4import re
5import string
6import copy
7import pickle
8import logging
9import random
10import subprocess
11
12from util import *
13from deter import fedid, generate_fedid
14from authorizer import authorizer, abac_authorizer
15from service_error import service_error
16from remote_service import xmlrpc_handler, soap_handler, service_caller
17
18from deter import topdl
19
20from access import access_base
21
22# Make log messages disappear if noone configures a fedd logger.  This is
23# something of an incantation, but basically it creates a logger object
24# registered to fedd.access if no other module above us has.  It's an extra
25# belt for the suspenders.
26class nullHandler(logging.Handler):
27    def emit(self, record): pass
28
29fl = logging.getLogger("fedd.access")
30fl.addHandler(nullHandler())
31
32
33# The plug-in itself.
34class access(access_base):
35    """
36    This is a demonstration plug-in for fedd.  It responds to all the
37    experiment_control requests and keeps internal state.  The allocations it
38    makes are simple integers associated with each valid request.  It makes use
39    of the general routines in access.access_base.
40
41    Detailed comments in the code and info at
42    """
43    def __init__(self, config=None, auth=None):
44        """
45        Initializer.  Pulls parameters out of the ConfigParser's access
46        section, and initializes simple internal state.  This version reads a
47        maximum integer to assign from the configuration file, while most other
48        configuration entries  are read by the base class. 
49
50        An access database in the cannonical format is also read as well as a
51        state database that is a hash of internal state.  Routines to
52        manipulate these are in the base class, but specializations appear
53        here.
54
55        The access database maps users to a simple string.
56        """
57
58        # Calling the base initializer, which reads canonical configuration
59        # information and initializes canonical members.
60        access_base.__init__(self, config, auth)
61        # Reading the maximum integer parameter from the configuration file
62
63        self.src_addr = config.get('access', 'interface_address')
64        self.router = config.get('access', 'gateway')
65        self.hostname = config.get('access', 'hostname')
66        # Storage for ephemeral ssh keys and host files
67        self.localdir = config.get('access', 'localdir')
68        # File containing the routing entries for external networks
69        self.external_networks = config.get('access', 'external_networks')
70        # values for locations of zebra and ospfd.
71        self.zebra = config.get('access', 'zebra')
72        if self.zebra is None:
73            self.zebra = '/usr/local/sbin/zebra'
74        self.ospfd = config.get('access', 'ospfd')
75        if self.ospfd is None:
76            self.ospfd = '/usr/local/sbin/ospfd'
77
78        # If this is a linux box that will be NATing, the iptables value
79        # must be the path of the iptables command and the nat_interface must
80        # be the nat interface.
81        self.iptables = config.get('access', 'iptables')
82        self.nat_interface = config.get('access', 'nat_interface')
83
84        self.ssh_identity = None
85
86        # hostname is the name of the ssh endpoint for the other side.  That
87        # side needs it to set up routing tables.  If hostname is not
88        # available, but an IP address is, use that.
89        if self.hostname is None:
90            if  self.src_addr is None:
91                raise service_error(service_error.server_config,
92                        'Hostname or interface_address must be set in config')
93            self.hostname = self.src_addr
94       
95        self.ssh_port = config.get('access', 'ssh_port', '22')
96
97        # authorization information
98        self.auth_type = config.get('access', 'auth_type') \
99                or 'abac'
100        self.auth_dir = config.get('access', 'auth_dir')
101        accessdb = config.get("access", "accessdb")
102        # initialize the authorization system.  We make a call to
103        # read the access database that maps from authorization information
104        # into local information.  The local information is parsed by the
105        # translator above.
106        if self.auth_type == 'abac':
107            #  Load the current authorization state
108            self.auth = abac_authorizer(load=self.auth_dir)
109            self.access = [ ]
110            if accessdb:
111                try:
112                    self.read_access(accessdb)
113                except EnvironmentError, e:
114                    self.log.error("Cannot read %s: %s" % \
115                            (config.get("access", "accessdb"), e))
116                    raise e
117        else:
118            raise service_error(service_error.internal, 
119                    "Unknown auth_type: %s" % self.auth_type)
120
121        # The superclass has read the state, but if this is the first run ever,
122        # we must initialise the running flag.  This plugin only supports one
123        # connection, so StartSegment will fail when self.state['running'] is
124        # true.
125        self.state_lock.acquire()
126        if 'running' not in self.state:
127            self.state['running'] = False
128        self.state_lock.release()
129
130        # These dictionaries register the plug-in's local routines for handline
131        # these four messages with the server code above.  There's a version
132        # for SOAP and XMLRPC, depending on which interfaces the plugin
133        # supports.  There's rarely a technical reason not to support one or
134        # the other - the plugin code almost never deals with the transport -
135        # but if a plug-in writer wanted to disable XMLRPC, they could leave
136        # the self.xmlrpc_services dictionary empty.
137        self.soap_services = {\
138            'RequestAccess': soap_handler("RequestAccess", self.RequestAccess),
139            'ReleaseAccess': soap_handler("ReleaseAccess", self.ReleaseAccess),
140            'StartSegment': soap_handler("StartSegment", self.StartSegment),
141            'TerminateSegment': soap_handler("TerminateSegment", 
142                self.TerminateSegment),
143            }
144        self.xmlrpc_services =  {\
145            'RequestAccess': xmlrpc_handler('RequestAccess',
146                self.RequestAccess),
147            'ReleaseAccess': xmlrpc_handler('ReleaseAccess',
148                self.ReleaseAccess),
149            'StartSegment': xmlrpc_handler("StartSegment", self.StartSegment),
150            'TerminateSegment': xmlrpc_handler('TerminateSegment',
151                self.TerminateSegment),
152            }
153        self.call_SetValue = service_caller('SetValue', log=self.log)
154        self.call_GetValue = service_caller('GetValue', log=self.log)
155
156    # ReleaseAccess come from the base class, this is a slightly modified
157    # RequestAccess from the base that includes a fedAttr to force this side to
158    # be active.
159    def RequestAccess(self, req, fid):
160        """
161        Handle an access request.  Success here maps the requester into the
162        local access control space and establishes state about that user keyed
163        to a fedid.  We also save a copy of the certificate underlying that
164        fedid so this allocation can access configuration information and
165        shared parameters on the experiment controller.
166        """
167
168        self.log.info("RequestAccess called by %s" % fid)
169        # The dance to get into the request body
170        if req.has_key('RequestAccessRequestBody'):
171            req = req['RequestAccessRequestBody']
172        else:
173            raise service_error(service_error.req, "No request!?")
174
175        # Base class lookup routine.  If this fails, it throws a service
176        # exception denying access that triggers a fault response back to the
177        # caller.
178        found,  owners, proof = self.lookup_access(req, fid)
179        self.log.info(
180                "[RequestAccess] Access granted local creds %s" % found)
181        # Make a fedid for this allocation
182        allocID, alloc_cert = generate_fedid(subj="alloc", log=self.log)
183        aid = unicode(allocID)
184
185        # Store the data about this allocation:
186        self.state_lock.acquire()
187        self.state[aid] = { }
188        self.state[aid]['user'] = found
189        self.state[aid]['owners'] = owners
190        self.state[aid]['auth'] = set()
191        # Authorize the creating fedid and the principal representing the
192        # allocation to manipulate it.
193        self.append_allocation_authorization(aid, 
194                ((fid, allocID), (allocID, allocID)))
195        self.write_state()
196        self.state_lock.release()
197
198        # Create a directory to stash the certificate in, ans stash it.
199        try:
200            f = open("%s/%s.pem" % (self.certdir, aid), "w")
201            print >>f, alloc_cert
202            f.close()
203        except EnvironmentError, e:
204            raise service_error(service_error.internal, 
205                    "Can't open %s/%s : %s" % (self.certdir, aid, e))
206        self.log.debug('[RequestAccess] Returning allocation ID: %s' % allocID)
207        msg = { 
208                'allocID': { 'fedid': allocID }, 
209                'fedAttr': [{ 'attribute': 'nat_portals', 'value': 'True' }],
210                'proof': proof.to_dict()
211                }
212        return msg
213
214    def validate_topology(self, top):
215        '''
216        Validate the topology.  Desktops can only be single connections.
217        Though the topology will include a portal and a node, the access
218        controller will implement both on one node.
219
220        As more capabilities are added to the contoller the constraints here
221        will relax.
222        '''
223
224        comps = []
225        for e in top.elements:
226            if isinstance(e, topdl.Computer): comps.append(e)
227        if len(comps) > 2: 
228            raise service_error(service_error.req,
229                    "Desktop only supports 1-node subexperiments")
230
231        portals = 0
232        for c in comps:
233            if c.get_attribute('portal') is not None: 
234                portals += 1
235                continue
236            if len(c.interface) > 1:
237                raise service_error(service_error.req,
238                        "Desktop Node has more than one interface")
239            i  = c.interface[0]
240            if len(i.subs) > 1: 
241                raise service_error(service_error.req,
242                        "Desktop Node has more than one substate on interface")
243            sub = i.subs[0]
244            for i in sub.interfaces:
245                if i.element not in comps:
246                    raise service_error(service_error.req,
247                            "Desktop Node connected to non-portal")
248
249        if portals > 1:
250            raise service_error(service_error.req,
251                    "Desktop segment has more than one portal")
252        return True
253
254    def validate_connInfo(self, connInfo):
255        if len(connInfo) != 1: 
256            raise service_error(service_error.req,
257                    "Desktop segment requests multiple connections")
258        if connInfo[0]['type'] != 'ssh':
259            raise service_error(service_error.req,
260                    "Desktop segment requires ssh connecton")
261        return True
262
263    def export_store_info(self, certfile, connInfo):
264        '''
265        Tell the other portal node where to reach this desktop.  The other side
266        uses this information to set up routing, though the ssh_port is unused
267        as the Desktop always initiates ssh connections.
268        '''
269        values = { 'peer': self.hostname, 'ssh_port': self.ssh_port }
270        for c in connInfo:
271            for p in c.get('parameter', []):
272                if p.get('type','') == 'input': continue
273                pname = p.get('name', '')
274                key = p.get('key', '')
275                surl = p.get('store', '')
276                if pname not in values:
277                    self.log('Unknown export parameter: %s'  % pname)
278                    continue
279                val = values[pname]
280                req = { 'name': key, 'value': val }
281                self.log.debug('Setting %s (%s) to %s on %s' % \
282                        (pname, key,  val, surl))
283                self.call_SetValue(surl, req, certfile)
284
285    def set_route(self, dest, script, gw=None, src=None):
286        if sys.platform.startswith('freebsd'):
287            if src is not None and gw is not None:
288                raise service_error(service_error.internal, 
289                        'FreeBSD will not route based on src address')
290            elif src is not None:
291                raise service_error(service_error.internal, 
292                        'FreeBSD will not route based on src address')
293            elif gw is not None:
294                print >>script, 'route add %s %s' % (dest, gw)
295        elif sys.platform.startswith('linux'):
296            if src is not None and gw is not None:
297                print >>script, 'ip route add %s via %s src %s' % \
298                        (dest, gw, src)
299            elif src is not None:
300                print >>script, 'ip route add %s src %s' % \
301                        (dest, src)
302            elif gw is not None:
303                print >>script, 'ip route add %s via %s' % (dest, gw)
304        else:
305            raise service_error(service_error.internal, 
306                    'Unknown platform %s' % sys.platform)
307
308    def unset_route(self, dest, script):
309        rv = 0
310        if sys.platform.startswith('freebsd'):
311            print >>script, 'route delete %s' % dest
312        elif sys.platform.startswith('linux'):
313            print >>script, 'ip route delete %s' % dest
314
315    def find_a_peer(self, addr): 
316        '''
317        Find another node in the experiment that's on our subnet.  This is a
318        hack to handle the problem that we really cannot require the desktop to
319        dynamically route.  Will be improved by distributing static routes.
320        '''
321
322        peer = None
323        hosts = os.path.join(self.localdir, 'hosts')
324        p = addr.rfind('.')
325        if p == -1:
326            raise service_error(service_error.req, 'bad address in topology')
327        prefix = addr[0:p]
328        addr_re = re.compile('(%s.\\d+)' % prefix)
329        try:
330            f = open(hosts, 'r')
331            for line in f:
332                m = addr_re.search(line)
333                if m is not None and m.group(1) != addr:
334                    peer = m.group(1)
335                    break
336            else:
337                raise service_error(service_error.req, 
338                        'No other nodes in this subnet??')
339        except EnvironmentError, e:
340            raise service_error(service_error.internal, 
341                    'Cannot open %s: %s' % (e.filename, e.strerror))
342        return peer
343
344
345
346
347    def configure_desktop(self, top, connInfo):
348        '''
349        Build the connection.  Establish routing to the peer if using a
350        separate interface, wait until the other end confirms setup, establish
351        the ssh layer-two tunnel (tap), assign the in-experiment IP address to
352        the tunnel and establish routing to the experiment through the tap.
353        '''
354
355
356        # get the peer and ssh port from the portal and our IP from the other
357        peer = None
358        port = None
359        my_addr = None
360        my_name = None
361        for e in top.elements:
362            if not isinstance(e, topdl.Computer): continue
363            if e.get_attribute('portal') is None: 
364                my_name = e.name
365                # there should be one interface with one IPv4 address
366                if len(e.interface) <1 :
367                    raise service_error(service_error.internal,
368                            'No interface on experiment node!?!?')
369                my_addr = e.interface[0].get_attribute('ip4_address')
370            else:
371                for ci in connInfo:
372                    if ci.get('portal', '') != e.name: continue
373                    peer = ci.get('peer')
374                    port = '22'
375                    for a in ci.get('fedAttr', []):
376                        if a['attribute'] == 'ssh_port': port = a['value']
377
378        # XXX scan hosts for IP addresses and compose better routing entry
379       
380        if not all([peer, port, my_addr]):
381            raise service_error(service_error.req, 
382                    'Cannot find all config parameters %s %s %s' % (peer, port, my_addr))
383
384        exp_peer = self.find_a_peer(my_addr)
385
386        cscript = os.path.join(self.localdir, 'connect')
387        dscript = os.path.join(self.localdir, 'disconnect')
388        local_hosts = os.path.join(self.localdir, 'hosts')
389        zebra_conf = os.path.join(self.localdir, 'zebra.conf')
390        ospfd_conf = os.path.join(self.localdir, 'ospfd.conf')
391        try:
392            f = open(cscript, 'w')
393            print >>f, '#!/bin/sh'
394            # This picks the outgoing interface to the experiment using the
395            # routing system.
396            self.set_route(peer, f, self.router, self.src_addr)
397            # Wait until the other end reports that it is configured py placing
398            # a file this end can access into its local file system.  Try once
399            # a minute.
400            print >>f,'while ! /usr/bin/scp -o "StrictHostKeyChecking no" -i %s %s:/usr/local/federation/etc/prep_done /dev/null; do' % (self.ssh_identity, peer)
401            print >>f, 'sleep 60; done'
402            print >>f, ('ssh -w 0:0 -p %s -o "Tunnel ethernet" ' + \
403                    '-o "StrictHostKeyChecking no" -i %s %s perl -I/usr/local/federation/lib /usr/local/federation/bin/setup_bridge.pl --tapno=0 --addr=%s &') % \
404                    (port, self.ssh_identity, peer, my_addr)
405            # This should give the tap a a chance to come up
406            print >>f,'sleep 10'
407            # Add experiment nodes to hosts
408            print >>f, 'cp /etc/hosts /etc/hosts.DETER.fedd.hold'
409            print >>f, 'echo "#--- BEGIN FEDD ADDITIONS ---" >> /etc/hosts'
410            print >>f, 'cat %s >> /etc/hosts' % local_hosts
411            print >>f, 'echo "#--- END FEDD ADDITIONS ---" >> /etc/hosts'
412            # Assign tap address and route experiment connections through it.
413            print >>f, 'ifconfig tap0 %s netmask 255.255.255.0 up' % \
414                    my_addr
415            print >>f, '%s -d -f %s' % (self.zebra, zebra_conf)
416            print >>f, '%s -d -f %s' % (self.ospfd, ospfd_conf)
417            if self.iptables is not None and self.nat_interface is not None:
418                print >>f, '%s -t nat -A POSTROUTING -o %s -j MASQUERADE' %\
419                        (self.iptables, self.nat_interface)
420                print >>f, ('%s -A FORWARD -i %s -o tap0 -m state ' +
421                    '--state RELATED,ESTABLISHED -j ACCEPT') % \
422                            (self.iptables, self.nat_interface)
423                print >>f, '%s -A FORWARD -i tap0 -o %s -j ACCEPT' % \
424                        (self.iptables, self.nat_interface)
425            f.close()
426            os.chmod(cscript, 0755)
427            f = open(dscript, 'w')
428            print >>f, '#!/bin/sh'
429            if self.iptables is not None and self.nat_interface is not None:
430                print >>f, '%s -t nat -D POSTROUTING -o %s -j MASQUERADE' %\
431                        (self.iptables, self.nat_interface)
432                print >>f, ('%s -D FORWARD -i %s -o tap0 -m state ' +
433                    '--state RELATED,ESTABLISHED -j ACCEPT') % \
434                            (self.iptables, self.nat_interface)
435                print >>f, '%s -D FORWARD -i tap0 -o %s -j ACCEPT' % \
436                        (self.iptables, self.nat_interface)
437            # Linux ?
438            print >>f, 'ifconfig tap0 destroy'
439            self.unset_route(peer, f)
440            print >>f, 'mv /etc/hosts.DETER.fedd.hold /etc/hosts'
441            print >>f, 'kill `cat /var/run/quagga/ospfd.pid`'
442            print >>f, 'kill `cat /var/run/quagga/zebra.pid`'
443            if self.iptables is not None and self.nat_interface is not None:
444                print >>f, '%s -t nat -D POSTROUTING -o %s -j MASQUERADE' %\
445                        (self.iptables, self.nat_interface)
446                print >>f, ('%s -D FORWARD -i %s -o tap0 -m state ' +
447                    '--state RELATED,ESTABLISHED -j ACCEPT') % \
448                            (self.iptables, self.nat_interface)
449                print >>f, '%s -D FORWARD -i tap0 -o %s -j ACCEPT' % \
450                        (self.iptables, self.nat_interface)
451            f.close()
452            os.chmod(dscript, 0755)
453            f = open(zebra_conf, 'w')
454            print >>f, 'hostname %s' % my_name
455            print >>f, 'interface tap0'
456            if  self.external_networks is not None:
457                try:
458                    extern = open(self.external_networks, 'r')
459                    if extern is not None:
460                        for l in extern:
461                            print >>f, "%s" % l.strip()
462                        extern.close()
463                except EnvironmentError:
464                    # No external_networks or problem reading it, ignore
465                    pass
466            f.close()
467            os.chmod(zebra_conf, 0644)
468            f = open(ospfd_conf, 'w')
469            print >>f, 'hostname %s' % my_name
470            print >>f, 'router ospf'
471            print >>f, ' redistribute static'
472            print >>f, ' network %s/24 area 0.0.0.2' % my_addr
473        except EnvironmentError, e:
474            raise service_error(service_error.internal, 
475                    'Cannot create connect %s: %s' % (e.filename, e.strerror))
476        script_log = open('/tmp/connect.log', 'w')
477        subprocess.Popen(['sudo', '/bin/sh', cscript], stdout=script_log, stderr=script_log)
478        return True
479
480    def StartSegment(self, req, fid):
481        """
482        Start a segment.  In this simple skeleton, this means to parse the
483        request and assign an unassigned integer to it.  We store the integer
484        in the persistent state.
485        """
486        try:
487            req = req['StartSegmentRequestBody']
488            # Get the request topology.  If not present, a KeyError is thrown.
489            topref = req['segmentdescription']['topdldescription']
490            # The fedid of the allocation we're attaching resources to
491            auth_attr = req['allocID']['fedid']
492        except KeyError:
493            raise service_error(service_error.req, "Badly formed request")
494
495        # String version of the allocation ID for keying
496        aid = "%s" % auth_attr
497        # Authorization check
498        access_ok, proof = self.auth.check_attribute(fid, auth_attr, 
499                with_proof=True)
500        if not access_ok:
501            raise service_error(service_error.access, "Access denied", 
502                    proof=proof)
503        else:
504            # See if this is a replay of an earlier succeeded StartSegment -
505            # sometimes SSL kills 'em.  If so, replay the response rather than
506            # redoing the allocation.
507            self.state_lock.acquire()
508            # Test and set :-)
509            running = self.state['running']
510            self.state['running'] = True
511            retval = self.state[aid].get('started', None)
512            self.state_lock.release()
513            if retval:
514                self.log.warning(
515                        "[StartSegment] Duplicate StartSegment for %s: " \
516                                % aid + \
517                        "replaying response")
518                return retval
519            if running:
520                self.log.debug('[StartSegment] already running')
521                raise service_error(service_error.federant,
522                        'Desktop is already in an experiment')
523
524        certfile = "%s/%s.pem" % (self.certdir, aid)
525
526        # Convert the topology into topdl data structures.  Again, the
527        # skeletion doesn't do anything with it, but this is how one parses a
528        # topology request.
529        if topref: topo = topdl.Topology(**topref)
530        else:
531            raise service_error(service_error.req, 
532                    "Request missing segmentdescription'")
533
534        err = None
535        try:
536            self.validate_topology(topo)
537
538            # The attributes of the request.  The ones we care about are the ssh
539            # keys to operate the tunnel.
540            attrs = req.get('fedAttr', [])
541            for a in attrs:
542                # Save the hosts and ssh_privkeys to our local dir
543                if a['attribute'] in ('hosts', 'ssh_secretkey'):
544                    self.log.debug('Getting %s from %s' % \
545                            (a['attribute'], a['value']))
546                    get_url(a['value'], certfile, self.localdir, log=self.log)
547                    base = os.path.basename(a['value'])
548                    if a['attribute'] == 'ssh_secretkey':
549                        self.ssh_identity = os.path.join(self.localdir, base)
550                    os.chmod(os.path.join(self.localdir, base), 0600)
551                else:
552                    self.log.debug('Ignoring attribute %s' % a['attribute'])
553
554            # Gather connection information and exchange parameters.
555            connInfo = req.get('connection', [])
556            self.validate_connInfo(connInfo)
557            self.export_store_info(certfile, connInfo)
558            self.import_store_info(certfile, connInfo)
559
560            #build it
561            self.configure_desktop(topo, connInfo)
562        except service_error, e:
563            err = e
564
565        # Save the information
566        if err is None:
567            # It's possible that the StartSegment call gets retried (!).  if
568            # the 'started' key is in the allocation, we'll return it rather
569            # than redo the setup.  The integer allocation was saved when we
570            # made it.
571            self.state_lock.acquire()
572            self.state[aid]['started'] = { 
573                    'allocID': req['allocID'],
574                    'allocationLog': "Allocatation complete",
575                    'segmentdescription': { 'topdldescription': topo.to_dict() },
576                    'proof': proof.to_dict(),
577                    }
578            retval = copy.deepcopy(self.state[aid]['started'])
579            self.write_state()
580            self.state_lock.release()
581        else:
582            # Something bad happened - clear the "running" flag so we can try
583            # again
584            self.state_lock.acquire()
585            self.state['running'] = False
586            self.state_lock.release()
587            raise err
588
589        return retval
590
591    def TerminateSegment(self, req, fid):
592        """
593        Remove the resources associated with th eallocation and stop the music.
594        In this example, this simply means removing the integer we allocated.
595        """
596        # Gather the same access information as for Start Segment
597        try:
598            req = req['TerminateSegmentRequestBody']
599        except KeyError:
600            raise service_error(service_error.req, "Badly formed request")
601
602        auth_attr = req['allocID']['fedid']
603        aid = "%s" % auth_attr
604
605        self.log.debug("Terminate request for %s" %aid)
606        # Check authorization
607        access_ok, proof = self.auth.check_attribute(fid, auth_attr, 
608                with_proof=True)
609        if not access_ok:
610            raise service_error(service_error.access, "Access denied", 
611                    proof=proof)
612        cscript = os.path.join(self.localdir, 'connect')
613        dscript = os.path.join(self.localdir, 'disconnect')
614        # Do the work of disconnecting
615        if os.path.exists(dscript):
616            self.log.debug('calling %s' % dscript)
617            rv = subprocess.call(['sudo', '/bin/sh', dscript])
618            if rv != 0:
619                self.log.warning('%s had an error: %d' % (dscript, rv))
620        else:
621            self.log.warn('No disconnection script!?')
622
623        try:
624            for bfn in os.listdir(self.localdir):
625                fn = os.path.join(self.localdir, bfn)
626                self.log.debug('Removing %s' % fn)
627                if os.path.exists(fn):
628                    os.remove(fn)
629        except EnvironmentError, e:
630            self.log.warn('Failed to remove %s: %s' % (e.filename, e.strerror))
631
632        self.ssh_identity = None
633
634        self.state_lock.acquire()
635        self.state['running'] = False
636        self.state_lock.release()
637   
638        return { 'allocID': req['allocID'], 'proof': proof.to_dict() }
Note: See TracBrowser for help on using the repository browser.