root/tags/v1.0/provisioner.py

Revision 29, 28.4 KB (checked in by daedalus, 4 years ago)

* Manual bailout when in authoritarian mode now works correctly.
* Fixed up some of the log level and stats reporting.
* Added 'backout' mode, that will only run the backout portion of changes. More

useful for testing, but can also be used to undo provisioning by repurposing
the same set of templates used for provisioning.

Line 
1"""
2Provisioners contact remote devices and apply changes to them.
3
4Should be implemented as a Protocol/Factory style thing to fit
5with the async mechanism. A Provisioner is an interface applied
6to a Factory that creates a, for example, ssh session with a
7remote site.
8
9Need to define some sort of Change protocol, to handle a
10generic send/expect style interface.
11"""
12import re
13
14from zope.interface import Interface, implements
15
16from twisted.internet import defer, reactor
17from twisted.internet import protocol
18from twisted.python import log as tlog
19from twisted.internet.error import ProcessDone, ProcessTerminated, AlreadyCancelled, AlreadyCalled
20
21from change import ChangeFailed, ChangeConditionFailure, CHANGE_STATE
22import util
23
24import logging
25import traceback
26
27import debug
28log = logging.getLogger('modipy')
29
30from twisted.internet.base import DelayedCall
31DelayedCall.debug = True
32
33class IProvisioner(Interface):
34    """
35    Defines the Provisioner interface
36    """
37
38    def __init__(self, name='', namespace={}):
39        """
40        """
41
42    def perform_change(self, ignored, change):
43        """
44        Applies a change to the devices the change is defined to affect.
45        """
46
47    def apply_change(self, device, change):
48        """
49        Apply a change to a specific device.
50        """
51
52    def backout_change(self, device, change):
53        """
54        Back out a change that was applied to a specific device.
55        """
56
57class UserBailout(Exception):
58    """
59    User denied a change command from executing in Authoritarian mode.
60    This causes the program to bail out complete at exactly that point.
61    """
62
63class Provisioner:
64    """
65    A Provisioner performs the actual execution of Changesets. It could, for example,
66    connect to a remote system via SSH and then execute the Changeset Actions.
67    """
68
69    implements( IProvisioner, )
70
71    def __init__(self, name='', namespace={}, authoritarian=False, **kwargs):
72        self.name = name
73        self.connectedDevice = None
74        self.namespace = namespace
75        self.authoritarian = authoritarian
76
77        log.debug("My namespace is: %s", self.namespace)
78
79    def parse_config_node(self, node):
80        pass
81
82    def perform_change(self, ignored, change, namespace={}, backout=False):
83        """
84        Perform a change on one or more (potentially) remote entities.
85
86        @param namespace: The global namespace passed in to the provisioner.
87               This gets merged with the provisioner, device and change namespaces.
88        """
89        self.change_ok = {}
90        self.change_failed = {}
91        self.backout_ok = {}
92        self.backout_failed = {}
93
94        log.debug("perform change with namespace: %s", namespace)
95       
96        # get the list of devices the change should be applied to
97        devices = change.devices
98
99        d = defer.succeed(None)
100       
101        if change.serial_mode:
102
103            # apply the change to each device one after the other,
104            log.debug("change %s will be performed on devices: %s", change.name, devices)
105            for device in devices:
106                log.debug("performing change '%s' on device '%s'", change.name, device)
107
108                namespace['provisioner.name'] = self.name
109                namespace['provisioner.type'] = self.__class__.__name__
110
111                namespace['change.name'] = change.name
112                namespace['change.type'] = change.__class__.__name__
113
114                namespace['device.name'] = device.name
115                namespace['device.fqdn'] = device.fqdn
116                namespace['device.ipaddress'] = device.ipaddress
117
118                namespace.update(self.namespace)
119                namespace.update(change.namespace)
120                namespace.update(device.namespace)
121
122                if backout:
123                    # Just do the backout portion
124                    d.addCallback(self.backout_change, device, change, namespace)
125
126                else:
127                    log.debug("applying change with namespace: %s", namespace)
128                    d.addCallback(self.apply_change, device, change, namespace)
129                   
130                    d.addCallback(self.change_complete_success, change, namespace)
131                    d.addErrback(self.change_failure, change, namespace)
132                    pass
133                pass
134            pass
135        return d
136
137    def apply_change(self, ignored, device, change, namespace={}):
138        """
139        Apply the change to a device
140        """
141        # connect to the entity that is having the change applied to it
142        d = self.connect(device, namespace)
143        d.addCallback(self.do_pre_apply_check, device, change, namespace)
144        d.addCallbacks(self.pre_apply_success, self.pre_apply_failed, callbackArgs=(device, change, namespace), errbackArgs=(device, change, namespace))
145        #d.addCallback(self.pre_apply_success, device, change, namespace)
146        #d.addErrback(self.pre_apply_failed, change, namespace)
147        return d
148
149    def change_apply_success(self, result, device, change, namespace):
150        """
151        Change was applied successfully.
152        """
153        log.debug("change applied to device '%s' successfully." % device)
154        log.debug("result: %s", result)
155        # mark the device as having had the change applied to it
156        self.change_ok[device] = change
157
158        # do the post-change checking
159        return change.test_apply_success(self, result, namespace)
160
161    def change_apply_failed(self, failure, device, change, namespace):
162
163        log.error("Change '%s' failed to apply", change.name)
164        e = failure.check( ChangeConditionFailure, UserBailout )
165        if e:
166            log.error("  failure was: %s, %s", failure.type, failure.value )
167
168            # If we want to bail out, bail now
169            if failure.type == UserBailout:
170                return defer.fail(failure)
171        else:
172            log.error("Unhandled failure in provisioner.change_apply_failed()")
173            tlog.err(failure)
174       
175        self.change_failed[device] = failure
176        change.set_state('total_failure')
177       
178        d = self.backout_change(None, device, change, namespace)
179
180        if change.on_fail_continue:
181            log.info("Attempting to continue, despite failure...")
182            change.set_state('partial_failure')
183            pass
184
185        elif change.backout_all:
186            for device in self.change_ok.keys():
187                d.addCallback( self.backout_change, device, change, namespace )
188
189                pass
190            pass
191
192        return d
193
194    def change_failure(self, failure, change, namespace):
195        log.debug("Change failure for %s: %s", change.name, failure.value)
196        if change.state in [ CHANGE_STATE['pending'], CHANGE_STATE['retry'] ]:
197            # If the change is marked as 'on_fail: retry', let it retry
198            if change.can_retry():
199                change.state = CHANGE_STATE['retry']
200            else:
201                change.state = CHANGE_STATE['total_failure']
202        else:
203            log.error("change_failure unhandled change state: %s", change.state)
204            raise ValueError("Invalid change state: %s" % change.state)
205
206        # Propogate a UserBailout failure
207        if failure.type == UserBailout:
208            return failure
209       
210        #tlog.err(failure)
211
212    def change_complete_success(self, result, change, namespace):
213        if change.state in [ CHANGE_STATE['pending'], CHANGE_STATE['retry'] ]:
214            change.state = CHANGE_STATE['success']
215            log.info("Change '%s' was a success!", change.name)
216        else:
217            log.error("change_complete_success in weird state: %s", change.state)
218            raise ValueError("wrong change state: %s" % change.state)
219            change.state = CHANGE_STATE['backout_ok']
220            log.info("Change '%s' backed out successfully", change.name)
221
222    def do_pre_apply_check(self, result, device, change, namespace):
223        # perform pre-implementation checks
224        log.info("Doing pre-apply check for change '%s' on device '%s'", change.name, device)
225        return change.pre_apply_check(self, namespace)
226
227    def pre_apply_success(self, result, device, change, namespace):
228        log.info("Pre-apply check passed for change '%s' on device '%s'", change.name, device)
229        d = self.do_apply(result, device, change, namespace)
230        d.addCallback(self.change_apply_success, device, change, namespace)
231        d.addErrback(self.change_apply_failed, device, change, namespace)
232        return d
233
234    def pre_apply_failed(self, failure, device, change, namespace):
235        log.error("Pre-apply check failed for change '%s' on device '%s'", change.name, device)
236        raise failure
237
238    def do_apply(self, result, device, change, namespace):
239        """
240        Apply the actual change
241        """
242        log.info("Applying change '%s' to device '%s'", change.name, device)
243        return change.apply(self, namespace)
244
245    def backout_change(self, ignored, device, change, namespace):
246        """
247        Back out a change that was applied to a device.
248        """
249        log.info("Backing out change '%s' from device '%s'", change.name, device)
250        d = change.backout(self, namespace)
251        d.addCallback(change.test_backout_success, self, namespace)
252        d.addCallback(self.backout_success, device, change, namespace)
253        d.addErrback(self.backout_failure, device, change, namespace)
254        return d
255
256    def backout_success(self, ignored, device, change, namespace):
257        log.info("Successfully backed out change: '%s' from device '%s'", change.name, device)
258        change.state = CHANGE_STATE['backout_ok']
259        self.backout_ok[device] = change
260
261    def backout_failure(self, failure, device, change, namespace):
262        log.critical("Backout of change '%s' failed for device '%s'!", change.name, device)
263        change.state = CHANGE_STATE['backout_failed']
264        self.backout_failed[device] = change
265        #tlog.err(failure)
266        return failure
267
268class CommandProvisioner(Provisioner):
269    """
270    A command provisioner provides methods for implementing command changes on
271    remote devices.
272    """
273
274    command_re = re.compile(r'^(?P<start>.*)"(?P<quoted>.*)"(?P<end>.*)$')
275
276    def parse_config_node(self, node):
277        Provisioner.parse_config_node(self, node)
278        pass
279
280    def __init__(self, name='', namespace={}, authoritarian=False, command_timeout=300):
281        Provisioner.__init__(self, name, namespace, authoritarian)
282        self.command_timeout = int(command_timeout)
283
284    def split_command(self, cmdstring):
285        """
286        Split a command string, returning everything before
287        a quoted section, the quoted section, and then everything
288        afterwards.
289        """
290        cmd_split = []
291        m = self.command_re.match(cmdstring)
292        if m:
293            cmd_split.append( m.group('quoted') )
294            cmd_split.extend(m.group('end').split())
295
296            splitpart = self.split_command(m.group('start'))
297            splitpart.extend(cmd_split)
298            cmd_split = splitpart
299            return cmd_split
300
301        return cmdstring.split()
302
303    def run_command(self, ignored, cmdstring):
304        """
305        Execute a command on the element, returning the results of the command.
306
307        @returns: (return_code, stdout, stderr) where
308          return_code: is the exit code of the command, as an integer
309          stdout: is a string containing anything printed to STDOUT
310          stderr: is a string containing anything printed to STDERR
311        """
312        log.debug("Executing provisioning command: %s" % cmdstring)
313        return defer.succeed( (0, '', '') )
314
315    def connect(self, device):
316        pass
317
318class ConnectingProvisioner(CommandProvisioner):
319    """
320    A ConnectingProvisioner runs a command to connect to the remote device
321    and then runs a series of commands by sending them to the remote
322    device via STDIN, and receives the output from STDOUT and STDERR.
323    """
324    def parse_config_node(self, node):
325        CommandProvisioner.parse_config_node(self, node)
326
327    def connect(self, device):
328        self.connectedDevice = device
329       
330        self.childConnectingDefer = defer.Deferred()
331        self.childConnectingTimeout = reactor.callLater(5, self.childConnectTimeout)
332
333        log.debug("connecting to device '%s'", device)
334
335        cmd = [ '/usr/bin/ssh',
336                '-T',
337                '-o','BatchMode=yes',
338                '-i','/home/daedalus/.ssh/localconnect',
339                'localhost' ]
340        #cmd = [ '/usr/bin/ssh', 'localhost' ]
341        self.ep = ExpectProtocol(self)
342        self.p = reactor.spawnProcess(self.ep, cmd[0], cmd )
343
344        #log.debug("timeout thing: %s" % self.childConnectingTimeout)
345
346        return self.childConnectingDefer
347
348    def childConnectTimeout(self):
349        """
350        This gets called if my connection via the child process doesn't
351        complete within a certain timeout value.
352        """
353        log.error("Connection to child timed out!")
354        self.ep.transport.loseConnection()
355        self.childConnectingDefer.errback( Exception("Connection timed out") )
356
357    def childConnectSuccess(self):
358        """
359        This gets called by the child when it finishes connecting.
360        Cancel the timeout and notify waiting things that the connection
361        completed successfully.
362        """
363        log.debug("successfully connected to child")
364        self.childConnectingTimeout.cancel()
365        log.debug("sent timeout cancel")
366        self.childConnectingDefer.callback(self)
367
368    def childConnectFailure(self):
369        """
370        This gets called by the child when it finishes connecting.
371        Cancel the timeout and notify waiting things that the connection
372        completed successfully.
373        """
374        log.error("connection by child failed")
375        self.childConnectingTimeout.cancel()
376        self.childConnectingDefer.errback(ValueError("Child connection failure!"))
377
378    def run_commands(self, ignored, expectset, namespace):
379        log.debug('running commands on remote host: %s' % expectset)
380
381        return self.ep.run_remote_commands(expectset)
382
383        # Run the command using the ExpectProtocol
384        #return defer.succeed( (0, '', '') )
385   
386class ExpectProtocol(protocol.Protocol):
387    """
388    The ExpectProtocol allows you to supply a series of
389    expect/send pairs that will be used to handle the
390    processing of received data.
391
392    Each expect/send pair is a tuple of the form:
393    (expect_re, send_string), where:
394
395    expect_re: is a regular expression that will be used
396    by the re module to parse received data. Each time
397    dataReceived() is triggered, the data is added to a
398    buffer, which is matched against the re. If a match is
399    successful, the corresponding send_dict is processed to
400    generate the data to send in response, and the receive
401    buffer is emptied.
402    If a match is unsuccessful, execution returns to wait
403    for more data.
404
405    send_string: is a series of octets to send in response
406    to the expect string that was received. % operator replacement
407    is possible using %(name)s syntax, with the (name) values
408    being populated from the previous expect re.match operation.
409    """
410
411    def __init__(self, provisioner):
412       
413        self.parent = provisioner
414        self.saidHello = False
415        self.waitingForCommand = None
416        self.databuf = ''
417        self.data_wait_timeout = None
418
419        self.results = ''
420
421    def connectionMade(self):
422        """
423        Called when the connection to the remote entity is
424        made, whatever it is.
425        """
426        log.debug("Connected to remote thing! Hurray!")
427        self.parent.childConnectSuccess()
428
429    def childConnectionLost(self, fd):
430        log.debug("Lost connection to child process fd %s" % fd)
431
432    def childDataReceived(self, fd, data):
433        log.debug("received data from child process %s: >%s<" % (fd, data) )
434
435        try:
436            self.databuf += data
437            log.debug("databuf: >%s<" % self.databuf)
438            self.do_expect_processing()
439
440        # force any unexpected errors to call the errback
441        except Exception, e:
442            self.waitingForCommand.errback(e)
443
444        # ignore stuff from STDERR?
445        #if fd == 2:
446        #    pass
447
448    def processEnded(self, status_object):
449        log.debug("process ended: %s" % status_object)
450
451        if isinstance(status_object.value, ProcessDone):
452            log.debug("process exited ok")
453        elif isinstance(status_object.value, ProcessTerminated):
454            log.debug("process did not exit ok: %s" % status_object.value.exitCode)
455            self.parent.childConnectFailure()
456
457        log.debug("handled error")
458
459    def connectionFinished(self, status):
460        self.parent.childConnected()
461
462    def run_remote_commands(self, expectset):
463        """
464        Run a set of commands, using expect processing to do it.
465        """
466        self.expectset = expectset
467        self.waitingForCommand = defer.Deferred()
468        self.do_expect_processing()
469        return self.waitingForCommand
470
471    def do_expect_processing(self):
472
473        if self.data_wait_timeout:
474            self.data_wait_timeout.cancel()
475
476        # get the first expression/cmd pair
477        expr, cmdstring = self.expectset[0]
478        self.expectset = self.expectset[1:]
479       
480        # check the expr to see if we should process it
481        if expr is None:
482            if cmdstring is not None:
483                log.debug("No expression to wait for. Running command '%s' immediately." % cmdstring)
484                self.issue_command(cmdstring)
485            else:
486                log.debug("cmdstring is 'None', so nothing to do")
487
488        else:
489            log.debug("checking for '%s' in databuf: %s", expr, self.databuf)
490            re_expr = re.compile(expr)
491            log.debug("compiled expr")
492
493            # Match in multi-line mode, and '.' character matches \n
494            match = re_expr.search(self.databuf)
495            log.debug("match object is: %s", match)
496            if match:
497                # check the current recv buffer to see if the expect string is a match
498                log.debug("Found expr '%s' in databuf! Yay!" % expr)
499
500                # reset the databuffer whenever we match something
501                # FIXME: maybe not do this?
502                self.results += self.databuf
503                self.databuf = ''
504               
505                if cmdstring is not None:
506                    self.issue_command(cmdstring)
507                    pass
508                else:
509                    # If the command is None, this is the end of processing,
510                    # so shut down the connection and trigger command success processing.
511                    self.transport.loseConnection()
512                    self.waitingForCommand.callback(self.results)
513                   
514            else:
515                log.debug("cannot find '%s' in databuf. Waiting for more data..." % expr)
516                self.expectset.insert(0, (expr, cmdstring) )
517                self.data_wait_timeout = reactor.callLater(10, self.data_wait_too_long)
518                pass
519            pass
520
521    def issue_command(self, cmdstring):
522        """
523        Write the command to the remote device.
524        """
525        # If we're in authoritarian mode, wait for confirmation
526        # that we should execute the command.
527        if self.parent.authoritarian:
528            log.debug("Authoritarian mode. Waiting for ok to proceed...")
529            isok = raw_input("Issue command (y/n)[n]?> ")
530            if isok.startswith('y'):
531                log.debug("Ok! Let's continue!")
532            else:
533                log.info(" Bailing out at your command.")
534                raise UserBailout("Bailing out at your command")
535                #self.waitingForCommand.errback("User requested manual bailout.")
536                return
537            pass
538        log.debug("running command: %s" % cmdstring)                   
539        self.transport.writeToChild(0, '%s\n' % cmdstring)
540        self.data_wait_timeout = reactor.callLater(10, self.data_wait_too_long)
541
542    def data_wait_too_long(self):
543        """
544        We have waited for data for too long, so return an error
545        """
546        self.waitingForCommand.errback( ValueError("Timed out waiting for command output.") )
547
548
549class MultiConnectingProvisioner(ConnectingProvisioner):
550    """
551    A provisioner that runs a command that sets up the connection to
552    the remote device for each and every command that needs to be run.
553    The command to be run is appended to the connecting command.
554    It is designed to be run with something like ssh, eg:
555    ssh remotehost <command1>
556    ssh remotehost <command2>
557    """
558
559    def parse_config_node(self, node):
560        """
561        Deals with additional configuration that I expect to be provided with.
562        """
563        ConnectingProvisioner.parse_config_node(self, node)
564        log.debug("Parsing config node: %s", node)
565        # parse command definition
566        if node.tag == 'command':
567            self.command = str(node.text)
568            log.debug("Set provisioner command to: %s", self.command)
569
570    def connect(self, device, namespace):
571        """
572        Don't actually connect, but take note of the device.
573        """
574        self.device = device
575        # perform variable substitution on the command
576
577##         self.cmd = [ '/usr/bin/ssh',
578##                      '-T',
579##                      '-o', 'BatchMode=yes',
580##                      '-i','/home/daedalus/.ssh/localconnect',
581##                      ]
582##        self.cmd.append('%s' % str(device))
583
584        #log.debug("Set up command to run: %s", self.cmd)
585        log.debug("Provisioner ready for device: %s", device)
586        return defer.succeed(None)
587   
588    def run_commands(self, ignored, expectset, namespace):
589        """
590        Run each command, one after the other.
591        """
592        log.debug("inside provisioner running expectset: %s" % expectset)
593        self.cmdoutput = ''
594        self.exitcode = None
595
596        # This deferred is called when all commands have finished
597        self.all_commands_defer = defer.Deferred()
598
599        # Set up a sequential chain of commands
600        d = defer.succeed(None)
601        for (expr, cmdstring) in expectset:
602
603            try:
604                # Perform variable substitution on the command string
605                log.debug("determining commandstring from template: %s", cmdstring)
606                cmdstring = str(util.substituteVariables(cmdstring, namespace))
607                log.debug("commandstring is: %s", cmdstring)
608
609                # add the cmdstring to the namespace
610                namespace['command.send'] = cmdstring
611
612                log.debug("Determining command base from template: %s", self.command)
613                command = str(util.substituteVariables(self.command, namespace))
614            except KeyError, e:
615                log.error("KeyError in commands: %s" % e)
616                self.all_commands_defer.errback( e )
617                return self.all_commands_defer
618           
619            #log.debug("checking expr: %s" % expr)
620            log.debug("cmdstring is: %s" % command)
621            if command is not None:
622                log.debug("splitting command: %s", command)
623                cmd = self.split_command(command)
624                log.debug("command is: %s", cmd)
625                d.addCallback(self.spawn_command, cmd, self.command_timeout)
626                d.addCallbacks(self.command_completed, self.command_failed)
627                pass
628            pass
629
630        d.addCallbacks(self.all_commands_done, self.all_commands_failure)
631        return self.all_commands_defer
632
633    def spawn_command(self, ignored, cmd, timeout=300):
634        """
635        Run the command in a sub-process, returning a deferred that
636        will fire based on the results.
637        """
638        log.debug("spawning command...")
639        self.waitingForCommand = defer.Deferred()
640
641        # If we're in authoritarian mode, wait for confirmation
642        # that we should execute the command.
643        log.debug("Checking for authoritarian mode...")
644        if self.authoritarian:
645            log.debug("Authoritarian mode. Waiting for ok to proceed...")
646            isok = raw_input("Issue command: %s\n(y/n)[n]?> " % ' '.join(cmd) )
647            if isok.startswith('y'):
648                log.debug("Ok! Let's continue!")
649            else:
650                log.info("Bailing out at your command.")
651                self.exitcode = -1
652                self.cmdoutput = 'User requested manual bailout'
653                log.critical("User requested manual bailout")
654                self.waitingForCommand.errback( UserBailout("User requested manual bailout.") )
655                return self.waitingForCommand
656            pass
657
658        log.debug("running command remotely: %s" % cmd)
659        self.ep = SingleCommandProtocol(self, timeout)
660        self.p = reactor.spawnProcess(self.ep, cmd[0], cmd)
661        log.debug("Spawned process: %s", self.p)
662
663        return self.waitingForCommand
664
665    def command_completed(self, result):
666        """
667        When the command is completed, add its output to a
668        global resultset. This sets the exitcode to the
669        exitcode of the last command that completed, and
670        """
671        log.debug("a command has completed with result: %s", result)
672        self.exitcode = result[0]
673        self.cmdoutput += result[1]
674        log.debug("current results: exit '%d', output: %s", self.exitcode, self.cmdoutput)
675       
676    def command_failed(self, failure):
677        errorstr = "%s: %s" % (self.exitcode, self.cmdoutput)
678        log.error("Command failed: %s", errorstr)
679        #self.all_commands_defer.errback( Exception(errorstr) )
680        return failure
681   
682    def all_commands_done(self, result):
683        """
684        Called when all the commands have completed.
685        """
686        log.debug("All commands have finished.")
687        self.all_commands_defer.callback( (self.exitcode, self.cmdoutput) )
688
689    def all_commands_failure(self, failure):
690        """
691        Some kind of failure in the commands occurred.
692        """
693        e = failure.check( UserBailout )
694        if e:
695            #log.error("User bailout detected.")
696            self.all_commands_defer.errback( failure )
697
698class CommandFailure(Exception):
699    """
700    Used to contain sub-process errors.
701    """
702
703class SingleCommandProtocol(protocol.Protocol):
704    """
705    Used to run a single command in a sub-process.
706    """
707   
708    def __init__(self, provisioner, timeout_after=300):
709
710        self.parent = provisioner
711        self.databuf = ''
712        self.exitCode = None
713        self.timeout_after = timeout_after
714
715    def connectionMade(self):
716        #log.debug("Pretend connection made! Hurray!")
717
718        # FIXME: provide a way to set this via the config file
719        self.timeout = reactor.callLater(self.timeout_after, self.timedOut)
720        pass
721   
722    def childConnectionLost(self, fd):
723        #log.debug("Lost connection to child process fd %s" % fd)
724        pass
725
726    def childDataReceived(self, fd, data):
727        #log.debug("received data from child process %s: >%s<" % (fd, data) )
728
729        self.databuf += data
730        log.debug("databuf: >%s<" % self.databuf)
731
732    def processEnded(self, status_object):
733        log.debug("process ended: %s" % status_object)
734        try:
735            if isinstance(status_object.value, ProcessDone):
736                log.debug("process exited ok: %s" % status_object.value.exitCode)
737                self.exitCode = 0
738                self.timeout.cancel()
739                self.parent.waitingForCommand.callback( (self.exitCode, self.databuf) )
740           
741            elif isinstance(status_object.value, ProcessTerminated):
742                log.debug("process did not exit ok: %s: [%s: %s] %s" % (status_object.value.exitCode, status_object.value.signal, status_object.value.status, self.databuf))
743                self.exitCode = status_object.value.exitCode
744                self.timeout.cancel()
745                self.parent.waitingForCommand.callback( (self.exitCode, self.databuf) )
746                pass
747
748        except AlreadyCalled, AlreadyCancelled:
749            pass
750               
751        except Exception, e:
752            log.error("Exception raised during end of process processing. That's quite bad.")
753            raise
754##             traceback.print_exc(e)
755##             self.timeout.cancel()
756##             self.parent.waitingForCommand.errback( e )
757
758    def timedOut(self):
759        # FIXME: If the command times out, we need to ignore anything that comes
760        # back from the process, or we'll notify of error conditions more than once
761        # I'm hoping the AlreadyCalled an AlreadyCancelled catch above will take care of this.
762        log.error("Timed out waiting for command to exit")
763        self.transport.loseConnection()
764        self.parent.waitingForCommand.errback( (1, '%s: command timeout' % self.__class__) )
Note: See TracBrowser for help on using the browser.