/mandos/release

To get this branch, use:
bzr branch http://bzr.recompile.se/loggerhead/mandos/release

« back to all changes in this revision

Viewing changes to mandos-monitor

* mandos-monitor: New prototype version of interactive server
                  administraton tool using D-Bus.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/python
 
2
# -*- mode: python; coding: utf-8 -*-
 
3
 
 
4
from __future__ import division, absolute_import, with_statement
 
5
 
 
6
import sys
 
7
import signal
 
8
 
 
9
import urwid.curses_display
 
10
import urwid
 
11
 
 
12
from dbus.mainloop.glib import DBusGMainLoop
 
13
import gobject
 
14
 
 
15
import dbus
 
16
 
 
17
import UserList
 
18
 
 
19
# Some useful constants
 
20
domain = 'se.bsnet.fukt'
 
21
server_interface = domain + '.Mandos'
 
22
client_interface = domain + '.Mandos.Client'
 
23
version = "1.0.14"
 
24
 
 
25
# Always run in monochrome mode
 
26
urwid.curses_display.curses.has_colors = lambda : False
 
27
 
 
28
# Urwid doesn't support blinking, but we want it.  Since we have no
 
29
# use for underline on its own, we make underline also always blink.
 
30
urwid.curses_display.curses.A_UNDERLINE |= (
 
31
    urwid.curses_display.curses.A_BLINK)
 
32
 
 
33
class MandosClientPropertyCache(object):
 
34
    """This wraps a Mandos Client D-Bus proxy object, caches the
 
35
    properties and calls a hook function when any of them are
 
36
    changed.
 
37
    """
 
38
    def __init__(self, proxy_object=None, properties=None, *args,
 
39
                 **kwargs):
 
40
        # Type conversion mapping
 
41
        self.type_map = {
 
42
            dbus.ObjectPath: unicode,
 
43
            dbus.ByteArray: str,
 
44
            dbus.Signature: unicode,
 
45
            dbus.Byte: chr,
 
46
            dbus.Int16: int,
 
47
            dbus.UInt16: int,
 
48
            dbus.Int32: int,
 
49
            dbus.UInt32: int,
 
50
            dbus.Int64: int,
 
51
            dbus.UInt64: int,
 
52
            dbus.Dictionary: dict,
 
53
            dbus.Array: list,
 
54
            dbus.String: unicode,
 
55
            dbus.Boolean: bool,
 
56
            dbus.Double: float,
 
57
            dbus.Struct: tuple,
 
58
            }
 
59
        self.proxy = proxy_object # Mandos Client proxy object
 
60
        
 
61
        if properties is None:
 
62
            self.properties = dict()
 
63
        else:
 
64
            self.properties = dict(self.convert_property(prop, val)
 
65
                                   for prop, val in
 
66
                                   properties.iteritems())
 
67
        self.proxy.connect_to_signal("PropertyChanged",
 
68
                                     self.property_changed,
 
69
                                     client_interface,
 
70
                                     byte_arrays=True)
 
71
        
 
72
        if properties is None:
 
73
            self.properties.update(
 
74
                self.convert_property(prop, val)
 
75
                for prop, val in
 
76
                self.proxy.GetAll(client_interface,
 
77
                                  dbus_interface =
 
78
                                  dbus.PROPERTIES_IFACE).iteritems())
 
79
        super(MandosClientPropertyCache, self).__init__(
 
80
            proxy_object=proxy_object,
 
81
            properties=properties, *args, **kwargs)
 
82
    
 
83
    def convert_property(self, property, value):
 
84
        """This converts the arguments from a D-Bus signal, which are
 
85
        D-Bus types, into normal Python types, using a conversion
 
86
        function from "self.type_map".
 
87
        """
 
88
        property_name = unicode(property) # Always a dbus.String
 
89
        if isinstance(value, dbus.UTF8String):
 
90
            # Should not happen, but prepare for it anyway
 
91
            value = dbus.String(str(value).decode("utf-8"))
 
92
        try:
 
93
            convfunc = self.type_map[type(value)]
 
94
        except KeyError:
 
95
            # Unknown type, return unmodified
 
96
            return property_name, value
 
97
        return property_name, convfunc(value)
 
98
    def property_changed(self, property=None, value=None):
 
99
        """This is called whenever we get a PropertyChanged signal
 
100
        It updates the changed property in the "properties" dict.
 
101
        """
 
102
        # Convert name and value
 
103
        property_name, cvalue = self.convert_property(property, value)
 
104
        # Update properties dict with new value
 
105
        self.properties[property_name] = cvalue
 
106
 
 
107
 
 
108
class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache):
 
109
    """A Mandos Client which is visible on the screen.
 
110
    """
 
111
    
 
112
    def __init__(self, server_proxy_object=None, update_hook=None,
 
113
                 delete_hook=None, *args, **kwargs):
 
114
        # Called on update
 
115
        self.update_hook = update_hook
 
116
        # Called on delete
 
117
        self.delete_hook = delete_hook
 
118
        # Mandos Server proxy object
 
119
        self.server_proxy_object = server_proxy_object
 
120
        
 
121
        # The widget shown normally
 
122
        self._text_widget = urwid.Text("")
 
123
        # The widget shown when we have focus
 
124
        self._focus_text_widget = urwid.Text("")
 
125
        super(MandosClientWidget, self).__init__(
 
126
            update_hook=update_hook, delete_hook=delete_hook,
 
127
            *args, **kwargs)
 
128
        self.update()
 
129
        self.opened = False
 
130
    
 
131
    def selectable(self):
 
132
        """Make this a "selectable" widget.
 
133
        This overrides the method from urwid.FlowWidget."""
 
134
        return True
 
135
    
 
136
    def rows(self, (maxcol,), focus=False):
 
137
        """How many rows this widget will occupy might depend on
 
138
        whether we have focus or not.
 
139
        This overrides the method from urwid.FlowWidget"""
 
140
        return self.current_widget(focus).rows((maxcol,), focus=focus)
 
141
    
 
142
    def current_widget(self, focus=False):
 
143
        if focus or self.opened:
 
144
            return self._focus_widget
 
145
        return self._widget
 
146
    
 
147
    def update(self):
 
148
        "Called when what is visible on the screen should be updated."
 
149
        # How to add standout mode to a style
 
150
        with_standout = { u"normal": u"standout",
 
151
                          u"bold": u"bold-standout",
 
152
                          u"underline-blink":
 
153
                              u"underline-blink-standout",
 
154
                          u"bold-underline-blink":
 
155
                              u"bold-underline-blink-standout",
 
156
                          }
 
157
        
 
158
        # Rebuild focus and non-focus widgets using current properties
 
159
        self._text = (u'name="%(name)s", enabled=%(enabled)s'
 
160
                      % self.properties)
 
161
        if not urwid.supports_unicode():
 
162
            self._text = self._text.encode("ascii", "replace")
 
163
        textlist = [(u"normal", u"BLÄRGH: "), (u"bold", self._text)]
 
164
        self._text_widget.set_text(textlist)
 
165
        self._focus_text_widget.set_text([(with_standout[text[0]],
 
166
                                           text[1])
 
167
                                          if isinstance(text, tuple)
 
168
                                          else text
 
169
                                          for text in textlist])
 
170
        self._widget = self._text_widget
 
171
        self._focus_widget = urwid.AttrWrap(self._focus_text_widget,
 
172
                                            "standout")
 
173
        # Run update hook, if any
 
174
        if self.update_hook is not None:
 
175
            self.update_hook()
 
176
    
 
177
    def delete(self):
 
178
        if self.delete_hook is not None:
 
179
            self.delete_hook(self)
 
180
    
 
181
    def render(self, (maxcol,), focus=False):
 
182
        """Render differently if we have focus.
 
183
        This overrides the method from urwid.FlowWidget"""
 
184
        return self.current_widget(focus).render((maxcol,),
 
185
                                                 focus=focus)
 
186
    
 
187
    def keypress(self, (maxcol,), key):
 
188
        """Handle keys.
 
189
        This overrides the method from urwid.FlowWidget"""
 
190
        if key == u"e" or key == u"+":
 
191
            self.proxy.Enable()
 
192
        elif key == u"d" or key == u"-":
 
193
            self.proxy.Disable()
 
194
        elif key == u"r" or key == u"_":
 
195
            self.server_proxy_object.RemoveClient(self.proxy
 
196
                                                  .object_path)
 
197
        elif key == u"s":
 
198
            self.proxy.StartChecker()
 
199
        elif key == u"c":
 
200
            self.proxy.StopChecker()
 
201
        elif key == u"S":
 
202
            self.proxy.CheckedOK()
 
203
        # xxx
 
204
#         elif key == u"p" or key == "=":
 
205
#             self.proxy.pause()
 
206
#         elif key == u"u" or key == ":":
 
207
#             self.proxy.unpause()
 
208
#         elif key == u"RET":
 
209
#             self.open()
 
210
        else:
 
211
            return key
 
212
    
 
213
    def property_changed(self, property=None, value=None,
 
214
                         *args, **kwargs):
 
215
        """Call self.update() if old value is not new value.
 
216
        This overrides the method from MandosClientPropertyCache"""
 
217
        property_name = unicode(property)
 
218
        old_value = self.properties.get(property_name)
 
219
        super(MandosClientWidget, self).property_changed(
 
220
            property=property, value=value, *args, **kwargs)
 
221
        if self.properties.get(property_name) != old_value:
 
222
            self.update()
 
223
 
 
224
 
 
225
class UserInterface(object):
 
226
    """This is the entire user interface - the whole screen
 
227
    with boxes, lists of client widgets, etc.
 
228
    """
 
229
    def __init__(self):
 
230
        DBusGMainLoop(set_as_default=True )
 
231
        
 
232
        self.screen = urwid.curses_display.Screen()
 
233
        
 
234
        self.screen.register_palette((
 
235
                (u"normal",
 
236
                 u"default", u"default", None),
 
237
                (u"bold",
 
238
                 u"default", u"default", u"bold"),
 
239
                (u"underline-blink",
 
240
                 u"default", u"default", u"underline"),
 
241
                (u"standout",
 
242
                 u"default", u"default", u"standout"),
 
243
                (u"bold-underline-blink",
 
244
                 u"default", u"default", (u"bold", u"underline")),
 
245
                (u"bold-standout",
 
246
                 u"default", u"default", (u"bold", u"standout")),
 
247
                (u"underline-blink-standout",
 
248
                 u"default", u"default", (u"underline", u"standout")),
 
249
                (u"bold-underline-blink-standout",
 
250
                 u"default", u"default", (u"bold", u"underline",
 
251
                                          u"standout")),
 
252
                ))
 
253
        
 
254
        self.screen.start()
 
255
        
 
256
        self.size = self.screen.get_cols_rows()
 
257
        
 
258
        self.clients = urwid.SimpleListWalker([])
 
259
        self.clients_dict = {}
 
260
        self.topwidget = urwid.LineBox(urwid.ListBox(self.clients))
 
261
        #self.topwidget = urwid.ListBox(clients)
 
262
        
 
263
        self.busname = domain + '.Mandos'
 
264
        self.main_loop = gobject.MainLoop()
 
265
        self.bus = dbus.SystemBus()
 
266
        mandos_dbus_objc = self.bus.get_object(
 
267
            self.busname, u"/", follow_name_owner_changes=True)
 
268
        self.mandos_serv = dbus.Interface(mandos_dbus_objc,
 
269
                                          dbus_interface
 
270
                                          = server_interface)
 
271
        try:
 
272
            mandos_clients = (self.mandos_serv
 
273
                              .GetAllClientsWithProperties())
 
274
        except dbus.exceptions.DBusException:
 
275
            mandos_clients = dbus.Dictionary()
 
276
        
 
277
        (self.mandos_serv
 
278
         .connect_to_signal("ClientRemoved",
 
279
                            self.find_and_remove_client,
 
280
                            dbus_interface=server_interface,
 
281
                            byte_arrays=True))
 
282
        (self.mandos_serv
 
283
         .connect_to_signal("ClientAdded",
 
284
                            self.add_new_client,
 
285
                            dbus_interface=server_interface,
 
286
                            byte_arrays=True))
 
287
        for path, client in (mandos_clients.iteritems()):
 
288
            client_proxy_object = self.bus.get_object(self.busname,
 
289
                                                      path)
 
290
            self.add_client(MandosClientWidget(server_proxy_object
 
291
                                               =self.mandos_serv,
 
292
                                               proxy_object
 
293
                                               =client_proxy_object,
 
294
                                               properties=client,
 
295
                                               update_hook
 
296
                                               =self.refresh,
 
297
                                               delete_hook
 
298
                                               =self.remove_client),
 
299
                            path=path)
 
300
    
 
301
    def find_and_remove_client(self, path, name):
 
302
        """Find an client from its object path and remove it.
 
303
        
 
304
        This is connected to the ClientRemoved signal from the
 
305
        Mandos server object."""
 
306
        try:
 
307
            client = self.clients_dict[path]
 
308
        except KeyError:
 
309
            # not found?
 
310
            return
 
311
        self.remove_client(client, path)
 
312
    
 
313
    def add_new_client(self, path, properties):
 
314
        client_proxy_object = self.bus.get_object(self.busname, path)
 
315
        self.add_client(MandosClientWidget(server_proxy_object
 
316
                                           =self.mandos_serv,
 
317
                                           proxy_object
 
318
                                           =client_proxy_object,
 
319
                                           properties=properties,
 
320
                                           update_hook
 
321
                                           =self.refresh,
 
322
                                           delete_hook
 
323
                                           =self.remove_client),
 
324
                        path=path)
 
325
    
 
326
    def add_client(self, client, path=None):
 
327
        self.clients.append(client)
 
328
        if path is None:
 
329
            path = client.proxy.object_path
 
330
        self.clients_dict[path] = client
 
331
        self.clients.sort(None, lambda c: c.properties[u"name"])
 
332
        self.refresh()
 
333
    
 
334
    def remove_client(self, client, path=None):
 
335
        self.clients.remove(client)
 
336
        if path is None:
 
337
            path = client.proxy.object_path
 
338
        del self.clients_dict[path]
 
339
        self.refresh()
 
340
    
 
341
    def refresh(self):
 
342
        """Redraw the screen"""
 
343
        canvas = self.topwidget.render(self.size, focus=True)
 
344
        self.screen.draw_screen(self.size, canvas)
 
345
    
 
346
    def run(self):
 
347
        """Start the main loop and exit when it's done."""
 
348
        self.refresh()
 
349
        self._input_callback_tag = (gobject.io_add_watch
 
350
                                    (sys.stdin.fileno(),
 
351
                                     gobject.IO_IN,
 
352
                                     self.process_input))
 
353
        self.main_loop.run()
 
354
        # Main loop has finished, we should close everything now
 
355
        gobject.source_remove(self._input_callback_tag)
 
356
        self.screen.stop()
 
357
    
 
358
    def stop(self):
 
359
        self.main_loop.quit()
 
360
    
 
361
    def process_input(self, source, condition):
 
362
        keys = self.screen.get_input()
 
363
        translations = { u"j": u"down",
 
364
                         u"k": u"up",
 
365
                         }
 
366
        for key in keys:
 
367
            try:
 
368
                key = translations[key]
 
369
            except KeyError:    # :-)
 
370
                pass
 
371
            
 
372
            if key == u"q" or key == u"Q":
 
373
                self.stop()
 
374
                break
 
375
            elif key == u"window resize":
 
376
                self.size = self.screen.get_cols_rows()
 
377
                self.refresh()
 
378
            elif key == " ":
 
379
                self.refresh()
 
380
            elif self.topwidget.selectable():
 
381
                self.topwidget.keypress(self.size, key)
 
382
                self.refresh()
 
383
        return True
 
384
 
 
385
ui = UserInterface()
 
386
try:
 
387
    ui.run()
 
388
except:
 
389
    ui.screen.stop()
 
390
    raise