/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-02-23 20:35:20 UTC
  • mto: This revision was merged to the branch mainline in revision 900.
  • Revision ID: teddy@recompile.se-20170223203520-7ti8vo1m86ib192m
Tags: version-1.7.15-1
* Makefile (version): Change to 1.7.15.
* NEWS (Version 1.7.15): Add new entry.
* debian/changelog (1.7.15-1): - '' -

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
#
 
25
 
26
26
from __future__ import (division, absolute_import, print_function,
27
27
                        unicode_literals)
28
 
 
29
28
try:
30
29
    from future_builtins import *
31
30
except ImportError:
32
31
    pass
33
32
 
34
33
import sys
35
 
import logging
36
34
import os
37
 
import warnings
 
35
 
38
36
import datetime
39
 
import locale
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
 
    __metaclass__ = type
51
51
    str = unicode
52
 
    input = raw_input
53
 
 
54
 
# Show warnings by default
55
 
if not sys.warnoptions:
56
 
    warnings.simplefilter("default")
57
 
 
58
 
log = logging.getLogger(os.path.basename(sys.argv[0]))
59
 
logging.basicConfig(level="NOTSET", # Show all messages
60
 
                    format="%(message)s") # Show basic log messages
61
 
 
62
 
logging.captureWarnings(True)   # Show warnings via the logging system
63
 
 
64
 
locale.setlocale(locale.LC_ALL, "")
65
 
 
66
 
logging.getLogger("dbus.proxies").setLevel(logging.CRITICAL)
67
 
logging.getLogger("urwid").setLevel(logging.INFO)
 
52
 
 
53
locale.setlocale(locale.LC_ALL, '')
 
54
 
 
55
logging.getLogger('dbus.proxies').setLevel(logging.CRITICAL)
68
56
 
69
57
# Some useful constants
70
 
domain = "se.recompile"
71
 
server_interface = domain + ".Mandos"
72
 
client_interface = domain + ".Mandos.Client"
73
 
version = "1.8.17"
 
58
domain = 'se.recompile'
 
59
server_interface = domain + '.Mandos'
 
60
client_interface = domain + '.Mandos.Client'
 
61
version = "1.7.15"
74
62
 
75
63
try:
76
64
    dbus.OBJECT_MANAGER_IFACE
95
83
                             int(fraction*1000000))  # Microseconds
96
84
 
97
85
 
98
 
class MandosClientPropertyCache:
 
86
class MandosClientPropertyCache(object):
99
87
    """This wraps a Mandos Client D-Bus proxy object, caches the
100
88
    properties and calls a hook function when any of them are
101
89
    changed.
128
116
        self.property_changed_match.remove()
129
117
 
130
118
 
131
 
class MandosClientWidget(MandosClientPropertyCache, urwid.Widget):
 
119
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
132
120
    """A Mandos Client which is visible on the screen.
133
121
    """
134
122
 
135
 
    _sizing = frozenset(["flow"])
136
 
 
137
123
    def __init__(self, server_proxy_object=None, update_hook=None,
138
 
                 delete_hook=None, **kwargs):
 
124
                 delete_hook=None, logger=None, **kwargs):
139
125
        # Called on update
140
126
        self.update_hook = update_hook
141
127
        # Called on delete
142
128
        self.delete_hook = delete_hook
143
129
        # Mandos Server proxy object
144
130
        self.server_proxy_object = server_proxy_object
 
131
        # Logger
 
132
        self.logger = logger
145
133
 
146
134
        self._update_timer_callback_tag = None
147
135
 
174
162
                                         self.rejected,
175
163
                                         client_interface,
176
164
                                         byte_arrays=True))
177
 
        log.debug("Created client %s", self.properties["Name"])
 
165
        self.logger('Created client {}'
 
166
                    .format(self.properties["Name"]), level=0)
178
167
 
179
168
    def using_timer(self, flag):
180
169
        """Call this method with True or False when timer should be
182
171
        """
183
172
        if flag and self._update_timer_callback_tag is None:
184
173
            # Will update the shown timer value every second
185
 
            self._update_timer_callback_tag = (
186
 
                GLib.timeout_add(1000,
187
 
                                 glib_safely(self.update_timer)))
 
174
            self._update_timer_callback_tag = (GLib.timeout_add
 
175
                                               (1000,
 
176
                                                self.update_timer))
188
177
        elif not (flag or self._update_timer_callback_tag is None):
189
178
            GLib.source_remove(self._update_timer_callback_tag)
190
179
            self._update_timer_callback_tag = None
191
180
 
192
181
    def checker_completed(self, exitstatus, condition, command):
193
182
        if exitstatus == 0:
194
 
            log.debug('Checker for client %s (command "%s")'
195
 
                      " succeeded", self.properties["Name"], command)
 
183
            self.logger('Checker for client {} (command "{}")'
 
184
                        ' succeeded'.format(self.properties["Name"],
 
185
                                            command), level=0)
196
186
            self.update()
197
187
            return
198
188
        # Checker failed
199
189
        if os.WIFEXITED(condition):
200
 
            log.info('Checker for client %s (command "%s") failed'
201
 
                     " with exit code %d", self.properties["Name"],
202
 
                     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)))
203
194
        elif os.WIFSIGNALED(condition):
204
 
            log.info('Checker for client %s (command "%s") was'
205
 
                     " killed by signal %d", self.properties["Name"],
206
 
                     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)))
207
199
        self.update()
208
200
 
209
201
    def checker_started(self, command):
210
202
        """Server signals that a checker started."""
211
 
        log.debug('Client %s started checker "%s"',
212
 
                  self.properties["Name"], command)
 
203
        self.logger('Client {} started checker "{}"'
 
204
                    .format(self.properties["Name"],
 
205
                            command), level=0)
213
206
 
214
207
    def got_secret(self):
215
 
        log.info("Client %s received its secret",
216
 
                 self.properties["Name"])
 
208
        self.logger('Client {} received its secret'
 
209
                    .format(self.properties["Name"]))
217
210
 
218
211
    def need_approval(self, timeout, default):
219
212
        if not default:
220
 
            message = "Client %s needs approval within %f seconds"
 
213
            message = 'Client {} needs approval within {} seconds'
221
214
        else:
222
 
            message = "Client %s will get its secret in %f seconds"
223
 
        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))
224
218
 
225
219
    def rejected(self, reason):
226
 
        log.info("Client %s was rejected; reason: %s",
227
 
                 self.properties["Name"], reason)
 
220
        self.logger('Client {} was rejected; reason: {}'
 
221
                    .format(self.properties["Name"], reason))
228
222
 
229
223
    def selectable(self):
230
224
        """Make this a "selectable" widget.
231
 
        This overrides the method from urwid.Widget."""
 
225
        This overrides the method from urwid.FlowWidget."""
232
226
        return True
233
227
 
234
228
    def rows(self, maxcolrow, focus=False):
235
229
        """How many rows this widget will occupy might depend on
236
230
        whether we have focus or not.
237
 
        This overrides the method from urwid.Widget"""
 
231
        This overrides the method from urwid.FlowWidget"""
238
232
        return self.current_widget(focus).rows(maxcolrow, focus=focus)
239
233
 
240
234
    def current_widget(self, focus=False):
256
250
        # Rebuild focus and non-focus widgets using current properties
257
251
 
258
252
        # Base part of a client. Name!
259
 
        base = "{name}: ".format(name=self.properties["Name"])
 
253
        base = '{name}: '.format(name=self.properties["Name"])
260
254
        if not self.properties["Enabled"]:
261
255
            message = "DISABLED"
262
256
            self.using_timer(False)
284
278
                timer = datetime.timedelta(0)
285
279
            else:
286
280
                expires = (datetime.datetime.strptime
287
 
                           (expires, "%Y-%m-%dT%H:%M:%S.%f"))
 
281
                           (expires, '%Y-%m-%dT%H:%M:%S.%f'))
288
282
                timer = max(expires - datetime.datetime.utcnow(),
289
283
                            datetime.timedelta())
290
 
            message = ("A checker has failed! Time until client"
291
 
                       " gets disabled: {}"
 
284
            message = ('A checker has failed! Time until client'
 
285
                       ' gets disabled: {}'
292
286
                       .format(str(timer).rsplit(".", 1)[0]))
293
287
            self.using_timer(True)
294
288
        else:
332
326
 
333
327
    def render(self, maxcolrow, focus=False):
334
328
        """Render differently if we have focus.
335
 
        This overrides the method from urwid.Widget"""
 
329
        This overrides the method from urwid.FlowWidget"""
336
330
        return self.current_widget(focus).render(maxcolrow,
337
331
                                                 focus=focus)
338
332
 
339
333
    def keypress(self, maxcolrow, key):
340
334
        """Handle keys.
341
 
        This overrides the method from urwid.Widget"""
 
335
        This overrides the method from urwid.FlowWidget"""
342
336
        if key == "+":
343
337
            self.proxy.Set(client_interface, "Enabled",
344
338
                           dbus.Boolean(True), ignore_reply=True,
392
386
            self.update()
393
387
 
394
388
 
395
 
def glib_safely(func, retval=True):
396
 
    def safe_func(*args, **kwargs):
397
 
        try:
398
 
            return func(*args, **kwargs)
399
 
        except Exception:
400
 
            log.exception("")
401
 
            return retval
402
 
    return safe_func
403
 
 
404
 
 
405
389
class ConstrainedListBox(urwid.ListBox):
406
390
    """Like a normal urwid.ListBox, but will consume all "up" or
407
391
    "down" key presses, thus not allowing any containing widgets to
415
399
        return ret
416
400
 
417
401
 
418
 
class UserInterface:
 
402
class UserInterface(object):
419
403
    """This is the entire user interface - the whole screen
420
404
    with boxes, lists of client widgets, etc.
421
405
    """
422
 
    def __init__(self, max_log_length=1000):
 
406
    def __init__(self, max_log_length=1000, log_level=1):
423
407
        DBusGMainLoop(set_as_default=True)
424
408
 
425
409
        self.screen = urwid.curses_display.Screen()
459
443
        self.clients_dict = {}
460
444
 
461
445
        # We will add Text widgets to this list
462
 
        self.log = urwid.SimpleListWalker([])
 
446
        self.log = []
463
447
        self.max_log_length = max_log_length
464
448
 
 
449
        self.log_level = log_level
 
450
 
465
451
        # We keep a reference to the log widget so we can remove it
466
452
        # from the ListWalker without it getting destroyed
467
453
        self.logbox = ConstrainedListBox(self.log)
471
457
        self.log_visible = True
472
458
        self.log_wrap = "any"
473
459
 
474
 
        self.loghandler = UILogHandler(self)
475
 
 
476
460
        self.rebuild()
477
 
        self.add_log_line(("bold",
478
 
                           "Mandos Monitor version " + version))
479
 
        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"))
480
465
 
481
 
        self.busname = domain + ".Mandos"
 
466
        self.busname = domain + '.Mandos'
482
467
        self.main_loop = GLib.MainLoop()
483
468
 
484
 
    def client_not_found(self, key_id, address):
485
 
        log.info("Client with address %s and key ID %s could"
486
 
                 " 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))
487
473
 
488
474
    def rebuild(self):
489
475
        """This rebuilds the User Interface.
500
486
            self.uilist.append(self.logbox)
501
487
        self.topwidget = urwid.Pile(self.uilist)
502
488
 
503
 
    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
504
501
        self.log.append(urwid.Text(markup, wrap=self.log_wrap))
505
502
        if self.max_log_length:
506
503
            if len(self.log) > self.max_log_length:
507
 
                del self.log[0:(len(self.log) - self.max_log_length)]
508
 
        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),
509
506
                              coming_from="above")
510
507
        self.refresh()
511
508
 
513
510
        """Toggle visibility of the log buffer."""
514
511
        self.log_visible = not self.log_visible
515
512
        self.rebuild()
516
 
        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)
517
515
 
518
516
    def change_log_display(self):
519
517
        """Change type of log display.
524
522
            self.log_wrap = "clip"
525
523
        for textwidget in self.log:
526
524
            textwidget.set_wrap_mode(self.log_wrap)
527
 
        log.debug("Wrap mode: %s", self.log_wrap)
 
525
        self.log_message("Wrap mode: {}".format(self.log_wrap),
 
526
                         level=0)
528
527
 
529
528
    def find_and_remove_client(self, path, interfaces):
530
529
        """Find a client by its object path and remove it.
538
537
            client = self.clients_dict[path]
539
538
        except KeyError:
540
539
            # not found?
541
 
            log.warning("Unknown client %s removed", path)
 
540
            self.log_message("Unknown client {!r} removed"
 
541
                             .format(path))
542
542
            return
543
543
        client.delete()
544
544
 
557
557
            proxy_object=client_proxy_object,
558
558
            update_hook=self.refresh,
559
559
            delete_hook=self.remove_client,
 
560
            logger=self.log_message,
560
561
            properties=dict(ifs_and_props[client_interface])),
561
562
                        path=path)
562
563
 
582
583
 
583
584
    def run(self):
584
585
        """Start the main loop and exit when it's done."""
585
 
        log.addHandler(self.loghandler)
586
 
        self.orig_log_propagate = log.propagate
587
 
        log.propagate = False
588
 
        self.orig_log_level = log.level
589
 
        log.setLevel("INFO")
590
586
        self.bus = dbus.SystemBus()
591
587
        mandos_dbus_objc = self.bus.get_object(
592
588
            self.busname, "/", follow_name_owner_changes=True)
596
592
            mandos_clients = (self.mandos_serv
597
593
                              .GetAllClientsWithProperties())
598
594
            if not mandos_clients:
599
 
                log.warning("Note: Server has no clients.")
 
595
                self.log_message_raw(("bold",
 
596
                                      "Note: Server has no clients."))
600
597
        except dbus.exceptions.DBusException:
601
 
            log.warning("Note: No Mandos server running.")
 
598
            self.log_message_raw(("bold",
 
599
                                  "Note: No Mandos server running."))
602
600
            mandos_clients = dbus.Dictionary()
603
601
 
604
602
        (self.mandos_serv
624
622
                proxy_object=client_proxy_object,
625
623
                properties=client,
626
624
                update_hook=self.refresh,
627
 
                delete_hook=self.remove_client),
 
625
                delete_hook=self.remove_client,
 
626
                logger=self.log_message),
628
627
                            path=path)
629
628
 
630
629
        self.refresh()
631
 
        self._input_callback_tag = (
632
 
            GLib.io_add_watch(
633
 
                GLib.IOChannel.unix_new(sys.stdin.fileno()),
634
 
                GLib.PRIORITY_DEFAULT, GLib.IO_IN,
635
 
                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))
636
634
        self.main_loop.run()
637
635
        # Main loop has finished, we should close everything now
638
636
        GLib.source_remove(self._input_callback_tag)
639
 
        with warnings.catch_warnings():
640
 
            warnings.simplefilter("ignore", BytesWarning)
641
 
            self.screen.stop()
 
637
        self.screen.stop()
642
638
 
643
639
    def stop(self):
644
640
        self.main_loop.quit()
645
 
        log.removeHandler(self.loghandler)
646
 
        log.propagate = self.orig_log_propagate
647
641
 
648
642
    def process_input(self, source, condition):
649
643
        keys = self.screen.get_input()
682
676
                if not self.log_visible:
683
677
                    self.log_visible = True
684
678
                    self.rebuild()
685
 
                self.add_log_line(("bold",
686
 
                                   "  ".join(("q: Quit",
687
 
                                              "?: Help",
688
 
                                              "l: Log window toggle",
689
 
                                              "TAB: Switch window",
690
 
                                              "w: Wrap (log lines)",
691
 
                                              "v: Toggle verbose log",
692
 
                                   ))))
693
 
                self.add_log_line(("bold",
694
 
                                   "  ".join(("Clients:",
695
 
                                              "+: Enable",
696
 
                                              "-: Disable",
697
 
                                              "R: Remove",
698
 
                                              "s: Start new checker",
699
 
                                              "S: Stop checker",
700
 
                                              "C: Checker OK",
701
 
                                              "a: Approve",
702
 
                                              "d: Deny",
703
 
                                   ))))
 
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"))))
704
699
                self.refresh()
705
700
            elif key == "tab":
706
701
                if self.topwidget.get_focus() is self.logbox:
709
704
                    self.topwidget.set_focus(self.logbox)
710
705
                self.refresh()
711
706
            elif key == "v":
712
 
                if log.level < logging.INFO:
713
 
                    log.setLevel(logging.INFO)
714
 
                    log.info("Verbose mode: Off")
 
707
                if self.log_level == 0:
 
708
                    self.log_level = 1
 
709
                    self.log_message("Verbose mode: Off")
715
710
                else:
716
 
                    log.setLevel(logging.NOTSET)
717
 
                    log.info("Verbose mode: On")
 
711
                    self.log_level = 0
 
712
                    self.log_message("Verbose mode: On")
718
713
            # elif (key == "end" or key == "meta >" or key == "G"
719
714
            #       or key == ">"):
720
715
            #     pass            # xxx end-of-buffer
739
734
        return True
740
735
 
741
736
 
742
 
class UILogHandler(logging.Handler):
743
 
    def __init__(self, ui, *args, **kwargs):
744
 
        self.ui = ui
745
 
        super(UILogHandler, self).__init__(*args, **kwargs)
746
 
        self.setFormatter(
747
 
            logging.Formatter("%(asctime)s: %(message)s"))
748
 
    def emit(self, record):
749
 
        msg = self.format(record)
750
 
        if record.levelno > logging.INFO:
751
 
            msg = ("bold", msg)
752
 
        self.ui.add_log_line(msg)
753
 
 
754
 
 
755
737
ui = UserInterface()
756
738
try:
757
739
    ui.run()
758
740
except KeyboardInterrupt:
759
 
    with warnings.catch_warnings():
760
 
        warnings.filterwarnings("ignore", "", BytesWarning)
761
 
        ui.screen.stop()
762
 
except Exception:
763
 
    with warnings.catch_warnings():
764
 
        warnings.filterwarnings("ignore", "", BytesWarning)
765
 
        ui.screen.stop()
 
741
    ui.screen.stop()
 
742
except Exception as e:
 
743
    ui.log_message(str(e))
 
744
    ui.screen.stop()
766
745
    raise