/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
24.1.163 by Björn Påhlsson
mandos-client: Added never ending loop for --connect
518
def main():
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
519
    parser = argparse.ArgumentParser()
520
    parser.add_argument("--version", action="version",
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
521
                        version="%(prog)s {}".format(version),
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
522
                        help="show version number and exit")
523
    parser.add_argument("-a", "--all", action="store_true",
524
                        help="Select all clients")
525
    parser.add_argument("-v", "--verbose", action="store_true",
526
                        help="Print all fields")
863 by Teddy Hogeborn
mandos-ctl: Implement --dump-json option
527
    parser.add_argument("-j", "--dump-json", action="store_true",
528
                        help="Dump client data in JSON format")
1002 by Teddy Hogeborn
mandos-ctl: Make option parsing slightly more strict
529
    enable_disable = parser.add_mutually_exclusive_group()
530
    enable_disable.add_argument("-e", "--enable", action="store_true",
531
                                help="Enable client")
532
    enable_disable.add_argument("-d", "--disable",
533
                                action="store_true",
534
                                help="disable client")
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
535
    parser.add_argument("-b", "--bump-timeout", action="store_true",
536
                        help="Bump timeout for client")
1002 by Teddy Hogeborn
mandos-ctl: Make option parsing slightly more strict
537
    start_stop_checker = parser.add_mutually_exclusive_group()
538
    start_stop_checker.add_argument("--start-checker",
539
                                    action="store_true",
540
                                    help="Start checker for client")
541
    start_stop_checker.add_argument("--stop-checker",
542
                                    action="store_true",
543
                                    help="Stop checker for client")
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
544
    parser.add_argument("-V", "--is-enabled", action="store_true",
545
                        help="Check if client is enabled")
546
    parser.add_argument("-r", "--remove", action="store_true",
547
                        help="Remove client")
548
    parser.add_argument("-c", "--checker",
549
                        help="Set checker command for client")
550
    parser.add_argument("-t", "--timeout",
551
                        help="Set timeout for client")
24.1.179 by Björn Påhlsson
New feature:
552
    parser.add_argument("--extended-timeout",
553
                        help="Set extended timeout for client")
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
554
    parser.add_argument("-i", "--interval",
555
                        help="Set checker interval for client")
1002 by Teddy Hogeborn
mandos-ctl: Make option parsing slightly more strict
556
    approve_deny_default = parser.add_mutually_exclusive_group()
557
    approve_deny_default.add_argument(
558
        "--approve-by-default", action="store_true",
559
        default=None, dest="approved_by_default",
560
        help="Set client to be approved by default")
561
    approve_deny_default.add_argument(
562
        "--deny-by-default", action="store_false",
563
        dest="approved_by_default",
564
        help="Set client to be denied by default")
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
565
    parser.add_argument("--approval-delay",
566
                        help="Set delay before client approve/deny")
567
    parser.add_argument("--approval-duration",
568
                        help="Set duration of one client approval")
569
    parser.add_argument("-H", "--host", help="Set host for client")
718 by Teddy Hogeborn
mandos-ctl: Make it work in Python 3.
570
    parser.add_argument("-s", "--secret",
571
                        type=argparse.FileType(mode="rb"),
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
572
                        help="Set password blob (file) for client")
1002 by Teddy Hogeborn
mandos-ctl: Make option parsing slightly more strict
573
    approve_deny = parser.add_mutually_exclusive_group()
574
    approve_deny.add_argument(
575
        "-A", "--approve", action="store_true",
576
        help="Approve any current client request")
577
    approve_deny.add_argument("-D", "--deny", action="store_true",
578
                              help="Deny any current client request")
608 by Teddy Hogeborn
* Makefile (check): Also check mandos-ctl.
579
    parser.add_argument("--check", action="store_true",
580
                        help="Run self-test")
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
581
    parser.add_argument("client", nargs="*", help="Client name")
582
    options = parser.parse_args()
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
583
572 by Teddy Hogeborn
* mandos-ctl: Break long lines.
584
    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.
585
        parser.error("Options require clients names or --all.")
586
    if options.verbose and has_actions(options):
863 by Teddy Hogeborn
mandos-ctl: Implement --dump-json option
587
        parser.error("--verbose can only be used alone.")
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
588
    if options.dump_json and (options.verbose
589
                              or has_actions(options)):
863 by Teddy Hogeborn
mandos-ctl: Implement --dump-json option
590
        parser.error("--dump-json can only be used alone.")
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
591
    if options.all and not has_actions(options):
592
        parser.error("--all requires an action.")
1002 by Teddy Hogeborn
mandos-ctl: Make option parsing slightly more strict
593
    if options.is_enabled and len(options.client) > 1:
594
            parser.error("--is-enabled requires exactly one client")
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
595
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
596
    try:
597
        bus = dbus.SystemBus()
598
        mandos_dbus_objc = bus.get_object(busname, server_path)
599
    except dbus.exceptions.DBusException:
987 by Teddy Hogeborn
mandos-ctl: Use logging module instead of print() for errors
600
        log.critical("Could not connect to Mandos server")
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
601
        sys.exit(1)
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
602
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
603
    mandos_serv = dbus.Interface(mandos_dbus_objc,
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
604
                                 dbus_interface=server_interface)
785 by Teddy Hogeborn
Support the standard org.freedesktop.DBus.ObjectManager interface.
605
    mandos_serv_object_manager = dbus.Interface(
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
606
        mandos_dbus_objc, dbus_interface=dbus.OBJECT_MANAGER_IFACE)
607
1003 by Teddy Hogeborn
mandos-ctl: Separate determining what to do and actually doing it
608
    commands = []
609
610
    if options.dump_json:
611
        commands.append(DumpJSONCmd())
612
613
    if options.enable:
614
        commands.append(EnableCmd())
615
616
    if options.disable:
617
        commands.append(DisableCmd())
618
619
    if options.bump_timeout:
620
        commands.append(BumpTimeoutCmd(options.bump_timeout))
621
622
    if options.start_checker:
623
        commands.append(StartCheckerCmd())
624
625
    if options.stop_checker:
626
        commands.append(StopCheckerCmd())
627
628
    if options.is_enabled:
629
        commands.append(IsEnabledCmd())
630
631
    if options.remove:
1007 by Teddy Hogeborn
mandos-ctl: Refactor
632
        commands.append(RemoveCmd())
1003 by Teddy Hogeborn
mandos-ctl: Separate determining what to do and actually doing it
633
634
    if options.checker is not None:
635
        commands.append(SetCheckerCmd())
636
637
    if options.timeout is not None:
638
        commands.append(SetTimeoutCmd(options.timeout))
639
640
    if options.extended_timeout:
641
        commands.append(
642
            SetExtendedTimeoutCmd(options.extended_timeout))
643
644
    if options.interval is not None:
645
        command.append(SetIntervalCmd(options.interval))
646
647
    if options.approved_by_default is not None:
648
        if options.approved_by_default:
649
            command.append(ApproveByDefaultCmd())
650
        else:
651
            command.append(DenyByDefaultCmd())
652
653
    if options.approval_delay is not None:
654
        command.append(SetApprovalDelayCmd(options.approval_delay))
655
656
    if options.approval_duration is not None:
657
        command.append(
658
            SetApprovalDurationCmd(options.approval_duration))
659
660
    if options.host is not None:
661
        command.append(SetHostCmd(options.host))
662
663
    if options.secret is not None:
664
        command.append(SetSecretCmd(options.secret))
665
666
    if options.approve:
667
        commands.append(ApproveCmd())
668
669
    if options.deny:
670
        commands.append(DenyCmd())
671
672
    # If no command option has been given, show table of clients,
673
    # optionally verbosely
674
    if not commands:
675
        commands.append(PrintTableCmd(verbose=options.verbose))
676
1005 by Teddy Hogeborn
mandos-ctl: Filter logging instead of messing with stderr
677
    # Filter out log message from dbus module
678
    dbus_logger = logging.getLogger("dbus.proxies")
679
    class NullFilter(logging.Filter):
680
        def filter(self, record):
681
            return False
682
    dbus_filter = NullFilter()
683
    dbus_logger.addFilter(dbus_filter)
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
684
    try:
685
        try:
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
686
            mandos_clients = {path: ifs_and_props[client_interface]
687
                              for path, ifs_and_props in
688
                              mandos_serv_object_manager
689
                              .GetManagedObjects().items()
690
                              if client_interface in ifs_and_props}
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
691
        finally:
1005 by Teddy Hogeborn
mandos-ctl: Filter logging instead of messing with stderr
692
            # restore dbus logger
693
            dbus_logger.removeFilter(dbus_filter)
785 by Teddy Hogeborn
Support the standard org.freedesktop.DBus.ObjectManager interface.
694
    except dbus.exceptions.DBusException as e:
987 by Teddy Hogeborn
mandos-ctl: Use logging module instead of print() for errors
695
        log.critical("Failed to access Mandos server through D-Bus:"
696
                     "\n%s", e)
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
697
        sys.exit(1)
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
698
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
699
    # Compile dict of (clients: properties) to process
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
700
    clients = {}
701
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
702
    if options.all or not options.client:
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
703
        clients = {bus.get_object(busname, path): properties
704
                   for path, properties in mandos_clients.items()}
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
705
    else:
706
        for name in options.client:
723.1.4 by Teddy Hogeborn
Use the .items() method instead of .iteritems().
707
            for path, client in mandos_clients.items():
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
708
                if client["Name"] == name:
709
                    client_objc = bus.get_object(busname, path)
710
                    clients[client_objc] = client
711
                    break
24.1.163 by Björn Påhlsson
mandos-client: Added never ending loop for --connect
712
            else:
987 by Teddy Hogeborn
mandos-ctl: Use logging module instead of print() for errors
713
                log.critical("Client not found on server: %r", name)
475 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
714
                sys.exit(1)
872 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
715
1003 by Teddy Hogeborn
mandos-ctl: Separate determining what to do and actually doing it
716
    # Run all commands on clients
717
    for command in commands:
1007 by Teddy Hogeborn
mandos-ctl: Refactor
718
        command.run(mandos_serv, clients)
24.1.163 by Björn Påhlsson
mandos-client: Added never ending loop for --connect
719
984 by Teddy Hogeborn
Make mandos-ctl use unittest instead of doctest module
720

986 by Teddy Hogeborn
Add tests to mandos-ctl's milliseconds_to_string function
721
class Test_milliseconds_to_string(unittest.TestCase):
722
    def test_all(self):
723
        self.assertEqual(milliseconds_to_string(93785000),
724
                         "1T02:03:05")
725
    def test_no_days(self):
726
        self.assertEqual(milliseconds_to_string(7385000), "02:03:05")
727
    def test_all_zero(self):
728
        self.assertEqual(milliseconds_to_string(0), "00:00:00")
729
    def test_no_fractional_seconds(self):
730
        self.assertEqual(milliseconds_to_string(400), "00:00:00")
731
        self.assertEqual(milliseconds_to_string(900), "00:00:00")
732
        self.assertEqual(milliseconds_to_string(1900), "00:00:01")
733
992 by Teddy Hogeborn
mandos-ctl: Add more tests
734
class Test_string_to_delta(unittest.TestCase):
735
    def test_handles_basic_rfc3339(self):
736
        self.assertEqual(string_to_delta("PT2H"),
737
                         datetime.timedelta(0, 7200))
738
    def test_falls_back_to_pre_1_6_1_with_warning(self):
739
        # assertLogs only exists in Python 3.4
740
        if hasattr(self, "assertLogs"):
741
            with self.assertLogs(log, logging.WARNING):
742
                value = string_to_delta("2h")
743
        else:
1006 by Teddy Hogeborn
mandos-ctl: Improve a test when running Python older than 3.4.
744
            class WarningFilter(logging.Filter):
745
                """Don't show, but record the presence of, warnings"""
746
                def filter(self, record):
747
                    is_warning = record.levelno >= logging.WARNING
748
                    self.found = is_warning or getattr(self, "found",
749
                                                       False)
750
                    return not is_warning
751
            warning_filter = WarningFilter()
752
            log.addFilter(warning_filter)
753
            try:
754
                value = string_to_delta("2h")
755
            finally:
756
                log.removeFilter(warning_filter)
757
            self.assertTrue(getattr(warning_filter, "found", False))
992 by Teddy Hogeborn
mandos-ctl: Add more tests
758
        self.assertEqual(value, datetime.timedelta(0, 7200))
759
995 by Teddy Hogeborn
mandos-ctl: Refactor
760
class Test_TableOfClients(unittest.TestCase):
994 by Teddy Hogeborn
mandos-ctl: Add tests for table_rows_of_clients()
761
    def setUp(self):
1004 by Teddy Hogeborn
mandos-ctl: Rename "tablewords" to "tableheaders"
762
        self.tableheaders = {
994 by Teddy Hogeborn
mandos-ctl: Add tests for table_rows_of_clients()
763
            "Attr1": "X",
764
            "AttrTwo": "Yy",
765
            "AttrThree": "Zzz",
766
            "Bool": "A D-BUS Boolean",
767
            "NonDbusBoolean": "A Non-D-BUS Boolean",
768
            "Integer": "An Integer",
769
            "Timeout": "Timedelta 1",
770
            "Interval": "Timedelta 2",
771
            "ApprovalDelay": "Timedelta 3",
772
            "ApprovalDuration": "Timedelta 4",
773
            "ExtendedTimeout": "Timedelta 5",
774
            "String": "A String",
775
        }
776
        self.keywords = ["Attr1", "AttrTwo"]
777
        self.clients = [
778
            {
779
                "Attr1": "x1",
780
                "AttrTwo": "y1",
781
                "AttrThree": "z1",
782
                "Bool": dbus.Boolean(False),
783
                "NonDbusBoolean": False,
784
                "Integer": 0,
785
                "Timeout": 0,
786
                "Interval": 1000,
787
                "ApprovalDelay": 2000,
788
                "ApprovalDuration": 3000,
789
                "ExtendedTimeout": 4000,
790
                "String": "",
791
            },
792
            {
793
                "Attr1": "x2",
794
                "AttrTwo": "y2",
795
                "AttrThree": "z2",
796
                "Bool": dbus.Boolean(True),
797
                "NonDbusBoolean": True,
798
                "Integer": 1,
799
                "Timeout": 93785000,
800
                "Interval": 93786000,
801
                "ApprovalDelay": 93787000,
802
                "ApprovalDuration": 93788000,
803
                "ExtendedTimeout": 93789000,
804
                "String": "A huge string which will not fit," * 10,
805
            },
806
        ]
807
    def test_short_header(self):
999 by Teddy Hogeborn
mandos-ctl: Refactor
808
        text = str(TableOfClients(self.clients, self.keywords,
1004 by Teddy Hogeborn
mandos-ctl: Rename "tablewords" to "tableheaders"
809
                                  self.tableheaders))
999 by Teddy Hogeborn
mandos-ctl: Refactor
810
        expected_text = """
811
X  Yy
812
x1 y1
813
x2 y2
814
"""[1:-1]
815
        self.assertEqual(text, expected_text)
994 by Teddy Hogeborn
mandos-ctl: Add tests for table_rows_of_clients()
816
    def test_booleans(self):
817
        keywords = ["Bool", "NonDbusBoolean"]
999 by Teddy Hogeborn
mandos-ctl: Refactor
818
        text = str(TableOfClients(self.clients, keywords,
1004 by Teddy Hogeborn
mandos-ctl: Rename "tablewords" to "tableheaders"
819
                                  self.tableheaders))
999 by Teddy Hogeborn
mandos-ctl: Refactor
820
        expected_text = """
821
A D-BUS Boolean A Non-D-BUS Boolean
822
No              False              
823
Yes             True               
824
"""[1:-1]
825
        self.assertEqual(text, expected_text)
994 by Teddy Hogeborn
mandos-ctl: Add tests for table_rows_of_clients()
826
    def test_milliseconds_detection(self):
827
        keywords = ["Integer", "Timeout", "Interval", "ApprovalDelay",
828
                    "ApprovalDuration", "ExtendedTimeout"]
999 by Teddy Hogeborn
mandos-ctl: Refactor
829
        text = str(TableOfClients(self.clients, keywords,
1004 by Teddy Hogeborn
mandos-ctl: Rename "tablewords" to "tableheaders"
830
                                  self.tableheaders))
999 by Teddy Hogeborn
mandos-ctl: Refactor
831
        expected_text = """
994 by Teddy Hogeborn
mandos-ctl: Add tests for table_rows_of_clients()
832
An Integer Timedelta 1 Timedelta 2 Timedelta 3 Timedelta 4 Timedelta 5
833
0          00:00:00    00:00:01    00:00:02    00:00:03    00:00:04   
834
1          1T02:03:05  1T02:03:06  1T02:03:07  1T02:03:08  1T02:03:09 
999 by Teddy Hogeborn
mandos-ctl: Refactor
835
"""[1:-1]
836
        self.assertEqual(text, expected_text)
994 by Teddy Hogeborn
mandos-ctl: Add tests for table_rows_of_clients()
837
    def test_empty_and_long_string_values(self):
838
        keywords = ["String"]
999 by Teddy Hogeborn
mandos-ctl: Refactor
839
        text = str(TableOfClients(self.clients, keywords,
1004 by Teddy Hogeborn
mandos-ctl: Rename "tablewords" to "tableheaders"
840
                                  self.tableheaders))
999 by Teddy Hogeborn
mandos-ctl: Refactor
841
        expected_text = """
994 by Teddy Hogeborn
mandos-ctl: Add tests for table_rows_of_clients()
842
A String                                                                                                                                                                                                                                                                                                                                  
843
                                                                                                                                                                                                                                                                                                                                          
844
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,
999 by Teddy Hogeborn
mandos-ctl: Refactor
845
"""[1:-1]
846
        self.assertEqual(text, expected_text)
994 by Teddy Hogeborn
mandos-ctl: Add tests for table_rows_of_clients()
847
848
986 by Teddy Hogeborn
Add tests to mandos-ctl's milliseconds_to_string function
849

984 by Teddy Hogeborn
Make mandos-ctl use unittest instead of doctest module
850
def should_only_run_tests():
851
    parser = argparse.ArgumentParser(add_help=False)
852
    parser.add_argument("--check", action='store_true')
853
    args, unknown_args = parser.parse_known_args()
854
    run_tests = args.check
855
    if run_tests:
856
        # Remove --check argument from sys.argv
857
        sys.argv[1:] = unknown_args
858
    return run_tests
859
860
# Add all tests from doctest strings
861
def load_tests(loader, tests, none):
862
    import doctest
863
    tests.addTests(doctest.DocTestSuite())
864
    return tests
745 by Teddy Hogeborn
mandos-ctl: Do minor formatting and whitespace adjustments.
865
463.1.8 by teddy at bsnet
* mandos-ctl: Use unicode string literals.
866
if __name__ == "__main__":
984 by Teddy Hogeborn
Make mandos-ctl use unittest instead of doctest module
867
    if should_only_run_tests():
868
        # Call using ./tdd-python-script --check [--verbose]
869
        unittest.main()
870
    else:
871
        main()