/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-12 19:15:52 UTC
  • Revision ID: teddy@recompile.se-20190312191552-f1di4dzya1pzxc9a
mandos-ctl: Refactor

* mandos-ctl (TestPrintTableCmd.test_verbose): Reformat for easier
                                               editing.

Show diffs side-by-side

added added

removed removed

Lines of Context:
44
44
import logging
45
45
import io
46
46
import tempfile
 
47
import contextlib
47
48
 
48
49
import dbus
49
50
 
63
64
 
64
65
locale.setlocale(locale.LC_ALL, "")
65
66
 
66
 
domain = "se.recompile"
67
 
busname = domain + ".Mandos"
68
 
server_path = "/"
69
 
server_interface = domain + ".Mandos"
70
 
client_interface = domain + ".Mandos.Client"
 
67
dbus_busname_domain = "se.recompile"
 
68
dbus_busname = dbus_busname_domain + ".Mandos"
 
69
server_dbus_path = "/"
 
70
server_dbus_interface = dbus_busname_domain + ".Mandos"
 
71
client_dbus_interface = dbus_busname_domain + ".Mandos.Client"
 
72
del dbus_busname_domain
71
73
version = "1.8.3"
72
74
 
73
75
 
275
277
# Abstract classes first
276
278
class Command(object):
277
279
    """Abstract class for commands"""
278
 
    def run(self, mandos, clients):
 
280
    def run(self, clients, bus=None, mandos=None):
279
281
        """Normal commands should implement run_on_one_client(), but
280
282
        commands which want to operate on all clients at the same time
281
283
        can override this run() method instead."""
282
284
        self.mandos = mandos
283
 
        for client, properties in clients.items():
 
285
        for clientpath, properties in clients.items():
 
286
            log.debug("D-Bus: Connect to: (busname=%r, path=%r)",
 
287
                      dbus_busname, str(clientpath))
 
288
            client = bus.get_object(dbus_busname, clientpath)
284
289
            self.run_on_one_client(client, properties)
285
290
 
286
291
class PrintCmd(Command):
292
297
                    "LastApprovalRequest", "ApprovalDelay",
293
298
                    "ApprovalDuration", "Checker", "ExtendedTimeout",
294
299
                    "Expires", "LastCheckerStatus")
295
 
    def run(self, mandos, clients):
296
 
        print(self.output(clients))
 
300
    def run(self, clients, bus=None, mandos=None):
 
301
        print(self.output(clients.values()))
 
302
    def output(self, clients):
 
303
        raise NotImplementedError()
297
304
 
298
305
class PropertyCmd(Command):
299
306
    """Abstract class for Actions for setting one client property"""
300
307
    def run_on_one_client(self, client, properties):
301
308
        """Set the Client's D-Bus property"""
302
 
        client.Set(client_interface, self.property, self.value_to_set,
 
309
        log.debug("D-Bus: %s:%s:%s.Set(%r, %r, %r)", dbus_busname,
 
310
                  client.__dbus_object_path__,
 
311
                  dbus.PROPERTIES_IFACE, client_dbus_interface,
 
312
                  self.propname, self.value_to_set
 
313
                  if not isinstance(self.value_to_set, dbus.Boolean)
 
314
                  else bool(self.value_to_set))
 
315
        client.Set(client_dbus_interface, self.propname,
 
316
                   self.value_to_set,
303
317
                   dbus_interface=dbus.PROPERTIES_IFACE)
 
318
    @property
 
319
    def propname(self):
 
320
        raise NotImplementedError()
304
321
 
305
 
class ValueArgumentMixIn(object):
306
 
    """Mixin class for commands taking a value as argument"""
 
322
class PropertyValueCmd(PropertyCmd):
 
323
    """Abstract class for PropertyCmd recieving a value as argument"""
307
324
    def __init__(self, value):
308
325
        self.value_to_set = value
309
326
 
310
 
class MillisecondsValueArgumentMixIn(ValueArgumentMixIn):
311
 
    """Mixin class for commands taking a value argument as
312
 
    milliseconds."""
 
327
class MillisecondsPropertyValueArgumentCmd(PropertyValueCmd):
 
328
    """Abstract class for PropertyValueCmd taking a value argument as
 
329
a datetime.timedelta() but should store it as milliseconds."""
313
330
    @property
314
331
    def value_to_set(self):
315
332
        return self._vts
316
333
    @value_to_set.setter
317
334
    def value_to_set(self, value):
318
 
        """When setting, convert value to a datetime.timedelta"""
 
335
        """When setting, convert value from a datetime.timedelta"""
319
336
        self._vts = int(round(value.total_seconds() * 1000))
320
337
 
321
338
# Actual (non-abstract) command classes
325
342
        self.verbose = verbose
326
343
 
327
344
    def output(self, clients):
328
 
        default_keywords = ("Name", "Enabled", "Timeout", "LastCheckedOK")
 
345
        default_keywords = ("Name", "Enabled", "Timeout",
 
346
                            "LastCheckedOK")
329
347
        keywords = default_keywords
330
348
        if self.verbose:
331
349
            keywords = self.all_keywords
332
 
        return str(self.TableOfClients(clients.values(), keywords))
 
350
        return str(self.TableOfClients(clients, keywords))
333
351
 
334
352
    class TableOfClients(object):
335
353
        tableheaders = {
421
439
        return value
422
440
 
423
441
class IsEnabledCmd(Command):
424
 
    def run_on_one_client(self, client, properties):
 
442
    def run(self, clients, bus=None, mandos=None):
 
443
        client, properties = next(iter(clients.items()))
425
444
        if self.is_enabled(client, properties):
426
445
            sys.exit(0)
427
446
        sys.exit(1)
428
447
    def is_enabled(self, client, properties):
429
 
        return bool(properties["Enabled"])
 
448
        return properties["Enabled"]
430
449
 
431
450
class RemoveCmd(Command):
432
451
    def run_on_one_client(self, client, properties):
 
452
        log.debug("D-Bus: %s:%s:%s.RemoveClient(%r)", dbus_busname,
 
453
                  server_dbus_path, server_dbus_interface,
 
454
                  str(client.__dbus_object_path__))
433
455
        self.mandos.RemoveClient(client.__dbus_object_path__)
434
456
 
435
457
class ApproveCmd(Command):
436
458
    def run_on_one_client(self, client, properties):
 
459
        log.debug("D-Bus: %s:%s:%s.Approve(True)", dbus_busname,
 
460
                  client.__dbus_object_path__, client_dbus_interface)
437
461
        client.Approve(dbus.Boolean(True),
438
 
                       dbus_interface=client_interface)
 
462
                       dbus_interface=client_dbus_interface)
439
463
 
440
464
class DenyCmd(Command):
441
465
    def run_on_one_client(self, client, properties):
 
466
        log.debug("D-Bus: %s:%s:%s.Approve(False)", dbus_busname,
 
467
                  client.__dbus_object_path__, client_dbus_interface)
442
468
        client.Approve(dbus.Boolean(False),
443
 
                       dbus_interface=client_interface)
 
469
                       dbus_interface=client_dbus_interface)
444
470
 
445
471
class EnableCmd(PropertyCmd):
446
 
    property = "Enabled"
 
472
    propname = "Enabled"
447
473
    value_to_set = dbus.Boolean(True)
448
474
 
449
475
class DisableCmd(PropertyCmd):
450
 
    property = "Enabled"
 
476
    propname = "Enabled"
451
477
    value_to_set = dbus.Boolean(False)
452
478
 
453
479
class BumpTimeoutCmd(PropertyCmd):
454
 
    property = "LastCheckedOK"
 
480
    propname = "LastCheckedOK"
455
481
    value_to_set = ""
456
482
 
457
483
class StartCheckerCmd(PropertyCmd):
458
 
    property = "CheckerRunning"
 
484
    propname = "CheckerRunning"
459
485
    value_to_set = dbus.Boolean(True)
460
486
 
461
487
class StopCheckerCmd(PropertyCmd):
462
 
    property = "CheckerRunning"
 
488
    propname = "CheckerRunning"
463
489
    value_to_set = dbus.Boolean(False)
464
490
 
465
491
class ApproveByDefaultCmd(PropertyCmd):
466
 
    property = "ApprovedByDefault"
 
492
    propname = "ApprovedByDefault"
467
493
    value_to_set = dbus.Boolean(True)
468
494
 
469
495
class DenyByDefaultCmd(PropertyCmd):
470
 
    property = "ApprovedByDefault"
 
496
    propname = "ApprovedByDefault"
471
497
    value_to_set = dbus.Boolean(False)
472
498
 
473
 
class SetCheckerCmd(PropertyCmd, ValueArgumentMixIn):
474
 
    property = "Checker"
475
 
 
476
 
class SetHostCmd(PropertyCmd, ValueArgumentMixIn):
477
 
    property = "Host"
478
 
 
479
 
class SetSecretCmd(PropertyCmd, ValueArgumentMixIn):
 
499
class SetCheckerCmd(PropertyValueCmd):
 
500
    propname = "Checker"
 
501
 
 
502
class SetHostCmd(PropertyValueCmd):
 
503
    propname = "Host"
 
504
 
 
505
class SetSecretCmd(PropertyValueCmd):
 
506
    propname = "Secret"
480
507
    @property
481
508
    def value_to_set(self):
482
509
        return self._vts
485
512
        """When setting, read data from supplied file object"""
486
513
        self._vts = value.read()
487
514
        value.close()
488
 
    property = "Secret"
489
 
 
490
 
class SetTimeoutCmd(PropertyCmd, MillisecondsValueArgumentMixIn):
491
 
    property = "Timeout"
492
 
 
493
 
class SetExtendedTimeoutCmd(PropertyCmd,
494
 
                            MillisecondsValueArgumentMixIn):
495
 
    property = "ExtendedTimeout"
496
 
 
497
 
class SetIntervalCmd(PropertyCmd, MillisecondsValueArgumentMixIn):
498
 
    property = "Interval"
499
 
 
500
 
class SetApprovalDelayCmd(PropertyCmd,
501
 
                          MillisecondsValueArgumentMixIn):
502
 
    property = "ApprovalDelay"
503
 
 
504
 
class SetApprovalDurationCmd(PropertyCmd,
505
 
                             MillisecondsValueArgumentMixIn):
506
 
    property = "ApprovalDuration"
 
515
 
 
516
class SetTimeoutCmd(MillisecondsPropertyValueArgumentCmd):
 
517
    propname = "Timeout"
 
518
 
 
519
class SetExtendedTimeoutCmd(MillisecondsPropertyValueArgumentCmd):
 
520
    propname = "ExtendedTimeout"
 
521
 
 
522
class SetIntervalCmd(MillisecondsPropertyValueArgumentCmd):
 
523
    propname = "Interval"
 
524
 
 
525
class SetApprovalDelayCmd(MillisecondsPropertyValueArgumentCmd):
 
526
    propname = "ApprovalDelay"
 
527
 
 
528
class SetApprovalDurationCmd(MillisecondsPropertyValueArgumentCmd):
 
529
    propname = "ApprovalDuration"
507
530
 
508
531
def add_command_line_options(parser):
509
532
    parser.add_argument("--version", action="version",
565
588
        help="Approve any current client request")
566
589
    approve_deny.add_argument("-D", "--deny", action="store_true",
567
590
                              help="Deny any current client request")
 
591
    parser.add_argument("--debug", action="store_true",
 
592
                        help="Debug mode (show D-Bus commands)")
568
593
    parser.add_argument("--check", action="store_true",
569
594
                        help="Run self-test")
570
595
    parser.add_argument("client", nargs="*", help="Client name")
595
620
    if options.is_enabled:
596
621
        commands.append(IsEnabledCmd())
597
622
 
598
 
    if options.remove:
599
 
        commands.append(RemoveCmd())
600
 
 
601
623
    if options.checker is not None:
602
624
        commands.append(SetCheckerCmd(options.checker))
603
625
 
636
658
    if options.deny:
637
659
        commands.append(DenyCmd())
638
660
 
 
661
    if options.remove:
 
662
        commands.append(RemoveCmd())
 
663
 
639
664
    # If no command option has been given, show table of clients,
640
665
    # optionally verbosely
641
666
    if not commands:
644
669
    return commands
645
670
 
646
671
 
647
 
def main():
648
 
    parser = argparse.ArgumentParser()
649
 
 
650
 
    add_command_line_options(parser)
651
 
 
652
 
    options = parser.parse_args()
 
672
def check_option_syntax(parser, options):
 
673
    """Apply additional restrictions on options, not expressible in
 
674
argparse"""
653
675
 
654
676
    def has_actions(options):
655
677
        return any((options.enable,
682
704
        parser.error("--all requires an action.")
683
705
    if options.is_enabled and len(options.client) > 1:
684
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
 
712
 
 
713
 
 
714
def main():
 
715
    parser = argparse.ArgumentParser()
 
716
 
 
717
    add_command_line_options(parser)
 
718
 
 
719
    options = parser.parse_args()
 
720
 
 
721
    check_option_syntax(parser, options)
685
722
 
686
723
    clientnames = options.client
687
724
 
 
725
    if options.debug:
 
726
        log.setLevel(logging.DEBUG)
 
727
 
688
728
    try:
689
729
        bus = dbus.SystemBus()
690
 
        mandos_dbus_objc = bus.get_object(busname, server_path)
 
730
        log.debug("D-Bus: Connect to: (busname=%r, path=%r)",
 
731
                  dbus_busname, server_dbus_path)
 
732
        mandos_dbus_objc = bus.get_object(dbus_busname,
 
733
                                          server_dbus_path)
691
734
    except dbus.exceptions.DBusException:
692
735
        log.critical("Could not connect to Mandos server")
693
736
        sys.exit(1)
694
737
 
695
738
    mandos_serv = dbus.Interface(mandos_dbus_objc,
696
 
                                 dbus_interface=server_interface)
 
739
                                 dbus_interface=server_dbus_interface)
697
740
    mandos_serv_object_manager = dbus.Interface(
698
741
        mandos_dbus_objc, dbus_interface=dbus.OBJECT_MANAGER_IFACE)
699
742
 
705
748
    dbus_filter = NullFilter()
706
749
    try:
707
750
        dbus_logger.addFilter(dbus_filter)
708
 
        mandos_clients = {path: ifs_and_props[client_interface]
 
751
        log.debug("D-Bus: %s:%s:%s.GetManagedObjects()", dbus_busname,
 
752
                  server_dbus_path, dbus.OBJECT_MANAGER_IFACE)
 
753
        mandos_clients = {path: ifs_and_props[client_dbus_interface]
709
754
                          for path, ifs_and_props in
710
755
                          mandos_serv_object_manager
711
756
                          .GetManagedObjects().items()
712
 
                          if client_interface in ifs_and_props}
 
757
                          if client_dbus_interface in ifs_and_props}
713
758
    except dbus.exceptions.DBusException as e:
714
759
        log.critical("Failed to access Mandos server through D-Bus:"
715
760
                     "\n%s", e)
722
767
    clients = {}
723
768
 
724
769
    if not clientnames:
725
 
        clients = {bus.get_object(busname, path): properties
726
 
                   for path, properties in mandos_clients.items()}
 
770
        clients = {objpath: properties
 
771
                   for objpath, properties in mandos_clients.items()}
727
772
    else:
728
773
        for name in clientnames:
729
 
            for path, client in mandos_clients.items():
730
 
                if client["Name"] == name:
731
 
                    client_objc = bus.get_object(busname, path)
732
 
                    clients[client_objc] = client
 
774
            for objpath, properties in mandos_clients.items():
 
775
                if properties["Name"] == name:
 
776
                    clients[objpath] = properties
733
777
                    break
734
778
            else:
735
779
                log.critical("Client not found on server: %r", name)
738
782
    # Run all commands on clients
739
783
    commands = commands_from_options(options)
740
784
    for command in commands:
741
 
        command.run(mandos_serv, clients)
 
785
        command.run(clients, bus, mandos_serv)
742
786
 
743
787
 
744
788
class Test_milliseconds_to_string(unittest.TestCase):
793
837
        testcase = self
794
838
        class MockClient(object):
795
839
            def __init__(self, name, **attributes):
796
 
                self.__dbus_object_path__ = "objpath_{}".format(name)
 
840
                self.__dbus_object_path__ = "/clients/{}".format(name)
797
841
                self.attributes = attributes
798
842
                self.attributes["Name"] = name
799
843
                self.calls = []
800
 
            def Set(self, interface, property, value, dbus_interface):
801
 
                testcase.assertEqual(interface, client_interface)
802
 
                testcase.assertEqual(dbus_interface,
803
 
                                     dbus.PROPERTIES_IFACE)
804
 
                self.attributes[property] = value
805
 
            def Get(self, interface, property, dbus_interface):
806
 
                testcase.assertEqual(interface, client_interface)
807
 
                testcase.assertEqual(dbus_interface,
808
 
                                     dbus.PROPERTIES_IFACE)
809
 
                return self.attributes[property]
 
844
            def Set(self, interface, propname, value, dbus_interface):
 
845
                testcase.assertEqual(interface, client_dbus_interface)
 
846
                testcase.assertEqual(dbus_interface,
 
847
                                     dbus.PROPERTIES_IFACE)
 
848
                self.attributes[propname] = value
 
849
            def Get(self, interface, propname, dbus_interface):
 
850
                testcase.assertEqual(interface, client_dbus_interface)
 
851
                testcase.assertEqual(dbus_interface,
 
852
                                     dbus.PROPERTIES_IFACE)
 
853
                return self.attributes[propname]
810
854
            def Approve(self, approve, dbus_interface):
811
 
                testcase.assertEqual(dbus_interface, client_interface)
 
855
                testcase.assertEqual(dbus_interface,
 
856
                                     client_dbus_interface)
812
857
                self.calls.append(("Approve", (approve,
813
858
                                               dbus_interface)))
814
859
        self.client = MockClient(
861
906
            LastCheckerStatus=-2)
862
907
        self.clients =  collections.OrderedDict(
863
908
            [
864
 
                (self.client, self.client.attributes),
865
 
                (self.other_client, self.other_client.attributes),
 
909
                ("/clients/foo", self.client.attributes),
 
910
                ("/clients/barbar", self.other_client.attributes),
866
911
            ])
867
 
        self.one_client = {self.client: self.client.attributes}
 
912
        self.one_client = {"/clients/foo": self.client.attributes}
 
913
    @property
 
914
    def bus(self):
 
915
        class Bus(object):
 
916
            @staticmethod
 
917
            def get_object(client_bus_name, path):
 
918
                self.assertEqual(client_bus_name, dbus_busname)
 
919
                return {
 
920
                    "/clients/foo": self.client,
 
921
                    "/clients/barbar": self.other_client,
 
922
                }[path]
 
923
        return Bus()
868
924
 
869
925
class TestPrintTableCmd(TestCmd):
870
926
    def test_normal(self):
871
 
        output = PrintTableCmd().output(self.clients)
872
 
        expected_output = """
873
 
Name   Enabled Timeout  Last Successful Check
874
 
foo    Yes     00:05:00 2019-02-03T00:00:00  
875
 
barbar Yes     00:05:00 2019-02-04T00:00:00  
876
 
"""[1:-1]
 
927
        output = PrintTableCmd().output(self.clients.values())
 
928
        expected_output = "\n".join((
 
929
            "Name   Enabled Timeout  Last Successful Check",
 
930
            "foo    Yes     00:05:00 2019-02-03T00:00:00  ",
 
931
            "barbar Yes     00:05:00 2019-02-04T00:00:00  ",
 
932
        ))
877
933
        self.assertEqual(output, expected_output)
878
934
    def test_verbose(self):
879
 
        output = PrintTableCmd(verbose=True).output(self.clients)
880
 
        expected_output = """
881
 
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
882
 
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                  
883
 
barbar Yes     00:05:00 2019-02-04T00:00:00   2019-01-03T00:00:00 00:02:00 192.0.2.3       0558568eedd67d622f5c83b35a115f796ab612cff5ad227247e46c2b020f441c 3E393AEAEFB84C7E89E2F547B3A107558FCA3A27 Yes              2019-01-04T00:00:00 No                  No                  2019-01-03T00:00:00   00:00:30       00:00:01          :                    00:15:00         2019-02-05T00:00:00 -2                 
884
 
"""[1:-1]
 
935
        output = PrintTableCmd(verbose=True).output(
 
936
            self.clients.values())
 
937
        columns = (
 
938
            (
 
939
                "Name   ",
 
940
                "foo    ",
 
941
                "barbar ",
 
942
            ),(
 
943
                "Enabled ",
 
944
                "Yes     ",
 
945
                "Yes     ",
 
946
            ),(
 
947
                "Timeout  ",
 
948
                "00:05:00 ",
 
949
                "00:05:00 ",
 
950
            ),(
 
951
                "Last Successful Check ",
 
952
                "2019-02-03T00:00:00   ",
 
953
                "2019-02-04T00:00:00   ",
 
954
            ),(
 
955
                "Created             ",
 
956
                "2019-01-02T00:00:00 ",
 
957
                "2019-01-03T00:00:00 ",
 
958
            ),(
 
959
                "Interval ",
 
960
                "00:02:00 ",
 
961
                "00:02:00 ",
 
962
            ),(
 
963
                "Host            ",
 
964
                "foo.example.org ",
 
965
                "192.0.2.3       ",
 
966
            ),(
 
967
                ("Key ID                                             "
 
968
                 "              "),
 
969
                ("92ed150794387c03ce684574b1139a6594a34f895daaaf09fd8"
 
970
                 "ea90a27cddb12 "),
 
971
                ("0558568eedd67d622f5c83b35a115f796ab612cff5ad227247e"
 
972
                 "46c2b020f441c "),
 
973
            ),(
 
974
                "Fingerprint                              ",
 
975
                "778827225BA7DE539C5A7CFA59CFF7CDBD9A5920 ",
 
976
                "3E393AEAEFB84C7E89E2F547B3A107558FCA3A27 ",
 
977
            ),(
 
978
                "Check Is Running ",
 
979
                "No               ",
 
980
                "Yes              ",
 
981
            ),(
 
982
                "Last Enabled        ",
 
983
                "2019-01-03T00:00:00 ",
 
984
                "2019-01-04T00:00:00 ",
 
985
            ),(
 
986
                "Approval Is Pending ",
 
987
                "No                  ",
 
988
                "No                  ",
 
989
            ),(
 
990
                "Approved By Default ",
 
991
                "Yes                 ",
 
992
                "No                  ",
 
993
            ),(
 
994
                "Last Approval Request ",
 
995
                "                      ",
 
996
                "2019-01-03T00:00:00   ",
 
997
            ),(
 
998
                "Approval Delay ",
 
999
                "00:00:00       ",
 
1000
                "00:00:30       ",
 
1001
            ),(
 
1002
                "Approval Duration ",
 
1003
                "00:00:01          ",
 
1004
                "00:00:01          ",
 
1005
            ),(
 
1006
                "Checker              ",
 
1007
                "fping -q -- %(host)s ",
 
1008
                ":                    ",
 
1009
            ),(
 
1010
                "Extended Timeout ",
 
1011
                "00:15:00         ",
 
1012
                "00:15:00         ",
 
1013
            ),(
 
1014
                "Expires             ",
 
1015
                "2019-02-04T00:00:00 ",
 
1016
                "2019-02-05T00:00:00 ",
 
1017
            ),(
 
1018
                "Last Checker Status",
 
1019
                "0                  ",
 
1020
                "-2                 ",
 
1021
            )
 
1022
        )
 
1023
        num_lines = max(len(rows) for rows in columns)
 
1024
        expected_output = "\n".join("".join(rows[line]
 
1025
                                            for rows in columns)
 
1026
                                    for line in range(num_lines))
885
1027
        self.assertEqual(output, expected_output)
886
1028
    def test_one_client(self):
887
 
        output = PrintTableCmd().output(self.one_client)
 
1029
        output = PrintTableCmd().output(self.one_client.values())
888
1030
        expected_output = """
889
1031
Name Enabled Timeout  Last Successful Check
890
1032
foo  Yes     00:05:00 2019-02-03T00:00:00  
955
1097
 
956
1098
class TestIsEnabledCmd(TestCmd):
957
1099
    def test_is_enabled(self):
958
 
        self.assertTrue(all(IsEnabledCmd().is_enabled(client, properties)
959
 
                            for client, properties in self.clients.items()))
 
1100
        self.assertTrue(all(IsEnabledCmd().is_enabled(client,
 
1101
                                                      properties)
 
1102
                            for client, properties
 
1103
                            in self.clients.items()))
960
1104
    def test_is_enabled_run_exits_successfully(self):
961
1105
        with self.assertRaises(SystemExit) as e:
962
 
            IsEnabledCmd().run(None, self.one_client)
 
1106
            IsEnabledCmd().run(self.one_client)
963
1107
        if e.exception.code is not None:
964
1108
            self.assertEqual(e.exception.code, 0)
965
1109
        else:
967
1111
    def test_is_enabled_run_exits_with_failure(self):
968
1112
        self.client.attributes["Enabled"] = dbus.Boolean(False)
969
1113
        with self.assertRaises(SystemExit) as e:
970
 
            IsEnabledCmd().run(None, self.one_client)
 
1114
            IsEnabledCmd().run(self.one_client)
971
1115
        if isinstance(e.exception.code, int):
972
1116
            self.assertNotEqual(e.exception.code, 0)
973
1117
        else:
982
1126
                self.calls.append(("RemoveClient", (dbus_path,)))
983
1127
        mandos = MockMandos()
984
1128
        super(TestRemoveCmd, self).setUp()
985
 
        RemoveCmd().run(mandos, self.clients)
 
1129
        RemoveCmd().run(self.clients, self.bus, mandos)
986
1130
        self.assertEqual(len(mandos.calls), 2)
987
 
        for client in self.clients:
988
 
            self.assertIn(("RemoveClient",
989
 
                           (client.__dbus_object_path__,)),
 
1131
        for clientpath in self.clients:
 
1132
            self.assertIn(("RemoveClient", (clientpath,)),
990
1133
                          mandos.calls)
991
1134
 
992
1135
class TestApproveCmd(TestCmd):
993
1136
    def test_approve(self):
994
 
        ApproveCmd().run(None, self.clients)
995
 
        for client in self.clients:
996
 
            self.assertIn(("Approve", (True, client_interface)),
 
1137
        ApproveCmd().run(self.clients, self.bus)
 
1138
        for clientpath in self.clients:
 
1139
            client = self.bus.get_object(dbus_busname, clientpath)
 
1140
            self.assertIn(("Approve", (True, client_dbus_interface)),
997
1141
                          client.calls)
998
1142
 
999
1143
class TestDenyCmd(TestCmd):
1000
1144
    def test_deny(self):
1001
 
        DenyCmd().run(None, self.clients)
1002
 
        for client in self.clients:
1003
 
            self.assertIn(("Approve", (False, client_interface)),
 
1145
        DenyCmd().run(self.clients, self.bus)
 
1146
        for clientpath in self.clients:
 
1147
            client = self.bus.get_object(dbus_busname, clientpath)
 
1148
            self.assertIn(("Approve", (False, client_dbus_interface)),
1004
1149
                          client.calls)
1005
1150
 
1006
1151
class TestEnableCmd(TestCmd):
1007
1152
    def test_enable(self):
1008
 
        for client in self.clients:
 
1153
        for clientpath in self.clients:
 
1154
            client = self.bus.get_object(dbus_busname, clientpath)
1009
1155
            client.attributes["Enabled"] = False
1010
1156
 
1011
 
        EnableCmd().run(None, self.clients)
 
1157
        EnableCmd().run(self.clients, self.bus)
1012
1158
 
1013
 
        for client in self.clients:
 
1159
        for clientpath in self.clients:
 
1160
            client = self.bus.get_object(dbus_busname, clientpath)
1014
1161
            self.assertTrue(client.attributes["Enabled"])
1015
1162
 
1016
1163
class TestDisableCmd(TestCmd):
1017
1164
    def test_disable(self):
1018
 
        DisableCmd().run(None, self.clients)
1019
 
 
1020
 
        for client in self.clients:
 
1165
        DisableCmd().run(self.clients, self.bus)
 
1166
        for clientpath in self.clients:
 
1167
            client = self.bus.get_object(dbus_busname, clientpath)
1021
1168
            self.assertFalse(client.attributes["Enabled"])
1022
1169
 
1023
1170
class Unique(object):
1033
1180
                                self.values_to_set)
1034
1181
        for value_to_set, value_to_get in zip(self.values_to_set,
1035
1182
                                              values_to_get):
1036
 
            for client in self.clients:
1037
 
                old_value = client.attributes[self.property]
 
1183
            for clientpath in self.clients:
 
1184
                client = self.bus.get_object(dbus_busname, clientpath)
 
1185
                old_value = client.attributes[self.propname]
1038
1186
                self.assertNotIsInstance(old_value, Unique)
1039
 
                client.attributes[self.property] = Unique()
 
1187
                client.attributes[self.propname] = Unique()
1040
1188
            self.run_command(value_to_set, self.clients)
1041
 
            for client in self.clients:
1042
 
                value = client.attributes[self.property]
 
1189
            for clientpath in self.clients:
 
1190
                client = self.bus.get_object(dbus_busname, clientpath)
 
1191
                value = client.attributes[self.propname]
1043
1192
                self.assertNotIsInstance(value, Unique)
1044
1193
                self.assertEqual(value, value_to_get)
1045
1194
    def run_command(self, value, clients):
1046
 
        self.command().run(None, clients)
 
1195
        self.command().run(clients, self.bus)
1047
1196
 
1048
1197
class TestBumpTimeoutCmd(TestPropertyCmd):
1049
1198
    command = BumpTimeoutCmd
1050
 
    property = "LastCheckedOK"
 
1199
    propname = "LastCheckedOK"
1051
1200
    values_to_set = [""]
1052
1201
 
1053
1202
class TestStartCheckerCmd(TestPropertyCmd):
1054
1203
    command = StartCheckerCmd
1055
 
    property = "CheckerRunning"
 
1204
    propname = "CheckerRunning"
1056
1205
    values_to_set = [dbus.Boolean(True)]
1057
1206
 
1058
1207
class TestStopCheckerCmd(TestPropertyCmd):
1059
1208
    command = StopCheckerCmd
1060
 
    property = "CheckerRunning"
 
1209
    propname = "CheckerRunning"
1061
1210
    values_to_set = [dbus.Boolean(False)]
1062
1211
 
1063
1212
class TestApproveByDefaultCmd(TestPropertyCmd):
1064
1213
    command = ApproveByDefaultCmd
1065
 
    property = "ApprovedByDefault"
 
1214
    propname = "ApprovedByDefault"
1066
1215
    values_to_set = [dbus.Boolean(True)]
1067
1216
 
1068
1217
class TestDenyByDefaultCmd(TestPropertyCmd):
1069
1218
    command = DenyByDefaultCmd
1070
 
    property = "ApprovedByDefault"
 
1219
    propname = "ApprovedByDefault"
1071
1220
    values_to_set = [dbus.Boolean(False)]
1072
1221
 
1073
 
class TestValueArgumentPropertyCmd(TestPropertyCmd):
1074
 
    """Abstract class for tests of PropertyCmd classes using the
1075
 
ValueArgumentMixIn"""
 
1222
class TestPropertyValueCmd(TestPropertyCmd):
 
1223
    """Abstract class for tests of PropertyValueCmd classes"""
1076
1224
    def runTest(self):
1077
 
        if type(self) is TestValueArgumentPropertyCmd:
 
1225
        if type(self) is TestPropertyValueCmd:
1078
1226
            return
1079
 
        return super(TestValueArgumentPropertyCmd, self).runTest()
 
1227
        return super(TestPropertyValueCmd, self).runTest()
1080
1228
    def run_command(self, value, clients):
1081
 
        self.command(value).run(None, clients)
 
1229
        self.command(value).run(clients, self.bus)
1082
1230
 
1083
 
class TestSetCheckerCmd(TestValueArgumentPropertyCmd):
 
1231
class TestSetCheckerCmd(TestPropertyValueCmd):
1084
1232
    command = SetCheckerCmd
1085
 
    property = "Checker"
 
1233
    propname = "Checker"
1086
1234
    values_to_set = ["", ":", "fping -q -- %s"]
1087
1235
 
1088
 
class TestSetHostCmd(TestValueArgumentPropertyCmd):
 
1236
class TestSetHostCmd(TestPropertyValueCmd):
1089
1237
    command = SetHostCmd
1090
 
    property = "Host"
 
1238
    propname = "Host"
1091
1239
    values_to_set = ["192.0.2.3", "foo.example.org"]
1092
1240
 
1093
 
class TestSetSecretCmd(TestValueArgumentPropertyCmd):
 
1241
class TestSetSecretCmd(TestPropertyValueCmd):
1094
1242
    command = SetSecretCmd
1095
 
    property = "Secret"
1096
 
    values_to_set = [open("/dev/null", "rb"),
 
1243
    propname = "Secret"
 
1244
    values_to_set = [io.BytesIO(b""),
1097
1245
                     io.BytesIO(b"secret\0xyzzy\nbar")]
1098
1246
    values_to_get = [b"", b"secret\0xyzzy\nbar"]
1099
1247
 
1100
 
class TestSetTimeoutCmd(TestValueArgumentPropertyCmd):
 
1248
class TestSetTimeoutCmd(TestPropertyValueCmd):
1101
1249
    command = SetTimeoutCmd
1102
 
    property = "Timeout"
 
1250
    propname = "Timeout"
1103
1251
    values_to_set = [datetime.timedelta(),
1104
1252
                     datetime.timedelta(minutes=5),
1105
1253
                     datetime.timedelta(seconds=1),
1107
1255
                     datetime.timedelta(weeks=52)]
1108
1256
    values_to_get = [0, 300000, 1000, 604800000, 31449600000]
1109
1257
 
1110
 
class TestSetExtendedTimeoutCmd(TestValueArgumentPropertyCmd):
 
1258
class TestSetExtendedTimeoutCmd(TestPropertyValueCmd):
1111
1259
    command = SetExtendedTimeoutCmd
1112
 
    property = "ExtendedTimeout"
 
1260
    propname = "ExtendedTimeout"
1113
1261
    values_to_set = [datetime.timedelta(),
1114
1262
                     datetime.timedelta(minutes=5),
1115
1263
                     datetime.timedelta(seconds=1),
1117
1265
                     datetime.timedelta(weeks=52)]
1118
1266
    values_to_get = [0, 300000, 1000, 604800000, 31449600000]
1119
1267
 
1120
 
class TestSetIntervalCmd(TestValueArgumentPropertyCmd):
 
1268
class TestSetIntervalCmd(TestPropertyValueCmd):
1121
1269
    command = SetIntervalCmd
1122
 
    property = "Interval"
 
1270
    propname = "Interval"
1123
1271
    values_to_set = [datetime.timedelta(),
1124
1272
                     datetime.timedelta(minutes=5),
1125
1273
                     datetime.timedelta(seconds=1),
1127
1275
                     datetime.timedelta(weeks=52)]
1128
1276
    values_to_get = [0, 300000, 1000, 604800000, 31449600000]
1129
1277
 
1130
 
class TestSetApprovalDelayCmd(TestValueArgumentPropertyCmd):
 
1278
class TestSetApprovalDelayCmd(TestPropertyValueCmd):
1131
1279
    command = SetApprovalDelayCmd
1132
 
    property = "ApprovalDelay"
 
1280
    propname = "ApprovalDelay"
1133
1281
    values_to_set = [datetime.timedelta(),
1134
1282
                     datetime.timedelta(minutes=5),
1135
1283
                     datetime.timedelta(seconds=1),
1137
1285
                     datetime.timedelta(weeks=52)]
1138
1286
    values_to_get = [0, 300000, 1000, 604800000, 31449600000]
1139
1287
 
1140
 
class TestSetApprovalDurationCmd(TestValueArgumentPropertyCmd):
 
1288
class TestSetApprovalDurationCmd(TestPropertyValueCmd):
1141
1289
    command = SetApprovalDurationCmd
1142
 
    property = "ApprovalDuration"
 
1290
    propname = "ApprovalDuration"
1143
1291
    values_to_set = [datetime.timedelta(),
1144
1292
                     datetime.timedelta(minutes=5),
1145
1293
                     datetime.timedelta(seconds=1),
1151
1299
    def setUp(self):
1152
1300
        self.parser = argparse.ArgumentParser()
1153
1301
        add_command_line_options(self.parser)
1154
 
    def assert_command_from_args(self, args, command_cls, **cmd_attrs):
 
1302
    def assert_command_from_args(self, args, command_cls,
 
1303
                                 **cmd_attrs):
1155
1304
        """Assert that parsing ARGS should result in an instance of
1156
1305
COMMAND_CLS with (optionally) all supplied attributes (CMD_ATTRS)."""
1157
1306
        options = self.parser.parse_args(args)
 
1307
        check_option_syntax(self.parser, options)
1158
1308
        commands = commands_from_options(options)
1159
1309
        self.assertEqual(len(commands), 1)
1160
1310
        command = commands[0]
1324
1474
    def test_is_enabled_short(self):
1325
1475
        self.assert_command_from_args(["-V", "foo"], IsEnabledCmd)
1326
1476
 
 
1477
    def test_deny_before_remove(self):
 
1478
        options = self.parser.parse_args(["--deny", "--remove",
 
1479
                                          "foo"])
 
1480
        check_option_syntax(self.parser, options)
 
1481
        commands = commands_from_options(options)
 
1482
        self.assertEqual(len(commands), 2)
 
1483
        self.assertIsInstance(commands[0], DenyCmd)
 
1484
        self.assertIsInstance(commands[1], RemoveCmd)
 
1485
 
 
1486
    def test_deny_before_remove_reversed(self):
 
1487
        options = self.parser.parse_args(["--remove", "--deny",
 
1488
                                          "--all"])
 
1489
        check_option_syntax(self.parser, options)
 
1490
        commands = commands_from_options(options)
 
1491
        self.assertEqual(len(commands), 2)
 
1492
        self.assertIsInstance(commands[0], DenyCmd)
 
1493
        self.assertIsInstance(commands[1], RemoveCmd)
 
1494
 
 
1495
 
 
1496
class Test_check_option_syntax(unittest.TestCase):
 
1497
    # This mostly corresponds to the definition from has_actions() in
 
1498
    # check_option_syntax()
 
1499
    actions = {
 
1500
        # The actual values set here are not that important, but we do
 
1501
        # at least stick to the correct types, even though they are
 
1502
        # never used
 
1503
        "enable": True,
 
1504
        "disable": True,
 
1505
        "bump_timeout": True,
 
1506
        "start_checker": True,
 
1507
        "stop_checker": True,
 
1508
        "is_enabled": True,
 
1509
        "remove": True,
 
1510
        "checker": "x",
 
1511
        "timeout": datetime.timedelta(),
 
1512
        "extended_timeout": datetime.timedelta(),
 
1513
        "interval": datetime.timedelta(),
 
1514
        "approved_by_default": True,
 
1515
        "approval_delay": datetime.timedelta(),
 
1516
        "approval_duration": datetime.timedelta(),
 
1517
        "host": "x",
 
1518
        "secret": io.BytesIO(b"x"),
 
1519
        "approve": True,
 
1520
        "deny": True,
 
1521
    }
 
1522
 
 
1523
    def setUp(self):
 
1524
        self.parser = argparse.ArgumentParser()
 
1525
        add_command_line_options(self.parser)
 
1526
 
 
1527
    @contextlib.contextmanager
 
1528
    def assertParseError(self):
 
1529
        with self.assertRaises(SystemExit) as e:
 
1530
            with self.temporarily_suppress_stderr():
 
1531
                yield
 
1532
        # Exit code from argparse is guaranteed to be "2".  Reference:
 
1533
        # https://docs.python.org/3/library
 
1534
        # /argparse.html#exiting-methods
 
1535
        self.assertEqual(e.exception.code, 2)
 
1536
 
 
1537
    @staticmethod
 
1538
    @contextlib.contextmanager
 
1539
    def temporarily_suppress_stderr():
 
1540
        null = os.open(os.path.devnull, os.O_RDWR)
 
1541
        stderrcopy = os.dup(sys.stderr.fileno())
 
1542
        os.dup2(null, sys.stderr.fileno())
 
1543
        os.close(null)
 
1544
        try:
 
1545
            yield
 
1546
        finally:
 
1547
            # restore stderr
 
1548
            os.dup2(stderrcopy, sys.stderr.fileno())
 
1549
            os.close(stderrcopy)
 
1550
 
 
1551
    def check_option_syntax(self, options):
 
1552
        check_option_syntax(self.parser, options)
 
1553
 
 
1554
    def test_actions_requires_client_or_all(self):
 
1555
        for action, value in self.actions.items():
 
1556
            options = self.parser.parse_args()
 
1557
            setattr(options, action, value)
 
1558
            with self.assertParseError():
 
1559
                self.check_option_syntax(options)
 
1560
 
 
1561
    def test_actions_conflicts_with_verbose(self):
 
1562
        for action, value in self.actions.items():
 
1563
            options = self.parser.parse_args()
 
1564
            setattr(options, action, value)
 
1565
            options.verbose = True
 
1566
            with self.assertParseError():
 
1567
                self.check_option_syntax(options)
 
1568
 
 
1569
    def test_dump_json_conflicts_with_verbose(self):
 
1570
        options = self.parser.parse_args()
 
1571
        options.dump_json = True
 
1572
        options.verbose = True
 
1573
        with self.assertParseError():
 
1574
            self.check_option_syntax(options)
 
1575
 
 
1576
    def test_dump_json_conflicts_with_action(self):
 
1577
        for action, value in self.actions.items():
 
1578
            options = self.parser.parse_args()
 
1579
            setattr(options, action, value)
 
1580
            options.dump_json = True
 
1581
            with self.assertParseError():
 
1582
                self.check_option_syntax(options)
 
1583
 
 
1584
    def test_all_can_not_be_alone(self):
 
1585
        options = self.parser.parse_args()
 
1586
        options.all = True
 
1587
        with self.assertParseError():
 
1588
            self.check_option_syntax(options)
 
1589
 
 
1590
    def test_all_is_ok_with_any_action(self):
 
1591
        for action, value in self.actions.items():
 
1592
            options = self.parser.parse_args()
 
1593
            setattr(options, action, value)
 
1594
            options.all = True
 
1595
            self.check_option_syntax(options)
 
1596
 
 
1597
    def test_is_enabled_fails_without_client(self):
 
1598
        options = self.parser.parse_args()
 
1599
        options.is_enabled = True
 
1600
        with self.assertParseError():
 
1601
            self.check_option_syntax(options)
 
1602
 
 
1603
    def test_is_enabled_works_with_one_client(self):
 
1604
        options = self.parser.parse_args()
 
1605
        options.is_enabled = True
 
1606
        options.client = ["foo"]
 
1607
        self.check_option_syntax(options)
 
1608
 
 
1609
    def test_is_enabled_fails_with_two_clients(self):
 
1610
        options = self.parser.parse_args()
 
1611
        options.is_enabled = True
 
1612
        options.client = ["foo", "barbar"]
 
1613
        with self.assertParseError():
 
1614
            self.check_option_syntax(options)
 
1615
 
 
1616
    def test_remove_can_only_be_combined_with_action_deny(self):
 
1617
        for action, value in self.actions.items():
 
1618
            if action in {"remove", "deny"}:
 
1619
                continue
 
1620
            options = self.parser.parse_args()
 
1621
            setattr(options, action, value)
 
1622
            options.all = True
 
1623
            options.remove = True
 
1624
            with self.assertParseError():
 
1625
                self.check_option_syntax(options)
 
1626
 
1327
1627
 
1328
1628
 
1329
1629
def should_only_run_tests():