/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: 2016-03-05 21:46:00 UTC
  • Revision ID: teddy@recompile.se-20160305214600-2d7peg8qb5xhpi64
Fix incorrect documentation of minor limitation

* mandos-monitor.xml (BUGS): The name which is hard-coded is the D-Bus
                             *bus* name, not the service name.  Fix
                             the documentation.

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-2014 Teddy Hogeborn
7
 
# Copyright © 2009-2014 Björn Påhlsson
 
6
# Copyright © 2009-2016 Teddy Hogeborn
 
7
# Copyright © 2009-2016 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
40
40
 
41
41
from dbus.mainloop.glib import DBusGMainLoop
42
42
try:
43
 
    import gobject
 
43
    from gi.repository import GObject
44
44
except ImportError:
45
 
    from gi.repository import GObject as gobject
 
45
    import gobject as GObject
46
46
 
47
47
import dbus
48
48
 
49
49
import locale
50
50
 
51
 
if sys.version_info[0] == 2:
 
51
if sys.version_info.major == 2:
52
52
    str = unicode
53
53
 
54
54
locale.setlocale(locale.LC_ALL, '')
60
60
domain = 'se.recompile'
61
61
server_interface = domain + '.Mandos'
62
62
client_interface = domain + '.Mandos.Client'
63
 
version = "1.6.4"
 
63
version = "1.7.3"
 
64
 
 
65
try:
 
66
    dbus.OBJECT_MANAGER_IFACE
 
67
except AttributeError:
 
68
    dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
64
69
 
65
70
def isoformat_to_datetime(iso):
66
71
    "Parse an ISO 8601 date string to a datetime.datetime()"
87
92
        self.proxy = proxy_object # Mandos Client proxy object
88
93
        self.properties = dict() if properties is None else properties
89
94
        self.property_changed_match = (
90
 
            self.proxy.connect_to_signal("PropertyChanged",
91
 
                                         self._property_changed,
92
 
                                         client_interface,
 
95
            self.proxy.connect_to_signal("PropertiesChanged",
 
96
                                         self.properties_changed,
 
97
                                         dbus.PROPERTIES_IFACE,
93
98
                                         byte_arrays=True))
94
99
        
95
100
        if properties is None:
100
105
        
101
106
        super(MandosClientPropertyCache, self).__init__(**kwargs)
102
107
    
103
 
    def _property_changed(self, property, value):
104
 
        """Helper which takes positional arguments"""
105
 
        return self.property_changed(property=property, value=value)
106
 
    
107
 
    def property_changed(self, property=None, value=None):
108
 
        """This is called whenever we get a PropertyChanged signal
109
 
        It updates the changed property in the "properties" dict.
 
108
    def properties_changed(self, interface, properties, invalidated):
 
109
        """This is called whenever we get a PropertiesChanged signal
 
110
        It updates the changed properties in the "properties" dict.
110
111
        """
111
112
        # Update properties dict with new value
112
 
        self.properties[property] = value
 
113
        if interface == client_interface:
 
114
            self.properties.update(properties)
113
115
    
114
116
    def delete(self):
115
117
        self.property_changed_match.remove()
161
163
                                         self.rejected,
162
164
                                         client_interface,
163
165
                                         byte_arrays=True))
164
 
        #self.logger('Created client {0}'
165
 
        #            .format(self.properties["Name"]))
 
166
        self.logger('Created client {}'
 
167
                    .format(self.properties["Name"]), level=0)
166
168
    
167
169
    def using_timer(self, flag):
168
170
        """Call this method with True or False when timer should be
170
172
        """
171
173
        if flag and self._update_timer_callback_tag is None:
172
174
            # Will update the shown timer value every second
173
 
            self._update_timer_callback_tag = (gobject.timeout_add
 
175
            self._update_timer_callback_tag = (GObject.timeout_add
174
176
                                               (1000,
175
177
                                                self.update_timer))
176
178
        elif not (flag or self._update_timer_callback_tag is None):
177
 
            gobject.source_remove(self._update_timer_callback_tag)
 
179
            GObject.source_remove(self._update_timer_callback_tag)
178
180
            self._update_timer_callback_tag = None
179
181
    
180
182
    def checker_completed(self, exitstatus, condition, command):
181
183
        if exitstatus == 0:
 
184
            self.logger('Checker for client {} (command "{}")'
 
185
                        ' succeeded'.format(self.properties["Name"],
 
186
                                            command), level=0)
182
187
            self.update()
183
188
            return
184
189
        # Checker failed
185
190
        if os.WIFEXITED(condition):
186
 
            self.logger('Checker for client {0} (command "{1}")'
187
 
                        ' failed with exit code {2}'
 
191
            self.logger('Checker for client {} (command "{}") failed'
 
192
                        ' with exit code {}'
188
193
                        .format(self.properties["Name"], command,
189
194
                                os.WEXITSTATUS(condition)))
190
195
        elif os.WIFSIGNALED(condition):
191
 
            self.logger('Checker for client {0} (command "{1}") was'
192
 
                        ' killed by signal {2}'
 
196
            self.logger('Checker for client {} (command "{}") was'
 
197
                        ' killed by signal {}'
193
198
                        .format(self.properties["Name"], command,
194
199
                                os.WTERMSIG(condition)))
195
 
        elif os.WCOREDUMP(condition):
196
 
            self.logger('Checker for client {0} (command "{1}")'
197
 
                        ' dumped core'
198
 
                        .format(self.properties["Name"], command))
199
 
        else:
200
 
            self.logger('Checker for client {0} completed'
201
 
                        ' mysteriously'
202
 
                        .format(self.properties["Name"]))
203
200
        self.update()
204
201
    
205
202
    def checker_started(self, command):
206
 
        """Server signals that a checker started. This could be useful
207
 
           to log in the future. """
208
 
        #self.logger('Client {0} started checker "{1}"'
209
 
        #            .format(self.properties["Name"],
210
 
        #                    str(command)))
211
 
        pass
 
203
        """Server signals that a checker started."""
 
204
        self.logger('Client {} started checker "{}"'
 
205
                    .format(self.properties["Name"],
 
206
                            command), level=0)
212
207
    
213
208
    def got_secret(self):
214
 
        self.logger('Client {0} received its secret'
 
209
        self.logger('Client {} received its secret'
215
210
                    .format(self.properties["Name"]))
216
211
    
217
212
    def need_approval(self, timeout, default):
218
213
        if not default:
219
 
            message = 'Client {0} needs approval within {1} seconds'
 
214
            message = 'Client {} needs approval within {} seconds'
220
215
        else:
221
 
            message = 'Client {0} will get its secret in {1} seconds'
 
216
            message = 'Client {} will get its secret in {} seconds'
222
217
        self.logger(message.format(self.properties["Name"],
223
218
                                   timeout/1000))
224
219
    
225
220
    def rejected(self, reason):
226
 
        self.logger('Client {0} was rejected; reason: {1}'
 
221
        self.logger('Client {} was rejected; reason: {}'
227
222
                    .format(self.properties["Name"], reason))
228
223
    
229
224
    def selectable(self):
273
268
            else:
274
269
                timer = datetime.timedelta()
275
270
            if self.properties["ApprovedByDefault"]:
276
 
                message = "Approval in {0}. (d)eny?"
 
271
                message = "Approval in {}. (d)eny?"
277
272
            else:
278
 
                message = "Denial in {0}. (a)pprove?"
 
273
                message = "Denial in {}. (a)pprove?"
279
274
            message = message.format(str(timer).rsplit(".", 1)[0])
280
275
            self.using_timer(True)
281
276
        elif self.properties["LastCheckerStatus"] != 0:
289
284
                timer = max(expires - datetime.datetime.utcnow(),
290
285
                            datetime.timedelta())
291
286
            message = ('A checker has failed! Time until client'
292
 
                       ' gets disabled: {0}'
 
287
                       ' gets disabled: {}'
293
288
                       .format(str(timer).rsplit(".", 1)[0]))
294
289
            self.using_timer(True)
295
290
        else:
296
291
            message = "enabled"
297
292
            self.using_timer(False)
298
 
        self._text = "{0}{1}".format(base, message)
 
293
        self._text = "{}{}".format(base, message)
299
294
        
300
295
        if not urwid.supports_unicode():
301
296
            self._text = self._text.encode("ascii", "replace")
314
309
            self.update_hook()
315
310
    
316
311
    def update_timer(self):
317
 
        """called by gobject. Will indefinitely loop until
318
 
        gobject.source_remove() on tag is called"""
 
312
        """called by GObject. Will indefinitely loop until
 
313
        GObject.source_remove() on tag is called"""
319
314
        self.update()
320
315
        return True             # Keep calling this
321
316
    
322
317
    def delete(self, **kwargs):
323
318
        if self._update_timer_callback_tag is not None:
324
 
            gobject.source_remove(self._update_timer_callback_tag)
 
319
            GObject.source_remove(self._update_timer_callback_tag)
325
320
            self._update_timer_callback_tag = None
326
321
        for match in self.match_objects:
327
322
            match.remove()
340
335
        """Handle keys.
341
336
        This overrides the method from urwid.FlowWidget"""
342
337
        if key == "+":
343
 
            self.proxy.Enable(dbus_interface = client_interface,
344
 
                              ignore_reply=True)
 
338
            self.proxy.Set(client_interface, "Enabled",
 
339
                           dbus.Boolean(True), ignore_reply = True,
 
340
                           dbus_interface = dbus.PROPERTIES_IFACE)
345
341
        elif key == "-":
346
 
            self.proxy.Disable(dbus_interface = client_interface,
347
 
                               ignore_reply=True)
 
342
            self.proxy.Set(client_interface, "Enabled", False,
 
343
                           ignore_reply = True,
 
344
                           dbus_interface = dbus.PROPERTIES_IFACE)
348
345
        elif key == "a":
349
346
            self.proxy.Approve(dbus.Boolean(True, variant_level=1),
350
347
                               dbus_interface = client_interface,
358
355
                                                  .object_path,
359
356
                                                  ignore_reply=True)
360
357
        elif key == "s":
361
 
            self.proxy.StartChecker(dbus_interface = client_interface,
362
 
                                    ignore_reply=True)
 
358
            self.proxy.Set(client_interface, "CheckerRunning",
 
359
                           dbus.Boolean(True), ignore_reply = True,
 
360
                           dbus_interface = dbus.PROPERTIES_IFACE)
363
361
        elif key == "S":
364
 
            self.proxy.StopChecker(dbus_interface = client_interface,
365
 
                                   ignore_reply=True)
 
362
            self.proxy.Set(client_interface, "CheckerRunning",
 
363
                           dbus.Boolean(False), ignore_reply = True,
 
364
                           dbus_interface = dbus.PROPERTIES_IFACE)
366
365
        elif key == "C":
367
366
            self.proxy.CheckedOK(dbus_interface = client_interface,
368
367
                                 ignore_reply=True)
376
375
        else:
377
376
            return key
378
377
    
379
 
    def property_changed(self, property=None, **kwargs):
380
 
        """Call self.update() if old value is not new value.
 
378
    def properties_changed(self, interface, properties, invalidated):
 
379
        """Call self.update() if any properties changed.
381
380
        This overrides the method from MandosClientPropertyCache"""
382
 
        property_name = str(property)
383
 
        old_value = self.properties.get(property_name)
384
 
        super(MandosClientWidget, self).property_changed(
385
 
            property=property, **kwargs)
386
 
        if self.properties.get(property_name) != old_value:
 
381
        old_values = { key: self.properties.get(key)
 
382
                       for key in properties.keys() }
 
383
        super(MandosClientWidget, self).properties_changed(
 
384
            interface, properties, invalidated)
 
385
        if any(old_values[key] != self.properties.get(key)
 
386
               for key in old_values):
387
387
            self.update()
388
388
 
389
389
 
403
403
    """This is the entire user interface - the whole screen
404
404
    with boxes, lists of client widgets, etc.
405
405
    """
406
 
    def __init__(self, max_log_length=1000):
 
406
    def __init__(self, max_log_length=1000, log_level=1):
407
407
        DBusGMainLoop(set_as_default=True)
408
408
        
409
409
        self.screen = urwid.curses_display.Screen()
447
447
        self.log = []
448
448
        self.max_log_length = max_log_length
449
449
        
 
450
        self.log_level = log_level
 
451
        
450
452
        # We keep a reference to the log widget so we can remove it
451
453
        # from the ListWalker without it getting destroyed
452
454
        self.logbox = ConstrainedListBox(self.log)
463
465
                              "q: Quit  ?: Help"))
464
466
        
465
467
        self.busname = domain + '.Mandos'
466
 
        self.main_loop = gobject.MainLoop()
 
468
        self.main_loop = GObject.MainLoop()
467
469
    
468
470
    def client_not_found(self, fingerprint, address):
469
 
        self.log_message("Client with address {0} and fingerprint"
470
 
                         " {1} could not be found"
 
471
        self.log_message("Client with address {} and fingerprint {}"
 
472
                         " could not be found"
471
473
                         .format(address, fingerprint))
472
474
    
473
475
    def rebuild(self):
486
488
            self.uilist.append(self.logbox)
487
489
        self.topwidget = urwid.Pile(self.uilist)
488
490
    
489
 
    def log_message(self, message):
 
491
    def log_message(self, message, level=1):
490
492
        """Log message formatted with timestamp"""
 
493
        if level < self.log_level:
 
494
            return
491
495
        timestamp = datetime.datetime.now().isoformat()
492
 
        self.log_message_raw(timestamp + ": " + message)
 
496
        self.log_message_raw("{}: {}".format(timestamp, message),
 
497
                             level=level)
493
498
    
494
 
    def log_message_raw(self, markup):
 
499
    def log_message_raw(self, markup, level=1):
495
500
        """Add a log message to the log buffer."""
 
501
        if level < self.log_level:
 
502
            return
496
503
        self.log.append(urwid.Text(markup, wrap=self.log_wrap))
497
504
        if (self.max_log_length
498
505
            and len(self.log) > self.max_log_length):
505
512
        """Toggle visibility of the log buffer."""
506
513
        self.log_visible = not self.log_visible
507
514
        self.rebuild()
508
 
        #self.log_message("Log visibility changed to: "
509
 
        #                 + str(self.log_visible))
 
515
        self.log_message("Log visibility changed to: {}"
 
516
                         .format(self.log_visible), level=0)
510
517
    
511
518
    def change_log_display(self):
512
519
        """Change type of log display.
517
524
            self.log_wrap = "clip"
518
525
        for textwidget in self.log:
519
526
            textwidget.set_wrap_mode(self.log_wrap)
520
 
        #self.log_message("Wrap mode: " + self.log_wrap)
 
527
        self.log_message("Wrap mode: {}".format(self.log_wrap),
 
528
                         level=0)
521
529
    
522
 
    def find_and_remove_client(self, path, name):
 
530
    def find_and_remove_client(self, path, interfaces):
523
531
        """Find a client by its object path and remove it.
524
532
        
525
 
        This is connected to the ClientRemoved signal from the
 
533
        This is connected to the InterfacesRemoved signal from the
526
534
        Mandos server object."""
 
535
        if client_interface not in interfaces:
 
536
            # Not a Mandos client object; ignore
 
537
            return
527
538
        try:
528
539
            client = self.clients_dict[path]
529
540
        except KeyError:
530
541
            # not found?
531
 
            self.log_message("Unknown client {0!r} ({1!r}) removed"
532
 
                             .format(name, path))
 
542
            self.log_message("Unknown client {!r} removed"
 
543
                             .format(path))
533
544
            return
534
545
        client.delete()
535
546
    
536
 
    def add_new_client(self, path):
 
547
    def add_new_client(self, path, ifs_and_props):
 
548
        """Find a client by its object path and remove it.
 
549
        
 
550
        This is connected to the InterfacesAdded signal from the
 
551
        Mandos server object.
 
552
        """
 
553
        if client_interface not in ifs_and_props:
 
554
            # Not a Mandos client object; ignore
 
555
            return
537
556
        client_proxy_object = self.bus.get_object(self.busname, path)
538
557
        self.add_client(MandosClientWidget(server_proxy_object
539
558
                                           =self.mandos_serv,
544
563
                                           delete_hook
545
564
                                           =self.remove_client,
546
565
                                           logger
547
 
                                           =self.log_message),
 
566
                                           =self.log_message,
 
567
                                           properties
 
568
                                           = dict(ifs_and_props[
 
569
                                               client_interface])),
548
570
                        path=path)
549
571
    
550
572
    def add_client(self, client, path=None):
585
607
            mandos_clients = dbus.Dictionary()
586
608
        
587
609
        (self.mandos_serv
588
 
         .connect_to_signal("ClientRemoved",
 
610
         .connect_to_signal("InterfacesRemoved",
589
611
                            self.find_and_remove_client,
590
 
                            dbus_interface=server_interface,
 
612
                            dbus_interface
 
613
                            = dbus.OBJECT_MANAGER_IFACE,
591
614
                            byte_arrays=True))
592
615
        (self.mandos_serv
593
 
         .connect_to_signal("ClientAdded",
 
616
         .connect_to_signal("InterfacesAdded",
594
617
                            self.add_new_client,
595
 
                            dbus_interface=server_interface,
 
618
                            dbus_interface
 
619
                            = dbus.OBJECT_MANAGER_IFACE,
596
620
                            byte_arrays=True))
597
621
        (self.mandos_serv
598
622
         .connect_to_signal("ClientNotFound",
616
640
                            path=path)
617
641
        
618
642
        self.refresh()
619
 
        self._input_callback_tag = (gobject.io_add_watch
 
643
        self._input_callback_tag = (GObject.io_add_watch
620
644
                                    (sys.stdin.fileno(),
621
 
                                     gobject.IO_IN,
 
645
                                     GObject.IO_IN,
622
646
                                     self.process_input))
623
647
        self.main_loop.run()
624
648
        # Main loop has finished, we should close everything now
625
 
        gobject.source_remove(self._input_callback_tag)
 
649
        GObject.source_remove(self._input_callback_tag)
626
650
        self.screen.stop()
627
651
    
628
652
    def stop(self):
652
676
            elif key == "window resize":
653
677
                self.size = self.screen.get_cols_rows()
654
678
                self.refresh()
655
 
            elif key == "\f":  # Ctrl-L
 
679
            elif key == "ctrl l":
 
680
                self.screen.clear()
656
681
                self.refresh()
657
682
            elif key == "l" or key == "D":
658
683
                self.toggle_log_display()
670
695
                                            "?: Help",
671
696
                                            "l: Log window toggle",
672
697
                                            "TAB: Switch window",
673
 
                                            "w: Wrap (log)"))))
 
698
                                            "w: Wrap (log lines)",
 
699
                                            "v: Toggle verbose log",
 
700
                                            ))))
674
701
                self.log_message_raw(("bold",
675
702
                                      "  "
676
703
                                      .join(("Clients:",
689
716
                else:
690
717
                    self.topwidget.set_focus(self.logbox)
691
718
                self.refresh()
 
719
            elif key == "v":
 
720
                if self.log_level == 0:
 
721
                    self.log_level = 1
 
722
                    self.log_message("Verbose mode: Off")
 
723
                else:
 
724
                    self.log_level = 0
 
725
                    self.log_message("Verbose mode: On")
692
726
            #elif (key == "end" or key == "meta >" or key == "G"
693
727
            #      or key == ">"):
694
728
            #    pass            # xxx end-of-buffer