Source code for sts.gui.view

# Copyright 2011-2013 Colin Scott
# Copyright 2012-2013 Andrew Or
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at:
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

'''
Graphical representation of STS topology switch, host and link entities
'''
from sts.entities import Host as STSHost, HostInterface as STSHostInterface, \
                         FuzzSoftwareSwitch as STSSwitch, AccessLink as STSAccessLink, \
                         Link as STSNetworkLink
from sts.gui.entities import GuiNode, GuiHost, GuiSwitch, GuiLink
from pox.lib.addresses import IPAddr, EthAddr
from pox.openflow.libopenflow_01 import ofp_phy_port

import os
import logging
import math
import json
from random import randint
from threading import Timer
from PyQt4 import QtGui, QtCore

log = logging.getLogger("sts.gui")

[docs]class TopologyView(QtGui.QGraphicsView): ''' QGraphicsView that provides a graphical representation of STS topology '''
[docs] def __init__(self, sts_topology, parent=None, sync_period=2.0, debugging=False): QtGui.QGraphicsView.__init__(self, parent) self.topology_scene = QtGui.QGraphicsScene(self) self.topology_scene.setItemIndexMethod(QtGui.QGraphicsScene.NoIndex) self.topology_scene.setSceneRect(-300, -300, 600, 600) self.setScene(self.topology_scene) self.setRenderHint(QtGui.QPainter.Antialiasing) self.setStyleSheet("background: black") self.scale(0.9, 0.9) self.setMinimumSize(400, 400) # Node boundaries self.minX, self.maxX = -300, 300 self.minY, self.maxY = -200, 200 # Cursor self.setDragMode(self.ScrollHandDrag) self.setCursor(QtCore.Qt.ArrowCursor) # Syncer updates topology scene to contain entities matching those in STS self.syncer = STSSyncer(sts_topology, self, sync_period, debugging)
[docs] def mouseReleaseEvent(self, event): ''' Show context menu when right-clicking on empty space on the scene. ''' if not self.itemAt(event.pos()): if event.button() == QtCore.Qt.RightButton: popup = QtGui.QMenu() popup.addAction("Turn %s Debug Messages" % ("Off" if self.syncer.debugging else "On"), self.syncer.toggle_debug_messages) popup.addAction("Load State", self.syncer.load_state) popup.addAction("Save State", self.syncer.save_state) popup.addAction("Create Switch", self.syncer.create_switch) popup.exec_(event.globalPos()) QtGui.QGraphicsView.mouseReleaseEvent(self, event)
[docs]class STSSyncer: ''' Container of Node and Link objects in GUI that periodically syncs with STS Topology '''
[docs] def __init__(self, sts_topology, topology_view, sync_period=2.0, debugging=True): self.dpid2switch = {} self.hid2host = {} self.access_links = [] self.network_links = [] self.sts_topology = sts_topology self.topology_view = topology_view self.topology_scene = topology_view.topology_scene self.debugging = debugging # Sync with STS topology self.sts_topology = sts_topology self.sync_period = sync_period self.mismatch_count = 0 self.sync_with_sts()
@property
[docs] def switches(self): return self.dpid2switch.values()
@property
[docs] def hosts(self): return self.hid2host.values()
@property
[docs] def dpids(self): return self.dpid2switch.keys()
@property
[docs] def hids(self): return self.hid2host.keys()
@property
[docs] def debug(self, msg): if self.debugging: print "sts.gui:%s" % msg #TODO: Change back to log.debug(msg)
[docs] def toggle_debug_messages(self): self.debugging = not self.debugging
[docs] def synced_with_sts(self): ''' Return True if GUI and STS share the same map of nodes and links ''' self.debug("\n================== GUI Sync Check ==================" + "\n\tSwitches: \tSTS = %s, Gui = %s" % (len(self.sts_topology.switches), len(self.switches)) + "\n\tHosts: \tSTS = %s, Gui = %s" % (len(self.sts_topology.hosts), len(self.hosts)) + "\n\tAccess Link: \tSTS = %s, Gui = %s" % (len(self.sts_topology.access_links), len(self.access_links)) + "\n\tNetwork Link: \tSTS = %s, Gui = %s" % (len(self.sts_topology.network_links), len(self.network_links)) + "\n====================================================\n") if len(self.switches) != len(self.sts_topology.switches) or\ len(self.hosts) != len(self.sts_topology.hosts) or\ len(self.access_links) != len(self.sts_topology.access_links) or\ len(self.network_links) != len(self.sts_topology.network_links): return False for hid in self.sts_topology.hid2host.keys(): if hid not in self.hids: return False for dpid in self.sts_topology.dpid2switch.keys(): if dpid not in self.dpids: return False for hid in self.hids: if hid not in self.sts_topology.hid2host.keys(): return False for dpid in self.dpids: if dpid not in self.sts_topology.dpid2switch.keys(): return False return True
[docs] def sync_with_sts(self): ''' Resync all network elements if STS and GUI are persistently desynchronized ''' if self.sts_topology is None or self.synced_with_sts(): Timer(self.sync_period, self.sync_with_sts).start() self.mismatch_count = 0 return self.mismatch_count += 1 # If inconsistency between STS and GUI is persistent, resync if self.mismatch_count >= 2: self.mismatch_count = 0 # Remove all links in GUI for link in self.links: link.source.linkList.remove(link) link.dest.linkList.remove(link) if link in self.topology_scene.items(): self.topology_scene.removeItem(link) self.access_links = [] self.network_links = [] # Remove switches found in GUI but not STS for dpid, switch in self.dpid2switch.items(): if dpid not in self.sts_topology.dpid2switch.keys(): if switch in self.topology_scene.items(): self.topology_scene.removeItem(switch) del self.dpid2switch[dpid] self.debug("--- Removing switch with dpid = %d" % dpid) # Remove hosts found in GUI but not STS for hid, host in self.hid2host.items(): if hid not in self.sts_topology.hid2host.keys(): if host in self.topology_scene.items(): self.topology_scene.removeItem(host) del self.hid2host[hid] self.debug("--- Removing host with hid = %d" % hid) # Add switches found in STS but not in GUI for dpid in self.sts_topology.dpid2switch.keys(): self.add_switch(dpid) self.debug("--- Adding new switch with dpid = %d" % dpid) # Add hosts found in STS but not in GUI for hid in self.sts_topology.hid2host.keys(): self.add_host(hid) self.debug("--- Adding new host with dpid = %d!" % hid) # Reconnect all access links for access_link in self.sts_topology.access_links: sts_host = access_link.host sts_switch = access_link.switch self.add_access_link(sts_host.hid, sts_switch.dpid) self.debug("--- Adding new access link connecting Host %d <--> Switch %d!" % (sts_host.hid, sts_switch.dpid)) # Reconnect all network links for network_link in self.sts_topology.network_links: sts_from_switch = network_link.start_software_switch sts_to_switch = network_link.end_software_switch self.add_network_link(sts_from_switch.dpid, sts_to_switch.dpid) self.debug("--- Adding new network link connecting Switch %d --> Switch %d!" % (sts_from_switch.dpid, sts_to_switch.dpid)) Timer(self.sync_period, self.sync_with_sts).start()
[docs] def reset(self): ''' Reset GUI topology ''' for item in self.hosts + self.switches + self.links: if item in self.topology_scene.items(): self.topology_scene.removeItem(item) self.hid2host = {} self.dpid2switch = {} self.access_links = [] self.network_links = []
[docs] def add_host(self, hid, position=None): ''' Add and register a host in GUI with the given hid and position ''' if hid is None or hid in self.hids: return host = GuiHost(self.topology_view, hid) self.hid2host[hid] = host self.topology_scene.addItem(host) if position is None: position = (randint(self.topology_view.minX, self.topology_view.maxX), randint(self.topology_view.minY, self.topology_view.maxY)) host.setPos(position[0], position[1])
[docs] def add_switch(self, dpid, position=None): ''' Add and register a switch in GUI with the given dpid and position ''' if dpid is None or dpid in self.dpids: return switch = GuiSwitch(self.topology_view, dpid) self.dpid2switch[dpid] = switch self.topology_scene.addItem(switch) if position is None: position = (randint(self.topology_view.minX, self.topology_view.maxX), randint(self.topology_view.minY, self.topology_view.maxY)) switch.setPos(position[0], position[1])
[docs] def get_sts_host(self, gui_host): ''' Given a host in GUI, return the corresponding host in STS by hid ''' if gui_host.id in self.sts_topology.hid2host.keys(): return self.sts_topology.hid2host[gui_host.id] return None
[docs] def get_sts_switch(self, gui_switch): ''' Given a switch in GUI, return the corresponding switch in STS by dpid ''' if gui_switch.id in self.sts_topology.dpid2switch.keys(): return self.sts_topology.dpid2switch[gui_switch.id] return None
[docs] def create_switch(self, dpid=None): ''' Create a switch with the given dpid in both STS and GUI ''' if dpid is None: dpid = max(self.dpid2switch.keys()) + 1 num_ports = 2 switch = self.sts_topology.create_switch(dpid, num_ports) self.add_switch(dpid)
[docs] def remove_switch(self, dpid): ''' Remove a switch in both STS and GUI, along with all associated links and any dangling hosts previously attached ''' if dpid is None or dpid not in self.dpids: return gui_switch = self.dpid2switch[dpid] sts_switch = self.sts_topology.dpid2switch[dpid] self.sts_topology.remove_switch(sts_switch) # Remove associated network links in GUI network_links_to_remove = [] for network_link in self.network_links: if network_link.source is gui_switch or\ network_link.dest is gui_switch: network_links_to_remove.append(network_link) for network_link in network_links_to_remove: network_link.source.linkList.remove(network_link) network_link.dest.linkList.remove(network_link) if network_link in self.topology_scene.items(): self.topology_scene.removeItem(network_link) self.network_links.remove(network_link) # Remove associated access links in GUI access_links_to_remove = [] for access_link in self.access_links: if access_link.dest is gui_switch: access_links_to_remove.append(access_link) for access_link in access_links_to_remove: gui_host = access_link.source gui_host.linkList.remove(access_link) if access_link in self.topology_scene.items(): self.topology_scene.removeItem(access_link) self.access_links.remove(access_link) # Remove dangling hosts in GUI, if any if len(gui_host.linkList) == 0: if gui_host in self.topology_scene.items(): self.topology_scene.removeItem(gui_host) del self.hid2host[gui_host.id] # Remove switch in GUI if gui_switch in self.topology_scene.items(): self.topology_scene.removeItem(gui_switch) del self.dpid2switch[dpid]
[docs] def attach_host_to_switch(self, dpid): ''' Create a host and attach it to the switch with the given dpid in both STS and GUI ''' if dpid is None or dpid not in self.dpids: return sts_switch = self.sts_topology.dpid2switch[dpid] sts_host = self.sts_topology.create_host(sts_switch, get_switch_port= lambda switch: self.sts_topology.link_tracker.find_unused_port(switch)) hid = sts_host.hid # Situate the new host near the switch gui_switch = self.dpid2switch[dpid] host_x = randint(int(gui_switch.x())-100, int(gui_switch.x())+100) host_y = randint(int(gui_switch.y())-100, int(gui_switch.y())+100) if host_x < self.topology_view.minX: host_x = self.topology_view.minX if host_x > self.topology_view.maxX: host_x = self.topology_view.maxX if host_y < self.topology_view.minY: host_y = self.topology_view.minY if host_y > self.topology_view.maxY: host_y = self.topology_view.maxY self.add_host(hid, (host_x, host_y)) self.add_access_link(hid, dpid)
[docs] def remove_host(self, hid): ''' Remove a host and all associated access links in both STS and GUI ''' if hid is None or hid not in self.hids: return gui_host = self.hid2host[hid] sts_host = self.sts_topology.hid2host[hid] self.sts_topology.remove_host(sts_host) # Remove access links in GUI for access_link in self.access_links: if access_link.source is gui_host: access_link.dest.linkList.remove(access_link) if access_link in self.topology_scene.items(): self.topology_scene.removeItem(access_link) self.access_links.remove(access_link) # Remove host in GUI if gui_host in self.topology_scene.items(): self.topology_scene.removeItem(gui_host) del self.hid2host[hid]
[docs] def save_state(self): ''' Save all JSON serialized entities of both the STS and GUI topologies to a file Each line is in the format (<entity type>, <serialized form>): "s" = switch "h" = host "l" = network link Access links need not be saved, because there is a one to one correspondence between hosts and access links ''' lines = [] for gui_switch in self.switches: sts_switch = self.get_sts_switch(gui_switch) line = json.dumps(("s", self.serialize_switch(gui_switch, sts_switch))) lines.append(line) for gui_host in self.hosts: sts_host = self.get_sts_host(gui_host) line = json.dumps(("h", self.serialize_host(gui_host, sts_host))) lines.append(line) for link in self.sts_topology.network_links: line = json.dumps(("l", self.serialize_network_link(link))) lines.append(line) title = "Specify file to store topology state" filename = QtGui.QFileDialog.getSaveFileName(self.topology_view, title, "gui/layouts") f = QtCore.QFile(filename) f.open(QtCore.QIODevice.WriteOnly) for line in lines: f.write(QtCore.QByteArray(line + "\n")) f.close()
[docs] def load_state(self): ''' Load all JSON serialized entities of both the STS and GUI topologies from a file Each line is in the format (type, serialized_entity): "s" = switch "h" = host "l" = network link Switches must be loaded first ''' title = "Load topology layout from file" filename = QtGui.QFileDialog.getOpenFileName(self.topology_view, title, "gui/layouts") f = QtCore.QFile(filename) if not f.open(QtCore.QIODevice.ReadOnly): self.debug("Error in load_state: Cannot open specified file!") return line = f.readLine() if line.isNull(): self.debug("Error in load_state: Empty file!") return # Reset both STS and GUI topology self.sts_topology.reset() self.reset() type2handler = { "s" : self.deserialize_switch, "h" : self.deserialize_host, "l" : self.deserialize_network_link, } while not line.isNull(): (type, s) = json.loads(str(line)) if type not in type2handler.keys(): self.debug("Error in load_state: Unparseable line detected!") type2handler[type](s) line = f.readLine() f.close()
[docs] def serialize_host(self, gui_host, sts_host): ''' Format Example: {"ingress_switch_dpids":[1,3], "position":(-60.0,20.0)} ''' ingress_switch_dpids = set() for access_link in self.access_links: if access_link.source.id == gui_host.id: ingress_switch_dpids.add(access_link.dest.id) ingress_switch_dpids = list(ingress_switch_dpids) position = (round(gui_host.x(), 2), round(gui_host.y(), 2)) info = {} info["ingress_switch_dpids"] = ingress_switch_dpids info["position"] = position return json.dumps(info)
[docs] def serialize_switch(self, gui_switch, sts_switch): ''' Format Example: {"dpid":5, "numports":3 "position":(-10.0,80.0)} ''' dpid = sts_switch.dpid numports = len(sts_switch.ports.values()) position = (round(gui_switch.x(), 2), round(gui_switch.y(), 2)) info = {} info["dpid"] = dpid info["numports"] = numports info["position"] = position return json.dumps(info)
[docs] def deserialize_host(self, s): ''' Format Example: {"ingress_switch_dpids":[1,3], "position":(-60.0,20.0)} ''' info = json.loads(s) ingress_switch_dpids = info["ingress_switch_dpids"] position = info["position"] sts_ingress_switches = [] for dpid in ingress_switch_dpids: sts_ingress_switches.append(self.sts_topology.dpid2switch[dpid]) sts_host = self.sts_topology.create_host(sts_ingress_switches) self.add_host(sts_host.hid, position) for dpid in ingress_switch_dpids: self.add_access_link(sts_host.hid, dpid)
[docs] def deserialize_switch(self, s): ''' Format Example: {"dpid":5, "numports":3 "position":(-10.0,80.0)} ''' info = json.loads(s) dpid = info["dpid"] numports = info["numports"] position = info["position"] sts_switch = self.sts_topology.create_switch(dpid, numports) self.add_switch(dpid, position)