2
# -*- mode: python; coding: utf-8; after-save-hook: (lambda () (let ((command (if (and (boundp 'tramp-file-name-structure) (string-match (car tramp-file-name-structure) (buffer-file-name))) (tramp-file-name-localname (tramp-dissect-file-name (buffer-file-name))) (buffer-file-name)))) (if (= (shell-command (format "%s --check" (shell-quote-argument command)) "*Test*") 0) (let ((w (get-buffer-window "*Test*"))) (if w (delete-window w)) (kill-buffer "*Test*")) (display-buffer "*Test*")))); -*-
4
# Mandos Monitor - Control and monitor the Mandos server
6
# Copyright © 2008-2019 Teddy Hogeborn
7
# Copyright © 2008-2019 Björn Påhlsson
9
# This file is part of Mandos.
11
# Mandos is free software: you can redistribute it and/or modify it
12
# under the terms of the GNU General Public License as published by
13
# the Free Software Foundation, either version 3 of the License, or
14
# (at your option) any later version.
16
# Mandos is distributed in the hope that it will be useful, but
17
# WITHOUT ANY WARRANTY; without even the implied warranty of
18
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
# GNU General Public License for more details.
21
# You should have received a copy of the GNU General Public License
22
# along with Mandos. If not, see <http://www.gnu.org/licenses/>.
24
# Contact the authors at <mandos@recompile.se>.
27
from __future__ import (division, absolute_import, print_function,
31
from future_builtins import *
48
# Show warnings by default
49
if not sys.warnoptions:
51
warnings.simplefilter("default")
53
log = logging.getLogger(sys.argv[0])
54
logging.basicConfig(level="INFO", # Show info level messages
55
format="%(message)s") # Show basic log messages
57
logging.captureWarnings(True) # Show warnings via the logging system
59
if sys.version_info.major == 2:
62
locale.setlocale(locale.LC_ALL, "")
64
domain = "se.recompile"
65
busname = domain + ".Mandos"
67
server_interface = domain + ".Mandos"
68
client_interface = domain + ".Mandos.Client"
73
dbus.OBJECT_MANAGER_IFACE
74
except AttributeError:
75
dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
78
def milliseconds_to_string(ms):
79
td = datetime.timedelta(0, 0, 0, ms)
80
return ("{days}{hours:02}:{minutes:02}:{seconds:02}"
81
.format(days="{}T".format(td.days) if td.days else "",
82
hours=td.seconds // 3600,
83
minutes=(td.seconds % 3600) // 60,
84
seconds=td.seconds % 60))
87
def rfc3339_duration_to_delta(duration):
88
"""Parse an RFC 3339 "duration" and return a datetime.timedelta
90
>>> rfc3339_duration_to_delta("P7D")
92
>>> rfc3339_duration_to_delta("PT60S")
93
datetime.timedelta(0, 60)
94
>>> rfc3339_duration_to_delta("PT60M")
95
datetime.timedelta(0, 3600)
96
>>> rfc3339_duration_to_delta("P60M")
97
datetime.timedelta(1680)
98
>>> rfc3339_duration_to_delta("PT24H")
100
>>> rfc3339_duration_to_delta("P1W")
101
datetime.timedelta(7)
102
>>> rfc3339_duration_to_delta("PT5M30S")
103
datetime.timedelta(0, 330)
104
>>> rfc3339_duration_to_delta("P1DT3M20S")
105
datetime.timedelta(1, 200)
106
>>> # Can not be empty:
107
>>> rfc3339_duration_to_delta("")
108
Traceback (most recent call last):
110
ValueError: Invalid RFC 3339 duration: u''
111
>>> # Must start with "P":
112
>>> rfc3339_duration_to_delta("1D")
113
Traceback (most recent call last):
115
ValueError: Invalid RFC 3339 duration: u'1D'
116
>>> # Must use correct order
117
>>> rfc3339_duration_to_delta("PT1S2M")
118
Traceback (most recent call last):
120
ValueError: Invalid RFC 3339 duration: u'PT1S2M'
121
>>> # Time needs time marker
122
>>> rfc3339_duration_to_delta("P1H2S")
123
Traceback (most recent call last):
125
ValueError: Invalid RFC 3339 duration: u'P1H2S'
126
>>> # Weeks can not be combined with anything else
127
>>> rfc3339_duration_to_delta("P1D2W")
128
Traceback (most recent call last):
130
ValueError: Invalid RFC 3339 duration: u'P1D2W'
131
>>> rfc3339_duration_to_delta("P2W2H")
132
Traceback (most recent call last):
134
ValueError: Invalid RFC 3339 duration: u'P2W2H'
137
# Parsing an RFC 3339 duration with regular expressions is not
138
# possible - there would have to be multiple places for the same
139
# values, like seconds. The current code, while more esoteric, is
140
# cleaner without depending on a parsing library. If Python had a
141
# built-in library for parsing we would use it, but we'd like to
142
# avoid excessive use of external libraries.
144
# New type for defining tokens, syntax, and semantics all-in-one
145
Token = collections.namedtuple("Token", (
146
"regexp", # To match token; if "value" is not None, must have
147
# a "group" containing digits
148
"value", # datetime.timedelta or None
149
"followers")) # Tokens valid after this token
150
# RFC 3339 "duration" tokens, syntax, and semantics; taken from
151
# the "duration" ABNF definition in RFC 3339, Appendix A.
152
token_end = Token(re.compile(r"$"), None, frozenset())
153
token_second = Token(re.compile(r"(\d+)S"),
154
datetime.timedelta(seconds=1),
155
frozenset((token_end, )))
156
token_minute = Token(re.compile(r"(\d+)M"),
157
datetime.timedelta(minutes=1),
158
frozenset((token_second, token_end)))
159
token_hour = Token(re.compile(r"(\d+)H"),
160
datetime.timedelta(hours=1),
161
frozenset((token_minute, token_end)))
162
token_time = Token(re.compile(r"T"),
164
frozenset((token_hour, token_minute,
166
token_day = Token(re.compile(r"(\d+)D"),
167
datetime.timedelta(days=1),
168
frozenset((token_time, token_end)))
169
token_month = Token(re.compile(r"(\d+)M"),
170
datetime.timedelta(weeks=4),
171
frozenset((token_day, token_end)))
172
token_year = Token(re.compile(r"(\d+)Y"),
173
datetime.timedelta(weeks=52),
174
frozenset((token_month, token_end)))
175
token_week = Token(re.compile(r"(\d+)W"),
176
datetime.timedelta(weeks=1),
177
frozenset((token_end, )))
178
token_duration = Token(re.compile(r"P"), None,
179
frozenset((token_year, token_month,
180
token_day, token_time,
182
# Define starting values:
184
value = datetime.timedelta()
186
# Following valid tokens
187
followers = frozenset((token_duration, ))
188
# String left to parse
190
# Loop until end token is found
191
while found_token is not token_end:
192
# Search for any currently valid tokens
193
for token in followers:
194
match = token.regexp.match(s)
195
if match is not None:
197
if token.value is not None:
198
# Value found, parse digits
199
factor = int(match.group(1), 10)
200
# Add to value so far
201
value += factor * token.value
202
# Strip token from string
203
s = token.regexp.sub("", s, 1)
206
# Set valid next tokens
207
followers = found_token.followers
210
# No currently valid tokens were found
211
raise ValueError("Invalid RFC 3339 duration: {!r}"
217
def string_to_delta(interval):
218
"""Parse a string and return a datetime.timedelta"""
221
return rfc3339_duration_to_delta(interval)
222
except ValueError as e:
223
log.warning("%s - Parsing as pre-1.6.1 interval instead",
225
return parse_pre_1_6_1_interval(interval)
228
def parse_pre_1_6_1_interval(interval):
229
"""Parse an interval string as documented by Mandos before 1.6.1,
230
and return a datetime.timedelta
232
>>> parse_pre_1_6_1_interval('7d')
233
datetime.timedelta(7)
234
>>> parse_pre_1_6_1_interval('60s')
235
datetime.timedelta(0, 60)
236
>>> parse_pre_1_6_1_interval('60m')
237
datetime.timedelta(0, 3600)
238
>>> parse_pre_1_6_1_interval('24h')
239
datetime.timedelta(1)
240
>>> parse_pre_1_6_1_interval('1w')
241
datetime.timedelta(7)
242
>>> parse_pre_1_6_1_interval('5m 30s')
243
datetime.timedelta(0, 330)
244
>>> parse_pre_1_6_1_interval('')
245
datetime.timedelta(0)
246
>>> # Ignore unknown characters, allow any order and repetitions
247
>>> parse_pre_1_6_1_interval('2dxy7zz11y3m5m')
248
datetime.timedelta(2, 480, 18000)
252
value = datetime.timedelta(0)
253
regexp = re.compile(r"(\d+)([dsmhw]?)")
255
for num, suffix in regexp.findall(interval):
257
value += datetime.timedelta(int(num))
259
value += datetime.timedelta(0, int(num))
261
value += datetime.timedelta(0, 0, 0, 0, int(num))
263
value += datetime.timedelta(0, 0, 0, 0, 0, int(num))
265
value += datetime.timedelta(0, 0, 0, 0, 0, 0, int(num))
267
value += datetime.timedelta(0, 0, 0, int(num))
271
## Classes for commands.
273
# Abstract classes first
274
class Command(object):
275
"""Abstract class for commands"""
276
def run(self, mandos, clients):
277
"""Normal commands should implement run_on_one_client(), but
278
commands which want to operate on all clients at the same time
279
can override this run() method instead."""
281
for client in clients:
282
self.run_on_one_client(client)
284
class PrintCmd(Command):
285
"""Abstract class for commands printing client details"""
286
all_keywords = ("Name", "Enabled", "Timeout", "LastCheckedOK",
287
"Created", "Interval", "Host", "KeyID",
288
"Fingerprint", "CheckerRunning", "LastEnabled",
289
"ApprovalPending", "ApprovedByDefault",
290
"LastApprovalRequest", "ApprovalDelay",
291
"ApprovalDuration", "Checker", "ExtendedTimeout",
292
"Expires", "LastCheckerStatus")
293
def run(self, mandos, clients):
294
print(self.output(clients))
296
class PropertyCmd(Command):
297
"""Abstract class for Actions for setting one client property"""
298
def run_on_one_client(self, client):
299
"""Set the Client's D-Bus property"""
300
client.Set(client_interface, self.property, self.value_to_set,
301
dbus_interface=dbus.PROPERTIES_IFACE)
303
class ValueArgumentMixIn(object):
304
"""Mixin class for commands taking a value as argument"""
305
def __init__(self, value):
306
self.value_to_set = value
308
class MillisecondsValueArgumentMixIn(ValueArgumentMixIn):
309
"""Mixin class for commands taking a value argument as
312
def value_to_set(self):
315
def value_to_set(self, value):
316
"""When setting, convert value to a datetime.timedelta"""
317
self._vts = string_to_delta(value).total_seconds() * 1000
319
# Actual (non-abstract) command classes
321
class PrintTableCmd(PrintCmd):
322
def __init__(self, verbose=False):
323
self.verbose = verbose
325
def output(self, clients):
327
keywords = self.all_keywords
329
keywords = ("Name", "Enabled", "Timeout", "LastCheckedOK")
330
return str(self.TableOfClients(clients.values(), keywords))
332
class TableOfClients(object):
335
"Enabled": "Enabled",
336
"Timeout": "Timeout",
337
"LastCheckedOK": "Last Successful Check",
338
"LastApprovalRequest": "Last Approval Request",
339
"Created": "Created",
340
"Interval": "Interval",
342
"Fingerprint": "Fingerprint",
344
"CheckerRunning": "Check Is Running",
345
"LastEnabled": "Last Enabled",
346
"ApprovalPending": "Approval Is Pending",
347
"ApprovedByDefault": "Approved By Default",
348
"ApprovalDelay": "Approval Delay",
349
"ApprovalDuration": "Approval Duration",
350
"Checker": "Checker",
351
"ExtendedTimeout": "Extended Timeout",
352
"Expires": "Expires",
353
"LastCheckerStatus": "Last Checker Status",
356
def __init__(self, clients, keywords, tableheaders=None):
357
self.clients = clients
358
self.keywords = keywords
359
if tableheaders is not None:
360
self.tableheaders = tableheaders
363
return "\n".join(self.rows())
365
if sys.version_info.major == 2:
366
__unicode__ = __str__
368
return str(self).encode(locale.getpreferredencoding())
371
format_string = self.row_formatting_string()
372
rows = [self.header_line(format_string)]
373
rows.extend(self.client_line(client, format_string)
374
for client in self.clients)
377
def row_formatting_string(self):
378
"Format string used to format table rows"
379
return " ".join("{{{key}:{width}}}".format(
380
width=max(len(self.tableheaders[key]),
381
*(len(self.string_from_client(client, key))
382
for client in self.clients)),
384
for key in self.keywords)
386
def string_from_client(self, client, key):
387
return self.valuetostring(client[key], key)
390
def valuetostring(value, keyword):
391
if isinstance(value, dbus.Boolean):
392
return "Yes" if value else "No"
393
if keyword in ("Timeout", "Interval", "ApprovalDelay",
394
"ApprovalDuration", "ExtendedTimeout"):
395
return milliseconds_to_string(value)
398
def header_line(self, format_string):
399
return format_string.format(**self.tableheaders)
401
def client_line(self, client, format_string):
402
return format_string.format(
403
**{key: self.string_from_client(client, key)
404
for key in self.keywords})
408
class DumpJSONCmd(PrintCmd):
409
def output(self, clients):
410
data = {client["Name"]:
411
{key: self.dbus_boolean_to_bool(client[key])
412
for key in self.all_keywords}
413
for client in clients.values()}
414
return json.dumps(data, indent=4, separators=(',', ': '))
416
def dbus_boolean_to_bool(value):
417
if isinstance(value, dbus.Boolean):
421
class IsEnabledCmd(Command):
422
def run_on_one_client(self, client):
423
if self.is_enabled(client):
426
def is_enabled(self, client):
427
return client.Get(client_interface, "Enabled",
428
dbus_interface=dbus.PROPERTIES_IFACE)
430
class RemoveCmd(Command):
431
def run_on_one_client(self, client):
432
self.mandos.RemoveClient(client.__dbus_object_path__)
434
class ApproveCmd(Command):
435
def run_on_one_client(self, client):
436
client.Approve(dbus.Boolean(True),
437
dbus_interface=client_interface)
439
class DenyCmd(Command):
440
def run_on_one_client(self, client):
441
client.Approve(dbus.Boolean(False),
442
dbus_interface=client_interface)
444
class EnableCmd(PropertyCmd):
446
value_to_set = dbus.Boolean(True)
448
class DisableCmd(PropertyCmd):
450
value_to_set = dbus.Boolean(False)
452
class BumpTimeoutCmd(PropertyCmd):
453
property = "LastCheckedOK"
456
class StartCheckerCmd(PropertyCmd):
457
property = "CheckerRunning"
458
value_to_set = dbus.Boolean(True)
460
class StopCheckerCmd(PropertyCmd):
461
property = "CheckerRunning"
462
value_to_set = dbus.Boolean(False)
464
class ApproveByDefaultCmd(PropertyCmd):
465
property = "ApprovedByDefault"
466
value_to_set = dbus.Boolean(True)
468
class DenyByDefaultCmd(PropertyCmd):
469
property = "ApprovedByDefault"
470
value_to_set = dbus.Boolean(False)
472
class SetCheckerCmd(PropertyCmd, ValueArgumentMixIn):
475
class SetHostCmd(PropertyCmd, ValueArgumentMixIn):
478
class SetSecretCmd(PropertyCmd, ValueArgumentMixIn):
481
class SetTimeoutCmd(PropertyCmd, MillisecondsValueArgumentMixIn):
484
class SetExtendedTimeoutCmd(PropertyCmd,
485
MillisecondsValueArgumentMixIn):
486
property = "ExtendedTimeout"
488
class SetIntervalCmd(PropertyCmd, MillisecondsValueArgumentMixIn):
489
property = "Interval"
491
class SetApprovalDelayCmd(PropertyCmd,
492
MillisecondsValueArgumentMixIn):
493
property = "ApprovalDelay"
495
class SetApprovalDurationCmd(PropertyCmd,
496
MillisecondsValueArgumentMixIn):
497
property = "ApprovalDuration"
499
def has_actions(options):
500
return any((options.enable,
502
options.bump_timeout,
503
options.start_checker,
504
options.stop_checker,
507
options.checker is not None,
508
options.timeout is not None,
509
options.extended_timeout is not None,
510
options.interval is not None,
511
options.approved_by_default is not None,
512
options.approval_delay is not None,
513
options.approval_duration is not None,
514
options.host is not None,
515
options.secret is not None,
519
def add_command_line_options(parser):
520
parser.add_argument("--version", action="version",
521
version="%(prog)s {}".format(version),
522
help="show version number and exit")
523
parser.add_argument("-a", "--all", action="store_true",
524
help="Select all clients")
525
parser.add_argument("-v", "--verbose", action="store_true",
526
help="Print all fields")
527
parser.add_argument("-j", "--dump-json", action="store_true",
528
help="Dump client data in JSON format")
529
enable_disable = parser.add_mutually_exclusive_group()
530
enable_disable.add_argument("-e", "--enable", action="store_true",
531
help="Enable client")
532
enable_disable.add_argument("-d", "--disable",
534
help="disable client")
535
parser.add_argument("-b", "--bump-timeout", action="store_true",
536
help="Bump timeout for client")
537
start_stop_checker = parser.add_mutually_exclusive_group()
538
start_stop_checker.add_argument("--start-checker",
540
help="Start checker for client")
541
start_stop_checker.add_argument("--stop-checker",
543
help="Stop checker for client")
544
parser.add_argument("-V", "--is-enabled", action="store_true",
545
help="Check if client is enabled")
546
parser.add_argument("-r", "--remove", action="store_true",
547
help="Remove client")
548
parser.add_argument("-c", "--checker",
549
help="Set checker command for client")
550
parser.add_argument("-t", "--timeout",
551
help="Set timeout for client")
552
parser.add_argument("--extended-timeout",
553
help="Set extended timeout for client")
554
parser.add_argument("-i", "--interval",
555
help="Set checker interval for client")
556
approve_deny_default = parser.add_mutually_exclusive_group()
557
approve_deny_default.add_argument(
558
"--approve-by-default", action="store_true",
559
default=None, dest="approved_by_default",
560
help="Set client to be approved by default")
561
approve_deny_default.add_argument(
562
"--deny-by-default", action="store_false",
563
dest="approved_by_default",
564
help="Set client to be denied by default")
565
parser.add_argument("--approval-delay",
566
help="Set delay before client approve/deny")
567
parser.add_argument("--approval-duration",
568
help="Set duration of one client approval")
569
parser.add_argument("-H", "--host", help="Set host for client")
570
parser.add_argument("-s", "--secret",
571
type=argparse.FileType(mode="rb"),
572
help="Set password blob (file) for client")
573
approve_deny = parser.add_mutually_exclusive_group()
574
approve_deny.add_argument(
575
"-A", "--approve", action="store_true",
576
help="Approve any current client request")
577
approve_deny.add_argument("-D", "--deny", action="store_true",
578
help="Deny any current client request")
579
parser.add_argument("--check", action="store_true",
580
help="Run self-test")
581
parser.add_argument("client", nargs="*", help="Client name")
584
def commands_and_clients_from_options(options):
588
if options.dump_json:
589
commands.append(DumpJSONCmd())
592
commands.append(EnableCmd())
595
commands.append(DisableCmd())
597
if options.bump_timeout:
598
commands.append(BumpTimeoutCmd(options.bump_timeout))
600
if options.start_checker:
601
commands.append(StartCheckerCmd())
603
if options.stop_checker:
604
commands.append(StopCheckerCmd())
606
if options.is_enabled:
607
commands.append(IsEnabledCmd())
610
commands.append(RemoveCmd())
612
if options.checker is not None:
613
commands.append(SetCheckerCmd())
615
if options.timeout is not None:
616
commands.append(SetTimeoutCmd(options.timeout))
618
if options.extended_timeout:
620
SetExtendedTimeoutCmd(options.extended_timeout))
622
if options.interval is not None:
623
command.append(SetIntervalCmd(options.interval))
625
if options.approved_by_default is not None:
626
if options.approved_by_default:
627
command.append(ApproveByDefaultCmd())
629
command.append(DenyByDefaultCmd())
631
if options.approval_delay is not None:
632
command.append(SetApprovalDelayCmd(options.approval_delay))
634
if options.approval_duration is not None:
636
SetApprovalDurationCmd(options.approval_duration))
638
if options.host is not None:
639
command.append(SetHostCmd(options.host))
641
if options.secret is not None:
642
command.append(SetSecretCmd(options.secret))
645
commands.append(ApproveCmd())
648
commands.append(DenyCmd())
650
# If no command option has been given, show table of clients,
651
# optionally verbosely
653
commands.append(PrintTableCmd(verbose=options.verbose))
655
return commands, options.client
659
parser = argparse.ArgumentParser()
661
add_command_line_options(parser)
663
options = parser.parse_args()
665
if has_actions(options) and not (options.client or options.all):
666
parser.error("Options require clients names or --all.")
667
if options.verbose and has_actions(options):
668
parser.error("--verbose can only be used alone.")
669
if options.dump_json and (options.verbose
670
or has_actions(options)):
671
parser.error("--dump-json can only be used alone.")
672
if options.all and not has_actions(options):
673
parser.error("--all requires an action.")
674
if options.is_enabled and len(options.client) > 1:
675
parser.error("--is-enabled requires exactly one client")
677
commands, clientnames = commands_and_clients_from_options(options)
680
bus = dbus.SystemBus()
681
mandos_dbus_objc = bus.get_object(busname, server_path)
682
except dbus.exceptions.DBusException:
683
log.critical("Could not connect to Mandos server")
686
mandos_serv = dbus.Interface(mandos_dbus_objc,
687
dbus_interface=server_interface)
688
mandos_serv_object_manager = dbus.Interface(
689
mandos_dbus_objc, dbus_interface=dbus.OBJECT_MANAGER_IFACE)
691
# Filter out log message from dbus module
692
dbus_logger = logging.getLogger("dbus.proxies")
693
class NullFilter(logging.Filter):
694
def filter(self, record):
696
dbus_filter = NullFilter()
697
dbus_logger.addFilter(dbus_filter)
700
mandos_clients = {path: ifs_and_props[client_interface]
701
for path, ifs_and_props in
702
mandos_serv_object_manager
703
.GetManagedObjects().items()
704
if client_interface in ifs_and_props}
706
# restore dbus logger
707
dbus_logger.removeFilter(dbus_filter)
708
except dbus.exceptions.DBusException as e:
709
log.critical("Failed to access Mandos server through D-Bus:"
713
# Compile dict of (clients: properties) to process
717
clients = {bus.get_object(busname, path): properties
718
for path, properties in mandos_clients.items()}
720
for name in clientnames:
721
for path, client in mandos_clients.items():
722
if client["Name"] == name:
723
client_objc = bus.get_object(busname, path)
724
clients[client_objc] = client
727
log.critical("Client not found on server: %r", name)
730
# Run all commands on clients
731
for command in commands:
732
command.run(mandos_serv, clients)
735
class Test_milliseconds_to_string(unittest.TestCase):
737
self.assertEqual(milliseconds_to_string(93785000),
739
def test_no_days(self):
740
self.assertEqual(milliseconds_to_string(7385000), "02:03:05")
741
def test_all_zero(self):
742
self.assertEqual(milliseconds_to_string(0), "00:00:00")
743
def test_no_fractional_seconds(self):
744
self.assertEqual(milliseconds_to_string(400), "00:00:00")
745
self.assertEqual(milliseconds_to_string(900), "00:00:00")
746
self.assertEqual(milliseconds_to_string(1900), "00:00:01")
748
class Test_string_to_delta(unittest.TestCase):
749
def test_handles_basic_rfc3339(self):
750
self.assertEqual(string_to_delta("PT2H"),
751
datetime.timedelta(0, 7200))
752
def test_falls_back_to_pre_1_6_1_with_warning(self):
753
# assertLogs only exists in Python 3.4
754
if hasattr(self, "assertLogs"):
755
with self.assertLogs(log, logging.WARNING):
756
value = string_to_delta("2h")
758
class WarningFilter(logging.Filter):
759
"""Don't show, but record the presence of, warnings"""
760
def filter(self, record):
761
is_warning = record.levelno >= logging.WARNING
762
self.found = is_warning or getattr(self, "found",
764
return not is_warning
765
warning_filter = WarningFilter()
766
log.addFilter(warning_filter)
768
value = string_to_delta("2h")
770
log.removeFilter(warning_filter)
771
self.assertTrue(getattr(warning_filter, "found", False))
772
self.assertEqual(value, datetime.timedelta(0, 7200))
775
class TestCmd(unittest.TestCase):
776
"""Abstract class for tests of command classes"""
779
class MockClient(object):
780
def __init__(self, name, **attributes):
781
self.__dbus_object_path__ = "objpath_{}".format(name)
782
self.attributes = attributes
783
self.attributes["Name"] = name
785
def Set(self, interface, property, value, dbus_interface):
786
testcase.assertEqual(interface, client_interface)
787
testcase.assertEqual(dbus_interface,
788
dbus.PROPERTIES_IFACE)
789
self.attributes[property] = value
790
self.calls.append(("Set", (interface, property, value,
792
def Get(self, interface, property, dbus_interface):
793
testcase.assertEqual(interface, client_interface)
794
testcase.assertEqual(dbus_interface,
795
dbus.PROPERTIES_IFACE)
796
self.calls.append(("Get", (interface, property,
798
return self.attributes[property]
799
def __getitem__(self, key):
800
return self.attributes[key]
801
def __setitem__(self, key, value):
802
self.attributes[key] = value
803
self.clients = collections.OrderedDict([
807
KeyID=("92ed150794387c03ce684574b1139a65"
808
"94a34f895daaaf09fd8ea90a27cddb12"),
810
Host="foo.example.org",
811
Enabled=dbus.Boolean(True),
813
LastCheckedOK="2019-02-03T00:00:00",
814
Created="2019-01-02T00:00:00",
816
Fingerprint=("778827225BA7DE539C5A"
817
"7CFA59CFF7CDBD9A5920"),
818
CheckerRunning=dbus.Boolean(False),
819
LastEnabled="2019-01-03T00:00:00",
820
ApprovalPending=dbus.Boolean(False),
821
ApprovedByDefault=dbus.Boolean(True),
822
LastApprovalRequest="",
824
ApprovalDuration=1000,
825
Checker="fping -q -- %(host)s",
826
ExtendedTimeout=900000,
827
Expires="2019-02-04T00:00:00",
828
LastCheckerStatus=0)),
832
KeyID=("0558568eedd67d622f5c83b35a115f79"
833
"6ab612cff5ad227247e46c2b020f441c"),
836
Enabled=dbus.Boolean(True),
838
LastCheckedOK="2019-02-04T00:00:00",
839
Created="2019-01-03T00:00:00",
841
Fingerprint=("3E393AEAEFB84C7E89E2"
842
"F547B3A107558FCA3A27"),
843
CheckerRunning=dbus.Boolean(True),
844
LastEnabled="2019-01-04T00:00:00",
845
ApprovalPending=dbus.Boolean(False),
846
ApprovedByDefault=dbus.Boolean(False),
847
LastApprovalRequest="2019-01-03T00:00:00",
849
ApprovalDuration=1000,
851
ExtendedTimeout=900000,
852
Expires="2019-02-05T00:00:00",
853
LastCheckerStatus=-2)),
856
class TestPrintTableCmd(TestCmd):
857
def test_normal(self):
858
output = PrintTableCmd().output(self.clients)
859
expected_output = """
860
Name Enabled Timeout Last Successful Check
861
foo Yes 00:05:00 2019-02-03T00:00:00
862
barbar Yes 00:05:00 2019-02-04T00:00:00
864
self.assertEqual(output, expected_output)
865
def test_verbose(self):
866
output = PrintTableCmd(verbose=True).output(self.clients)
867
expected_output = """
868
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
869
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
870
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
872
self.assertEqual(output, expected_output)
873
def test_one_client(self):
874
output = PrintTableCmd().output({"foo": self.clients["foo"]})
875
expected_output = """
876
Name Enabled Timeout Last Successful Check
877
foo Yes 00:05:00 2019-02-03T00:00:00
879
self.assertEqual(output, expected_output)
881
class TestDumpJSONCmd(TestCmd):
883
self.expected_json = {
886
"KeyID": ("92ed150794387c03ce684574b1139a65"
887
"94a34f895daaaf09fd8ea90a27cddb12"),
888
"Host": "foo.example.org",
891
"LastCheckedOK": "2019-02-03T00:00:00",
892
"Created": "2019-01-02T00:00:00",
894
"Fingerprint": ("778827225BA7DE539C5A"
895
"7CFA59CFF7CDBD9A5920"),
896
"CheckerRunning": False,
897
"LastEnabled": "2019-01-03T00:00:00",
898
"ApprovalPending": False,
899
"ApprovedByDefault": True,
900
"LastApprovalRequest": "",
902
"ApprovalDuration": 1000,
903
"Checker": "fping -q -- %(host)s",
904
"ExtendedTimeout": 900000,
905
"Expires": "2019-02-04T00:00:00",
906
"LastCheckerStatus": 0,
910
"KeyID": ("0558568eedd67d622f5c83b35a115f79"
911
"6ab612cff5ad227247e46c2b020f441c"),
915
"LastCheckedOK": "2019-02-04T00:00:00",
916
"Created": "2019-01-03T00:00:00",
918
"Fingerprint": ("3E393AEAEFB84C7E89E2"
919
"F547B3A107558FCA3A27"),
920
"CheckerRunning": True,
921
"LastEnabled": "2019-01-04T00:00:00",
922
"ApprovalPending": False,
923
"ApprovedByDefault": False,
924
"LastApprovalRequest": "2019-01-03T00:00:00",
925
"ApprovalDelay": 30000,
926
"ApprovalDuration": 1000,
928
"ExtendedTimeout": 900000,
929
"Expires": "2019-02-05T00:00:00",
930
"LastCheckerStatus": -2,
933
return super(TestDumpJSONCmd, self).setUp()
934
def test_normal(self):
935
json_data = json.loads(DumpJSONCmd().output(self.clients))
936
self.assertDictEqual(json_data, self.expected_json)
937
def test_one_client(self):
938
clients = {"foo": self.clients["foo"]}
939
json_data = json.loads(DumpJSONCmd().output(clients))
940
expected_json = {"foo": self.expected_json["foo"]}
941
self.assertDictEqual(json_data, expected_json)
943
class TestIsEnabledCmd(TestCmd):
944
def test_is_enabled(self):
945
self.assertTrue(all(IsEnabledCmd().is_enabled(client)
946
for client in self.clients.values()))
947
def test_is_enabled_does_get_attribute(self):
948
client = self.clients["foo"]
949
self.assertTrue(IsEnabledCmd().is_enabled(client))
950
self.assertListEqual(client.calls,
952
("se.recompile.Mandos.Client",
954
"org.freedesktop.DBus.Properties"))])
955
def test_is_enabled_run_exits_successfully(self):
956
client = self.clients["foo"]
957
with self.assertRaises(SystemExit) as e:
958
IsEnabledCmd().run_on_one_client(client)
959
if e.exception.code is not None:
960
self.assertEqual(e.exception.code, 0)
962
self.assertIsNone(e.exception.code)
963
def test_is_enabled_run_exits_with_failure(self):
964
client = self.clients["foo"]
965
client["Enabled"] = dbus.Boolean(False)
966
with self.assertRaises(SystemExit) as e:
967
IsEnabledCmd().run_on_one_client(client)
968
if isinstance(e.exception.code, int):
969
self.assertNotEqual(e.exception.code, 0)
971
self.assertIsNotNone(e.exception.code)
975
def should_only_run_tests():
976
parser = argparse.ArgumentParser(add_help=False)
977
parser.add_argument("--check", action='store_true')
978
args, unknown_args = parser.parse_known_args()
979
run_tests = args.check
981
# Remove --check argument from sys.argv
982
sys.argv[1:] = unknown_args
985
# Add all tests from doctest strings
986
def load_tests(loader, tests, none):
988
tests.addTests(doctest.DocTestSuite())
991
if __name__ == "__main__":
992
if should_only_run_tests():
993
# Call using ./tdd-python-script --check [--verbose]