blob: efc8b3d882da7385b6bf5ab75c52f91da8ab1772 [file] [log] [blame]
Steve McIntyre2454bf02015-09-23 18:33:02 +01001#! /usr/bin/python
2
3# Copyright 2015 Linaro Limited
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, write to the Free Software
17# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
18# MA 02110-1301, USA.
19#
20# Visualisation module for VLANd. Fork a trivial webserver
21# implementation on an extra, and generate a simple set of pages and
22# graphics on demand.
23#
24
25import os, sys, logging, time, datetime, re
26from multiprocessing import Process
27from BaseHTTPServer import BaseHTTPRequestHandler
28from BaseHTTPServer import HTTPServer
29import urlparse
30
31if __name__ == '__main__':
32 vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0])))
33 sys.path.insert(0, vlandpath)
34 sys.path.insert(0, "%s/.." % vlandpath)
35
36from errors import InputError
37from db.db import VlanDB
38from config.config import VlanConfig
39from graphics import Graphics,Switch
40from util import VlanUtil
41class VlandHTTPServer(HTTPServer):
42 """ Trivial wrapper for HTTPServer so we can include our own state. """
43 def __init__(self, server_address, handler, state):
44 HTTPServer.__init__(self, server_address, handler)
45 self.state = state
46
47class Visualisation(object):
48 """ Code and config for the visualisation graphics module. """
49
50 state = None
51 p = None
52
53 # Fork a new process for the visualisation webserver
54 def __init__(self, state):
55 self.state = state
56 self.p = Process(target=self.visloop, args=())
57 self.p.start()
58
59 # The main loop for the visualisation webserver
60 def visloop(self):
61 self.state.db = VlanDB(db_name=self.state.config.database.dbname,
62 username=self.state.config.database.username)
63
64 loglevel = VlanUtil().set_logging_level(self.state.config.logging.level)
65
66 # Should we log to stderr?
67 if self.state.config.logging.filename is None:
68 logging.basicConfig(level = loglevel,
69 format = '%(asctime)s %(levelname)-8s %(message)s')
70 else:
71 logging.basicConfig(level = loglevel,
72 format = '%(asctime)s %(levelname)-8s VIS %(message)s',
73 datefmt = '%Y-%m-%d %H:%M:%S %Z',
74 filename = self.state.config.logging.filename,
75 filemode = 'a')
76 logging.info('%s visualisation starting up', self.state.banner)
77
78 server = VlandHTTPServer(('localhost', self.state.config.visualisation.port),
79 GetHandler, self.state)
80 server.serve_forever()
81
82 # Kill the webserver
83 def shutdown(self):
84 self.p.terminate()
85
86class GetHandler(BaseHTTPRequestHandler):
87 """ Methods to generate and serve the pages """
88
89 parsed_path = None
90
91 # Trivial top-level page. Link to images for each of the VLANs we
92 # know about.
93 def send_index(self):
94 self.send_response(200)
95 self.wfile.write('Content-type: text/html\r\n')
96 self.end_headers()
97 page = []
98 page.append('<html>')
99 page.append('<head>')
100 page.append('<TITLE>VLANd visualisation</TITLE>')
101 page.append('</HEAD>')
102 page.append('<body>')
103 page.append('<h1>VLANd visualisation, version %s</h1>' % self.server.state.version)
104
105 switches = self.server.state.db.all_switches()
106 if len(switches) == 0:
107 page.append('<p>No switches found in the database, nothing to show...</p>')
108 else:
109 vlans = self.server.state.db.all_vlans()
110 page.append('<h2>VLANs currently in use: %d</h2>' % len(vlans))
111 for vlan in vlans:
112 page.append('<h3>VLAN id %d, tag %d, name %s</h3>' % (vlan.vlan_id, vlan.tag, vlan.name))
113 page.append('<p><a href="/images/vlan/%d.png"><img src="/images/vlan/%d.png" alt="VLAN %d diagram"></a></p>' % (vlan.vlan_id,vlan.vlan_id,vlan.vlan_id))
114
115 page.append('Current time: %s' % datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC"))
116 page.append('</body>')
117 self.wfile.write('\r\n'.join(page))
118
119 # Trivial style sheet, TODO!
120 def send_style(self):
121 self.send_response(200)
122 self.wfile.write('Content-type: text/css\r\n')
123 self.end_headers()
124 self.wfile.write('style.css')
125
126 # Generate a PNG showing the layout of switches/port/trunks for a
127 # specific VLAN
128 def send_graphic(self):
129 vlan_id = 0
130 vlan_re = re.compile(r'^/images/vlan/(\d+).png$')
131 match = vlan_re.match(self.parsed_path.path)
132 if match:
133 vlan_id = match.group(1)
134 db = self.server.state.db
135 vlan = db.get_vlan_by_id(vlan_id)
136 # We've been asked for a VLAN that doesn't exist
137 if vlan is None:
138 self.send_response(404)
139 self.wfile.write('Content-type: text/html\r\n')
140 self.end_headers()
141 self.wfile.write('404 Not Found')
142 return
143
144 gim = Graphics()
145
146 print "Looking at vlan_id %d, tag %d" % (int(vlan_id), int(vlan.tag))
147
148 # Pick fonts. TODO: Make these configurable?
149 gim.set_font(['/usr/share/fonts/truetype/inconsolata/Inconsolata.otf',
150 '/usr/share/fonts/truetype/freefont/FreeMono.ttf'])
151 try:
152 gim.font
153 # If we can't get the font we need, fail
154 except NameError:
155 self.send_response(500)
156 self.wfile.write('Content-type: text/html\r\n')
157 self.end_headers()
158 self.wfile.write('500 Internal Server Error')
159 return
160
161 switch = {}
162 size_x = {}
163 size_y = {}
164
165 switches = db.all_switches()
166
167 # Need to set gaps big enough for the number of trunks, at least.
168 trunks = db.all_trunks()
169 y_gap = max(20, 15 * len(trunks))
170 x_gap = max(20, 15 * len(trunks))
171
172 x = 0
173 y = y_gap
174
175 # Work out how much space we need for the switches
176 for i in range(0, len(switches)):
177 ports = db.get_ports_by_switch(switches[i].switch_id)
178 switch[i] = Switch(gim, len(ports), switches[i].name)
179 (size_x[i], size_y[i]) = switch[i].get_dimensions()
180 x = max(x, size_x[i])
181 y += size_y[i] + y_gap
182
183 # Add space for the legend and the label
184 label = "VLAN %d - %s" % (vlan.tag, vlan.name)
185 (legend_width, legend_height) = gim.get_legend_dimensions()
186 (label_width, label_height) = gim.get_label_size(label, gim.label_font_size)
187 x = max(x, legend_width + 2*x_gap + label_width)
188 x = x_gap + x + x_gap
189 y = y + max(legend_height + y_gap, label_height)
190
191 # Create a canvas of the right size
192 gim.create_canvas(x, y)
193
194 # Draw the switches and ports in it
195 curr_y = y_gap
196 for i in range(0, len(switches)):
197 switch[i].draw_switch(gim, x_gap, curr_y)
198 ports = db.get_ports_by_switch(switches[i].switch_id)
199 for port_id in ports:
200 port = db.get_port_by_id(port_id)
201 if port.is_locked:
202 switch[i].draw_port(gim, port.number, 'locked')
203 elif port.is_trunk:
204 switch[i].draw_port(gim, port.number, 'trunk')
205 elif port.current_vlan_id == int(vlan_id):
206 switch[i].draw_port(gim, port.number, 'VLAN')
207 else:
208 switch[i].draw_port(gim, port.number, 'normal')
209 curr_y += size_y[i] + y_gap
210
211 # Now add the trunks
212 for i in range(0, len(trunks)):
213 ports = db.get_ports_by_trunk(trunks[i].trunk_id)
214 port1 = db.get_port_by_id(ports[0])
215 port2 = db.get_port_by_id(ports[1])
216 for s in range(0, len(switches)):
217 if switches[s].switch_id == port1.switch_id:
218 switch1 = s
219 if switches[s].switch_id == port2.switch_id:
220 switch2 = s
221 gim.draw_trunk(i,
222 switch[switch1].get_port_location(port1.number),
223 switch[switch2].get_port_location(port2.number),
224 gim.port_pallette['trunk']['trace'])
225
226 # And the legend and label
227 gim.draw_legend(x_gap, curr_y)
228 gim.draw_label(x - label_width - 2*x_gap, curr_y, label, int(x_gap / 2))
229
230 # All done - send it down the http socket
231 self.send_response(200)
232 self.wfile.write('Content-type: image/png\r\n')
233 self.end_headers()
234 gim.im.writePng(self.wfile)
235
236 # Implement an HTTP GET handler for the HTTPServer instance
237 def do_GET(self):
238 # Compare the URL path to any of the names we recognise and
239 # call the right generator function if we get a match
240 self.parsed_path = urlparse.urlparse(self.path)
241 for url in self.functionMap:
242 match = re.match(url['re'], self.parsed_path.path)
243 if match:
244 return url['fn'](self)
245
246 # Fall-through for any files we don't recognise
247 self.send_response(404)
248 self.wfile.write('Content-type: text/html\r\n')
249 self.end_headers()
250 self.wfile.write('404 Not Found')
251 return
252
253 # Override the BaseHTTPRequestHandler log_message() method so we
254 # can log requests properly
255 def log_message(self, format, *args):
256 """Log an arbitrary message. """
257 logging.info('%s %s', self.client_address[0], format%args)
258
259 functionMap = (
260 {'re': '^/$', 'fn': send_index},
261 {'re': '^/style.css$', 'fn': send_style},
262 {'re': '^/images/vlan/(\d+).png$', 'fn': send_graphic}
263 )