/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

* Makefile: Merge branch adding warning messages to "run-*" targets.

Show diffs side-by-side

added added

removed removed

Lines of Context:
4
4
from __future__ import division, absolute_import, with_statement
5
5
 
6
6
import sys
 
7
import os
7
8
import signal
8
9
 
 
10
import datetime
 
11
 
9
12
import urwid.curses_display
10
13
import urwid
11
14
 
16
19
 
17
20
import UserList
18
21
 
 
22
import locale
 
23
 
 
24
locale.setlocale(locale.LC_ALL, u'')
 
25
 
19
26
# Some useful constants
20
27
domain = 'se.bsnet.fukt'
21
28
server_interface = domain + '.Mandos'
30
37
urwid.curses_display.curses.A_UNDERLINE |= (
31
38
    urwid.curses_display.curses.A_BLINK)
32
39
 
 
40
def isoformat_to_datetime(iso):
 
41
    "Parse an ISO 8601 date string to a datetime.datetime()"
 
42
    if not iso:
 
43
        return None
 
44
    d, t = iso.split(u"T", 1)
 
45
    year, month, day = d.split(u"-", 2)
 
46
    hour, minute, second = t.split(u":", 2)
 
47
    second, fraction = divmod(float(second), 1)
 
48
    return datetime.datetime(int(year),
 
49
                             int(month),
 
50
                             int(day),
 
51
                             int(hour),
 
52
                             int(minute),
 
53
                             int(second),           # Whole seconds
 
54
                             int(fraction*1000000)) # Microseconds
 
55
 
33
56
class MandosClientPropertyCache(object):
34
57
    """This wraps a Mandos Client D-Bus proxy object, caches the
35
58
    properties and calls a hook function when any of them are
36
59
    changed.
37
60
    """
38
 
    def __init__(self, proxy_object=None, properties=None, *args,
39
 
                 **kwargs):
40
 
        # Type conversion mapping
41
 
        self.type_map = {
42
 
            dbus.ObjectPath: unicode,
43
 
            dbus.ByteArray: str,
44
 
            dbus.Signature: unicode,
45
 
            dbus.Byte: chr,
46
 
            dbus.Int16: int,
47
 
            dbus.UInt16: int,
48
 
            dbus.Int32: int,
49
 
            dbus.UInt32: int,
50
 
            dbus.Int64: int,
51
 
            dbus.UInt64: int,
52
 
            dbus.Dictionary: dict,
53
 
            dbus.Array: list,
54
 
            dbus.String: unicode,
55
 
            dbus.Boolean: bool,
56
 
            dbus.Double: float,
57
 
            dbus.Struct: tuple,
58
 
            }
 
61
    def __init__(self, proxy_object=None, *args, **kwargs):
59
62
        self.proxy = proxy_object # Mandos Client proxy object
60
63
        
61
 
        if properties is None:
62
 
            self.properties = dict()
63
 
        else:
64
 
            self.properties = dict(self.convert_property(prop, val)
65
 
                                   for prop, val in
66
 
                                   properties.iteritems())
67
 
        self.proxy.connect_to_signal("PropertyChanged",
 
64
        self.properties = dict()
 
65
        self.proxy.connect_to_signal(u"PropertyChanged",
68
66
                                     self.property_changed,
69
67
                                     client_interface,
70
68
                                     byte_arrays=True)
71
69
        
72
 
        if properties is None:
73
 
            self.properties.update(
74
 
                self.convert_property(prop, val)
75
 
                for prop, val in
76
 
                self.proxy.GetAll(client_interface,
77
 
                                  dbus_interface =
78
 
                                  dbus.PROPERTIES_IFACE).iteritems())
 
70
        self.properties.update(
 
71
            self.proxy.GetAll(client_interface,
 
72
                              dbus_interface = dbus.PROPERTIES_IFACE))
79
73
        super(MandosClientPropertyCache, self).__init__(
80
 
            proxy_object=proxy_object,
81
 
            properties=properties, *args, **kwargs)
 
74
            proxy_object=proxy_object, *args, **kwargs)
82
75
    
83
 
    def convert_property(self, property, value):
84
 
        """This converts the arguments from a D-Bus signal, which are
85
 
        D-Bus types, into normal Python types, using a conversion
86
 
        function from "self.type_map".
87
 
        """
88
 
        property_name = unicode(property) # Always a dbus.String
89
 
        if isinstance(value, dbus.UTF8String):
90
 
            # Should not happen, but prepare for it anyway
91
 
            value = dbus.String(str(value).decode("utf-8"))
92
 
        try:
93
 
            convfunc = self.type_map[type(value)]
94
 
        except KeyError:
95
 
            # Unknown type, return unmodified
96
 
            return property_name, value
97
 
        return property_name, convfunc(value)
98
76
    def property_changed(self, property=None, value=None):
99
77
        """This is called whenever we get a PropertyChanged signal
100
78
        It updates the changed property in the "properties" dict.
101
79
        """
102
 
        # Convert name and value
103
 
        property_name, cvalue = self.convert_property(property, value)
104
80
        # Update properties dict with new value
105
 
        self.properties[property_name] = cvalue
 
81
        self.properties[property] = value
106
82
 
107
83
 
108
84
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
110
86
    """
111
87
    
112
88
    def __init__(self, server_proxy_object=None, update_hook=None,
113
 
                 delete_hook=None, *args, **kwargs):
 
89
                 delete_hook=None, logger=None, *args, **kwargs):
114
90
        # Called on update
115
91
        self.update_hook = update_hook
116
92
        # Called on delete
117
93
        self.delete_hook = delete_hook
118
94
        # Mandos Server proxy object
119
95
        self.server_proxy_object = server_proxy_object
 
96
        # Logger
 
97
        self.logger = logger
 
98
        
 
99
        self._update_timer_callback_tag = None
 
100
        self.last_checker_failed = False
120
101
        
121
102
        # The widget shown normally
122
 
        self._text_widget = urwid.Text("")
 
103
        self._text_widget = urwid.Text(u"")
123
104
        # The widget shown when we have focus
124
 
        self._focus_text_widget = urwid.Text("")
 
105
        self._focus_text_widget = urwid.Text(u"")
125
106
        super(MandosClientWidget, self).__init__(
126
107
            update_hook=update_hook, delete_hook=delete_hook,
127
108
            *args, **kwargs)
128
109
        self.update()
129
110
        self.opened = False
 
111
        self.proxy.connect_to_signal(u"CheckerCompleted",
 
112
                                     self.checker_completed,
 
113
                                     client_interface,
 
114
                                     byte_arrays=True)
 
115
        self.proxy.connect_to_signal(u"CheckerStarted",
 
116
                                     self.checker_started,
 
117
                                     client_interface,
 
118
                                     byte_arrays=True)
 
119
        self.proxy.connect_to_signal(u"GotSecret",
 
120
                                     self.got_secret,
 
121
                                     client_interface,
 
122
                                     byte_arrays=True)
 
123
        self.proxy.connect_to_signal(u"Rejected",
 
124
                                     self.rejected,
 
125
                                     client_interface,
 
126
                                     byte_arrays=True)
 
127
        last_checked_ok = isoformat_to_datetime(self.properties
 
128
                                                ["last_checked_ok"])
 
129
        if last_checked_ok is None:
 
130
            self.last_checker_failed = True
 
131
        else:
 
132
            self.last_checker_failed = ((datetime.datetime.utcnow()
 
133
                                         - last_checked_ok)
 
134
                                        > datetime.timedelta
 
135
                                        (milliseconds=
 
136
                                         self.properties["interval"]))
 
137
        if self.last_checker_failed:
 
138
            self._update_timer_callback_tag = (gobject.timeout_add
 
139
                                               (1000,
 
140
                                                self.update_timer))
 
141
    
 
142
    def checker_completed(self, exitstatus, condition, command):
 
143
        if exitstatus == 0:
 
144
            if self.last_checker_failed:
 
145
                self.last_checker_failed = False
 
146
                gobject.source_remove(self._update_timer_callback_tag)
 
147
                self._update_timer_callback_tag = None
 
148
            self.logger(u'Checker for client %s (command "%s")'
 
149
                        u' was successful'
 
150
                        % (self.properties[u"name"], command))
 
151
            self.update()
 
152
            return
 
153
        # Checker failed
 
154
        if not self.last_checker_failed:
 
155
            self.last_checker_failed = True
 
156
            self._update_timer_callback_tag = (gobject.timeout_add
 
157
                                               (1000,
 
158
                                                self.update_timer))
 
159
        if os.WIFEXITED(condition):
 
160
            self.logger(u'Checker for client %s (command "%s")'
 
161
                        u' failed with exit code %s'
 
162
                        % (self.properties[u"name"], command,
 
163
                           os.WEXITSTATUS(condition)))
 
164
        elif os.WIFSIGNALED(condition):
 
165
            self.logger(u'Checker for client %s (command "%s")'
 
166
                        u' was killed by signal %s'
 
167
                        % (self.properties[u"name"], command,
 
168
                           os.WTERMSIG(condition)))
 
169
        elif os.WCOREDUMP(condition):
 
170
            self.logger(u'Checker for client %s (command "%s")'
 
171
                        u' dumped core'
 
172
                        % (self.properties[u"name"], command))
 
173
        else:
 
174
            self.logger(u'Checker for client %s completed mysteriously')
 
175
        self.update()
 
176
    
 
177
    def checker_started(self, command):
 
178
        self.logger(u'Client %s started checker "%s"'
 
179
                    % (self.properties[u"name"], unicode(command)))
 
180
    
 
181
    def got_secret(self):
 
182
        self.logger(u'Client %s received its secret'
 
183
                    % self.properties[u"name"])
 
184
    
 
185
    def rejected(self):
 
186
        self.logger(u'Client %s was rejected'
 
187
                    % self.properties[u"name"])
130
188
    
131
189
    def selectable(self):
132
190
        """Make this a "selectable" widget.
156
214
                          }
157
215
        
158
216
        # Rebuild focus and non-focus widgets using current properties
159
 
        self._text = (u'name="%(name)s", enabled=%(enabled)s'
160
 
                      % self.properties)
 
217
        self._text = (u'%(name)s: %(enabled)s%(timer)s'
 
218
                      % { u"name": self.properties[u"name"],
 
219
                          u"enabled":
 
220
                              (u"enabled"
 
221
                               if self.properties[u"enabled"]
 
222
                               else u"DISABLED"),
 
223
                          u"timer": (unicode(datetime.timedelta
 
224
                                             (milliseconds =
 
225
                                              self.properties
 
226
                                              [u"timeout"])
 
227
                                             - (datetime.datetime
 
228
                                                .utcnow()
 
229
                                                - isoformat_to_datetime
 
230
                                                (max((self.properties
 
231
                                                 ["last_checked_ok"]
 
232
                                                 or
 
233
                                                 self.properties
 
234
                                                 ["created"]),
 
235
                                                    self.properties[u"last_enabled"]))))
 
236
                                     if (self.last_checker_failed
 
237
                                         and self.properties
 
238
                                         [u"enabled"])
 
239
                                     else u"")})
161
240
        if not urwid.supports_unicode():
162
241
            self._text = self._text.encode("ascii", "replace")
163
 
        textlist = [(u"normal", u"BLÄRGH: "), (u"bold", self._text)]
 
242
        textlist = [(u"normal", self._text)]
164
243
        self._text_widget.set_text(textlist)
165
244
        self._focus_text_widget.set_text([(with_standout[text[0]],
166
245
                                           text[1])
174
253
        if self.update_hook is not None:
175
254
            self.update_hook()
176
255
    
 
256
    def update_timer(self):
 
257
        "called by gobject"
 
258
        self.update()
 
259
        return True             # Keep calling this
 
260
    
177
261
    def delete(self):
 
262
        if self._update_timer_callback_tag is not None:
 
263
            gobject.source_remove(self._update_timer_callback_tag)
 
264
            self._update_timer_callback_tag = None
178
265
        if self.delete_hook is not None:
179
266
            self.delete_hook(self)
180
267
    
191
278
            self.proxy.Enable()
192
279
        elif key == u"d" or key == u"-":
193
280
            self.proxy.Disable()
194
 
        elif key == u"r" or key == u"_":
 
281
        elif key == u"r" or key == u"_" or key == u"ctrl k":
195
282
            self.server_proxy_object.RemoveClient(self.proxy
196
283
                                                  .object_path)
197
284
        elif key == u"s":
198
285
            self.proxy.StartChecker()
199
 
        elif key == u"c":
 
286
        elif key == u"S":
200
287
            self.proxy.StopChecker()
201
 
        elif key == u"S":
 
288
        elif key == u"C":
202
289
            self.proxy.CheckedOK()
203
290
        # xxx
204
291
#         elif key == u"p" or key == "=":
222
309
            self.update()
223
310
 
224
311
 
 
312
class ConstrainedListBox(urwid.ListBox):
 
313
    """Like a normal urwid.ListBox, but will consume all "up" or
 
314
    "down" key presses, thus not allowing any containing widgets to
 
315
    use them as an excuse to shift focus away from this widget.
 
316
    """
 
317
    def keypress(self, (maxcol, maxrow), key):
 
318
        ret = super(ConstrainedListBox, self).keypress((maxcol, maxrow), key)
 
319
        if ret in (u"up", u"down"):
 
320
            return
 
321
        return ret
 
322
 
 
323
 
225
324
class UserInterface(object):
226
325
    """This is the entire user interface - the whole screen
227
326
    with boxes, lists of client widgets, etc.
228
327
    """
229
 
    def __init__(self):
230
 
        DBusGMainLoop(set_as_default=True )
 
328
    def __init__(self, max_log_length=1000):
 
329
        DBusGMainLoop(set_as_default=True)
231
330
        
232
331
        self.screen = urwid.curses_display.Screen()
233
332
        
251
350
                                          u"standout")),
252
351
                ))
253
352
        
 
353
        if urwid.supports_unicode():
 
354
            self.divider = u"─" # \u2500
 
355
            #self.divider = u"━" # \u2501
 
356
        else:
 
357
            #self.divider = u"-" # \u002d
 
358
            self.divider = u"_" # \u005f
 
359
        
254
360
        self.screen.start()
255
361
        
256
362
        self.size = self.screen.get_cols_rows()
257
363
        
258
364
        self.clients = urwid.SimpleListWalker([])
259
365
        self.clients_dict = {}
260
 
        self.topwidget = urwid.LineBox(urwid.ListBox(self.clients))
261
 
        #self.topwidget = urwid.ListBox(clients)
 
366
        
 
367
        # We will add Text widgets to this list
 
368
        self.log = []
 
369
        self.max_log_length = max_log_length
 
370
        
 
371
        # We keep a reference to the log widget so we can remove it
 
372
        # from the ListWalker without it getting destroyed
 
373
        self.logbox = ConstrainedListBox(self.log)
 
374
        
 
375
        # This keeps track of whether self.uilist currently has
 
376
        # self.logbox in it or not
 
377
        self.log_visible = True
 
378
        self.log_wrap = u"any"
 
379
        
 
380
        self.rebuild()
 
381
        self.log_message_raw((u"bold",
 
382
                              u"Mandos Monitor version " + version))
 
383
        self.log_message_raw((u"bold",
 
384
                              u"q: Quit  ?: Help"))
262
385
        
263
386
        self.busname = domain + '.Mandos'
264
387
        self.main_loop = gobject.MainLoop()
275
398
            mandos_clients = dbus.Dictionary()
276
399
        
277
400
        (self.mandos_serv
278
 
         .connect_to_signal("ClientRemoved",
 
401
         .connect_to_signal(u"ClientRemoved",
279
402
                            self.find_and_remove_client,
280
403
                            dbus_interface=server_interface,
281
404
                            byte_arrays=True))
282
405
        (self.mandos_serv
283
 
         .connect_to_signal("ClientAdded",
 
406
         .connect_to_signal(u"ClientAdded",
284
407
                            self.add_new_client,
285
408
                            dbus_interface=server_interface,
286
409
                            byte_arrays=True))
287
 
        for path, client in (mandos_clients.iteritems()):
 
410
        (self.mandos_serv
 
411
         .connect_to_signal(u"ClientNotFound",
 
412
                            self.client_not_found,
 
413
                            dbus_interface=server_interface,
 
414
                            byte_arrays=True))
 
415
        for path, client in mandos_clients.iteritems():
288
416
            client_proxy_object = self.bus.get_object(self.busname,
289
417
                                                      path)
290
418
            self.add_client(MandosClientWidget(server_proxy_object
295
423
                                               update_hook
296
424
                                               =self.refresh,
297
425
                                               delete_hook
298
 
                                               =self.remove_client),
 
426
                                               =self.remove_client,
 
427
                                               logger
 
428
                                               =self.log_message),
299
429
                            path=path)
300
430
    
 
431
    def client_not_found(self, fingerprint, address):
 
432
        self.log_message((u"Client with address %s and fingerprint %s"
 
433
                          u" could not be found" % (address,
 
434
                                                    fingerprint)))
 
435
    
 
436
    def rebuild(self):
 
437
        """This rebuilds the User Interface.
 
438
        Call this when the widget layout needs to change"""
 
439
        self.uilist = []
 
440
        #self.uilist.append(urwid.ListBox(self.clients))
 
441
        self.uilist.append(urwid.Frame(ConstrainedListBox(self.clients),
 
442
                                       #header=urwid.Divider(),
 
443
                                       header=None,
 
444
                                       footer=urwid.Divider(div_char=self.divider)))
 
445
        if self.log_visible:
 
446
            self.uilist.append(self.logbox)
 
447
            pass
 
448
        self.topwidget = urwid.Pile(self.uilist)
 
449
    
 
450
    def log_message(self, message):
 
451
        timestamp = datetime.datetime.now().isoformat()
 
452
        self.log_message_raw(timestamp + u": " + message)
 
453
    
 
454
    def log_message_raw(self, markup):
 
455
        """Add a log message to the log buffer."""
 
456
        self.log.append(urwid.Text(markup, wrap=self.log_wrap))
 
457
        if (self.max_log_length
 
458
            and len(self.log) > self.max_log_length):
 
459
            del self.log[0:len(self.log)-self.max_log_length-1]
 
460
        self.logbox.set_focus(len(self.logbox.body.contents),
 
461
                              coming_from=u"above")
 
462
        self.refresh()
 
463
    
 
464
    def toggle_log_display(self):
 
465
        """Toggle visibility of the log buffer."""
 
466
        self.log_visible = not self.log_visible
 
467
        self.rebuild()
 
468
        self.log_message(u"Log visibility changed to: "
 
469
                         + unicode(self.log_visible))
 
470
    
 
471
    def change_log_display(self):
 
472
        """Change type of log display.
 
473
        Currently, this toggles wrapping of text lines."""
 
474
        if self.log_wrap == u"clip":
 
475
            self.log_wrap = u"any"
 
476
        else:
 
477
            self.log_wrap = u"clip"
 
478
        for textwidget in self.log:
 
479
            textwidget.set_wrap_mode(self.log_wrap)
 
480
        self.log_message(u"Wrap mode: " + self.log_wrap)
 
481
    
301
482
    def find_and_remove_client(self, path, name):
302
483
        """Find an client from its object path and remove it.
303
484
        
310
491
            return
311
492
        self.remove_client(client, path)
312
493
    
313
 
    def add_new_client(self, path, properties):
 
494
    def add_new_client(self, path):
314
495
        client_proxy_object = self.bus.get_object(self.busname, path)
315
496
        self.add_client(MandosClientWidget(server_proxy_object
316
497
                                           =self.mandos_serv,
317
498
                                           proxy_object
318
499
                                           =client_proxy_object,
319
 
                                           properties=properties,
320
500
                                           update_hook
321
501
                                           =self.refresh,
322
502
                                           delete_hook
323
 
                                           =self.remove_client),
 
503
                                           =self.remove_client,
 
504
                                           logger
 
505
                                           =self.log_message),
324
506
                        path=path)
325
507
    
326
508
    def add_client(self, client, path=None):
336
518
        if path is None:
337
519
            path = client.proxy.object_path
338
520
        del self.clients_dict[path]
 
521
        if not self.clients_dict:
 
522
            # Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker
 
523
            # is completely emptied, we need to recreate it.
 
524
            self.clients = urwid.SimpleListWalker([])
 
525
            self.rebuild()
339
526
        self.refresh()
340
527
    
341
528
    def refresh(self):
360
547
    
361
548
    def process_input(self, source, condition):
362
549
        keys = self.screen.get_input()
363
 
        translations = { u"j": u"down",
364
 
                         u"k": u"up",
 
550
        translations = { u"ctrl n": u"down",      # Emacs
 
551
                         u"ctrl p": u"up",        # Emacs
 
552
                         u"ctrl v": u"page down", # Emacs
 
553
                         u"meta v": u"page up",   # Emacs
 
554
                         u" ": u"page down",      # less
 
555
                         u"f": u"page down",      # less
 
556
                         u"b": u"page up",        # less
 
557
                         u"j": u"down",           # vi
 
558
                         u"k": u"up",             # vi
365
559
                         }
366
560
        for key in keys:
367
561
            try:
375
569
            elif key == u"window resize":
376
570
                self.size = self.screen.get_cols_rows()
377
571
                self.refresh()
378
 
            elif key == " ":
379
 
                self.refresh()
 
572
            elif key == u"\f":  # Ctrl-L
 
573
                self.refresh()
 
574
            elif key == u"l" or key == u"D":
 
575
                self.toggle_log_display()
 
576
                self.refresh()
 
577
            elif key == u"w" or key == u"i":
 
578
                self.change_log_display()
 
579
                self.refresh()
 
580
            elif key == u"?" or key == u"f1" or key == u"esc":
 
581
                if not self.log_visible:
 
582
                    self.log_visible = True
 
583
                    self.rebuild()
 
584
                self.log_message_raw((u"bold",
 
585
                                      u"  ".
 
586
                                      join((u"q: Quit",
 
587
                                            u"?: Help",
 
588
                                            u"l: Log window toggle",
 
589
                                            u"TAB: Switch window",
 
590
                                            u"w: Wrap (log)"))))
 
591
                self.log_message_raw((u"bold",
 
592
                                      u"  "
 
593
                                      .join((u"Clients:",
 
594
                                             u"e: Enable",
 
595
                                             u"d: Disable",
 
596
                                             u"r: Remove",
 
597
                                             u"s: Start new checker",
 
598
                                             u"S: Stop checker",
 
599
                                             u"C: Checker OK"))))
 
600
                self.refresh()
 
601
            elif key == u"tab":
 
602
                if self.topwidget.get_focus() is self.logbox:
 
603
                    self.topwidget.set_focus(0)
 
604
                else:
 
605
                    self.topwidget.set_focus(self.logbox)
 
606
                self.refresh()
 
607
            #elif (key == u"end" or key == u"meta >" or key == u"G"
 
608
            #      or key == u">"):
 
609
            #    pass            # xxx end-of-buffer
 
610
            #elif (key == u"home" or key == u"meta <" or key == u"g"
 
611
            #      or key == u"<"):
 
612
            #    pass            # xxx beginning-of-buffer
 
613
            #elif key == u"ctrl e" or key == u"$":
 
614
            #    pass            # xxx move-end-of-line
 
615
            #elif key == u"ctrl a" or key == u"^":
 
616
            #    pass            # xxx move-beginning-of-line
 
617
            #elif key == u"ctrl b" or key == u"meta (" or key == u"h":
 
618
            #    pass            # xxx left
 
619
            #elif key == u"ctrl f" or key == u"meta )" or key == u"l":
 
620
            #    pass            # xxx right
 
621
            #elif key == u"a":
 
622
            #    pass            # scroll up log
 
623
            #elif key == u"z":
 
624
            #    pass            # scroll down log
380
625
            elif self.topwidget.selectable():
381
626
                self.topwidget.keypress(self.size, key)
382
627
                self.refresh()
385
630
ui = UserInterface()
386
631
try:
387
632
    ui.run()
388
 
except:
 
633
except Exception, e:
 
634
    ui.log_message(unicode(e))
389
635
    ui.screen.stop()
390
636
    raise