/mandos/release

To get this branch, use:
bzr branch http://bzr.recompile.se/loggerhead/mandos/release
24.1.116 by Björn Påhlsson
added a mandos list client program
1
#!/usr/bin/python
237.7.533 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*")))); -*-
237.7.420 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
3
#
237.2.207 by Teddy Hogeborn
Update copyright year to "2010" wherever appropriate.
4
# Mandos Monitor - Control and monitor the Mandos server
237.7.420 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
5
#
237.7.517 by Teddy Hogeborn
Update copyright year to 2019
6
# Copyright © 2008-2019 Teddy Hogeborn
7
# Copyright © 2008-2019 Björn Påhlsson
237.7.420 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
8
#
237.7.455 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
237.2.207 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
#
237.7.455 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
237.2.207 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.
237.7.420 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
20
#
237.2.207 by Teddy Hogeborn
Update copyright year to "2010" wherever appropriate.
21
# You should have received a copy of the GNU General Public License
237.7.455 by Teddy Hogeborn
Alter copyright notices slightly. Actual license is unchanged!
22
# along with Mandos.  If not, see <http://www.gnu.org/licenses/>.
237.7.420 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
23
#
237.11.2 by Teddy Hogeborn
Change "fukt.bsnet.se" to "recompile.se" throughout.
24
# Contact the authors at <mandos@recompile.se>.
237.7.420 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
25
#
24.1.116 by Björn Påhlsson
added a mandos list client program
26
237.8.9 by teddy at bsnet
* mandos-ctl: Use print function.
27
from __future__ import (division, absolute_import, print_function,
28
                        unicode_literals)
237.8.8 by teddy at bsnet
* mandos-ctl: Use unicode string literals.
29
237.7.266 by Teddy Hogeborn
mandos-ctl: Make it work in Python 3.
30
try:
31
    from future_builtins import *
32
except ImportError:
33
    pass
237.7.127 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
237.7.23 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
36
import argparse
237.2.3 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
237.7.156 by Teddy Hogeborn
* Makefile (check): Also check mandos-ctl.
41
import collections
237.7.411 by Teddy Hogeborn
mandos-ctl: Implement --dump-json option
42
import json
237.7.532 by Teddy Hogeborn
Make mandos-ctl use unittest instead of doctest module
43
import unittest
237.7.535 by Teddy Hogeborn
mandos-ctl: Use logging module instead of print() for errors
44
import logging
237.7.156 by Teddy Hogeborn
* Makefile (check): Also check mandos-ctl.
45
46
import dbus
237.2.3 by Teddy Hogeborn
Merge "mandos-list" from belorn.
47
237.7.536 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
237.7.535 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
237.7.536 by Teddy Hogeborn
mandos-ctl: Show warnings
57
logging.captureWarnings(True)   # Show warnings via the logging system
58
237.23.7 by Teddy Hogeborn
Use the .major attribute on sys.version_info instead of using "[0]".
59
if sys.version_info.major == 2:
237.7.266 by Teddy Hogeborn
mandos-ctl: Make it work in Python 3.
60
    str = unicode
61
237.8.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"
237.8.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"
377 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
237.7.293 by Teddy Hogeborn
mandos-ctl: Do minor formatting and whitespace adjustments.
71
237.7.333 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
237.7.420 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)
237.7.420 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
237.7.156 by Teddy Hogeborn
* Makefile (check): Also check mandos-ctl.
86
87
def rfc3339_duration_to_delta(duration):
237.7.157 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
237.7.420 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
89
237.7.156 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)
237.7.538 by Teddy Hogeborn
mandos-ctl (rfc3339_duration_to_delta): Improve tests
96
    >>> rfc3339_duration_to_delta("P60M")
97
    datetime.timedelta(1680)
237.7.156 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)
237.7.538 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'
237.7.156 by Teddy Hogeborn
* Makefile (check): Also check mandos-ctl.
135
    """
237.7.420 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
136
237.7.157 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
237.7.156 by Teddy Hogeborn
* Makefile (check): Also check mandos-ctl.
138
    # possible - there would have to be multiple places for the same
237.7.157 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
237.7.156 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.
237.7.420 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
143
237.7.156 by Teddy Hogeborn
* Makefile (check): Also check mandos-ctl.
144
    # New type for defining tokens, syntax, and semantics all-in-one
237.7.301 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
237.7.156 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),
237.7.293 by Teddy Hogeborn
mandos-ctl: Do minor formatting and whitespace adjustments.
155
                         frozenset((token_end, )))
237.7.156 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),
237.7.293 by Teddy Hogeborn
mandos-ctl: Do minor formatting and whitespace adjustments.
177
                       frozenset((token_end, )))
237.7.156 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,
237.7.269 by Teddy Hogeborn
Fix two mutually cancelling bugs.
181
                                      token_week)))
237.7.420 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
182
    # Define starting values:
183
    # Value so far
184
    value = datetime.timedelta()
237.7.156 by Teddy Hogeborn
* Makefile (check): Also check mandos-ctl.
185
    found_token = None
237.7.420 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
186
    # Following valid tokens
187
    followers = frozenset((token_duration, ))
188
    # String left to parse
189
    s = duration
237.7.156 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
237.7.301 by Teddy Hogeborn
mandos-ctl: Generate better messages in exceptions.
211
            raise ValueError("Invalid RFC 3339 duration: {!r}"
212
                             .format(duration))
237.7.156 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):
237.7.549 by Teddy Hogeborn
mandos-ctl: White space changes only
218
    """Parse a string and return a datetime.timedelta"""
237.7.420 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
219
237.7.156 by Teddy Hogeborn
* Makefile (check): Also check mandos-ctl.
220
    try:
221
        return rfc3339_duration_to_delta(interval)
237.7.539 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):
237.7.549 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
237.7.539 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
    """
237.7.420 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
251
237.7.164 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]?)")
237.7.420 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
254
237.14.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
237.7.293 by Teddy Hogeborn
mandos-ctl: Do minor formatting and whitespace adjustments.
270
237.7.543 by Teddy Hogeborn
mandos-ctl: Refactor
271
class TableOfClients(object):
237.7.552 by Teddy Hogeborn
mandos-ctl: Rename "tablewords" to "tableheaders"
272
    tableheaders = {
237.7.544 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
237.7.552 by Teddy Hogeborn
mandos-ctl: Rename "tablewords" to "tableheaders"
295
    def __init__(self, clients, keywords, tableheaders=None):
237.7.543 by Teddy Hogeborn
mandos-ctl: Refactor
296
        self.clients = clients
297
        self.keywords = keywords
237.7.552 by Teddy Hogeborn
mandos-ctl: Rename "tablewords" to "tableheaders"
298
        if tableheaders is not None:
299
            self.tableheaders = tableheaders
237.7.543 by Teddy Hogeborn
mandos-ctl: Refactor
300
237.7.546 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
237.7.545 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(
237.7.552 by Teddy Hogeborn
mandos-ctl: Rename "tablewords" to "tableheaders"
319
            width=max(len(self.tableheaders[key]),
237.7.548 by Teddy Hogeborn
mandos-ctl: Refactor
320
                      *(len(self.string_from_client(client, key))
321
                        for client in self.clients)),
237.7.545 by Teddy Hogeborn
mandos-ctl: Refactor
322
            key=key)
237.7.549 by Teddy Hogeborn
mandos-ctl: White space changes only
323
                        for key in self.keywords)
237.7.545 by Teddy Hogeborn
mandos-ctl: Refactor
324
325
    def string_from_client(self, client, key):
326
        return self.valuetostring(client[key], key)
327
237.7.543 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):
237.7.537 by Teddy Hogeborn
mandos-ctl: Use isinstance(x, X) instead of type(x) is X
330
        if isinstance(value, dbus.Boolean):
237.8.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",
237.7.119 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)
237.7.266 by Teddy Hogeborn
mandos-ctl: Make it work in Python 3.
335
        return str(value)
237.7.420 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
336
237.7.545 by Teddy Hogeborn
mandos-ctl: Refactor
337
    def header_line(self, format_string):
237.7.552 by Teddy Hogeborn
mandos-ctl: Rename "tablewords" to "tableheaders"
338
        return format_string.format(**self.tableheaders)
237.7.545 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})
237.7.293 by Teddy Hogeborn
mandos-ctl: Do minor formatting and whitespace adjustments.
344
237.2.201 by Teddy Hogeborn
* mandos (Client.runtime_expansions): New attribute containing the
345
237.7.551 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"""
237.7.555 by Teddy Hogeborn
mandos-ctl: Refactor
351
    def run(self, mandos, clients):
237.7.551 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."""
237.7.555 by Teddy Hogeborn
mandos-ctl: Refactor
355
        self.mandos = mandos
237.7.551 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")
237.7.555 by Teddy Hogeborn
mandos-ctl: Refactor
368
    def run(self, mandos, clients):
237.7.551 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,
237.2.204 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))
237.7.23 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
516
237.7.293 by Teddy Hogeborn
mandos-ctl: Do minor formatting and whitespace adjustments.
517
237.7.556 by Teddy Hogeborn
mandos-ctl: Refactor
518
def commands_and_clients_from_options():
237.7.23 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",
237.7.420 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
521
                        version="%(prog)s {}".format(version),
237.7.23 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")
237.7.411 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")
237.7.550 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")
237.7.23 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")
237.7.550 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")
237.7.23 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")
237.7.23 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")
237.7.550 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")
237.7.23 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")
237.7.266 by Teddy Hogeborn
mandos-ctl: Make it work in Python 3.
570
    parser.add_argument("-s", "--secret",
571
                        type=argparse.FileType(mode="rb"),
237.7.23 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
572
                        help="Set password blob (file) for client")
237.7.550 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")
237.7.156 by Teddy Hogeborn
* Makefile (check): Also check mandos-ctl.
579
    parser.add_argument("--check", action="store_true",
580
                        help="Run self-test")
237.7.23 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()
237.7.420 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
583
237.7.120 by Teddy Hogeborn
* mandos-ctl: Break long lines.
584
    if has_actions(options) and not (options.client or options.all):
237.7.23 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):
237.7.411 by Teddy Hogeborn
mandos-ctl: Implement --dump-json option
587
        parser.error("--verbose can only be used alone.")
237.7.420 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
588
    if options.dump_json and (options.verbose
589
                              or has_actions(options)):
237.7.411 by Teddy Hogeborn
mandos-ctl: Implement --dump-json option
590
        parser.error("--dump-json can only be used alone.")
237.7.23 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.")
237.7.550 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")
237.7.420 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
595
237.7.551 by Teddy Hogeborn
mandos-ctl: Separate determining what to do and actually doing it
596
    commands = []
597
598
    if options.dump_json:
599
        commands.append(DumpJSONCmd())
600
601
    if options.enable:
602
        commands.append(EnableCmd())
603
604
    if options.disable:
605
        commands.append(DisableCmd())
606
607
    if options.bump_timeout:
608
        commands.append(BumpTimeoutCmd(options.bump_timeout))
609
610
    if options.start_checker:
611
        commands.append(StartCheckerCmd())
612
613
    if options.stop_checker:
614
        commands.append(StopCheckerCmd())
615
616
    if options.is_enabled:
617
        commands.append(IsEnabledCmd())
618
619
    if options.remove:
237.7.555 by Teddy Hogeborn
mandos-ctl: Refactor
620
        commands.append(RemoveCmd())
237.7.551 by Teddy Hogeborn
mandos-ctl: Separate determining what to do and actually doing it
621
622
    if options.checker is not None:
623
        commands.append(SetCheckerCmd())
624
625
    if options.timeout is not None:
626
        commands.append(SetTimeoutCmd(options.timeout))
627
628
    if options.extended_timeout:
629
        commands.append(
630
            SetExtendedTimeoutCmd(options.extended_timeout))
631
632
    if options.interval is not None:
633
        command.append(SetIntervalCmd(options.interval))
634
635
    if options.approved_by_default is not None:
636
        if options.approved_by_default:
637
            command.append(ApproveByDefaultCmd())
638
        else:
639
            command.append(DenyByDefaultCmd())
640
641
    if options.approval_delay is not None:
642
        command.append(SetApprovalDelayCmd(options.approval_delay))
643
644
    if options.approval_duration is not None:
645
        command.append(
646
            SetApprovalDurationCmd(options.approval_duration))
647
648
    if options.host is not None:
649
        command.append(SetHostCmd(options.host))
650
651
    if options.secret is not None:
652
        command.append(SetSecretCmd(options.secret))
653
654
    if options.approve:
655
        commands.append(ApproveCmd())
656
657
    if options.deny:
658
        commands.append(DenyCmd())
659
660
    # If no command option has been given, show table of clients,
661
    # optionally verbosely
662
    if not commands:
663
        commands.append(PrintTableCmd(verbose=options.verbose))
664
237.7.556 by Teddy Hogeborn
mandos-ctl: Refactor
665
    return commands, options.client
666
667
668
def main():
669
    commands, clientnames = commands_and_clients_from_options()
670
671
    try:
672
        bus = dbus.SystemBus()
673
        mandos_dbus_objc = bus.get_object(busname, server_path)
674
    except dbus.exceptions.DBusException:
675
        log.critical("Could not connect to Mandos server")
676
        sys.exit(1)
677
678
    mandos_serv = dbus.Interface(mandos_dbus_objc,
679
                                 dbus_interface=server_interface)
680
    mandos_serv_object_manager = dbus.Interface(
681
        mandos_dbus_objc, dbus_interface=dbus.OBJECT_MANAGER_IFACE)
682
237.7.553 by Teddy Hogeborn
mandos-ctl: Filter logging instead of messing with stderr
683
    # Filter out log message from dbus module
684
    dbus_logger = logging.getLogger("dbus.proxies")
685
    class NullFilter(logging.Filter):
686
        def filter(self, record):
687
            return False
688
    dbus_filter = NullFilter()
689
    dbus_logger.addFilter(dbus_filter)
237.7.23 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
690
    try:
691
        try:
237.7.420 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
692
            mandos_clients = {path: ifs_and_props[client_interface]
693
                              for path, ifs_and_props in
694
                              mandos_serv_object_manager
695
                              .GetManagedObjects().items()
696
                              if client_interface in ifs_and_props}
237.7.23 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
697
        finally:
237.7.553 by Teddy Hogeborn
mandos-ctl: Filter logging instead of messing with stderr
698
            # restore dbus logger
699
            dbus_logger.removeFilter(dbus_filter)
237.7.333 by Teddy Hogeborn
Support the standard org.freedesktop.DBus.ObjectManager interface.
700
    except dbus.exceptions.DBusException as e:
237.7.535 by Teddy Hogeborn
mandos-ctl: Use logging module instead of print() for errors
701
        log.critical("Failed to access Mandos server through D-Bus:"
702
                     "\n%s", e)
237.7.23 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
703
        sys.exit(1)
237.7.420 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
704
237.7.23 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
705
    # Compile dict of (clients: properties) to process
237.7.420 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
706
    clients = {}
707
237.7.556 by Teddy Hogeborn
mandos-ctl: Refactor
708
    if not clientnames:
237.7.420 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
709
        clients = {bus.get_object(busname, path): properties
710
                   for path, properties in mandos_clients.items()}
237.7.23 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
711
    else:
237.7.556 by Teddy Hogeborn
mandos-ctl: Refactor
712
        for name in clientnames:
237.23.4 by Teddy Hogeborn
Use the .items() method instead of .iteritems().
713
            for path, client in mandos_clients.items():
237.7.23 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
714
                if client["Name"] == name:
715
                    client_objc = bus.get_object(busname, path)
716
                    clients[client_objc] = client
717
                    break
24.1.163 by Björn Påhlsson
mandos-client: Added never ending loop for --connect
718
            else:
237.7.535 by Teddy Hogeborn
mandos-ctl: Use logging module instead of print() for errors
719
                log.critical("Client not found on server: %r", name)
237.7.23 by teddy at bsnet
* mandos-ctl: Use the new argparse library instead of optparse.
720
                sys.exit(1)
237.7.420 by Teddy Hogeborn
PEP8 compliance: mandos-ctl
721
237.7.551 by Teddy Hogeborn
mandos-ctl: Separate determining what to do and actually doing it
722
    # Run all commands on clients
723
    for command in commands:
237.7.555 by Teddy Hogeborn
mandos-ctl: Refactor
724
        command.run(mandos_serv, clients)
24.1.163 by Björn Påhlsson
mandos-client: Added never ending loop for --connect
725
237.7.532 by Teddy Hogeborn
Make mandos-ctl use unittest instead of doctest module
726

237.7.534 by Teddy Hogeborn
Add tests to mandos-ctl's milliseconds_to_string function
727
class Test_milliseconds_to_string(unittest.TestCase):
728
    def test_all(self):
729
        self.assertEqual(milliseconds_to_string(93785000),
730
                         "1T02:03:05")
731
    def test_no_days(self):
732
        self.assertEqual(milliseconds_to_string(7385000), "02:03:05")
733
    def test_all_zero(self):
734
        self.assertEqual(milliseconds_to_string(0), "00:00:00")
735
    def test_no_fractional_seconds(self):
736
        self.assertEqual(milliseconds_to_string(400), "00:00:00")
737
        self.assertEqual(milliseconds_to_string(900), "00:00:00")
738
        self.assertEqual(milliseconds_to_string(1900), "00:00:01")
739
237.7.540 by Teddy Hogeborn
mandos-ctl: Add more tests
740
class Test_string_to_delta(unittest.TestCase):
741
    def test_handles_basic_rfc3339(self):
742
        self.assertEqual(string_to_delta("PT2H"),
743
                         datetime.timedelta(0, 7200))
744
    def test_falls_back_to_pre_1_6_1_with_warning(self):
745
        # assertLogs only exists in Python 3.4
746
        if hasattr(self, "assertLogs"):
747
            with self.assertLogs(log, logging.WARNING):
748
                value = string_to_delta("2h")
749
        else:
237.7.554 by Teddy Hogeborn
mandos-ctl: Improve a test when running Python older than 3.4.
750
            class WarningFilter(logging.Filter):
751
                """Don't show, but record the presence of, warnings"""
752
                def filter(self, record):
753
                    is_warning = record.levelno >= logging.WARNING
754
                    self.found = is_warning or getattr(self, "found",
755
                                                       False)
756
                    return not is_warning
757
            warning_filter = WarningFilter()
758
            log.addFilter(warning_filter)
759
            try:
760
                value = string_to_delta("2h")
761
            finally:
762
                log.removeFilter(warning_filter)
763
            self.assertTrue(getattr(warning_filter, "found", False))
237.7.540 by Teddy Hogeborn
mandos-ctl: Add more tests
764
        self.assertEqual(value, datetime.timedelta(0, 7200))
765
237.7.543 by Teddy Hogeborn
mandos-ctl: Refactor
766
class Test_TableOfClients(unittest.TestCase):
237.7.542 by Teddy Hogeborn
mandos-ctl: Add tests for table_rows_of_clients()
767
    def setUp(self):
237.7.552 by Teddy Hogeborn
mandos-ctl: Rename "tablewords" to "tableheaders"
768
        self.tableheaders = {
237.7.542 by Teddy Hogeborn
mandos-ctl: Add tests for table_rows_of_clients()
769
            "Attr1": "X",
770
            "AttrTwo": "Yy",
771
            "AttrThree": "Zzz",
772
            "Bool": "A D-BUS Boolean",
773
            "NonDbusBoolean": "A Non-D-BUS Boolean",
774
            "Integer": "An Integer",
775
            "Timeout": "Timedelta 1",
776
            "Interval": "Timedelta 2",
777
            "ApprovalDelay": "Timedelta 3",
778
            "ApprovalDuration": "Timedelta 4",
779
            "ExtendedTimeout": "Timedelta 5",
780
            "String": "A String",
781
        }
782
        self.keywords = ["Attr1", "AttrTwo"]
783
        self.clients = [
784
            {
785
                "Attr1": "x1",
786
                "AttrTwo": "y1",
787
                "AttrThree": "z1",
788
                "Bool": dbus.Boolean(False),
789
                "NonDbusBoolean": False,
790
                "Integer": 0,
791
                "Timeout": 0,
792
                "Interval": 1000,
793
                "ApprovalDelay": 2000,
794
                "ApprovalDuration": 3000,
795
                "ExtendedTimeout": 4000,
796
                "String": "",
797
            },
798
            {
799
                "Attr1": "x2",
800
                "AttrTwo": "y2",
801
                "AttrThree": "z2",
802
                "Bool": dbus.Boolean(True),
803
                "NonDbusBoolean": True,
804
                "Integer": 1,
805
                "Timeout": 93785000,
806
                "Interval": 93786000,
807
                "ApprovalDelay": 93787000,
808
                "ApprovalDuration": 93788000,
809
                "ExtendedTimeout": 93789000,
810
                "String": "A huge string which will not fit," * 10,
811
            },
812
        ]
813
    def test_short_header(self):
237.7.547 by Teddy Hogeborn
mandos-ctl: Refactor
814
        text = str(TableOfClients(self.clients, self.keywords,
237.7.552 by Teddy Hogeborn
mandos-ctl: Rename "tablewords" to "tableheaders"
815
                                  self.tableheaders))
237.7.547 by Teddy Hogeborn
mandos-ctl: Refactor
816
        expected_text = """
817
X  Yy
818
x1 y1
819
x2 y2
820
"""[1:-1]
821
        self.assertEqual(text, expected_text)
237.7.542 by Teddy Hogeborn
mandos-ctl: Add tests for table_rows_of_clients()
822
    def test_booleans(self):
823
        keywords = ["Bool", "NonDbusBoolean"]
237.7.547 by Teddy Hogeborn
mandos-ctl: Refactor
824
        text = str(TableOfClients(self.clients, keywords,
237.7.552 by Teddy Hogeborn
mandos-ctl: Rename "tablewords" to "tableheaders"
825
                                  self.tableheaders))
237.7.547 by Teddy Hogeborn
mandos-ctl: Refactor
826
        expected_text = """
827
A D-BUS Boolean A Non-D-BUS Boolean
828
No              False              
829
Yes             True               
830
"""[1:-1]
831
        self.assertEqual(text, expected_text)
237.7.542 by Teddy Hogeborn
mandos-ctl: Add tests for table_rows_of_clients()
832
    def test_milliseconds_detection(self):
833
        keywords = ["Integer", "Timeout", "Interval", "ApprovalDelay",
834
                    "ApprovalDuration", "ExtendedTimeout"]
237.7.547 by Teddy Hogeborn
mandos-ctl: Refactor
835
        text = str(TableOfClients(self.clients, keywords,
237.7.552 by Teddy Hogeborn
mandos-ctl: Rename "tablewords" to "tableheaders"
836
                                  self.tableheaders))
237.7.547 by Teddy Hogeborn
mandos-ctl: Refactor
837
        expected_text = """
237.7.542 by Teddy Hogeborn
mandos-ctl: Add tests for table_rows_of_clients()
838
An Integer Timedelta 1 Timedelta 2 Timedelta 3 Timedelta 4 Timedelta 5
839
0          00:00:00    00:00:01    00:00:02    00:00:03    00:00:04   
840
1          1T02:03:05  1T02:03:06  1T02:03:07  1T02:03:08  1T02:03:09 
237.7.547 by Teddy Hogeborn
mandos-ctl: Refactor
841
"""[1:-1]
842
        self.assertEqual(text, expected_text)
237.7.542 by Teddy Hogeborn
mandos-ctl: Add tests for table_rows_of_clients()
843
    def test_empty_and_long_string_values(self):
844
        keywords = ["String"]
237.7.547 by Teddy Hogeborn
mandos-ctl: Refactor
845
        text = str(TableOfClients(self.clients, keywords,
237.7.552 by Teddy Hogeborn
mandos-ctl: Rename "tablewords" to "tableheaders"
846
                                  self.tableheaders))
237.7.547 by Teddy Hogeborn
mandos-ctl: Refactor
847
        expected_text = """
237.7.542 by Teddy Hogeborn
mandos-ctl: Add tests for table_rows_of_clients()
848
A String                                                                                                                                                                                                                                                                                                                                  
849
                                                                                                                                                                                                                                                                                                                                          
850
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,
237.7.547 by Teddy Hogeborn
mandos-ctl: Refactor
851
"""[1:-1]
852
        self.assertEqual(text, expected_text)
237.7.542 by Teddy Hogeborn
mandos-ctl: Add tests for table_rows_of_clients()
853
854
237.7.534 by Teddy Hogeborn
Add tests to mandos-ctl's milliseconds_to_string function
855

237.7.532 by Teddy Hogeborn
Make mandos-ctl use unittest instead of doctest module
856
def should_only_run_tests():
857
    parser = argparse.ArgumentParser(add_help=False)
858
    parser.add_argument("--check", action='store_true')
859
    args, unknown_args = parser.parse_known_args()
860
    run_tests = args.check
861
    if run_tests:
862
        # Remove --check argument from sys.argv
863
        sys.argv[1:] = unknown_args
864
    return run_tests
865
866
# Add all tests from doctest strings
867
def load_tests(loader, tests, none):
868
    import doctest
869
    tests.addTests(doctest.DocTestSuite())
870
    return tests
237.7.293 by Teddy Hogeborn
mandos-ctl: Do minor formatting and whitespace adjustments.
871
237.8.8 by teddy at bsnet
* mandos-ctl: Use unicode string literals.
872
if __name__ == "__main__":
237.7.532 by Teddy Hogeborn
Make mandos-ctl use unittest instead of doctest module
873
    if should_only_run_tests():
874
        # Call using ./tdd-python-script --check [--verbose]
875
        unittest.main()
876
    else:
877
        main()