Let the GUI handle the new dump format.
[tinc] / gui / tinc-gui
1 #!/usr/bin/python
2
3 # tinc-gui -- GUI for controlling a running tincd
4 # Copyright (C) 2009-2012 Guus Sliepen <guus@tinc-vpn.org>
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License along
17 # with this program; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20 import string
21 import socket
22 import wx
23 import sys
24 import os
25 import platform
26 import time
27 from wx.lib.mixins.listctrl import ColumnSorterMixin
28 from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin
29
30 if platform.system == 'Windows':
31         import _winreg
32
33 # Classes to interface with a running tinc daemon
34
35 REQ_STOP = 0
36 REQ_RELOAD = 1
37 REQ_RESTART = 2
38 REQ_DUMP_NODES = 3
39 REQ_DUMP_EDGES = 4
40 REQ_DUMP_SUBNETS = 5
41 REQ_DUMP_CONNECTIONS = 6
42 REQ_DUMP_GRAPH = 7
43 REQ_PURGE = 8
44 REQ_SET_DEBUG = 9
45 REQ_RETRY = 10
46 REQ_CONNECT = 11
47 REQ_DISCONNECT = 12
48
49 ID = 0
50 ACK = 4
51 CONTROL = 18
52
53 class Node:
54         def parse(self, args):
55                 self.name = args[0]
56                 self.address = args[1]
57                 self.port = args[3]
58                 self.cipher = int(args[4])
59                 self.digest = int(args[5])
60                 self.maclength = int(args[6])
61                 self.compression = int(args[7])
62                 self.options = int(args[8], 0x10)
63                 self.status = int(args[9], 0x10)
64                 self.nexthop = args[10]
65                 self.via = args[11]
66                 self.distance = int(args[12])
67                 self.pmtu = int(args[13])
68                 self.minmtu = int(args[14])
69                 self.maxmtu = int(args[15])
70                 self.last_state_change = float(args[16])
71
72                 self.subnets = {}
73
74 class Edge:
75         def parse(self, args):
76                 self.fr = args[0]
77                 self.to = args[1]
78                 self.address = args[2]
79                 self.port = args[4]
80                 self.options = int(args[5], 16)
81                 self.weight = int(args[6])
82
83 class Subnet:
84         def parse(self, args):
85                 if args[0].find('#') >= 0:
86                         (address, self.weight) = args[0].split('#', 1)
87                 else:
88                         self.weight = 10
89                         address = args[0]
90
91                 if address.find('/') >= 0:
92                         (self.address, self.prefixlen) = address.split('/', 1)
93                 else:
94                         self.address = address
95                         self.prefixlen = '48'
96
97                 self.owner = args[1]    
98
99 class Connection:
100         def parse(self, args):
101                 self.name = args[0]
102                 self.address = args[1]
103                 self.port = args[3]
104                 self.options = int(args[4], 0x10)
105                 self.socket = int(args[5])
106                 self.status = int(args[6], 0x10)
107                 self.weight = 123
108
109 class VPN:
110         confdir = '/etc/tinc'
111         piddir = '/var/run/'
112
113         def connect(self):
114                 f = open(self.pidfile)
115                 info = string.split(f.readline())
116                 f.close()
117                 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
118                 s.connect((info[2], int(info[4])))
119                 self.sf = s.makefile()
120                 s.close()
121                 hello = string.split(self.sf.readline())
122                 self.name = hello[1]
123                 self.sf.write('0 ^' + info[1] + ' 17\r\n')
124                 self.sf.flush()
125                 resp = string.split(self.sf.readline())
126                 self.port = info[4]
127                 self.nodes = {}
128                 self.edges = {}
129                 self.subnets = {}
130                 self.connections = {}
131                 self.refresh()
132
133         def refresh(self):
134                 self.sf.write('18 3\r\n18 4\r\n18 5\r\n18 6\r\n')
135                 self.sf.flush()
136
137                 for node in self.nodes.values():
138                         node.visited = False
139                 for edge in self.edges.values():
140                         edge.visited = False
141                 for subnet in self.subnets.values():
142                         subnet.visited = False
143                 for connections in self.connections.values():
144                         connections.visited = False
145
146                 while True:
147                         resp = string.split(self.sf.readline())
148                         if len(resp) < 2:
149                                 break
150                         if resp[0] != '18':
151                                 break
152                         if resp[1] == '3':
153                                 if len(resp) < 19:
154                                         continue
155                                 node = self.nodes.get(resp[2]) or Node()
156                                 node.parse(resp[2:])
157                                 node.visited = True
158                                 self.nodes[resp[2]] = node
159                         elif resp[1] == '4':
160                                 if len(resp) < 9:
161                                         continue
162                                 edge = self.nodes.get((resp[2], resp[3])) or Edge()
163                                 edge.parse(resp[2:])
164                                 edge.visited = True
165                                 self.edges[(resp[2], resp[3])] = edge
166                         elif resp[1] == '5':
167                                 if len(resp) < 4:
168                                         continue
169                                 subnet = self.subnets.get((resp[2], resp[3])) or Subnet()
170                                 subnet.parse(resp[2:])
171                                 subnet.visited = True
172                                 self.subnets[(resp[2], resp[3])] = subnet
173                                 self.nodes[subnet.owner].subnets[resp[2]] = subnet
174                         elif resp[1] == '6':
175                                 if len(resp) < 9:
176                                         break
177                                 connection = self.connections.get((resp[2], resp[3], resp[5])) or Connection()
178                                 connection.parse(resp[2:])
179                                 connection.visited = True
180                                 self.connections[(resp[2], resp[3], resp[5])] = connection
181                         else:
182                                 break
183
184                 for key, subnet in self.subnets.items():
185                         if not subnet.visited:
186                                 del self.subnets[key]
187
188                 for key, edge in self.edges.items():
189                         if not edge.visited:
190                                 del self.edges[key]
191
192                 for key, node in self.nodes.items():
193                         if not node.visited:
194                                 del self.nodes[key]
195                         else:
196                                 for key, subnet in node.subnets.items():
197                                         if not subnet.visited:
198                                                 del node.subnets[key]
199
200                 for key, connection in self.connections.items():
201                         if not connection.visited:
202                                 del self.connections[key]
203
204         def close(self):
205                 self.sf.close()
206
207         def disconnect(self, name):
208                 self.sf.write('18 12 ' + name + '\r\n')
209                 self.sf.flush()
210                 resp = string.split(self.sf.readline())
211
212         def debug(self, level = -1):
213                 self.sf.write('18 9 ' + str(level) + '\r\n')
214                 self.sf.flush()
215                 resp = string.split(self.sf.readline())
216                 return int(resp[2])
217
218         def __init__(self, netname = None, pidfile = None):
219                 if platform.system == 'Windows':
220                         try:
221                                 reg = _winreg.ConnectRegistry(None, HKEY_LOCAL_MACHINE)
222                                 key = _winreg.OpenKey(reg, "SOFTWARE\\tinc")
223                                 VPN.confdir = _winreg.QueryValue(key, None)
224                         except WindowsError:
225                                 pass
226
227                 if netname:
228                         self.netname = netname
229                         self.confbase = os.path.join(VPN.confdir, netname)
230                 else:
231                         self.confbase = VPN.confdir
232
233                 self.tincconf = os.path.join(self.confbase, 'tinc.conf')
234
235                 if pidfile != None:
236                         self.pidfile = pidfile
237                 else:
238                         if platform.system == 'Windows':
239                                 self.pidfile = os.path.join(self.confbase, 'pid')
240                         else:
241                                 if netname:
242                                         self.pidfile = os.path.join(VPN.piddir, 'tinc.' + netname + '.pid')
243                                 else:
244                                         self.pidfile = os.path.join(VPN.piddir, 'tinc.pid')
245
246 # GUI starts here
247
248 argv0 = sys.argv[0]
249 del sys.argv[0]
250 netname = None
251 pidfile = None
252
253 def usage(exitcode = 0):
254         print('Usage: ' + argv0 + ' [options]')
255         print('\nValid options are:')
256         print('  -n, --net=NETNAME       Connect to net NETNAME.')
257         print('      --pidfile=FILENAME  Read control cookie from FILENAME.')
258         print('      --help              Display this help and exit.')
259         print('\nReport bugs to tinc@tinc-vpn.org.')
260         sys.exit(exitcode)
261
262 while sys.argv:
263         if sys.argv[0] in ('-n', '--net'):
264                 del sys.argv[0]
265                 netname = sys.argv[0]
266         elif sys.argv[0] in ('--pidfile'):
267                 del sys.argv[0]
268                 pidfile = sys.argv[0]
269         elif sys.argv[0] in ('--help'):
270                 usage(0)
271         else:
272                 print(argv0 + ': unrecognized option \'' + sys.argv[0] + '\'')
273                 usage(1)
274
275         del sys.argv[0]
276
277 if netname == None:
278         netname = os.getenv("NETNAME")
279
280 if netname == ".":
281         netname = None
282
283 vpn = VPN(netname, pidfile)
284 vpn.connect()
285
286 class SuperListCtrl(wx.ListCtrl, ColumnSorterMixin, ListCtrlAutoWidthMixin):
287     def __init__(self, parent, style):
288         wx.ListCtrl.__init__(self, parent, -1, style=wx.LC_REPORT | wx.LC_HRULES | wx.LC_VRULES)
289         ListCtrlAutoWidthMixin.__init__(self)
290         ColumnSorterMixin.__init__(self, 14)
291
292     def GetListCtrl(self):
293         return self
294
295
296 class SettingsPage(wx.Panel):
297         def OnDebugLevel(self, event):
298                 vpn.debug(self.debug.GetValue())
299
300         def __init__(self, parent, id):
301                 wx.Panel.__init__(self, parent, id)
302                 grid = wx.FlexGridSizer(cols = 2)
303                 grid.AddGrowableCol(0, 1)
304
305                 namelabel = wx.StaticText(self, -1, 'Name:')
306                 self.name = wx.TextCtrl(self, -1, vpn.name)
307                 grid.Add(namelabel)
308                 grid.Add(self.name)
309
310                 portlabel = wx.StaticText(self, -1, 'Port:')
311                 self.port = wx.TextCtrl(self, -1, vpn.port)
312                 grid.Add(portlabel)
313                 grid.Add(self.port)
314
315                 debuglabel = wx.StaticText(self, -1, 'Debug level:')
316                 self.debug = wx.SpinCtrl(self, min = 0, max = 5, initial = vpn.debug())
317                 self.debug.Bind(wx.EVT_SPINCTRL, self.OnDebugLevel)
318                 grid.Add(debuglabel)
319                 grid.Add(self.debug)
320
321                 modelabel = wx.StaticText(self, -1, 'Mode:')
322                 self.mode = wx.ComboBox(self, -1, style = wx.CB_READONLY, value = 'Router', choices = ['Router', 'Switch', 'Hub'])
323                 grid.Add(modelabel)
324                 grid.Add(self.mode)
325
326                 self.SetSizer(grid)
327
328 class ConnectionsPage(wx.Panel):
329         def __init__(self, parent, id):
330                 wx.Panel.__init__(self, parent, id)
331                 self.list = wx.ListCtrl(self, id, style=wx.LC_REPORT | wx.LC_HRULES | wx.LC_VRULES)
332                 self.list.InsertColumn(0, 'Name')
333                 self.list.InsertColumn(1, 'Address')
334                 self.list.InsertColumn(2, 'Port')
335                 self.list.InsertColumn(3, 'Options')
336                 self.list.InsertColumn(4, 'Weight')
337
338                 hbox = wx.BoxSizer(wx.HORIZONTAL)
339                 hbox.Add(self.list, 1, wx.EXPAND)
340                 self.SetSizer(hbox)
341                 self.refresh()
342
343         class ContextMenu(wx.Menu):
344                 def __init__(self, item):
345                         wx.Menu.__init__(self)
346
347                         self.item = item
348
349                         disconnect = wx.MenuItem(self, -1, 'Disconnect')
350                         self.AppendItem(disconnect)
351                         self.Bind(wx.EVT_MENU, self.OnDisconnect, id=disconnect.GetId())
352
353                 def OnDisconnect(self, event):
354                         vpn.disconnect(self.item[0])
355
356         def OnContext(self, event):
357                 i = event.GetIndex()
358                 self.PopupMenu(self.ContextMenu(self.list.itemDataMap[event.GetIndex()]), event.GetPosition())
359
360         def refresh(self):
361                 self.list.itemDataMap = {}
362                 i = 0
363
364                 for key, connection in vpn.connections.items():
365                         if self.list.GetItemCount() <= i:
366                                 self.list.InsertStringItem(i, connection.name)
367                         else:
368                                 self.list.SetStringItem(i, 0, connection.name)
369                         self.list.SetStringItem(i, 1, connection.address)
370                         self.list.SetStringItem(i, 2, connection.port)
371                         self.list.SetStringItem(i, 3, str(connection.options))
372                         self.list.SetStringItem(i, 4, str(connection.weight))
373                         self.list.itemDataMap[i] = (connection.name, connection.address, connection.port, connection.options, connection.weight)
374                         self.list.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnContext)
375                         i += 1
376
377                 while self.list.GetItemCount() > i:
378                         self.list.DeleteItem(self.list.GetItemCount() - 1)
379
380
381 class NodesPage(wx.Panel):
382         def __init__(self, parent, id):
383                 wx.Panel.__init__(self, parent, id)
384                 self.list = SuperListCtrl(self, id)
385                 self.list.InsertColumn( 0, 'Name')
386                 self.list.InsertColumn( 1, 'Address')
387                 self.list.InsertColumn( 2, 'Port')
388                 self.list.InsertColumn( 3, 'Cipher')
389                 self.list.InsertColumn( 4, 'Digest')
390                 self.list.InsertColumn( 5, 'MACLength')
391                 self.list.InsertColumn( 6, 'Compression')
392                 self.list.InsertColumn( 7, 'Options')
393                 self.list.InsertColumn( 8, 'Status')
394                 self.list.InsertColumn( 9, 'Nexthop')
395                 self.list.InsertColumn(10, 'Via')
396                 self.list.InsertColumn(11, 'Distance')
397                 self.list.InsertColumn(12, 'PMTU')
398                 self.list.InsertColumn(13, 'Min MTU')
399                 self.list.InsertColumn(14, 'Max MTU')
400                 self.list.InsertColumn(15, 'Since')
401
402                 hbox = wx.BoxSizer(wx.HORIZONTAL)
403                 hbox.Add(self.list, 1, wx.EXPAND)
404                 self.SetSizer(hbox)
405                 self.refresh()
406
407         def refresh(self):
408                 self.list.itemDataMap = {}
409                 i = 0
410
411                 for key, node in vpn.nodes.items():
412                         if self.list.GetItemCount() <= i:
413                                 self.list.InsertStringItem(i, node.name)
414                         else:
415                                 self.list.SetStringItem(i,  0, node.name)
416                         self.list.SetStringItem(i,  1, node.address)
417                         self.list.SetStringItem(i,  2, node.port)
418                         self.list.SetStringItem(i,  3, str(node.cipher))
419                         self.list.SetStringItem(i,  4, str(node.digest))
420                         self.list.SetStringItem(i,  5, str(node.maclength))
421                         self.list.SetStringItem(i,  6, str(node.compression))
422                         self.list.SetStringItem(i,  7, format(node.options, "x"))
423                         self.list.SetStringItem(i,  8, format(node.status, "04x"))
424                         self.list.SetStringItem(i,  9, node.nexthop)
425                         self.list.SetStringItem(i, 10, node.via)
426                         self.list.SetStringItem(i, 11, str(node.distance))
427                         self.list.SetStringItem(i, 12, str(node.pmtu))
428                         self.list.SetStringItem(i, 13, str(node.minmtu))
429                         self.list.SetStringItem(i, 14, str(node.maxmtu))
430                         if node.last_state_change:
431                                 since = time.strftime("%Y-%m-%d %H:%M", time.localtime(node.last_state_change))
432                         else:
433                                 since = "never"
434                         self.list.SetStringItem(i, 15, since)
435                         self.list.itemDataMap[i] = (node.name, node.address, node.port, node.cipher, node.digest, node.maclength, node.compression, node.options, node.status, node.nexthop, node.via, node.distance, node.pmtu, node.minmtu, node.maxmtu)
436                         self.list.SetItemData(i, i)
437                         i += 1
438
439                 while self.list.GetItemCount() > i:
440                         self.list.DeleteItem(self.list.GetItemCount() - 1)
441
442 class EdgesPage(wx.Panel):
443         def __init__(self, parent, id):
444                 wx.Panel.__init__(self, parent, id)
445                 self.list = wx.ListCtrl(self, id, style=wx.LC_REPORT | wx.LC_HRULES | wx.LC_VRULES)
446                 self.list.InsertColumn(0, 'From')
447                 self.list.InsertColumn(1, 'To')
448                 self.list.InsertColumn(2, 'Address')
449                 self.list.InsertColumn(3, 'Port')
450                 self.list.InsertColumn(4, 'Options')
451                 self.list.InsertColumn(5, 'Weight')
452
453                 hbox = wx.BoxSizer(wx.HORIZONTAL)
454                 hbox.Add(self.list, 1, wx.EXPAND)
455                 self.SetSizer(hbox)
456                 self.refresh()
457
458         def refresh(self):
459                 self.list.itemDataMap = {}
460                 i = 0
461
462                 for key, edge in vpn.edges.items():
463                         if self.list.GetItemCount() <= i:
464                                 self.list.InsertStringItem(i, edge.fr)
465                         else:
466                                 self.list.SetStringItem(i, 0, edge.fr)
467                         self.list.SetStringItem(i, 1, edge.to)
468                         self.list.SetStringItem(i, 2, edge.address)
469                         self.list.SetStringItem(i, 3, edge.port)
470                         self.list.SetStringItem(i, 4, format(edge.options, "x"))
471                         self.list.SetStringItem(i, 5, str(edge.weight))
472                         self.list.itemDataMap[i] = (edge.fr, edge.to, edge.address, edge.port, edge.options, edge.weight)
473                         i += 1
474
475                 while self.list.GetItemCount() > i:
476                         self.list.DeleteItem(self.list.GetItemCount() - 1)
477
478 class SubnetsPage(wx.Panel):
479         def __init__(self, parent, id):
480                 wx.Panel.__init__(self, parent, id)
481                 self.list = SuperListCtrl(self, id)
482                 self.list.InsertColumn(0, 'Subnet', wx.LIST_FORMAT_RIGHT)
483                 self.list.InsertColumn(1, 'Weight', wx.LIST_FORMAT_RIGHT)
484                 self.list.InsertColumn(2, 'Owner')
485                 hbox = wx.BoxSizer(wx.HORIZONTAL)
486                 hbox.Add(self.list, 1, wx.EXPAND)
487                 self.SetSizer(hbox)
488                 self.refresh()
489
490         def refresh(self):
491                 self.list.itemDataMap = {}
492                 i = 0
493
494                 for key, subnet in vpn.subnets.items():
495                         if self.list.GetItemCount() <= i:
496                                 self.list.InsertStringItem(i, subnet.address + '/' + subnet.prefixlen)
497                         else:
498                                 self.list.SetStringItem(i, 0, subnet.address + '/' + subnet.prefixlen)
499                         self.list.SetStringItem(i, 1, subnet.weight)
500                         self.list.SetStringItem(i, 2, subnet.owner)
501                         self.list.itemDataMap[i] = (subnet.address + '/' + subnet.prefixlen, subnet.weight, subnet.owner)
502                         i = i + 1
503
504                 while self.list.GetItemCount() > i:
505                         self.list.DeleteItem(self.list.GetItemCount() - 1)
506
507 class StatusPage(wx.Panel):
508         def __init__(self, parent, id):
509                 wx.Panel.__init__(self, parent, id)
510
511 class GraphPage(wx.Window):
512         def __init__(self, parent, id):
513                 wx.Window.__init__(self, parent, id)
514
515 class NetPage(wx.Notebook):
516         def __init__(self, parent, id):
517                 wx.Notebook.__init__(self, parent)
518                 self.settings = SettingsPage(self, id)
519                 self.connections = ConnectionsPage(self, id)
520                 self.nodes = NodesPage(self, id)
521                 self.edges = EdgesPage(self, id)
522                 self.subnets = SubnetsPage(self, id)
523                 self.graph = GraphPage(self, id)
524                 self.status = StatusPage(self, id)
525
526                 self.AddPage(self.settings, 'Settings')
527                 #self.AddPage(self.status, 'Status')
528                 self.AddPage(self.connections, 'Connections')
529                 self.AddPage(self.nodes, 'Nodes')
530                 self.AddPage(self.edges, 'Edges')
531                 self.AddPage(self.subnets, 'Subnets')
532                 #self.AddPage(self.graph, 'Graph')
533                 
534
535 class MainWindow(wx.Frame):
536         def OnQuit(self, event):
537                 app.ExitMainLoop()
538
539         def OnTimer(self, event):
540                 vpn.refresh()
541                 self.np.nodes.refresh()
542                 self.np.subnets.refresh()
543                 self.np.edges.refresh()
544                 self.np.connections.refresh()
545
546         def __init__(self, parent, id, title):
547                 wx.Frame.__init__(self, parent, id, title)
548
549                 menubar = wx.MenuBar()
550                 file = wx.Menu()
551                 file.Append(1, '&Quit\tCtrl-X', 'Quit tinc GUI')
552                 menubar.Append(file, '&File')
553
554                 #nb = wx.Notebook(self, -1)
555                 #nb.SetPadding((0, 0))
556                 self.np = NetPage(self, -1)
557                 #nb.AddPage(np, 'VPN')
558                 
559                 self.timer = wx.Timer(self, -1)
560                 self.Bind(wx.EVT_TIMER, self.OnTimer, self.timer)
561                 self.timer.Start(1000)
562                 self.Bind(wx.EVT_MENU, self.OnQuit, id=1)
563                 self.SetMenuBar(menubar)
564                 self.Show()
565
566 app = wx.App()
567 mw = MainWindow(None, -1, 'Tinc GUI')
568
569 #def OnTaskBarIcon(event):
570 #       mw.Raise()
571 #
572 #icon = wx.Icon("tincgui.ico", wx.BITMAP_TYPE_PNG)
573 #taskbaricon = wx.TaskBarIcon()
574 #taskbaricon.SetIcon(icon, 'Tinc GUI')
575 #wx.EVT_TASKBAR_RIGHT_UP(taskbaricon, OnTaskBarIcon)
576
577 app.MainLoop()
578 vpn.close()