blob: fb861ea7cb35638d985d111c7082706257ca492a [file] [log] [blame]
#! /usr/bin/python
# Copyright 2015 Linaro Limited
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
#
# Visualisation graphics module for VLANd
#
# This code uses python-gd to generate graphics ready for insertion
# into our web interface. Example code in the self-test at the
# bottom.
import gd, os, sys
if __name__ == '__main__':
vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0])))
sys.path.insert(0, vlandpath)
sys.path.insert(0, "%s/.." % vlandpath)
from errors import InputError
class Graphics:
""" Code and config for the visualisation graphics module """
font = None
# Default font size for the small labels
small_font_size = 12
# And the size for the top-level label
label_font_size = 24
# Size in pixels of that font, calculated later
twocharwidth = 0
charheight = 0
# How big a gap to leave between trunk connections
trunk_gap = 8
# Details of the legend
legend_width = 0
legend_height = 0
legend_text_width = 0
legend_text_height = 0
legend_total_width = 0
# Basic colour definitions used later
colour_defs = {}
colour_defs['black'] = (0, 0, 0)
colour_defs['white'] = (255, 255, 255)
colour_defs['purple'] = (255, 0, 255)
colour_defs['blue'] = (0, 0, 255)
colour_defs['darkgrey'] = (60, 60, 60)
colour_defs['yellow'] = (255, 255, 0)
colour_defs['red'] = (255, 0, 0)
colour_defs['aqua'] = (0, 255, 255)
pallette = {}
# colours for the background
pallette['bg_colour'] = 'purple'
pallette['transparent_colour'] = 'purple'
pallette['graphic_label_colour'] = 'black'
# switch colours
pallette['switch_outline_colour'] = 'black'
pallette['switch_fill_colour'] = 'darkgrey'
pallette['switch_label_colour'] = 'white'
# verious sets of port colours, matching the 'highlight' options in
# draw_port()
port_pallette = {}
port_pallette['normal'] = {}
port_pallette['normal']['port_box'] = 'white'
port_pallette['normal']['port_bg'] = 'black'
port_pallette['normal']['port_label'] = 'white'
port_pallette['normal']['trace'] = 'black'
port_pallette['trunk'] = {}
port_pallette['trunk']['port_box'] = 'white'
port_pallette['trunk']['port_bg'] = 'blue'
port_pallette['trunk']['port_label'] = 'yellow'
port_pallette['trunk']['trace'] = 'blue'
port_pallette['locked'] = {}
port_pallette['locked']['port_box'] = 'white'
port_pallette['locked']['port_bg'] = 'red'
port_pallette['locked']['port_label'] = 'yellow'
port_pallette['locked']['trace'] = 'red'
port_pallette['VLAN'] = {}
port_pallette['VLAN']['port_box'] = 'white'
port_pallette['VLAN']['port_bg'] = 'aqua'
port_pallette['VLAN']['port_label'] = 'black'
port_pallette['VLAN']['trace'] = 'aqua'
im = None
# TODO: make colours configurable, add maybe parsing for
# /etc/X11/rgb.txt to allow people to use arbitrary names?
# Choose a font for our graphics to use. Pass in a list of fonts
# to be tried, in priority order.
def set_font(self, fontlist):
for font in fontlist:
if os.path.exists(font):
self.font = os.path.abspath(font)
break
# Work out how big we need to be for the biggest possible text
# in a 2-digit number. Grotty, but we need to know this later.
for value in range (0, 100):
(width, height) = self.get_label_size(repr(value), self.small_font_size)
self.twocharwidth = max(self.twocharwidth, width)
self.charheight = max(self.charheight, height)
# Now we can also calulate other stuff
self._calc_legend_size()
# Create a canvas and set things up ready for use
def create_canvas(self, x, y):
im = gd.image((x, y))
# Allocate our colours in the image's colour map
for key in self.colour_defs.iterkeys():
im.colorAllocate((self.colour_defs[key][0],
self.colour_defs[key][1],
self.colour_defs[key][2]))
im.fill((0,0), im.colorExact(self.colour_defs[self.pallette['bg_colour']]))
im.colorTransparent(im.colorExact(self.colour_defs[self.pallette['transparent_colour']]))
im.interlace(0)
self.im = im
# Using our selected font, what dimensions will a particular piece
# of text take?
def get_label_size(self, label, font_size):
tmp_im = gd.image((200, 200))
(llx, lly, lrx, lry, urx, ury, ulx, uly) = tmp_im.get_bounding_rect(self.font,
font_size,
0.0,
(10, 100), label)
width = max(lrx, urx) - min(llx, ulx)
height = max(lly, lry) - min(uly, ury)
return (width, height)
# Draw a trunk connection between two ports
#
# Ports are defined as (x, y, top): x, y co-ordinates and whether
# the port is on the top or bottom row of a switch, i.e. does the
# wire come up or down when it leaves the port.
def draw_trunk(self, trunknum, node1, node2, colour):
for node in (node1, node2):
(x1,y1,top) = node
x2 = x1
if (top):
y2 = y1 - (self.trunk_gap * (trunknum + 1))
else:
y2 = y1 + (self.trunk_gap * (trunknum + 1))
# Quick hack - use 2-pixel wide rectangles as thick lines :-)
# First line, vertically up/down from the port
self.im.rectangle((x1-1,y1), (x2,y2), self.im.colorExact(self.colour_defs[colour]))
# Now draw horizontally across to the left margin space
x3 = self.trunk_gap * (trunknum + 1)
self.im.rectangle((x3, y2), (x2,y2+1), self.im.colorExact(self.colour_defs[colour]))
# Now join up the trunks vertically
(x1,y1,top1) = node1
if (top1):
y1 -= self.trunk_gap * (trunknum + 1)
else:
y1 += self.trunk_gap * (trunknum + 1)
(x2,y2,top2) = node2
if (top2):
y2 -= self.trunk_gap * (trunknum + 1)
else:
y2 += self.trunk_gap * (trunknum + 1)
x1 = self.trunk_gap * (trunknum + 1)
self.im.rectangle((x1, y1), (x1+1,y2), self.im.colorExact(self.colour_defs[colour]))
# How big is the legend?
def _calc_legend_size(self):
max_width = 0
max_height = 0
for value in self.port_pallette.iterkeys():
(width, height) = self.get_label_size(value, self.small_font_size)
max_width = max(max_width, width)
max_height = max(max_height, height)
(width, height) = self.get_label_size('##', self.small_font_size)
self.legend_box_width = width + 6
self.legend_box_height = height + 6
self.legend_width = max_width + self.legend_box_width + 10
self.legend_height = 3 + self.legend_box_height + 3
self.legend_text_width = max_width
self.legend_text_height = max_height
self.legend_total_width = 6 + (len(self.port_pallette) * self.legend_width)
# Return the legend dimensions
def get_legend_dimensions(self):
return (self.legend_total_width, self.legend_height)
# Draw the legend using (left, top) as the top left corner
def draw_legend(self, left, top):
lrx = left + self.legend_total_width - 1
lry = top + self.legend_height - 1
self.im.rectangle((left, top), (lrx, lry),
self.im.colorExact(self.colour_defs[self.pallette['switch_outline_colour']]),
self.im.colorExact(self.colour_defs[self.pallette['switch_fill_colour']]))
curr_x = left + 3
curr_y = top + 3
for value in sorted(self.port_pallette):
box_colour = self.im.colorExact(self.colour_defs[self.port_pallette[value]['port_box']])
box_bg_colour = self.im.colorExact(self.colour_defs[self.port_pallette[value]['port_bg']])
text_colour = self.im.colorExact(self.colour_defs[self.port_pallette[value]['port_label']])
lrx = curr_x + self.legend_box_width - 1
lry = curr_y + self.legend_box_height - 1
self.im.rectangle((curr_x,curr_y), (lrx,lry), box_colour, box_bg_colour)
llx = curr_x + 4
lly = curr_y + self.legend_box_height - 4
self.im.string_ttf(self.font, self.small_font_size, 0.0, (llx, lly), '##', text_colour)
curr_x += self.legend_box_width
self.im.string_ttf(self.font, self.small_font_size, 0.0, (curr_x + 3, lly), value,
self.im.colorExact(self.colour_defs[self.pallette['switch_label_colour']]))
curr_x += self.legend_text_width + 10
# Draw the graphic's label using (left, top) as the top left
# corner with a box around
def draw_label(self, left, top, label, gap):
box_colour = self.im.colorExact(self.colour_defs[self.pallette['switch_label_colour']])
box_bg_colour = self.im.colorExact(self.colour_defs[self.pallette['switch_fill_colour']])
text_colour = self.im.colorExact(self.colour_defs[self.pallette['switch_label_colour']])
(width, height) = self.get_label_size(label, self.label_font_size)
curr_x = left
curr_y = top
lrx = curr_x + width + gap
lry = curr_y + height + 20
self.im.rectangle((curr_x,curr_y), (lrx,lry), box_colour, box_bg_colour)
curr_x = left + 10
curr_y = top + height + 6
self.im.string_ttf(self.font, self.label_font_size, 0.0, (curr_x, curr_y), label, text_colour)
class Switch:
""" Code and config for dealing with a switch """
port_width = 0
port_height = 0
text_width = 0
text_height = 0
label_left = 0
label_bot = 0
total_width = 0
total_height = 0
num_ports = 0
left = None
top = None
name = None
# Set up a new switch instance; calculate all the sizes so we can
# size our canvas
def __init__(self, g, num_ports, name):
self.num_ports = num_ports
self.name = name
self._calc_port_size(g)
self._calc_switch_size(g)
# How big is a port and the text within it?
def _calc_port_size(self, g):
max_width = 0
max_height = 0
self.text_width = g.twocharwidth
self.text_height = g.charheight
# Leave enough space around the text for a nice clear box
self.port_width = self.text_width + 6
self.port_height = self.text_height + 6
# How big is the full switch, including all the ports and the
# switch name label?
def _calc_switch_size(self, g):
(label_width, label_height) = g.get_label_size(self.name, g.small_font_size)
num_ports = self.num_ports
# Make sure we have an even number for 2 rows
if (self.num_ports & 1):
num_ports += 1
self.label_left = 3 + (num_ports * self.port_width / 2) + 3
self.label_bot = self.port_height - 2
self.total_width = self.label_left + label_width + 3
self.total_height = 3 + max(label_height, (2 * self.port_height)) + 3
# Return the switch dimensions
def get_dimensions(self):
return (self.total_width, self.total_height)
# Draw the basic switch outline and label using (left, top) as the
# top left corner. The switch object will remember this origin for
# later use when drawing ports.
def draw_switch(self, g, left, top):
self.left = left
self.top = top
ulx = left
uly = top
lrx = left + self.total_width -1
lry = top + self.total_height - 1
g.im.rectangle((left, top), (lrx, lry),
g.im.colorExact(g.colour_defs[g.pallette['switch_outline_colour']]),
g.im.colorExact(g.colour_defs[g.pallette['switch_fill_colour']]))
llx = left + self.label_left
lly = top + self.label_bot
g.im.string_ttf(g.font, g.small_font_size, 0.0, (llx, lly), self.name,
g.im.colorExact(g.colour_defs[g.pallette['switch_label_colour']]))
# Draw a port inside the switch, using a specified colour scheme
# to denote its type. The switch outline must have been drawn
# first, for its origin to be set.
def draw_port(self, g, portnum, highlight):
if portnum < 1 or portnum > self.num_ports:
raise InputError('port number out of range')
if not self.left or not self.top:
raise InputError('cannot draw ports before switch is drawn')
if highlight not in g.port_pallette.iterkeys():
raise InputError('unknown highlight type \"%s\"' % highlight)
box_colour = g.im.colorExact(g.colour_defs[g.port_pallette[highlight]['port_box']])
box_bg_colour = g.im.colorExact(g.colour_defs[g.port_pallette[highlight]['port_bg']])
text_colour = g.im.colorExact(g.colour_defs[g.port_pallette[highlight]['port_label']])
if (portnum & 1): # odd port number, so top row
ulx = self.left + 3 + ((portnum-1) * self.port_width / 2)
uly = self.top + 3
else: # bottom row
ulx = self.left + 3 + ((portnum-2) * self.port_width / 2)
uly = self.top + 3 + self.port_height
lrx = ulx + self.port_width - 1
lry = uly + self.port_height - 1
g.im.rectangle((ulx,uly), (lrx,lry), box_colour, box_bg_colour)
# centre the text
(width, height) = g.get_label_size(repr(portnum), g.small_font_size)
llx = ulx + 3 + (self.text_width - width) / 2
lly = uly + max(height, self.text_height) + 1
g.im.string_ttf(g.font, g.small_font_size,
0.0, (llx, lly), repr(portnum), text_colour)
# Quick helper: draw all the ports for a switch in the default
# colour scheme.
def draw_default_ports(self, g):
for portnum in range(1, self.num_ports + 1):
self.draw_port(g, portnum, 'normal')
# Get the (x,y) co-ordinates of the middle of the external edge of
# the port box (i.e. top of a top-row port, bottom of a bottom-row
# port) so that we can draw a connection to that point
def get_port_location(self, portnum):
if portnum > self.num_ports:
raise InputError('port number out of range')
if (portnum & 1): # odd port number, so top row
ulx = self.left + 3 + ((portnum-1) * self.port_width / 2)
uly = self.top + 3
mid_edge = ulx + int((self.port_width / 2))
return (mid_edge, uly, True)
else: # bottom row
ulx = self.left + 3 + ((portnum-2) * self.port_width / 2)
uly = self.top + 3 + self.port_height
lry = uly + self.port_height
mid_edge = ulx + int((self.port_width / 2))
return (mid_edge, lry, False)
# Debug: print some of the state of the switch object
def dump_state(self):
print 'port_width %d' % self.port_width
print 'port_height %d' % self.port_height
print 'text_width %d' % self.text_width
print 'text_height %d' % self.text_height
print 'label_left %d' % self.label_left
print 'label_bot %d' % self.label_bot
print 'total_width %d' % self.total_width
print 'total_height %d' % self.total_height
# Test harness - generate a PNG using fake data
if __name__ == '__main__':
gim = Graphics()
gim.set_font(['/usr/share/fonts/truetype/inconsolata/Inconsolata.otf',
'/usr/share/fonts/truetype/freefont/FreeMono.ttf'])
try:
gim.font
except NameError:
print 'no fonts found'
sys.exit(1)
switch = {}
size_x = {}
size_y = {}
switch[0] = Switch(gim, 48, 'lngswitch01')
switch[1] = Switch(gim, 24, 'lngswitch02')
switch[2] = Switch(gim, 52, 'lngswitch03')
label = "VLAN 4jj"
# Need to set gaps big enough for the number of trunks, at least.
num_trunks = 3
y_gap = max(20, 15 * num_trunks)
x_gap = max(20, 15 * num_trunks)
x = 0
y = y_gap
for i in range (0, 3):
(size_x[i], size_y[i]) = switch[i].get_dimensions()
x = max(x, size_x[i])
y += size_y[i] + y_gap
# Add space for the legend and the label
(legend_width, legend_height) = gim.get_legend_dimensions()
(label_width, label_height) = gim.get_label_size(label, gim.label_font_size)
x = max(x, legend_width + 2*x_gap + label_width)
x = x_gap + x + x_gap
y = y + max(legend_height + y_gap, label_height)
gim.create_canvas(x, y)
curr_y = y_gap
switch[0].draw_switch(gim, x_gap, curr_y)
switch[0].draw_default_ports(gim)
switch[0].draw_port(gim, 2, 'VLAN')
switch[0].draw_port(gim, 5, 'locked')
switch[0].draw_port(gim, 11, 'trunk')
switch[0].draw_port(gim, 44, 'trunk')
curr_y += size_y[0] + y_gap
switch[1].draw_switch(gim, x_gap, curr_y)
switch[1].draw_default_ports(gim)
switch[1].draw_port(gim, 5, 'VLAN')
switch[1].draw_port(gim, 8, 'locked')
switch[1].draw_port(gim, 13, 'trunk')
switch[1].draw_port(gim, 16, 'trunk')
curr_y += size_y[2] + y_gap
switch[2].draw_switch(gim, x_gap, curr_y)
switch[2].draw_default_ports(gim)
switch[2].draw_port(gim, 1, 'trunk')
switch[2].draw_port(gim, 2, 'locked')
switch[2].draw_port(gim, 14, 'trunk')
switch[2].draw_port(gim, 19, 'VLAN')
curr_y += size_y[2] + y_gap
# Now let's try and draw some trunks!
gim.draw_trunk(0,
switch[0].get_port_location(11),
switch[1].get_port_location(16),
gim.port_pallette['trunk']['trace'])
gim.draw_trunk(1,
switch[1].get_port_location(13),
switch[2].get_port_location(1),
gim.port_pallette['trunk']['trace'])
gim.draw_trunk(2,
switch[0].get_port_location(44),
switch[2].get_port_location(14),
gim.port_pallette['trunk']['trace'])
gim.draw_legend(x_gap, curr_y)
gim.draw_label(x - label_width - 2*x_gap, curr_y, label, int(x_gap / 2))
f=open('xx.png','w')
gim.im.writePng(f)
f.close()
print 'Test graphic written to xx.png'