/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-08 23:55:34 UTC
  • Revision ID: teddy@recompile.se-20190308235534-kkiwc3oeyp0vxw5h
mandos-ctl: Add new --debug option to show D-Bus calls

* mandos-ctl (PropertyCmd, RemoveCmd, ApproveCmd, DenyCmd): Add debug
  output
  (add_command_line_options): Also add "--debug" options.
  (main): If debug option is set, set logging level to DEBUG.  Also
          add debug output when connecting to Mandos server.
* mandos-ctl.xml (SYNOPSIS): Show --debug option where appropriate.
  (OPTIONS): Document new "--debug" option.

Show diffs side-by-side

added added

removed removed

Lines of Context:
42
42
import json
43
43
import unittest
44
44
import logging
 
45
import io
 
46
import tempfile
 
47
import contextlib
45
48
 
46
49
import dbus
47
50
 
268
271
    return value
269
272
 
270
273
 
271
 
class TableOfClients(object):
272
 
    tableheaders = {
273
 
        "Name": "Name",
274
 
        "Enabled": "Enabled",
275
 
        "Timeout": "Timeout",
276
 
        "LastCheckedOK": "Last Successful Check",
277
 
        "LastApprovalRequest": "Last Approval Request",
278
 
        "Created": "Created",
279
 
        "Interval": "Interval",
280
 
        "Host": "Host",
281
 
        "Fingerprint": "Fingerprint",
282
 
        "KeyID": "Key ID",
283
 
        "CheckerRunning": "Check Is Running",
284
 
        "LastEnabled": "Last Enabled",
285
 
        "ApprovalPending": "Approval Is Pending",
286
 
        "ApprovedByDefault": "Approved By Default",
287
 
        "ApprovalDelay": "Approval Delay",
288
 
        "ApprovalDuration": "Approval Duration",
289
 
        "Checker": "Checker",
290
 
        "ExtendedTimeout": "Extended Timeout",
291
 
        "Expires": "Expires",
292
 
        "LastCheckerStatus": "Last Checker Status",
293
 
    }
294
 
 
295
 
    def __init__(self, clients, keywords, tableheaders=None):
296
 
        self.clients = clients
297
 
        self.keywords = keywords
298
 
        if tableheaders is not None:
299
 
            self.tableheaders = tableheaders
300
 
 
301
 
    def __str__(self):
302
 
        return "\n".join(self.rows())
303
 
 
304
 
    if sys.version_info.major == 2:
305
 
        __unicode__ = __str__
306
 
        def __str__(self):
307
 
            return str(self).encode(locale.getpreferredencoding())
308
 
 
309
 
    def rows(self):
310
 
        format_string = self.row_formatting_string()
311
 
        rows = [self.header_line(format_string)]
312
 
        rows.extend(self.client_line(client, format_string)
313
 
                    for client in self.clients)
314
 
        return rows
315
 
 
316
 
    def row_formatting_string(self):
317
 
        "Format string used to format table rows"
318
 
        return " ".join("{{{key}:{width}}}".format(
319
 
            width=max(len(self.tableheaders[key]),
320
 
                      *(len(self.string_from_client(client, key))
321
 
                        for client in self.clients)),
322
 
            key=key)
323
 
                        for key in self.keywords)
324
 
 
325
 
    def string_from_client(self, client, key):
326
 
        return self.valuetostring(client[key], key)
327
 
 
328
 
    @staticmethod
329
 
    def valuetostring(value, keyword):
330
 
        if isinstance(value, dbus.Boolean):
331
 
            return "Yes" if value else "No"
332
 
        if keyword in ("Timeout", "Interval", "ApprovalDelay",
333
 
                       "ApprovalDuration", "ExtendedTimeout"):
334
 
            return milliseconds_to_string(value)
335
 
        return str(value)
336
 
 
337
 
    def header_line(self, format_string):
338
 
        return format_string.format(**self.tableheaders)
339
 
 
340
 
    def client_line(self, client, format_string):
341
 
        return format_string.format(
342
 
            **{key: self.string_from_client(client, key)
343
 
               for key in self.keywords})
344
 
 
345
 
 
346
274
## Classes for commands.
347
275
 
348
276
# Abstract classes first
349
277
class Command(object):
350
278
    """Abstract class for commands"""
351
 
    def run(self, clients):
 
279
    def run(self, mandos, clients):
352
280
        """Normal commands should implement run_on_one_client(), but
353
281
        commands which want to operate on all clients at the same time
354
282
        can override this run() method instead."""
355
 
        for client in clients:
356
 
            self.run_on_one_client(client)
 
283
        self.mandos = mandos
 
284
        for client, properties in clients.items():
 
285
            self.run_on_one_client(client, properties)
357
286
 
358
287
class PrintCmd(Command):
359
288
    """Abstract class for commands printing client details"""
364
293
                    "LastApprovalRequest", "ApprovalDelay",
365
294
                    "ApprovalDuration", "Checker", "ExtendedTimeout",
366
295
                    "Expires", "LastCheckerStatus")
367
 
    def run(self, clients):
 
296
    def run(self, mandos, clients):
368
297
        print(self.output(clients))
369
298
 
370
299
class PropertyCmd(Command):
371
300
    """Abstract class for Actions for setting one client property"""
372
 
    def run_on_one_client(self, client):
 
301
    def run_on_one_client(self, client, properties):
373
302
        """Set the Client's D-Bus property"""
 
303
        log.debug("D-Bus: %s:%s:%s.Set(%r, %r, %r)", busname,
 
304
                  client.__dbus_object_path__,
 
305
                  dbus.PROPERTIES_IFACE, client_interface,
 
306
                  self.property, self.value_to_set
 
307
                  if not isinstance(self.value_to_set, dbus.Boolean)
 
308
                  else bool(self.value_to_set))
374
309
        client.Set(client_interface, self.property, self.value_to_set,
375
310
                   dbus_interface=dbus.PROPERTIES_IFACE)
376
311
 
388
323
    @value_to_set.setter
389
324
    def value_to_set(self, value):
390
325
        """When setting, convert value to a datetime.timedelta"""
391
 
        self._vts = string_to_delta(value).total_seconds() * 1000
 
326
        self._vts = int(round(value.total_seconds() * 1000))
392
327
 
393
328
# Actual (non-abstract) command classes
394
329
 
395
330
class PrintTableCmd(PrintCmd):
396
331
    def __init__(self, verbose=False):
397
332
        self.verbose = verbose
 
333
 
398
334
    def output(self, clients):
 
335
        default_keywords = ("Name", "Enabled", "Timeout", "LastCheckedOK")
 
336
        keywords = default_keywords
399
337
        if self.verbose:
400
338
            keywords = self.all_keywords
401
 
        else:
402
 
            keywords = ("Name", "Enabled", "Timeout", "LastCheckedOK")
403
 
        return str(TableOfClients(clients.values(), keywords))
 
339
        return str(self.TableOfClients(clients.values(), keywords))
 
340
 
 
341
    class TableOfClients(object):
 
342
        tableheaders = {
 
343
            "Name": "Name",
 
344
            "Enabled": "Enabled",
 
345
            "Timeout": "Timeout",
 
346
            "LastCheckedOK": "Last Successful Check",
 
347
            "LastApprovalRequest": "Last Approval Request",
 
348
            "Created": "Created",
 
349
            "Interval": "Interval",
 
350
            "Host": "Host",
 
351
            "Fingerprint": "Fingerprint",
 
352
            "KeyID": "Key ID",
 
353
            "CheckerRunning": "Check Is Running",
 
354
            "LastEnabled": "Last Enabled",
 
355
            "ApprovalPending": "Approval Is Pending",
 
356
            "ApprovedByDefault": "Approved By Default",
 
357
            "ApprovalDelay": "Approval Delay",
 
358
            "ApprovalDuration": "Approval Duration",
 
359
            "Checker": "Checker",
 
360
            "ExtendedTimeout": "Extended Timeout",
 
361
            "Expires": "Expires",
 
362
            "LastCheckerStatus": "Last Checker Status",
 
363
        }
 
364
 
 
365
        def __init__(self, clients, keywords, tableheaders=None):
 
366
            self.clients = clients
 
367
            self.keywords = keywords
 
368
            if tableheaders is not None:
 
369
                self.tableheaders = tableheaders
 
370
 
 
371
        def __str__(self):
 
372
            return "\n".join(self.rows())
 
373
 
 
374
        if sys.version_info.major == 2:
 
375
            __unicode__ = __str__
 
376
            def __str__(self):
 
377
                return str(self).encode(locale.getpreferredencoding())
 
378
 
 
379
        def rows(self):
 
380
            format_string = self.row_formatting_string()
 
381
            rows = [self.header_line(format_string)]
 
382
            rows.extend(self.client_line(client, format_string)
 
383
                        for client in self.clients)
 
384
            return rows
 
385
 
 
386
        def row_formatting_string(self):
 
387
            "Format string used to format table rows"
 
388
            return " ".join("{{{key}:{width}}}".format(
 
389
                width=max(len(self.tableheaders[key]),
 
390
                          *(len(self.string_from_client(client, key))
 
391
                            for client in self.clients)),
 
392
                key=key)
 
393
                            for key in self.keywords)
 
394
 
 
395
        def string_from_client(self, client, key):
 
396
            return self.valuetostring(client[key], key)
 
397
 
 
398
        @staticmethod
 
399
        def valuetostring(value, keyword):
 
400
            if isinstance(value, dbus.Boolean):
 
401
                return "Yes" if value else "No"
 
402
            if keyword in ("Timeout", "Interval", "ApprovalDelay",
 
403
                           "ApprovalDuration", "ExtendedTimeout"):
 
404
                return milliseconds_to_string(value)
 
405
            return str(value)
 
406
 
 
407
        def header_line(self, format_string):
 
408
            return format_string.format(**self.tableheaders)
 
409
 
 
410
        def client_line(self, client, format_string):
 
411
            return format_string.format(
 
412
                **{key: self.string_from_client(client, key)
 
413
                   for key in self.keywords})
 
414
 
 
415
 
404
416
 
405
417
class DumpJSONCmd(PrintCmd):
406
418
    def output(self, clients):
416
428
        return value
417
429
 
418
430
class IsEnabledCmd(Command):
419
 
    def run_on_one_client(self, client):
420
 
        if self.is_enabled(client):
 
431
    def run_on_one_client(self, client, properties):
 
432
        if self.is_enabled(client, properties):
421
433
            sys.exit(0)
422
434
        sys.exit(1)
423
 
    def is_enabled(self, client):
424
 
        return client.Get(client_interface, "Enabled",
425
 
                          dbus_interface=dbus.PROPERTIES_IFACE)
 
435
    def is_enabled(self, client, properties):
 
436
        return bool(properties["Enabled"])
426
437
 
427
438
class RemoveCmd(Command):
428
 
    def __init__(self, mandos):
429
 
        self.mandos = mandos
430
 
    def run_on_one_client(self, client):
 
439
    def run_on_one_client(self, client, properties):
 
440
        log.debug("D-Bus: %s:%s:%s.RemoveClient(%r)", busname,
 
441
                  server_path, server_interface,
 
442
                  str(client.__dbus_object_path__))
431
443
        self.mandos.RemoveClient(client.__dbus_object_path__)
432
444
 
433
445
class ApproveCmd(Command):
434
 
    def run_on_one_client(self, client):
 
446
    def run_on_one_client(self, client, properties):
 
447
        log.debug("D-Bus: %s:%s.Approve(True)",
 
448
                  client.__dbus_object_path__, client_interface)
435
449
        client.Approve(dbus.Boolean(True),
436
450
                       dbus_interface=client_interface)
437
451
 
438
452
class DenyCmd(Command):
439
 
    def run_on_one_client(self, client):
 
453
    def run_on_one_client(self, client, properties):
 
454
        log.debug("D-Bus: %s:%s.Approve(False)",
 
455
                  client.__dbus_object_path__, client_interface)
440
456
        client.Approve(dbus.Boolean(False),
441
457
                       dbus_interface=client_interface)
442
458
 
475
491
    property = "Host"
476
492
 
477
493
class SetSecretCmd(PropertyCmd, ValueArgumentMixIn):
 
494
    @property
 
495
    def value_to_set(self):
 
496
        return self._vts
 
497
    @value_to_set.setter
 
498
    def value_to_set(self, value):
 
499
        """When setting, read data from supplied file object"""
 
500
        self._vts = value.read()
 
501
        value.close()
478
502
    property = "Secret"
479
503
 
480
504
class SetTimeoutCmd(PropertyCmd, MillisecondsValueArgumentMixIn):
495
519
                             MillisecondsValueArgumentMixIn):
496
520
    property = "ApprovalDuration"
497
521
 
498
 
def has_actions(options):
499
 
    return any((options.enable,
500
 
                options.disable,
501
 
                options.bump_timeout,
502
 
                options.start_checker,
503
 
                options.stop_checker,
504
 
                options.is_enabled,
505
 
                options.remove,
506
 
                options.checker is not None,
507
 
                options.timeout is not None,
508
 
                options.extended_timeout is not None,
509
 
                options.interval is not None,
510
 
                options.approved_by_default is not None,
511
 
                options.approval_delay is not None,
512
 
                options.approval_duration is not None,
513
 
                options.host is not None,
514
 
                options.secret is not None,
515
 
                options.approve,
516
 
                options.deny))
517
 
 
518
 
 
519
 
def main():
520
 
    parser = argparse.ArgumentParser()
 
522
def add_command_line_options(parser):
521
523
    parser.add_argument("--version", action="version",
522
524
                        version="%(prog)s {}".format(version),
523
525
                        help="show version number and exit")
548
550
                        help="Remove client")
549
551
    parser.add_argument("-c", "--checker",
550
552
                        help="Set checker command for client")
551
 
    parser.add_argument("-t", "--timeout",
 
553
    parser.add_argument("-t", "--timeout", type=string_to_delta,
552
554
                        help="Set timeout for client")
553
 
    parser.add_argument("--extended-timeout",
 
555
    parser.add_argument("--extended-timeout", type=string_to_delta,
554
556
                        help="Set extended timeout for client")
555
 
    parser.add_argument("-i", "--interval",
 
557
    parser.add_argument("-i", "--interval", type=string_to_delta,
556
558
                        help="Set checker interval for client")
557
559
    approve_deny_default = parser.add_mutually_exclusive_group()
558
560
    approve_deny_default.add_argument(
563
565
        "--deny-by-default", action="store_false",
564
566
        dest="approved_by_default",
565
567
        help="Set client to be denied by default")
566
 
    parser.add_argument("--approval-delay",
 
568
    parser.add_argument("--approval-delay", type=string_to_delta,
567
569
                        help="Set delay before client approve/deny")
568
 
    parser.add_argument("--approval-duration",
 
570
    parser.add_argument("--approval-duration", type=string_to_delta,
569
571
                        help="Set duration of one client approval")
570
572
    parser.add_argument("-H", "--host", help="Set host for client")
571
573
    parser.add_argument("-s", "--secret",
577
579
        help="Approve any current client request")
578
580
    approve_deny.add_argument("-D", "--deny", action="store_true",
579
581
                              help="Deny any current client request")
 
582
    parser.add_argument("--debug", action="store_true",
 
583
                        help="Debug mode (show D-Bus commands)")
580
584
    parser.add_argument("--check", action="store_true",
581
585
                        help="Run self-test")
582
586
    parser.add_argument("client", nargs="*", help="Client name")
583
 
    options = parser.parse_args()
584
 
 
585
 
    if has_actions(options) and not (options.client or options.all):
586
 
        parser.error("Options require clients names or --all.")
587
 
    if options.verbose and has_actions(options):
588
 
        parser.error("--verbose can only be used alone.")
589
 
    if options.dump_json and (options.verbose
590
 
                              or has_actions(options)):
591
 
        parser.error("--dump-json can only be used alone.")
592
 
    if options.all and not has_actions(options):
593
 
        parser.error("--all requires an action.")
594
 
    if options.is_enabled and len(options.client) > 1:
595
 
            parser.error("--is-enabled requires exactly one client")
596
 
 
597
 
    try:
598
 
        bus = dbus.SystemBus()
599
 
        mandos_dbus_objc = bus.get_object(busname, server_path)
600
 
    except dbus.exceptions.DBusException:
601
 
        log.critical("Could not connect to Mandos server")
602
 
        sys.exit(1)
603
 
 
604
 
    mandos_serv = dbus.Interface(mandos_dbus_objc,
605
 
                                 dbus_interface=server_interface)
606
 
    mandos_serv_object_manager = dbus.Interface(
607
 
        mandos_dbus_objc, dbus_interface=dbus.OBJECT_MANAGER_IFACE)
 
587
 
 
588
 
 
589
def commands_from_options(options):
608
590
 
609
591
    commands = []
610
592
 
618
600
        commands.append(DisableCmd())
619
601
 
620
602
    if options.bump_timeout:
621
 
        commands.append(BumpTimeoutCmd(options.bump_timeout))
 
603
        commands.append(BumpTimeoutCmd())
622
604
 
623
605
    if options.start_checker:
624
606
        commands.append(StartCheckerCmd())
630
612
        commands.append(IsEnabledCmd())
631
613
 
632
614
    if options.remove:
633
 
        commands.append(RemoveCmd(mandos_serv))
 
615
        commands.append(RemoveCmd())
634
616
 
635
617
    if options.checker is not None:
636
 
        commands.append(SetCheckerCmd())
 
618
        commands.append(SetCheckerCmd(options.checker))
637
619
 
638
620
    if options.timeout is not None:
639
621
        commands.append(SetTimeoutCmd(options.timeout))
643
625
            SetExtendedTimeoutCmd(options.extended_timeout))
644
626
 
645
627
    if options.interval is not None:
646
 
        command.append(SetIntervalCmd(options.interval))
 
628
        commands.append(SetIntervalCmd(options.interval))
647
629
 
648
630
    if options.approved_by_default is not None:
649
631
        if options.approved_by_default:
650
 
            command.append(ApproveByDefaultCmd())
 
632
            commands.append(ApproveByDefaultCmd())
651
633
        else:
652
 
            command.append(DenyByDefaultCmd())
 
634
            commands.append(DenyByDefaultCmd())
653
635
 
654
636
    if options.approval_delay is not None:
655
 
        command.append(SetApprovalDelayCmd(options.approval_delay))
 
637
        commands.append(SetApprovalDelayCmd(options.approval_delay))
656
638
 
657
639
    if options.approval_duration is not None:
658
 
        command.append(
 
640
        commands.append(
659
641
            SetApprovalDurationCmd(options.approval_duration))
660
642
 
661
643
    if options.host is not None:
662
 
        command.append(SetHostCmd(options.host))
 
644
        commands.append(SetHostCmd(options.host))
663
645
 
664
646
    if options.secret is not None:
665
 
        command.append(SetSecretCmd(options.secret))
 
647
        commands.append(SetSecretCmd(options.secret))
666
648
 
667
649
    if options.approve:
668
650
        commands.append(ApproveCmd())
675
657
    if not commands:
676
658
        commands.append(PrintTableCmd(verbose=options.verbose))
677
659
 
 
660
    return commands
 
661
 
 
662
 
 
663
def check_option_syntax(parser, options):
 
664
    """Apply additional restrictions on options, not expressible in
 
665
argparse"""
 
666
 
 
667
    def has_actions(options):
 
668
        return any((options.enable,
 
669
                    options.disable,
 
670
                    options.bump_timeout,
 
671
                    options.start_checker,
 
672
                    options.stop_checker,
 
673
                    options.is_enabled,
 
674
                    options.remove,
 
675
                    options.checker is not None,
 
676
                    options.timeout is not None,
 
677
                    options.extended_timeout is not None,
 
678
                    options.interval is not None,
 
679
                    options.approved_by_default is not None,
 
680
                    options.approval_delay is not None,
 
681
                    options.approval_duration is not None,
 
682
                    options.host is not None,
 
683
                    options.secret is not None,
 
684
                    options.approve,
 
685
                    options.deny))
 
686
 
 
687
    if has_actions(options) and not (options.client or options.all):
 
688
        parser.error("Options require clients names or --all.")
 
689
    if options.verbose and has_actions(options):
 
690
        parser.error("--verbose can only be used alone.")
 
691
    if options.dump_json and (options.verbose
 
692
                              or has_actions(options)):
 
693
        parser.error("--dump-json can only be used alone.")
 
694
    if options.all and not has_actions(options):
 
695
        parser.error("--all requires an action.")
 
696
    if options.is_enabled and len(options.client) > 1:
 
697
        parser.error("--is-enabled requires exactly one client")
 
698
 
 
699
 
 
700
def main():
 
701
    parser = argparse.ArgumentParser()
 
702
 
 
703
    add_command_line_options(parser)
 
704
 
 
705
    options = parser.parse_args()
 
706
 
 
707
    check_option_syntax(parser, options)
 
708
 
 
709
    clientnames = options.client
 
710
 
 
711
    if options.debug:
 
712
        log.setLevel(logging.DEBUG)
 
713
 
 
714
    try:
 
715
        bus = dbus.SystemBus()
 
716
        log.debug("D-Bus: Connect to: (name=%r, path=%r)", busname,
 
717
                  server_path)
 
718
        mandos_dbus_objc = bus.get_object(busname, server_path)
 
719
    except dbus.exceptions.DBusException:
 
720
        log.critical("Could not connect to Mandos server")
 
721
        sys.exit(1)
 
722
 
 
723
    mandos_serv = dbus.Interface(mandos_dbus_objc,
 
724
                                 dbus_interface=server_interface)
 
725
    mandos_serv_object_manager = dbus.Interface(
 
726
        mandos_dbus_objc, dbus_interface=dbus.OBJECT_MANAGER_IFACE)
 
727
 
678
728
    # Filter out log message from dbus module
679
729
    dbus_logger = logging.getLogger("dbus.proxies")
680
730
    class NullFilter(logging.Filter):
681
731
        def filter(self, record):
682
732
            return False
683
733
    dbus_filter = NullFilter()
684
 
    dbus_logger.addFilter(dbus_filter)
685
734
    try:
686
 
        try:
687
 
            mandos_clients = {path: ifs_and_props[client_interface]
688
 
                              for path, ifs_and_props in
689
 
                              mandos_serv_object_manager
690
 
                              .GetManagedObjects().items()
691
 
                              if client_interface in ifs_and_props}
692
 
        finally:
693
 
            # restore dbus logger
694
 
            dbus_logger.removeFilter(dbus_filter)
 
735
        dbus_logger.addFilter(dbus_filter)
 
736
        log.debug("D-Bus: %s:%s:%s.GetManagedObjects()", busname,
 
737
                  server_path, dbus.OBJECT_MANAGER_IFACE)
 
738
        mandos_clients = {path: ifs_and_props[client_interface]
 
739
                          for path, ifs_and_props in
 
740
                          mandos_serv_object_manager
 
741
                          .GetManagedObjects().items()
 
742
                          if client_interface in ifs_and_props}
695
743
    except dbus.exceptions.DBusException as e:
696
744
        log.critical("Failed to access Mandos server through D-Bus:"
697
745
                     "\n%s", e)
698
746
        sys.exit(1)
 
747
    finally:
 
748
        # restore dbus logger
 
749
        dbus_logger.removeFilter(dbus_filter)
699
750
 
700
751
    # Compile dict of (clients: properties) to process
701
752
    clients = {}
702
753
 
703
 
    if options.all or not options.client:
 
754
    if not clientnames:
704
755
        clients = {bus.get_object(busname, path): properties
705
756
                   for path, properties in mandos_clients.items()}
706
757
    else:
707
 
        for name in options.client:
 
758
        for name in clientnames:
708
759
            for path, client in mandos_clients.items():
709
760
                if client["Name"] == name:
710
761
                    client_objc = bus.get_object(busname, path)
715
766
                sys.exit(1)
716
767
 
717
768
    # Run all commands on clients
 
769
    commands = commands_from_options(options)
718
770
    for command in commands:
719
 
        command.run(clients)
 
771
        command.run(mandos_serv, clients)
720
772
 
721
773
 
722
774
class Test_milliseconds_to_string(unittest.TestCase):
734
786
 
735
787
class Test_string_to_delta(unittest.TestCase):
736
788
    def test_handles_basic_rfc3339(self):
 
789
        self.assertEqual(string_to_delta("PT0S"),
 
790
                         datetime.timedelta())
 
791
        self.assertEqual(string_to_delta("P0D"),
 
792
                         datetime.timedelta())
 
793
        self.assertEqual(string_to_delta("PT1S"),
 
794
                         datetime.timedelta(0, 1))
737
795
        self.assertEqual(string_to_delta("PT2H"),
738
796
                         datetime.timedelta(0, 7200))
739
797
    def test_falls_back_to_pre_1_6_1_with_warning(self):
742
800
            with self.assertLogs(log, logging.WARNING):
743
801
                value = string_to_delta("2h")
744
802
        else:
745
 
            value = string_to_delta("2h")
 
803
            class WarningFilter(logging.Filter):
 
804
                """Don't show, but record the presence of, warnings"""
 
805
                def filter(self, record):
 
806
                    is_warning = record.levelno >= logging.WARNING
 
807
                    self.found = is_warning or getattr(self, "found",
 
808
                                                       False)
 
809
                    return not is_warning
 
810
            warning_filter = WarningFilter()
 
811
            log.addFilter(warning_filter)
 
812
            try:
 
813
                value = string_to_delta("2h")
 
814
            finally:
 
815
                log.removeFilter(warning_filter)
 
816
            self.assertTrue(getattr(warning_filter, "found", False))
746
817
        self.assertEqual(value, datetime.timedelta(0, 7200))
747
818
 
748
 
class Test_TableOfClients(unittest.TestCase):
749
 
    def setUp(self):
750
 
        self.tableheaders = {
751
 
            "Attr1": "X",
752
 
            "AttrTwo": "Yy",
753
 
            "AttrThree": "Zzz",
754
 
            "Bool": "A D-BUS Boolean",
755
 
            "NonDbusBoolean": "A Non-D-BUS Boolean",
756
 
            "Integer": "An Integer",
757
 
            "Timeout": "Timedelta 1",
758
 
            "Interval": "Timedelta 2",
759
 
            "ApprovalDelay": "Timedelta 3",
760
 
            "ApprovalDuration": "Timedelta 4",
761
 
            "ExtendedTimeout": "Timedelta 5",
762
 
            "String": "A String",
 
819
 
 
820
class TestCmd(unittest.TestCase):
 
821
    """Abstract class for tests of command classes"""
 
822
    def setUp(self):
 
823
        testcase = self
 
824
        class MockClient(object):
 
825
            def __init__(self, name, **attributes):
 
826
                self.__dbus_object_path__ = "objpath_{}".format(name)
 
827
                self.attributes = attributes
 
828
                self.attributes["Name"] = name
 
829
                self.calls = []
 
830
            def Set(self, interface, property, value, dbus_interface):
 
831
                testcase.assertEqual(interface, client_interface)
 
832
                testcase.assertEqual(dbus_interface,
 
833
                                     dbus.PROPERTIES_IFACE)
 
834
                self.attributes[property] = value
 
835
            def Get(self, interface, property, dbus_interface):
 
836
                testcase.assertEqual(interface, client_interface)
 
837
                testcase.assertEqual(dbus_interface,
 
838
                                     dbus.PROPERTIES_IFACE)
 
839
                return self.attributes[property]
 
840
            def Approve(self, approve, dbus_interface):
 
841
                testcase.assertEqual(dbus_interface, client_interface)
 
842
                self.calls.append(("Approve", (approve,
 
843
                                               dbus_interface)))
 
844
        self.client = MockClient(
 
845
            "foo",
 
846
            KeyID=("92ed150794387c03ce684574b1139a65"
 
847
                   "94a34f895daaaf09fd8ea90a27cddb12"),
 
848
            Secret=b"secret",
 
849
            Host="foo.example.org",
 
850
            Enabled=dbus.Boolean(True),
 
851
            Timeout=300000,
 
852
            LastCheckedOK="2019-02-03T00:00:00",
 
853
            Created="2019-01-02T00:00:00",
 
854
            Interval=120000,
 
855
            Fingerprint=("778827225BA7DE539C5A"
 
856
                         "7CFA59CFF7CDBD9A5920"),
 
857
            CheckerRunning=dbus.Boolean(False),
 
858
            LastEnabled="2019-01-03T00:00:00",
 
859
            ApprovalPending=dbus.Boolean(False),
 
860
            ApprovedByDefault=dbus.Boolean(True),
 
861
            LastApprovalRequest="",
 
862
            ApprovalDelay=0,
 
863
            ApprovalDuration=1000,
 
864
            Checker="fping -q -- %(host)s",
 
865
            ExtendedTimeout=900000,
 
866
            Expires="2019-02-04T00:00:00",
 
867
            LastCheckerStatus=0)
 
868
        self.other_client = MockClient(
 
869
            "barbar",
 
870
            KeyID=("0558568eedd67d622f5c83b35a115f79"
 
871
                   "6ab612cff5ad227247e46c2b020f441c"),
 
872
            Secret=b"secretbar",
 
873
            Host="192.0.2.3",
 
874
            Enabled=dbus.Boolean(True),
 
875
            Timeout=300000,
 
876
            LastCheckedOK="2019-02-04T00:00:00",
 
877
            Created="2019-01-03T00:00:00",
 
878
            Interval=120000,
 
879
            Fingerprint=("3E393AEAEFB84C7E89E2"
 
880
                         "F547B3A107558FCA3A27"),
 
881
            CheckerRunning=dbus.Boolean(True),
 
882
            LastEnabled="2019-01-04T00:00:00",
 
883
            ApprovalPending=dbus.Boolean(False),
 
884
            ApprovedByDefault=dbus.Boolean(False),
 
885
            LastApprovalRequest="2019-01-03T00:00:00",
 
886
            ApprovalDelay=30000,
 
887
            ApprovalDuration=1000,
 
888
            Checker=":",
 
889
            ExtendedTimeout=900000,
 
890
            Expires="2019-02-05T00:00:00",
 
891
            LastCheckerStatus=-2)
 
892
        self.clients =  collections.OrderedDict(
 
893
            [
 
894
                (self.client, self.client.attributes),
 
895
                (self.other_client, self.other_client.attributes),
 
896
            ])
 
897
        self.one_client = {self.client: self.client.attributes}
 
898
 
 
899
class TestPrintTableCmd(TestCmd):
 
900
    def test_normal(self):
 
901
        output = PrintTableCmd().output(self.clients)
 
902
        expected_output = """
 
903
Name   Enabled Timeout  Last Successful Check
 
904
foo    Yes     00:05:00 2019-02-03T00:00:00  
 
905
barbar Yes     00:05:00 2019-02-04T00:00:00  
 
906
"""[1:-1]
 
907
        self.assertEqual(output, expected_output)
 
908
    def test_verbose(self):
 
909
        output = PrintTableCmd(verbose=True).output(self.clients)
 
910
        expected_output = """
 
911
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
 
912
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                  
 
913
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                 
 
914
"""[1:-1]
 
915
        self.assertEqual(output, expected_output)
 
916
    def test_one_client(self):
 
917
        output = PrintTableCmd().output(self.one_client)
 
918
        expected_output = """
 
919
Name Enabled Timeout  Last Successful Check
 
920
foo  Yes     00:05:00 2019-02-03T00:00:00  
 
921
"""[1:-1]
 
922
        self.assertEqual(output, expected_output)
 
923
 
 
924
class TestDumpJSONCmd(TestCmd):
 
925
    def setUp(self):
 
926
        self.expected_json = {
 
927
            "foo": {
 
928
                "Name": "foo",
 
929
                "KeyID": ("92ed150794387c03ce684574b1139a65"
 
930
                          "94a34f895daaaf09fd8ea90a27cddb12"),
 
931
                "Host": "foo.example.org",
 
932
                "Enabled": True,
 
933
                "Timeout": 300000,
 
934
                "LastCheckedOK": "2019-02-03T00:00:00",
 
935
                "Created": "2019-01-02T00:00:00",
 
936
                "Interval": 120000,
 
937
                "Fingerprint": ("778827225BA7DE539C5A"
 
938
                                "7CFA59CFF7CDBD9A5920"),
 
939
                "CheckerRunning": False,
 
940
                "LastEnabled": "2019-01-03T00:00:00",
 
941
                "ApprovalPending": False,
 
942
                "ApprovedByDefault": True,
 
943
                "LastApprovalRequest": "",
 
944
                "ApprovalDelay": 0,
 
945
                "ApprovalDuration": 1000,
 
946
                "Checker": "fping -q -- %(host)s",
 
947
                "ExtendedTimeout": 900000,
 
948
                "Expires": "2019-02-04T00:00:00",
 
949
                "LastCheckerStatus": 0,
 
950
            },
 
951
            "barbar": {
 
952
                "Name": "barbar",
 
953
                "KeyID": ("0558568eedd67d622f5c83b35a115f79"
 
954
                          "6ab612cff5ad227247e46c2b020f441c"),
 
955
                "Host": "192.0.2.3",
 
956
                "Enabled": True,
 
957
                "Timeout": 300000,
 
958
                "LastCheckedOK": "2019-02-04T00:00:00",
 
959
                "Created": "2019-01-03T00:00:00",
 
960
                "Interval": 120000,
 
961
                "Fingerprint": ("3E393AEAEFB84C7E89E2"
 
962
                                "F547B3A107558FCA3A27"),
 
963
                "CheckerRunning": True,
 
964
                "LastEnabled": "2019-01-04T00:00:00",
 
965
                "ApprovalPending": False,
 
966
                "ApprovedByDefault": False,
 
967
                "LastApprovalRequest": "2019-01-03T00:00:00",
 
968
                "ApprovalDelay": 30000,
 
969
                "ApprovalDuration": 1000,
 
970
                "Checker": ":",
 
971
                "ExtendedTimeout": 900000,
 
972
                "Expires": "2019-02-05T00:00:00",
 
973
                "LastCheckerStatus": -2,
 
974
            },
763
975
        }
764
 
        self.keywords = ["Attr1", "AttrTwo"]
765
 
        self.clients = [
766
 
            {
767
 
                "Attr1": "x1",
768
 
                "AttrTwo": "y1",
769
 
                "AttrThree": "z1",
770
 
                "Bool": dbus.Boolean(False),
771
 
                "NonDbusBoolean": False,
772
 
                "Integer": 0,
773
 
                "Timeout": 0,
774
 
                "Interval": 1000,
775
 
                "ApprovalDelay": 2000,
776
 
                "ApprovalDuration": 3000,
777
 
                "ExtendedTimeout": 4000,
778
 
                "String": "",
779
 
            },
780
 
            {
781
 
                "Attr1": "x2",
782
 
                "AttrTwo": "y2",
783
 
                "AttrThree": "z2",
784
 
                "Bool": dbus.Boolean(True),
785
 
                "NonDbusBoolean": True,
786
 
                "Integer": 1,
787
 
                "Timeout": 93785000,
788
 
                "Interval": 93786000,
789
 
                "ApprovalDelay": 93787000,
790
 
                "ApprovalDuration": 93788000,
791
 
                "ExtendedTimeout": 93789000,
792
 
                "String": "A huge string which will not fit," * 10,
793
 
            },
794
 
        ]
795
 
    def test_short_header(self):
796
 
        text = str(TableOfClients(self.clients, self.keywords,
797
 
                                  self.tableheaders))
798
 
        expected_text = """
799
 
X  Yy
800
 
x1 y1
801
 
x2 y2
802
 
"""[1:-1]
803
 
        self.assertEqual(text, expected_text)
804
 
    def test_booleans(self):
805
 
        keywords = ["Bool", "NonDbusBoolean"]
806
 
        text = str(TableOfClients(self.clients, keywords,
807
 
                                  self.tableheaders))
808
 
        expected_text = """
809
 
A D-BUS Boolean A Non-D-BUS Boolean
810
 
No              False              
811
 
Yes             True               
812
 
"""[1:-1]
813
 
        self.assertEqual(text, expected_text)
814
 
    def test_milliseconds_detection(self):
815
 
        keywords = ["Integer", "Timeout", "Interval", "ApprovalDelay",
816
 
                    "ApprovalDuration", "ExtendedTimeout"]
817
 
        text = str(TableOfClients(self.clients, keywords,
818
 
                                  self.tableheaders))
819
 
        expected_text = """
820
 
An Integer Timedelta 1 Timedelta 2 Timedelta 3 Timedelta 4 Timedelta 5
821
 
0          00:00:00    00:00:01    00:00:02    00:00:03    00:00:04   
822
 
1          1T02:03:05  1T02:03:06  1T02:03:07  1T02:03:08  1T02:03:09 
823
 
"""[1:-1]
824
 
        self.assertEqual(text, expected_text)
825
 
    def test_empty_and_long_string_values(self):
826
 
        keywords = ["String"]
827
 
        text = str(TableOfClients(self.clients, keywords,
828
 
                                  self.tableheaders))
829
 
        expected_text = """
830
 
A String                                                                                                                                                                                                                                                                                                                                  
831
 
                                                                                                                                                                                                                                                                                                                                          
832
 
A huge string which will not fit,A huge string which will not fit,A huge string which will not fit,A huge string which will not fit,A huge string which will not fit,A huge string which will not fit,A huge string which will not fit,A huge string which will not fit,A huge string which will not fit,A huge string which will not fit,
833
 
"""[1:-1]
834
 
        self.assertEqual(text, expected_text)
 
976
        return super(TestDumpJSONCmd, self).setUp()
 
977
    def test_normal(self):
 
978
        json_data = json.loads(DumpJSONCmd().output(self.clients))
 
979
        self.assertDictEqual(json_data, self.expected_json)
 
980
    def test_one_client(self):
 
981
        clients = self.one_client
 
982
        json_data = json.loads(DumpJSONCmd().output(clients))
 
983
        expected_json = {"foo": self.expected_json["foo"]}
 
984
        self.assertDictEqual(json_data, expected_json)
 
985
 
 
986
class TestIsEnabledCmd(TestCmd):
 
987
    def test_is_enabled(self):
 
988
        self.assertTrue(all(IsEnabledCmd().is_enabled(client, properties)
 
989
                            for client, properties in self.clients.items()))
 
990
    def test_is_enabled_run_exits_successfully(self):
 
991
        with self.assertRaises(SystemExit) as e:
 
992
            IsEnabledCmd().run(None, self.one_client)
 
993
        if e.exception.code is not None:
 
994
            self.assertEqual(e.exception.code, 0)
 
995
        else:
 
996
            self.assertIsNone(e.exception.code)
 
997
    def test_is_enabled_run_exits_with_failure(self):
 
998
        self.client.attributes["Enabled"] = dbus.Boolean(False)
 
999
        with self.assertRaises(SystemExit) as e:
 
1000
            IsEnabledCmd().run(None, self.one_client)
 
1001
        if isinstance(e.exception.code, int):
 
1002
            self.assertNotEqual(e.exception.code, 0)
 
1003
        else:
 
1004
            self.assertIsNotNone(e.exception.code)
 
1005
 
 
1006
class TestRemoveCmd(TestCmd):
 
1007
    def test_remove(self):
 
1008
        class MockMandos(object):
 
1009
            def __init__(self):
 
1010
                self.calls = []
 
1011
            def RemoveClient(self, dbus_path):
 
1012
                self.calls.append(("RemoveClient", (dbus_path,)))
 
1013
        mandos = MockMandos()
 
1014
        super(TestRemoveCmd, self).setUp()
 
1015
        RemoveCmd().run(mandos, self.clients)
 
1016
        self.assertEqual(len(mandos.calls), 2)
 
1017
        for client in self.clients:
 
1018
            self.assertIn(("RemoveClient",
 
1019
                           (client.__dbus_object_path__,)),
 
1020
                          mandos.calls)
 
1021
 
 
1022
class TestApproveCmd(TestCmd):
 
1023
    def test_approve(self):
 
1024
        ApproveCmd().run(None, self.clients)
 
1025
        for client in self.clients:
 
1026
            self.assertIn(("Approve", (True, client_interface)),
 
1027
                          client.calls)
 
1028
 
 
1029
class TestDenyCmd(TestCmd):
 
1030
    def test_deny(self):
 
1031
        DenyCmd().run(None, self.clients)
 
1032
        for client in self.clients:
 
1033
            self.assertIn(("Approve", (False, client_interface)),
 
1034
                          client.calls)
 
1035
 
 
1036
class TestEnableCmd(TestCmd):
 
1037
    def test_enable(self):
 
1038
        for client in self.clients:
 
1039
            client.attributes["Enabled"] = False
 
1040
 
 
1041
        EnableCmd().run(None, self.clients)
 
1042
 
 
1043
        for client in self.clients:
 
1044
            self.assertTrue(client.attributes["Enabled"])
 
1045
 
 
1046
class TestDisableCmd(TestCmd):
 
1047
    def test_disable(self):
 
1048
        DisableCmd().run(None, self.clients)
 
1049
 
 
1050
        for client in self.clients:
 
1051
            self.assertFalse(client.attributes["Enabled"])
 
1052
 
 
1053
class Unique(object):
 
1054
    """Class for objects which exist only to be unique objects, since
 
1055
unittest.mock.sentinel only exists in Python 3.3"""
 
1056
 
 
1057
class TestPropertyCmd(TestCmd):
 
1058
    """Abstract class for tests of PropertyCmd classes"""
 
1059
    def runTest(self):
 
1060
        if not hasattr(self, "command"):
 
1061
            return
 
1062
        values_to_get = getattr(self, "values_to_get",
 
1063
                                self.values_to_set)
 
1064
        for value_to_set, value_to_get in zip(self.values_to_set,
 
1065
                                              values_to_get):
 
1066
            for client in self.clients:
 
1067
                old_value = client.attributes[self.property]
 
1068
                self.assertNotIsInstance(old_value, Unique)
 
1069
                client.attributes[self.property] = Unique()
 
1070
            self.run_command(value_to_set, self.clients)
 
1071
            for client in self.clients:
 
1072
                value = client.attributes[self.property]
 
1073
                self.assertNotIsInstance(value, Unique)
 
1074
                self.assertEqual(value, value_to_get)
 
1075
    def run_command(self, value, clients):
 
1076
        self.command().run(None, clients)
 
1077
 
 
1078
class TestBumpTimeoutCmd(TestPropertyCmd):
 
1079
    command = BumpTimeoutCmd
 
1080
    property = "LastCheckedOK"
 
1081
    values_to_set = [""]
 
1082
 
 
1083
class TestStartCheckerCmd(TestPropertyCmd):
 
1084
    command = StartCheckerCmd
 
1085
    property = "CheckerRunning"
 
1086
    values_to_set = [dbus.Boolean(True)]
 
1087
 
 
1088
class TestStopCheckerCmd(TestPropertyCmd):
 
1089
    command = StopCheckerCmd
 
1090
    property = "CheckerRunning"
 
1091
    values_to_set = [dbus.Boolean(False)]
 
1092
 
 
1093
class TestApproveByDefaultCmd(TestPropertyCmd):
 
1094
    command = ApproveByDefaultCmd
 
1095
    property = "ApprovedByDefault"
 
1096
    values_to_set = [dbus.Boolean(True)]
 
1097
 
 
1098
class TestDenyByDefaultCmd(TestPropertyCmd):
 
1099
    command = DenyByDefaultCmd
 
1100
    property = "ApprovedByDefault"
 
1101
    values_to_set = [dbus.Boolean(False)]
 
1102
 
 
1103
class TestValueArgumentPropertyCmd(TestPropertyCmd):
 
1104
    """Abstract class for tests of PropertyCmd classes using the
 
1105
ValueArgumentMixIn"""
 
1106
    def runTest(self):
 
1107
        if type(self) is TestValueArgumentPropertyCmd:
 
1108
            return
 
1109
        return super(TestValueArgumentPropertyCmd, self).runTest()
 
1110
    def run_command(self, value, clients):
 
1111
        self.command(value).run(None, clients)
 
1112
 
 
1113
class TestSetCheckerCmd(TestValueArgumentPropertyCmd):
 
1114
    command = SetCheckerCmd
 
1115
    property = "Checker"
 
1116
    values_to_set = ["", ":", "fping -q -- %s"]
 
1117
 
 
1118
class TestSetHostCmd(TestValueArgumentPropertyCmd):
 
1119
    command = SetHostCmd
 
1120
    property = "Host"
 
1121
    values_to_set = ["192.0.2.3", "foo.example.org"]
 
1122
 
 
1123
class TestSetSecretCmd(TestValueArgumentPropertyCmd):
 
1124
    command = SetSecretCmd
 
1125
    property = "Secret"
 
1126
    values_to_set = [io.BytesIO(b""),
 
1127
                     io.BytesIO(b"secret\0xyzzy\nbar")]
 
1128
    values_to_get = [b"", b"secret\0xyzzy\nbar"]
 
1129
 
 
1130
class TestSetTimeoutCmd(TestValueArgumentPropertyCmd):
 
1131
    command = SetTimeoutCmd
 
1132
    property = "Timeout"
 
1133
    values_to_set = [datetime.timedelta(),
 
1134
                     datetime.timedelta(minutes=5),
 
1135
                     datetime.timedelta(seconds=1),
 
1136
                     datetime.timedelta(weeks=1),
 
1137
                     datetime.timedelta(weeks=52)]
 
1138
    values_to_get = [0, 300000, 1000, 604800000, 31449600000]
 
1139
 
 
1140
class TestSetExtendedTimeoutCmd(TestValueArgumentPropertyCmd):
 
1141
    command = SetExtendedTimeoutCmd
 
1142
    property = "ExtendedTimeout"
 
1143
    values_to_set = [datetime.timedelta(),
 
1144
                     datetime.timedelta(minutes=5),
 
1145
                     datetime.timedelta(seconds=1),
 
1146
                     datetime.timedelta(weeks=1),
 
1147
                     datetime.timedelta(weeks=52)]
 
1148
    values_to_get = [0, 300000, 1000, 604800000, 31449600000]
 
1149
 
 
1150
class TestSetIntervalCmd(TestValueArgumentPropertyCmd):
 
1151
    command = SetIntervalCmd
 
1152
    property = "Interval"
 
1153
    values_to_set = [datetime.timedelta(),
 
1154
                     datetime.timedelta(minutes=5),
 
1155
                     datetime.timedelta(seconds=1),
 
1156
                     datetime.timedelta(weeks=1),
 
1157
                     datetime.timedelta(weeks=52)]
 
1158
    values_to_get = [0, 300000, 1000, 604800000, 31449600000]
 
1159
 
 
1160
class TestSetApprovalDelayCmd(TestValueArgumentPropertyCmd):
 
1161
    command = SetApprovalDelayCmd
 
1162
    property = "ApprovalDelay"
 
1163
    values_to_set = [datetime.timedelta(),
 
1164
                     datetime.timedelta(minutes=5),
 
1165
                     datetime.timedelta(seconds=1),
 
1166
                     datetime.timedelta(weeks=1),
 
1167
                     datetime.timedelta(weeks=52)]
 
1168
    values_to_get = [0, 300000, 1000, 604800000, 31449600000]
 
1169
 
 
1170
class TestSetApprovalDurationCmd(TestValueArgumentPropertyCmd):
 
1171
    command = SetApprovalDurationCmd
 
1172
    property = "ApprovalDuration"
 
1173
    values_to_set = [datetime.timedelta(),
 
1174
                     datetime.timedelta(minutes=5),
 
1175
                     datetime.timedelta(seconds=1),
 
1176
                     datetime.timedelta(weeks=1),
 
1177
                     datetime.timedelta(weeks=52)]
 
1178
    values_to_get = [0, 300000, 1000, 604800000, 31449600000]
 
1179
 
 
1180
class Test_command_from_options(unittest.TestCase):
 
1181
    def setUp(self):
 
1182
        self.parser = argparse.ArgumentParser()
 
1183
        add_command_line_options(self.parser)
 
1184
    def assert_command_from_args(self, args, command_cls, **cmd_attrs):
 
1185
        """Assert that parsing ARGS should result in an instance of
 
1186
COMMAND_CLS with (optionally) all supplied attributes (CMD_ATTRS)."""
 
1187
        options = self.parser.parse_args(args)
 
1188
        check_option_syntax(self.parser, options)
 
1189
        commands = commands_from_options(options)
 
1190
        self.assertEqual(len(commands), 1)
 
1191
        command = commands[0]
 
1192
        self.assertIsInstance(command, command_cls)
 
1193
        for key, value in cmd_attrs.items():
 
1194
            self.assertEqual(getattr(command, key), value)
 
1195
    def test_print_table(self):
 
1196
        self.assert_command_from_args([], PrintTableCmd,
 
1197
                                      verbose=False)
 
1198
 
 
1199
    def test_print_table_verbose(self):
 
1200
        self.assert_command_from_args(["--verbose"], PrintTableCmd,
 
1201
                                      verbose=True)
 
1202
 
 
1203
    def test_print_table_verbose_short(self):
 
1204
        self.assert_command_from_args(["-v"], PrintTableCmd,
 
1205
                                      verbose=True)
 
1206
 
 
1207
    def test_enable(self):
 
1208
        self.assert_command_from_args(["--enable", "foo"], EnableCmd)
 
1209
 
 
1210
    def test_enable_short(self):
 
1211
        self.assert_command_from_args(["-e", "foo"], EnableCmd)
 
1212
 
 
1213
    def test_disable(self):
 
1214
        self.assert_command_from_args(["--disable", "foo"],
 
1215
                                      DisableCmd)
 
1216
 
 
1217
    def test_disable_short(self):
 
1218
        self.assert_command_from_args(["-d", "foo"], DisableCmd)
 
1219
 
 
1220
    def test_bump_timeout(self):
 
1221
        self.assert_command_from_args(["--bump-timeout", "foo"],
 
1222
                                      BumpTimeoutCmd)
 
1223
 
 
1224
    def test_bump_timeout_short(self):
 
1225
        self.assert_command_from_args(["-b", "foo"], BumpTimeoutCmd)
 
1226
 
 
1227
    def test_start_checker(self):
 
1228
        self.assert_command_from_args(["--start-checker", "foo"],
 
1229
                                      StartCheckerCmd)
 
1230
 
 
1231
    def test_stop_checker(self):
 
1232
        self.assert_command_from_args(["--stop-checker", "foo"],
 
1233
                                      StopCheckerCmd)
 
1234
 
 
1235
    def test_remove(self):
 
1236
        self.assert_command_from_args(["--remove", "foo"],
 
1237
                                      RemoveCmd)
 
1238
 
 
1239
    def test_remove_short(self):
 
1240
        self.assert_command_from_args(["-r", "foo"], RemoveCmd)
 
1241
 
 
1242
    def test_checker(self):
 
1243
        self.assert_command_from_args(["--checker", ":", "foo"],
 
1244
                                      SetCheckerCmd, value_to_set=":")
 
1245
 
 
1246
    def test_checker_empty(self):
 
1247
        self.assert_command_from_args(["--checker", "", "foo"],
 
1248
                                      SetCheckerCmd, value_to_set="")
 
1249
 
 
1250
    def test_checker_short(self):
 
1251
        self.assert_command_from_args(["-c", ":", "foo"],
 
1252
                                      SetCheckerCmd, value_to_set=":")
 
1253
 
 
1254
    def test_timeout(self):
 
1255
        self.assert_command_from_args(["--timeout", "PT5M", "foo"],
 
1256
                                      SetTimeoutCmd,
 
1257
                                      value_to_set=300000)
 
1258
 
 
1259
    def test_timeout_short(self):
 
1260
        self.assert_command_from_args(["-t", "PT5M", "foo"],
 
1261
                                      SetTimeoutCmd,
 
1262
                                      value_to_set=300000)
 
1263
 
 
1264
    def test_extended_timeout(self):
 
1265
        self.assert_command_from_args(["--extended-timeout", "PT15M",
 
1266
                                       "foo"],
 
1267
                                      SetExtendedTimeoutCmd,
 
1268
                                      value_to_set=900000)
 
1269
 
 
1270
    def test_interval(self):
 
1271
        self.assert_command_from_args(["--interval", "PT2M", "foo"],
 
1272
                                      SetIntervalCmd,
 
1273
                                      value_to_set=120000)
 
1274
 
 
1275
    def test_interval_short(self):
 
1276
        self.assert_command_from_args(["-i", "PT2M", "foo"],
 
1277
                                      SetIntervalCmd,
 
1278
                                      value_to_set=120000)
 
1279
 
 
1280
    def test_approve_by_default(self):
 
1281
        self.assert_command_from_args(["--approve-by-default", "foo"],
 
1282
                                      ApproveByDefaultCmd)
 
1283
 
 
1284
    def test_deny_by_default(self):
 
1285
        self.assert_command_from_args(["--deny-by-default", "foo"],
 
1286
                                      DenyByDefaultCmd)
 
1287
 
 
1288
    def test_approval_delay(self):
 
1289
        self.assert_command_from_args(["--approval-delay", "PT30S",
 
1290
                                       "foo"], SetApprovalDelayCmd,
 
1291
                                      value_to_set=30000)
 
1292
 
 
1293
    def test_approval_duration(self):
 
1294
        self.assert_command_from_args(["--approval-duration", "PT1S",
 
1295
                                       "foo"], SetApprovalDurationCmd,
 
1296
                                      value_to_set=1000)
 
1297
 
 
1298
    def test_host(self):
 
1299
        self.assert_command_from_args(["--host", "foo.example.org",
 
1300
                                       "foo"], SetHostCmd,
 
1301
                                      value_to_set="foo.example.org")
 
1302
 
 
1303
    def test_host_short(self):
 
1304
        self.assert_command_from_args(["-H", "foo.example.org",
 
1305
                                       "foo"], SetHostCmd,
 
1306
                                      value_to_set="foo.example.org")
 
1307
 
 
1308
    def test_secret_devnull(self):
 
1309
        self.assert_command_from_args(["--secret", os.path.devnull,
 
1310
                                       "foo"], SetSecretCmd,
 
1311
                                      value_to_set=b"")
 
1312
 
 
1313
    def test_secret_tempfile(self):
 
1314
        with tempfile.NamedTemporaryFile(mode="r+b") as f:
 
1315
            value = b"secret\0xyzzy\nbar"
 
1316
            f.write(value)
 
1317
            f.seek(0)
 
1318
            self.assert_command_from_args(["--secret", f.name,
 
1319
                                           "foo"], SetSecretCmd,
 
1320
                                          value_to_set=value)
 
1321
 
 
1322
    def test_secret_devnull_short(self):
 
1323
        self.assert_command_from_args(["-s", os.path.devnull, "foo"],
 
1324
                                      SetSecretCmd, value_to_set=b"")
 
1325
 
 
1326
    def test_secret_tempfile_short(self):
 
1327
        with tempfile.NamedTemporaryFile(mode="r+b") as f:
 
1328
            value = b"secret\0xyzzy\nbar"
 
1329
            f.write(value)
 
1330
            f.seek(0)
 
1331
            self.assert_command_from_args(["-s", f.name, "foo"],
 
1332
                                          SetSecretCmd,
 
1333
                                          value_to_set=value)
 
1334
 
 
1335
    def test_approve(self):
 
1336
        self.assert_command_from_args(["--approve", "foo"],
 
1337
                                      ApproveCmd)
 
1338
 
 
1339
    def test_approve_short(self):
 
1340
        self.assert_command_from_args(["-A", "foo"], ApproveCmd)
 
1341
 
 
1342
    def test_deny(self):
 
1343
        self.assert_command_from_args(["--deny", "foo"], DenyCmd)
 
1344
 
 
1345
    def test_deny_short(self):
 
1346
        self.assert_command_from_args(["-D", "foo"], DenyCmd)
 
1347
 
 
1348
    def test_dump_json(self):
 
1349
        self.assert_command_from_args(["--dump-json"], DumpJSONCmd)
 
1350
 
 
1351
    def test_is_enabled(self):
 
1352
        self.assert_command_from_args(["--is-enabled", "foo"],
 
1353
                                      IsEnabledCmd)
 
1354
 
 
1355
    def test_is_enabled_short(self):
 
1356
        self.assert_command_from_args(["-V", "foo"], IsEnabledCmd)
 
1357
 
 
1358
 
 
1359
class Test_check_option_syntax(unittest.TestCase):
 
1360
    # This mostly corresponds to the definition from has_actions() in
 
1361
    # check_option_syntax()
 
1362
    actions = {
 
1363
        # The actual values set here are not that important, but we do
 
1364
        # at least stick to the correct types, even though they are
 
1365
        # never used
 
1366
        "enable": True,
 
1367
        "disable": True,
 
1368
        "bump_timeout": True,
 
1369
        "start_checker": True,
 
1370
        "stop_checker": True,
 
1371
        "is_enabled": True,
 
1372
        "remove": True,
 
1373
        "checker": "x",
 
1374
        "timeout": datetime.timedelta(),
 
1375
        "extended_timeout": datetime.timedelta(),
 
1376
        "interval": datetime.timedelta(),
 
1377
        "approved_by_default": True,
 
1378
        "approval_delay": datetime.timedelta(),
 
1379
        "approval_duration": datetime.timedelta(),
 
1380
        "host": "x",
 
1381
        "secret": io.BytesIO(b"x"),
 
1382
        "approve": True,
 
1383
        "deny": True,
 
1384
    }
 
1385
 
 
1386
    def setUp(self):
 
1387
        self.parser = argparse.ArgumentParser()
 
1388
        add_command_line_options(self.parser)
 
1389
 
 
1390
    @contextlib.contextmanager
 
1391
    def assertParseError(self):
 
1392
        with self.assertRaises(SystemExit) as e:
 
1393
            with self.temporarily_suppress_stderr():
 
1394
                yield
 
1395
        # Exit code from argparse is guaranteed to be "2".  Reference:
 
1396
        # https://docs.python.org/3/library/argparse.html#exiting-methods
 
1397
        self.assertEqual(e.exception.code, 2)
 
1398
 
 
1399
    @staticmethod
 
1400
    @contextlib.contextmanager
 
1401
    def temporarily_suppress_stderr():
 
1402
        null = os.open(os.path.devnull, os.O_RDWR)
 
1403
        stderrcopy = os.dup(sys.stderr.fileno())
 
1404
        os.dup2(null, sys.stderr.fileno())
 
1405
        os.close(null)
 
1406
        try:
 
1407
            yield
 
1408
        finally:
 
1409
            # restore stderr
 
1410
            os.dup2(stderrcopy, sys.stderr.fileno())
 
1411
            os.close(stderrcopy)
 
1412
 
 
1413
    def check_option_syntax(self, options):
 
1414
        check_option_syntax(self.parser, options)
 
1415
 
 
1416
    def test_actions_requires_client_or_all(self):
 
1417
        for action, value in self.actions.items():
 
1418
            options = self.parser.parse_args()
 
1419
            setattr(options, action, value)
 
1420
            with self.assertParseError():
 
1421
                self.check_option_syntax(options)
 
1422
 
 
1423
    def test_actions_conflicts_with_verbose(self):
 
1424
        for action, value in self.actions.items():
 
1425
            options = self.parser.parse_args()
 
1426
            setattr(options, action, value)
 
1427
            options.verbose = True
 
1428
            with self.assertParseError():
 
1429
                self.check_option_syntax(options)
 
1430
 
 
1431
    def test_dump_json_conflicts_with_verbose(self):
 
1432
        options = self.parser.parse_args()
 
1433
        options.dump_json = True
 
1434
        options.verbose = True
 
1435
        with self.assertParseError():
 
1436
            self.check_option_syntax(options)
 
1437
 
 
1438
    def test_dump_json_conflicts_with_action(self):
 
1439
        for action, value in self.actions.items():
 
1440
            options = self.parser.parse_args()
 
1441
            setattr(options, action, value)
 
1442
            options.dump_json = True
 
1443
            with self.assertParseError():
 
1444
                self.check_option_syntax(options)
 
1445
 
 
1446
    def test_all_can_not_be_alone(self):
 
1447
        options = self.parser.parse_args()
 
1448
        options.all = True
 
1449
        with self.assertParseError():
 
1450
            self.check_option_syntax(options)
 
1451
 
 
1452
    def test_all_is_ok_with_any_action(self):
 
1453
        for action, value in self.actions.items():
 
1454
            options = self.parser.parse_args()
 
1455
            setattr(options, action, value)
 
1456
            options.all = True
 
1457
            self.check_option_syntax(options)
 
1458
 
 
1459
    def test_is_enabled_fails_without_client(self):
 
1460
        options = self.parser.parse_args()
 
1461
        options.is_enabled = True
 
1462
        with self.assertParseError():
 
1463
            self.check_option_syntax(options)
 
1464
 
 
1465
    def test_is_enabled_works_with_one_client(self):
 
1466
        options = self.parser.parse_args()
 
1467
        options.is_enabled = True
 
1468
        options.client = ["foo"]
 
1469
        self.check_option_syntax(options)
 
1470
 
 
1471
    def test_is_enabled_fails_with_two_clients(self):
 
1472
        options = self.parser.parse_args()
 
1473
        options.is_enabled = True
 
1474
        options.client = ["foo", "barbar"]
 
1475
        with self.assertParseError():
 
1476
            self.check_option_syntax(options)
835
1477
 
836
1478
 
837
1479