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