/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: 2014-07-25 23:42:53 UTC
  • mto: This revision was merged to the branch mainline in revision 724.
  • Revision ID: teddy@recompile.se-20140725234253-m11fdsv01fetbk51
Use the .major attribute on sys.version_info instead of using "[0]".

The components of sys.version_info can now be accessed by attributes
instead of by numerical index, so do that.

* mandos-ctl: Get major version by using "sys.version_info.major".
* mandos-monitor: - '' -

Show diffs side-by-side

added added

removed removed

Lines of Context:
3
3
4
4
# Mandos Monitor - Control and monitor the Mandos server
5
5
6
 
# Copyright © 2009-2012 Teddy Hogeborn
7
 
# Copyright © 2009-2012 Björn Påhlsson
 
6
# Copyright © 2009-2014 Teddy Hogeborn
 
7
# Copyright © 2009-2014 Björn Påhlsson
8
8
9
9
# This program is free software: you can redistribute it and/or modify
10
10
# it under the terms of the GNU General Public License as published by
25
25
 
26
26
from __future__ import (division, absolute_import, print_function,
27
27
                        unicode_literals)
 
28
try:
 
29
    from future_builtins import *
 
30
except ImportError:
 
31
    pass
28
32
 
29
33
import sys
30
34
import os
31
 
import signal
32
35
 
33
36
import datetime
34
37
 
36
39
import urwid
37
40
 
38
41
from dbus.mainloop.glib import DBusGMainLoop
39
 
import gobject
 
42
try:
 
43
    import gobject
 
44
except ImportError:
 
45
    from gi.repository import GObject as gobject
40
46
 
41
47
import dbus
42
48
 
43
 
import UserList
44
 
 
45
49
import locale
46
50
 
 
51
if sys.version_info.major == 2:
 
52
    str = unicode
 
53
 
47
54
locale.setlocale(locale.LC_ALL, '')
48
55
 
49
56
import logging
53
60
domain = 'se.recompile'
54
61
server_interface = domain + '.Mandos'
55
62
client_interface = domain + '.Mandos.Client'
56
 
version = "1.5.3"
57
 
 
58
 
# Always run in monochrome mode
59
 
urwid.curses_display.curses.has_colors = lambda : False
60
 
 
61
 
# Urwid doesn't support blinking, but we want it.  Since we have no
62
 
# use for underline on its own, we make underline also always blink.
63
 
urwid.curses_display.curses.A_UNDERLINE |= (
64
 
    urwid.curses_display.curses.A_BLINK)
 
63
version = "1.6.7"
65
64
 
66
65
def isoformat_to_datetime(iso):
67
66
    "Parse an ISO 8601 date string to a datetime.datetime()"
84
83
    properties and calls a hook function when any of them are
85
84
    changed.
86
85
    """
87
 
    def __init__(self, proxy_object=None, *args, **kwargs):
 
86
    def __init__(self, proxy_object=None, properties=None, **kwargs):
88
87
        self.proxy = proxy_object # Mandos Client proxy object
89
 
        
90
 
        self.properties = dict()
 
88
        self.properties = dict() if properties is None else properties
91
89
        self.property_changed_match = (
92
90
            self.proxy.connect_to_signal("PropertyChanged",
93
 
                                         self.property_changed,
 
91
                                         self._property_changed,
94
92
                                         client_interface,
95
93
                                         byte_arrays=True))
96
94
        
97
 
        self.properties.update(
98
 
            self.proxy.GetAll(client_interface,
99
 
                              dbus_interface = dbus.PROPERTIES_IFACE))
100
 
 
101
 
        #XXX This breaks good super behaviour
102
 
#        super(MandosClientPropertyCache, self).__init__(
103
 
#            *args, **kwargs)
 
95
        if properties is None:
 
96
            self.properties.update(
 
97
                self.proxy.GetAll(client_interface,
 
98
                                  dbus_interface
 
99
                                  = dbus.PROPERTIES_IFACE))
 
100
        
 
101
        super(MandosClientPropertyCache, self).__init__(**kwargs)
 
102
    
 
103
    def _property_changed(self, property, value):
 
104
        """Helper which takes positional arguments"""
 
105
        return self.property_changed(property=property, value=value)
104
106
    
105
107
    def property_changed(self, property=None, value=None):
106
108
        """This is called whenever we get a PropertyChanged signal
109
111
        # Update properties dict with new value
110
112
        self.properties[property] = value
111
113
    
112
 
    def delete(self, *args, **kwargs):
 
114
    def delete(self):
113
115
        self.property_changed_match.remove()
114
 
        super(MandosClientPropertyCache, self).__init__(
115
 
            *args, **kwargs)
116
116
 
117
117
 
118
118
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
120
120
    """
121
121
    
122
122
    def __init__(self, server_proxy_object=None, update_hook=None,
123
 
                 delete_hook=None, logger=None, *args, **kwargs):
 
123
                 delete_hook=None, logger=None, **kwargs):
124
124
        # Called on update
125
125
        self.update_hook = update_hook
126
126
        # Called on delete
131
131
        self.logger = logger
132
132
        
133
133
        self._update_timer_callback_tag = None
134
 
        self._update_timer_callback_lock = 0
135
134
        
136
135
        # The widget shown normally
137
136
        self._text_widget = urwid.Text("")
138
137
        # The widget shown when we have focus
139
138
        self._focus_text_widget = urwid.Text("")
140
 
        super(MandosClientWidget, self).__init__(
141
 
            update_hook=update_hook, delete_hook=delete_hook,
142
 
            *args, **kwargs)
 
139
        super(MandosClientWidget, self).__init__(**kwargs)
143
140
        self.update()
144
141
        self.opened = False
145
142
        
146
 
        last_checked_ok = isoformat_to_datetime(self.properties
147
 
                                                ["LastCheckedOK"])
148
 
        
149
 
        if self.properties ["LastCheckerStatus"] != 0:
150
 
            self.using_timer(True)
151
 
        
152
 
        if self.need_approval:
153
 
            self.using_timer(True)
154
 
        
155
143
        self.match_objects = (
156
144
            self.proxy.connect_to_signal("CheckerCompleted",
157
145
                                         self.checker_completed,
173
161
                                         self.rejected,
174
162
                                         client_interface,
175
163
                                         byte_arrays=True))
176
 
        #self.logger('Created client {0}'
177
 
        #            .format(self.properties["Name"]))
178
 
    
179
 
    def property_changed(self, property=None, value=None):
180
 
        super(self, MandosClientWidget).property_changed(property,
181
 
                                                         value)
182
 
        if property == "ApprovalPending":
183
 
            using_timer(bool(value))
184
 
        if property == "LastCheckerStatus":
185
 
            using_timer(value != 0)
186
 
            #self.logger('Checker for client {0} (command "{1}") was '
187
 
            #            ' successful'.format(self.properties["Name"],
188
 
            #                                 command))
 
164
        self.logger('Created client {}'
 
165
                    .format(self.properties["Name"]), level=0)
189
166
    
190
167
    def using_timer(self, flag):
191
168
        """Call this method with True or False when timer should be
192
169
        activated or deactivated.
193
170
        """
194
 
        old = self._update_timer_callback_lock
195
 
        if flag:
196
 
            self._update_timer_callback_lock += 1
197
 
        else:
198
 
            self._update_timer_callback_lock -= 1
199
 
        if old == 0 and self._update_timer_callback_lock:
 
171
        if flag and self._update_timer_callback_tag is None:
200
172
            # Will update the shown timer value every second
201
173
            self._update_timer_callback_tag = (gobject.timeout_add
202
174
                                               (1000,
203
175
                                                self.update_timer))
204
 
        elif old and self._update_timer_callback_lock == 0:
 
176
        elif not (flag or self._update_timer_callback_tag is None):
205
177
            gobject.source_remove(self._update_timer_callback_tag)
206
178
            self._update_timer_callback_tag = None
207
179
    
208
180
    def checker_completed(self, exitstatus, condition, command):
209
181
        if exitstatus == 0:
 
182
            self.logger('Checker for client {} (command "{}")'
 
183
                        ' succeeded'.format(self.properties["Name"],
 
184
                                            command), level=0)
210
185
            self.update()
211
186
            return
212
187
        # Checker failed
213
188
        if os.WIFEXITED(condition):
214
 
            self.logger('Checker for client {0} (command "{1}")'
215
 
                        ' failed with exit code {2}'
 
189
            self.logger('Checker for client {} (command "{}") failed'
 
190
                        ' with exit code {}'
216
191
                        .format(self.properties["Name"], command,
217
192
                                os.WEXITSTATUS(condition)))
218
193
        elif os.WIFSIGNALED(condition):
219
 
            self.logger('Checker for client {0} (command "{1}") was'
220
 
                        ' killed by signal {2}'
 
194
            self.logger('Checker for client {} (command "{}") was'
 
195
                        ' killed by signal {}'
221
196
                        .format(self.properties["Name"], command,
222
197
                                os.WTERMSIG(condition)))
223
198
        elif os.WCOREDUMP(condition):
224
 
            self.logger('Checker for client {0} (command "{1}")'
225
 
                        ' dumped core'
226
 
                        .format(self.properties["Name"], command))
 
199
            self.logger('Checker for client {} (command "{}") dumped'
 
200
                        ' core'.format(self.properties["Name"],
 
201
                                       command))
227
202
        else:
228
 
            self.logger('Checker for client {0} completed'
 
203
            self.logger('Checker for client {} completed'
229
204
                        ' mysteriously'
230
205
                        .format(self.properties["Name"]))
231
206
        self.update()
232
207
    
233
208
    def checker_started(self, command):
234
 
        """Server signals that a checker started. This could be useful
235
 
           to log in the future. """
236
 
        #self.logger('Client {0} started checker "{1}"'
237
 
        #            .format(self.properties["Name"],
238
 
        #                    unicode(command)))
239
 
        pass
 
209
        """Server signals that a checker started."""
 
210
        self.logger('Client {} started checker "{}"'
 
211
                    .format(self.properties["Name"],
 
212
                            command), level=0)
240
213
    
241
214
    def got_secret(self):
242
 
        self.logger('Client {0} received its secret'
 
215
        self.logger('Client {} received its secret'
243
216
                    .format(self.properties["Name"]))
244
217
    
245
218
    def need_approval(self, timeout, default):
246
219
        if not default:
247
 
            message = 'Client {0} needs approval within {1} seconds'
 
220
            message = 'Client {} needs approval within {} seconds'
248
221
        else:
249
 
            message = 'Client {0} will get its secret in {1} seconds'
 
222
            message = 'Client {} will get its secret in {} seconds'
250
223
        self.logger(message.format(self.properties["Name"],
251
224
                                   timeout/1000))
252
 
        self.using_timer(True)
253
225
    
254
226
    def rejected(self, reason):
255
 
        self.logger('Client {0} was rejected; reason: {1}'
 
227
        self.logger('Client {} was rejected; reason: {}'
256
228
                    .format(self.properties["Name"], reason))
257
229
    
258
230
    def selectable(self):
281
253
                          "bold-underline-blink":
282
254
                              "bold-underline-blink-standout",
283
255
                          }
284
 
 
 
256
        
285
257
        # Rebuild focus and non-focus widgets using current properties
286
 
 
 
258
        
287
259
        # Base part of a client. Name!
288
260
        base = '{name}: '.format(name=self.properties["Name"])
289
261
        if not self.properties["Enabled"]:
290
262
            message = "DISABLED"
 
263
            self.using_timer(False)
291
264
        elif self.properties["ApprovalPending"]:
292
265
            timeout = datetime.timedelta(milliseconds
293
266
                                         = self.properties
295
268
            last_approval_request = isoformat_to_datetime(
296
269
                self.properties["LastApprovalRequest"])
297
270
            if last_approval_request is not None:
298
 
                timer = timeout - (datetime.datetime.utcnow()
299
 
                                   - last_approval_request)
 
271
                timer = max(timeout - (datetime.datetime.utcnow()
 
272
                                       - last_approval_request),
 
273
                            datetime.timedelta())
300
274
            else:
301
275
                timer = datetime.timedelta()
302
276
            if self.properties["ApprovedByDefault"]:
303
 
                message = "Approval in {0}. (d)eny?"
 
277
                message = "Approval in {}. (d)eny?"
304
278
            else:
305
 
                message = "Denial in {0}. (a)pprove?"
306
 
            message = message.format(unicode(timer).rsplit(".", 1)[0])
 
279
                message = "Denial in {}. (a)pprove?"
 
280
            message = message.format(str(timer).rsplit(".", 1)[0])
 
281
            self.using_timer(True)
307
282
        elif self.properties["LastCheckerStatus"] != 0:
308
283
            # When checker has failed, show timer until client expires
309
284
            expires = self.properties["Expires"]
312
287
            else:
313
288
                expires = (datetime.datetime.strptime
314
289
                           (expires, '%Y-%m-%dT%H:%M:%S.%f'))
315
 
                timer = expires - datetime.datetime.utcnow()
 
290
                timer = max(expires - datetime.datetime.utcnow(),
 
291
                            datetime.timedelta())
316
292
            message = ('A checker has failed! Time until client'
317
 
                       ' gets disabled: {0}'
318
 
                       .format(unicode(timer).rsplit(".", 1)[0]))
 
293
                       ' gets disabled: {}'
 
294
                       .format(str(timer).rsplit(".", 1)[0]))
 
295
            self.using_timer(True)
319
296
        else:
320
297
            message = "enabled"
321
 
        self._text = "{0}{1}".format(base, message)
322
 
            
 
298
            self.using_timer(False)
 
299
        self._text = "{}{}".format(base, message)
 
300
        
323
301
        if not urwid.supports_unicode():
324
302
            self._text = self._text.encode("ascii", "replace")
325
303
        textlist = [("normal", self._text)]
342
320
        self.update()
343
321
        return True             # Keep calling this
344
322
    
345
 
    def delete(self, *args, **kwargs):
 
323
    def delete(self, **kwargs):
346
324
        if self._update_timer_callback_tag is not None:
347
325
            gobject.source_remove(self._update_timer_callback_tag)
348
326
            self._update_timer_callback_tag = None
351
329
        self.match_objects = ()
352
330
        if self.delete_hook is not None:
353
331
            self.delete_hook(self)
354
 
        return super(MandosClientWidget, self).delete(*args, **kwargs)
 
332
        return super(MandosClientWidget, self).delete(**kwargs)
355
333
    
356
334
    def render(self, maxcolrow, focus=False):
357
335
        """Render differently if we have focus.
399
377
        else:
400
378
            return key
401
379
    
402
 
    def property_changed(self, property=None, value=None,
403
 
                         *args, **kwargs):
 
380
    def property_changed(self, property=None, **kwargs):
404
381
        """Call self.update() if old value is not new value.
405
382
        This overrides the method from MandosClientPropertyCache"""
406
 
        property_name = unicode(property)
 
383
        property_name = str(property)
407
384
        old_value = self.properties.get(property_name)
408
385
        super(MandosClientWidget, self).property_changed(
409
 
            property=property, value=value, *args, **kwargs)
 
386
            property=property, **kwargs)
410
387
        if self.properties.get(property_name) != old_value:
411
388
            self.update()
412
389
 
416
393
    "down" key presses, thus not allowing any containing widgets to
417
394
    use them as an excuse to shift focus away from this widget.
418
395
    """
419
 
    def keypress(self, maxcolrow, key):
420
 
        ret = super(ConstrainedListBox, self).keypress(maxcolrow, key)
 
396
    def keypress(self, *args, **kwargs):
 
397
        ret = super(ConstrainedListBox, self).keypress(*args, **kwargs)
421
398
        if ret in ("up", "down"):
422
399
            return
423
400
        return ret
427
404
    """This is the entire user interface - the whole screen
428
405
    with boxes, lists of client widgets, etc.
429
406
    """
430
 
    def __init__(self, max_log_length=1000):
 
407
    def __init__(self, max_log_length=1000, log_level=1):
431
408
        DBusGMainLoop(set_as_default=True)
432
409
        
433
410
        self.screen = urwid.curses_display.Screen()
436
413
                ("normal",
437
414
                 "default", "default", None),
438
415
                ("bold",
439
 
                 "default", "default", "bold"),
 
416
                 "bold", "default", "bold"),
440
417
                ("underline-blink",
441
 
                 "default", "default", "underline"),
 
418
                 "underline,blink", "default", "underline,blink"),
442
419
                ("standout",
443
 
                 "default", "default", "standout"),
 
420
                 "standout", "default", "standout"),
444
421
                ("bold-underline-blink",
445
 
                 "default", "default", ("bold", "underline")),
 
422
                 "bold,underline,blink", "default", "bold,underline,blink"),
446
423
                ("bold-standout",
447
 
                 "default", "default", ("bold", "standout")),
 
424
                 "bold,standout", "default", "bold,standout"),
448
425
                ("underline-blink-standout",
449
 
                 "default", "default", ("underline", "standout")),
 
426
                 "underline,blink,standout", "default",
 
427
                 "underline,blink,standout"),
450
428
                ("bold-underline-blink-standout",
451
 
                 "default", "default", ("bold", "underline",
452
 
                                          "standout")),
 
429
                 "bold,underline,blink,standout", "default",
 
430
                 "bold,underline,blink,standout"),
453
431
                ))
454
432
        
455
433
        if urwid.supports_unicode():
470
448
        self.log = []
471
449
        self.max_log_length = max_log_length
472
450
        
 
451
        self.log_level = log_level
 
452
        
473
453
        # We keep a reference to the log widget so we can remove it
474
454
        # from the ListWalker without it getting destroyed
475
455
        self.logbox = ConstrainedListBox(self.log)
489
469
        self.main_loop = gobject.MainLoop()
490
470
    
491
471
    def client_not_found(self, fingerprint, address):
492
 
        self.log_message("Client with address {0} and fingerprint"
493
 
                         " {1} could not be found"
 
472
        self.log_message("Client with address {} and fingerprint {}"
 
473
                         " could not be found"
494
474
                         .format(address, fingerprint))
495
475
    
496
476
    def rebuild(self):
509
489
            self.uilist.append(self.logbox)
510
490
        self.topwidget = urwid.Pile(self.uilist)
511
491
    
512
 
    def log_message(self, message):
 
492
    def log_message(self, message, level=1):
 
493
        """Log message formatted with timestamp"""
 
494
        if level < self.log_level:
 
495
            return
513
496
        timestamp = datetime.datetime.now().isoformat()
514
 
        self.log_message_raw(timestamp + ": " + message)
 
497
        self.log_message_raw("{}: {}".format(timestamp, message),
 
498
                             level=level)
515
499
    
516
 
    def log_message_raw(self, markup):
 
500
    def log_message_raw(self, markup, level=1):
517
501
        """Add a log message to the log buffer."""
 
502
        if level < self.log_level:
 
503
            return
518
504
        self.log.append(urwid.Text(markup, wrap=self.log_wrap))
519
505
        if (self.max_log_length
520
506
            and len(self.log) > self.max_log_length):
527
513
        """Toggle visibility of the log buffer."""
528
514
        self.log_visible = not self.log_visible
529
515
        self.rebuild()
530
 
        #self.log_message("Log visibility changed to: "
531
 
        #                 + unicode(self.log_visible))
 
516
        self.log_message("Log visibility changed to: {}"
 
517
                         .format(self.log_visible), level=0)
532
518
    
533
519
    def change_log_display(self):
534
520
        """Change type of log display.
539
525
            self.log_wrap = "clip"
540
526
        for textwidget in self.log:
541
527
            textwidget.set_wrap_mode(self.log_wrap)
542
 
        #self.log_message("Wrap mode: " + self.log_wrap)
 
528
        self.log_message("Wrap mode: {}".format(self.log_wrap),
 
529
                         level=0)
543
530
    
544
531
    def find_and_remove_client(self, path, name):
545
532
        """Find a client by its object path and remove it.
550
537
            client = self.clients_dict[path]
551
538
        except KeyError:
552
539
            # not found?
553
 
            self.log_message("Unknown client {0!r} ({1!r}) removed"
 
540
            self.log_message("Unknown client {!r} ({!r}) removed"
554
541
                             .format(name, path))
555
542
            return
556
543
        client.delete()
574
561
        if path is None:
575
562
            path = client.proxy.object_path
576
563
        self.clients_dict[path] = client
577
 
        self.clients.sort(None, lambda c: c.properties["Name"])
 
564
        self.clients.sort(key=lambda c: c.properties["Name"])
578
565
        self.refresh()
579
566
    
580
567
    def remove_client(self, client, path=None):
582
569
        if path is None:
583
570
            path = client.proxy.object_path
584
571
        del self.clients_dict[path]
585
 
        if not self.clients_dict:
586
 
            # Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker
587
 
            # is completely emptied, we need to recreate it.
588
 
            self.clients = urwid.SimpleListWalker([])
589
 
            self.rebuild()
590
572
        self.refresh()
591
573
    
592
574
    def refresh(self):
605
587
        try:
606
588
            mandos_clients = (self.mandos_serv
607
589
                              .GetAllClientsWithProperties())
 
590
            if not mandos_clients:
 
591
                self.log_message_raw(("bold", "Note: Server has no clients."))
608
592
        except dbus.exceptions.DBusException:
 
593
            self.log_message_raw(("bold", "Note: No Mandos server running."))
609
594
            mandos_clients = dbus.Dictionary()
610
595
        
611
596
        (self.mandos_serv
623
608
                            self.client_not_found,
624
609
                            dbus_interface=server_interface,
625
610
                            byte_arrays=True))
626
 
        for path, client in mandos_clients.iteritems():
 
611
        for path, client in mandos_clients.items():
627
612
            client_proxy_object = self.bus.get_object(self.busname,
628
613
                                                      path)
629
614
            self.add_client(MandosClientWidget(server_proxy_object
638
623
                                               logger
639
624
                                               =self.log_message),
640
625
                            path=path)
641
 
 
 
626
        
642
627
        self.refresh()
643
628
        self._input_callback_tag = (gobject.io_add_watch
644
629
                                    (sys.stdin.fileno(),
676
661
            elif key == "window resize":
677
662
                self.size = self.screen.get_cols_rows()
678
663
                self.refresh()
679
 
            elif key == "\f":  # Ctrl-L
 
664
            elif key == "ctrl l":
 
665
                self.screen.clear()
680
666
                self.refresh()
681
667
            elif key == "l" or key == "D":
682
668
                self.toggle_log_display()
694
680
                                            "?: Help",
695
681
                                            "l: Log window toggle",
696
682
                                            "TAB: Switch window",
697
 
                                            "w: Wrap (log)"))))
 
683
                                            "w: Wrap (log lines)",
 
684
                                            "v: Toggle verbose log",
 
685
                                            ))))
698
686
                self.log_message_raw(("bold",
699
687
                                      "  "
700
688
                                      .join(("Clients:",
713
701
                else:
714
702
                    self.topwidget.set_focus(self.logbox)
715
703
                self.refresh()
 
704
            elif key == "v":
 
705
                if self.log_level == 0:
 
706
                    self.log_level = 1
 
707
                    self.log_message("Verbose mode: Off")
 
708
                else:
 
709
                    self.log_level = 0
 
710
                    self.log_message("Verbose mode: On")
716
711
            #elif (key == "end" or key == "meta >" or key == "G"
717
712
            #      or key == ">"):
718
713
            #    pass            # xxx end-of-buffer
741
736
    ui.run()
742
737
except KeyboardInterrupt:
743
738
    ui.screen.stop()
744
 
except Exception, e:
745
 
    ui.log_message(unicode(e))
 
739
except Exception as e:
 
740
    ui.log_message(str(e))
746
741
    ui.screen.stop()
747
742
    raise