/mandos/trunk

To get this branch, use:
bzr branch http://bzr.recompile.se/loggerhead/mandos/trunk
404 by Teddy Hogeborn
* mandos-monitor: New prototype version of interactive server
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