/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 at bsnet
  • Date: 2010-08-24 18:18:01 UTC
  • mto: (24.1.154 mandos)
  • mto: This revision was merged to the branch mainline in revision 421.
  • Revision ID: teddy@fukt.bsnet.se-20100824181801-n798wxs117trv8iu
* mandos: Use logging.getLogger() as in the documentation.
* mandos-monitor: Suppress logging from dbus.proxies.

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