blob: cf8480a914fbc3b63af3a89499dab6d83b46d3ec [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
Steve McIntyre57a9d0a2015-10-28 18:22:31 +000030import cStringIO
Steve McIntyre2454bf02015-09-23 18:33:02 +010031
32if __name__ == '__main__':
33 vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0])))
34 sys.path.insert(0, vlandpath)
35 sys.path.insert(0, "%s/.." % vlandpath)
36
37from errors import InputError
38from db.db import VlanDB
39from config.config import VlanConfig
40from graphics import Graphics,Switch
41from util import VlanUtil
42class VlandHTTPServer(HTTPServer):
43 """ Trivial wrapper for HTTPServer so we can include our own state. """
44 def __init__(self, server_address, handler, state):
45 HTTPServer.__init__(self, server_address, handler)
46 self.state = state
47
Steve McIntyre57a9d0a2015-10-28 18:22:31 +000048class GraphicsCache(object):
49 """ Cache for graphics state, to avoid having to recalculate every
50 query too many times. """
51 last_update = None
52 max_width = 0
53 graphics = {}
54
55 def __init__(self):
56 # Pick an epoch older than any sensible use
57 self.last_update = datetime.datetime(2000, 01, 01)
58
Steve McIntyre2454bf02015-09-23 18:33:02 +010059class Visualisation(object):
60 """ Code and config for the visualisation graphics module. """
61
62 state = None
63 p = None
64
65 # Fork a new process for the visualisation webserver
66 def __init__(self, state):
67 self.state = state
68 self.p = Process(target=self.visloop, args=())
69 self.p.start()
70
71 # The main loop for the visualisation webserver
72 def visloop(self):
Steve McIntyre57a9d0a2015-10-28 18:22:31 +000073 self.state.cache = GraphicsCache()
Steve McIntyre2454bf02015-09-23 18:33:02 +010074 self.state.db = VlanDB(db_name=self.state.config.database.dbname,
Steve McIntyreea343aa2015-10-23 17:46:17 +010075 username=self.state.config.database.username,
76 readonly=True)
Steve McIntyre2454bf02015-09-23 18:33:02 +010077
78 loglevel = VlanUtil().set_logging_level(self.state.config.logging.level)
79
80 # Should we log to stderr?
81 if self.state.config.logging.filename is None:
82 logging.basicConfig(level = loglevel,
83 format = '%(asctime)s %(levelname)-8s %(message)s')
84 else:
85 logging.basicConfig(level = loglevel,
86 format = '%(asctime)s %(levelname)-8s VIS %(message)s',
87 datefmt = '%Y-%m-%d %H:%M:%S %Z',
88 filename = self.state.config.logging.filename,
89 filemode = 'a')
90 logging.info('%s visualisation starting up', self.state.banner)
91
Steve McIntyre86916e42015-09-28 02:39:32 +010092 server = VlandHTTPServer(('', self.state.config.visualisation.port),
Steve McIntyre2454bf02015-09-23 18:33:02 +010093 GetHandler, self.state)
94 server.serve_forever()
95
96 # Kill the webserver
97 def shutdown(self):
98 self.p.terminate()
99
100class GetHandler(BaseHTTPRequestHandler):
101 """ Methods to generate and serve the pages """
102
103 parsed_path = None
104
105 # Trivial top-level page. Link to images for each of the VLANs we
106 # know about.
107 def send_index(self):
108 self.send_response(200)
109 self.wfile.write('Content-type: text/html\r\n')
110 self.end_headers()
Steve McIntyreb0aa4602015-10-08 15:33:28 +0100111 config = self.server.state.config.visualisation
Steve McIntyre57a9d0a2015-10-28 18:22:31 +0000112 cache = self.server.state.cache
113 db = self.server.state.db
114 switches = db.all_switches()
115 vlans = db.all_vlans()
116 vlan_tags = {}
117
118 for vlan in vlans:
119 vlan_tags[vlan.vlan_id] = vlan.tag
120
121 if cache.last_update < self.server.state.db.get_last_modified_time():
122 logging.debug('Cache is out of date')
123 # Fill the cache with all the information we need:
124 # * the graphics themselves
125 # * the data to match each graphic, so we can generate imagemap/tooltips
126 cache.graphics = {}
127 if len(switches) > 0:
128 for vlan in vlans:
129 cache.graphics[vlan.vlan_id] = self.generate_graphic(vlan.vlan_id)
130 cache.last_update = datetime.datetime.utcnow()
131
Steve McIntyre2454bf02015-09-23 18:33:02 +0100132 page = []
133 page.append('<html>')
134 page.append('<head>')
135 page.append('<TITLE>VLANd visualisation</TITLE>')
Steve McIntyreb74ea6b2015-09-24 20:45:31 +0100136 page.append('<link rel="stylesheet" type="text/css" href="style.css">')
Steve McIntyreb0aa4602015-10-08 15:33:28 +0100137 if config.refresh and config.refresh > 0:
138 page.append('<meta http-equiv="refresh" content="%d">' % config.refresh)
Steve McIntyre2454bf02015-09-23 18:33:02 +0100139 page.append('</HEAD>')
140 page.append('<body>')
Steve McIntyreb74ea6b2015-09-24 20:45:31 +0100141 page.append('<div class="menu">')
142 if len(switches) > 0:
143 page.append('<h2>Menu</h2>')
144 page.append('<p>VLANs: %d</p>' % len(vlans))
145 page.append('<ul>')
146 for vlan in vlans:
147 page.append('<li><a href="./#vlan%d">VLAN id %d, tag %d<br>(%s)</a>' % (vlan.vlan_id, vlan.vlan_id, vlan.tag, vlan.name))
148 page.append('</ul>')
149 page.append('<div class="date"><p>Current time: %s</p>' % datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC"))
150 page.append('<p>version %s</p>' % self.server.state.version)
151 page.append('</div>')
152 page.append('</div>')
153
154 page.append('<div class="content">')
155 page.append('<h1>VLANd visualisation</h1>')
156
Steve McIntyre2454bf02015-09-23 18:33:02 +0100157 if len(switches) == 0:
158 page.append('<p>No switches found in the database, nothing to show...</p>')
159 else:
Steve McIntyre2454bf02015-09-23 18:33:02 +0100160 for vlan in vlans:
Steve McIntyre57a9d0a2015-10-28 18:22:31 +0000161 this_image = cache.graphics[vlan.vlan_id]
Steve McIntyreb74ea6b2015-09-24 20:45:31 +0100162 page.append('<a name="vlan%d"></a>' % vlan.vlan_id)
Steve McIntyre2454bf02015-09-23 18:33:02 +0100163 page.append('<h3>VLAN id %d, tag %d, name %s</h3>' % (vlan.vlan_id, vlan.tag, vlan.name))
Steve McIntyre57a9d0a2015-10-28 18:22:31 +0000164 page.append('<p><img src="/images/vlan/%d.png" ' % vlan.vlan_id)
165 page.append('width="%d" height="%d" ' % ( this_image['image']['width'], this_image['image']['height']))
166 page.append('alt="VLAN %d diagram" usemap="#MAPVLAN%d">' % (vlan.vlan_id,vlan.vlan_id))
167 page.append('<map name="MAPVLAN%d">' % vlan.vlan_id)
168 for switch in this_image['ports'].keys():
169 for portnum in this_image['ports'][switch].keys():
170 this_port = this_image['ports'][switch][portnum]
171 # Grab the data about the port that we stored
172 # earlier when generating the image
173 port = this_port['db']
174 ((ulx,uly),(lrx,lry),upper) = this_port['location']
175 page.append('<area shape="rect" ')
176 page.append('coords="%d,%d,%d,%d" ' % (ulx,uly,lrx,lry))
177 page.append(' />')
178 page.append('<span>Port id: %d Port number: %d<br>' % (port.port_id, port.number))
179 if port.is_locked:
180 page.append('Locked<br>')
181 if port.is_trunk:
182 page.append('Trunk')
183 if port.trunk_id != -1:
184 page.append(' (trunk id %d)' % port.trunk_id)
185 page.append('<br>')
186 else:
187 page.append('Current VLAN id: %d (tag %d)<br>' % (port.current_vlan_id, vlan_tags[port.current_vlan_id]))
188 page.append('Base VLAN id: %d (tag %d)<br>' % (port.base_vlan_id, vlan_tags[port.base_vlan_id]))
189 page.append('</span>')
190 page.append('</map></p>')
Steve McIntyreb74ea6b2015-09-24 20:45:31 +0100191 page.append('<hr>')
Steve McIntyre2454bf02015-09-23 18:33:02 +0100192
Steve McIntyreb74ea6b2015-09-24 20:45:31 +0100193 page.append('</div>')
Steve McIntyre2454bf02015-09-23 18:33:02 +0100194 page.append('</body>')
195 self.wfile.write('\r\n'.join(page))
196
Steve McIntyre57a9d0a2015-10-28 18:22:31 +0000197 # Simple-ish style sheet
Steve McIntyre2454bf02015-09-23 18:33:02 +0100198 def send_style(self):
199 self.send_response(200)
200 self.wfile.write('Content-type: text/css\r\n')
201 self.end_headers()
Steve McIntyre57a9d0a2015-10-28 18:22:31 +0000202 cache = self.server.state.cache
Steve McIntyreb74ea6b2015-09-24 20:45:31 +0100203 page = []
Steve McIntyre57a9d0a2015-10-28 18:22:31 +0000204 page.append('body {')
205 page.append(' background: white;')
206 page.append(' color: black;')
207 page.append(' font-size: 12pt;')
208 page.append('}')
209 page.append('')
210 page.append('.menu {')
211 page.append(' position:fixed;')
212 page.append(' float:left;')
213 page.append(' font-family: arial, Helvetica, sans-serif;')
214 page.append(' width:20%;')
215 page.append(' height:100%;')
216 page.append(' font-size: 10pt;')
217 page.append(' padding-top: 10px;')
218 page.append('}')
219 page.append('')
220 page.append('.content {')
221 page.append(' position:relative;')
222 page.append(' padding-top: 10px;')
223 page.append(' width: %dpx;' % cache.max_width)
224 page.append(' max-width:80%;')
225 page.append(' margin-left: 21%;')
226 page.append(' margin-top: 50px;')
227 page.append(' height:100%;')
228 page.append('}')
229 page.append('')
230 page.append('.footer {')
231 page.append(' vertical-align: bottom;')
232 page.append(' text-align: left;')
233 page.append('}')
234 page.append('')
235 page.append('.caption {')
236 page.append(' padding-top: 1px;')
237 page.append(' padding-left: 10%;')
238 page.append(' padding-right: 10%;')
239 page.append(' font-size: 8pt;')
240 page.append(' font-style: italic;')
241 page.append(' text-align: center;')
242 page.append('}')
243 page.append('td.headline {')
244 page.append(' font-family: arial, Helvetica, sans-serif;')
245 page.append(' font-size: 20pt;')
246 page.append('}')
247 page.append('h1,h2,h3,h4,h5 {')
248 page.append(' font-family: arial, Helvetica, sans-serif;')
249 page.append(' padding-right:3pt;')
250 page.append(' padding-top:2pt;')
251 page.append(' padding-bottom:2pt;')
252 page.append(' margin-top:8pt;')
253 page.append(' margin-bottom:8pt;')
254 page.append(' border-style:none;')
255 page.append(' border-width:thin;')
256 page.append('}')
257 page.append('A:link { text-decoration: none; }')
258 page.append('A:visited { text-decoration: none}')
259 page.append('h1 { font-size: 18pt; }')
260 page.append('h2 { font-size: 14pt; }')
261 page.append('h3 { font-size: 12pt; }')
262 page.append('h4 { font-size: 10pt; }')
263 page.append('h5 { font-size: 8pt; }')
264 page.append('dl,ul { margin-top: 1pt; text-indent: 0 }')
265 page.append('ol { margin-top: 1pt; text-indent: 0 }')
266 page.append('')
267 page.append('tt,pre {')
268 page.append(' font-family: Lucida Console,Courier New,Courier,monotype;')
269 page.append(' font-size: 10pt;')
270 page.append('}')
271 page.append('pre.code {')
272 page.append(' font-family: Lucida Console,Courier New,Courier,monotype;')
273 page.append(' margin-top: 8pt;')
274 page.append(' margin-bottom: 8pt;')
275 page.append(' background-color: #FFFFEE;')
276 page.append(' white-space:pre;')
277 page.append(' border-style:solid;')
278 page.append(' border-width:1pt;')
279 page.append(' border-color:#999999;')
280 page.append(' color:#111111;')
281 page.append(' padding:5px;')
282 page.append('}')
283 page.append('div.date { font-size: 8pt; }')
284 page.append('div.sig { font-size: 8pt; }')
285 page.append('map { ')
286 page.append(' position: relative;')
287 page.append(' text-indent: 0;')
288 page.append('}')
289 page.append('area + span {')
290 page.append(' position: fixed;')
291 page.append(' margin-left: -9999em;')
292 page.append(' background: #00FFFF;')
293 page.append('}')
294 page.append('area:hover + span {')
295 page.append(' display: block;')
296 page.append(' position: fixed;')
297 page.append(' left: 9999em;')
298 page.append(' bottom: 0px;')
299 page.append(' z-index: 99;')
300 page.append(' background: #FFFF00;')
301 page.append(' border-style:solid;')
302 page.append(' border-width:3pt;')
303 page.append(' border-color: #3B3B3B;')
304 page.append(' margin: 2;')
305 page.append(' width: 300px;')
306 page.append(' padding: 5px;')
307 page.append(' font-size: 10pt;')
308 page.append(' font-family: Courier,monotype;')
309 page.append('}')
Steve McIntyreb74ea6b2015-09-24 20:45:31 +0100310 self.wfile.write('\r\n'.join(page))
Steve McIntyre2454bf02015-09-23 18:33:02 +0100311
Steve McIntyre57a9d0a2015-10-28 18:22:31 +0000312 # Send a graphic from our cache
Steve McIntyre2454bf02015-09-23 18:33:02 +0100313 def send_graphic(self):
314 vlan_id = 0
315 vlan_re = re.compile(r'^/images/vlan/(\d+).png$')
316 match = vlan_re.match(self.parsed_path.path)
317 if match:
Steve McIntyre57a9d0a2015-10-28 18:22:31 +0000318 vlan_id = int(match.group(1))
319 cache = self.server.state.cache
320
321 # Do we have a graphic for this VLAN id?
322 if not vlan_id in cache.graphics.keys():
323 logging.debug('asked for vlan_id %s', vlan_id)
324 logging.debug(cache.graphics.keys())
Steve McIntyre2454bf02015-09-23 18:33:02 +0100325 self.send_response(404)
Steve McIntyre4f584a72015-09-28 02:28:56 +0100326 self.wfile.write('Content-type: text/plain\r\n')
Steve McIntyre2454bf02015-09-23 18:33:02 +0100327 self.end_headers()
Steve McIntyre4f584a72015-09-28 02:28:56 +0100328 self.wfile.write('404 Not Found\r\n')
Steve McIntyre0f561cd2015-10-28 18:05:20 +0000329 self.wfile.write('%s' % self.parsed_path.path)
Steve McIntyre4f584a72015-09-28 02:28:56 +0100330 logging.error('VLAN graphic not found - asked for %s', self.parsed_path.path)
Steve McIntyre2454bf02015-09-23 18:33:02 +0100331 return
332
Steve McIntyre57a9d0a2015-10-28 18:22:31 +0000333 # Yes - just send it from the cache
334 self.send_response(200)
335 self.wfile.write('Content-type: image/png\r\n')
336 self.end_headers()
337 self.wfile.write(cache.graphics[vlan_id]['image']['png'].getvalue())
338 return
339
340 # Generate a PNG showing the layout of switches/port/trunks for a
341 # specific VLAN, and return that PNG along with geometry details
342 def generate_graphic(self, vlan_id):
343 db = self.server.state.db
344 vlan = db.get_vlan_by_id(vlan_id)
345 # We've been asked for a VLAN that doesn't exist
346 if vlan is None:
347 return None
348
349 data = {}
350 data['image'] = {}
351 data['ports'] = {}
352
Steve McIntyre2454bf02015-09-23 18:33:02 +0100353 gim = Graphics()
354
Steve McIntyre2454bf02015-09-23 18:33:02 +0100355 # Pick fonts. TODO: Make these configurable?
356 gim.set_font(['/usr/share/fonts/truetype/inconsolata/Inconsolata.otf',
357 '/usr/share/fonts/truetype/freefont/FreeMono.ttf'])
358 try:
359 gim.font
360 # If we can't get the font we need, fail
361 except NameError:
362 self.send_response(500)
Steve McIntyre4f584a72015-09-28 02:28:56 +0100363 self.wfile.write('Content-type: text/plain\r\n')
Steve McIntyre2454bf02015-09-23 18:33:02 +0100364 self.end_headers()
Steve McIntyre4f584a72015-09-28 02:28:56 +0100365 self.wfile.write('500 Internal Server Error\r\n')
366 logging.error('Unable to generate graphic, no fonts found - asked for %s',
367 self.parsed_path.path)
Steve McIntyre2454bf02015-09-23 18:33:02 +0100368 return
369
370 switch = {}
371 size_x = {}
372 size_y = {}
373
374 switches = db.all_switches()
375
376 # Need to set gaps big enough for the number of trunks, at least.
377 trunks = db.all_trunks()
378 y_gap = max(20, 15 * len(trunks))
379 x_gap = max(20, 15 * len(trunks))
380
381 x = 0
382 y = y_gap
383
384 # Work out how much space we need for the switches
385 for i in range(0, len(switches)):
386 ports = db.get_ports_by_switch(switches[i].switch_id)
387 switch[i] = Switch(gim, len(ports), switches[i].name)
388 (size_x[i], size_y[i]) = switch[i].get_dimensions()
389 x = max(x, size_x[i])
390 y += size_y[i] + y_gap
391
392 # Add space for the legend and the label
393 label = "VLAN %d - %s" % (vlan.tag, vlan.name)
394 (legend_width, legend_height) = gim.get_legend_dimensions()
395 (label_width, label_height) = gim.get_label_size(label, gim.label_font_size)
396 x = max(x, legend_width + 2*x_gap + label_width)
397 x = x_gap + x + x_gap
398 y = y + max(legend_height + y_gap, label_height)
399
400 # Create a canvas of the right size
401 gim.create_canvas(x, y)
402
403 # Draw the switches and ports in it
404 curr_y = y_gap
405 for i in range(0, len(switches)):
406 switch[i].draw_switch(gim, x_gap, curr_y)
407 ports = db.get_ports_by_switch(switches[i].switch_id)
Steve McIntyre57a9d0a2015-10-28 18:22:31 +0000408 data['ports'][i] = {}
Steve McIntyre2454bf02015-09-23 18:33:02 +0100409 for port_id in ports:
410 port = db.get_port_by_id(port_id)
Steve McIntyre57a9d0a2015-10-28 18:22:31 +0000411 port_location = switch[i].get_port_location(port.number)
412 data['ports'][i][port.number] = {}
413 data['ports'][i][port.number]['db'] = port
414 data['ports'][i][port.number]['location'] = port_location
Steve McIntyre2454bf02015-09-23 18:33:02 +0100415 if port.is_locked:
416 switch[i].draw_port(gim, port.number, 'locked')
417 elif port.is_trunk:
418 switch[i].draw_port(gim, port.number, 'trunk')
419 elif port.current_vlan_id == int(vlan_id):
420 switch[i].draw_port(gim, port.number, 'VLAN')
421 else:
422 switch[i].draw_port(gim, port.number, 'normal')
423 curr_y += size_y[i] + y_gap
424
425 # Now add the trunks
426 for i in range(0, len(trunks)):
427 ports = db.get_ports_by_trunk(trunks[i].trunk_id)
428 port1 = db.get_port_by_id(ports[0])
429 port2 = db.get_port_by_id(ports[1])
430 for s in range(0, len(switches)):
431 if switches[s].switch_id == port1.switch_id:
432 switch1 = s
433 if switches[s].switch_id == port2.switch_id:
434 switch2 = s
435 gim.draw_trunk(i,
436 switch[switch1].get_port_location(port1.number),
437 switch[switch2].get_port_location(port2.number),
438 gim.port_pallette['trunk']['trace'])
439
440 # And the legend and label
441 gim.draw_legend(x_gap, curr_y)
442 gim.draw_label(x - label_width - 2*x_gap, curr_y, label, int(x_gap / 2))
443
Steve McIntyre57a9d0a2015-10-28 18:22:31 +0000444 # All done - push the image file into the cache for this vlan
445 data['image']['png'] = cStringIO.StringIO()
446 gim.im.writePng(data['image']['png'])
447 data['image']['width'] = x
448 data['image']['height'] = y
449 return data
Steve McIntyre2454bf02015-09-23 18:33:02 +0100450
451 # Implement an HTTP GET handler for the HTTPServer instance
452 def do_GET(self):
453 # Compare the URL path to any of the names we recognise and
454 # call the right generator function if we get a match
455 self.parsed_path = urlparse.urlparse(self.path)
456 for url in self.functionMap:
457 match = re.match(url['re'], self.parsed_path.path)
458 if match:
459 return url['fn'](self)
460
461 # Fall-through for any files we don't recognise
462 self.send_response(404)
Steve McIntyre4f584a72015-09-28 02:28:56 +0100463 self.wfile.write('Content-type: text/plain\r\n')
Steve McIntyre2454bf02015-09-23 18:33:02 +0100464 self.end_headers()
465 self.wfile.write('404 Not Found')
Steve McIntyre0f561cd2015-10-28 18:05:20 +0000466 self.wfile.write('%s' % self.parsed_path.path)
Steve McIntyre4f584a72015-09-28 02:28:56 +0100467 logging.error('File not supported - asked for %s', self.parsed_path.path)
Steve McIntyre2454bf02015-09-23 18:33:02 +0100468 return
469
470 # Override the BaseHTTPRequestHandler log_message() method so we
471 # can log requests properly
Steve McIntyre9ff96bf2015-09-23 18:54:53 +0100472 def log_message(self, fmt, *args):
Steve McIntyre2454bf02015-09-23 18:33:02 +0100473 """Log an arbitrary message. """
Steve McIntyre9ff96bf2015-09-23 18:54:53 +0100474 logging.info('%s %s', self.client_address[0], fmt%args)
Steve McIntyre2454bf02015-09-23 18:33:02 +0100475
476 functionMap = (
Steve McIntyre9ff96bf2015-09-23 18:54:53 +0100477 {'re': r'^/$', 'fn': send_index},
478 {'re': r'^/style.css$', 'fn': send_style},
479 {'re': r'^/images/vlan/(\d+).png$', 'fn': send_graphic}
Steve McIntyre2454bf02015-09-23 18:33:02 +0100480 )