/mandos/trunk

To get this branch, use:
bzr branch http://bzr.recompile.se/loggerhead/mandos/trunk

« back to all changes in this revision

Viewing changes to mandos

  • Committer: Teddy Hogeborn
  • Date: 2015-05-31 16:13:39 UTC
  • Revision ID: teddy@recompile.se-20150531161339-igb3l0ip3c10ejpz
mandos-ctl: Generate better messages in exceptions.

mandos (rfc3339_duration_to_delta): Remove dead code.  Adjust white
                                    space.
mandos-ctl (rfc3339_duration_to_delta): Do minor formatting and
                                        whitespace adjustments, and
                                        Generate better message in
                                        exception.
(string_to_delta): Adjust quote character in doc string.

Show diffs side-by-side

added added

removed removed

Lines of Context:
424
424
            .format(self.name)))
425
425
        return ret
426
426
 
427
 
def call_pipe(connection,       # : multiprocessing.Connection
428
 
              func, *args, **kwargs):
429
 
    """This function is meant to be called by multiprocessing.Process
430
 
    
431
 
    This function runs func(*args, **kwargs), and writes the resulting
432
 
    return value on the provided multiprocessing.Connection.
433
 
    """
434
 
    connection.send(func(*args, **kwargs))
435
 
    connection.close()
436
427
 
437
428
class Client(object):
438
429
    """A representation of a client host served by this server.
465
456
    last_checker_status: integer between 0 and 255 reflecting exit
466
457
                         status of last checker. -1 reflects crashed
467
458
                         checker, -2 means no checker completed yet.
468
 
    last_checker_signal: The signal which killed the last checker, if
469
 
                         last_checker_status is -1
470
459
    last_enabled: datetime.datetime(); (UTC) or None
471
460
    name:       string; from the config file, used in log messages and
472
461
                        D-Bus identifiers
646
635
        # Also start a new checker *right now*.
647
636
        self.start_checker()
648
637
    
649
 
    def checker_callback(self, source, condition, connection,
650
 
                         command):
 
638
    def checker_callback(self, pid, condition, command):
651
639
        """The checker has completed, so take appropriate actions."""
652
640
        self.checker_callback_tag = None
653
641
        self.checker = None
654
 
        # Read return code from connection (see call_pipe)
655
 
        returncode = connection.recv()
656
 
        connection.close()
657
 
        
658
 
        if returncode >= 0:
659
 
            self.last_checker_status = returncode
660
 
            self.last_checker_signal = None
 
642
        if os.WIFEXITED(condition):
 
643
            self.last_checker_status = os.WEXITSTATUS(condition)
661
644
            if self.last_checker_status == 0:
662
645
                logger.info("Checker for %(name)s succeeded",
663
646
                            vars(self))
666
649
                logger.info("Checker for %(name)s failed", vars(self))
667
650
        else:
668
651
            self.last_checker_status = -1
669
 
            self.last_checker_signal = -returncode
670
652
            logger.warning("Checker for %(name)s crashed?",
671
653
                           vars(self))
672
 
        return False
673
654
    
674
655
    def checked_ok(self):
675
656
        """Assert that the client has been seen, alive and well."""
676
657
        self.last_checked_ok = datetime.datetime.utcnow()
677
658
        self.last_checker_status = 0
678
 
        self.last_checker_signal = None
679
659
        self.bump_timeout()
680
660
    
681
661
    def bump_timeout(self, timeout=None):
707
687
        # than 'timeout' for the client to be disabled, which is as it
708
688
        # should be.
709
689
        
710
 
        if self.checker is not None and not self.checker.is_alive():
711
 
            logger.warning("Checker was not alive; joining")
712
 
            self.checker.join()
713
 
            self.checker = None
 
690
        # If a checker exists, make sure it is not a zombie
 
691
        try:
 
692
            pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
 
693
        except AttributeError:
 
694
            pass
 
695
        except OSError as error:
 
696
            if error.errno != errno.ECHILD:
 
697
                raise
 
698
        else:
 
699
            if pid:
 
700
                logger.warning("Checker was a zombie")
 
701
                gobject.source_remove(self.checker_callback_tag)
 
702
                self.checker_callback(pid, status,
 
703
                                      self.current_checker_command)
714
704
        # Start a new checker if needed
715
705
        if self.checker is None:
716
706
            # Escape attributes for the shell
725
715
                             exc_info=error)
726
716
                return True     # Try again later
727
717
            self.current_checker_command = command
728
 
            logger.info("Starting checker %r for %s", command,
729
 
                        self.name)
730
 
            # We don't need to redirect stdout and stderr, since
731
 
            # in normal mode, that is already done by daemon(),
732
 
            # and in debug mode we don't want to.  (Stdin is
733
 
            # always replaced by /dev/null.)
734
 
            # The exception is when not debugging but nevertheless
735
 
            # running in the foreground; use the previously
736
 
            # created wnull.
737
 
            popen_args = { "close_fds": True,
738
 
                           "shell": True,
739
 
                           "cwd": "/" }
740
 
            if (not self.server_settings["debug"]
741
 
                and self.server_settings["foreground"]):
742
 
                popen_args.update({"stdout": wnull,
743
 
                                   "stderr": wnull })
744
 
            pipe = multiprocessing.Pipe(duplex = False)
745
 
            self.checker = multiprocessing.Process(
746
 
                target = call_pipe,
747
 
                args = (pipe[1], subprocess.call, command),
748
 
                kwargs = popen_args)
749
 
            self.checker.start()
750
 
            self.checker_callback_tag = gobject.io_add_watch(
751
 
                pipe[0].fileno(), gobject.IO_IN,
752
 
                self.checker_callback, pipe[0], command)
 
718
            try:
 
719
                logger.info("Starting checker %r for %s", command,
 
720
                            self.name)
 
721
                # We don't need to redirect stdout and stderr, since
 
722
                # in normal mode, that is already done by daemon(),
 
723
                # and in debug mode we don't want to.  (Stdin is
 
724
                # always replaced by /dev/null.)
 
725
                # The exception is when not debugging but nevertheless
 
726
                # running in the foreground; use the previously
 
727
                # created wnull.
 
728
                popen_args = {}
 
729
                if (not self.server_settings["debug"]
 
730
                    and self.server_settings["foreground"]):
 
731
                    popen_args.update({"stdout": wnull,
 
732
                                       "stderr": wnull })
 
733
                self.checker = subprocess.Popen(command,
 
734
                                                close_fds=True,
 
735
                                                shell=True,
 
736
                                                cwd="/",
 
737
                                                **popen_args)
 
738
            except OSError as error:
 
739
                logger.error("Failed to start subprocess",
 
740
                             exc_info=error)
 
741
                return True
 
742
            self.checker_callback_tag = gobject.child_watch_add(
 
743
                self.checker.pid, self.checker_callback, data=command)
 
744
            # The checker may have completed before the gobject
 
745
            # watch was added.  Check for this.
 
746
            try:
 
747
                pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
 
748
            except OSError as error:
 
749
                if error.errno == errno.ECHILD:
 
750
                    # This should never happen
 
751
                    logger.error("Child process vanished",
 
752
                                 exc_info=error)
 
753
                    return True
 
754
                raise
 
755
            if pid:
 
756
                gobject.source_remove(self.checker_callback_tag)
 
757
                self.checker_callback(pid, status, command)
753
758
        # Re-run this periodically if run by gobject.timeout_add
754
759
        return True
755
760
    
761
766
        if getattr(self, "checker", None) is None:
762
767
            return
763
768
        logger.debug("Stopping checker for %(name)s", vars(self))
764
 
        self.checker.terminate()
 
769
        try:
 
770
            self.checker.terminate()
 
771
            #time.sleep(0.5)
 
772
            #if self.checker.poll() is None:
 
773
            #    self.checker.kill()
 
774
        except OSError as error:
 
775
            if error.errno != errno.ESRCH: # No such process
 
776
                raise
765
777
        self.checker = None
766
778
 
767
779
 
1099
1111
                interface_names.add(alt_interface)
1100
1112
                # Is this a D-Bus signal?
1101
1113
                if getattr(attribute, "_dbus_is_signal", False):
1102
 
                    if sys.version_info.major == 2:
1103
 
                        # Extract the original non-method undecorated
1104
 
                        # function by black magic
1105
 
                        nonmethod_func = (dict(
1106
 
                            zip(attribute.func_code.co_freevars,
1107
 
                                attribute.__closure__))
1108
 
                                          ["func"].cell_contents)
1109
 
                    else:
1110
 
                        nonmethod_func = attribute
 
1114
                    # Extract the original non-method undecorated
 
1115
                    # function by black magic
 
1116
                    nonmethod_func = (dict(
 
1117
                        zip(attribute.func_code.co_freevars,
 
1118
                            attribute.__closure__))
 
1119
                                      ["func"].cell_contents)
1111
1120
                    # Create a new, but exactly alike, function
1112
1121
                    # object, and decorate it to be a new D-Bus signal
1113
1122
                    # with the alternate D-Bus interface name
1114
 
                    if sys.version_info.major == 2:
1115
 
                        new_function = types.FunctionType(
1116
 
                            nonmethod_func.func_code,
1117
 
                            nonmethod_func.func_globals,
1118
 
                            nonmethod_func.func_name,
1119
 
                            nonmethod_func.func_defaults,
1120
 
                            nonmethod_func.func_closure)
1121
 
                    else:
1122
 
                        new_function = types.FunctionType(
1123
 
                            nonmethod_func.__code__,
1124
 
                            nonmethod_func.__globals__,
1125
 
                            nonmethod_func.__name__,
1126
 
                            nonmethod_func.__defaults__,
1127
 
                            nonmethod_func.__closure__)
1128
1123
                    new_function = (dbus.service.signal(
1129
 
                        alt_interface,
1130
 
                        attribute._dbus_signature)(new_function))
 
1124
                        alt_interface, attribute._dbus_signature)
 
1125
                                    (types.FunctionType(
 
1126
                                        nonmethod_func.func_code,
 
1127
                                        nonmethod_func.func_globals,
 
1128
                                        nonmethod_func.func_name,
 
1129
                                        nonmethod_func.func_defaults,
 
1130
                                        nonmethod_func.func_closure)))
1131
1131
                    # Copy annotations, if any
1132
1132
                    try:
1133
1133
                        new_function._dbus_annotations = dict(
1356
1356
            DBusObjectWithProperties.__del__(self, *args, **kwargs)
1357
1357
        Client.__del__(self, *args, **kwargs)
1358
1358
    
1359
 
    def checker_callback(self, source, condition,
1360
 
                         connection, command, *args, **kwargs):
1361
 
        ret = Client.checker_callback(self, source, condition,
1362
 
                                      connection, command, *args,
1363
 
                                      **kwargs)
1364
 
        exitstatus = self.last_checker_status
1365
 
        if exitstatus >= 0:
 
1359
    def checker_callback(self, pid, condition, command,
 
1360
                         *args, **kwargs):
 
1361
        self.checker_callback_tag = None
 
1362
        self.checker = None
 
1363
        if os.WIFEXITED(condition):
 
1364
            exitstatus = os.WEXITSTATUS(condition)
1366
1365
            # Emit D-Bus signal
1367
1366
            self.CheckerCompleted(dbus.Int16(exitstatus),
1368
 
                                  dbus.Int64(0),
 
1367
                                  dbus.Int64(condition),
1369
1368
                                  dbus.String(command))
1370
1369
        else:
1371
1370
            # Emit D-Bus signal
1372
1371
            self.CheckerCompleted(dbus.Int16(-1),
1373
 
                                  dbus.Int64(
1374
 
                                      self.last_checker_signal),
 
1372
                                  dbus.Int64(condition),
1375
1373
                                  dbus.String(command))
1376
 
        return ret
 
1374
        
 
1375
        return Client.checker_callback(self, pid, condition, command,
 
1376
                                       *args, **kwargs)
1377
1377
    
1378
1378
    def start_checker(self, *args, **kwargs):
1379
1379
        old_checker_pid = getattr(self.checker, "pid", None)
2141
2141
        
2142
2142
        if command == 'getattr':
2143
2143
            attrname = request[1]
2144
 
            if isinstance(client_object.__getattribute__(attrname),
2145
 
                          collections.Callable):
 
2144
            if callable(client_object.__getattribute__(attrname)):
2146
2145
                parent_pipe.send(('function', ))
2147
2146
            else:
2148
2147
                parent_pipe.send((
2648
2647
                    pass
2649
2648
            
2650
2649
            # Clients who has passed its expire date can still be
2651
 
            # enabled if its last checker was successful.  A Client
 
2650
            # enabled if its last checker was successful.  Clients
2652
2651
            # whose checker succeeded before we stored its state is
2653
2652
            # assumed to have successfully run all checkers during
2654
2653
            # downtime.