/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-05 19:16:46 UTC
  • Revision ID: teddy@fukt.bsnet.se-20091105191646-5l7bkq5h4wkh3huh
* mandos-monitor: New prototype version of interactive server
                  administraton tool using D-Bus.

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
8
7
import signal
9
8
 
10
 
import datetime
11
 
 
12
9
import urwid.curses_display
13
10
import urwid
14
11
 
19
16
 
20
17
import UserList
21
18
 
22
 
import locale
23
 
 
24
 
locale.setlocale(locale.LC_ALL, u'')
25
 
 
26
19
# Some useful constants
27
20
domain = 'se.bsnet.fukt'
28
21
server_interface = domain + '.Mandos'
37
30
urwid.curses_display.curses.A_UNDERLINE |= (
38
31
    urwid.curses_display.curses.A_BLINK)
39
32
 
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
 
 
56
33
class MandosClientPropertyCache(object):
57
34
    """This wraps a Mandos Client D-Bus proxy object, caches the
58
35
    properties and calls a hook function when any of them are
59
36
    changed.
60
37
    """
61
 
    def __init__(self, proxy_object=None, *args, **kwargs):
 
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
            }
62
59
        self.proxy = proxy_object # Mandos Client proxy object
63
60
        
64
 
        self.properties = dict()
65
 
        self.proxy.connect_to_signal(u"PropertyChanged",
 
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",
66
68
                                     self.property_changed,
67
69
                                     client_interface,
68
70
                                     byte_arrays=True)
69
71
        
70
 
        self.properties.update(
71
 
            self.proxy.GetAll(client_interface,
72
 
                              dbus_interface = dbus.PROPERTIES_IFACE))
 
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())
73
79
        super(MandosClientPropertyCache, self).__init__(
74
 
            proxy_object=proxy_object, *args, **kwargs)
 
80
            proxy_object=proxy_object,
 
81
            properties=properties, *args, **kwargs)
75
82
    
 
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)
76
98
    def property_changed(self, property=None, value=None):
77
99
        """This is called whenever we get a PropertyChanged signal
78
100
        It updates the changed property in the "properties" dict.
79
101
        """
 
102
        # Convert name and value
 
103
        property_name, cvalue = self.convert_property(property, value)
80
104
        # Update properties dict with new value
81
 
        self.properties[property] = value
 
105
        self.properties[property_name] = cvalue
82
106
 
83
107
 
84
108
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
86
110
    """
87
111
    
88
112
    def __init__(self, server_proxy_object=None, update_hook=None,
89
 
                 delete_hook=None, logger=None, *args, **kwargs):
 
113
                 delete_hook=None, *args, **kwargs):
90
114
        # Called on update
91
115
        self.update_hook = update_hook
92
116
        # Called on delete
93
117
        self.delete_hook = delete_hook
94
118
        # Mandos Server proxy object
95
119
        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
101
120
        
102
121
        # The widget shown normally
103
 
        self._text_widget = urwid.Text(u"")
 
122
        self._text_widget = urwid.Text("")
104
123
        # The widget shown when we have focus
105
 
        self._focus_text_widget = urwid.Text(u"")
 
124
        self._focus_text_widget = urwid.Text("")
106
125
        super(MandosClientWidget, self).__init__(
107
126
            update_hook=update_hook, delete_hook=delete_hook,
108
127
            *args, **kwargs)
109
128
        self.update()
110
129
        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"])
188
130
    
189
131
    def selectable(self):
190
132
        """Make this a "selectable" widget.
214
156
                          }
215
157
        
216
158
        # Rebuild focus and non-focus widgets using current 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"")})
 
159
        self._text = (u'name="%(name)s", enabled=%(enabled)s'
 
160
                      % self.properties)
240
161
        if not urwid.supports_unicode():
241
162
            self._text = self._text.encode("ascii", "replace")
242
 
        textlist = [(u"normal", self._text)]
 
163
        textlist = [(u"normal", u"BLÄRGH: "), (u"bold", self._text)]
243
164
        self._text_widget.set_text(textlist)
244
165
        self._focus_text_widget.set_text([(with_standout[text[0]],
245
166
                                           text[1])
253
174
        if self.update_hook is not None:
254
175
            self.update_hook()
255
176
    
256
 
    def update_timer(self):
257
 
        "called by gobject"
258
 
        self.update()
259
 
        return True             # Keep calling this
260
 
    
261
177
    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
265
178
        if self.delete_hook is not None:
266
179
            self.delete_hook(self)
267
180
    
278
191
            self.proxy.Enable()
279
192
        elif key == u"d" or key == u"-":
280
193
            self.proxy.Disable()
281
 
        elif key == u"r" or key == u"_" or key == u"ctrl k":
 
194
        elif key == u"r" or key == u"_":
282
195
            self.server_proxy_object.RemoveClient(self.proxy
283
196
                                                  .object_path)
284
197
        elif key == u"s":
285
198
            self.proxy.StartChecker()
 
199
        elif key == u"c":
 
200
            self.proxy.StopChecker()
286
201
        elif key == u"S":
287
 
            self.proxy.StopChecker()
288
 
        elif key == u"C":
289
202
            self.proxy.CheckedOK()
290
203
        # xxx
291
204
#         elif key == u"p" or key == "=":
309
222
            self.update()
310
223
 
311
224
 
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
 
 
324
225
class UserInterface(object):
325
226
    """This is the entire user interface - the whole screen
326
227
    with boxes, lists of client widgets, etc.
327
228
    """
328
 
    def __init__(self, max_log_length=1000):
329
 
        DBusGMainLoop(set_as_default=True)
 
229
    def __init__(self):
 
230
        DBusGMainLoop(set_as_default=True )
330
231
        
331
232
        self.screen = urwid.curses_display.Screen()
332
233
        
350
251
                                          u"standout")),
351
252
                ))
352
253
        
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
 
        
360
254
        self.screen.start()
361
255
        
362
256
        self.size = self.screen.get_cols_rows()
363
257
        
364
258
        self.clients = urwid.SimpleListWalker([])
365
259
        self.clients_dict = {}
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"))
 
260
        self.topwidget = urwid.LineBox(urwid.ListBox(self.clients))
 
261
        #self.topwidget = urwid.ListBox(clients)
385
262
        
386
263
        self.busname = domain + '.Mandos'
387
264
        self.main_loop = gobject.MainLoop()
398
275
            mandos_clients = dbus.Dictionary()
399
276
        
400
277
        (self.mandos_serv
401
 
         .connect_to_signal(u"ClientRemoved",
 
278
         .connect_to_signal("ClientRemoved",
402
279
                            self.find_and_remove_client,
403
280
                            dbus_interface=server_interface,
404
281
                            byte_arrays=True))
405
282
        (self.mandos_serv
406
 
         .connect_to_signal(u"ClientAdded",
 
283
         .connect_to_signal("ClientAdded",
407
284
                            self.add_new_client,
408
285
                            dbus_interface=server_interface,
409
286
                            byte_arrays=True))
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():
 
287
        for path, client in (mandos_clients.iteritems()):
416
288
            client_proxy_object = self.bus.get_object(self.busname,
417
289
                                                      path)
418
290
            self.add_client(MandosClientWidget(server_proxy_object
423
295
                                               update_hook
424
296
                                               =self.refresh,
425
297
                                               delete_hook
426
 
                                               =self.remove_client,
427
 
                                               logger
428
 
                                               =self.log_message),
 
298
                                               =self.remove_client),
429
299
                            path=path)
430
300
    
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
 
    
482
301
    def find_and_remove_client(self, path, name):
483
302
        """Find an client from its object path and remove it.
484
303
        
491
310
            return
492
311
        self.remove_client(client, path)
493
312
    
494
 
    def add_new_client(self, path):
 
313
    def add_new_client(self, path, properties):
495
314
        client_proxy_object = self.bus.get_object(self.busname, path)
496
315
        self.add_client(MandosClientWidget(server_proxy_object
497
316
                                           =self.mandos_serv,
498
317
                                           proxy_object
499
318
                                           =client_proxy_object,
 
319
                                           properties=properties,
500
320
                                           update_hook
501
321
                                           =self.refresh,
502
322
                                           delete_hook
503
 
                                           =self.remove_client,
504
 
                                           logger
505
 
                                           =self.log_message),
 
323
                                           =self.remove_client),
506
324
                        path=path)
507
325
    
508
326
    def add_client(self, client, path=None):
518
336
        if path is None:
519
337
            path = client.proxy.object_path
520
338
        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()
526
339
        self.refresh()
527
340
    
528
341
    def refresh(self):
547
360
    
548
361
    def process_input(self, source, condition):
549
362
        keys = self.screen.get_input()
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
 
363
        translations = { u"j": u"down",
 
364
                         u"k": u"up",
559
365
                         }
560
366
        for key in keys:
561
367
            try:
569
375
            elif key == u"window resize":
570
376
                self.size = self.screen.get_cols_rows()
571
377
                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
 
378
            elif key == " ":
 
379
                self.refresh()
625
380
            elif self.topwidget.selectable():
626
381
                self.topwidget.keypress(self.size, key)
627
382
                self.refresh()
630
385
ui = UserInterface()
631
386
try:
632
387
    ui.run()
633
 
except Exception, e:
634
 
    ui.log_message(unicode(e))
 
388
except:
635
389
    ui.screen.stop()
636
390
    raise