/mandos/trunk

To get this branch, use:
bzr branch http://bzr.recompile.se/loggerhead/mandos/trunk
24.1.116 by Björn Påhlsson
added a mandos list client program
1
#!/usr/bin/python
985 by Teddy Hogeborn
Make Emacs run tests when mandos-ctl file is saved
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*")))); -*-
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
3
#
444 by Teddy Hogeborn
Update copyright year to "2010" wherever appropriate.
4
# Mandos Monitor - Control and monitor the Mandos server
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
5
#
969 by Teddy Hogeborn
Update copyright year to 2019
6
# Copyright © 2008-2019 Teddy Hogeborn
7
# Copyright © 2008-2019 Björn Påhlsson
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
8
#
907 by Teddy Hogeborn
Alter copyright notices slightly. Actual license is unchanged!
9
# This file is part of Mandos.
10
#
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
444 by Teddy Hogeborn
Update copyright year to "2010" wherever appropriate.
13
# the Free Software Foundation, either version 3 of the License, or
14
# (at your option) any later version.
15
#
907 by Teddy Hogeborn
Alter copyright notices slightly. Actual license is unchanged!
16
#     Mandos is distributed in the hope that it will be useful, but
17
#     WITHOUT ANY WARRANTY; without even the implied warranty of
444 by Teddy Hogeborn
Update copyright year to "2010" wherever appropriate.
18
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
#     GNU General Public License for more details.
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
20
#
444 by Teddy Hogeborn
Update copyright year to "2010" wherever appropriate.
21
# You should have received a copy of the GNU General Public License
907 by Teddy Hogeborn
Alter copyright notices slightly. Actual license is unchanged!
22
# along with Mandos.  If not, see <http://www.gnu.org/licenses/>.
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
23
#
505.1.2 by Teddy Hogeborn
Change "fukt.bsnet.se" to "recompile.se" throughout.
24
# Contact the authors at <mandos@recompile.se>.
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
25
#
24.1.116 by Björn Påhlsson
added a mandos list client program
26
463.1.9 by teddy at bsnet
* mandos-ctl: Use print function.
27
from __future__ import (division, absolute_import, print_function,
28
                        unicode_literals)
463.1.8 by teddy at bsnet
* mandos-ctl: Use unicode string literals.
29
718 by Teddy Hogeborn
mandos-ctl: Make it work in Python 3.
30
try:
31
    from future_builtins import *
32
except ImportError:
33
    pass
579 by Teddy Hogeborn
* mandos: Use all new builtins.
34
24.1.119 by Björn Påhlsson
Added more method support for mandos clients through mandos-ctl
35
import sys
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
36
import argparse
240 by Teddy Hogeborn
Merge "mandos-list" from belorn.
37
import locale
24.1.121 by Björn Påhlsson
mandos-ctl: Added support for all client calls
38
import datetime
39
import re
24.1.163 by Björn Påhlsson
mandos-client: Added never ending loop for --connect
40
import os
608 by Teddy Hogeborn
* Makefile (check): Also check mandos-ctl.
41
import collections
863 by Teddy Hogeborn
mandos-ctl: Implement --dump-json option
42
import json
984 by Teddy Hogeborn
Make mandos-ctl use unittest instead of doctest module
43
import unittest
987 by Teddy Hogeborn
mandos-ctl: Use logging module instead of print() for errors
44
import logging
608 by Teddy Hogeborn
* Makefile (check): Also check mandos-ctl.
45
46
import dbus
240 by Teddy Hogeborn
Merge "mandos-list" from belorn.
47
988 by Teddy Hogeborn
mandos-ctl: Show warnings
48
# Show warnings by default
49
if not sys.warnoptions:
50
    import warnings
51
    warnings.simplefilter("default")
52
987 by Teddy Hogeborn
mandos-ctl: Use logging module instead of print() for errors
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
988 by Teddy Hogeborn
mandos-ctl: Show warnings
57
logging.captureWarnings(True)   # Show warnings via the logging system
58
723.1.7 by Teddy Hogeborn
Use the .major attribute on sys.version_info instead of using "[0]".
59
if sys.version_info.major == 2:
718 by Teddy Hogeborn
mandos-ctl: Make it work in Python 3.
60
    str = unicode
61
463.1.8 by teddy at bsnet
* mandos-ctl: Use unicode string literals.
62
locale.setlocale(locale.LC_ALL, "")
24.1.116 by Björn Påhlsson
added a mandos list client program
63
24.1.186 by Björn Påhlsson
transitional stuff actually working
64
domain = "se.recompile"
463.1.8 by teddy at bsnet
* mandos-ctl: Use unicode string literals.
65
busname = domain + ".Mandos"
66
server_path = "/"
67
server_interface = domain + ".Mandos"
68
client_interface = domain + ".Mandos.Client"
237.4.108 by Teddy Hogeborn
* Makefile (version): Change to 1.8.3.
69
version = "1.8.3"
24.1.118 by Björn Påhlsson
Added enable/disable
70
745 by Teddy Hogeborn
mandos-ctl: Do minor formatting and whitespace adjustments.
71
785 by Teddy Hogeborn
Support the standard org.freedesktop.DBus.ObjectManager interface.
72
try:
73
    dbus.OBJECT_MANAGER_IFACE
74
except AttributeError:
75
    dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
76
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
77
24.1.121 by Björn Påhlsson
mandos-ctl: Added support for all client calls
78
def milliseconds_to_string(ms):
79
    td = datetime.timedelta(0, 0, 0, ms)
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
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))
24.1.121 by Björn Påhlsson
mandos-ctl: Added support for all client calls
85
608 by Teddy Hogeborn
* Makefile (check): Also check mandos-ctl.
86
87
def rfc3339_duration_to_delta(duration):
609 by Teddy Hogeborn
* clients.conf: Convert all time intervals to new RFC 3339 syntax.
88
    """Parse an RFC 3339 "duration" and return a datetime.timedelta
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
89
608 by Teddy Hogeborn
* Makefile (check): Also check mandos-ctl.
90
    >>> rfc3339_duration_to_delta("P7D")
91
    datetime.timedelta(7)
92
    >>> rfc3339_duration_to_delta("PT60S")
93
    datetime.timedelta(0, 60)
94
    >>> rfc3339_duration_to_delta("PT60M")
95
    datetime.timedelta(0, 3600)
990 by Teddy Hogeborn
mandos-ctl (rfc3339_duration_to_delta): Improve tests
96
    >>> rfc3339_duration_to_delta("P60M")
97
    datetime.timedelta(1680)
608 by Teddy Hogeborn
* Makefile (check): Also check mandos-ctl.
98
    >>> rfc3339_duration_to_delta("PT24H")
99
    datetime.timedelta(1)
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)
990 by Teddy Hogeborn
mandos-ctl (rfc3339_duration_to_delta): Improve tests
106
    >>> # Can not be empty:
107
    >>> rfc3339_duration_to_delta("")
108
    Traceback (most recent call last):
109
    ...
110
    ValueError: Invalid RFC 3339 duration: u''
111
    >>> # Must start with "P":
112
    >>> rfc3339_duration_to_delta("1D")
113
    Traceback (most recent call last):
114
    ...
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):
119
    ...
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):
124
    ...
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):
129
    ...
130
    ValueError: Invalid RFC 3339 duration: u'P1D2W'
131
    >>> rfc3339_duration_to_delta("P2W2H")
132
    Traceback (most recent call last):
133
    ...
134
    ValueError: Invalid RFC 3339 duration: u'P2W2H'
608 by Teddy Hogeborn
* Makefile (check): Also check mandos-ctl.
135
    """
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
136
609 by Teddy Hogeborn
* clients.conf: Convert all time intervals to new RFC 3339 syntax.
137
    # Parsing an RFC 3339 duration with regular expressions is not
608 by Teddy Hogeborn
* Makefile (check): Also check mandos-ctl.
138
    # possible - there would have to be multiple places for the same
609 by Teddy Hogeborn
* clients.conf: Convert all time intervals to new RFC 3339 syntax.
139
    # values, like seconds.  The current code, while more esoteric, is
140
    # cleaner without depending on a parsing library.  If Python had a
608 by Teddy Hogeborn
* Makefile (check): Also check mandos-ctl.
141
    # built-in library for parsing we would use it, but we'd like to
142
    # avoid excessive use of external libraries.
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
143
608 by Teddy Hogeborn
* Makefile (check): Also check mandos-ctl.
144
    # New type for defining tokens, syntax, and semantics all-in-one
753 by Teddy Hogeborn
mandos-ctl: Generate better messages in exceptions.
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
608 by Teddy Hogeborn
* Makefile (check): Also check mandos-ctl.
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),
745 by Teddy Hogeborn
mandos-ctl: Do minor formatting and whitespace adjustments.
155
                         frozenset((token_end, )))
608 by Teddy Hogeborn
* Makefile (check): Also check mandos-ctl.
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"),
163
                       None,
164
                       frozenset((token_hour, token_minute,
165
                                  token_second)))
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),
745 by Teddy Hogeborn
mandos-ctl: Do minor formatting and whitespace adjustments.
177
                       frozenset((token_end, )))
608 by Teddy Hogeborn
* Makefile (check): Also check mandos-ctl.
178
    token_duration = Token(re.compile(r"P"), None,
179
                           frozenset((token_year, token_month,
180
                                      token_day, token_time,
721 by Teddy Hogeborn
Fix two mutually cancelling bugs.
181
                                      token_week)))
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
182
    # Define starting values:
183
    # Value so far
184
    value = datetime.timedelta()
608 by Teddy Hogeborn
* Makefile (check): Also check mandos-ctl.
185
    found_token = None
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
186
    # Following valid tokens
187
    followers = frozenset((token_duration, ))
188
    # String left to parse
189
    s = duration
608 by Teddy Hogeborn
* Makefile (check): Also check mandos-ctl.
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:
196
                # Token found
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)
204
                # Go to found token
205
                found_token = token
206
                # Set valid next tokens
207
                followers = found_token.followers
208
                break
209
        else:
210
            # No currently valid tokens were found
753 by Teddy Hogeborn
mandos-ctl: Generate better messages in exceptions.
211
            raise ValueError("Invalid RFC 3339 duration: {!r}"
212
                             .format(duration))
608 by Teddy Hogeborn
* Makefile (check): Also check mandos-ctl.
213
    # End token found
214
    return value
215
216
24.1.121 by Björn Påhlsson
mandos-ctl: Added support for all client calls
217
def string_to_delta(interval):
1001 by Teddy Hogeborn
mandos-ctl: White space changes only
218
    """Parse a string and return a datetime.timedelta"""
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
219
608 by Teddy Hogeborn
* Makefile (check): Also check mandos-ctl.
220
    try:
221
        return rfc3339_duration_to_delta(interval)
991 by Teddy Hogeborn
mandos-ctl: Refactor and add more tests
222
    except ValueError as e:
223
        log.warning("%s - Parsing as pre-1.6.1 interval instead",
224
                    ' '.join(e.args))
225
    return parse_pre_1_6_1_interval(interval)
226
227
228
def parse_pre_1_6_1_interval(interval):
1001 by Teddy Hogeborn
mandos-ctl: White space changes only
229
    """Parse an interval string as documented by Mandos before 1.6.1,
230
    and return a datetime.timedelta
231
991 by Teddy Hogeborn
mandos-ctl: Refactor and add more tests
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)
249
250
    """
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
251
616 by Teddy Hogeborn
* mandos-ctl (string_to_delta): Try to parse RFC 3339 duration before
252
    value = datetime.timedelta(0)
253
    regexp = re.compile(r"(\d+)([dsmhw]?)")
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
254
518.2.9 by Teddy Hogeborn
* mandos (ClientDBus.approval_delay, ClientDBus.approval_duration,
255
    for num, suffix in regexp.findall(interval):
256
        if suffix == "d":
257
            value += datetime.timedelta(int(num))
258
        elif suffix == "s":
259
            value += datetime.timedelta(0, int(num))
260
        elif suffix == "m":
261
            value += datetime.timedelta(0, 0, 0, 0, int(num))
262
        elif suffix == "h":
263
            value += datetime.timedelta(0, 0, 0, 0, 0, int(num))
264
        elif suffix == "w":
265
            value += datetime.timedelta(0, 0, 0, 0, 0, 0, int(num))
266
        elif suffix == "":
267
            value += datetime.timedelta(0, 0, 0, int(num))
268
    return value
24.1.121 by Björn Påhlsson
mandos-ctl: Added support for all client calls
269
745 by Teddy Hogeborn
mandos-ctl: Do minor formatting and whitespace adjustments.
270
995 by Teddy Hogeborn
mandos-ctl: Refactor
271
class TableOfClients(object):
1004 by Teddy Hogeborn
mandos-ctl: Rename "tablewords" to "tableheaders"
272
    tableheaders = {
996 by Teddy Hogeborn
mandos-ctl: Refactor
273
        "Name": "Name",
274
        "Enabled": "Enabled",
275
        "Timeout": "Timeout",
276
        "LastCheckedOK": "Last Successful Check",
277
        "LastApprovalRequest": "Last Approval Request",
278
        "Created": "Created",
279
        "Interval": "Interval",
280
        "Host": "Host",
281
        "Fingerprint": "Fingerprint",
282
        "KeyID": "Key ID",
283
        "CheckerRunning": "Check Is Running",
284
        "LastEnabled": "Last Enabled",
285
        "ApprovalPending": "Approval Is Pending",
286
        "ApprovedByDefault": "Approved By Default",
287
        "ApprovalDelay": "Approval Delay",
288
        "ApprovalDuration": "Approval Duration",
289
        "Checker": "Checker",
290
        "ExtendedTimeout": "Extended Timeout",
291
        "Expires": "Expires",
292
        "LastCheckerStatus": "Last Checker Status",
293
    }
294
1004 by Teddy Hogeborn
mandos-ctl: Rename "tablewords" to "tableheaders"
295
    def __init__(self, clients, keywords, tableheaders=None):
995 by Teddy Hogeborn
mandos-ctl: Refactor
296
        self.clients = clients
297
        self.keywords = keywords
1004 by Teddy Hogeborn
mandos-ctl: Rename "tablewords" to "tableheaders"
298
        if tableheaders is not None:
299
            self.tableheaders = tableheaders
995 by Teddy Hogeborn
mandos-ctl: Refactor
300
998 by Teddy Hogeborn
mandos-ctl: Refactor
301
    def __str__(self):
302
        return "\n".join(self.rows())
303
304
    if sys.version_info.major == 2:
305
        __unicode__ = __str__
306
        def __str__(self):
307
            return str(self).encode(locale.getpreferredencoding())
308
997 by Teddy Hogeborn
mandos-ctl: Refactor
309
    def rows(self):
310
        format_string = self.row_formatting_string()
311
        rows = [self.header_line(format_string)]
312
        rows.extend(self.client_line(client, format_string)
313
                    for client in self.clients)
314
        return rows
315
316
    def row_formatting_string(self):
317
        "Format string used to format table rows"
318
        return " ".join("{{{key}:{width}}}".format(
1004 by Teddy Hogeborn
mandos-ctl: Rename "tablewords" to "tableheaders"
319
            width=max(len(self.tableheaders[key]),
1000 by Teddy Hogeborn
mandos-ctl: Refactor
320
                      *(len(self.string_from_client(client, key))
321
                        for client in self.clients)),
997 by Teddy Hogeborn
mandos-ctl: Refactor
322
            key=key)
1001 by Teddy Hogeborn
mandos-ctl: White space changes only
323
                        for key in self.keywords)
997 by Teddy Hogeborn
mandos-ctl: Refactor
324
325
    def string_from_client(self, client, key):
326
        return self.valuetostring(client[key], key)
327
995 by Teddy Hogeborn
mandos-ctl: Refactor
328
    @staticmethod
24.1.121 by Björn Påhlsson
mandos-ctl: Added support for all client calls
329
    def valuetostring(value, keyword):
989 by Teddy Hogeborn
mandos-ctl: Use isinstance(x, X) instead of type(x) is X
330
        if isinstance(value, dbus.Boolean):
463.1.8 by teddy at bsnet
* mandos-ctl: Use unicode string literals.
331
            return "Yes" if value else "No"
332
        if keyword in ("Timeout", "Interval", "ApprovalDelay",
571 by Teddy Hogeborn
* mandos-ctl (print_clients): Bug fix: Don't show "Extended Timeout"
333
                       "ApprovalDuration", "ExtendedTimeout"):
24.1.121 by Björn Påhlsson
mandos-ctl: Added support for all client calls
334
            return milliseconds_to_string(value)
718 by Teddy Hogeborn
mandos-ctl: Make it work in Python 3.
335
        return str(value)
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
336
997 by Teddy Hogeborn
mandos-ctl: Refactor
337
    def header_line(self, format_string):
1004 by Teddy Hogeborn
mandos-ctl: Rename "tablewords" to "tableheaders"
338
        return format_string.format(**self.tableheaders)
997 by Teddy Hogeborn
mandos-ctl: Refactor
339
340
    def client_line(self, client, format_string):
341
        return format_string.format(
342
            **{key: self.string_from_client(client, key)
343
               for key in self.keywords})
745 by Teddy Hogeborn
mandos-ctl: Do minor formatting and whitespace adjustments.
344
438 by Teddy Hogeborn
* mandos (Client.runtime_expansions): New attribute containing the
345
1003 by Teddy Hogeborn
mandos-ctl: Separate determining what to do and actually doing it
346
## Classes for commands.
347
348
# Abstract classes first
349
class Command(object):
350
    """Abstract class for commands"""
1007 by Teddy Hogeborn
mandos-ctl: Refactor
351
    def run(self, mandos, clients):
1003 by Teddy Hogeborn
mandos-ctl: Separate determining what to do and actually doing it
352
        """Normal commands should implement run_on_one_client(), but
353
        commands which want to operate on all clients at the same time
354
        can override this run() method instead."""
1007 by Teddy Hogeborn
mandos-ctl: Refactor
355
        self.mandos = mandos
1003 by Teddy Hogeborn
mandos-ctl: Separate determining what to do and actually doing it
356
        for client in clients:
357
            self.run_on_one_client(client)
358
359
class PrintCmd(Command):
360
    """Abstract class for commands printing client details"""
361
    all_keywords = ("Name", "Enabled", "Timeout", "LastCheckedOK",
362
                    "Created", "Interval", "Host", "KeyID",
363
                    "Fingerprint", "CheckerRunning", "LastEnabled",
364
                    "ApprovalPending", "ApprovedByDefault",
365
                    "LastApprovalRequest", "ApprovalDelay",
366
                    "ApprovalDuration", "Checker", "ExtendedTimeout",
367
                    "Expires", "LastCheckerStatus")
1007 by Teddy Hogeborn
mandos-ctl: Refactor
368
    def run(self, mandos, clients):
1003 by Teddy Hogeborn
mandos-ctl: Separate determining what to do and actually doing it
369
        print(self.output(clients))
370
371
class PropertyCmd(Command):
372
    """Abstract class for Actions for setting one client property"""
373
    def run_on_one_client(self, client):
374
        """Set the Client's D-Bus property"""
375
        client.Set(client_interface, self.property, self.value_to_set,
376
                   dbus_interface=dbus.PROPERTIES_IFACE)
377
378
class ValueArgumentMixIn(object):
379
    """Mixin class for commands taking a value as argument"""
380
    def __init__(self, value):
381
        self.value_to_set = value
382
383
class MillisecondsValueArgumentMixIn(ValueArgumentMixIn):
384
    """Mixin class for commands taking a value argument as
385
    milliseconds."""
386
    @property
387
    def value_to_set(self):
388
        return self._vts
389
    @value_to_set.setter
390
    def value_to_set(self, value):
391
        """When setting, convert value to a datetime.timedelta"""
392
        self._vts = string_to_delta(value).total_seconds() * 1000
393
394
# Actual (non-abstract) command classes
395
396
class PrintTableCmd(PrintCmd):
397
    def __init__(self, verbose=False):
398
        self.verbose = verbose
399
    def output(self, clients):
400
        if self.verbose:
401
            keywords = self.all_keywords
402
        else:
403
            keywords = ("Name", "Enabled", "Timeout", "LastCheckedOK")
404
        return str(TableOfClients(clients.values(), keywords))
405
406
class DumpJSONCmd(PrintCmd):
407
    def output(self, clients):
408
        data = {client["Name"]:
409
                {key: self.dbus_boolean_to_bool(client[key])
410
                 for key in self.all_keywords}
411
                for client in clients.values()}
412
        return json.dumps(data, indent=4, separators=(',', ': '))
413
    @staticmethod
414
    def dbus_boolean_to_bool(value):
415
        if isinstance(value, dbus.Boolean):
416
            value = bool(value)
417
        return value
418
419
class IsEnabledCmd(Command):
420
    def run_on_one_client(self, client):
421
        if self.is_enabled(client):
422
            sys.exit(0)
423
        sys.exit(1)
424
    def is_enabled(self, client):
425
        return client.Get(client_interface, "Enabled",
426
                          dbus_interface=dbus.PROPERTIES_IFACE)
427
428
class RemoveCmd(Command):
429
    def run_on_one_client(self, client):
430
        self.mandos.RemoveClient(client.__dbus_object_path__)
431
432
class ApproveCmd(Command):
433
    def run_on_one_client(self, client):
434
        client.Approve(dbus.Boolean(True),
435
                       dbus_interface=client_interface)
436
437
class DenyCmd(Command):
438
    def run_on_one_client(self, client):
439
        client.Approve(dbus.Boolean(False),
440
                       dbus_interface=client_interface)
441
442
class EnableCmd(PropertyCmd):
443
    property = "Enabled"
444
    value_to_set = dbus.Boolean(True)
445
446
class DisableCmd(PropertyCmd):
447
    property = "Enabled"
448
    value_to_set = dbus.Boolean(False)
449
450
class BumpTimeoutCmd(PropertyCmd):
451
    property = "LastCheckedOK"
452
    value_to_set = ""
453
454
class StartCheckerCmd(PropertyCmd):
455
    property = "CheckerRunning"
456
    value_to_set = dbus.Boolean(True)
457
458
class StopCheckerCmd(PropertyCmd):
459
    property = "CheckerRunning"
460
    value_to_set = dbus.Boolean(False)
461
462
class ApproveByDefaultCmd(PropertyCmd):
463
    property = "ApprovedByDefault"
464
    value_to_set = dbus.Boolean(True)
465
466
class DenyByDefaultCmd(PropertyCmd):
467
    property = "ApprovedByDefault"
468
    value_to_set = dbus.Boolean(False)
469
470
class SetCheckerCmd(PropertyCmd, ValueArgumentMixIn):
471
    property = "Checker"
472
473
class SetHostCmd(PropertyCmd, ValueArgumentMixIn):
474
    property = "Host"
475
476
class SetSecretCmd(PropertyCmd, ValueArgumentMixIn):
477
    property = "Secret"
478
479
class SetTimeoutCmd(PropertyCmd, MillisecondsValueArgumentMixIn):
480
    property = "Timeout"
481
482
class SetExtendedTimeoutCmd(PropertyCmd,
483
                            MillisecondsValueArgumentMixIn):
484
    property = "ExtendedTimeout"
485
486
class SetIntervalCmd(PropertyCmd, MillisecondsValueArgumentMixIn):
487
    property = "Interval"
488
489
class SetApprovalDelayCmd(PropertyCmd,
490
                          MillisecondsValueArgumentMixIn):
491
    property = "ApprovalDelay"
492
493
class SetApprovalDurationCmd(PropertyCmd,
494
                             MillisecondsValueArgumentMixIn):
495
    property = "ApprovalDuration"
496
24.1.163 by Björn Påhlsson
mandos-client: Added never ending loop for --connect
497
def has_actions(options):
498
    return any((options.enable,
499
                options.disable,
500
                options.bump_timeout,
501
                options.start_checker,
502
                options.stop_checker,
503
                options.is_enabled,
504
                options.remove,
505
                options.checker is not None,
506
                options.timeout is not None,
24.1.179 by Björn Påhlsson
New feature:
507
                options.extended_timeout is not None,
24.1.163 by Björn Påhlsson
mandos-client: Added never ending loop for --connect
508
                options.interval is not None,
441 by Teddy Hogeborn
* mandos (ClientDBus.__init__): Bug fix: Translate "-" in client names
509
                options.approved_by_default is not None,
510
                options.approval_delay is not None,
511
                options.approval_duration is not None,
24.1.163 by Björn Påhlsson
mandos-client: Added never ending loop for --connect
512
                options.host is not None,
513
                options.secret is not None,
514
                options.approve,
515
                options.deny))
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
516
745 by Teddy Hogeborn
mandos-ctl: Do minor formatting and whitespace adjustments.
517
1009 by Teddy Hogeborn
mandos-ctl: Refactor
518
def commands_and_clients_from_options(args=None):
519
    if args is None:
520
        args=sys.argv[1:]
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
521
    parser = argparse.ArgumentParser()
522
    parser.add_argument("--version", action="version",
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
523
                        version="%(prog)s {}".format(version),
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
524
                        help="show version number and exit")
525
    parser.add_argument("-a", "--all", action="store_true",
526
                        help="Select all clients")
527
    parser.add_argument("-v", "--verbose", action="store_true",
528
                        help="Print all fields")
863 by Teddy Hogeborn
mandos-ctl: Implement --dump-json option
529
    parser.add_argument("-j", "--dump-json", action="store_true",
530
                        help="Dump client data in JSON format")
1002 by Teddy Hogeborn
mandos-ctl: Make option parsing slightly more strict
531
    enable_disable = parser.add_mutually_exclusive_group()
532
    enable_disable.add_argument("-e", "--enable", action="store_true",
533
                                help="Enable client")
534
    enable_disable.add_argument("-d", "--disable",
535
                                action="store_true",
536
                                help="disable client")
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
537
    parser.add_argument("-b", "--bump-timeout", action="store_true",
538
                        help="Bump timeout for client")
1002 by Teddy Hogeborn
mandos-ctl: Make option parsing slightly more strict
539
    start_stop_checker = parser.add_mutually_exclusive_group()
540
    start_stop_checker.add_argument("--start-checker",
541
                                    action="store_true",
542
                                    help="Start checker for client")
543
    start_stop_checker.add_argument("--stop-checker",
544
                                    action="store_true",
545
                                    help="Stop checker for client")
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
546
    parser.add_argument("-V", "--is-enabled", action="store_true",
547
                        help="Check if client is enabled")
548
    parser.add_argument("-r", "--remove", action="store_true",
549
                        help="Remove client")
550
    parser.add_argument("-c", "--checker",
551
                        help="Set checker command for client")
552
    parser.add_argument("-t", "--timeout",
553
                        help="Set timeout for client")
24.1.179 by Björn Påhlsson
New feature:
554
    parser.add_argument("--extended-timeout",
555
                        help="Set extended timeout for client")
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
556
    parser.add_argument("-i", "--interval",
557
                        help="Set checker interval for client")
1002 by Teddy Hogeborn
mandos-ctl: Make option parsing slightly more strict
558
    approve_deny_default = parser.add_mutually_exclusive_group()
559
    approve_deny_default.add_argument(
560
        "--approve-by-default", action="store_true",
561
        default=None, dest="approved_by_default",
562
        help="Set client to be approved by default")
563
    approve_deny_default.add_argument(
564
        "--deny-by-default", action="store_false",
565
        dest="approved_by_default",
566
        help="Set client to be denied by default")
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
567
    parser.add_argument("--approval-delay",
568
                        help="Set delay before client approve/deny")
569
    parser.add_argument("--approval-duration",
570
                        help="Set duration of one client approval")
571
    parser.add_argument("-H", "--host", help="Set host for client")
718 by Teddy Hogeborn
mandos-ctl: Make it work in Python 3.
572
    parser.add_argument("-s", "--secret",
573
                        type=argparse.FileType(mode="rb"),
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
574
                        help="Set password blob (file) for client")
1002 by Teddy Hogeborn
mandos-ctl: Make option parsing slightly more strict
575
    approve_deny = parser.add_mutually_exclusive_group()
576
    approve_deny.add_argument(
577
        "-A", "--approve", action="store_true",
578
        help="Approve any current client request")
579
    approve_deny.add_argument("-D", "--deny", action="store_true",
580
                              help="Deny any current client request")
608 by Teddy Hogeborn
* Makefile (check): Also check mandos-ctl.
581
    parser.add_argument("--check", action="store_true",
582
                        help="Run self-test")
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
583
    parser.add_argument("client", nargs="*", help="Client name")
1009 by Teddy Hogeborn
mandos-ctl: Refactor
584
    options = parser.parse_args(args=args)
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
585
572 by Teddy Hogeborn
* mandos-ctl: Break long lines.
586
    if has_actions(options) and not (options.client or options.all):
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
587
        parser.error("Options require clients names or --all.")
588
    if options.verbose and has_actions(options):
863 by Teddy Hogeborn
mandos-ctl: Implement --dump-json option
589
        parser.error("--verbose can only be used alone.")
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
590
    if options.dump_json and (options.verbose
591
                              or has_actions(options)):
863 by Teddy Hogeborn
mandos-ctl: Implement --dump-json option
592
        parser.error("--dump-json can only be used alone.")
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
593
    if options.all and not has_actions(options):
594
        parser.error("--all requires an action.")
1002 by Teddy Hogeborn
mandos-ctl: Make option parsing slightly more strict
595
    if options.is_enabled and len(options.client) > 1:
596
            parser.error("--is-enabled requires exactly one client")
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
597
1003 by Teddy Hogeborn
mandos-ctl: Separate determining what to do and actually doing it
598
    commands = []
599
600
    if options.dump_json:
601
        commands.append(DumpJSONCmd())
602
603
    if options.enable:
604
        commands.append(EnableCmd())
605
606
    if options.disable:
607
        commands.append(DisableCmd())
608
609
    if options.bump_timeout:
610
        commands.append(BumpTimeoutCmd(options.bump_timeout))
611
612
    if options.start_checker:
613
        commands.append(StartCheckerCmd())
614
615
    if options.stop_checker:
616
        commands.append(StopCheckerCmd())
617
618
    if options.is_enabled:
619
        commands.append(IsEnabledCmd())
620
621
    if options.remove:
1007 by Teddy Hogeborn
mandos-ctl: Refactor
622
        commands.append(RemoveCmd())
1003 by Teddy Hogeborn
mandos-ctl: Separate determining what to do and actually doing it
623
624
    if options.checker is not None:
625
        commands.append(SetCheckerCmd())
626
627
    if options.timeout is not None:
628
        commands.append(SetTimeoutCmd(options.timeout))
629
630
    if options.extended_timeout:
631
        commands.append(
632
            SetExtendedTimeoutCmd(options.extended_timeout))
633
634
    if options.interval is not None:
635
        command.append(SetIntervalCmd(options.interval))
636
637
    if options.approved_by_default is not None:
638
        if options.approved_by_default:
639
            command.append(ApproveByDefaultCmd())
640
        else:
641
            command.append(DenyByDefaultCmd())
642
643
    if options.approval_delay is not None:
644
        command.append(SetApprovalDelayCmd(options.approval_delay))
645
646
    if options.approval_duration is not None:
647
        command.append(
648
            SetApprovalDurationCmd(options.approval_duration))
649
650
    if options.host is not None:
651
        command.append(SetHostCmd(options.host))
652
653
    if options.secret is not None:
654
        command.append(SetSecretCmd(options.secret))
655
656
    if options.approve:
657
        commands.append(ApproveCmd())
658
659
    if options.deny:
660
        commands.append(DenyCmd())
661
662
    # If no command option has been given, show table of clients,
663
    # optionally verbosely
664
    if not commands:
665
        commands.append(PrintTableCmd(verbose=options.verbose))
666
1008 by Teddy Hogeborn
mandos-ctl: Refactor
667
    return commands, options.client
668
669
670
def main():
671
    commands, clientnames = commands_and_clients_from_options()
672
673
    try:
674
        bus = dbus.SystemBus()
675
        mandos_dbus_objc = bus.get_object(busname, server_path)
676
    except dbus.exceptions.DBusException:
677
        log.critical("Could not connect to Mandos server")
678
        sys.exit(1)
679
680
    mandos_serv = dbus.Interface(mandos_dbus_objc,
681
                                 dbus_interface=server_interface)
682
    mandos_serv_object_manager = dbus.Interface(
683
        mandos_dbus_objc, dbus_interface=dbus.OBJECT_MANAGER_IFACE)
684
1005 by Teddy Hogeborn
mandos-ctl: Filter logging instead of messing with stderr
685
    # Filter out log message from dbus module
686
    dbus_logger = logging.getLogger("dbus.proxies")
687
    class NullFilter(logging.Filter):
688
        def filter(self, record):
689
            return False
690
    dbus_filter = NullFilter()
691
    dbus_logger.addFilter(dbus_filter)
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
692
    try:
693
        try:
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
694
            mandos_clients = {path: ifs_and_props[client_interface]
695
                              for path, ifs_and_props in
696
                              mandos_serv_object_manager
697
                              .GetManagedObjects().items()
698
                              if client_interface in ifs_and_props}
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
699
        finally:
1005 by Teddy Hogeborn
mandos-ctl: Filter logging instead of messing with stderr
700
            # restore dbus logger
701
            dbus_logger.removeFilter(dbus_filter)
785 by Teddy Hogeborn
Support the standard org.freedesktop.DBus.ObjectManager interface.
702
    except dbus.exceptions.DBusException as e:
987 by Teddy Hogeborn
mandos-ctl: Use logging module instead of print() for errors
703
        log.critical("Failed to access Mandos server through D-Bus:"
704
                     "\n%s", e)
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
705
        sys.exit(1)
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
706
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
707
    # Compile dict of (clients: properties) to process
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
708
    clients = {}
709
1008 by Teddy Hogeborn
mandos-ctl: Refactor
710
    if not clientnames:
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
711
        clients = {bus.get_object(busname, path): properties
712
                   for path, properties in mandos_clients.items()}
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
713
    else:
1008 by Teddy Hogeborn
mandos-ctl: Refactor
714
        for name in clientnames:
723.1.4 by Teddy Hogeborn
Use the .items() method instead of .iteritems().
715
            for path, client in mandos_clients.items():
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
716
                if client["Name"] == name:
717
                    client_objc = bus.get_object(busname, path)
718
                    clients[client_objc] = client
719
                    break
24.1.163 by Björn Påhlsson
mandos-client: Added never ending loop for --connect
720
            else:
987 by Teddy Hogeborn
mandos-ctl: Use logging module instead of print() for errors
721
                log.critical("Client not found on server: %r", name)
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
722
                sys.exit(1)
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
723
1003 by Teddy Hogeborn
mandos-ctl: Separate determining what to do and actually doing it
724
    # Run all commands on clients
725
    for command in commands:
1007 by Teddy Hogeborn
mandos-ctl: Refactor
726
        command.run(mandos_serv, clients)
24.1.163 by Björn Påhlsson
mandos-client: Added never ending loop for --connect
727
984 by Teddy Hogeborn
Make mandos-ctl use unittest instead of doctest module
728

986 by Teddy Hogeborn
Add tests to mandos-ctl's milliseconds_to_string function
729
class Test_milliseconds_to_string(unittest.TestCase):
730
    def test_all(self):
731
        self.assertEqual(milliseconds_to_string(93785000),
732
                         "1T02:03:05")
733
    def test_no_days(self):
734
        self.assertEqual(milliseconds_to_string(7385000), "02:03:05")
735
    def test_all_zero(self):
736
        self.assertEqual(milliseconds_to_string(0), "00:00:00")
737
    def test_no_fractional_seconds(self):
738
        self.assertEqual(milliseconds_to_string(400), "00:00:00")
739
        self.assertEqual(milliseconds_to_string(900), "00:00:00")
740
        self.assertEqual(milliseconds_to_string(1900), "00:00:01")
741
992 by Teddy Hogeborn
mandos-ctl: Add more tests
742
class Test_string_to_delta(unittest.TestCase):
743
    def test_handles_basic_rfc3339(self):
744
        self.assertEqual(string_to_delta("PT2H"),
745
                         datetime.timedelta(0, 7200))
746
    def test_falls_back_to_pre_1_6_1_with_warning(self):
747
        # assertLogs only exists in Python 3.4
748
        if hasattr(self, "assertLogs"):
749
            with self.assertLogs(log, logging.WARNING):
750
                value = string_to_delta("2h")
751
        else:
1006 by Teddy Hogeborn
mandos-ctl: Improve a test when running Python older than 3.4.
752
            class WarningFilter(logging.Filter):
753
                """Don't show, but record the presence of, warnings"""
754
                def filter(self, record):
755
                    is_warning = record.levelno >= logging.WARNING
756
                    self.found = is_warning or getattr(self, "found",
757
                                                       False)
758
                    return not is_warning
759
            warning_filter = WarningFilter()
760
            log.addFilter(warning_filter)
761
            try:
762
                value = string_to_delta("2h")
763
            finally:
764
                log.removeFilter(warning_filter)
765
            self.assertTrue(getattr(warning_filter, "found", False))
992 by Teddy Hogeborn
mandos-ctl: Add more tests
766
        self.assertEqual(value, datetime.timedelta(0, 7200))
767
1010 by Teddy Hogeborn
mandos-ctl: Refactor; test PrintTableCmd instead of TableOfClients
768
769
class TestCmd(unittest.TestCase):
770
    """Abstract class for tests of command classes"""
994 by Teddy Hogeborn
mandos-ctl: Add tests for table_rows_of_clients()
771
    def setUp(self):
1010 by Teddy Hogeborn
mandos-ctl: Refactor; test PrintTableCmd instead of TableOfClients
772
        testcase = self
773
        class MockClient(object):
774
            def __init__(self, name, **attributes):
775
                self.__dbus_object_path__ = "objpath_{}".format(name)
776
                self.attributes = attributes
777
                self.attributes["Name"] = name
778
            def Set(interface, property, value,
779
                    properties_interface):
780
                testcase.assertEqual(interface, client_interface)
781
                testcase.assertEqual(properties_interface,
782
                                     dbus.PROPERTIES_IFACE)
783
                self.attributes[property] = value
784
            def Get(interface, property, properties_interface):
785
                testcase.assertEqual(interface, client_interface)
786
                testcase.assertEqual(properties_interface,
787
                                     dbus.PROPERTIES_IFACE)
788
                return self.attributes[property]
789
            def __getitem__(self, key):
790
                return self.attributes[key]
791
        self.clients = collections.OrderedDict([
792
            ("foo",
793
             MockClient(
794
                 "foo",
795
                 KeyID=("92ed150794387c03ce684574b1139a65"
796
                        "94a34f895daaaf09fd8ea90a27cddb12"),
797
                 Secret=b"secret",
798
                 Host="foo.example.org",
799
                 Enabled=dbus.Boolean(True),
800
                 Timeout=300000,
801
                 LastCheckedOK="2019-02-03T00:00:00",
802
                 Created="2019-01-02T00:00:00",
803
                 Interval=120000,
804
                 Fingerprint=("778827225BA7DE539C5A"
805
                              "7CFA59CFF7CDBD9A5920"),
806
                 CheckerRunning=dbus.Boolean(False),
807
                 LastEnabled="2019-01-03T00:00:00",
808
                 ApprovalPending=dbus.Boolean(False),
809
                 ApprovedByDefault=dbus.Boolean(True),
810
                 LastApprovalRequest="",
811
                 ApprovalDelay=0,
812
                 ApprovalDuration=1000,
813
                 Checker="fping -q -- %(host)s",
814
                 ExtendedTimeout=900000,
815
                 Expires="2019-02-04T00:00:00",
816
                 LastCheckerStatus=0)),
817
            ("barbar",
818
             MockClient(
819
                 "barbar",
820
                 KeyID=("0558568eedd67d622f5c83b35a115f79"
821
                        "6ab612cff5ad227247e46c2b020f441c"),
822
                 Secret=b"secretbar",
823
                 Host="192.0.2.3",
824
                 Enabled=dbus.Boolean(True),
825
                 Timeout=300000,
826
                 LastCheckedOK="2019-02-04T00:00:00",
827
                 Created="2019-01-03T00:00:00",
828
                 Interval=120000,
829
                 Fingerprint=("3E393AEAEFB84C7E89E2"
830
                              "F547B3A107558FCA3A27"),
831
                 CheckerRunning=dbus.Boolean(True),
832
                 LastEnabled="2019-01-04T00:00:00",
833
                 ApprovalPending=dbus.Boolean(False),
834
                 ApprovedByDefault=dbus.Boolean(False),
835
                 LastApprovalRequest="2019-01-03T00:00:00",
836
                 ApprovalDelay=30000,
837
                 ApprovalDuration=1000,
838
                 Checker=":",
839
                 ExtendedTimeout=900000,
840
                 Expires="2019-02-05T00:00:00",
841
                 LastCheckerStatus=-2)),
842
            ])
843
844
class TestPrintTableCmd(TestCmd):
845
    def test_normal(self):
846
        output = PrintTableCmd().output(self.clients)
847
        expected_output = """
848
Name   Enabled Timeout  Last Successful Check
849
foo    Yes     00:05:00 2019-02-03T00:00:00  
850
barbar Yes     00:05:00 2019-02-04T00:00:00  
851
"""[1:-1]
852
        self.assertEqual(output, expected_output)
853
    def test_verbose(self):
854
        output = PrintTableCmd(verbose=True).output(self.clients)
855
        expected_output = """
856
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
857
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                  
858
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                 
859
"""[1:-1]
860
        self.assertEqual(output, expected_output)
861
    def test_one_client(self):
862
        output = PrintTableCmd().output({"foo": self.clients["foo"]})
863
        expected_output = """
864
Name Enabled Timeout  Last Successful Check
865
foo  Yes     00:05:00 2019-02-03T00:00:00  
866
"""[1:-1]
867
        self.assertEqual(output, expected_output)
994 by Teddy Hogeborn
mandos-ctl: Add tests for table_rows_of_clients()
868
869
986 by Teddy Hogeborn
Add tests to mandos-ctl's milliseconds_to_string function
870

984 by Teddy Hogeborn
Make mandos-ctl use unittest instead of doctest module
871
def should_only_run_tests():
872
    parser = argparse.ArgumentParser(add_help=False)
873
    parser.add_argument("--check", action='store_true')
874
    args, unknown_args = parser.parse_known_args()
875
    run_tests = args.check
876
    if run_tests:
877
        # Remove --check argument from sys.argv
878
        sys.argv[1:] = unknown_args
879
    return run_tests
880
881
# Add all tests from doctest strings
882
def load_tests(loader, tests, none):
883
    import doctest
884
    tests.addTests(doctest.DocTestSuite())
885
    return tests
745 by Teddy Hogeborn
mandos-ctl: Do minor formatting and whitespace adjustments.
886
463.1.8 by teddy at bsnet
* mandos-ctl: Use unicode string literals.
887
if __name__ == "__main__":
984 by Teddy Hogeborn
Make mandos-ctl use unittest instead of doctest module
888
    if should_only_run_tests():
889
        # Call using ./tdd-python-script --check [--verbose]
890
        unittest.main()
891
    else:
892
        main()