/mandos/trunk

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

« back to all changes in this revision

Viewing changes to mandos-monitor

  • Committer: Teddy Hogeborn
  • Date: 2009-11-10 03:45:18 UTC
  • Revision ID: teddy@fukt.bsnet.se-20091110034518-2ofins935ffqbbse
* mandos-monitor (MandosClientWidget): Change "StopChecker" key to "S"
                                       and changed "CheckedOK" key to
                                       "C".
  (UserInterface.__init__): Show version number and brief help on start.
  (UserInterface.process_input): Help key now shows help in log
                                 buffer, and shows log buffer if
                                 hidden.  Commented out
                                 non-implemented keys.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#!/usr/bin/python
2
2
# -*- mode: python; coding: utf-8 -*-
3
 
4
 
# Mandos Monitor - Control and monitor the Mandos server
5
 
6
 
# Copyright © 2009-2015 Teddy Hogeborn
7
 
# Copyright © 2009-2015 Björn Påhlsson
8
 
9
 
# This program is free software: you can redistribute it and/or modify
10
 
# it under the terms of the GNU General Public License as published by
11
 
# the Free Software Foundation, either version 3 of the License, or
12
 
# (at your option) any later version.
13
 
#
14
 
#     This program is distributed in the hope that it will be useful,
15
 
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
 
#     GNU General Public License for more details.
18
 
19
 
# You should have received a copy of the GNU General Public License
20
 
# along with this program.  If not, see
21
 
# <http://www.gnu.org/licenses/>.
22
 
23
 
# Contact the authors at <mandos@recompile.se>.
24
 
25
3
 
26
 
from __future__ import (division, absolute_import, print_function,
27
 
                        unicode_literals)
28
 
try:
29
 
    from future_builtins import *
30
 
except ImportError:
31
 
    pass
 
4
from __future__ import division, absolute_import, with_statement
32
5
 
33
6
import sys
34
 
import os
35
 
 
36
 
import datetime
 
7
import signal
37
8
 
38
9
import urwid.curses_display
39
10
import urwid
40
11
 
41
12
from dbus.mainloop.glib import DBusGMainLoop
42
 
try:
43
 
    import gobject
44
 
except ImportError:
45
 
    from gi.repository import GObject as gobject
 
13
import gobject
46
14
 
47
15
import dbus
48
16
 
49
 
import locale
50
 
 
51
 
if sys.version_info.major == 2:
52
 
    str = unicode
53
 
 
54
 
locale.setlocale(locale.LC_ALL, '')
55
 
 
56
 
import logging
57
 
logging.getLogger('dbus.proxies').setLevel(logging.CRITICAL)
 
17
import UserList
58
18
 
59
19
# Some useful constants
60
 
domain = 'se.recompile'
 
20
domain = 'se.bsnet.fukt'
61
21
server_interface = domain + '.Mandos'
62
22
client_interface = domain + '.Mandos.Client'
63
 
version = "1.6.9"
64
 
 
65
 
def isoformat_to_datetime(iso):
66
 
    "Parse an ISO 8601 date string to a datetime.datetime()"
67
 
    if not iso:
68
 
        return None
69
 
    d, t = iso.split("T", 1)
70
 
    year, month, day = d.split("-", 2)
71
 
    hour, minute, second = t.split(":", 2)
72
 
    second, fraction = divmod(float(second), 1)
73
 
    return datetime.datetime(int(year),
74
 
                             int(month),
75
 
                             int(day),
76
 
                             int(hour),
77
 
                             int(minute),
78
 
                             int(second),           # Whole seconds
79
 
                             int(fraction*1000000)) # Microseconds
 
23
version = "1.0.14"
 
24
 
 
25
# Always run in monochrome mode
 
26
urwid.curses_display.curses.has_colors = lambda : False
 
27
 
 
28
# Urwid doesn't support blinking, but we want it.  Since we have no
 
29
# use for underline on its own, we make underline also always blink.
 
30
urwid.curses_display.curses.A_UNDERLINE |= (
 
31
    urwid.curses_display.curses.A_BLINK)
80
32
 
81
33
class MandosClientPropertyCache(object):
82
34
    """This wraps a Mandos Client D-Bus proxy object, caches the
83
35
    properties and calls a hook function when any of them are
84
36
    changed.
85
37
    """
86
 
    def __init__(self, proxy_object=None, properties=None, **kwargs):
 
38
    def __init__(self, proxy_object=None, properties=None, *args,
 
39
                 **kwargs):
87
40
        self.proxy = proxy_object # Mandos Client proxy object
88
 
        self.properties = dict() if properties is None else properties
89
 
        self.property_changed_match = (
90
 
            self.proxy.connect_to_signal("PropertiesChanged",
91
 
                                         self.properties_changed,
92
 
                                         dbus.PROPERTIES_IFACE,
93
 
                                         byte_arrays=True))
94
 
        
95
 
        if properties is None:
96
 
            self.properties.update(
97
 
                self.proxy.GetAll(client_interface,
98
 
                                  dbus_interface
99
 
                                  = dbus.PROPERTIES_IFACE))
100
 
        
101
 
        super(MandosClientPropertyCache, self).__init__(**kwargs)
 
41
        
 
42
        if properties is None:
 
43
            self.properties = dict()
 
44
        else:
 
45
            self.properties = properties
 
46
        self.proxy.connect_to_signal("PropertyChanged",
 
47
                                     self.property_changed,
 
48
                                     client_interface,
 
49
                                     byte_arrays=True)
 
50
        
 
51
        if properties is None:
 
52
            self.properties.update(self.proxy.GetAll(client_interface,
 
53
                                                     dbus_interface =
 
54
                                                     dbus.PROPERTIES_IFACE))
 
55
        super(MandosClientPropertyCache, self).__init__(
 
56
            proxy_object=proxy_object,
 
57
            properties=properties, *args, **kwargs)
102
58
    
103
 
    def properties_changed(self, interface, properties, invalidated):
104
 
        """This is called whenever we get a PropertiesChanged signal
105
 
        It updates the changed properties in the "properties" dict.
 
59
    def property_changed(self, property=None, value=None):
 
60
        """This is called whenever we get a PropertyChanged signal
 
61
        It updates the changed property in the "properties" dict.
106
62
        """
107
63
        # Update properties dict with new value
108
 
        self.properties.update(properties)
109
 
    
110
 
    def delete(self):
111
 
        self.property_changed_match.remove()
 
64
        self.properties[property] = value
112
65
 
113
66
 
114
67
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
116
69
    """
117
70
    
118
71
    def __init__(self, server_proxy_object=None, update_hook=None,
119
 
                 delete_hook=None, logger=None, **kwargs):
 
72
                 delete_hook=None, *args, **kwargs):
120
73
        # Called on update
121
74
        self.update_hook = update_hook
122
75
        # Called on delete
123
76
        self.delete_hook = delete_hook
124
77
        # Mandos Server proxy object
125
78
        self.server_proxy_object = server_proxy_object
126
 
        # Logger
127
 
        self.logger = logger
128
 
        
129
 
        self._update_timer_callback_tag = None
130
79
        
131
80
        # The widget shown normally
132
81
        self._text_widget = urwid.Text("")
133
82
        # The widget shown when we have focus
134
83
        self._focus_text_widget = urwid.Text("")
135
 
        super(MandosClientWidget, self).__init__(**kwargs)
 
84
        super(MandosClientWidget, self).__init__(
 
85
            update_hook=update_hook, delete_hook=delete_hook,
 
86
            *args, **kwargs)
136
87
        self.update()
137
88
        self.opened = False
138
 
        
139
 
        self.match_objects = (
140
 
            self.proxy.connect_to_signal("CheckerCompleted",
141
 
                                         self.checker_completed,
142
 
                                         client_interface,
143
 
                                         byte_arrays=True),
144
 
            self.proxy.connect_to_signal("CheckerStarted",
145
 
                                         self.checker_started,
146
 
                                         client_interface,
147
 
                                         byte_arrays=True),
148
 
            self.proxy.connect_to_signal("GotSecret",
149
 
                                         self.got_secret,
150
 
                                         client_interface,
151
 
                                         byte_arrays=True),
152
 
            self.proxy.connect_to_signal("NeedApproval",
153
 
                                         self.need_approval,
154
 
                                         client_interface,
155
 
                                         byte_arrays=True),
156
 
            self.proxy.connect_to_signal("Rejected",
157
 
                                         self.rejected,
158
 
                                         client_interface,
159
 
                                         byte_arrays=True))
160
 
        self.logger('Created client {}'
161
 
                    .format(self.properties["Name"]), level=0)
162
 
    
163
 
    def using_timer(self, flag):
164
 
        """Call this method with True or False when timer should be
165
 
        activated or deactivated.
166
 
        """
167
 
        if flag and self._update_timer_callback_tag is None:
168
 
            # Will update the shown timer value every second
169
 
            self._update_timer_callback_tag = (gobject.timeout_add
170
 
                                               (1000,
171
 
                                                self.update_timer))
172
 
        elif not (flag or self._update_timer_callback_tag is None):
173
 
            gobject.source_remove(self._update_timer_callback_tag)
174
 
            self._update_timer_callback_tag = None
175
 
    
176
 
    def checker_completed(self, exitstatus, condition, command):
177
 
        if exitstatus == 0:
178
 
            self.logger('Checker for client {} (command "{}")'
179
 
                        ' succeeded'.format(self.properties["Name"],
180
 
                                            command), level=0)
181
 
            self.update()
182
 
            return
183
 
        # Checker failed
184
 
        if os.WIFEXITED(condition):
185
 
            self.logger('Checker for client {} (command "{}") failed'
186
 
                        ' with exit code {}'
187
 
                        .format(self.properties["Name"], command,
188
 
                                os.WEXITSTATUS(condition)))
189
 
        elif os.WIFSIGNALED(condition):
190
 
            self.logger('Checker for client {} (command "{}") was'
191
 
                        ' killed by signal {}'
192
 
                        .format(self.properties["Name"], command,
193
 
                                os.WTERMSIG(condition)))
194
 
        self.update()
195
 
    
196
 
    def checker_started(self, command):
197
 
        """Server signals that a checker started."""
198
 
        self.logger('Client {} started checker "{}"'
199
 
                    .format(self.properties["Name"],
200
 
                            command), level=0)
201
 
    
202
 
    def got_secret(self):
203
 
        self.logger('Client {} received its secret'
204
 
                    .format(self.properties["Name"]))
205
 
    
206
 
    def need_approval(self, timeout, default):
207
 
        if not default:
208
 
            message = 'Client {} needs approval within {} seconds'
209
 
        else:
210
 
            message = 'Client {} will get its secret in {} seconds'
211
 
        self.logger(message.format(self.properties["Name"],
212
 
                                   timeout/1000))
213
 
    
214
 
    def rejected(self, reason):
215
 
        self.logger('Client {} was rejected; reason: {}'
216
 
                    .format(self.properties["Name"], reason))
217
89
    
218
90
    def selectable(self):
219
91
        """Make this a "selectable" widget.
220
92
        This overrides the method from urwid.FlowWidget."""
221
93
        return True
222
94
    
223
 
    def rows(self, maxcolrow, focus=False):
 
95
    def rows(self, (maxcol,), focus=False):
224
96
        """How many rows this widget will occupy might depend on
225
97
        whether we have focus or not.
226
98
        This overrides the method from urwid.FlowWidget"""
227
 
        return self.current_widget(focus).rows(maxcolrow, focus=focus)
 
99
        return self.current_widget(focus).rows((maxcol,), focus=focus)
228
100
    
229
101
    def current_widget(self, focus=False):
230
102
        if focus or self.opened:
234
106
    def update(self):
235
107
        "Called when what is visible on the screen should be updated."
236
108
        # How to add standout mode to a style
237
 
        with_standout = { "normal": "standout",
238
 
                          "bold": "bold-standout",
239
 
                          "underline-blink":
240
 
                              "underline-blink-standout",
241
 
                          "bold-underline-blink":
242
 
                              "bold-underline-blink-standout",
 
109
        with_standout = { u"normal": u"standout",
 
110
                          u"bold": u"bold-standout",
 
111
                          u"underline-blink":
 
112
                              u"underline-blink-standout",
 
113
                          u"bold-underline-blink":
 
114
                              u"bold-underline-blink-standout",
243
115
                          }
244
116
        
245
117
        # Rebuild focus and non-focus widgets using current properties
246
 
        
247
 
        # Base part of a client. Name!
248
 
        base = '{name}: '.format(name=self.properties["Name"])
249
 
        if not self.properties["Enabled"]:
250
 
            message = "DISABLED"
251
 
            self.using_timer(False)
252
 
        elif self.properties["ApprovalPending"]:
253
 
            timeout = datetime.timedelta(milliseconds
254
 
                                         = self.properties
255
 
                                         ["ApprovalDelay"])
256
 
            last_approval_request = isoformat_to_datetime(
257
 
                self.properties["LastApprovalRequest"])
258
 
            if last_approval_request is not None:
259
 
                timer = max(timeout - (datetime.datetime.utcnow()
260
 
                                       - last_approval_request),
261
 
                            datetime.timedelta())
262
 
            else:
263
 
                timer = datetime.timedelta()
264
 
            if self.properties["ApprovedByDefault"]:
265
 
                message = "Approval in {}. (d)eny?"
266
 
            else:
267
 
                message = "Denial in {}. (a)pprove?"
268
 
            message = message.format(str(timer).rsplit(".", 1)[0])
269
 
            self.using_timer(True)
270
 
        elif self.properties["LastCheckerStatus"] != 0:
271
 
            # When checker has failed, show timer until client expires
272
 
            expires = self.properties["Expires"]
273
 
            if expires == "":
274
 
                timer = datetime.timedelta(0)
275
 
            else:
276
 
                expires = (datetime.datetime.strptime
277
 
                           (expires, '%Y-%m-%dT%H:%M:%S.%f'))
278
 
                timer = max(expires - datetime.datetime.utcnow(),
279
 
                            datetime.timedelta())
280
 
            message = ('A checker has failed! Time until client'
281
 
                       ' gets disabled: {}'
282
 
                       .format(str(timer).rsplit(".", 1)[0]))
283
 
            self.using_timer(True)
284
 
        else:
285
 
            message = "enabled"
286
 
            self.using_timer(False)
287
 
        self._text = "{}{}".format(base, message)
288
 
        
 
118
        self._text = (u'name="%(name)s", enabled=%(enabled)s'
 
119
                      % self.properties)
289
120
        if not urwid.supports_unicode():
290
121
            self._text = self._text.encode("ascii", "replace")
291
 
        textlist = [("normal", self._text)]
 
122
        textlist = [(u"normal", u"BLARGH: "), (u"bold", self._text)]
292
123
        self._text_widget.set_text(textlist)
293
124
        self._focus_text_widget.set_text([(with_standout[text[0]],
294
125
                                           text[1])
302
133
        if self.update_hook is not None:
303
134
            self.update_hook()
304
135
    
305
 
    def update_timer(self):
306
 
        """called by gobject. Will indefinitely loop until
307
 
        gobject.source_remove() on tag is called"""
308
 
        self.update()
309
 
        return True             # Keep calling this
310
 
    
311
 
    def delete(self, **kwargs):
312
 
        if self._update_timer_callback_tag is not None:
313
 
            gobject.source_remove(self._update_timer_callback_tag)
314
 
            self._update_timer_callback_tag = None
315
 
        for match in self.match_objects:
316
 
            match.remove()
317
 
        self.match_objects = ()
 
136
    def delete(self):
318
137
        if self.delete_hook is not None:
319
138
            self.delete_hook(self)
320
 
        return super(MandosClientWidget, self).delete(**kwargs)
321
139
    
322
 
    def render(self, maxcolrow, focus=False):
 
140
    def render(self, (maxcol,), focus=False):
323
141
        """Render differently if we have focus.
324
142
        This overrides the method from urwid.FlowWidget"""
325
 
        return self.current_widget(focus).render(maxcolrow,
 
143
        return self.current_widget(focus).render((maxcol,),
326
144
                                                 focus=focus)
327
145
    
328
 
    def keypress(self, maxcolrow, key):
 
146
    def keypress(self, (maxcol,), key):
329
147
        """Handle keys.
330
148
        This overrides the method from urwid.FlowWidget"""
331
 
        if key == "+":
332
 
            self.proxy.Set(client_interface, "Enabled",
333
 
                           dbus.Boolean(True), ignore_reply = True,
334
 
                           dbus_interface = dbus.PROPERTIES_IFACE)
335
 
        elif key == "-":
336
 
            self.proxy.Set(client_interface, "Enabled", False,
337
 
                           ignore_reply = True,
338
 
                           dbus_interface = dbus.PROPERTIES_IFACE)
339
 
        elif key == "a":
340
 
            self.proxy.Approve(dbus.Boolean(True, variant_level=1),
341
 
                               dbus_interface = client_interface,
342
 
                               ignore_reply=True)
343
 
        elif key == "d":
344
 
            self.proxy.Approve(dbus.Boolean(False, variant_level=1),
345
 
                                  dbus_interface = client_interface,
346
 
                               ignore_reply=True)
347
 
        elif key == "R" or key == "_" or key == "ctrl k":
 
149
        if key == u"e" or key == u"+":
 
150
            self.proxy.Enable()
 
151
        elif key == u"d" or key == u"-":
 
152
            self.proxy.Disable()
 
153
        elif key == u"r" or key == u"_":
348
154
            self.server_proxy_object.RemoveClient(self.proxy
349
 
                                                  .object_path,
350
 
                                                  ignore_reply=True)
351
 
        elif key == "s":
352
 
            self.proxy.Set(client_interface, "CheckerRunning",
353
 
                           dbus.Boolean(True), ignore_reply = True,
354
 
                           dbus_interface = dbus.PROPERTIES_IFACE)
355
 
        elif key == "S":
356
 
            self.proxy.Set(client_interface, "CheckerRunning",
357
 
                           dbus.Boolean(False), ignore_reply = True,
358
 
                           dbus_interface = dbus.PROPERTIES_IFACE)
359
 
        elif key == "C":
360
 
            self.proxy.CheckedOK(dbus_interface = client_interface,
361
 
                                 ignore_reply=True)
 
155
                                                  .object_path)
 
156
        elif key == u"s":
 
157
            self.proxy.StartChecker()
 
158
        elif key == u"S":
 
159
            self.proxy.StopChecker()
 
160
        elif key == u"C":
 
161
            self.proxy.CheckedOK()
362
162
        # xxx
363
 
#         elif key == "p" or key == "=":
 
163
#         elif key == u"p" or key == "=":
364
164
#             self.proxy.pause()
365
 
#         elif key == "u" or key == ":":
 
165
#         elif key == u"u" or key == ":":
366
166
#             self.proxy.unpause()
367
 
#         elif key == "RET":
 
167
#         elif key == u"RET":
368
168
#             self.open()
369
169
        else:
370
170
            return key
371
171
    
372
 
    def properties_changed(self, interface, properties, invalidated):
373
 
        """Call self.update() if any properties changed.
 
172
    def property_changed(self, property=None, value=None,
 
173
                         *args, **kwargs):
 
174
        """Call self.update() if old value is not new value.
374
175
        This overrides the method from MandosClientPropertyCache"""
375
 
        old_values = { key: self.properties.get(key)
376
 
                       for key in properties.keys() }
377
 
        super(MandosClientWidget, self).properties_changed(
378
 
            interface, properties, invalidated)
379
 
        if any(old_values[key] != self.properties.get(key)
380
 
               for key in old_values):
 
176
        property_name = unicode(property)
 
177
        old_value = self.properties.get(property_name)
 
178
        super(MandosClientWidget, self).property_changed(
 
179
            property=property, value=value, *args, **kwargs)
 
180
        if self.properties.get(property_name) != old_value:
381
181
            self.update()
382
182
 
383
183
 
386
186
    "down" key presses, thus not allowing any containing widgets to
387
187
    use them as an excuse to shift focus away from this widget.
388
188
    """
389
 
    def keypress(self, *args, **kwargs):
390
 
        ret = super(ConstrainedListBox, self).keypress(*args, **kwargs)
391
 
        if ret in ("up", "down"):
 
189
    def keypress(self, (maxcol, maxrow), key):
 
190
        ret = super(ConstrainedListBox, self).keypress((maxcol, maxrow), key)
 
191
        if ret in (u"up", u"down"):
392
192
            return
393
193
        return ret
394
194
 
397
197
    """This is the entire user interface - the whole screen
398
198
    with boxes, lists of client widgets, etc.
399
199
    """
400
 
    def __init__(self, max_log_length=1000, log_level=1):
 
200
    def __init__(self, max_log_length=1000):
401
201
        DBusGMainLoop(set_as_default=True)
402
202
        
403
203
        self.screen = urwid.curses_display.Screen()
404
204
        
405
205
        self.screen.register_palette((
406
 
                ("normal",
407
 
                 "default", "default", None),
408
 
                ("bold",
409
 
                 "bold", "default", "bold"),
410
 
                ("underline-blink",
411
 
                 "underline,blink", "default", "underline,blink"),
412
 
                ("standout",
413
 
                 "standout", "default", "standout"),
414
 
                ("bold-underline-blink",
415
 
                 "bold,underline,blink", "default", "bold,underline,blink"),
416
 
                ("bold-standout",
417
 
                 "bold,standout", "default", "bold,standout"),
418
 
                ("underline-blink-standout",
419
 
                 "underline,blink,standout", "default",
420
 
                 "underline,blink,standout"),
421
 
                ("bold-underline-blink-standout",
422
 
                 "bold,underline,blink,standout", "default",
423
 
                 "bold,underline,blink,standout"),
 
206
                (u"normal",
 
207
                 u"default", u"default", None),
 
208
                (u"bold",
 
209
                 u"default", u"default", u"bold"),
 
210
                (u"underline-blink",
 
211
                 u"default", u"default", u"underline"),
 
212
                (u"standout",
 
213
                 u"default", u"default", u"standout"),
 
214
                (u"bold-underline-blink",
 
215
                 u"default", u"default", (u"bold", u"underline")),
 
216
                (u"bold-standout",
 
217
                 u"default", u"default", (u"bold", u"standout")),
 
218
                (u"underline-blink-standout",
 
219
                 u"default", u"default", (u"underline", u"standout")),
 
220
                (u"bold-underline-blink-standout",
 
221
                 u"default", u"default", (u"bold", u"underline",
 
222
                                          u"standout")),
424
223
                ))
425
224
        
426
225
        if urwid.supports_unicode():
427
 
            self.divider = "─" # \u2500
428
 
            #self.divider = "━" # \u2501
 
226
            #self.divider = u"─" # \u2500
 
227
            self.divider = u"━" # \u2501
429
228
        else:
430
 
            #self.divider = "-" # \u002d
431
 
            self.divider = "_" # \u005f
 
229
            #self.divider = u"-" # \u002d
 
230
            self.divider = u"_" # \u005f
432
231
        
433
232
        self.screen.start()
434
233
        
441
240
        self.log = []
442
241
        self.max_log_length = max_log_length
443
242
        
444
 
        self.log_level = log_level
445
 
        
446
243
        # We keep a reference to the log widget so we can remove it
447
244
        # from the ListWalker without it getting destroyed
448
245
        self.logbox = ConstrainedListBox(self.log)
450
247
        # This keeps track of whether self.uilist currently has
451
248
        # self.logbox in it or not
452
249
        self.log_visible = True
453
 
        self.log_wrap = "any"
 
250
        self.log_wrap = u"any"
454
251
        
455
252
        self.rebuild()
456
 
        self.log_message_raw(("bold",
457
 
                              "Mandos Monitor version " + version))
458
 
        self.log_message_raw(("bold",
459
 
                              "q: Quit  ?: Help"))
 
253
        self.log_message((u"bold",
 
254
                          u"Mandos Monitor version " + version))
 
255
        self.log_message((u"bold",
 
256
                          u"q: Quit  ?: Help"))
460
257
        
461
258
        self.busname = domain + '.Mandos'
462
259
        self.main_loop = gobject.MainLoop()
463
 
    
464
 
    def client_not_found(self, fingerprint, address):
465
 
        self.log_message("Client with address {} and fingerprint {}"
466
 
                         " could not be found"
467
 
                         .format(address, fingerprint))
 
260
        self.bus = dbus.SystemBus()
 
261
        mandos_dbus_objc = self.bus.get_object(
 
262
            self.busname, u"/", follow_name_owner_changes=True)
 
263
        self.mandos_serv = dbus.Interface(mandos_dbus_objc,
 
264
                                          dbus_interface
 
265
                                          = server_interface)
 
266
        try:
 
267
            mandos_clients = (self.mandos_serv
 
268
                              .GetAllClientsWithProperties())
 
269
        except dbus.exceptions.DBusException:
 
270
            mandos_clients = dbus.Dictionary()
 
271
        
 
272
        (self.mandos_serv
 
273
         .connect_to_signal("ClientRemoved",
 
274
                            self.find_and_remove_client,
 
275
                            dbus_interface=server_interface,
 
276
                            byte_arrays=True))
 
277
        (self.mandos_serv
 
278
         .connect_to_signal("ClientAdded",
 
279
                            self.add_new_client,
 
280
                            dbus_interface=server_interface,
 
281
                            byte_arrays=True))
 
282
        for path, client in mandos_clients.iteritems():
 
283
            client_proxy_object = self.bus.get_object(self.busname,
 
284
                                                      path)
 
285
            self.add_client(MandosClientWidget(server_proxy_object
 
286
                                               =self.mandos_serv,
 
287
                                               proxy_object
 
288
                                               =client_proxy_object,
 
289
                                               properties=client,
 
290
                                               update_hook
 
291
                                               =self.refresh,
 
292
                                               delete_hook
 
293
                                               =self.remove_client),
 
294
                            path=path)
468
295
    
469
296
    def rebuild(self):
470
297
        """This rebuilds the User Interface.
471
298
        Call this when the widget layout needs to change"""
472
299
        self.uilist = []
473
300
        #self.uilist.append(urwid.ListBox(self.clients))
474
 
        self.uilist.append(urwid.Frame(ConstrainedListBox(self.
475
 
                                                          clients),
 
301
        self.uilist.append(urwid.Frame(ConstrainedListBox(self.clients),
476
302
                                       #header=urwid.Divider(),
477
303
                                       header=None,
478
 
                                       footer=
479
 
                                       urwid.Divider(div_char=
480
 
                                                     self.divider)))
 
304
                                       footer=urwid.Divider(div_char=self.divider)))
481
305
        if self.log_visible:
482
306
            self.uilist.append(self.logbox)
 
307
            pass
483
308
        self.topwidget = urwid.Pile(self.uilist)
484
309
    
485
 
    def log_message(self, message, level=1):
486
 
        """Log message formatted with timestamp"""
487
 
        if level < self.log_level:
488
 
            return
489
 
        timestamp = datetime.datetime.now().isoformat()
490
 
        self.log_message_raw("{}: {}".format(timestamp, message),
491
 
                             level=level)
492
 
    
493
 
    def log_message_raw(self, markup, level=1):
 
310
    def log_message(self, markup):
494
311
        """Add a log message to the log buffer."""
495
 
        if level < self.log_level:
496
 
            return
497
312
        self.log.append(urwid.Text(markup, wrap=self.log_wrap))
498
313
        if (self.max_log_length
499
314
            and len(self.log) > self.max_log_length):
500
315
            del self.log[0:len(self.log)-self.max_log_length-1]
501
 
        self.logbox.set_focus(len(self.logbox.body.contents),
502
 
                              coming_from="above")
503
 
        self.refresh()
504
316
    
505
317
    def toggle_log_display(self):
506
318
        """Toggle visibility of the log buffer."""
507
319
        self.log_visible = not self.log_visible
508
320
        self.rebuild()
509
 
        self.log_message("Log visibility changed to: {}"
510
 
                         .format(self.log_visible), level=0)
 
321
        self.log_message(u"Log visibility changed to: "
 
322
                         + unicode(self.log_visible))
511
323
    
512
324
    def change_log_display(self):
513
325
        """Change type of log display.
514
326
        Currently, this toggles wrapping of text lines."""
515
 
        if self.log_wrap == "clip":
516
 
            self.log_wrap = "any"
 
327
        if self.log_wrap == u"clip":
 
328
            self.log_wrap = u"any"
517
329
        else:
518
 
            self.log_wrap = "clip"
 
330
            self.log_wrap = u"clip"
519
331
        for textwidget in self.log:
520
332
            textwidget.set_wrap_mode(self.log_wrap)
521
 
        self.log_message("Wrap mode: {}".format(self.log_wrap),
522
 
                         level=0)
 
333
        self.log_message(u"Wrap mode: " + self.log_wrap)
523
334
    
524
335
    def find_and_remove_client(self, path, name):
525
 
        """Find a client by its object path and remove it.
 
336
        """Find an client from its object path and remove it.
526
337
        
527
338
        This is connected to the ClientRemoved signal from the
528
339
        Mandos server object."""
530
341
            client = self.clients_dict[path]
531
342
        except KeyError:
532
343
            # not found?
533
 
            self.log_message("Unknown client {!r} ({!r}) removed"
534
 
                             .format(name, path))
535
344
            return
536
 
        client.delete()
 
345
        self.remove_client(client, path)
537
346
    
538
 
    def add_new_client(self, path):
 
347
    def add_new_client(self, path, properties):
539
348
        client_proxy_object = self.bus.get_object(self.busname, path)
540
349
        self.add_client(MandosClientWidget(server_proxy_object
541
350
                                           =self.mandos_serv,
542
351
                                           proxy_object
543
352
                                           =client_proxy_object,
 
353
                                           properties=properties,
544
354
                                           update_hook
545
355
                                           =self.refresh,
546
356
                                           delete_hook
547
 
                                           =self.remove_client,
548
 
                                           logger
549
 
                                           =self.log_message),
 
357
                                           =self.remove_client),
550
358
                        path=path)
551
359
    
552
360
    def add_client(self, client, path=None):
554
362
        if path is None:
555
363
            path = client.proxy.object_path
556
364
        self.clients_dict[path] = client
557
 
        self.clients.sort(key=lambda c: c.properties["Name"])
 
365
        self.clients.sort(None, lambda c: c.properties[u"name"])
558
366
        self.refresh()
559
367
    
560
368
    def remove_client(self, client, path=None):
562
370
        if path is None:
563
371
            path = client.proxy.object_path
564
372
        del self.clients_dict[path]
 
373
        if not self.clients_dict:
 
374
            # Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker
 
375
            # is completely emptied, we need to recreate it.
 
376
            self.clients = urwid.SimpleListWalker([])
 
377
            self.rebuild()
565
378
        self.refresh()
566
379
    
567
380
    def refresh(self):
571
384
    
572
385
    def run(self):
573
386
        """Start the main loop and exit when it's done."""
574
 
        self.bus = dbus.SystemBus()
575
 
        mandos_dbus_objc = self.bus.get_object(
576
 
            self.busname, "/", follow_name_owner_changes=True)
577
 
        self.mandos_serv = dbus.Interface(mandos_dbus_objc,
578
 
                                          dbus_interface
579
 
                                          = server_interface)
580
 
        try:
581
 
            mandos_clients = (self.mandos_serv
582
 
                              .GetAllClientsWithProperties())
583
 
            if not mandos_clients:
584
 
                self.log_message_raw(("bold", "Note: Server has no clients."))
585
 
        except dbus.exceptions.DBusException:
586
 
            self.log_message_raw(("bold", "Note: No Mandos server running."))
587
 
            mandos_clients = dbus.Dictionary()
588
 
        
589
 
        (self.mandos_serv
590
 
         .connect_to_signal("ClientRemoved",
591
 
                            self.find_and_remove_client,
592
 
                            dbus_interface=server_interface,
593
 
                            byte_arrays=True))
594
 
        (self.mandos_serv
595
 
         .connect_to_signal("ClientAdded",
596
 
                            self.add_new_client,
597
 
                            dbus_interface=server_interface,
598
 
                            byte_arrays=True))
599
 
        (self.mandos_serv
600
 
         .connect_to_signal("ClientNotFound",
601
 
                            self.client_not_found,
602
 
                            dbus_interface=server_interface,
603
 
                            byte_arrays=True))
604
 
        for path, client in mandos_clients.items():
605
 
            client_proxy_object = self.bus.get_object(self.busname,
606
 
                                                      path)
607
 
            self.add_client(MandosClientWidget(server_proxy_object
608
 
                                               =self.mandos_serv,
609
 
                                               proxy_object
610
 
                                               =client_proxy_object,
611
 
                                               properties=client,
612
 
                                               update_hook
613
 
                                               =self.refresh,
614
 
                                               delete_hook
615
 
                                               =self.remove_client,
616
 
                                               logger
617
 
                                               =self.log_message),
618
 
                            path=path)
619
 
        
620
387
        self.refresh()
621
388
        self._input_callback_tag = (gobject.io_add_watch
622
389
                                    (sys.stdin.fileno(),
632
399
    
633
400
    def process_input(self, source, condition):
634
401
        keys = self.screen.get_input()
635
 
        translations = { "ctrl n": "down",      # Emacs
636
 
                         "ctrl p": "up",        # Emacs
637
 
                         "ctrl v": "page down", # Emacs
638
 
                         "meta v": "page up",   # Emacs
639
 
                         " ": "page down",      # less
640
 
                         "f": "page down",      # less
641
 
                         "b": "page up",        # less
642
 
                         "j": "down",           # vi
643
 
                         "k": "up",             # vi
 
402
        translations = { u"ctrl n": u"down",      # Emacs
 
403
                         u"ctrl p": u"up",        # Emacs
 
404
                         u"ctrl v": u"page down", # Emacs
 
405
                         u"meta v": u"page up",   # Emacs
 
406
                         u" ": u"page down",      # less
 
407
                         u"f": u"page down",      # less
 
408
                         u"b": u"page up",        # less
 
409
                         u"j": u"down",           # vi
 
410
                         u"k": u"up",             # vi
644
411
                         }
645
412
        for key in keys:
646
413
            try:
648
415
            except KeyError:    # :-)
649
416
                pass
650
417
            
651
 
            if key == "q" or key == "Q":
 
418
            if key == u"q" or key == u"Q":
652
419
                self.stop()
653
420
                break
654
 
            elif key == "window resize":
 
421
            elif key == u"window resize":
655
422
                self.size = self.screen.get_cols_rows()
656
423
                self.refresh()
657
 
            elif key == "ctrl l":
658
 
                self.screen.clear()
 
424
            elif key == u"\f":  # Ctrl-L
659
425
                self.refresh()
660
 
            elif key == "l" or key == "D":
 
426
            elif key == u"l" or key == u"D":
661
427
                self.toggle_log_display()
662
428
                self.refresh()
663
 
            elif key == "w" or key == "i":
 
429
            elif key == u"w" or key == u"i":
664
430
                self.change_log_display()
665
431
                self.refresh()
666
 
            elif key == "?" or key == "f1" or key == "esc":
 
432
            elif key == u"?" or key == u"f1":
667
433
                if not self.log_visible:
668
434
                    self.log_visible = True
669
435
                    self.rebuild()
670
 
                self.log_message_raw(("bold",
671
 
                                      "  ".
672
 
                                      join(("q: Quit",
673
 
                                            "?: Help",
674
 
                                            "l: Log window toggle",
675
 
                                            "TAB: Switch window",
676
 
                                            "w: Wrap (log lines)",
677
 
                                            "v: Toggle verbose log",
678
 
                                            ))))
679
 
                self.log_message_raw(("bold",
680
 
                                      "  "
681
 
                                      .join(("Clients:",
682
 
                                             "+: Enable",
683
 
                                             "-: Disable",
684
 
                                             "R: Remove",
685
 
                                             "s: Start new checker",
686
 
                                             "S: Stop checker",
687
 
                                             "C: Checker OK",
688
 
                                             "a: Approve",
689
 
                                             "d: Deny"))))
 
436
                self.log_message((u"bold",
 
437
                                  u"  ".join((u"q: Quit",
 
438
                                              u"?: Help",
 
439
                                              u"l: Log window toggle",
 
440
                                              u"TAB: Switch window",
 
441
                                              u"w: Wrap (log)"))))
 
442
                self.log_message((u"bold",
 
443
                                  u"  ".join((u"Clients:",
 
444
                                              u"e: Enable",
 
445
                                              u"d: Disable",
 
446
                                              u"r: Remove",
 
447
                                              u"s: Start new checker",
 
448
                                              u"S: Stop checker",
 
449
                                              u"C: Checker OK"))))
690
450
                self.refresh()
691
 
            elif key == "tab":
 
451
            elif key == u"tab":
692
452
                if self.topwidget.get_focus() is self.logbox:
693
453
                    self.topwidget.set_focus(0)
694
454
                else:
695
455
                    self.topwidget.set_focus(self.logbox)
696
456
                self.refresh()
697
 
            elif key == "v":
698
 
                if self.log_level == 0:
699
 
                    self.log_level = 1
700
 
                    self.log_message("Verbose mode: Off")
701
 
                else:
702
 
                    self.log_level = 0
703
 
                    self.log_message("Verbose mode: On")
704
 
            #elif (key == "end" or key == "meta >" or key == "G"
705
 
            #      or key == ">"):
 
457
            #elif (key == u"end" or key == u"meta >" or key == u"G"
 
458
            #      or key == u">"):
706
459
            #    pass            # xxx end-of-buffer
707
 
            #elif (key == "home" or key == "meta <" or key == "g"
708
 
            #      or key == "<"):
 
460
            #elif (key == u"home" or key == u"meta <" or key == u"g"
 
461
            #      or key == u"<"):
709
462
            #    pass            # xxx beginning-of-buffer
710
 
            #elif key == "ctrl e" or key == "$":
 
463
            #elif key == u"ctrl e" or key == u"$":
711
464
            #    pass            # xxx move-end-of-line
712
 
            #elif key == "ctrl a" or key == "^":
 
465
            #elif key == u"ctrl a" or key == u"^":
713
466
            #    pass            # xxx move-beginning-of-line
714
 
            #elif key == "ctrl b" or key == "meta (" or key == "h":
 
467
            #elif key == u"ctrl b" or key == u"meta (" or key == u"h":
715
468
            #    pass            # xxx left
716
 
            #elif key == "ctrl f" or key == "meta )" or key == "l":
 
469
            #elif key == u"ctrl f" or key == u"meta )" or key == u"l":
717
470
            #    pass            # xxx right
718
 
            #elif key == "a":
 
471
            #elif key == u"a":
719
472
            #    pass            # scroll up log
720
 
            #elif key == "z":
 
473
            #elif key == u"z":
721
474
            #    pass            # scroll down log
722
475
            elif self.topwidget.selectable():
723
476
                self.topwidget.keypress(self.size, key)
727
480
ui = UserInterface()
728
481
try:
729
482
    ui.run()
730
 
except KeyboardInterrupt:
731
 
    ui.screen.stop()
732
 
except Exception as e:
733
 
    ui.log_message(str(e))
 
483
except:
734
484
    ui.screen.stop()
735
485
    raise