/mandos/release

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

« back to all changes in this revision

Viewing changes to mandos-ctl

  • Committer: Teddy Hogeborn
  • Date: 2019-03-15 21:59:44 UTC
  • mto: This revision was merged to the branch mainline in revision 382.
  • Revision ID: teddy@recompile.se-20190315215944-lomtn36y7o3jphp8
mandos-ctl: Refactor

* mandos-ctl (main): Call get_mandos_dbus_object() to get
                     "mandos_dbus_objc", and rename it to
                     "mandos_dbus_object".
  (get_mandos_dbus_object): New.
  (Test_get_mandos_dbus_object): - '' -

Show diffs side-by-side

added added

removed removed

Lines of Context:
102
102
    mandos_serv_object_manager = dbus.Interface(
103
103
        mandos_dbus_object, dbus_interface=dbus.OBJECT_MANAGER_IFACE)
104
104
 
105
 
    managed_objects = get_managed_objects(mandos_serv_object_manager)
106
 
 
107
 
    all_clients = {}
108
 
    for path, ifs_and_props in managed_objects.items():
109
 
        try:
110
 
            all_clients[path] = ifs_and_props[client_dbus_interface]
111
 
        except KeyError:
112
 
            pass
113
 
 
114
 
    # Compile dict of (clientpath: properties) to process
 
105
    try:
 
106
        log.debug("D-Bus: %s:%s:%s.GetManagedObjects()", dbus_busname,
 
107
                  server_dbus_path, dbus.OBJECT_MANAGER_IFACE)
 
108
        with SilenceLogger("dbus.proxies"):
 
109
            all_clients = {path: ifs_and_props[client_dbus_interface]
 
110
                              for path, ifs_and_props in
 
111
                              mandos_serv_object_manager
 
112
                              .GetManagedObjects().items()
 
113
                              if client_dbus_interface in ifs_and_props}
 
114
    except dbus.exceptions.DBusException as e:
 
115
        log.critical("Failed to access Mandos server through D-Bus:"
 
116
                     "\n%s", e)
 
117
        sys.exit(1)
 
118
 
 
119
    # Compile dict of (clients: properties) to process
 
120
    clients = {}
 
121
 
115
122
    if not clientnames:
116
123
        clients = all_clients
117
124
    else:
118
 
        clients = {}
119
125
        for name in clientnames:
120
126
            for objpath, properties in all_clients.items():
121
127
                if properties["Name"] == name:
125
131
                log.critical("Client not found on server: %r", name)
126
132
                sys.exit(1)
127
133
 
 
134
    # Run all commands on clients
128
135
    commands = commands_from_options(options)
129
 
 
130
136
    for command in commands:
131
137
        command.run(clients, bus, mandos_serv)
132
138
 
232
238
    >>> rfc3339_duration_to_delta("")
233
239
    Traceback (most recent call last):
234
240
    ...
235
 
    ValueError: Invalid RFC 3339 duration: ""
 
241
    ValueError: Invalid RFC 3339 duration: u''
236
242
    >>> # Must start with "P":
237
243
    >>> rfc3339_duration_to_delta("1D")
238
244
    Traceback (most recent call last):
239
245
    ...
240
 
    ValueError: Invalid RFC 3339 duration: "1D"
 
246
    ValueError: Invalid RFC 3339 duration: u'1D'
241
247
    >>> # Must use correct order
242
248
    >>> rfc3339_duration_to_delta("PT1S2M")
243
249
    Traceback (most recent call last):
244
250
    ...
245
 
    ValueError: Invalid RFC 3339 duration: "PT1S2M"
 
251
    ValueError: Invalid RFC 3339 duration: u'PT1S2M'
246
252
    >>> # Time needs time marker
247
253
    >>> rfc3339_duration_to_delta("P1H2S")
248
254
    Traceback (most recent call last):
249
255
    ...
250
 
    ValueError: Invalid RFC 3339 duration: "P1H2S"
 
256
    ValueError: Invalid RFC 3339 duration: u'P1H2S'
251
257
    >>> # Weeks can not be combined with anything else
252
258
    >>> rfc3339_duration_to_delta("P1D2W")
253
259
    Traceback (most recent call last):
254
260
    ...
255
 
    ValueError: Invalid RFC 3339 duration: "P1D2W"
 
261
    ValueError: Invalid RFC 3339 duration: u'P1D2W'
256
262
    >>> rfc3339_duration_to_delta("P2W2H")
257
263
    Traceback (most recent call last):
258
264
    ...
259
 
    ValueError: Invalid RFC 3339 duration: "P2W2H"
 
265
    ValueError: Invalid RFC 3339 duration: u'P2W2H'
260
266
    """
261
267
 
262
268
    # Parsing an RFC 3339 duration with regular expressions is not
333
339
                break
334
340
        else:
335
341
            # No currently valid tokens were found
336
 
            raise ValueError("Invalid RFC 3339 duration: \"{}\""
 
342
            raise ValueError("Invalid RFC 3339 duration: {!r}"
337
343
                             .format(duration))
338
344
    # End token found
339
345
    return value
425
431
 
426
432
 
427
433
def get_mandos_dbus_object(bus):
428
 
    log.debug("D-Bus: Connect to: (busname=%r, path=%r)",
429
 
              dbus_busname, server_dbus_path)
430
 
    with if_dbus_exception_log_with_exception_and_exit(
431
 
            "Could not connect to Mandos server: %s"):
 
434
    try:
 
435
        log.debug("D-Bus: Connect to: (busname=%r, path=%r)",
 
436
                  dbus_busname, server_dbus_path)
432
437
        mandos_dbus_object = bus.get_object(dbus_busname,
433
438
                                            server_dbus_path)
 
439
    except dbus.exceptions.DBusException:
 
440
        log.critical("Could not connect to Mandos server")
 
441
        sys.exit(1)
 
442
 
434
443
    return mandos_dbus_object
435
444
 
436
445
 
437
 
@contextlib.contextmanager
438
 
def if_dbus_exception_log_with_exception_and_exit(*args, **kwargs):
439
 
    try:
440
 
        yield
441
 
    except dbus.exceptions.DBusException as e:
442
 
        log.critical(*(args + (e,)), **kwargs)
443
 
        sys.exit(1)
444
 
 
445
 
 
446
 
def get_managed_objects(object_manager):
447
 
    log.debug("D-Bus: %s:%s:%s.GetManagedObjects()", dbus_busname,
448
 
              server_dbus_path, dbus.OBJECT_MANAGER_IFACE)
449
 
    with if_dbus_exception_log_with_exception_and_exit(
450
 
            "Failed to access Mandos server through D-Bus:\n%s"):
451
 
        with SilenceLogger("dbus.proxies"):
452
 
            managed_objects = object_manager.GetManagedObjects()
453
 
    return managed_objects
454
 
 
455
 
 
456
446
class SilenceLogger(object):
457
447
    "Simple context manager to silence a particular logger"
458
448
    def __init__(self, loggername):
839
829
 
840
830
 
841
831
 
842
 
class TestCaseWithAssertLogs(unittest.TestCase):
843
 
    """unittest.TestCase.assertLogs only exists in Python 3.4"""
844
 
 
845
 
    if not hasattr(unittest.TestCase, "assertLogs"):
846
 
        @contextlib.contextmanager
847
 
        def assertLogs(self, logger, level=logging.INFO):
848
 
            capturing_handler = self.CapturingLevelHandler(level)
849
 
            old_level = logger.level
850
 
            old_propagate = logger.propagate
851
 
            logger.addHandler(capturing_handler)
852
 
            logger.setLevel(level)
853
 
            logger.propagate = False
854
 
            try:
855
 
                yield capturing_handler.watcher
856
 
            finally:
857
 
                logger.propagate = old_propagate
858
 
                logger.removeHandler(capturing_handler)
859
 
                logger.setLevel(old_level)
860
 
            self.assertGreater(len(capturing_handler.watcher.records),
861
 
                               0)
862
 
 
863
 
        class CapturingLevelHandler(logging.Handler):
864
 
            def __init__(self, level, *args, **kwargs):
865
 
                logging.Handler.__init__(self, *args, **kwargs)
866
 
                self.watcher = self.LoggingWatcher([], [])
867
 
            def emit(self, record):
868
 
                self.watcher.records.append(record)
869
 
                self.watcher.output.append(self.format(record))
870
 
 
871
 
            LoggingWatcher = collections.namedtuple("LoggingWatcher",
872
 
                                                    ("records",
873
 
                                                     "output"))
874
 
 
875
 
 
876
 
class Test_string_to_delta(TestCaseWithAssertLogs):
 
832
class Test_string_to_delta(unittest.TestCase):
877
833
    def test_handles_basic_rfc3339(self):
878
834
        self.assertEqual(string_to_delta("PT0S"),
879
835
                         datetime.timedelta())
885
841
                         datetime.timedelta(0, 7200))
886
842
 
887
843
    def test_falls_back_to_pre_1_6_1_with_warning(self):
888
 
        with self.assertLogs(log, logging.WARNING):
889
 
            value = string_to_delta("2h")
 
844
        # assertLogs only exists in Python 3.4
 
845
        if hasattr(self, "assertLogs"):
 
846
            with self.assertLogs(log, logging.WARNING):
 
847
                value = string_to_delta("2h")
 
848
        else:
 
849
            class WarningFilter(logging.Filter):
 
850
                """Don't show, but record the presence of, warnings"""
 
851
                def filter(self, record):
 
852
                    is_warning = record.levelno >= logging.WARNING
 
853
                    self.found = is_warning or getattr(self, "found",
 
854
                                                       False)
 
855
                    return not is_warning
 
856
            warning_filter = WarningFilter()
 
857
            log.addFilter(warning_filter)
 
858
            try:
 
859
                value = string_to_delta("2h")
 
860
            finally:
 
861
                log.removeFilter(warning_filter)
 
862
            self.assertTrue(getattr(warning_filter, "found", False))
890
863
        self.assertEqual(value, datetime.timedelta(0, 7200))
891
864
 
892
865
 
931
904
    @contextlib.contextmanager
932
905
    def assertParseError(self):
933
906
        with self.assertRaises(SystemExit) as e:
934
 
            with self.redirect_stderr_to_devnull():
 
907
            with self.temporarily_suppress_stderr():
935
908
                yield
936
909
        # Exit code from argparse is guaranteed to be "2".  Reference:
937
910
        # https://docs.python.org/3/library
940
913
 
941
914
    @staticmethod
942
915
    @contextlib.contextmanager
943
 
    def redirect_stderr_to_devnull():
 
916
    def temporarily_suppress_stderr():
944
917
        null = os.open(os.path.devnull, os.O_RDWR)
945
918
        stderrcopy = os.dup(sys.stderr.fileno())
946
919
        os.dup2(null, sys.stderr.fileno())
955
928
    def check_option_syntax(self, options):
956
929
        check_option_syntax(self.parser, options)
957
930
 
958
 
    def test_actions_all_conflicts_with_verbose(self):
959
 
        for action, value in self.actions.items():
960
 
            options = self.parser.parse_args()
961
 
            setattr(options, action, value)
962
 
            options.all = True
963
 
            options.verbose = True
964
 
            with self.assertParseError():
965
 
                self.check_option_syntax(options)
966
 
 
967
 
    def test_actions_with_client_conflicts_with_verbose(self):
968
 
        for action, value in self.actions.items():
969
 
            options = self.parser.parse_args()
970
 
            setattr(options, action, value)
971
 
            options.verbose = True
972
 
            options.client = ["foo"]
 
931
    def test_actions_conflicts_with_verbose(self):
 
932
        for action, value in self.actions.items():
 
933
            options = self.parser.parse_args()
 
934
            setattr(options, action, value)
 
935
            options.verbose = True
973
936
            with self.assertParseError():
974
937
                self.check_option_syntax(options)
975
938
 
1001
964
            options.all = True
1002
965
            self.check_option_syntax(options)
1003
966
 
1004
 
    def test_any_action_is_ok_with_one_client(self):
1005
 
        for action, value in self.actions.items():
1006
 
            options = self.parser.parse_args()
1007
 
            setattr(options, action, value)
1008
 
            options.client = ["foo"]
 
967
    def test_is_enabled_fails_without_client(self):
 
968
        options = self.parser.parse_args()
 
969
        options.is_enabled = True
 
970
        with self.assertParseError():
1009
971
            self.check_option_syntax(options)
1010
972
 
1011
 
    def test_one_client_with_all_actions_except_is_enabled(self):
 
973
    def test_is_enabled_works_with_one_client(self):
1012
974
        options = self.parser.parse_args()
1013
 
        for action, value in self.actions.items():
1014
 
            if action == "is_enabled":
1015
 
                continue
1016
 
            setattr(options, action, value)
 
975
        options.is_enabled = True
1017
976
        options.client = ["foo"]
1018
977
        self.check_option_syntax(options)
1019
978
 
1020
 
    def test_two_clients_with_all_actions_except_is_enabled(self):
1021
 
        options = self.parser.parse_args()
1022
 
        for action, value in self.actions.items():
1023
 
            if action == "is_enabled":
1024
 
                continue
1025
 
            setattr(options, action, value)
1026
 
        options.client = ["foo", "barbar"]
1027
 
        self.check_option_syntax(options)
1028
 
 
1029
 
    def test_two_clients_are_ok_with_actions_except_is_enabled(self):
1030
 
        for action, value in self.actions.items():
1031
 
            if action == "is_enabled":
1032
 
                continue
1033
 
            options = self.parser.parse_args()
1034
 
            setattr(options, action, value)
1035
 
            options.client = ["foo", "barbar"]
1036
 
            self.check_option_syntax(options)
1037
 
 
1038
 
    def test_is_enabled_fails_without_client(self):
1039
 
        options = self.parser.parse_args()
1040
 
        options.is_enabled = True
1041
 
        with self.assertParseError():
1042
 
            self.check_option_syntax(options)
1043
 
 
1044
979
    def test_is_enabled_fails_with_two_clients(self):
1045
980
        options = self.parser.parse_args()
1046
981
        options.is_enabled = True
1060
995
                self.check_option_syntax(options)
1061
996
 
1062
997
 
1063
 
class Test_get_mandos_dbus_object(TestCaseWithAssertLogs):
 
998
class Test_get_mandos_dbus_object(unittest.TestCase):
1064
999
    def test_calls_and_returns_get_object_on_bus(self):
1065
1000
        class MockBus(object):
1066
1001
            called = False
1081
1016
            def get_object(self, busname, dbus_path):
1082
1017
                raise dbus.exceptions.DBusException("Test")
1083
1018
 
1084
 
        with self.assertLogs(log, logging.CRITICAL):
1085
 
            with self.assertRaises(SystemExit) as e:
1086
 
                bus = get_mandos_dbus_object(bus=MockBusFailing())
1087
 
 
 
1019
        # assertLogs only exists in Python 3.4
 
1020
        if hasattr(self, "assertLogs"):
 
1021
            with self.assertLogs(log, logging.CRITICAL):
 
1022
                with self.assertRaises(SystemExit) as e:
 
1023
                    bus = get_mandos_dbus_object(bus=MockBus())
 
1024
        else:
 
1025
            critical_filter = self.CriticalFilter()
 
1026
            log.addFilter(critical_filter)
 
1027
            try:
 
1028
                with self.assertRaises(SystemExit) as e:
 
1029
                    get_mandos_dbus_object(bus=MockBusFailing())
 
1030
            finally:
 
1031
                log.removeFilter(critical_filter)
 
1032
            self.assertTrue(critical_filter.found)
1088
1033
        if isinstance(e.exception.code, int):
1089
1034
            self.assertNotEqual(e.exception.code, 0)
1090
1035
        else:
1091
1036
            self.assertIsNotNone(e.exception.code)
1092
1037
 
1093
 
 
1094
 
class Test_get_managed_objects(TestCaseWithAssertLogs):
1095
 
    def test_calls_and_returns_GetManagedObjects(self):
1096
 
        managed_objects = {"/clients/foo": { "Name": "foo"}}
1097
 
        class MockObjectManager(object):
1098
 
            def GetManagedObjects(self):
1099
 
                return managed_objects
1100
 
        retval = get_managed_objects(MockObjectManager())
1101
 
        self.assertDictEqual(managed_objects, retval)
1102
 
 
1103
 
    def test_logs_and_exits_on_dbus_error(self):
1104
 
        dbus_logger = logging.getLogger("dbus.proxies")
1105
 
 
1106
 
        class MockObjectManagerFailing(object):
1107
 
            def GetManagedObjects(self):
1108
 
                dbus_logger.error("Test")
1109
 
                raise dbus.exceptions.DBusException("Test")
1110
 
 
1111
 
        class CountingHandler(logging.Handler):
1112
 
            count = 0
1113
 
            def emit(self, record):
1114
 
                self.count += 1
1115
 
 
1116
 
        counting_handler = CountingHandler()
1117
 
 
1118
 
        dbus_logger.addHandler(counting_handler)
1119
 
 
 
1038
    class CriticalFilter(logging.Filter):
 
1039
        """Don't show, but register, critical messages"""
 
1040
        found = False
 
1041
        def filter(self, record):
 
1042
            is_critical = record.levelno >= logging.CRITICAL
 
1043
            self.found = is_critical or self.found
 
1044
            return not is_critical
 
1045
 
 
1046
 
 
1047
class Test_SilenceLogger(unittest.TestCase):
 
1048
    loggername = "mandos-ctl.Test_SilenceLogger"
 
1049
    log = logging.getLogger(loggername)
 
1050
    log.propagate = False
 
1051
    log.addHandler(logging.NullHandler())
 
1052
 
 
1053
    def setUp(self):
 
1054
        self.counting_filter = self.CountingFilter()
 
1055
 
 
1056
    class CountingFilter(logging.Filter):
 
1057
        "Count number of records"
 
1058
        count = 0
 
1059
        def filter(self, record):
 
1060
            self.count += 1
 
1061
            return True
 
1062
 
 
1063
    def test_should_filter_records_only_when_active(self):
1120
1064
        try:
1121
 
            with self.assertLogs(log, logging.CRITICAL) as watcher:
1122
 
                with self.assertRaises(SystemExit) as e:
1123
 
                    get_managed_objects(MockObjectManagerFailing())
 
1065
            with SilenceLogger(self.loggername):
 
1066
                self.log.addFilter(self.counting_filter)
 
1067
                self.log.info("Filtered log message 1")
 
1068
            self.log.info("Non-filtered message 2")
 
1069
            self.log.info("Non-filtered message 3")
1124
1070
        finally:
1125
 
            dbus_logger.removeFilter(counting_handler)
1126
 
 
1127
 
        # Make sure the dbus logger was suppressed
1128
 
        self.assertEqual(counting_handler.count, 0)
1129
 
 
1130
 
        # Test that the dbus_logger still works
1131
 
        with self.assertLogs(dbus_logger, logging.ERROR):
1132
 
            dbus_logger.error("Test")
1133
 
 
1134
 
        if isinstance(e.exception.code, int):
1135
 
            self.assertNotEqual(e.exception.code, 0)
1136
 
        else:
1137
 
            self.assertIsNotNone(e.exception.code)
 
1071
            self.log.removeFilter(self.counting_filter)
 
1072
        self.assertEqual(self.counting_filter.count, 2)
1138
1073
 
1139
1074
 
1140
1075
class Test_commands_from_options(unittest.TestCase):