/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: 2019-07-29 16:35:53 UTC
  • Revision ID: teddy@recompile.se-20190729163553-1i442i2cbx64c537
Make tests and man page examples match

Make the tests test_manual_page_example[1-5] match exactly what is
written in the manual page, and add comments to manual page as
reminders to keep tests and manual page examples in sync.

* mandos-ctl (Test_commands_from_options.test_manual_page_example_1):
  Remove "--verbose" option, since the manual does not have it as the
  first example, and change assertion to match.
* mandos-ctl.xml (EXAMPLE): Add comments to all examples documenting
  which test function they correspond to.  Also remove unnecessary
  quotes from option arguments in fourth example, and clarify language
  slightly in fifth example.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/python3 -bb
 
1
#!/usr/bin/python
2
2
# -*- mode: python; coding: utf-8 -*-
3
3
#
4
4
# Mandos Monitor - Control and monitor the Mandos server
33
33
 
34
34
import sys
35
35
import os
36
 
import warnings
 
36
 
37
37
import datetime
38
38
 
39
39
import urwid.curses_display
51
51
if sys.version_info.major == 2:
52
52
    str = unicode
53
53
 
54
 
log = logging.getLogger(os.path.basename(sys.argv[0]))
55
 
logging.basicConfig(level="NOTSET", # Show all messages
56
 
                    format="%(message)s") # Show basic log messages
57
 
 
58
 
logging.captureWarnings(True)   # Show warnings via the logging system
59
 
 
60
54
locale.setlocale(locale.LC_ALL, '')
61
55
 
62
56
logging.getLogger('dbus.proxies').setLevel(logging.CRITICAL)
65
59
domain = 'se.recompile'
66
60
server_interface = domain + '.Mandos'
67
61
client_interface = domain + '.Mandos.Client'
68
 
version = "1.8.9"
 
62
version = "1.8.4"
69
63
 
70
64
try:
71
65
    dbus.OBJECT_MANAGER_IFACE
128
122
    """
129
123
 
130
124
    def __init__(self, server_proxy_object=None, update_hook=None,
131
 
                 delete_hook=None, **kwargs):
 
125
                 delete_hook=None, logger=None, **kwargs):
132
126
        # Called on update
133
127
        self.update_hook = update_hook
134
128
        # Called on delete
135
129
        self.delete_hook = delete_hook
136
130
        # Mandos Server proxy object
137
131
        self.server_proxy_object = server_proxy_object
 
132
        # Logger
 
133
        self.logger = logger
138
134
 
139
135
        self._update_timer_callback_tag = None
140
136
 
167
163
                                         self.rejected,
168
164
                                         client_interface,
169
165
                                         byte_arrays=True))
170
 
        log.debug('Created client %s', self.properties["Name"])
 
166
        self.logger('Created client {}'
 
167
                    .format(self.properties["Name"]), level=0)
171
168
 
172
169
    def using_timer(self, flag):
173
170
        """Call this method with True or False when timer should be
175
172
        """
176
173
        if flag and self._update_timer_callback_tag is None:
177
174
            # Will update the shown timer value every second
178
 
            self._update_timer_callback_tag = (
179
 
                GLib.timeout_add(1000,
180
 
                                 glib_safely(self.update_timer)))
 
175
            self._update_timer_callback_tag = (GLib.timeout_add
 
176
                                               (1000,
 
177
                                                self.update_timer))
181
178
        elif not (flag or self._update_timer_callback_tag is None):
182
179
            GLib.source_remove(self._update_timer_callback_tag)
183
180
            self._update_timer_callback_tag = None
184
181
 
185
182
    def checker_completed(self, exitstatus, condition, command):
186
183
        if exitstatus == 0:
187
 
            log.debug('Checker for client %s (command "%s")'
188
 
                      ' succeeded', self.properties["Name"], command)
 
184
            self.logger('Checker for client {} (command "{}")'
 
185
                        ' succeeded'.format(self.properties["Name"],
 
186
                                            command), level=0)
189
187
            self.update()
190
188
            return
191
189
        # Checker failed
192
190
        if os.WIFEXITED(condition):
193
 
            log.info('Checker for client %s (command "%s") failed'
194
 
                     ' with exit code %d', self.properties["Name"],
195
 
                     command, os.WEXITSTATUS(condition))
 
191
            self.logger('Checker for client {} (command "{}") failed'
 
192
                        ' with exit code {}'
 
193
                        .format(self.properties["Name"], command,
 
194
                                os.WEXITSTATUS(condition)))
196
195
        elif os.WIFSIGNALED(condition):
197
 
            log.info('Checker for client %s (command "%s") was'
198
 
                     ' killed by signal %d', self.properties["Name"],
199
 
                     command, os.WTERMSIG(condition))
 
196
            self.logger('Checker for client {} (command "{}") was'
 
197
                        ' killed by signal {}'
 
198
                        .format(self.properties["Name"], command,
 
199
                                os.WTERMSIG(condition)))
200
200
        self.update()
201
201
 
202
202
    def checker_started(self, command):
203
203
        """Server signals that a checker started."""
204
 
        log.debug('Client %s started checker "%s"',
205
 
                  self.properties["Name"], command)
 
204
        self.logger('Client {} started checker "{}"'
 
205
                    .format(self.properties["Name"],
 
206
                            command), level=0)
206
207
 
207
208
    def got_secret(self):
208
 
        log.info("Client %s received its secret",
209
 
                 self.properties["Name"])
 
209
        self.logger('Client {} received its secret'
 
210
                    .format(self.properties["Name"]))
210
211
 
211
212
    def need_approval(self, timeout, default):
212
213
        if not default:
213
 
            message = "Client %s needs approval within %f seconds"
 
214
            message = 'Client {} needs approval within {} seconds'
214
215
        else:
215
 
            message = "Client %s will get its secret in %f seconds"
216
 
        log.info(message, self.properties["Name"], timeout/1000)
 
216
            message = 'Client {} will get its secret in {} seconds'
 
217
        self.logger(message.format(self.properties["Name"],
 
218
                                   timeout/1000))
217
219
 
218
220
    def rejected(self, reason):
219
 
        log.info("Client %s was rejected; reason: %s",
220
 
                 self.properties["Name"], reason)
 
221
        self.logger('Client {} was rejected; reason: {}'
 
222
                    .format(self.properties["Name"], reason))
221
223
 
222
224
    def selectable(self):
223
225
        """Make this a "selectable" widget.
385
387
            self.update()
386
388
 
387
389
 
388
 
def glib_safely(func, retval=True):
389
 
    def safe_func(*args, **kwargs):
390
 
        try:
391
 
            return func(*args, **kwargs)
392
 
        except Exception:
393
 
            log.exception("")
394
 
            return retval
395
 
    return safe_func
396
 
 
397
 
 
398
390
class ConstrainedListBox(urwid.ListBox):
399
391
    """Like a normal urwid.ListBox, but will consume all "up" or
400
392
    "down" key presses, thus not allowing any containing widgets to
412
404
    """This is the entire user interface - the whole screen
413
405
    with boxes, lists of client widgets, etc.
414
406
    """
415
 
    def __init__(self, max_log_length=1000):
 
407
    def __init__(self, max_log_length=1000, log_level=1):
416
408
        DBusGMainLoop(set_as_default=True)
417
409
 
418
410
        self.screen = urwid.curses_display.Screen()
455
447
        self.log = urwid.SimpleListWalker([])
456
448
        self.max_log_length = max_log_length
457
449
 
 
450
        self.log_level = log_level
 
451
 
458
452
        # We keep a reference to the log widget so we can remove it
459
453
        # from the ListWalker without it getting destroyed
460
454
        self.logbox = ConstrainedListBox(self.log)
464
458
        self.log_visible = True
465
459
        self.log_wrap = "any"
466
460
 
467
 
        self.loghandler = UILogHandler(self)
468
 
 
469
461
        self.rebuild()
470
 
        self.add_log_line(("bold",
471
 
                           "Mandos Monitor version " + version))
472
 
        self.add_log_line(("bold", "q: Quit  ?: Help"))
 
462
        self.log_message_raw(("bold",
 
463
                              "Mandos Monitor version " + version))
 
464
        self.log_message_raw(("bold",
 
465
                              "q: Quit  ?: Help"))
473
466
 
474
467
        self.busname = domain + '.Mandos'
475
468
        self.main_loop = GLib.MainLoop()
476
469
 
477
 
    def client_not_found(self, key_id, address):
478
 
        log.info("Client with address %s and key ID %s could"
479
 
                 " not be found", address, key_id)
 
470
    def client_not_found(self, fingerprint, address):
 
471
        self.log_message("Client with address {} and fingerprint {}"
 
472
                         " could not be found"
 
473
                         .format(address, fingerprint))
480
474
 
481
475
    def rebuild(self):
482
476
        """This rebuilds the User Interface.
493
487
            self.uilist.append(self.logbox)
494
488
        self.topwidget = urwid.Pile(self.uilist)
495
489
 
496
 
    def add_log_line(self, markup):
 
490
    def log_message(self, message, level=1):
 
491
        """Log message formatted with timestamp"""
 
492
        if level < self.log_level:
 
493
            return
 
494
        timestamp = datetime.datetime.now().isoformat()
 
495
        self.log_message_raw("{}: {}".format(timestamp, message),
 
496
                             level=level)
 
497
 
 
498
    def log_message_raw(self, markup, level=1):
 
499
        """Add a log message to the log buffer."""
 
500
        if level < self.log_level:
 
501
            return
497
502
        self.log.append(urwid.Text(markup, wrap=self.log_wrap))
498
503
        if self.max_log_length:
499
504
            if len(self.log) > self.max_log_length:
500
 
                del self.log[0:(len(self.log) - self.max_log_length)]
 
505
                del self.log[0:len(self.log)-self.max_log_length-1]
501
506
        self.logbox.set_focus(len(self.logbox.body.contents)-1,
502
507
                              coming_from="above")
503
508
        self.refresh()
506
511
        """Toggle visibility of the log buffer."""
507
512
        self.log_visible = not self.log_visible
508
513
        self.rebuild()
509
 
        log.debug("Log visibility changed to: %s", self.log_visible)
 
514
        self.log_message("Log visibility changed to: {}"
 
515
                         .format(self.log_visible), level=0)
510
516
 
511
517
    def change_log_display(self):
512
518
        """Change type of log display.
517
523
            self.log_wrap = "clip"
518
524
        for textwidget in self.log:
519
525
            textwidget.set_wrap_mode(self.log_wrap)
520
 
        log.debug("Wrap mode: %s", self.log_wrap)
 
526
        self.log_message("Wrap mode: {}".format(self.log_wrap),
 
527
                         level=0)
521
528
 
522
529
    def find_and_remove_client(self, path, interfaces):
523
530
        """Find a client by its object path and remove it.
531
538
            client = self.clients_dict[path]
532
539
        except KeyError:
533
540
            # not found?
534
 
            log.warning("Unknown client %s removed", path)
 
541
            self.log_message("Unknown client {!r} removed"
 
542
                             .format(path))
535
543
            return
536
544
        client.delete()
537
545
 
550
558
            proxy_object=client_proxy_object,
551
559
            update_hook=self.refresh,
552
560
            delete_hook=self.remove_client,
 
561
            logger=self.log_message,
553
562
            properties=dict(ifs_and_props[client_interface])),
554
563
                        path=path)
555
564
 
575
584
 
576
585
    def run(self):
577
586
        """Start the main loop and exit when it's done."""
578
 
        log.addHandler(self.loghandler)
579
 
        self.orig_log_propagate = log.propagate
580
 
        log.propagate = False
581
 
        self.orig_log_level = log.level
582
 
        log.setLevel("INFO")
583
587
        self.bus = dbus.SystemBus()
584
588
        mandos_dbus_objc = self.bus.get_object(
585
589
            self.busname, "/", follow_name_owner_changes=True)
589
593
            mandos_clients = (self.mandos_serv
590
594
                              .GetAllClientsWithProperties())
591
595
            if not mandos_clients:
592
 
                log.warning("Note: Server has no clients.")
 
596
                self.log_message_raw(("bold",
 
597
                                      "Note: Server has no clients."))
593
598
        except dbus.exceptions.DBusException:
594
 
            log.warning("Note: No Mandos server running.")
 
599
            self.log_message_raw(("bold",
 
600
                                  "Note: No Mandos server running."))
595
601
            mandos_clients = dbus.Dictionary()
596
602
 
597
603
        (self.mandos_serv
617
623
                proxy_object=client_proxy_object,
618
624
                properties=client,
619
625
                update_hook=self.refresh,
620
 
                delete_hook=self.remove_client),
 
626
                delete_hook=self.remove_client,
 
627
                logger=self.log_message),
621
628
                            path=path)
622
629
 
623
630
        self.refresh()
624
 
        self._input_callback_tag = (
625
 
            GLib.io_add_watch(
626
 
                GLib.IOChannel.unix_new(sys.stdin.fileno()),
627
 
                GLib.PRIORITY_DEFAULT, GLib.IO_IN,
628
 
                glib_safely(self.process_input)))
 
631
        self._input_callback_tag = (GLib.io_add_watch
 
632
                                    (sys.stdin.fileno(),
 
633
                                     GLib.IO_IN,
 
634
                                     self.process_input))
629
635
        self.main_loop.run()
630
636
        # Main loop has finished, we should close everything now
631
637
        GLib.source_remove(self._input_callback_tag)
632
 
        with warnings.catch_warnings():
633
 
            warnings.simplefilter("ignore", BytesWarning)
634
 
            self.screen.stop()
 
638
        self.screen.stop()
635
639
 
636
640
    def stop(self):
637
641
        self.main_loop.quit()
638
 
        log.removeHandler(self.loghandler)
639
 
        log.propagate = self.orig_log_propagate
640
642
 
641
643
    def process_input(self, source, condition):
642
644
        keys = self.screen.get_input()
675
677
                if not self.log_visible:
676
678
                    self.log_visible = True
677
679
                    self.rebuild()
678
 
                self.add_log_line(("bold",
679
 
                                   "  ".join(("q: Quit",
680
 
                                              "?: Help",
681
 
                                              "l: Log window toggle",
682
 
                                              "TAB: Switch window",
683
 
                                              "w: Wrap (log lines)",
684
 
                                              "v: Toggle verbose log",
685
 
                                   ))))
686
 
                self.add_log_line(("bold",
687
 
                                   "  ".join(("Clients:",
688
 
                                              "+: Enable",
689
 
                                              "-: Disable",
690
 
                                              "R: Remove",
691
 
                                              "s: Start new checker",
692
 
                                              "S: Stop checker",
693
 
                                              "C: Checker OK",
694
 
                                              "a: Approve",
695
 
                                              "d: Deny",
696
 
                                   ))))
 
680
                self.log_message_raw(("bold",
 
681
                                      "  ".
 
682
                                      join(("q: Quit",
 
683
                                            "?: Help",
 
684
                                            "l: Log window toggle",
 
685
                                            "TAB: Switch window",
 
686
                                            "w: Wrap (log lines)",
 
687
                                            "v: Toggle verbose log",
 
688
                                            ))))
 
689
                self.log_message_raw(("bold",
 
690
                                      "  "
 
691
                                      .join(("Clients:",
 
692
                                             "+: Enable",
 
693
                                             "-: Disable",
 
694
                                             "R: Remove",
 
695
                                             "s: Start new checker",
 
696
                                             "S: Stop checker",
 
697
                                             "C: Checker OK",
 
698
                                             "a: Approve",
 
699
                                             "d: Deny"))))
697
700
                self.refresh()
698
701
            elif key == "tab":
699
702
                if self.topwidget.get_focus() is self.logbox:
702
705
                    self.topwidget.set_focus(self.logbox)
703
706
                self.refresh()
704
707
            elif key == "v":
705
 
                if log.level < logging.INFO:
706
 
                    log.setLevel(logging.INFO)
707
 
                    log.info("Verbose mode: Off")
 
708
                if self.log_level == 0:
 
709
                    self.log_level = 1
 
710
                    self.log_message("Verbose mode: Off")
708
711
                else:
709
 
                    log.setLevel(logging.NOTSET)
710
 
                    log.info("Verbose mode: On")
 
712
                    self.log_level = 0
 
713
                    self.log_message("Verbose mode: On")
711
714
            # elif (key == "end" or key == "meta >" or key == "G"
712
715
            #       or key == ">"):
713
716
            #     pass            # xxx end-of-buffer
732
735
        return True
733
736
 
734
737
 
735
 
class UILogHandler(logging.Handler):
736
 
    def __init__(self, ui, *args, **kwargs):
737
 
        self.ui = ui
738
 
        super(UILogHandler, self).__init__(*args, **kwargs)
739
 
        self.setFormatter(
740
 
            logging.Formatter("%(asctime)s: %(message)s"))
741
 
    def emit(self, record):
742
 
        msg = self.format(record)
743
 
        if record.levelno > logging.INFO:
744
 
            msg = ("bold", msg)
745
 
        self.ui.add_log_line(msg)
746
 
 
747
 
 
748
738
ui = UserInterface()
749
739
try:
750
740
    ui.run()
751
741
except KeyboardInterrupt:
752
 
    with warnings.catch_warnings():
753
 
        warnings.filterwarnings("ignore", "", BytesWarning)
754
 
        ui.screen.stop()
755
 
except Exception:
756
 
    with warnings.catch_warnings():
757
 
        warnings.filterwarnings("ignore", "", BytesWarning)
758
 
        ui.screen.stop()
 
742
    ui.screen.stop()
 
743
except Exception as e:
 
744
    ui.log_message(str(e))
 
745
    ui.screen.stop()
759
746
    raise