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,
520
def commands_and_clients_from_options(args=None):
523
parser = argparse.ArgumentParser()
524
parser.add_argument("--version", action="version",
525
version="%(prog)s {}".format(version),
526
help="show version number and exit")
527
parser.add_argument("-a", "--all", action="store_true",
528
help="Select all clients")
529
parser.add_argument("-v", "--verbose", action="store_true",
530
help="Print all fields")
531
parser.add_argument("-j", "--dump-json", action="store_true",
532
help="Dump client data in JSON format")
533
enable_disable = parser.add_mutually_exclusive_group()
534
enable_disable.add_argument("-e", "--enable", action="store_true",
535
help="Enable client")
536
enable_disable.add_argument("-d", "--disable",
538
help="disable client")
539
parser.add_argument("-b", "--bump-timeout", action="store_true",
540
help="Bump timeout for client")
541
start_stop_checker = parser.add_mutually_exclusive_group()
542
start_stop_checker.add_argument("--start-checker",
544
help="Start checker for client")
545
start_stop_checker.add_argument("--stop-checker",
547
help="Stop checker for client")
548
parser.add_argument("-V", "--is-enabled", action="store_true",
549
help="Check if client is enabled")
550
parser.add_argument("-r", "--remove", action="store_true",
551
help="Remove client")
552
parser.add_argument("-c", "--checker",
553
help="Set checker command for client")
554
parser.add_argument("-t", "--timeout",
555
help="Set timeout for client")
556
parser.add_argument("--extended-timeout",
557
help="Set extended timeout for client")
558
parser.add_argument("-i", "--interval",
559
help="Set checker interval for client")
560
approve_deny_default = parser.add_mutually_exclusive_group()
561
approve_deny_default.add_argument(
562
"--approve-by-default", action="store_true",
563
default=None, dest="approved_by_default",
564
help="Set client to be approved by default")
565
approve_deny_default.add_argument(
566
"--deny-by-default", action="store_false",
567
dest="approved_by_default",
568
help="Set client to be denied by default")
569
parser.add_argument("--approval-delay",
570
help="Set delay before client approve/deny")
571
parser.add_argument("--approval-duration",
572
help="Set duration of one client approval")
573
parser.add_argument("-H", "--host", help="Set host for client")
574
parser.add_argument("-s", "--secret",
575
type=argparse.FileType(mode="rb"),
576
help="Set password blob (file) for client")
577
approve_deny = parser.add_mutually_exclusive_group()
578
approve_deny.add_argument(
579
"-A", "--approve", action="store_true",
580
help="Approve any current client request")
581
approve_deny.add_argument("-D", "--deny", action="store_true",
582
help="Deny any current client request")
583
parser.add_argument("--check", action="store_true",
584
help="Run self-test")
585
parser.add_argument("client", nargs="*", help="Client name")
586
options = parser.parse_args(args=args)
588
if has_actions(options) and not (options.client or options.all):
589
parser.error("Options require clients names or --all.")
590
if options.verbose and has_actions(options):
591
parser.error("--verbose can only be used alone.")
592
if options.dump_json and (options.verbose
593
or has_actions(options)):
594
parser.error("--dump-json can only be used alone.")
595
if options.all and not has_actions(options):
596
parser.error("--all requires an action.")
597
if options.is_enabled and len(options.client) > 1:
598
parser.error("--is-enabled requires exactly one client")
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(options.bump_timeout))
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())
624
commands.append(RemoveCmd())
626
if options.checker is not None:
627
commands.append(SetCheckerCmd())
629
if options.timeout is not None:
630
commands.append(SetTimeoutCmd(options.timeout))
632
if options.extended_timeout:
634
SetExtendedTimeoutCmd(options.extended_timeout))
636
if options.interval is not None:
637
command.append(SetIntervalCmd(options.interval))
639
if options.approved_by_default is not None:
640
if options.approved_by_default:
641
command.append(ApproveByDefaultCmd())
643
command.append(DenyByDefaultCmd())
645
if options.approval_delay is not None:
646
command.append(SetApprovalDelayCmd(options.approval_delay))
648
if options.approval_duration is not None:
650
SetApprovalDurationCmd(options.approval_duration))
652
if options.host is not None:
653
command.append(SetHostCmd(options.host))
655
if options.secret is not None:
656
command.append(SetSecretCmd(options.secret))
659
commands.append(ApproveCmd())
662
commands.append(DenyCmd())
664
# If no command option has been given, show table of clients,
665
# optionally verbosely
667
commands.append(PrintTableCmd(verbose=options.verbose))
669
return commands, options.client
673
commands, clientnames = commands_and_clients_from_options()
676
bus = dbus.SystemBus()
677
mandos_dbus_objc = bus.get_object(busname, server_path)
678
except dbus.exceptions.DBusException:
679
log.critical("Could not connect to Mandos server")
682
mandos_serv = dbus.Interface(mandos_dbus_objc,
683
dbus_interface=server_interface)
684
mandos_serv_object_manager = dbus.Interface(
685
mandos_dbus_objc, dbus_interface=dbus.OBJECT_MANAGER_IFACE)
687
# Filter out log message from dbus module
688
dbus_logger = logging.getLogger("dbus.proxies")
689
class NullFilter(logging.Filter):
690
def filter(self, record):
692
dbus_filter = NullFilter()
693
dbus_logger.addFilter(dbus_filter)
696
mandos_clients = {path: ifs_and_props[client_interface]
697
for path, ifs_and_props in
698
mandos_serv_object_manager
699
.GetManagedObjects().items()
700
if client_interface in ifs_and_props}
702
# restore dbus logger
703
dbus_logger.removeFilter(dbus_filter)
704
except dbus.exceptions.DBusException as e:
705
log.critical("Failed to access Mandos server through D-Bus:"
709
# Compile dict of (clients: properties) to process
713
clients = {bus.get_object(busname, path): properties
714
for path, properties in mandos_clients.items()}
716
for name in clientnames:
717
for path, client in mandos_clients.items():
718
if client["Name"] == name:
719
client_objc = bus.get_object(busname, path)
720
clients[client_objc] = client
723
log.critical("Client not found on server: %r", name)
726
# Run all commands on clients
727
for command in commands:
728
command.run(mandos_serv, clients)
731
class Test_milliseconds_to_string(unittest.TestCase):
733
self.assertEqual(milliseconds_to_string(93785000),
735
def test_no_days(self):
736
self.assertEqual(milliseconds_to_string(7385000), "02:03:05")
737
def test_all_zero(self):
738
self.assertEqual(milliseconds_to_string(0), "00:00:00")
739
def test_no_fractional_seconds(self):
740
self.assertEqual(milliseconds_to_string(400), "00:00:00")
741
self.assertEqual(milliseconds_to_string(900), "00:00:00")
742
self.assertEqual(milliseconds_to_string(1900), "00:00:01")
744
class Test_string_to_delta(unittest.TestCase):
745
def test_handles_basic_rfc3339(self):
746
self.assertEqual(string_to_delta("PT2H"),
747
datetime.timedelta(0, 7200))
748
def test_falls_back_to_pre_1_6_1_with_warning(self):
749
# assertLogs only exists in Python 3.4
750
if hasattr(self, "assertLogs"):
751
with self.assertLogs(log, logging.WARNING):
752
value = string_to_delta("2h")
754
class WarningFilter(logging.Filter):
755
"""Don't show, but record the presence of, warnings"""
756
def filter(self, record):
757
is_warning = record.levelno >= logging.WARNING
758
self.found = is_warning or getattr(self, "found",
760
return not is_warning
761
warning_filter = WarningFilter()
762
log.addFilter(warning_filter)
764
value = string_to_delta("2h")
766
log.removeFilter(warning_filter)
767
self.assertTrue(getattr(warning_filter, "found", False))
768
self.assertEqual(value, datetime.timedelta(0, 7200))
771
class TestCmd(unittest.TestCase):
772
"""Abstract class for tests of command classes"""
775
class MockClient(object):
776
def __init__(self, name, **attributes):
777
self.__dbus_object_path__ = "objpath_{}".format(name)
778
self.attributes = attributes
779
self.attributes["Name"] = name
781
def Set(self, interface, property, value, dbus_interface):
782
testcase.assertEqual(interface, client_interface)
783
testcase.assertEqual(dbus_interface,
784
dbus.PROPERTIES_IFACE)
785
self.attributes[property] = value
786
self.calls.append(("Set", (interface, property, value,
788
def Get(self, interface, property, dbus_interface):
789
testcase.assertEqual(interface, client_interface)
790
testcase.assertEqual(dbus_interface,
791
dbus.PROPERTIES_IFACE)
792
self.calls.append(("Get", (interface, property,
794
return self.attributes[property]
795
def __getitem__(self, key):
796
return self.attributes[key]
797
def __setitem__(self, key, value):
798
self.attributes[key] = value
799
self.clients = collections.OrderedDict([
803
KeyID=("92ed150794387c03ce684574b1139a65"
804
"94a34f895daaaf09fd8ea90a27cddb12"),
806
Host="foo.example.org",
807
Enabled=dbus.Boolean(True),
809
LastCheckedOK="2019-02-03T00:00:00",
810
Created="2019-01-02T00:00:00",
812
Fingerprint=("778827225BA7DE539C5A"
813
"7CFA59CFF7CDBD9A5920"),
814
CheckerRunning=dbus.Boolean(False),
815
LastEnabled="2019-01-03T00:00:00",
816
ApprovalPending=dbus.Boolean(False),
817
ApprovedByDefault=dbus.Boolean(True),
818
LastApprovalRequest="",
820
ApprovalDuration=1000,
821
Checker="fping -q -- %(host)s",
822
ExtendedTimeout=900000,
823
Expires="2019-02-04T00:00:00",
824
LastCheckerStatus=0)),
828
KeyID=("0558568eedd67d622f5c83b35a115f79"
829
"6ab612cff5ad227247e46c2b020f441c"),
832
Enabled=dbus.Boolean(True),
834
LastCheckedOK="2019-02-04T00:00:00",
835
Created="2019-01-03T00:00:00",
837
Fingerprint=("3E393AEAEFB84C7E89E2"
838
"F547B3A107558FCA3A27"),
839
CheckerRunning=dbus.Boolean(True),
840
LastEnabled="2019-01-04T00:00:00",
841
ApprovalPending=dbus.Boolean(False),
842
ApprovedByDefault=dbus.Boolean(False),
843
LastApprovalRequest="2019-01-03T00:00:00",
845
ApprovalDuration=1000,
847
ExtendedTimeout=900000,
848
Expires="2019-02-05T00:00:00",
849
LastCheckerStatus=-2)),
852
class TestPrintTableCmd(TestCmd):
853
def test_normal(self):
854
output = PrintTableCmd().output(self.clients)
855
expected_output = """
856
Name Enabled Timeout Last Successful Check
857
foo Yes 00:05:00 2019-02-03T00:00:00
858
barbar Yes 00:05:00 2019-02-04T00:00:00
860
self.assertEqual(output, expected_output)
861
def test_verbose(self):
862
output = PrintTableCmd(verbose=True).output(self.clients)
863
expected_output = """
864
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
865
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
866
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
868
self.assertEqual(output, expected_output)
869
def test_one_client(self):
870
output = PrintTableCmd().output({"foo": self.clients["foo"]})
871
expected_output = """
872
Name Enabled Timeout Last Successful Check
873
foo Yes 00:05:00 2019-02-03T00:00:00
875
self.assertEqual(output, expected_output)
877
class TestDumpJSONCmd(TestCmd):
879
self.expected_json = {
882
"KeyID": ("92ed150794387c03ce684574b1139a65"
883
"94a34f895daaaf09fd8ea90a27cddb12"),
884
"Host": "foo.example.org",
887
"LastCheckedOK": "2019-02-03T00:00:00",
888
"Created": "2019-01-02T00:00:00",
890
"Fingerprint": ("778827225BA7DE539C5A"
891
"7CFA59CFF7CDBD9A5920"),
892
"CheckerRunning": False,
893
"LastEnabled": "2019-01-03T00:00:00",
894
"ApprovalPending": False,
895
"ApprovedByDefault": True,
896
"LastApprovalRequest": "",
898
"ApprovalDuration": 1000,
899
"Checker": "fping -q -- %(host)s",
900
"ExtendedTimeout": 900000,
901
"Expires": "2019-02-04T00:00:00",
902
"LastCheckerStatus": 0,
906
"KeyID": ("0558568eedd67d622f5c83b35a115f79"
907
"6ab612cff5ad227247e46c2b020f441c"),
911
"LastCheckedOK": "2019-02-04T00:00:00",
912
"Created": "2019-01-03T00:00:00",
914
"Fingerprint": ("3E393AEAEFB84C7E89E2"
915
"F547B3A107558FCA3A27"),
916
"CheckerRunning": True,
917
"LastEnabled": "2019-01-04T00:00:00",
918
"ApprovalPending": False,
919
"ApprovedByDefault": False,
920
"LastApprovalRequest": "2019-01-03T00:00:00",
921
"ApprovalDelay": 30000,
922
"ApprovalDuration": 1000,
924
"ExtendedTimeout": 900000,
925
"Expires": "2019-02-05T00:00:00",
926
"LastCheckerStatus": -2,
929
return super(TestDumpJSONCmd, self).setUp()
930
def test_normal(self):
931
json_data = json.loads(DumpJSONCmd().output(self.clients))
932
self.assertDictEqual(json_data, self.expected_json)
933
def test_one_client(self):
934
clients = {"foo": self.clients["foo"]}
935
json_data = json.loads(DumpJSONCmd().output(clients))
936
expected_json = {"foo": self.expected_json["foo"]}
937
self.assertDictEqual(json_data, expected_json)
939
class TestIsEnabledCmd(TestCmd):
940
def test_is_enabled(self):
941
self.assertTrue(all(IsEnabledCmd().is_enabled(client)
942
for client in self.clients.values()))
943
def test_is_enabled_does_get_attribute(self):
944
client = self.clients["foo"]
945
self.assertTrue(IsEnabledCmd().is_enabled(client))
946
self.assertListEqual(client.calls,
948
("se.recompile.Mandos.Client",
950
"org.freedesktop.DBus.Properties"))])
951
def test_is_enabled_run_exits_successfully(self):
952
client = self.clients["foo"]
953
with self.assertRaises(SystemExit) as e:
954
IsEnabledCmd().run_on_one_client(client)
955
if e.exception.code is not None:
956
self.assertEqual(e.exception.code, 0)
958
self.assertIsNone(e.exception.code)
959
def test_is_enabled_run_exits_with_failure(self):
960
client = self.clients["foo"]
961
client["Enabled"] = dbus.Boolean(False)
962
with self.assertRaises(SystemExit) as e:
963
IsEnabledCmd().run_on_one_client(client)
964
if isinstance(e.exception.code, int):
965
self.assertNotEqual(e.exception.code, 0)
967
self.assertIsNotNone(e.exception.code)
971
def should_only_run_tests():
972
parser = argparse.ArgumentParser(add_help=False)
973
parser.add_argument("--check", action='store_true')
974
args, unknown_args = parser.parse_known_args()
975
run_tests = args.check
977
# Remove --check argument from sys.argv
978
sys.argv[1:] = unknown_args
981
# Add all tests from doctest strings
982
def load_tests(loader, tests, none):
984
tests.addTests(doctest.DocTestSuite())
987
if __name__ == "__main__":
988
if should_only_run_tests():
989
# Call using ./tdd-python-script --check [--verbose]