/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-15 10:12:09 UTC
  • Revision ID: teddy@fukt.bsnet.se-20091115101209-d0zrfwkp6u0l7v7b
* mandos (MandosServer.handle_ipc): Better log message.
  (main/MandosDBusService.ClientNotFound): Add "address" argument.
                                           All callers changed.
* mandos-monitor (MandosClientWidget.__init__): Add "logger" argument.
  (MandosClientWidget.checker_completed,
  MandosClientWidget.checker_started, MandosClientWidget.got_secret,
  MandosClientWidget.rejected): New methods, connected to signals.
  (MandosClientWidget.update): Improve display.
  (UserInterface.client_not_found): New method, conneced to signal.
  (UserInterface.log_message): New; log with timestamp.
  (UserInterface.log_message_raw): Same as old "log_message".  Bug
                                  fix; always do "refresh()".

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/python
 
2
# -*- mode: python; coding: utf-8 -*-
 
3
 
 
4
from __future__ import division, absolute_import, with_statement
 
5
 
 
6
import sys
 
7
import os
 
8
import signal
 
9
 
 
10
import datetime
 
11
 
 
12
import urwid.curses_display
 
13
import urwid
 
14
 
 
15
from dbus.mainloop.glib import DBusGMainLoop
 
16
import gobject
 
17
 
 
18
import dbus
 
19
 
 
20
import UserList
 
21
 
 
22
# Some useful constants
 
23
domain = 'se.bsnet.fukt'
 
24
server_interface = domain + '.Mandos'
 
25
client_interface = domain + '.Mandos.Client'
 
26
version = "1.0.14"
 
27
 
 
28
# Always run in monochrome mode
 
29
urwid.curses_display.curses.has_colors = lambda : False
 
30
 
 
31
# Urwid doesn't support blinking, but we want it.  Since we have no
 
32
# use for underline on its own, we make underline also always blink.
 
33
urwid.curses_display.curses.A_UNDERLINE |= (
 
34
    urwid.curses_display.curses.A_BLINK)
 
35
 
 
36
class MandosClientPropertyCache(object):
 
37
    """This wraps a Mandos Client D-Bus proxy object, caches the
 
38
    properties and calls a hook function when any of them are
 
39
    changed.
 
40
    """
 
41
    def __init__(self, proxy_object=None, properties=None, *args,
 
42
                 **kwargs):
 
43
        self.proxy = proxy_object # Mandos Client proxy object
 
44
        
 
45
        if properties is None:
 
46
            self.properties = dict()
 
47
        else:
 
48
            self.properties = properties
 
49
        self.proxy.connect_to_signal(u"PropertyChanged",
 
50
                                     self.property_changed,
 
51
                                     client_interface,
 
52
                                     byte_arrays=True)
 
53
        
 
54
        if properties is None:
 
55
            self.properties.update(self.proxy.GetAll(client_interface,
 
56
                                                     dbus_interface =
 
57
                                                     dbus.PROPERTIES_IFACE))
 
58
        super(MandosClientPropertyCache, self).__init__(
 
59
            proxy_object=proxy_object,
 
60
            properties=properties, *args, **kwargs)
 
61
    
 
62
    def property_changed(self, property=None, value=None):
 
63
        """This is called whenever we get a PropertyChanged signal
 
64
        It updates the changed property in the "properties" dict.
 
65
        """
 
66
        # Update properties dict with new value
 
67
        self.properties[property] = value
 
68
 
 
69
 
 
70
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
 
71
    """A Mandos Client which is visible on the screen.
 
72
    """
 
73
    
 
74
    def __init__(self, server_proxy_object=None, update_hook=None,
 
75
                 delete_hook=None, logger=None, *args, **kwargs):
 
76
        # Called on update
 
77
        self.update_hook = update_hook
 
78
        # Called on delete
 
79
        self.delete_hook = delete_hook
 
80
        # Mandos Server proxy object
 
81
        self.server_proxy_object = server_proxy_object
 
82
        # Logger
 
83
        self.logger = logger
 
84
        
 
85
        # The widget shown normally
 
86
        self._text_widget = urwid.Text(u"")
 
87
        # The widget shown when we have focus
 
88
        self._focus_text_widget = urwid.Text(u"")
 
89
        super(MandosClientWidget, self).__init__(
 
90
            update_hook=update_hook, delete_hook=delete_hook,
 
91
            *args, **kwargs)
 
92
        self.update()
 
93
        self.opened = False
 
94
        self.proxy.connect_to_signal(u"CheckerCompleted",
 
95
                                     self.checker_completed,
 
96
                                     client_interface,
 
97
                                     byte_arrays=True)
 
98
        self.proxy.connect_to_signal(u"CheckerStarted",
 
99
                                     self.checker_started,
 
100
                                     client_interface,
 
101
                                     byte_arrays=True)
 
102
        self.proxy.connect_to_signal(u"GotSecret",
 
103
                                     self.got_secret,
 
104
                                     client_interface,
 
105
                                     byte_arrays=True)
 
106
        self.proxy.connect_to_signal(u"Rejected",
 
107
                                     self.rejected,
 
108
                                     client_interface,
 
109
                                     byte_arrays=True)
 
110
    
 
111
    def checker_completed(self, exitstatus, condition, command):
 
112
        if exitstatus == 0:
 
113
            self.logger(u'Checker for client %s (command "%s")'
 
114
                        u' was successful'
 
115
                        % (self.properties[u"name"], command))
 
116
            return
 
117
        if os.WIFEXITED(condition):
 
118
            self.logger(u'Checker for client %s (command "%s")'
 
119
                        u' failed with exit code %s'
 
120
                        % (self.properties[u"name"], command,
 
121
                           os.WEXITSTATUS(condition)))
 
122
            return
 
123
        if os.WIFSIGNALED(condition):
 
124
            self.logger(u'Checker for client %s (command "%s")'
 
125
                        u' was killed by signal %s'
 
126
                        % (self.properties[u"name"], command,
 
127
                           os.WTERMSIG(condition)))
 
128
            return
 
129
        if os.WCOREDUMP(condition):
 
130
            self.logger(u'Checker for client %s (command "%s")'
 
131
                        u' dumped core'
 
132
                        % (self.properties[u"name"], command))
 
133
        self.logger(u'Checker for client %s completed mysteriously')
 
134
    
 
135
    def checker_started(self, command):
 
136
        self.logger(u'Client %s started checker "%s"'
 
137
                    % (self.properties[u"name"], unicode(command)))
 
138
    
 
139
    def got_secret(self):
 
140
        self.logger(u'Client %s received its secret'
 
141
                    % self.properties[u"name"])
 
142
    
 
143
    def rejected(self):
 
144
        self.logger(u'Client %s was rejected'
 
145
                    % self.properties[u"name"])
 
146
    
 
147
    def selectable(self):
 
148
        """Make this a "selectable" widget.
 
149
        This overrides the method from urwid.FlowWidget."""
 
150
        return True
 
151
    
 
152
    def rows(self, (maxcol,), focus=False):
 
153
        """How many rows this widget will occupy might depend on
 
154
        whether we have focus or not.
 
155
        This overrides the method from urwid.FlowWidget"""
 
156
        return self.current_widget(focus).rows((maxcol,), focus=focus)
 
157
    
 
158
    def current_widget(self, focus=False):
 
159
        if focus or self.opened:
 
160
            return self._focus_widget
 
161
        return self._widget
 
162
    
 
163
    def update(self):
 
164
        "Called when what is visible on the screen should be updated."
 
165
        # How to add standout mode to a style
 
166
        with_standout = { u"normal": u"standout",
 
167
                          u"bold": u"bold-standout",
 
168
                          u"underline-blink":
 
169
                              u"underline-blink-standout",
 
170
                          u"bold-underline-blink":
 
171
                              u"bold-underline-blink-standout",
 
172
                          }
 
173
        
 
174
        # Rebuild focus and non-focus widgets using current properties
 
175
        self._text = (u'%(name)s: %(enabled)s'
 
176
                      % { u"name": self.properties[u"name"],
 
177
                          u"enabled":
 
178
                              (u"enabled"
 
179
                               if self.properties[u"enabled"]
 
180
                               else u"DISABLED")})
 
181
        if not urwid.supports_unicode():
 
182
            self._text = self._text.encode("ascii", "replace")
 
183
        textlist = [(u"normal", self._text)]
 
184
        self._text_widget.set_text(textlist)
 
185
        self._focus_text_widget.set_text([(with_standout[text[0]],
 
186
                                           text[1])
 
187
                                          if isinstance(text, tuple)
 
188
                                          else text
 
189
                                          for text in textlist])
 
190
        self._widget = self._text_widget
 
191
        self._focus_widget = urwid.AttrWrap(self._focus_text_widget,
 
192
                                            "standout")
 
193
        # Run update hook, if any
 
194
        if self.update_hook is not None:
 
195
            self.update_hook()
 
196
    
 
197
    def delete(self):
 
198
        if self.delete_hook is not None:
 
199
            self.delete_hook(self)
 
200
    
 
201
    def render(self, (maxcol,), focus=False):
 
202
        """Render differently if we have focus.
 
203
        This overrides the method from urwid.FlowWidget"""
 
204
        return self.current_widget(focus).render((maxcol,),
 
205
                                                 focus=focus)
 
206
    
 
207
    def keypress(self, (maxcol,), key):
 
208
        """Handle keys.
 
209
        This overrides the method from urwid.FlowWidget"""
 
210
        if key == u"e" or key == u"+":
 
211
            self.proxy.Enable()
 
212
        elif key == u"d" or key == u"-":
 
213
            self.proxy.Disable()
 
214
        elif key == u"r" or key == u"_" or key == u"ctrl k":
 
215
            self.server_proxy_object.RemoveClient(self.proxy
 
216
                                                  .object_path)
 
217
        elif key == u"s":
 
218
            self.proxy.StartChecker()
 
219
        elif key == u"S":
 
220
            self.proxy.StopChecker()
 
221
        elif key == u"C":
 
222
            self.proxy.CheckedOK()
 
223
        # xxx
 
224
#         elif key == u"p" or key == "=":
 
225
#             self.proxy.pause()
 
226
#         elif key == u"u" or key == ":":
 
227
#             self.proxy.unpause()
 
228
#         elif key == u"RET":
 
229
#             self.open()
 
230
        else:
 
231
            return key
 
232
    
 
233
    def property_changed(self, property=None, value=None,
 
234
                         *args, **kwargs):
 
235
        """Call self.update() if old value is not new value.
 
236
        This overrides the method from MandosClientPropertyCache"""
 
237
        property_name = unicode(property)
 
238
        old_value = self.properties.get(property_name)
 
239
        super(MandosClientWidget, self).property_changed(
 
240
            property=property, value=value, *args, **kwargs)
 
241
        if self.properties.get(property_name) != old_value:
 
242
            self.update()
 
243
 
 
244
 
 
245
class ConstrainedListBox(urwid.ListBox):
 
246
    """Like a normal urwid.ListBox, but will consume all "up" or
 
247
    "down" key presses, thus not allowing any containing widgets to
 
248
    use them as an excuse to shift focus away from this widget.
 
249
    """
 
250
    def keypress(self, (maxcol, maxrow), key):
 
251
        ret = super(ConstrainedListBox, self).keypress((maxcol, maxrow), key)
 
252
        if ret in (u"up", u"down"):
 
253
            return
 
254
        return ret
 
255
 
 
256
 
 
257
class UserInterface(object):
 
258
    """This is the entire user interface - the whole screen
 
259
    with boxes, lists of client widgets, etc.
 
260
    """
 
261
    def __init__(self, max_log_length=1000):
 
262
        DBusGMainLoop(set_as_default=True)
 
263
        
 
264
        self.screen = urwid.curses_display.Screen()
 
265
        
 
266
        self.screen.register_palette((
 
267
                (u"normal",
 
268
                 u"default", u"default", None),
 
269
                (u"bold",
 
270
                 u"default", u"default", u"bold"),
 
271
                (u"underline-blink",
 
272
                 u"default", u"default", u"underline"),
 
273
                (u"standout",
 
274
                 u"default", u"default", u"standout"),
 
275
                (u"bold-underline-blink",
 
276
                 u"default", u"default", (u"bold", u"underline")),
 
277
                (u"bold-standout",
 
278
                 u"default", u"default", (u"bold", u"standout")),
 
279
                (u"underline-blink-standout",
 
280
                 u"default", u"default", (u"underline", u"standout")),
 
281
                (u"bold-underline-blink-standout",
 
282
                 u"default", u"default", (u"bold", u"underline",
 
283
                                          u"standout")),
 
284
                ))
 
285
        
 
286
        if urwid.supports_unicode():
 
287
            self.divider = u"─" # \u2500
 
288
            #self.divider = u"━" # \u2501
 
289
        else:
 
290
            #self.divider = u"-" # \u002d
 
291
            self.divider = u"_" # \u005f
 
292
        
 
293
        self.screen.start()
 
294
        
 
295
        self.size = self.screen.get_cols_rows()
 
296
        
 
297
        self.clients = urwid.SimpleListWalker([])
 
298
        self.clients_dict = {}
 
299
        
 
300
        # We will add Text widgets to this list
 
301
        self.log = []
 
302
        self.max_log_length = max_log_length
 
303
        
 
304
        # We keep a reference to the log widget so we can remove it
 
305
        # from the ListWalker without it getting destroyed
 
306
        self.logbox = ConstrainedListBox(self.log)
 
307
        
 
308
        # This keeps track of whether self.uilist currently has
 
309
        # self.logbox in it or not
 
310
        self.log_visible = True
 
311
        self.log_wrap = u"any"
 
312
        
 
313
        self.rebuild()
 
314
        self.log_message_raw((u"bold",
 
315
                              u"Mandos Monitor version " + version))
 
316
        self.log_message_raw((u"bold",
 
317
                              u"q: Quit  ?: Help"))
 
318
        
 
319
        self.busname = domain + '.Mandos'
 
320
        self.main_loop = gobject.MainLoop()
 
321
        self.bus = dbus.SystemBus()
 
322
        mandos_dbus_objc = self.bus.get_object(
 
323
            self.busname, u"/", follow_name_owner_changes=True)
 
324
        self.mandos_serv = dbus.Interface(mandos_dbus_objc,
 
325
                                          dbus_interface
 
326
                                          = server_interface)
 
327
        try:
 
328
            mandos_clients = (self.mandos_serv
 
329
                              .GetAllClientsWithProperties())
 
330
        except dbus.exceptions.DBusException:
 
331
            mandos_clients = dbus.Dictionary()
 
332
        
 
333
        (self.mandos_serv
 
334
         .connect_to_signal(u"ClientRemoved",
 
335
                            self.find_and_remove_client,
 
336
                            dbus_interface=server_interface,
 
337
                            byte_arrays=True))
 
338
        (self.mandos_serv
 
339
         .connect_to_signal(u"ClientAdded",
 
340
                            self.add_new_client,
 
341
                            dbus_interface=server_interface,
 
342
                            byte_arrays=True))
 
343
        (self.mandos_serv
 
344
         .connect_to_signal(u"ClientNotFound",
 
345
                            self.client_not_found,
 
346
                            dbus_interface=server_interface,
 
347
                            byte_arrays=True))
 
348
        for path, client in mandos_clients.iteritems():
 
349
            client_proxy_object = self.bus.get_object(self.busname,
 
350
                                                      path)
 
351
            self.add_client(MandosClientWidget(server_proxy_object
 
352
                                               =self.mandos_serv,
 
353
                                               proxy_object
 
354
                                               =client_proxy_object,
 
355
                                               properties=client,
 
356
                                               update_hook
 
357
                                               =self.refresh,
 
358
                                               delete_hook
 
359
                                               =self.remove_client,
 
360
                                               logger
 
361
                                               =self.log_message),
 
362
                            path=path)
 
363
    
 
364
    def client_not_found(self, fingerprint, address):
 
365
        self.log_message((u"Client with address %s and fingerprint %s"
 
366
                          u" could not be found" % (address,
 
367
                                                    fingerprint)))
 
368
    
 
369
    def rebuild(self):
 
370
        """This rebuilds the User Interface.
 
371
        Call this when the widget layout needs to change"""
 
372
        self.uilist = []
 
373
        #self.uilist.append(urwid.ListBox(self.clients))
 
374
        self.uilist.append(urwid.Frame(ConstrainedListBox(self.clients),
 
375
                                       #header=urwid.Divider(),
 
376
                                       header=None,
 
377
                                       footer=urwid.Divider(div_char=self.divider)))
 
378
        if self.log_visible:
 
379
            self.uilist.append(self.logbox)
 
380
            pass
 
381
        self.topwidget = urwid.Pile(self.uilist)
 
382
    
 
383
    def log_message(self, message):
 
384
        timestamp = datetime.datetime.now().isoformat()
 
385
        self.log_message_raw(timestamp + u": " + message)
 
386
    
 
387
    def log_message_raw(self, markup):
 
388
        """Add a log message to the log buffer."""
 
389
        self.log.append(urwid.Text(markup, wrap=self.log_wrap))
 
390
        if (self.max_log_length
 
391
            and len(self.log) > self.max_log_length):
 
392
            del self.log[0:len(self.log)-self.max_log_length-1]
 
393
        self.logbox.set_focus(len(self.logbox.body.contents),
 
394
                              coming_from=u"above")
 
395
        self.refresh()
 
396
    
 
397
    def toggle_log_display(self):
 
398
        """Toggle visibility of the log buffer."""
 
399
        self.log_visible = not self.log_visible
 
400
        self.rebuild()
 
401
        self.log_message(u"Log visibility changed to: "
 
402
                         + unicode(self.log_visible))
 
403
    
 
404
    def change_log_display(self):
 
405
        """Change type of log display.
 
406
        Currently, this toggles wrapping of text lines."""
 
407
        if self.log_wrap == u"clip":
 
408
            self.log_wrap = u"any"
 
409
        else:
 
410
            self.log_wrap = u"clip"
 
411
        for textwidget in self.log:
 
412
            textwidget.set_wrap_mode(self.log_wrap)
 
413
        self.log_message(u"Wrap mode: " + self.log_wrap)
 
414
    
 
415
    def find_and_remove_client(self, path, name):
 
416
        """Find an client from its object path and remove it.
 
417
        
 
418
        This is connected to the ClientRemoved signal from the
 
419
        Mandos server object."""
 
420
        try:
 
421
            client = self.clients_dict[path]
 
422
        except KeyError:
 
423
            # not found?
 
424
            return
 
425
        self.remove_client(client, path)
 
426
    
 
427
    def add_new_client(self, path, properties):
 
428
        client_proxy_object = self.bus.get_object(self.busname, path)
 
429
        self.add_client(MandosClientWidget(server_proxy_object
 
430
                                           =self.mandos_serv,
 
431
                                           proxy_object
 
432
                                           =client_proxy_object,
 
433
                                           properties=properties,
 
434
                                           update_hook
 
435
                                           =self.refresh,
 
436
                                           delete_hook
 
437
                                           =self.remove_client),
 
438
                        path=path)
 
439
    
 
440
    def add_client(self, client, path=None):
 
441
        self.clients.append(client)
 
442
        if path is None:
 
443
            path = client.proxy.object_path
 
444
        self.clients_dict[path] = client
 
445
        self.clients.sort(None, lambda c: c.properties[u"name"])
 
446
        self.refresh()
 
447
    
 
448
    def remove_client(self, client, path=None):
 
449
        self.clients.remove(client)
 
450
        if path is None:
 
451
            path = client.proxy.object_path
 
452
        del self.clients_dict[path]
 
453
        if not self.clients_dict:
 
454
            # Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker
 
455
            # is completely emptied, we need to recreate it.
 
456
            self.clients = urwid.SimpleListWalker([])
 
457
            self.rebuild()
 
458
        self.refresh()
 
459
    
 
460
    def refresh(self):
 
461
        """Redraw the screen"""
 
462
        canvas = self.topwidget.render(self.size, focus=True)
 
463
        self.screen.draw_screen(self.size, canvas)
 
464
    
 
465
    def run(self):
 
466
        """Start the main loop and exit when it's done."""
 
467
        self.refresh()
 
468
        self._input_callback_tag = (gobject.io_add_watch
 
469
                                    (sys.stdin.fileno(),
 
470
                                     gobject.IO_IN,
 
471
                                     self.process_input))
 
472
        self.main_loop.run()
 
473
        # Main loop has finished, we should close everything now
 
474
        gobject.source_remove(self._input_callback_tag)
 
475
        self.screen.stop()
 
476
    
 
477
    def stop(self):
 
478
        self.main_loop.quit()
 
479
    
 
480
    def process_input(self, source, condition):
 
481
        keys = self.screen.get_input()
 
482
        translations = { u"ctrl n": u"down",      # Emacs
 
483
                         u"ctrl p": u"up",        # Emacs
 
484
                         u"ctrl v": u"page down", # Emacs
 
485
                         u"meta v": u"page up",   # Emacs
 
486
                         u" ": u"page down",      # less
 
487
                         u"f": u"page down",      # less
 
488
                         u"b": u"page up",        # less
 
489
                         u"j": u"down",           # vi
 
490
                         u"k": u"up",             # vi
 
491
                         }
 
492
        for key in keys:
 
493
            try:
 
494
                key = translations[key]
 
495
            except KeyError:    # :-)
 
496
                pass
 
497
            
 
498
            if key == u"q" or key == u"Q":
 
499
                self.stop()
 
500
                break
 
501
            elif key == u"window resize":
 
502
                self.size = self.screen.get_cols_rows()
 
503
                self.refresh()
 
504
            elif key == u"\f":  # Ctrl-L
 
505
                self.refresh()
 
506
            elif key == u"l" or key == u"D":
 
507
                self.toggle_log_display()
 
508
                self.refresh()
 
509
            elif key == u"w" or key == u"i":
 
510
                self.change_log_display()
 
511
                self.refresh()
 
512
            elif key == u"?" or key == u"f1" or key == u"esc":
 
513
                if not self.log_visible:
 
514
                    self.log_visible = True
 
515
                    self.rebuild()
 
516
                self.log_message_raw((u"bold",
 
517
                                      u"  ".
 
518
                                      join((u"q: Quit",
 
519
                                            u"?: Help",
 
520
                                            u"l: Log window toggle",
 
521
                                            u"TAB: Switch window",
 
522
                                            u"w: Wrap (log)"))))
 
523
                self.log_message_raw((u"bold",
 
524
                                      u"  "
 
525
                                      .join((u"Clients:",
 
526
                                             u"e: Enable",
 
527
                                             u"d: Disable",
 
528
                                             u"r: Remove",
 
529
                                             u"s: Start new checker",
 
530
                                             u"S: Stop checker",
 
531
                                             u"C: Checker OK"))))
 
532
                self.refresh()
 
533
            elif key == u"tab":
 
534
                if self.topwidget.get_focus() is self.logbox:
 
535
                    self.topwidget.set_focus(0)
 
536
                else:
 
537
                    self.topwidget.set_focus(self.logbox)
 
538
                self.refresh()
 
539
            #elif (key == u"end" or key == u"meta >" or key == u"G"
 
540
            #      or key == u">"):
 
541
            #    pass            # xxx end-of-buffer
 
542
            #elif (key == u"home" or key == u"meta <" or key == u"g"
 
543
            #      or key == u"<"):
 
544
            #    pass            # xxx beginning-of-buffer
 
545
            #elif key == u"ctrl e" or key == u"$":
 
546
            #    pass            # xxx move-end-of-line
 
547
            #elif key == u"ctrl a" or key == u"^":
 
548
            #    pass            # xxx move-beginning-of-line
 
549
            #elif key == u"ctrl b" or key == u"meta (" or key == u"h":
 
550
            #    pass            # xxx left
 
551
            #elif key == u"ctrl f" or key == u"meta )" or key == u"l":
 
552
            #    pass            # xxx right
 
553
            #elif key == u"a":
 
554
            #    pass            # scroll up log
 
555
            #elif key == u"z":
 
556
            #    pass            # scroll down log
 
557
            elif self.topwidget.selectable():
 
558
                self.topwidget.keypress(self.size, key)
 
559
                self.refresh()
 
560
        return True
 
561
 
 
562
ui = UserInterface()
 
563
try:
 
564
    ui.run()
 
565
except:
 
566
    ui.screen.stop()
 
567
    raise