/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-ctl

  • Committer: Teddy Hogeborn
  • Date: 2019-03-10 02:35:22 UTC
  • Revision ID: teddy@recompile.se-20190310023522-cvos2tw2si6t7if3
mandos-ctl: Minimize number of D-Bus calls

* mandos-ctl (Command.run): Now takes a {objpath: properties} dict,
                            followed by optional "bus" and "mandos"
                            arguments.  Use "bus" to connect to
                            clients when dispatching to method
                            run_on_one_client().  All callers changed.
  (IsEnabledCmd.run): New.
  (IsEnabledCmd.run_on_one_client): Remove.
  (ApproveCmd.run_on_one_client): Add busname to debug output.
  (DenyCmd.run_on_one_client): - '' -
  (main): In D-Bus debug output, change "name" to "busname".  Also,
          don't connect to clients, just use the object path as the
          key of the "clients" dict passed to Command.run().
  (TestCmd.clients): Changed to an {objpath: properties} dict.
  (TestCmd.one_client): - '' -
  (TestCmd.bus): New mock bus object having a get_object() method.

Show diffs side-by-side

added added

removed removed

Lines of Context:
276
276
# Abstract classes first
277
277
class Command(object):
278
278
    """Abstract class for commands"""
279
 
    def run(self, mandos, clients):
 
279
    def run(self, clients, bus=None, mandos=None):
280
280
        """Normal commands should implement run_on_one_client(), but
281
281
        commands which want to operate on all clients at the same time
282
282
        can override this run() method instead."""
283
283
        self.mandos = mandos
284
 
        for client, properties in clients.items():
 
284
        for clientpath, properties in clients.items():
 
285
            log.debug("D-Bus: Connect to: (busname=%r, path=%r)",
 
286
                      busname, str(clientpath))
 
287
            client = bus.get_object(busname, clientpath)
285
288
            self.run_on_one_client(client, properties)
286
289
 
287
290
class PrintCmd(Command):
293
296
                    "LastApprovalRequest", "ApprovalDelay",
294
297
                    "ApprovalDuration", "Checker", "ExtendedTimeout",
295
298
                    "Expires", "LastCheckerStatus")
296
 
    def run(self, mandos, clients):
297
 
        print(self.output(clients))
 
299
    def run(self, clients, bus=None, mandos=None):
 
300
        print(self.output(clients.values()))
 
301
    def output(self, clients):
 
302
        raise NotImplementedError()
298
303
 
299
304
class PropertyCmd(Command):
300
305
    """Abstract class for Actions for setting one client property"""
301
306
    def run_on_one_client(self, client, properties):
302
307
        """Set the Client's D-Bus property"""
303
 
        client.Set(client_interface, self.property, self.value_to_set,
 
308
        log.debug("D-Bus: %s:%s:%s.Set(%r, %r, %r)", busname,
 
309
                  client.__dbus_object_path__,
 
310
                  dbus.PROPERTIES_IFACE, client_interface,
 
311
                  self.propname, self.value_to_set
 
312
                  if not isinstance(self.value_to_set, dbus.Boolean)
 
313
                  else bool(self.value_to_set))
 
314
        client.Set(client_interface, self.propname, self.value_to_set,
304
315
                   dbus_interface=dbus.PROPERTIES_IFACE)
 
316
    @property
 
317
    def propname(self):
 
318
        raise NotImplementedError()
305
319
 
306
320
class ValueArgumentMixIn(object):
307
321
    """Mixin class for commands taking a value as argument"""
330
344
        keywords = default_keywords
331
345
        if self.verbose:
332
346
            keywords = self.all_keywords
333
 
        return str(self.TableOfClients(clients.values(), keywords))
 
347
        return str(self.TableOfClients(clients, keywords))
334
348
 
335
349
    class TableOfClients(object):
336
350
        tableheaders = {
422
436
        return value
423
437
 
424
438
class IsEnabledCmd(Command):
425
 
    def run_on_one_client(self, client, properties):
 
439
    def run(self, clients, bus=None, mandos=None):
 
440
        client, properties = next(iter(clients.items()))
426
441
        if self.is_enabled(client, properties):
427
442
            sys.exit(0)
428
443
        sys.exit(1)
429
444
    def is_enabled(self, client, properties):
430
 
        return bool(properties["Enabled"])
 
445
        return properties["Enabled"]
431
446
 
432
447
class RemoveCmd(Command):
433
448
    def run_on_one_client(self, client, properties):
 
449
        log.debug("D-Bus: %s:%s:%s.RemoveClient(%r)", busname,
 
450
                  server_path, server_interface,
 
451
                  str(client.__dbus_object_path__))
434
452
        self.mandos.RemoveClient(client.__dbus_object_path__)
435
453
 
436
454
class ApproveCmd(Command):
437
455
    def run_on_one_client(self, client, properties):
 
456
        log.debug("D-Bus: %s:%s:%s.Approve(True)", busname,
 
457
                  client.__dbus_object_path__, client_interface)
438
458
        client.Approve(dbus.Boolean(True),
439
459
                       dbus_interface=client_interface)
440
460
 
441
461
class DenyCmd(Command):
442
462
    def run_on_one_client(self, client, properties):
 
463
        log.debug("D-Bus: %s:%s:%s.Approve(False)", busname,
 
464
                  client.__dbus_object_path__, client_interface)
443
465
        client.Approve(dbus.Boolean(False),
444
466
                       dbus_interface=client_interface)
445
467
 
446
468
class EnableCmd(PropertyCmd):
447
 
    property = "Enabled"
 
469
    propname = "Enabled"
448
470
    value_to_set = dbus.Boolean(True)
449
471
 
450
472
class DisableCmd(PropertyCmd):
451
 
    property = "Enabled"
 
473
    propname = "Enabled"
452
474
    value_to_set = dbus.Boolean(False)
453
475
 
454
476
class BumpTimeoutCmd(PropertyCmd):
455
 
    property = "LastCheckedOK"
 
477
    propname = "LastCheckedOK"
456
478
    value_to_set = ""
457
479
 
458
480
class StartCheckerCmd(PropertyCmd):
459
 
    property = "CheckerRunning"
 
481
    propname = "CheckerRunning"
460
482
    value_to_set = dbus.Boolean(True)
461
483
 
462
484
class StopCheckerCmd(PropertyCmd):
463
 
    property = "CheckerRunning"
 
485
    propname = "CheckerRunning"
464
486
    value_to_set = dbus.Boolean(False)
465
487
 
466
488
class ApproveByDefaultCmd(PropertyCmd):
467
 
    property = "ApprovedByDefault"
 
489
    propname = "ApprovedByDefault"
468
490
    value_to_set = dbus.Boolean(True)
469
491
 
470
492
class DenyByDefaultCmd(PropertyCmd):
471
 
    property = "ApprovedByDefault"
 
493
    propname = "ApprovedByDefault"
472
494
    value_to_set = dbus.Boolean(False)
473
495
 
474
496
class SetCheckerCmd(PropertyCmd, ValueArgumentMixIn):
475
 
    property = "Checker"
 
497
    propname = "Checker"
476
498
 
477
499
class SetHostCmd(PropertyCmd, ValueArgumentMixIn):
478
 
    property = "Host"
 
500
    propname = "Host"
479
501
 
480
502
class SetSecretCmd(PropertyCmd, ValueArgumentMixIn):
 
503
    propname = "Secret"
481
504
    @property
482
505
    def value_to_set(self):
483
506
        return self._vts
486
509
        """When setting, read data from supplied file object"""
487
510
        self._vts = value.read()
488
511
        value.close()
489
 
    property = "Secret"
490
512
 
491
513
class SetTimeoutCmd(PropertyCmd, MillisecondsValueArgumentMixIn):
492
 
    property = "Timeout"
 
514
    propname = "Timeout"
493
515
 
494
516
class SetExtendedTimeoutCmd(PropertyCmd,
495
517
                            MillisecondsValueArgumentMixIn):
496
 
    property = "ExtendedTimeout"
 
518
    propname = "ExtendedTimeout"
497
519
 
498
520
class SetIntervalCmd(PropertyCmd, MillisecondsValueArgumentMixIn):
499
 
    property = "Interval"
 
521
    propname = "Interval"
500
522
 
501
523
class SetApprovalDelayCmd(PropertyCmd,
502
524
                          MillisecondsValueArgumentMixIn):
503
 
    property = "ApprovalDelay"
 
525
    propname = "ApprovalDelay"
504
526
 
505
527
class SetApprovalDurationCmd(PropertyCmd,
506
528
                             MillisecondsValueArgumentMixIn):
507
 
    property = "ApprovalDuration"
 
529
    propname = "ApprovalDuration"
508
530
 
509
531
def add_command_line_options(parser):
510
532
    parser.add_argument("--version", action="version",
566
588
        help="Approve any current client request")
567
589
    approve_deny.add_argument("-D", "--deny", action="store_true",
568
590
                              help="Deny any current client request")
 
591
    parser.add_argument("--debug", action="store_true",
 
592
                        help="Debug mode (show D-Bus commands)")
569
593
    parser.add_argument("--check", action="store_true",
570
594
                        help="Run self-test")
571
595
    parser.add_argument("client", nargs="*", help="Client name")
596
620
    if options.is_enabled:
597
621
        commands.append(IsEnabledCmd())
598
622
 
599
 
    if options.remove:
600
 
        commands.append(RemoveCmd())
601
 
 
602
623
    if options.checker is not None:
603
624
        commands.append(SetCheckerCmd(options.checker))
604
625
 
637
658
    if options.deny:
638
659
        commands.append(DenyCmd())
639
660
 
 
661
    if options.remove:
 
662
        commands.append(RemoveCmd())
 
663
 
640
664
    # If no command option has been given, show table of clients,
641
665
    # optionally verbosely
642
666
    if not commands:
680
704
        parser.error("--all requires an action.")
681
705
    if options.is_enabled and len(options.client) > 1:
682
706
        parser.error("--is-enabled requires exactly one client")
 
707
    if options.remove:
 
708
        options.remove = False
 
709
        if has_actions(options) and not options.deny:
 
710
            parser.error("--remove can only be combined with --deny")
 
711
        options.remove = True
683
712
 
684
713
 
685
714
def main():
693
722
 
694
723
    clientnames = options.client
695
724
 
 
725
    if options.debug:
 
726
        log.setLevel(logging.DEBUG)
 
727
 
696
728
    try:
697
729
        bus = dbus.SystemBus()
 
730
        log.debug("D-Bus: Connect to: (busname=%r, path=%r)", busname,
 
731
                  server_path)
698
732
        mandos_dbus_objc = bus.get_object(busname, server_path)
699
733
    except dbus.exceptions.DBusException:
700
734
        log.critical("Could not connect to Mandos server")
713
747
    dbus_filter = NullFilter()
714
748
    try:
715
749
        dbus_logger.addFilter(dbus_filter)
 
750
        log.debug("D-Bus: %s:%s:%s.GetManagedObjects()", busname,
 
751
                  server_path, dbus.OBJECT_MANAGER_IFACE)
716
752
        mandos_clients = {path: ifs_and_props[client_interface]
717
753
                          for path, ifs_and_props in
718
754
                          mandos_serv_object_manager
730
766
    clients = {}
731
767
 
732
768
    if not clientnames:
733
 
        clients = {bus.get_object(busname, path): properties
734
 
                   for path, properties in mandos_clients.items()}
 
769
        clients = {objpath: properties
 
770
                   for objpath, properties in mandos_clients.items()}
735
771
    else:
736
772
        for name in clientnames:
737
 
            for path, client in mandos_clients.items():
738
 
                if client["Name"] == name:
739
 
                    client_objc = bus.get_object(busname, path)
740
 
                    clients[client_objc] = client
 
773
            for objpath, properties in mandos_clients.items():
 
774
                if properties["Name"] == name:
 
775
                    clients[objpath] = properties
741
776
                    break
742
777
            else:
743
778
                log.critical("Client not found on server: %r", name)
746
781
    # Run all commands on clients
747
782
    commands = commands_from_options(options)
748
783
    for command in commands:
749
 
        command.run(mandos_serv, clients)
 
784
        command.run(clients, bus, mandos_serv)
750
785
 
751
786
 
752
787
class Test_milliseconds_to_string(unittest.TestCase):
801
836
        testcase = self
802
837
        class MockClient(object):
803
838
            def __init__(self, name, **attributes):
804
 
                self.__dbus_object_path__ = "objpath_{}".format(name)
 
839
                self.__dbus_object_path__ = "/clients/{}".format(name)
805
840
                self.attributes = attributes
806
841
                self.attributes["Name"] = name
807
842
                self.calls = []
808
 
            def Set(self, interface, property, value, dbus_interface):
809
 
                testcase.assertEqual(interface, client_interface)
810
 
                testcase.assertEqual(dbus_interface,
811
 
                                     dbus.PROPERTIES_IFACE)
812
 
                self.attributes[property] = value
813
 
            def Get(self, interface, property, dbus_interface):
814
 
                testcase.assertEqual(interface, client_interface)
815
 
                testcase.assertEqual(dbus_interface,
816
 
                                     dbus.PROPERTIES_IFACE)
817
 
                return self.attributes[property]
 
843
            def Set(self, interface, propname, value, dbus_interface):
 
844
                testcase.assertEqual(interface, client_interface)
 
845
                testcase.assertEqual(dbus_interface,
 
846
                                     dbus.PROPERTIES_IFACE)
 
847
                self.attributes[propname] = value
 
848
            def Get(self, interface, propname, dbus_interface):
 
849
                testcase.assertEqual(interface, client_interface)
 
850
                testcase.assertEqual(dbus_interface,
 
851
                                     dbus.PROPERTIES_IFACE)
 
852
                return self.attributes[propname]
818
853
            def Approve(self, approve, dbus_interface):
819
854
                testcase.assertEqual(dbus_interface, client_interface)
820
855
                self.calls.append(("Approve", (approve,
869
904
            LastCheckerStatus=-2)
870
905
        self.clients =  collections.OrderedDict(
871
906
            [
872
 
                (self.client, self.client.attributes),
873
 
                (self.other_client, self.other_client.attributes),
 
907
                ("/clients/foo", self.client.attributes),
 
908
                ("/clients/barbar", self.other_client.attributes),
874
909
            ])
875
 
        self.one_client = {self.client: self.client.attributes}
 
910
        self.one_client = {"/clients/foo": self.client.attributes}
 
911
    @property
 
912
    def bus(self):
 
913
        class Bus(object):
 
914
            @staticmethod
 
915
            def get_object(client_bus_name, path):
 
916
                self.assertEqual(client_bus_name, busname)
 
917
                return {
 
918
                    "/clients/foo": self.client,
 
919
                    "/clients/barbar": self.other_client,
 
920
                }[path]
 
921
        return Bus()
876
922
 
877
923
class TestPrintTableCmd(TestCmd):
878
924
    def test_normal(self):
879
 
        output = PrintTableCmd().output(self.clients)
 
925
        output = PrintTableCmd().output(self.clients.values())
880
926
        expected_output = """
881
927
Name   Enabled Timeout  Last Successful Check
882
928
foo    Yes     00:05:00 2019-02-03T00:00:00  
884
930
"""[1:-1]
885
931
        self.assertEqual(output, expected_output)
886
932
    def test_verbose(self):
887
 
        output = PrintTableCmd(verbose=True).output(self.clients)
 
933
        output = PrintTableCmd(verbose=True).output(
 
934
            self.clients.values())
888
935
        expected_output = """
889
936
Name   Enabled Timeout  Last Successful Check Created             Interval Host            Key ID                                                           Fingerprint                              Check Is Running Last Enabled        Approval Is Pending Approved By Default Last Approval Request Approval Delay Approval Duration Checker              Extended Timeout Expires             Last Checker Status
890
937
foo    Yes     00:05:00 2019-02-03T00:00:00   2019-01-02T00:00:00 00:02:00 foo.example.org 92ed150794387c03ce684574b1139a6594a34f895daaaf09fd8ea90a27cddb12 778827225BA7DE539C5A7CFA59CFF7CDBD9A5920 No               2019-01-03T00:00:00 No                  Yes                                       00:00:00       00:00:01          fping -q -- %(host)s 00:15:00         2019-02-04T00:00:00 0                  
892
939
"""[1:-1]
893
940
        self.assertEqual(output, expected_output)
894
941
    def test_one_client(self):
895
 
        output = PrintTableCmd().output(self.one_client)
 
942
        output = PrintTableCmd().output(self.one_client.values())
896
943
        expected_output = """
897
944
Name Enabled Timeout  Last Successful Check
898
945
foo  Yes     00:05:00 2019-02-03T00:00:00  
967
1014
                            for client, properties in self.clients.items()))
968
1015
    def test_is_enabled_run_exits_successfully(self):
969
1016
        with self.assertRaises(SystemExit) as e:
970
 
            IsEnabledCmd().run(None, self.one_client)
 
1017
            IsEnabledCmd().run(self.one_client)
971
1018
        if e.exception.code is not None:
972
1019
            self.assertEqual(e.exception.code, 0)
973
1020
        else:
975
1022
    def test_is_enabled_run_exits_with_failure(self):
976
1023
        self.client.attributes["Enabled"] = dbus.Boolean(False)
977
1024
        with self.assertRaises(SystemExit) as e:
978
 
            IsEnabledCmd().run(None, self.one_client)
 
1025
            IsEnabledCmd().run(self.one_client)
979
1026
        if isinstance(e.exception.code, int):
980
1027
            self.assertNotEqual(e.exception.code, 0)
981
1028
        else:
990
1037
                self.calls.append(("RemoveClient", (dbus_path,)))
991
1038
        mandos = MockMandos()
992
1039
        super(TestRemoveCmd, self).setUp()
993
 
        RemoveCmd().run(mandos, self.clients)
 
1040
        RemoveCmd().run(self.clients, self.bus, mandos)
994
1041
        self.assertEqual(len(mandos.calls), 2)
995
 
        for client in self.clients:
996
 
            self.assertIn(("RemoveClient",
997
 
                           (client.__dbus_object_path__,)),
 
1042
        for clientpath in self.clients:
 
1043
            self.assertIn(("RemoveClient", (clientpath,)),
998
1044
                          mandos.calls)
999
1045
 
1000
1046
class TestApproveCmd(TestCmd):
1001
1047
    def test_approve(self):
1002
 
        ApproveCmd().run(None, self.clients)
1003
 
        for client in self.clients:
 
1048
        ApproveCmd().run(self.clients, self.bus)
 
1049
        for clientpath in self.clients:
 
1050
            client = self.bus.get_object(busname, clientpath)
1004
1051
            self.assertIn(("Approve", (True, client_interface)),
1005
1052
                          client.calls)
1006
1053
 
1007
1054
class TestDenyCmd(TestCmd):
1008
1055
    def test_deny(self):
1009
 
        DenyCmd().run(None, self.clients)
1010
 
        for client in self.clients:
 
1056
        DenyCmd().run(self.clients, self.bus)
 
1057
        for clientpath in self.clients:
 
1058
            client = self.bus.get_object(busname, clientpath)
1011
1059
            self.assertIn(("Approve", (False, client_interface)),
1012
1060
                          client.calls)
1013
1061
 
1014
1062
class TestEnableCmd(TestCmd):
1015
1063
    def test_enable(self):
1016
 
        for client in self.clients:
 
1064
        for clientpath in self.clients:
 
1065
            client = self.bus.get_object(busname, clientpath)
1017
1066
            client.attributes["Enabled"] = False
1018
1067
 
1019
 
        EnableCmd().run(None, self.clients)
 
1068
        EnableCmd().run(self.clients, self.bus)
1020
1069
 
1021
 
        for client in self.clients:
 
1070
        for clientpath in self.clients:
 
1071
            client = self.bus.get_object(busname, clientpath)
1022
1072
            self.assertTrue(client.attributes["Enabled"])
1023
1073
 
1024
1074
class TestDisableCmd(TestCmd):
1025
1075
    def test_disable(self):
1026
 
        DisableCmd().run(None, self.clients)
1027
 
 
1028
 
        for client in self.clients:
 
1076
        DisableCmd().run(self.clients, self.bus)
 
1077
        for clientpath in self.clients:
 
1078
            client = self.bus.get_object(busname, clientpath)
1029
1079
            self.assertFalse(client.attributes["Enabled"])
1030
1080
 
1031
1081
class Unique(object):
1041
1091
                                self.values_to_set)
1042
1092
        for value_to_set, value_to_get in zip(self.values_to_set,
1043
1093
                                              values_to_get):
1044
 
            for client in self.clients:
1045
 
                old_value = client.attributes[self.property]
 
1094
            for clientpath in self.clients:
 
1095
                client = self.bus.get_object(busname, clientpath)
 
1096
                old_value = client.attributes[self.propname]
1046
1097
                self.assertNotIsInstance(old_value, Unique)
1047
 
                client.attributes[self.property] = Unique()
 
1098
                client.attributes[self.propname] = Unique()
1048
1099
            self.run_command(value_to_set, self.clients)
1049
 
            for client in self.clients:
1050
 
                value = client.attributes[self.property]
 
1100
            for clientpath in self.clients:
 
1101
                client = self.bus.get_object(busname, clientpath)
 
1102
                value = client.attributes[self.propname]
1051
1103
                self.assertNotIsInstance(value, Unique)
1052
1104
                self.assertEqual(value, value_to_get)
1053
1105
    def run_command(self, value, clients):
1054
 
        self.command().run(None, clients)
 
1106
        self.command().run(clients, self.bus)
1055
1107
 
1056
1108
class TestBumpTimeoutCmd(TestPropertyCmd):
1057
1109
    command = BumpTimeoutCmd
1058
 
    property = "LastCheckedOK"
 
1110
    propname = "LastCheckedOK"
1059
1111
    values_to_set = [""]
1060
1112
 
1061
1113
class TestStartCheckerCmd(TestPropertyCmd):
1062
1114
    command = StartCheckerCmd
1063
 
    property = "CheckerRunning"
 
1115
    propname = "CheckerRunning"
1064
1116
    values_to_set = [dbus.Boolean(True)]
1065
1117
 
1066
1118
class TestStopCheckerCmd(TestPropertyCmd):
1067
1119
    command = StopCheckerCmd
1068
 
    property = "CheckerRunning"
 
1120
    propname = "CheckerRunning"
1069
1121
    values_to_set = [dbus.Boolean(False)]
1070
1122
 
1071
1123
class TestApproveByDefaultCmd(TestPropertyCmd):
1072
1124
    command = ApproveByDefaultCmd
1073
 
    property = "ApprovedByDefault"
 
1125
    propname = "ApprovedByDefault"
1074
1126
    values_to_set = [dbus.Boolean(True)]
1075
1127
 
1076
1128
class TestDenyByDefaultCmd(TestPropertyCmd):
1077
1129
    command = DenyByDefaultCmd
1078
 
    property = "ApprovedByDefault"
 
1130
    propname = "ApprovedByDefault"
1079
1131
    values_to_set = [dbus.Boolean(False)]
1080
1132
 
1081
1133
class TestValueArgumentPropertyCmd(TestPropertyCmd):
1086
1138
            return
1087
1139
        return super(TestValueArgumentPropertyCmd, self).runTest()
1088
1140
    def run_command(self, value, clients):
1089
 
        self.command(value).run(None, clients)
 
1141
        self.command(value).run(clients, self.bus)
1090
1142
 
1091
1143
class TestSetCheckerCmd(TestValueArgumentPropertyCmd):
1092
1144
    command = SetCheckerCmd
1093
 
    property = "Checker"
 
1145
    propname = "Checker"
1094
1146
    values_to_set = ["", ":", "fping -q -- %s"]
1095
1147
 
1096
1148
class TestSetHostCmd(TestValueArgumentPropertyCmd):
1097
1149
    command = SetHostCmd
1098
 
    property = "Host"
 
1150
    propname = "Host"
1099
1151
    values_to_set = ["192.0.2.3", "foo.example.org"]
1100
1152
 
1101
1153
class TestSetSecretCmd(TestValueArgumentPropertyCmd):
1102
1154
    command = SetSecretCmd
1103
 
    property = "Secret"
 
1155
    propname = "Secret"
1104
1156
    values_to_set = [io.BytesIO(b""),
1105
1157
                     io.BytesIO(b"secret\0xyzzy\nbar")]
1106
1158
    values_to_get = [b"", b"secret\0xyzzy\nbar"]
1107
1159
 
1108
1160
class TestSetTimeoutCmd(TestValueArgumentPropertyCmd):
1109
1161
    command = SetTimeoutCmd
1110
 
    property = "Timeout"
 
1162
    propname = "Timeout"
1111
1163
    values_to_set = [datetime.timedelta(),
1112
1164
                     datetime.timedelta(minutes=5),
1113
1165
                     datetime.timedelta(seconds=1),
1117
1169
 
1118
1170
class TestSetExtendedTimeoutCmd(TestValueArgumentPropertyCmd):
1119
1171
    command = SetExtendedTimeoutCmd
1120
 
    property = "ExtendedTimeout"
 
1172
    propname = "ExtendedTimeout"
1121
1173
    values_to_set = [datetime.timedelta(),
1122
1174
                     datetime.timedelta(minutes=5),
1123
1175
                     datetime.timedelta(seconds=1),
1127
1179
 
1128
1180
class TestSetIntervalCmd(TestValueArgumentPropertyCmd):
1129
1181
    command = SetIntervalCmd
1130
 
    property = "Interval"
 
1182
    propname = "Interval"
1131
1183
    values_to_set = [datetime.timedelta(),
1132
1184
                     datetime.timedelta(minutes=5),
1133
1185
                     datetime.timedelta(seconds=1),
1137
1189
 
1138
1190
class TestSetApprovalDelayCmd(TestValueArgumentPropertyCmd):
1139
1191
    command = SetApprovalDelayCmd
1140
 
    property = "ApprovalDelay"
 
1192
    propname = "ApprovalDelay"
1141
1193
    values_to_set = [datetime.timedelta(),
1142
1194
                     datetime.timedelta(minutes=5),
1143
1195
                     datetime.timedelta(seconds=1),
1147
1199
 
1148
1200
class TestSetApprovalDurationCmd(TestValueArgumentPropertyCmd):
1149
1201
    command = SetApprovalDurationCmd
1150
 
    property = "ApprovalDuration"
 
1202
    propname = "ApprovalDuration"
1151
1203
    values_to_set = [datetime.timedelta(),
1152
1204
                     datetime.timedelta(minutes=5),
1153
1205
                     datetime.timedelta(seconds=1),
1333
1385
    def test_is_enabled_short(self):
1334
1386
        self.assert_command_from_args(["-V", "foo"], IsEnabledCmd)
1335
1387
 
 
1388
    def test_deny_before_remove(self):
 
1389
        options = self.parser.parse_args(["--deny", "--remove", "foo"])
 
1390
        check_option_syntax(self.parser, options)
 
1391
        commands = commands_from_options(options)
 
1392
        self.assertEqual(len(commands), 2)
 
1393
        self.assertIsInstance(commands[0], DenyCmd)
 
1394
        self.assertIsInstance(commands[1], RemoveCmd)
 
1395
 
 
1396
    def test_deny_before_remove_reversed(self):
 
1397
        options = self.parser.parse_args(["--remove", "--deny", "--all"])
 
1398
        check_option_syntax(self.parser, options)
 
1399
        commands = commands_from_options(options)
 
1400
        self.assertEqual(len(commands), 2)
 
1401
        self.assertIsInstance(commands[0], DenyCmd)
 
1402
        self.assertIsInstance(commands[1], RemoveCmd)
 
1403
 
1336
1404
 
1337
1405
class Test_check_option_syntax(unittest.TestCase):
1338
1406
    # This mostly corresponds to the definition from has_actions() in
1453
1521
        with self.assertParseError():
1454
1522
            self.check_option_syntax(options)
1455
1523
 
 
1524
    def test_remove_can_only_be_combined_with_action_deny(self):
 
1525
        for action, value in self.actions.items():
 
1526
            if action in {"remove", "deny"}:
 
1527
                continue
 
1528
            options = self.parser.parse_args()
 
1529
            setattr(options, action, value)
 
1530
            options.all = True
 
1531
            options.remove = True
 
1532
            with self.assertParseError():
 
1533
                self.check_option_syntax(options)
 
1534
 
1456
1535
 
1457
1536
 
1458
1537
def should_only_run_tests():