/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: 2017-08-20 13:50:57 UTC
  • Revision ID: teddy@recompile.se-20170820135057-u069yh7hhk2ha1a2
Don't import doctest module in mandos-ctl unless running tests.

* mandos-ctl: Don't import doctest module unless running tests.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/python3 -bbI
 
1
#!/usr/bin/python
2
2
# -*- mode: python; coding: utf-8 -*-
3
3
#
4
4
# Mandos Monitor - Control and monitor the Mandos server
5
5
#
6
 
# Copyright © 2009-2019 Teddy Hogeborn
7
 
# Copyright © 2009-2019 Björn Påhlsson
8
 
#
9
 
# This file is part of Mandos.
10
 
#
11
 
# Mandos is free software: you can redistribute it and/or modify it
12
 
# under the terms of the GNU General Public License as published by
 
6
# Copyright © 2009-2017 Teddy Hogeborn
 
7
# Copyright © 2009-2017 Björn Påhlsson
 
8
#
 
9
# This program is free software: you can redistribute it and/or modify
 
10
# it under the terms of the GNU General Public License as published by
13
11
# the Free Software Foundation, either version 3 of the License, or
14
12
# (at your option) any later version.
15
13
#
16
 
#     Mandos is distributed in the hope that it will be useful, but
17
 
#     WITHOUT ANY WARRANTY; without even the implied warranty of
 
14
#     This program is distributed in the hope that it will be useful,
 
15
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
18
16
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
17
#     GNU General Public License for more details.
20
18
#
21
19
# You should have received a copy of the GNU General Public License
22
 
# along with Mandos.  If not, see <http://www.gnu.org/licenses/>.
 
20
# along with this program.  If not, see
 
21
# <http://www.gnu.org/licenses/>.
23
22
#
24
23
# Contact the authors at <mandos@recompile.se>.
25
24
#
33
32
 
34
33
import sys
35
34
import os
36
 
import warnings
 
35
 
37
36
import datetime
38
 
import locale
39
 
import logging
40
37
 
41
38
import urwid.curses_display
42
39
import urwid
46
43
 
47
44
import dbus
48
45
 
 
46
import locale
 
47
 
 
48
import logging
 
49
 
49
50
if sys.version_info.major == 2:
50
51
    str = unicode
51
52
 
52
 
log = logging.getLogger(os.path.basename(sys.argv[0]))
53
 
logging.basicConfig(level="NOTSET", # Show all messages
54
 
                    format="%(message)s") # Show basic log messages
55
 
 
56
 
logging.captureWarnings(True)   # Show warnings via the logging system
57
 
 
58
 
locale.setlocale(locale.LC_ALL, "")
59
 
 
60
 
logging.getLogger("dbus.proxies").setLevel(logging.CRITICAL)
 
53
locale.setlocale(locale.LC_ALL, '')
 
54
 
 
55
logging.getLogger('dbus.proxies').setLevel(logging.CRITICAL)
61
56
 
62
57
# Some useful constants
63
 
domain = "se.recompile"
64
 
server_interface = domain + ".Mandos"
65
 
client_interface = domain + ".Mandos.Client"
66
 
version = "1.8.9"
 
58
domain = 'se.recompile'
 
59
server_interface = domain + '.Mandos'
 
60
client_interface = domain + '.Mandos.Client'
 
61
version = "1.7.15"
67
62
 
68
63
try:
69
64
    dbus.OBJECT_MANAGER_IFACE
126
121
    """
127
122
 
128
123
    def __init__(self, server_proxy_object=None, update_hook=None,
129
 
                 delete_hook=None, **kwargs):
 
124
                 delete_hook=None, logger=None, **kwargs):
130
125
        # Called on update
131
126
        self.update_hook = update_hook
132
127
        # Called on delete
133
128
        self.delete_hook = delete_hook
134
129
        # Mandos Server proxy object
135
130
        self.server_proxy_object = server_proxy_object
 
131
        # Logger
 
132
        self.logger = logger
136
133
 
137
134
        self._update_timer_callback_tag = None
138
135
 
165
162
                                         self.rejected,
166
163
                                         client_interface,
167
164
                                         byte_arrays=True))
168
 
        log.debug("Created client %s", self.properties["Name"])
 
165
        self.logger('Created client {}'
 
166
                    .format(self.properties["Name"]), level=0)
169
167
 
170
168
    def using_timer(self, flag):
171
169
        """Call this method with True or False when timer should be
173
171
        """
174
172
        if flag and self._update_timer_callback_tag is None:
175
173
            # Will update the shown timer value every second
176
 
            self._update_timer_callback_tag = (
177
 
                GLib.timeout_add(1000,
178
 
                                 glib_safely(self.update_timer)))
 
174
            self._update_timer_callback_tag = (GLib.timeout_add
 
175
                                               (1000,
 
176
                                                self.update_timer))
179
177
        elif not (flag or self._update_timer_callback_tag is None):
180
178
            GLib.source_remove(self._update_timer_callback_tag)
181
179
            self._update_timer_callback_tag = None
182
180
 
183
181
    def checker_completed(self, exitstatus, condition, command):
184
182
        if exitstatus == 0:
185
 
            log.debug('Checker for client %s (command "%s")'
186
 
                      " succeeded", self.properties["Name"], command)
 
183
            self.logger('Checker for client {} (command "{}")'
 
184
                        ' succeeded'.format(self.properties["Name"],
 
185
                                            command), level=0)
187
186
            self.update()
188
187
            return
189
188
        # Checker failed
190
189
        if os.WIFEXITED(condition):
191
 
            log.info('Checker for client %s (command "%s") failed'
192
 
                     " with exit code %d", self.properties["Name"],
193
 
                     command, os.WEXITSTATUS(condition))
 
190
            self.logger('Checker for client {} (command "{}") failed'
 
191
                        ' with exit code {}'
 
192
                        .format(self.properties["Name"], command,
 
193
                                os.WEXITSTATUS(condition)))
194
194
        elif os.WIFSIGNALED(condition):
195
 
            log.info('Checker for client %s (command "%s") was'
196
 
                     " killed by signal %d", self.properties["Name"],
197
 
                     command, os.WTERMSIG(condition))
 
195
            self.logger('Checker for client {} (command "{}") was'
 
196
                        ' killed by signal {}'
 
197
                        .format(self.properties["Name"], command,
 
198
                                os.WTERMSIG(condition)))
198
199
        self.update()
199
200
 
200
201
    def checker_started(self, command):
201
202
        """Server signals that a checker started."""
202
 
        log.debug('Client %s started checker "%s"',
203
 
                  self.properties["Name"], command)
 
203
        self.logger('Client {} started checker "{}"'
 
204
                    .format(self.properties["Name"],
 
205
                            command), level=0)
204
206
 
205
207
    def got_secret(self):
206
 
        log.info("Client %s received its secret",
207
 
                 self.properties["Name"])
 
208
        self.logger('Client {} received its secret'
 
209
                    .format(self.properties["Name"]))
208
210
 
209
211
    def need_approval(self, timeout, default):
210
212
        if not default:
211
 
            message = "Client %s needs approval within %f seconds"
 
213
            message = 'Client {} needs approval within {} seconds'
212
214
        else:
213
 
            message = "Client %s will get its secret in %f seconds"
214
 
        log.info(message, self.properties["Name"], timeout/1000)
 
215
            message = 'Client {} will get its secret in {} seconds'
 
216
        self.logger(message.format(self.properties["Name"],
 
217
                                   timeout/1000))
215
218
 
216
219
    def rejected(self, reason):
217
 
        log.info("Client %s was rejected; reason: %s",
218
 
                 self.properties["Name"], reason)
 
220
        self.logger('Client {} was rejected; reason: {}'
 
221
                    .format(self.properties["Name"], reason))
219
222
 
220
223
    def selectable(self):
221
224
        """Make this a "selectable" widget.
247
250
        # Rebuild focus and non-focus widgets using current properties
248
251
 
249
252
        # Base part of a client. Name!
250
 
        base = "{name}: ".format(name=self.properties["Name"])
 
253
        base = '{name}: '.format(name=self.properties["Name"])
251
254
        if not self.properties["Enabled"]:
252
255
            message = "DISABLED"
253
256
            self.using_timer(False)
275
278
                timer = datetime.timedelta(0)
276
279
            else:
277
280
                expires = (datetime.datetime.strptime
278
 
                           (expires, "%Y-%m-%dT%H:%M:%S.%f"))
 
281
                           (expires, '%Y-%m-%dT%H:%M:%S.%f'))
279
282
                timer = max(expires - datetime.datetime.utcnow(),
280
283
                            datetime.timedelta())
281
 
            message = ("A checker has failed! Time until client"
282
 
                       " gets disabled: {}"
 
284
            message = ('A checker has failed! Time until client'
 
285
                       ' gets disabled: {}'
283
286
                       .format(str(timer).rsplit(".", 1)[0]))
284
287
            self.using_timer(True)
285
288
        else:
383
386
            self.update()
384
387
 
385
388
 
386
 
def glib_safely(func, retval=True):
387
 
    def safe_func(*args, **kwargs):
388
 
        try:
389
 
            return func(*args, **kwargs)
390
 
        except Exception:
391
 
            log.exception("")
392
 
            return retval
393
 
    return safe_func
394
 
 
395
 
 
396
389
class ConstrainedListBox(urwid.ListBox):
397
390
    """Like a normal urwid.ListBox, but will consume all "up" or
398
391
    "down" key presses, thus not allowing any containing widgets to
410
403
    """This is the entire user interface - the whole screen
411
404
    with boxes, lists of client widgets, etc.
412
405
    """
413
 
    def __init__(self, max_log_length=1000):
 
406
    def __init__(self, max_log_length=1000, log_level=1):
414
407
        DBusGMainLoop(set_as_default=True)
415
408
 
416
409
        self.screen = urwid.curses_display.Screen()
450
443
        self.clients_dict = {}
451
444
 
452
445
        # We will add Text widgets to this list
453
 
        self.log = urwid.SimpleListWalker([])
 
446
        self.log = []
454
447
        self.max_log_length = max_log_length
455
448
 
 
449
        self.log_level = log_level
 
450
 
456
451
        # We keep a reference to the log widget so we can remove it
457
452
        # from the ListWalker without it getting destroyed
458
453
        self.logbox = ConstrainedListBox(self.log)
462
457
        self.log_visible = True
463
458
        self.log_wrap = "any"
464
459
 
465
 
        self.loghandler = UILogHandler(self)
466
 
 
467
460
        self.rebuild()
468
 
        self.add_log_line(("bold",
469
 
                           "Mandos Monitor version " + version))
470
 
        self.add_log_line(("bold", "q: Quit  ?: Help"))
 
461
        self.log_message_raw(("bold",
 
462
                              "Mandos Monitor version " + version))
 
463
        self.log_message_raw(("bold",
 
464
                              "q: Quit  ?: Help"))
471
465
 
472
 
        self.busname = domain + ".Mandos"
 
466
        self.busname = domain + '.Mandos'
473
467
        self.main_loop = GLib.MainLoop()
474
468
 
475
 
    def client_not_found(self, key_id, address):
476
 
        log.info("Client with address %s and key ID %s could"
477
 
                 " not be found", address, key_id)
 
469
    def client_not_found(self, fingerprint, address):
 
470
        self.log_message("Client with address {} and fingerprint {}"
 
471
                         " could not be found"
 
472
                         .format(address, fingerprint))
478
473
 
479
474
    def rebuild(self):
480
475
        """This rebuilds the User Interface.
491
486
            self.uilist.append(self.logbox)
492
487
        self.topwidget = urwid.Pile(self.uilist)
493
488
 
494
 
    def add_log_line(self, markup):
 
489
    def log_message(self, message, level=1):
 
490
        """Log message formatted with timestamp"""
 
491
        if level < self.log_level:
 
492
            return
 
493
        timestamp = datetime.datetime.now().isoformat()
 
494
        self.log_message_raw("{}: {}".format(timestamp, message),
 
495
                             level=level)
 
496
 
 
497
    def log_message_raw(self, markup, level=1):
 
498
        """Add a log message to the log buffer."""
 
499
        if level < self.log_level:
 
500
            return
495
501
        self.log.append(urwid.Text(markup, wrap=self.log_wrap))
496
502
        if self.max_log_length:
497
503
            if len(self.log) > self.max_log_length:
498
 
                del self.log[0:(len(self.log) - self.max_log_length)]
499
 
        self.logbox.set_focus(len(self.logbox.body.contents)-1,
 
504
                del self.log[0:len(self.log)-self.max_log_length-1]
 
505
        self.logbox.set_focus(len(self.logbox.body.contents),
500
506
                              coming_from="above")
501
507
        self.refresh()
502
508
 
504
510
        """Toggle visibility of the log buffer."""
505
511
        self.log_visible = not self.log_visible
506
512
        self.rebuild()
507
 
        log.debug("Log visibility changed to: %s", self.log_visible)
 
513
        self.log_message("Log visibility changed to: {}"
 
514
                         .format(self.log_visible), level=0)
508
515
 
509
516
    def change_log_display(self):
510
517
        """Change type of log display.
515
522
            self.log_wrap = "clip"
516
523
        for textwidget in self.log:
517
524
            textwidget.set_wrap_mode(self.log_wrap)
518
 
        log.debug("Wrap mode: %s", self.log_wrap)
 
525
        self.log_message("Wrap mode: {}".format(self.log_wrap),
 
526
                         level=0)
519
527
 
520
528
    def find_and_remove_client(self, path, interfaces):
521
529
        """Find a client by its object path and remove it.
529
537
            client = self.clients_dict[path]
530
538
        except KeyError:
531
539
            # not found?
532
 
            log.warning("Unknown client %s removed", path)
 
540
            self.log_message("Unknown client {!r} removed"
 
541
                             .format(path))
533
542
            return
534
543
        client.delete()
535
544
 
548
557
            proxy_object=client_proxy_object,
549
558
            update_hook=self.refresh,
550
559
            delete_hook=self.remove_client,
 
560
            logger=self.log_message,
551
561
            properties=dict(ifs_and_props[client_interface])),
552
562
                        path=path)
553
563
 
573
583
 
574
584
    def run(self):
575
585
        """Start the main loop and exit when it's done."""
576
 
        log.addHandler(self.loghandler)
577
 
        self.orig_log_propagate = log.propagate
578
 
        log.propagate = False
579
 
        self.orig_log_level = log.level
580
 
        log.setLevel("INFO")
581
586
        self.bus = dbus.SystemBus()
582
587
        mandos_dbus_objc = self.bus.get_object(
583
588
            self.busname, "/", follow_name_owner_changes=True)
587
592
            mandos_clients = (self.mandos_serv
588
593
                              .GetAllClientsWithProperties())
589
594
            if not mandos_clients:
590
 
                log.warning("Note: Server has no clients.")
 
595
                self.log_message_raw(("bold",
 
596
                                      "Note: Server has no clients."))
591
597
        except dbus.exceptions.DBusException:
592
 
            log.warning("Note: No Mandos server running.")
 
598
            self.log_message_raw(("bold",
 
599
                                  "Note: No Mandos server running."))
593
600
            mandos_clients = dbus.Dictionary()
594
601
 
595
602
        (self.mandos_serv
615
622
                proxy_object=client_proxy_object,
616
623
                properties=client,
617
624
                update_hook=self.refresh,
618
 
                delete_hook=self.remove_client),
 
625
                delete_hook=self.remove_client,
 
626
                logger=self.log_message),
619
627
                            path=path)
620
628
 
621
629
        self.refresh()
622
 
        self._input_callback_tag = (
623
 
            GLib.io_add_watch(
624
 
                GLib.IOChannel.unix_new(sys.stdin.fileno()),
625
 
                GLib.PRIORITY_DEFAULT, GLib.IO_IN,
626
 
                glib_safely(self.process_input)))
 
630
        self._input_callback_tag = (GLib.io_add_watch
 
631
                                    (sys.stdin.fileno(),
 
632
                                     GLib.IO_IN,
 
633
                                     self.process_input))
627
634
        self.main_loop.run()
628
635
        # Main loop has finished, we should close everything now
629
636
        GLib.source_remove(self._input_callback_tag)
630
 
        with warnings.catch_warnings():
631
 
            warnings.simplefilter("ignore", BytesWarning)
632
 
            self.screen.stop()
 
637
        self.screen.stop()
633
638
 
634
639
    def stop(self):
635
640
        self.main_loop.quit()
636
 
        log.removeHandler(self.loghandler)
637
 
        log.propagate = self.orig_log_propagate
638
641
 
639
642
    def process_input(self, source, condition):
640
643
        keys = self.screen.get_input()
673
676
                if not self.log_visible:
674
677
                    self.log_visible = True
675
678
                    self.rebuild()
676
 
                self.add_log_line(("bold",
677
 
                                   "  ".join(("q: Quit",
678
 
                                              "?: Help",
679
 
                                              "l: Log window toggle",
680
 
                                              "TAB: Switch window",
681
 
                                              "w: Wrap (log lines)",
682
 
                                              "v: Toggle verbose log",
683
 
                                   ))))
684
 
                self.add_log_line(("bold",
685
 
                                   "  ".join(("Clients:",
686
 
                                              "+: Enable",
687
 
                                              "-: Disable",
688
 
                                              "R: Remove",
689
 
                                              "s: Start new checker",
690
 
                                              "S: Stop checker",
691
 
                                              "C: Checker OK",
692
 
                                              "a: Approve",
693
 
                                              "d: Deny",
694
 
                                   ))))
 
679
                self.log_message_raw(("bold",
 
680
                                      "  ".
 
681
                                      join(("q: Quit",
 
682
                                            "?: Help",
 
683
                                            "l: Log window toggle",
 
684
                                            "TAB: Switch window",
 
685
                                            "w: Wrap (log lines)",
 
686
                                            "v: Toggle verbose log",
 
687
                                            ))))
 
688
                self.log_message_raw(("bold",
 
689
                                      "  "
 
690
                                      .join(("Clients:",
 
691
                                             "+: Enable",
 
692
                                             "-: Disable",
 
693
                                             "R: Remove",
 
694
                                             "s: Start new checker",
 
695
                                             "S: Stop checker",
 
696
                                             "C: Checker OK",
 
697
                                             "a: Approve",
 
698
                                             "d: Deny"))))
695
699
                self.refresh()
696
700
            elif key == "tab":
697
701
                if self.topwidget.get_focus() is self.logbox:
700
704
                    self.topwidget.set_focus(self.logbox)
701
705
                self.refresh()
702
706
            elif key == "v":
703
 
                if log.level < logging.INFO:
704
 
                    log.setLevel(logging.INFO)
705
 
                    log.info("Verbose mode: Off")
 
707
                if self.log_level == 0:
 
708
                    self.log_level = 1
 
709
                    self.log_message("Verbose mode: Off")
706
710
                else:
707
 
                    log.setLevel(logging.NOTSET)
708
 
                    log.info("Verbose mode: On")
 
711
                    self.log_level = 0
 
712
                    self.log_message("Verbose mode: On")
709
713
            # elif (key == "end" or key == "meta >" or key == "G"
710
714
            #       or key == ">"):
711
715
            #     pass            # xxx end-of-buffer
730
734
        return True
731
735
 
732
736
 
733
 
class UILogHandler(logging.Handler):
734
 
    def __init__(self, ui, *args, **kwargs):
735
 
        self.ui = ui
736
 
        super(UILogHandler, self).__init__(*args, **kwargs)
737
 
        self.setFormatter(
738
 
            logging.Formatter("%(asctime)s: %(message)s"))
739
 
    def emit(self, record):
740
 
        msg = self.format(record)
741
 
        if record.levelno > logging.INFO:
742
 
            msg = ("bold", msg)
743
 
        self.ui.add_log_line(msg)
744
 
 
745
 
 
746
737
ui = UserInterface()
747
738
try:
748
739
    ui.run()
749
740
except KeyboardInterrupt:
750
 
    with warnings.catch_warnings():
751
 
        warnings.filterwarnings("ignore", "", BytesWarning)
752
 
        ui.screen.stop()
753
 
except Exception:
754
 
    with warnings.catch_warnings():
755
 
        warnings.filterwarnings("ignore", "", BytesWarning)
756
 
        ui.screen.stop()
 
741
    ui.screen.stop()
 
742
except Exception as e:
 
743
    ui.log_message(str(e))
 
744
    ui.screen.stop()
757
745
    raise