/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-03-17 16:54:02 UTC
  • Revision ID: teddy@recompile.se-20190317165402-g3ljjeea05p5ouvt
mandos-ctl: Refactor tests

* mandos-ctl (TestBaseCommands.test_IsEnabled): Removed; don't test
                                                internal details of
                                                command.IsEnabled.
  (TestBaseCommands.test_IsEnabled_run_exits_successfully): Rename to
                                  "test_IsEnabled_exits_successfully".
  (TestBaseCommands.test_IsEnabled_run_exits_with_failure): Rename to
                                  "test_IsEnabled_exits_with_failure".

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.3"
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