272
class TableOfClients(object):
275
"Enabled": "Enabled",
276
"Timeout": "Timeout",
277
"LastCheckedOK": "Last Successful Check",
278
"LastApprovalRequest": "Last Approval Request",
279
"Created": "Created",
280
"Interval": "Interval",
282
"Fingerprint": "Fingerprint",
284
"CheckerRunning": "Check Is Running",
285
"LastEnabled": "Last Enabled",
286
"ApprovalPending": "Approval Is Pending",
287
"ApprovedByDefault": "Approved By Default",
288
"ApprovalDelay": "Approval Delay",
289
"ApprovalDuration": "Approval Duration",
290
"Checker": "Checker",
291
"ExtendedTimeout": "Extended Timeout",
292
"Expires": "Expires",
293
"LastCheckerStatus": "Last Checker Status",
296
def __init__(self, clients, keywords, tablewords=None):
297
self.clients = clients
298
self.keywords = keywords
299
if tablewords is not None:
300
self.tablewords = tablewords
303
return "\n".join(self.rows())
305
if sys.version_info.major == 2:
306
__unicode__ = __str__
275
## Classes for commands.
277
# Abstract classes first
278
class Command(object):
279
"""Abstract class for commands"""
280
def run(self, clients, bus=None, mandos=None):
281
"""Normal commands should implement run_on_one_client(), but
282
commands which want to operate on all clients at the same time
283
can override this run() method instead."""
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)
289
self.run_on_one_client(client, properties)
291
class PrintCmd(Command):
292
"""Abstract class for commands printing client details"""
293
all_keywords = ("Name", "Enabled", "Timeout", "LastCheckedOK",
294
"Created", "Interval", "Host", "KeyID",
295
"Fingerprint", "CheckerRunning", "LastEnabled",
296
"ApprovalPending", "ApprovedByDefault",
297
"LastApprovalRequest", "ApprovalDelay",
298
"ApprovalDuration", "Checker", "ExtendedTimeout",
299
"Expires", "LastCheckerStatus")
300
def run(self, clients, bus=None, mandos=None):
301
print(self.output(clients.values()))
302
def output(self, clients):
303
raise NotImplementedError()
305
class PropertyCmd(Command):
306
"""Abstract class for Actions for setting one client property"""
307
def run_on_one_client(self, client, properties):
308
"""Set the Client's D-Bus property"""
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,
317
dbus_interface=dbus.PROPERTIES_IFACE)
320
raise NotImplementedError()
322
class PropertyValueCmd(PropertyCmd):
323
"""Abstract class for PropertyCmd recieving a value as argument"""
324
def __init__(self, value):
325
self.value_to_set = value
327
class MillisecondsPropertyValueArgumentCmd(PropertyValueCmd):
328
"""Abstract class for PropertyValueCmd taking a value argument as
329
a datetime.timedelta() but should store it as milliseconds."""
331
def value_to_set(self):
334
def value_to_set(self, value):
335
"""When setting, convert value from a datetime.timedelta"""
336
self._vts = int(round(value.total_seconds() * 1000))
338
# Actual (non-abstract) command classes
340
class PrintTableCmd(PrintCmd):
341
def __init__(self, verbose=False):
342
self.verbose = verbose
344
def output(self, clients):
345
default_keywords = ("Name", "Enabled", "Timeout",
347
keywords = default_keywords
349
keywords = self.all_keywords
350
return str(self.TableOfClients(clients, keywords))
352
class TableOfClients(object):
355
"Enabled": "Enabled",
356
"Timeout": "Timeout",
357
"LastCheckedOK": "Last Successful Check",
358
"LastApprovalRequest": "Last Approval Request",
359
"Created": "Created",
360
"Interval": "Interval",
362
"Fingerprint": "Fingerprint",
364
"CheckerRunning": "Check Is Running",
365
"LastEnabled": "Last Enabled",
366
"ApprovalPending": "Approval Is Pending",
367
"ApprovedByDefault": "Approved By Default",
368
"ApprovalDelay": "Approval Delay",
369
"ApprovalDuration": "Approval Duration",
370
"Checker": "Checker",
371
"ExtendedTimeout": "Extended Timeout",
372
"Expires": "Expires",
373
"LastCheckerStatus": "Last Checker Status",
376
def __init__(self, clients, keywords, tableheaders=None):
377
self.clients = clients
378
self.keywords = keywords
379
if tableheaders is not None:
380
self.tableheaders = tableheaders
307
382
def __str__(self):
308
return str(self).encode(locale.getpreferredencoding())
311
format_string = self.row_formatting_string()
312
rows = [self.header_line(format_string)]
313
rows.extend(self.client_line(client, format_string)
314
for client in self.clients)
317
def row_formatting_string(self):
318
"Format string used to format table rows"
319
return " ".join("{{{key}:{width}}}".format(
320
width=max(len(self.tablewords[key]),
321
*(len(self.string_from_client(client, key))
322
for client in self.clients)),
324
for key in self.keywords)
326
def string_from_client(self, client, key):
327
return self.valuetostring(client[key], key)
383
return "\n".join(self.rows())
385
if sys.version_info.major == 2:
386
__unicode__ = __str__
388
return str(self).encode(locale.getpreferredencoding())
391
format_string = self.row_formatting_string()
392
rows = [self.header_line(format_string)]
393
rows.extend(self.client_line(client, format_string)
394
for client in self.clients)
397
def row_formatting_string(self):
398
"Format string used to format table rows"
399
return " ".join("{{{key}:{width}}}".format(
400
width=max(len(self.tableheaders[key]),
401
*(len(self.string_from_client(client, key))
402
for client in self.clients)),
404
for key in self.keywords)
406
def string_from_client(self, client, key):
407
return self.valuetostring(client[key], key)
410
def valuetostring(value, keyword):
411
if isinstance(value, dbus.Boolean):
412
return "Yes" if value else "No"
413
if keyword in ("Timeout", "Interval", "ApprovalDelay",
414
"ApprovalDuration", "ExtendedTimeout"):
415
return milliseconds_to_string(value)
418
def header_line(self, format_string):
419
return format_string.format(**self.tableheaders)
421
def client_line(self, client, format_string):
422
return format_string.format(
423
**{key: self.string_from_client(client, key)
424
for key in self.keywords})
428
class DumpJSONCmd(PrintCmd):
429
def output(self, clients):
430
data = {client["Name"]:
431
{key: self.dbus_boolean_to_bool(client[key])
432
for key in self.all_keywords}
433
for client in clients.values()}
434
return json.dumps(data, indent=4, separators=(',', ': '))
330
def valuetostring(value, keyword):
436
def dbus_boolean_to_bool(value):
331
437
if isinstance(value, dbus.Boolean):
332
return "Yes" if value else "No"
333
if keyword in ("Timeout", "Interval", "ApprovalDelay",
334
"ApprovalDuration", "ExtendedTimeout"):
335
return milliseconds_to_string(value)
338
def header_line(self, format_string):
339
return format_string.format(**self.tablewords)
341
def client_line(self, client, format_string):
342
return format_string.format(
343
**{key: self.string_from_client(client, key)
344
for key in self.keywords})
347
def has_actions(options):
348
return any((options.enable,
350
options.bump_timeout,
351
options.start_checker,
352
options.stop_checker,
355
options.checker is not None,
356
options.timeout is not None,
357
options.extended_timeout is not None,
358
options.interval is not None,
359
options.approved_by_default is not None,
360
options.approval_delay is not None,
361
options.approval_duration is not None,
362
options.host is not None,
363
options.secret is not None,
369
parser = argparse.ArgumentParser()
441
class IsEnabledCmd(Command):
442
def run(self, clients, bus=None, mandos=None):
443
client, properties = next(iter(clients.items()))
444
if self.is_enabled(client, properties):
447
def is_enabled(self, client, properties):
448
return properties["Enabled"]
450
class RemoveCmd(Command):
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__))
455
self.mandos.RemoveClient(client.__dbus_object_path__)
457
class ApproveCmd(Command):
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)
461
client.Approve(dbus.Boolean(True),
462
dbus_interface=client_dbus_interface)
464
class DenyCmd(Command):
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)
468
client.Approve(dbus.Boolean(False),
469
dbus_interface=client_dbus_interface)
471
class EnableCmd(PropertyCmd):
473
value_to_set = dbus.Boolean(True)
475
class DisableCmd(PropertyCmd):
477
value_to_set = dbus.Boolean(False)
479
class BumpTimeoutCmd(PropertyCmd):
480
propname = "LastCheckedOK"
483
class StartCheckerCmd(PropertyCmd):
484
propname = "CheckerRunning"
485
value_to_set = dbus.Boolean(True)
487
class StopCheckerCmd(PropertyCmd):
488
propname = "CheckerRunning"
489
value_to_set = dbus.Boolean(False)
491
class ApproveByDefaultCmd(PropertyCmd):
492
propname = "ApprovedByDefault"
493
value_to_set = dbus.Boolean(True)
495
class DenyByDefaultCmd(PropertyCmd):
496
propname = "ApprovedByDefault"
497
value_to_set = dbus.Boolean(False)
499
class SetCheckerCmd(PropertyValueCmd):
502
class SetHostCmd(PropertyValueCmd):
505
class SetSecretCmd(PropertyValueCmd):
508
def value_to_set(self):
511
def value_to_set(self, value):
512
"""When setting, read data from supplied file object"""
513
self._vts = value.read()
516
class SetTimeoutCmd(MillisecondsPropertyValueArgumentCmd):
519
class SetExtendedTimeoutCmd(MillisecondsPropertyValueArgumentCmd):
520
propname = "ExtendedTimeout"
522
class SetIntervalCmd(MillisecondsPropertyValueArgumentCmd):
523
propname = "Interval"
525
class SetApprovalDelayCmd(MillisecondsPropertyValueArgumentCmd):
526
propname = "ApprovalDelay"
528
class SetApprovalDurationCmd(MillisecondsPropertyValueArgumentCmd):
529
propname = "ApprovalDuration"
531
def add_command_line_options(parser):
370
532
parser.add_argument("--version", action="version",
371
533
version="%(prog)s {}".format(version),
372
534
help="show version number and exit")
376
538
help="Print all fields")
377
539
parser.add_argument("-j", "--dump-json", action="store_true",
378
540
help="Dump client data in JSON format")
379
parser.add_argument("-e", "--enable", action="store_true",
380
help="Enable client")
381
parser.add_argument("-d", "--disable", action="store_true",
382
help="disable client")
541
enable_disable = parser.add_mutually_exclusive_group()
542
enable_disable.add_argument("-e", "--enable", action="store_true",
543
help="Enable client")
544
enable_disable.add_argument("-d", "--disable",
546
help="disable client")
383
547
parser.add_argument("-b", "--bump-timeout", action="store_true",
384
548
help="Bump timeout for client")
385
parser.add_argument("--start-checker", action="store_true",
386
help="Start checker for client")
387
parser.add_argument("--stop-checker", action="store_true",
388
help="Stop checker for client")
549
start_stop_checker = parser.add_mutually_exclusive_group()
550
start_stop_checker.add_argument("--start-checker",
552
help="Start checker for client")
553
start_stop_checker.add_argument("--stop-checker",
555
help="Stop checker for client")
389
556
parser.add_argument("-V", "--is-enabled", action="store_true",
390
557
help="Check if client is enabled")
391
558
parser.add_argument("-r", "--remove", action="store_true",
392
559
help="Remove client")
393
560
parser.add_argument("-c", "--checker",
394
561
help="Set checker command for client")
395
parser.add_argument("-t", "--timeout",
562
parser.add_argument("-t", "--timeout", type=string_to_delta,
396
563
help="Set timeout for client")
397
parser.add_argument("--extended-timeout",
564
parser.add_argument("--extended-timeout", type=string_to_delta,
398
565
help="Set extended timeout for client")
399
parser.add_argument("-i", "--interval",
566
parser.add_argument("-i", "--interval", type=string_to_delta,
400
567
help="Set checker interval for client")
401
parser.add_argument("--approve-by-default", action="store_true",
402
default=None, dest="approved_by_default",
403
help="Set client to be approved by default")
404
parser.add_argument("--deny-by-default", action="store_false",
405
dest="approved_by_default",
406
help="Set client to be denied by default")
407
parser.add_argument("--approval-delay",
568
approve_deny_default = parser.add_mutually_exclusive_group()
569
approve_deny_default.add_argument(
570
"--approve-by-default", action="store_true",
571
default=None, dest="approved_by_default",
572
help="Set client to be approved by default")
573
approve_deny_default.add_argument(
574
"--deny-by-default", action="store_false",
575
dest="approved_by_default",
576
help="Set client to be denied by default")
577
parser.add_argument("--approval-delay", type=string_to_delta,
408
578
help="Set delay before client approve/deny")
409
parser.add_argument("--approval-duration",
579
parser.add_argument("--approval-duration", type=string_to_delta,
410
580
help="Set duration of one client approval")
411
581
parser.add_argument("-H", "--host", help="Set host for client")
412
582
parser.add_argument("-s", "--secret",
413
583
type=argparse.FileType(mode="rb"),
414
584
help="Set password blob (file) for client")
415
parser.add_argument("-A", "--approve", action="store_true",
416
help="Approve any current client request")
417
parser.add_argument("-D", "--deny", action="store_true",
418
help="Deny any current client request")
585
approve_deny = parser.add_mutually_exclusive_group()
586
approve_deny.add_argument(
587
"-A", "--approve", action="store_true",
588
help="Approve any current client request")
589
approve_deny.add_argument("-D", "--deny", action="store_true",
590
help="Deny any current client request")
591
parser.add_argument("--debug", action="store_true",
592
help="Debug mode (show D-Bus commands)")
419
593
parser.add_argument("--check", action="store_true",
420
594
help="Run self-test")
421
595
parser.add_argument("client", nargs="*", help="Client name")
422
options = parser.parse_args()
598
def commands_from_options(options):
602
if options.dump_json:
603
commands.append(DumpJSONCmd())
606
commands.append(EnableCmd())
609
commands.append(DisableCmd())
611
if options.bump_timeout:
612
commands.append(BumpTimeoutCmd())
614
if options.start_checker:
615
commands.append(StartCheckerCmd())
617
if options.stop_checker:
618
commands.append(StopCheckerCmd())
620
if options.is_enabled:
621
commands.append(IsEnabledCmd())
623
if options.checker is not None:
624
commands.append(SetCheckerCmd(options.checker))
626
if options.timeout is not None:
627
commands.append(SetTimeoutCmd(options.timeout))
629
if options.extended_timeout:
631
SetExtendedTimeoutCmd(options.extended_timeout))
633
if options.interval is not None:
634
commands.append(SetIntervalCmd(options.interval))
636
if options.approved_by_default is not None:
637
if options.approved_by_default:
638
commands.append(ApproveByDefaultCmd())
640
commands.append(DenyByDefaultCmd())
642
if options.approval_delay is not None:
643
commands.append(SetApprovalDelayCmd(options.approval_delay))
645
if options.approval_duration is not None:
647
SetApprovalDurationCmd(options.approval_duration))
649
if options.host is not None:
650
commands.append(SetHostCmd(options.host))
652
if options.secret is not None:
653
commands.append(SetSecretCmd(options.secret))
656
commands.append(ApproveCmd())
659
commands.append(DenyCmd())
662
commands.append(RemoveCmd())
664
# If no command option has been given, show table of clients,
665
# optionally verbosely
667
commands.append(PrintTableCmd(verbose=options.verbose))
672
def check_option_syntax(parser, options):
673
"""Apply additional restrictions on options, not expressible in
676
def has_actions(options):
677
return any((options.enable,
679
options.bump_timeout,
680
options.start_checker,
681
options.stop_checker,
684
options.checker is not None,
685
options.timeout is not None,
686
options.extended_timeout is not None,
687
options.interval is not None,
688
options.approved_by_default is not None,
689
options.approval_delay is not None,
690
options.approval_duration is not None,
691
options.host is not None,
692
options.secret is not None,
424
696
if has_actions(options) and not (options.client or options.all):
425
697
parser.error("Options require clients names or --all.")
430
702
parser.error("--dump-json can only be used alone.")
431
703
if options.all and not has_actions(options):
432
704
parser.error("--all requires an action.")
705
if options.is_enabled and len(options.client) > 1:
706
parser.error("--is-enabled requires exactly one client")
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
715
parser = argparse.ArgumentParser()
717
add_command_line_options(parser)
719
options = parser.parse_args()
721
check_option_syntax(parser, options)
723
clientnames = options.client
726
log.setLevel(logging.DEBUG)
435
729
bus = dbus.SystemBus()
436
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,
437
734
except dbus.exceptions.DBusException:
438
735
log.critical("Could not connect to Mandos server")
441
738
mandos_serv = dbus.Interface(mandos_dbus_objc,
442
dbus_interface=server_interface)
739
dbus_interface=server_dbus_interface)
443
740
mandos_serv_object_manager = dbus.Interface(
444
741
mandos_dbus_objc, dbus_interface=dbus.OBJECT_MANAGER_IFACE)
446
# block stderr since dbus library prints to stderr
447
null = os.open(os.path.devnull, os.O_RDWR)
448
stderrcopy = os.dup(sys.stderr.fileno())
449
os.dup2(null, sys.stderr.fileno())
743
# Filter out log message from dbus module
744
dbus_logger = logging.getLogger("dbus.proxies")
745
class NullFilter(logging.Filter):
746
def filter(self, record):
748
dbus_filter = NullFilter()
453
mandos_clients = {path: ifs_and_props[client_interface]
454
for path, ifs_and_props in
455
mandos_serv_object_manager
456
.GetManagedObjects().items()
457
if client_interface in ifs_and_props}
460
os.dup2(stderrcopy, sys.stderr.fileno())
750
dbus_logger.addFilter(dbus_filter)
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]
754
for path, ifs_and_props in
755
mandos_serv_object_manager
756
.GetManagedObjects().items()
757
if client_dbus_interface in ifs_and_props}
462
758
except dbus.exceptions.DBusException as e:
463
759
log.critical("Failed to access Mandos server through D-Bus:"
763
# restore dbus logger
764
dbus_logger.removeFilter(dbus_filter)
467
766
# Compile dict of (clients: properties) to process
470
if options.all or not options.client:
471
clients = {bus.get_object(busname, path): properties
472
for path, properties in mandos_clients.items()}
770
clients = {objpath: properties
771
for objpath, properties in mandos_clients.items()}
474
for name in options.client:
475
for path, client in mandos_clients.items():
476
if client["Name"] == name:
477
client_objc = bus.get_object(busname, path)
478
clients[client_objc] = client
773
for name in clientnames:
774
for objpath, properties in mandos_clients.items():
775
if properties["Name"] == name:
776
clients[objpath] = properties
481
779
log.critical("Client not found on server: %r", name)
484
if not has_actions(options) and clients:
485
if options.verbose or options.dump_json:
486
keywords = ("Name", "Enabled", "Timeout", "LastCheckedOK",
487
"Created", "Interval", "Host", "KeyID",
488
"Fingerprint", "CheckerRunning",
489
"LastEnabled", "ApprovalPending",
490
"ApprovedByDefault", "LastApprovalRequest",
491
"ApprovalDelay", "ApprovalDuration",
492
"Checker", "ExtendedTimeout", "Expires",
495
keywords = defaultkeywords
497
if options.dump_json:
498
json.dump({client["Name"]: {key:
500
if isinstance(client[key],
504
for client in clients.values()},
505
fp=sys.stdout, indent=4,
506
separators=(',', ': '))
509
print(TableOfClients(clients.values(), keywords))
511
# Process each client in the list by all selected options
512
for client in clients:
514
def set_client_prop(prop, value):
515
"""Set a Client D-Bus property"""
516
client.Set(client_interface, prop, value,
517
dbus_interface=dbus.PROPERTIES_IFACE)
519
def set_client_prop_ms(prop, value):
520
"""Set a Client D-Bus property, converted
521
from a string to milliseconds."""
522
set_client_prop(prop,
523
string_to_delta(value).total_seconds()
527
mandos_serv.RemoveClient(client.__dbus_object_path__)
529
set_client_prop("Enabled", dbus.Boolean(True))
531
set_client_prop("Enabled", dbus.Boolean(False))
532
if options.bump_timeout:
533
set_client_prop("LastCheckedOK", "")
534
if options.start_checker:
535
set_client_prop("CheckerRunning", dbus.Boolean(True))
536
if options.stop_checker:
537
set_client_prop("CheckerRunning", dbus.Boolean(False))
538
if options.is_enabled:
539
if client.Get(client_interface, "Enabled",
540
dbus_interface=dbus.PROPERTIES_IFACE):
544
if options.checker is not None:
545
set_client_prop("Checker", options.checker)
546
if options.host is not None:
547
set_client_prop("Host", options.host)
548
if options.interval is not None:
549
set_client_prop_ms("Interval", options.interval)
550
if options.approval_delay is not None:
551
set_client_prop_ms("ApprovalDelay",
552
options.approval_delay)
553
if options.approval_duration is not None:
554
set_client_prop_ms("ApprovalDuration",
555
options.approval_duration)
556
if options.timeout is not None:
557
set_client_prop_ms("Timeout", options.timeout)
558
if options.extended_timeout is not None:
559
set_client_prop_ms("ExtendedTimeout",
560
options.extended_timeout)
561
if options.secret is not None:
562
set_client_prop("Secret",
563
dbus.ByteArray(options.secret.read()))
564
if options.approved_by_default is not None:
565
set_client_prop("ApprovedByDefault",
567
.approved_by_default))
569
client.Approve(dbus.Boolean(True),
570
dbus_interface=client_interface)
572
client.Approve(dbus.Boolean(False),
573
dbus_interface=client_interface)
782
# Run all commands on clients
783
commands = commands_from_options(options)
784
for command in commands:
785
command.run(clients, bus, mandos_serv)
576
788
class Test_milliseconds_to_string(unittest.TestCase):
596
814
with self.assertLogs(log, logging.WARNING):
597
815
value = string_to_delta("2h")
599
value = string_to_delta("2h")
817
class WarningFilter(logging.Filter):
818
"""Don't show, but record the presence of, warnings"""
819
def filter(self, record):
820
is_warning = record.levelno >= logging.WARNING
821
self.found = is_warning or getattr(self, "found",
823
return not is_warning
824
warning_filter = WarningFilter()
825
log.addFilter(warning_filter)
827
value = string_to_delta("2h")
829
log.removeFilter(warning_filter)
830
self.assertTrue(getattr(warning_filter, "found", False))
600
831
self.assertEqual(value, datetime.timedelta(0, 7200))
602
class Test_TableOfClients(unittest.TestCase):
608
"Bool": "A D-BUS Boolean",
609
"NonDbusBoolean": "A Non-D-BUS Boolean",
610
"Integer": "An Integer",
611
"Timeout": "Timedelta 1",
612
"Interval": "Timedelta 2",
613
"ApprovalDelay": "Timedelta 3",
614
"ApprovalDuration": "Timedelta 4",
615
"ExtendedTimeout": "Timedelta 5",
616
"String": "A String",
834
class TestCmd(unittest.TestCase):
835
"""Abstract class for tests of command classes"""
838
class MockClient(object):
839
def __init__(self, name, **attributes):
840
self.__dbus_object_path__ = "/clients/{}".format(name)
841
self.attributes = attributes
842
self.attributes["Name"] = name
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]
854
def Approve(self, approve, dbus_interface):
855
testcase.assertEqual(dbus_interface,
856
client_dbus_interface)
857
self.calls.append(("Approve", (approve,
859
self.client = MockClient(
861
KeyID=("92ed150794387c03ce684574b1139a65"
862
"94a34f895daaaf09fd8ea90a27cddb12"),
864
Host="foo.example.org",
865
Enabled=dbus.Boolean(True),
867
LastCheckedOK="2019-02-03T00:00:00",
868
Created="2019-01-02T00:00:00",
870
Fingerprint=("778827225BA7DE539C5A"
871
"7CFA59CFF7CDBD9A5920"),
872
CheckerRunning=dbus.Boolean(False),
873
LastEnabled="2019-01-03T00:00:00",
874
ApprovalPending=dbus.Boolean(False),
875
ApprovedByDefault=dbus.Boolean(True),
876
LastApprovalRequest="",
878
ApprovalDuration=1000,
879
Checker="fping -q -- %(host)s",
880
ExtendedTimeout=900000,
881
Expires="2019-02-04T00:00:00",
883
self.other_client = MockClient(
885
KeyID=("0558568eedd67d622f5c83b35a115f79"
886
"6ab612cff5ad227247e46c2b020f441c"),
889
Enabled=dbus.Boolean(True),
891
LastCheckedOK="2019-02-04T00:00:00",
892
Created="2019-01-03T00:00:00",
894
Fingerprint=("3E393AEAEFB84C7E89E2"
895
"F547B3A107558FCA3A27"),
896
CheckerRunning=dbus.Boolean(True),
897
LastEnabled="2019-01-04T00:00:00",
898
ApprovalPending=dbus.Boolean(False),
899
ApprovedByDefault=dbus.Boolean(False),
900
LastApprovalRequest="2019-01-03T00:00:00",
902
ApprovalDuration=1000,
904
ExtendedTimeout=900000,
905
Expires="2019-02-05T00:00:00",
906
LastCheckerStatus=-2)
907
self.clients = collections.OrderedDict(
909
("/clients/foo", self.client.attributes),
910
("/clients/barbar", self.other_client.attributes),
912
self.one_client = {"/clients/foo": self.client.attributes}
917
def get_object(client_bus_name, path):
918
self.assertEqual(client_bus_name, dbus_busname)
920
"/clients/foo": self.client,
921
"/clients/barbar": self.other_client,
925
class TestPrintTableCmd(TestCmd):
926
def test_normal(self):
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 ",
933
self.assertEqual(output, expected_output)
934
def test_verbose(self):
935
output = PrintTableCmd(verbose=True).output(
936
self.clients.values())
937
expected_output = "\n".join((
938
# First line (headers)
939
"Name Enabled Timeout Last Successful Check Created "
940
" Interval Host Key ID "
942
" Check Is Running Last Enabl"
943
"ed Approval Is Pending Approved By Default Last A"
944
"pproval Request Approval Delay Approval Duration Checker"
945
" Extended Timeout Expires Last "
947
# Second line (client "foo")
948
"foo Yes 00:05:00 2019-02-03T00:00:00 2019-01-02"
949
"T00:00:00 00:02:00 foo.example.org 92ed150794387c03ce684"
950
"574b1139a6594a34f895daaaf09fd8ea90a27cddb12 778827225BA7"
951
"DE539C5A7CFA59CFF7CDBD9A5920 No 2019-01-03"
953
" 00:00:00 00:00:01 fping -"
954
"q -- %(host)s 00:15:00 2019-02-04T00:00:00 0 "
956
# Third line (client "barbar")
957
"barbar Yes 00:05:00 2019-02-04T00:00:00 2019-01-03"
958
"T00:00:00 00:02:00 192.0.2.3 0558568eedd67d622f5c8"
959
"3b35a115f796ab612cff5ad227247e46c2b020f441c 3E393AEAEFB8"
960
"4C7E89E2F547B3A107558FCA3A27 Yes 2019-01-04"
961
"T00:00:00 No No 2019-0"
962
"1-03T00:00:00 00:00:30 00:00:01 : "
963
" 00:15:00 2019-02-05T00:00:00 -2 "
966
self.assertEqual(output, expected_output)
967
def test_one_client(self):
968
output = PrintTableCmd().output(self.one_client.values())
969
expected_output = """
970
Name Enabled Timeout Last Successful Check
971
foo Yes 00:05:00 2019-02-03T00:00:00
973
self.assertEqual(output, expected_output)
975
class TestDumpJSONCmd(TestCmd):
977
self.expected_json = {
980
"KeyID": ("92ed150794387c03ce684574b1139a65"
981
"94a34f895daaaf09fd8ea90a27cddb12"),
982
"Host": "foo.example.org",
985
"LastCheckedOK": "2019-02-03T00:00:00",
986
"Created": "2019-01-02T00:00:00",
988
"Fingerprint": ("778827225BA7DE539C5A"
989
"7CFA59CFF7CDBD9A5920"),
990
"CheckerRunning": False,
991
"LastEnabled": "2019-01-03T00:00:00",
992
"ApprovalPending": False,
993
"ApprovedByDefault": True,
994
"LastApprovalRequest": "",
996
"ApprovalDuration": 1000,
997
"Checker": "fping -q -- %(host)s",
998
"ExtendedTimeout": 900000,
999
"Expires": "2019-02-04T00:00:00",
1000
"LastCheckerStatus": 0,
1004
"KeyID": ("0558568eedd67d622f5c83b35a115f79"
1005
"6ab612cff5ad227247e46c2b020f441c"),
1006
"Host": "192.0.2.3",
1009
"LastCheckedOK": "2019-02-04T00:00:00",
1010
"Created": "2019-01-03T00:00:00",
1012
"Fingerprint": ("3E393AEAEFB84C7E89E2"
1013
"F547B3A107558FCA3A27"),
1014
"CheckerRunning": True,
1015
"LastEnabled": "2019-01-04T00:00:00",
1016
"ApprovalPending": False,
1017
"ApprovedByDefault": False,
1018
"LastApprovalRequest": "2019-01-03T00:00:00",
1019
"ApprovalDelay": 30000,
1020
"ApprovalDuration": 1000,
1022
"ExtendedTimeout": 900000,
1023
"Expires": "2019-02-05T00:00:00",
1024
"LastCheckerStatus": -2,
618
self.keywords = ["Attr1", "AttrTwo"]
624
"Bool": dbus.Boolean(False),
625
"NonDbusBoolean": False,
629
"ApprovalDelay": 2000,
630
"ApprovalDuration": 3000,
631
"ExtendedTimeout": 4000,
638
"Bool": dbus.Boolean(True),
639
"NonDbusBoolean": True,
642
"Interval": 93786000,
643
"ApprovalDelay": 93787000,
644
"ApprovalDuration": 93788000,
645
"ExtendedTimeout": 93789000,
646
"String": "A huge string which will not fit," * 10,
649
def test_short_header(self):
650
text = str(TableOfClients(self.clients, self.keywords,
657
self.assertEqual(text, expected_text)
658
def test_booleans(self):
659
keywords = ["Bool", "NonDbusBoolean"]
660
text = str(TableOfClients(self.clients, keywords,
663
A D-BUS Boolean A Non-D-BUS Boolean
667
self.assertEqual(text, expected_text)
668
def test_milliseconds_detection(self):
669
keywords = ["Integer", "Timeout", "Interval", "ApprovalDelay",
670
"ApprovalDuration", "ExtendedTimeout"]
671
text = str(TableOfClients(self.clients, keywords,
674
An Integer Timedelta 1 Timedelta 2 Timedelta 3 Timedelta 4 Timedelta 5
675
0 00:00:00 00:00:01 00:00:02 00:00:03 00:00:04
676
1 1T02:03:05 1T02:03:06 1T02:03:07 1T02:03:08 1T02:03:09
678
self.assertEqual(text, expected_text)
679
def test_empty_and_long_string_values(self):
680
keywords = ["String"]
681
text = str(TableOfClients(self.clients, keywords,
686
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,
688
self.assertEqual(text, expected_text)
1027
return super(TestDumpJSONCmd, self).setUp()
1028
def test_normal(self):
1029
json_data = json.loads(DumpJSONCmd().output(self.clients))
1030
self.assertDictEqual(json_data, self.expected_json)
1031
def test_one_client(self):
1032
clients = self.one_client
1033
json_data = json.loads(DumpJSONCmd().output(clients))
1034
expected_json = {"foo": self.expected_json["foo"]}
1035
self.assertDictEqual(json_data, expected_json)
1037
class TestIsEnabledCmd(TestCmd):
1038
def test_is_enabled(self):
1039
self.assertTrue(all(IsEnabledCmd().is_enabled(client,
1041
for client, properties
1042
in self.clients.items()))
1043
def test_is_enabled_run_exits_successfully(self):
1044
with self.assertRaises(SystemExit) as e:
1045
IsEnabledCmd().run(self.one_client)
1046
if e.exception.code is not None:
1047
self.assertEqual(e.exception.code, 0)
1049
self.assertIsNone(e.exception.code)
1050
def test_is_enabled_run_exits_with_failure(self):
1051
self.client.attributes["Enabled"] = dbus.Boolean(False)
1052
with self.assertRaises(SystemExit) as e:
1053
IsEnabledCmd().run(self.one_client)
1054
if isinstance(e.exception.code, int):
1055
self.assertNotEqual(e.exception.code, 0)
1057
self.assertIsNotNone(e.exception.code)
1059
class TestRemoveCmd(TestCmd):
1060
def test_remove(self):
1061
class MockMandos(object):
1064
def RemoveClient(self, dbus_path):
1065
self.calls.append(("RemoveClient", (dbus_path,)))
1066
mandos = MockMandos()
1067
super(TestRemoveCmd, self).setUp()
1068
RemoveCmd().run(self.clients, self.bus, mandos)
1069
self.assertEqual(len(mandos.calls), 2)
1070
for clientpath in self.clients:
1071
self.assertIn(("RemoveClient", (clientpath,)),
1074
class TestApproveCmd(TestCmd):
1075
def test_approve(self):
1076
ApproveCmd().run(self.clients, self.bus)
1077
for clientpath in self.clients:
1078
client = self.bus.get_object(dbus_busname, clientpath)
1079
self.assertIn(("Approve", (True, client_dbus_interface)),
1082
class TestDenyCmd(TestCmd):
1083
def test_deny(self):
1084
DenyCmd().run(self.clients, self.bus)
1085
for clientpath in self.clients:
1086
client = self.bus.get_object(dbus_busname, clientpath)
1087
self.assertIn(("Approve", (False, client_dbus_interface)),
1090
class TestEnableCmd(TestCmd):
1091
def test_enable(self):
1092
for clientpath in self.clients:
1093
client = self.bus.get_object(dbus_busname, clientpath)
1094
client.attributes["Enabled"] = False
1096
EnableCmd().run(self.clients, self.bus)
1098
for clientpath in self.clients:
1099
client = self.bus.get_object(dbus_busname, clientpath)
1100
self.assertTrue(client.attributes["Enabled"])
1102
class TestDisableCmd(TestCmd):
1103
def test_disable(self):
1104
DisableCmd().run(self.clients, self.bus)
1105
for clientpath in self.clients:
1106
client = self.bus.get_object(dbus_busname, clientpath)
1107
self.assertFalse(client.attributes["Enabled"])
1109
class Unique(object):
1110
"""Class for objects which exist only to be unique objects, since
1111
unittest.mock.sentinel only exists in Python 3.3"""
1113
class TestPropertyCmd(TestCmd):
1114
"""Abstract class for tests of PropertyCmd classes"""
1116
if not hasattr(self, "command"):
1118
values_to_get = getattr(self, "values_to_get",
1120
for value_to_set, value_to_get in zip(self.values_to_set,
1122
for clientpath in self.clients:
1123
client = self.bus.get_object(dbus_busname, clientpath)
1124
old_value = client.attributes[self.propname]
1125
self.assertNotIsInstance(old_value, Unique)
1126
client.attributes[self.propname] = Unique()
1127
self.run_command(value_to_set, self.clients)
1128
for clientpath in self.clients:
1129
client = self.bus.get_object(dbus_busname, clientpath)
1130
value = client.attributes[self.propname]
1131
self.assertNotIsInstance(value, Unique)
1132
self.assertEqual(value, value_to_get)
1133
def run_command(self, value, clients):
1134
self.command().run(clients, self.bus)
1136
class TestBumpTimeoutCmd(TestPropertyCmd):
1137
command = BumpTimeoutCmd
1138
propname = "LastCheckedOK"
1139
values_to_set = [""]
1141
class TestStartCheckerCmd(TestPropertyCmd):
1142
command = StartCheckerCmd
1143
propname = "CheckerRunning"
1144
values_to_set = [dbus.Boolean(True)]
1146
class TestStopCheckerCmd(TestPropertyCmd):
1147
command = StopCheckerCmd
1148
propname = "CheckerRunning"
1149
values_to_set = [dbus.Boolean(False)]
1151
class TestApproveByDefaultCmd(TestPropertyCmd):
1152
command = ApproveByDefaultCmd
1153
propname = "ApprovedByDefault"
1154
values_to_set = [dbus.Boolean(True)]
1156
class TestDenyByDefaultCmd(TestPropertyCmd):
1157
command = DenyByDefaultCmd
1158
propname = "ApprovedByDefault"
1159
values_to_set = [dbus.Boolean(False)]
1161
class TestPropertyValueCmd(TestPropertyCmd):
1162
"""Abstract class for tests of PropertyValueCmd classes"""
1164
if type(self) is TestPropertyValueCmd:
1166
return super(TestPropertyValueCmd, self).runTest()
1167
def run_command(self, value, clients):
1168
self.command(value).run(clients, self.bus)
1170
class TestSetCheckerCmd(TestPropertyValueCmd):
1171
command = SetCheckerCmd
1172
propname = "Checker"
1173
values_to_set = ["", ":", "fping -q -- %s"]
1175
class TestSetHostCmd(TestPropertyValueCmd):
1176
command = SetHostCmd
1178
values_to_set = ["192.0.2.3", "foo.example.org"]
1180
class TestSetSecretCmd(TestPropertyValueCmd):
1181
command = SetSecretCmd
1183
values_to_set = [io.BytesIO(b""),
1184
io.BytesIO(b"secret\0xyzzy\nbar")]
1185
values_to_get = [b"", b"secret\0xyzzy\nbar"]
1187
class TestSetTimeoutCmd(TestPropertyValueCmd):
1188
command = SetTimeoutCmd
1189
propname = "Timeout"
1190
values_to_set = [datetime.timedelta(),
1191
datetime.timedelta(minutes=5),
1192
datetime.timedelta(seconds=1),
1193
datetime.timedelta(weeks=1),
1194
datetime.timedelta(weeks=52)]
1195
values_to_get = [0, 300000, 1000, 604800000, 31449600000]
1197
class TestSetExtendedTimeoutCmd(TestPropertyValueCmd):
1198
command = SetExtendedTimeoutCmd
1199
propname = "ExtendedTimeout"
1200
values_to_set = [datetime.timedelta(),
1201
datetime.timedelta(minutes=5),
1202
datetime.timedelta(seconds=1),
1203
datetime.timedelta(weeks=1),
1204
datetime.timedelta(weeks=52)]
1205
values_to_get = [0, 300000, 1000, 604800000, 31449600000]
1207
class TestSetIntervalCmd(TestPropertyValueCmd):
1208
command = SetIntervalCmd
1209
propname = "Interval"
1210
values_to_set = [datetime.timedelta(),
1211
datetime.timedelta(minutes=5),
1212
datetime.timedelta(seconds=1),
1213
datetime.timedelta(weeks=1),
1214
datetime.timedelta(weeks=52)]
1215
values_to_get = [0, 300000, 1000, 604800000, 31449600000]
1217
class TestSetApprovalDelayCmd(TestPropertyValueCmd):
1218
command = SetApprovalDelayCmd
1219
propname = "ApprovalDelay"
1220
values_to_set = [datetime.timedelta(),
1221
datetime.timedelta(minutes=5),
1222
datetime.timedelta(seconds=1),
1223
datetime.timedelta(weeks=1),
1224
datetime.timedelta(weeks=52)]
1225
values_to_get = [0, 300000, 1000, 604800000, 31449600000]
1227
class TestSetApprovalDurationCmd(TestPropertyValueCmd):
1228
command = SetApprovalDurationCmd
1229
propname = "ApprovalDuration"
1230
values_to_set = [datetime.timedelta(),
1231
datetime.timedelta(minutes=5),
1232
datetime.timedelta(seconds=1),
1233
datetime.timedelta(weeks=1),
1234
datetime.timedelta(weeks=52)]
1235
values_to_get = [0, 300000, 1000, 604800000, 31449600000]
1237
class Test_command_from_options(unittest.TestCase):
1239
self.parser = argparse.ArgumentParser()
1240
add_command_line_options(self.parser)
1241
def assert_command_from_args(self, args, command_cls,
1243
"""Assert that parsing ARGS should result in an instance of
1244
COMMAND_CLS with (optionally) all supplied attributes (CMD_ATTRS)."""
1245
options = self.parser.parse_args(args)
1246
check_option_syntax(self.parser, options)
1247
commands = commands_from_options(options)
1248
self.assertEqual(len(commands), 1)
1249
command = commands[0]
1250
self.assertIsInstance(command, command_cls)
1251
for key, value in cmd_attrs.items():
1252
self.assertEqual(getattr(command, key), value)
1253
def test_print_table(self):
1254
self.assert_command_from_args([], PrintTableCmd,
1257
def test_print_table_verbose(self):
1258
self.assert_command_from_args(["--verbose"], PrintTableCmd,
1261
def test_print_table_verbose_short(self):
1262
self.assert_command_from_args(["-v"], PrintTableCmd,
1265
def test_enable(self):
1266
self.assert_command_from_args(["--enable", "foo"], EnableCmd)
1268
def test_enable_short(self):
1269
self.assert_command_from_args(["-e", "foo"], EnableCmd)
1271
def test_disable(self):
1272
self.assert_command_from_args(["--disable", "foo"],
1275
def test_disable_short(self):
1276
self.assert_command_from_args(["-d", "foo"], DisableCmd)
1278
def test_bump_timeout(self):
1279
self.assert_command_from_args(["--bump-timeout", "foo"],
1282
def test_bump_timeout_short(self):
1283
self.assert_command_from_args(["-b", "foo"], BumpTimeoutCmd)
1285
def test_start_checker(self):
1286
self.assert_command_from_args(["--start-checker", "foo"],
1289
def test_stop_checker(self):
1290
self.assert_command_from_args(["--stop-checker", "foo"],
1293
def test_remove(self):
1294
self.assert_command_from_args(["--remove", "foo"],
1297
def test_remove_short(self):
1298
self.assert_command_from_args(["-r", "foo"], RemoveCmd)
1300
def test_checker(self):
1301
self.assert_command_from_args(["--checker", ":", "foo"],
1302
SetCheckerCmd, value_to_set=":")
1304
def test_checker_empty(self):
1305
self.assert_command_from_args(["--checker", "", "foo"],
1306
SetCheckerCmd, value_to_set="")
1308
def test_checker_short(self):
1309
self.assert_command_from_args(["-c", ":", "foo"],
1310
SetCheckerCmd, value_to_set=":")
1312
def test_timeout(self):
1313
self.assert_command_from_args(["--timeout", "PT5M", "foo"],
1315
value_to_set=300000)
1317
def test_timeout_short(self):
1318
self.assert_command_from_args(["-t", "PT5M", "foo"],
1320
value_to_set=300000)
1322
def test_extended_timeout(self):
1323
self.assert_command_from_args(["--extended-timeout", "PT15M",
1325
SetExtendedTimeoutCmd,
1326
value_to_set=900000)
1328
def test_interval(self):
1329
self.assert_command_from_args(["--interval", "PT2M", "foo"],
1331
value_to_set=120000)
1333
def test_interval_short(self):
1334
self.assert_command_from_args(["-i", "PT2M", "foo"],
1336
value_to_set=120000)
1338
def test_approve_by_default(self):
1339
self.assert_command_from_args(["--approve-by-default", "foo"],
1340
ApproveByDefaultCmd)
1342
def test_deny_by_default(self):
1343
self.assert_command_from_args(["--deny-by-default", "foo"],
1346
def test_approval_delay(self):
1347
self.assert_command_from_args(["--approval-delay", "PT30S",
1348
"foo"], SetApprovalDelayCmd,
1351
def test_approval_duration(self):
1352
self.assert_command_from_args(["--approval-duration", "PT1S",
1353
"foo"], SetApprovalDurationCmd,
1356
def test_host(self):
1357
self.assert_command_from_args(["--host", "foo.example.org",
1359
value_to_set="foo.example.org")
1361
def test_host_short(self):
1362
self.assert_command_from_args(["-H", "foo.example.org",
1364
value_to_set="foo.example.org")
1366
def test_secret_devnull(self):
1367
self.assert_command_from_args(["--secret", os.path.devnull,
1368
"foo"], SetSecretCmd,
1371
def test_secret_tempfile(self):
1372
with tempfile.NamedTemporaryFile(mode="r+b") as f:
1373
value = b"secret\0xyzzy\nbar"
1376
self.assert_command_from_args(["--secret", f.name,
1377
"foo"], SetSecretCmd,
1380
def test_secret_devnull_short(self):
1381
self.assert_command_from_args(["-s", os.path.devnull, "foo"],
1382
SetSecretCmd, value_to_set=b"")
1384
def test_secret_tempfile_short(self):
1385
with tempfile.NamedTemporaryFile(mode="r+b") as f:
1386
value = b"secret\0xyzzy\nbar"
1389
self.assert_command_from_args(["-s", f.name, "foo"],
1393
def test_approve(self):
1394
self.assert_command_from_args(["--approve", "foo"],
1397
def test_approve_short(self):
1398
self.assert_command_from_args(["-A", "foo"], ApproveCmd)
1400
def test_deny(self):
1401
self.assert_command_from_args(["--deny", "foo"], DenyCmd)
1403
def test_deny_short(self):
1404
self.assert_command_from_args(["-D", "foo"], DenyCmd)
1406
def test_dump_json(self):
1407
self.assert_command_from_args(["--dump-json"], DumpJSONCmd)
1409
def test_is_enabled(self):
1410
self.assert_command_from_args(["--is-enabled", "foo"],
1413
def test_is_enabled_short(self):
1414
self.assert_command_from_args(["-V", "foo"], IsEnabledCmd)
1416
def test_deny_before_remove(self):
1417
options = self.parser.parse_args(["--deny", "--remove",
1419
check_option_syntax(self.parser, options)
1420
commands = commands_from_options(options)
1421
self.assertEqual(len(commands), 2)
1422
self.assertIsInstance(commands[0], DenyCmd)
1423
self.assertIsInstance(commands[1], RemoveCmd)
1425
def test_deny_before_remove_reversed(self):
1426
options = self.parser.parse_args(["--remove", "--deny",
1428
check_option_syntax(self.parser, options)
1429
commands = commands_from_options(options)
1430
self.assertEqual(len(commands), 2)
1431
self.assertIsInstance(commands[0], DenyCmd)
1432
self.assertIsInstance(commands[1], RemoveCmd)
1435
class Test_check_option_syntax(unittest.TestCase):
1436
# This mostly corresponds to the definition from has_actions() in
1437
# check_option_syntax()
1439
# The actual values set here are not that important, but we do
1440
# at least stick to the correct types, even though they are
1444
"bump_timeout": True,
1445
"start_checker": True,
1446
"stop_checker": True,
1450
"timeout": datetime.timedelta(),
1451
"extended_timeout": datetime.timedelta(),
1452
"interval": datetime.timedelta(),
1453
"approved_by_default": True,
1454
"approval_delay": datetime.timedelta(),
1455
"approval_duration": datetime.timedelta(),
1457
"secret": io.BytesIO(b"x"),
1463
self.parser = argparse.ArgumentParser()
1464
add_command_line_options(self.parser)
1466
@contextlib.contextmanager
1467
def assertParseError(self):
1468
with self.assertRaises(SystemExit) as e:
1469
with self.temporarily_suppress_stderr():
1471
# Exit code from argparse is guaranteed to be "2". Reference:
1472
# https://docs.python.org/3/library
1473
# /argparse.html#exiting-methods
1474
self.assertEqual(e.exception.code, 2)
1477
@contextlib.contextmanager
1478
def temporarily_suppress_stderr():
1479
null = os.open(os.path.devnull, os.O_RDWR)
1480
stderrcopy = os.dup(sys.stderr.fileno())
1481
os.dup2(null, sys.stderr.fileno())
1487
os.dup2(stderrcopy, sys.stderr.fileno())
1488
os.close(stderrcopy)
1490
def check_option_syntax(self, options):
1491
check_option_syntax(self.parser, options)
1493
def test_actions_requires_client_or_all(self):
1494
for action, value in self.actions.items():
1495
options = self.parser.parse_args()
1496
setattr(options, action, value)
1497
with self.assertParseError():
1498
self.check_option_syntax(options)
1500
def test_actions_conflicts_with_verbose(self):
1501
for action, value in self.actions.items():
1502
options = self.parser.parse_args()
1503
setattr(options, action, value)
1504
options.verbose = True
1505
with self.assertParseError():
1506
self.check_option_syntax(options)
1508
def test_dump_json_conflicts_with_verbose(self):
1509
options = self.parser.parse_args()
1510
options.dump_json = True
1511
options.verbose = True
1512
with self.assertParseError():
1513
self.check_option_syntax(options)
1515
def test_dump_json_conflicts_with_action(self):
1516
for action, value in self.actions.items():
1517
options = self.parser.parse_args()
1518
setattr(options, action, value)
1519
options.dump_json = True
1520
with self.assertParseError():
1521
self.check_option_syntax(options)
1523
def test_all_can_not_be_alone(self):
1524
options = self.parser.parse_args()
1526
with self.assertParseError():
1527
self.check_option_syntax(options)
1529
def test_all_is_ok_with_any_action(self):
1530
for action, value in self.actions.items():
1531
options = self.parser.parse_args()
1532
setattr(options, action, value)
1534
self.check_option_syntax(options)
1536
def test_is_enabled_fails_without_client(self):
1537
options = self.parser.parse_args()
1538
options.is_enabled = True
1539
with self.assertParseError():
1540
self.check_option_syntax(options)
1542
def test_is_enabled_works_with_one_client(self):
1543
options = self.parser.parse_args()
1544
options.is_enabled = True
1545
options.client = ["foo"]
1546
self.check_option_syntax(options)
1548
def test_is_enabled_fails_with_two_clients(self):
1549
options = self.parser.parse_args()
1550
options.is_enabled = True
1551
options.client = ["foo", "barbar"]
1552
with self.assertParseError():
1553
self.check_option_syntax(options)
1555
def test_remove_can_only_be_combined_with_action_deny(self):
1556
for action, value in self.actions.items():
1557
if action in {"remove", "deny"}:
1559
options = self.parser.parse_args()
1560
setattr(options, action, value)
1562
options.remove = True
1563
with self.assertParseError():
1564
self.check_option_syntax(options)