blob: f004f706cd80178173cc6887ebb8df1aaf519309 [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,
Steve McIntyreea343aa2015-10-23 17:46:17 +010062 username=self.state.config.database.username,
63 readonly=True)
Steve McIntyre2454bf02015-09-23 18:33:02 +010064
65 loglevel = VlanUtil().set_logging_level(self.state.config.logging.level)
66
67 # Should we log to stderr?
68 if self.state.config.logging.filename is None:
69 logging.basicConfig(level = loglevel,
70 format = '%(asctime)s %(levelname)-8s %(message)s')
71 else:
72 logging.basicConfig(level = loglevel,
73 format = '%(asctime)s %(levelname)-8s VIS %(message)s',
74 datefmt = '%Y-%m-%d %H:%M:%S %Z',
75 filename = self.state.config.logging.filename,
76 filemode = 'a')
77 logging.info('%s visualisation starting up', self.state.banner)
78
Steve McIntyre86916e42015-09-28 02:39:32 +010079 server = VlandHTTPServer(('', self.state.config.visualisation.port),
Steve McIntyre2454bf02015-09-23 18:33:02 +010080 GetHandler, self.state)
81 server.serve_forever()
82
83 # Kill the webserver
84 def shutdown(self):
85 self.p.terminate()
86
87class GetHandler(BaseHTTPRequestHandler):
88 """ Methods to generate and serve the pages """
89
90 parsed_path = None
91
92 # Trivial top-level page. Link to images for each of the VLANs we
93 # know about.
94 def send_index(self):
95 self.send_response(200)
96 self.wfile.write('Content-type: text/html\r\n')
97 self.end_headers()
Steve McIntyreb0aa4602015-10-08 15:33:28 +010098 config = self.server.state.config.visualisation
Steve McIntyre2454bf02015-09-23 18:33:02 +010099 page = []
100 page.append('<html>')
101 page.append('<head>')
102 page.append('<TITLE>VLANd visualisation</TITLE>')
Steve McIntyreb74ea6b2015-09-24 20:45:31 +0100103 page.append('<link rel="stylesheet" type="text/css" href="style.css">')
Steve McIntyreb0aa4602015-10-08 15:33:28 +0100104 if config.refresh and config.refresh > 0:
105 page.append('<meta http-equiv="refresh" content="%d">' % config.refresh)
Steve McIntyre2454bf02015-09-23 18:33:02 +0100106 page.append('</HEAD>')
107 page.append('<body>')
Steve McIntyre2454bf02015-09-23 18:33:02 +0100108 switches = self.server.state.db.all_switches()
Steve McIntyreb74ea6b2015-09-24 20:45:31 +0100109 vlans = self.server.state.db.all_vlans()
110 page.append('<div class="menu">')
111 if len(switches) > 0:
112 page.append('<h2>Menu</h2>')
113 page.append('<p>VLANs: %d</p>' % len(vlans))
114 page.append('<ul>')
115 for vlan in vlans:
116 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))
117 page.append('</ul>')
118 page.append('<div class="date"><p>Current time: %s</p>' % datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC"))
119 page.append('<p>version %s</p>' % self.server.state.version)
120 page.append('</div>')
121 page.append('</div>')
122
123 page.append('<div class="content">')
124 page.append('<h1>VLANd visualisation</h1>')
125
Steve McIntyre2454bf02015-09-23 18:33:02 +0100126 if len(switches) == 0:
127 page.append('<p>No switches found in the database, nothing to show...</p>')
128 else:
Steve McIntyre2454bf02015-09-23 18:33:02 +0100129 for vlan in vlans:
Steve McIntyreb74ea6b2015-09-24 20:45:31 +0100130 page.append('<a name="vlan%d"></a>' % vlan.vlan_id)
Steve McIntyre2454bf02015-09-23 18:33:02 +0100131 page.append('<h3>VLAN id %d, tag %d, name %s</h3>' % (vlan.vlan_id, vlan.tag, vlan.name))
132 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))
Steve McIntyreb74ea6b2015-09-24 20:45:31 +0100133 page.append('<hr>')
Steve McIntyre2454bf02015-09-23 18:33:02 +0100134
Steve McIntyreb74ea6b2015-09-24 20:45:31 +0100135 page.append('</div>')
Steve McIntyre2454bf02015-09-23 18:33:02 +0100136 page.append('</body>')
137 self.wfile.write('\r\n'.join(page))
138
139 # Trivial style sheet, TODO!
140 def send_style(self):
141 self.send_response(200)
142 self.wfile.write('Content-type: text/css\r\n')
143 self.end_headers()
Steve McIntyreb74ea6b2015-09-24 20:45:31 +0100144 page = []
145 page.append("body {")
146 page.append(" background: white;")
147 page.append(" color: black;")
148 page.append(" font-size: 12pt;")
149 page.append("}")
150 page.append("")
151 page.append(".menu {")
152 page.append(" position:fixed;")
153 page.append(" float:left;")
154 page.append(" font-family: arial, Helvetica, sans-serif;")
155 page.append(" width:20%;")
156 page.append(" height:100%;")
157 page.append(" font-size: 10pt;")
158 page.append(" padding-top: 10px;")
159 page.append("}")
160 page.append("")
161 page.append(".content {")
162 page.append(" padding-top: 10px;")
163 page.append(" width:80%;")
164 page.append(" max-width:80%;")
165 page.append(" margin-left: 21%;")
166 page.append(" margin-top: 50px;")
167 page.append(" height:100%;")
168 page.append("}")
169 page.append("")
170 page.append(".footer {")
171 page.append(" vertical-align: bottom;")
172 page.append(" text-align: left;")
173 page.append("}")
174 page.append("")
175 page.append(".caption {")
176 page.append(" padding-top: 1px;")
177 page.append(" padding-left: 10%;")
178 page.append(" padding-right: 10%;")
179 page.append(" font-size: 8pt;")
180 page.append(" font-style: italic;")
181 page.append(" text-align: center;")
182 page.append("}")
183 page.append("")
184 page.append("td.headline {")
185 page.append(" font-family: arial, Helvetica, sans-serif;")
186 page.append(" font-size: 20pt;")
187 page.append("}")
188 page.append("h1,h2,h3,h4,h5 {")
189 page.append(" font-family: arial, Helvetica, sans-serif;")
190 page.append(" padding-right:3pt;")
191 page.append(" padding-top:2pt;")
192 page.append(" padding-bottom:2pt;")
193 page.append(" margin-top:8pt;")
194 page.append(" margin-bottom:8pt;")
195 page.append(" border-style:none;")
196 page.append(" border-width:thin;")
197 page.append("}")
198 page.append("")
199 page.append("A:link { text-decoration: none; }")
200 page.append("A:visited { text-decoration: none}")
201 page.append("")
202 page.append("h1 { font-size: 18pt; }")
203 page.append("h2 { font-size: 14pt; }")
204 page.append("h3 { font-size: 12pt; }")
205 page.append("h4 { font-size: 10pt; }")
206 page.append("h5 { font-size: 8pt; }")
207 page.append("dl,ul { margin-top: 1pt; text-indent: 0 }")
208 page.append("ol { margin-top: 1pt; text-indent: 0 }")
209 page.append("")
210 page.append("tt,pre {")
211 page.append(" font-family: Lucida Console,Courier New,Courier,monotype;")
212 page.append(" font-size: 10pt;")
213 page.append("}")
214 page.append("")
215 page.append("pre.code {")
216 page.append(" font-family: Lucida Console,Courier New,Courier,monotype;")
217 page.append(" margin-top: 8pt;")
218 page.append(" margin-bottom: 8pt;")
219 page.append(" background-color: #FFFFEE;")
220 page.append(" white-space:pre;")
221 page.append(" border-style:solid;")
222 page.append(" border-width:1pt;")
223 page.append(" border-color:#999999;")
224 page.append(" color:#111111;")
225 page.append(" padding:5px;")
226 page.append("}")
227 page.append("")
228 page.append("div.date {")
229 page.append(" font-size: 8pt;")
230 page.append("}")
231 page.append("")
232 page.append("div.sig {")
233 page.append(" font-size: 8pt;")
234 page.append("}")
235 page.append("")
236 self.wfile.write('\r\n'.join(page))
Steve McIntyre2454bf02015-09-23 18:33:02 +0100237
238 # Generate a PNG showing the layout of switches/port/trunks for a
239 # specific VLAN
240 def send_graphic(self):
241 vlan_id = 0
242 vlan_re = re.compile(r'^/images/vlan/(\d+).png$')
243 match = vlan_re.match(self.parsed_path.path)
244 if match:
245 vlan_id = match.group(1)
246 db = self.server.state.db
247 vlan = db.get_vlan_by_id(vlan_id)
248 # We've been asked for a VLAN that doesn't exist
249 if vlan is None:
250 self.send_response(404)
Steve McIntyre4f584a72015-09-28 02:28:56 +0100251 self.wfile.write('Content-type: text/plain\r\n')
Steve McIntyre2454bf02015-09-23 18:33:02 +0100252 self.end_headers()
Steve McIntyre4f584a72015-09-28 02:28:56 +0100253 self.wfile.write('404 Not Found\r\n')
254 logging.error('VLAN graphic not found - asked for %s', self.parsed_path.path)
Steve McIntyre2454bf02015-09-23 18:33:02 +0100255 return
256
257 gim = Graphics()
258
Steve McIntyre2454bf02015-09-23 18:33:02 +0100259 # Pick fonts. TODO: Make these configurable?
260 gim.set_font(['/usr/share/fonts/truetype/inconsolata/Inconsolata.otf',
261 '/usr/share/fonts/truetype/freefont/FreeMono.ttf'])
262 try:
263 gim.font
264 # If we can't get the font we need, fail
265 except NameError:
266 self.send_response(500)
Steve McIntyre4f584a72015-09-28 02:28:56 +0100267 self.wfile.write('Content-type: text/plain\r\n')
Steve McIntyre2454bf02015-09-23 18:33:02 +0100268 self.end_headers()
Steve McIntyre4f584a72015-09-28 02:28:56 +0100269 self.wfile.write('500 Internal Server Error\r\n')
270 logging.error('Unable to generate graphic, no fonts found - asked for %s',
271 self.parsed_path.path)
Steve McIntyre2454bf02015-09-23 18:33:02 +0100272 return
273
274 switch = {}
275 size_x = {}
276 size_y = {}
277
278 switches = db.all_switches()
279
280 # Need to set gaps big enough for the number of trunks, at least.
281 trunks = db.all_trunks()
282 y_gap = max(20, 15 * len(trunks))
283 x_gap = max(20, 15 * len(trunks))
284
285 x = 0
286 y = y_gap
287
288 # Work out how much space we need for the switches
289 for i in range(0, len(switches)):
290 ports = db.get_ports_by_switch(switches[i].switch_id)
291 switch[i] = Switch(gim, len(ports), switches[i].name)
292 (size_x[i], size_y[i]) = switch[i].get_dimensions()
293 x = max(x, size_x[i])
294 y += size_y[i] + y_gap
295
296 # Add space for the legend and the label
297 label = "VLAN %d - %s" % (vlan.tag, vlan.name)
298 (legend_width, legend_height) = gim.get_legend_dimensions()
299 (label_width, label_height) = gim.get_label_size(label, gim.label_font_size)
300 x = max(x, legend_width + 2*x_gap + label_width)
301 x = x_gap + x + x_gap
302 y = y + max(legend_height + y_gap, label_height)
303
304 # Create a canvas of the right size
305 gim.create_canvas(x, y)
306
307 # Draw the switches and ports in it
308 curr_y = y_gap
309 for i in range(0, len(switches)):
310 switch[i].draw_switch(gim, x_gap, curr_y)
311 ports = db.get_ports_by_switch(switches[i].switch_id)
312 for port_id in ports:
313 port = db.get_port_by_id(port_id)
314 if port.is_locked:
315 switch[i].draw_port(gim, port.number, 'locked')
316 elif port.is_trunk:
317 switch[i].draw_port(gim, port.number, 'trunk')
318 elif port.current_vlan_id == int(vlan_id):
319 switch[i].draw_port(gim, port.number, 'VLAN')
320 else:
321 switch[i].draw_port(gim, port.number, 'normal')
322 curr_y += size_y[i] + y_gap
323
324 # Now add the trunks
325 for i in range(0, len(trunks)):
326 ports = db.get_ports_by_trunk(trunks[i].trunk_id)
327 port1 = db.get_port_by_id(ports[0])
328 port2 = db.get_port_by_id(ports[1])
329 for s in range(0, len(switches)):
330 if switches[s].switch_id == port1.switch_id:
331 switch1 = s
332 if switches[s].switch_id == port2.switch_id:
333 switch2 = s
334 gim.draw_trunk(i,
335 switch[switch1].get_port_location(port1.number),
336 switch[switch2].get_port_location(port2.number),
337 gim.port_pallette['trunk']['trace'])
338
339 # And the legend and label
340 gim.draw_legend(x_gap, curr_y)
341 gim.draw_label(x - label_width - 2*x_gap, curr_y, label, int(x_gap / 2))
342
343 # All done - send it down the http socket
344 self.send_response(200)
345 self.wfile.write('Content-type: image/png\r\n')
346 self.end_headers()
347 gim.im.writePng(self.wfile)
348
349 # Implement an HTTP GET handler for the HTTPServer instance
350 def do_GET(self):
351 # Compare the URL path to any of the names we recognise and
352 # call the right generator function if we get a match
353 self.parsed_path = urlparse.urlparse(self.path)
354 for url in self.functionMap:
355 match = re.match(url['re'], self.parsed_path.path)
356 if match:
357 return url['fn'](self)
358
359 # Fall-through for any files we don't recognise
360 self.send_response(404)
Steve McIntyre4f584a72015-09-28 02:28:56 +0100361 self.wfile.write('Content-type: text/plain\r\n')
Steve McIntyre2454bf02015-09-23 18:33:02 +0100362 self.end_headers()
363 self.wfile.write('404 Not Found')
Steve McIntyre4f584a72015-09-28 02:28:56 +0100364 logging.error('File not supported - asked for %s', self.parsed_path.path)
Steve McIntyre2454bf02015-09-23 18:33:02 +0100365 return
366
367 # Override the BaseHTTPRequestHandler log_message() method so we
368 # can log requests properly
Steve McIntyre9ff96bf2015-09-23 18:54:53 +0100369 def log_message(self, fmt, *args):
Steve McIntyre2454bf02015-09-23 18:33:02 +0100370 """Log an arbitrary message. """
Steve McIntyre9ff96bf2015-09-23 18:54:53 +0100371 logging.info('%s %s', self.client_address[0], fmt%args)
Steve McIntyre2454bf02015-09-23 18:33:02 +0100372
373 functionMap = (
Steve McIntyre9ff96bf2015-09-23 18:54:53 +0100374 {'re': r'^/$', 'fn': send_index},
375 {'re': r'^/style.css$', 'fn': send_style},
376 {'re': r'^/images/vlan/(\d+).png$', 'fn': send_graphic}
Steve McIntyre2454bf02015-09-23 18:33:02 +0100377 )