/mandos/release

To get this branch, use:
bzr branch http://bzr.recompile.se/loggerhead/mandos/release

« back to all changes in this revision

Viewing changes to mandos-ctl

  • Committer: Teddy Hogeborn
  • Date: 2019-02-28 18:48:33 UTC
  • mto: (237.7.594 trunk)
  • mto: This revision was merged to the branch mainline in revision 382.
  • Revision ID: teddy@recompile.se-20190228184833-nlg1xxlhnldso9ya
Make mandos-ctl use unittest instead of doctest module

* mandos-ctl: Before running main(), check if we should only run tests
              by seeing if the --check option was passed.
  (main): Don't look for the --check option.
  (should_only_run_tests): New.
  (load_tests): New.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#!/usr/bin/python
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*")))); -*-
 
2
# -*- mode: python; coding: utf-8 -*-
3
3
#
4
4
# Mandos Monitor - Control and monitor the Mandos server
5
5
#
41
41
import collections
42
42
import json
43
43
import unittest
44
 
import logging
45
44
 
46
45
import dbus
47
46
 
48
 
# Show warnings by default
49
 
if not sys.warnoptions:
50
 
    import warnings
51
 
    warnings.simplefilter("default")
52
 
 
53
 
log = logging.getLogger(sys.argv[0])
54
 
logging.basicConfig(level="INFO", # Show info level messages
55
 
                    format="%(message)s") # Show basic log messages
56
 
 
57
 
logging.captureWarnings(True)   # Show warnings via the logging system
58
 
 
59
47
if sys.version_info.major == 2:
60
48
    str = unicode
61
49
 
62
50
locale.setlocale(locale.LC_ALL, "")
63
51
 
 
52
tablewords = {
 
53
    "Name": "Name",
 
54
    "Enabled": "Enabled",
 
55
    "Timeout": "Timeout",
 
56
    "LastCheckedOK": "Last Successful Check",
 
57
    "LastApprovalRequest": "Last Approval Request",
 
58
    "Created": "Created",
 
59
    "Interval": "Interval",
 
60
    "Host": "Host",
 
61
    "Fingerprint": "Fingerprint",
 
62
    "KeyID": "Key ID",
 
63
    "CheckerRunning": "Check Is Running",
 
64
    "LastEnabled": "Last Enabled",
 
65
    "ApprovalPending": "Approval Is Pending",
 
66
    "ApprovedByDefault": "Approved By Default",
 
67
    "ApprovalDelay": "Approval Delay",
 
68
    "ApprovalDuration": "Approval Duration",
 
69
    "Checker": "Checker",
 
70
    "ExtendedTimeout": "Extended Timeout",
 
71
    "Expires": "Expires",
 
72
    "LastCheckerStatus": "Last Checker Status",
 
73
}
64
74
defaultkeywords = ("Name", "Enabled", "Timeout", "LastCheckedOK")
65
75
domain = "se.recompile"
66
76
busname = domain + ".Mandos"
94
104
    datetime.timedelta(0, 60)
95
105
    >>> rfc3339_duration_to_delta("PT60M")
96
106
    datetime.timedelta(0, 3600)
97
 
    >>> rfc3339_duration_to_delta("P60M")
98
 
    datetime.timedelta(1680)
99
107
    >>> rfc3339_duration_to_delta("PT24H")
100
108
    datetime.timedelta(1)
101
109
    >>> rfc3339_duration_to_delta("P1W")
104
112
    datetime.timedelta(0, 330)
105
113
    >>> rfc3339_duration_to_delta("P1DT3M20S")
106
114
    datetime.timedelta(1, 200)
107
 
    >>> # Can not be empty:
108
 
    >>> rfc3339_duration_to_delta("")
109
 
    Traceback (most recent call last):
110
 
    ...
111
 
    ValueError: Invalid RFC 3339 duration: u''
112
 
    >>> # Must start with "P":
113
 
    >>> rfc3339_duration_to_delta("1D")
114
 
    Traceback (most recent call last):
115
 
    ...
116
 
    ValueError: Invalid RFC 3339 duration: u'1D'
117
 
    >>> # Must use correct order
118
 
    >>> rfc3339_duration_to_delta("PT1S2M")
119
 
    Traceback (most recent call last):
120
 
    ...
121
 
    ValueError: Invalid RFC 3339 duration: u'PT1S2M'
122
 
    >>> # Time needs time marker
123
 
    >>> rfc3339_duration_to_delta("P1H2S")
124
 
    Traceback (most recent call last):
125
 
    ...
126
 
    ValueError: Invalid RFC 3339 duration: u'P1H2S'
127
 
    >>> # Weeks can not be combined with anything else
128
 
    >>> rfc3339_duration_to_delta("P1D2W")
129
 
    Traceback (most recent call last):
130
 
    ...
131
 
    ValueError: Invalid RFC 3339 duration: u'P1D2W'
132
 
    >>> rfc3339_duration_to_delta("P2W2H")
133
 
    Traceback (most recent call last):
134
 
    ...
135
 
    ValueError: Invalid RFC 3339 duration: u'P2W2H'
136
115
    """
137
116
 
138
117
    # Parsing an RFC 3339 duration with regular expressions is not
217
196
 
218
197
def string_to_delta(interval):
219
198
    """Parse a string and return a datetime.timedelta
 
199
 
 
200
    >>> string_to_delta('7d')
 
201
    datetime.timedelta(7)
 
202
    >>> string_to_delta('60s')
 
203
    datetime.timedelta(0, 60)
 
204
    >>> string_to_delta('60m')
 
205
    datetime.timedelta(0, 3600)
 
206
    >>> string_to_delta('24h')
 
207
    datetime.timedelta(1)
 
208
    >>> string_to_delta('1w')
 
209
    datetime.timedelta(7)
 
210
    >>> string_to_delta('5m 30s')
 
211
    datetime.timedelta(0, 330)
220
212
    """
221
213
 
222
214
    try:
223
215
        return rfc3339_duration_to_delta(interval)
224
 
    except ValueError as e:
225
 
        log.warning("%s - Parsing as pre-1.6.1 interval instead",
226
 
                    ' '.join(e.args))
227
 
    return parse_pre_1_6_1_interval(interval)
228
 
 
229
 
 
230
 
def parse_pre_1_6_1_interval(interval):
231
 
    """Parse an interval string as documented by Mandos before 1.6.1, and
232
 
    return a datetime.timedelta
233
 
    >>> parse_pre_1_6_1_interval('7d')
234
 
    datetime.timedelta(7)
235
 
    >>> parse_pre_1_6_1_interval('60s')
236
 
    datetime.timedelta(0, 60)
237
 
    >>> parse_pre_1_6_1_interval('60m')
238
 
    datetime.timedelta(0, 3600)
239
 
    >>> parse_pre_1_6_1_interval('24h')
240
 
    datetime.timedelta(1)
241
 
    >>> parse_pre_1_6_1_interval('1w')
242
 
    datetime.timedelta(7)
243
 
    >>> parse_pre_1_6_1_interval('5m 30s')
244
 
    datetime.timedelta(0, 330)
245
 
    >>> parse_pre_1_6_1_interval('')
246
 
    datetime.timedelta(0)
247
 
    >>> # Ignore unknown characters, allow any order and repetitions
248
 
    >>> parse_pre_1_6_1_interval('2dxy7zz11y3m5m')
249
 
    datetime.timedelta(2, 480, 18000)
250
 
 
251
 
    """
 
216
    except ValueError:
 
217
        pass
252
218
 
253
219
    value = datetime.timedelta(0)
254
220
    regexp = re.compile(r"(\d+)([dsmhw]?)")
269
235
    return value
270
236
 
271
237
 
272
 
class TableOfClients(object):
273
 
    tablewords = {
274
 
        "Name": "Name",
275
 
        "Enabled": "Enabled",
276
 
        "Timeout": "Timeout",
277
 
        "LastCheckedOK": "Last Successful Check",
278
 
        "LastApprovalRequest": "Last Approval Request",
279
 
        "Created": "Created",
280
 
        "Interval": "Interval",
281
 
        "Host": "Host",
282
 
        "Fingerprint": "Fingerprint",
283
 
        "KeyID": "Key ID",
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",
294
 
    }
295
 
 
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
301
 
 
302
 
    def __str__(self):
303
 
        return "\n".join(self.rows())
304
 
 
305
 
    if sys.version_info.major == 2:
306
 
        __unicode__ = __str__
307
 
        def __str__(self):
308
 
            return str(self).encode(locale.getpreferredencoding())
309
 
 
310
 
    def rows(self):
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)
315
 
        return rows
316
 
 
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)),
323
 
            key=key)
324
 
                                 for key in self.keywords)
325
 
 
326
 
    def string_from_client(self, client, key):
327
 
        return self.valuetostring(client[key], key)
328
 
 
329
 
    @staticmethod
 
238
def print_clients(clients, keywords):
330
239
    def valuetostring(value, keyword):
331
 
        if isinstance(value, dbus.Boolean):
 
240
        if type(value) is dbus.Boolean:
332
241
            return "Yes" if value else "No"
333
242
        if keyword in ("Timeout", "Interval", "ApprovalDelay",
334
243
                       "ApprovalDuration", "ExtendedTimeout"):
335
244
            return milliseconds_to_string(value)
336
245
        return str(value)
337
246
 
338
 
    def header_line(self, format_string):
339
 
        return format_string.format(**self.tablewords)
340
 
 
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})
 
247
    # Create format string to print table rows
 
248
    format_string = " ".join("{{{key}:{width}}}".format(
 
249
        width=max(len(tablewords[key]),
 
250
                  max(len(valuetostring(client[key], key))
 
251
                      for client in clients)),
 
252
        key=key)
 
253
                             for key in keywords)
 
254
    # Print header line
 
255
    print(format_string.format(**tablewords))
 
256
    for client in clients:
 
257
        print(format_string
 
258
              .format(**{key: valuetostring(client[key], key)
 
259
                         for key in keywords}))
345
260
 
346
261
 
347
262
def has_actions(options):
435
350
        bus = dbus.SystemBus()
436
351
        mandos_dbus_objc = bus.get_object(busname, server_path)
437
352
    except dbus.exceptions.DBusException:
438
 
        log.critical("Could not connect to Mandos server")
 
353
        print("Could not connect to Mandos server", file=sys.stderr)
439
354
        sys.exit(1)
440
355
 
441
356
    mandos_serv = dbus.Interface(mandos_dbus_objc,
460
375
            os.dup2(stderrcopy, sys.stderr.fileno())
461
376
            os.close(stderrcopy)
462
377
    except dbus.exceptions.DBusException as e:
463
 
        log.critical("Failed to access Mandos server through D-Bus:"
464
 
                     "\n%s", e)
 
378
        print("Access denied: "
 
379
              "Accessing mandos server through D-Bus: {}".format(e),
 
380
              file=sys.stderr)
465
381
        sys.exit(1)
466
382
 
467
383
    # Compile dict of (clients: properties) to process
478
394
                    clients[client_objc] = client
479
395
                    break
480
396
            else:
481
 
                log.critical("Client not found on server: %r", name)
 
397
                print("Client not found on server: {!r}"
 
398
                      .format(name), file=sys.stderr)
482
399
                sys.exit(1)
483
400
 
484
401
    if not has_actions(options) and clients:
506
423
                      separators=(',', ': '))
507
424
            print()
508
425
        else:
509
 
            print(TableOfClients(clients.values(), keywords))
 
426
            print_clients(clients.values(), keywords)
510
427
    else:
511
428
        # Process each client in the list by all selected options
512
429
        for client in clients:
573
490
                               dbus_interface=client_interface)
574
491
 
575
492
 
576
 
class Test_milliseconds_to_string(unittest.TestCase):
577
 
    def test_all(self):
578
 
        self.assertEqual(milliseconds_to_string(93785000),
579
 
                         "1T02:03:05")
580
 
    def test_no_days(self):
581
 
        self.assertEqual(milliseconds_to_string(7385000), "02:03:05")
582
 
    def test_all_zero(self):
583
 
        self.assertEqual(milliseconds_to_string(0), "00:00:00")
584
 
    def test_no_fractional_seconds(self):
585
 
        self.assertEqual(milliseconds_to_string(400), "00:00:00")
586
 
        self.assertEqual(milliseconds_to_string(900), "00:00:00")
587
 
        self.assertEqual(milliseconds_to_string(1900), "00:00:01")
588
 
 
589
 
class Test_string_to_delta(unittest.TestCase):
590
 
    def test_handles_basic_rfc3339(self):
591
 
        self.assertEqual(string_to_delta("PT2H"),
592
 
                         datetime.timedelta(0, 7200))
593
 
    def test_falls_back_to_pre_1_6_1_with_warning(self):
594
 
        # assertLogs only exists in Python 3.4
595
 
        if hasattr(self, "assertLogs"):
596
 
            with self.assertLogs(log, logging.WARNING):
597
 
                value = string_to_delta("2h")
598
 
        else:
599
 
            value = string_to_delta("2h")
600
 
        self.assertEqual(value, datetime.timedelta(0, 7200))
601
 
 
602
 
class Test_TableOfClients(unittest.TestCase):
603
 
    def setUp(self):
604
 
        self.tablewords = {
605
 
            "Attr1": "X",
606
 
            "AttrTwo": "Yy",
607
 
            "AttrThree": "Zzz",
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",
617
 
        }
618
 
        self.keywords = ["Attr1", "AttrTwo"]
619
 
        self.clients = [
620
 
            {
621
 
                "Attr1": "x1",
622
 
                "AttrTwo": "y1",
623
 
                "AttrThree": "z1",
624
 
                "Bool": dbus.Boolean(False),
625
 
                "NonDbusBoolean": False,
626
 
                "Integer": 0,
627
 
                "Timeout": 0,
628
 
                "Interval": 1000,
629
 
                "ApprovalDelay": 2000,
630
 
                "ApprovalDuration": 3000,
631
 
                "ExtendedTimeout": 4000,
632
 
                "String": "",
633
 
            },
634
 
            {
635
 
                "Attr1": "x2",
636
 
                "AttrTwo": "y2",
637
 
                "AttrThree": "z2",
638
 
                "Bool": dbus.Boolean(True),
639
 
                "NonDbusBoolean": True,
640
 
                "Integer": 1,
641
 
                "Timeout": 93785000,
642
 
                "Interval": 93786000,
643
 
                "ApprovalDelay": 93787000,
644
 
                "ApprovalDuration": 93788000,
645
 
                "ExtendedTimeout": 93789000,
646
 
                "String": "A huge string which will not fit," * 10,
647
 
            },
648
 
        ]
649
 
    def test_short_header(self):
650
 
        text = str(TableOfClients(self.clients, self.keywords,
651
 
                                  self.tablewords))
652
 
        expected_text = """
653
 
X  Yy
654
 
x1 y1
655
 
x2 y2
656
 
"""[1:-1]
657
 
        self.assertEqual(text, expected_text)
658
 
    def test_booleans(self):
659
 
        keywords = ["Bool", "NonDbusBoolean"]
660
 
        text = str(TableOfClients(self.clients, keywords,
661
 
                                  self.tablewords))
662
 
        expected_text = """
663
 
A D-BUS Boolean A Non-D-BUS Boolean
664
 
No              False              
665
 
Yes             True               
666
 
"""[1:-1]
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,
672
 
                                  self.tablewords))
673
 
        expected_text = """
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 
677
 
"""[1:-1]
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,
682
 
                                  self.tablewords))
683
 
        expected_text = """
684
 
A String                                                                                                                                                                                                                                                                                                                                  
685
 
                                                                                                                                                                                                                                                                                                                                          
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,
687
 
"""[1:-1]
688
 
        self.assertEqual(text, expected_text)
689
 
 
690
 
 
691
 
 
692
493
def should_only_run_tests():
693
494
    parser = argparse.ArgumentParser(add_help=False)
694
495
    parser.add_argument("--check", action='store_true')