blob: ec144838155a8dc1485ef74a989a05f4f5e89520 [file] [log] [blame]
#!/usr/bin/python
"""
MiniCCNxEdit: a simple network editor for MiniCCNx
Based on miniedit by Bob Lantz, April 2010
Carlos Cabral Jan, 2013
"""
import optparse
from Tkinter import Frame, Button, Label, Scrollbar, Canvas
from Tkinter import Menu, BitmapImage, PhotoImage, Wm, Toplevel
# someday: from ttk import *
from mininet.log import setLogLevel
from mininet.net import Mininet
from mininet.util import ipStr
from mininet.term import makeTerm, cleanUpScreens
def parse_args():
usage="""Usage: miniccnxedit [template_file]
If no template_file is given, generated template will be written
to the file miniccnx.conf in the current directory.
"""
parser = optparse.OptionParser(usage)
_, arg = parser.parse_args()
if len(arg) != 1:
arg = None
else:
arg = arg[0]
return arg
class MiniEdit( Frame ):
"A simple network editor for Mininet."
def __init__( self, parent=None, cheight=200, cwidth=500, template_file='miniccnx.conf' ):
Frame.__init__( self, parent )
self.action = None
self.appName = 'Mini-CCNx'
if template_file == None:
self.template_file='miniccnx.conf'
else:
self.template_file = template_file
# Style
self.font = ( 'Geneva', 9 )
self.smallFont = ( 'Geneva', 7 )
self.bg = 'white'
# Title
self.top = self.winfo_toplevel()
self.top.title( self.appName )
# Menu bar
self.createMenubar()
# Editing canvas
self.cheight, self.cwidth = cheight, cwidth
self.cframe, self.canvas = self.createCanvas()
# Toolbar
self.images = miniEditImages()
self.buttons = {}
self.active = None
self.tools = ( 'Select', 'Host', 'Switch', 'Link' )
self.customColors = { 'Switch': 'darkGreen', 'Host': 'blue' }
self.toolbar = self.createToolbar()
# Layout
self.toolbar.grid( column=0, row=0, sticky='nsew')
self.cframe.grid( column=1, row=0 )
self.columnconfigure( 1, weight=1 )
self.rowconfigure( 0, weight=1 )
self.pack( expand=True, fill='both' )
# About box
self.aboutBox = None
# Initialize node data
self.nodeBindings = self.createNodeBindings()
self.nodePrefixes = { 'Switch': 's', 'Host': 'h' }
self.widgetToItem = {}
self.itemToWidget = {}
# Initialize link tool
self.link = self.linkWidget = None
# Selection support
self.selection = None
# Keyboard bindings
self.bind( '<Control-q>', lambda event: self.quit() )
self.bind( '<KeyPress-Delete>', self.deleteSelection )
self.bind( '<KeyPress-BackSpace>', self.deleteSelection )
self.focus()
# Event handling initalization
self.linkx = self.linky = self.linkItem = None
self.lastSelection = None
# Model initialization
self.links = {}
self.nodeCount = 0
self.net = None
# Close window gracefully
Wm.wm_protocol( self.top, name='WM_DELETE_WINDOW', func=self.quit )
def quit( self ):
"Stop our network, if any, then quit."
self.stop()
Frame.quit( self )
def createMenubar( self ):
"Create our menu bar."
font = self.font
mbar = Menu( self.top, font=font )
self.top.configure( menu=mbar )
# Application menu
appMenu = Menu( mbar, tearoff=False )
mbar.add_cascade( label=self.appName, font=font, menu=appMenu )
appMenu.add_command( label='About Mini-CCNx', command=self.about,
font=font)
appMenu.add_separator()
appMenu.add_command( label='Quit', command=self.quit, font=font )
#fileMenu = Menu( mbar, tearoff=False )
#mbar.add_cascade( label="File", font=font, menu=fileMenu )
#fileMenu.add_command( label="Load...", font=font )
#fileMenu.add_separator()
#fileMenu.add_command( label="Save", font=font )
#fileMenu.add_separator()
#fileMenu.add_command( label="Print", font=font )
editMenu = Menu( mbar, tearoff=False )
mbar.add_cascade( label="Edit", font=font, menu=editMenu )
editMenu.add_command( label="Cut", font=font,
command=lambda: self.deleteSelection( None ) )
# runMenu = Menu( mbar, tearoff=False )
# mbar.add_cascade( label="Run", font=font, menu=runMenu )
# runMenu.add_command( label="Run", font=font, command=self.doRun )
# runMenu.add_command( label="Stop", font=font, command=self.doStop )
# runMenu.add_separator()
# runMenu.add_command( label='Xterm', font=font, command=self.xterm )
# Canvas
def createCanvas( self ):
"Create and return our scrolling canvas frame."
f = Frame( self )
canvas = Canvas( f, width=self.cwidth, height=self.cheight,
bg=self.bg )
# Scroll bars
xbar = Scrollbar( f, orient='horizontal', command=canvas.xview )
ybar = Scrollbar( f, orient='vertical', command=canvas.yview )
canvas.configure( xscrollcommand=xbar.set, yscrollcommand=ybar.set )
# Resize box
resize = Label( f, bg='white' )
# Layout
canvas.grid( row=0, column=1, sticky='nsew')
ybar.grid( row=0, column=2, sticky='ns')
xbar.grid( row=1, column=1, sticky='ew' )
resize.grid( row=1, column=2, sticky='nsew' )
# Resize behavior
f.rowconfigure( 0, weight=1 )
f.columnconfigure( 1, weight=1 )
f.grid( row=0, column=0, sticky='nsew' )
f.bind( '<Configure>', lambda event: self.updateScrollRegion() )
# Mouse bindings
canvas.bind( '<ButtonPress-1>', self.clickCanvas )
canvas.bind( '<B1-Motion>', self.dragCanvas )
canvas.bind( '<ButtonRelease-1>', self.releaseCanvas )
return f, canvas
def updateScrollRegion( self ):
"Update canvas scroll region to hold everything."
bbox = self.canvas.bbox( 'all' )
if bbox is not None:
self.canvas.configure( scrollregion=( 0, 0, bbox[ 2 ],
bbox[ 3 ] ) )
def canvasx( self, x_root ):
"Convert root x coordinate to canvas coordinate."
c = self.canvas
return c.canvasx( x_root ) - c.winfo_rootx()
def canvasy( self, y_root ):
"Convert root y coordinate to canvas coordinate."
c = self.canvas
return c.canvasy( y_root ) - c.winfo_rooty()
# Toolbar
def activate( self, toolName ):
"Activate a tool and press its button."
# Adjust button appearance
if self.active:
self.buttons[ self.active ].configure( relief='raised' )
self.buttons[ toolName ].configure( relief='sunken' )
# Activate dynamic bindings
self.active = toolName
def createToolbar( self ):
"Create and return our toolbar frame."
toolbar = Frame( self )
# Tools
for tool in self.tools:
cmd = ( lambda t=tool: self.activate( t ) )
b = Button( toolbar, text=tool, font=self.smallFont, command=cmd)
if tool in self.images:
b.config( height=50, image=self.images[ tool ] )
# b.config( compound='top' )
b.pack( fill='x' )
self.buttons[ tool ] = b
self.activate( self.tools[ 0 ] )
# Spacer
Label( toolbar, text='' ).pack()
# Commands
#for cmd, color in [ ( 'Stop', 'darkRed' ), ( 'Run', 'darkGreen' ) ]:
# doCmd = getattr( self, 'do' + cmd )
# b = Button( toolbar, text=cmd, font=self.smallFont,
# fg=color, command=doCmd )
# b.pack( fill='x', side='bottom' )
for cmd, color in [ ( 'Generate', 'darkGreen' ) ]:
doCmd = getattr( self, 'do' + cmd )
b = Button( toolbar, text=cmd, font=self.smallFont,
fg=color, command=doCmd )
b.pack( fill='x', side='bottom' )
return toolbar
def doGenerate( self ):
"Generate template."
self.activate( 'Select' )
for tool in self.tools:
self.buttons[ tool ].config( state='disabled' )
self.buildTemplate()
for tool in self.tools:
self.buttons[ tool ].config( state='normal' )
def doStop( self ):
"Stop command."
self.stop()
for tool in self.tools:
self.buttons[ tool ].config( state='normal' )
def buildTemplate( self ):
"Generate template"
template = open(self.template_file, 'w')
# hosts
template.write('[nodes]\n')
for widget in self.widgetToItem:
name = widget[ 'text' ]
tags = self.canvas.gettags( self.widgetToItem[ widget ] )
if 'Host' in tags:
template.write(name + ':\n')
# switches/routers
# template.write('[routers]\n')
for widget in self.widgetToItem:
name = widget[ 'text' ]
tags = self.canvas.gettags( self.widgetToItem[ widget ] )
if 'Switch' in tags:
template.write(name + ':\n')
# Make links
template.write('[links]\n')
for link in self.links.values():
( src, dst ) = link
srcName, dstName = src[ 'text' ], dst[ 'text' ]
template.write(srcName + ':' + dstName + '\n')
template.close()
# Generic canvas handler
#
# We could have used bindtags, as in nodeIcon, but
# the dynamic approach used here
# may actually require less code. In any case, it's an
# interesting introspection-based alternative to bindtags.
def canvasHandle( self, eventName, event ):
"Generic canvas event handler"
if self.active is None:
return
toolName = self.active
handler = getattr( self, eventName + toolName, None )
if handler is not None:
handler( event )
def clickCanvas( self, event ):
"Canvas click handler."
self.canvasHandle( 'click', event )
def dragCanvas( self, event ):
"Canvas drag handler."
self.canvasHandle( 'drag', event )
def releaseCanvas( self, event ):
"Canvas mouse up handler."
self.canvasHandle( 'release', event )
# Currently the only items we can select directly are
# links. Nodes are handled by bindings in the node icon.
def findItem( self, x, y ):
"Find items at a location in our canvas."
items = self.canvas.find_overlapping( x, y, x, y )
if len( items ) == 0:
return None
else:
return items[ 0 ]
# Canvas bindings for Select, Host, Switch and Link tools
def clickSelect( self, event ):
"Select an item."
self.selectItem( self.findItem( event.x, event.y ) )
def deleteItem( self, item ):
"Delete an item."
# Don't delete while network is running
if self.buttons[ 'Select' ][ 'state' ] == 'disabled':
return
# Delete from model
if item in self.links:
self.deleteLink( item )
if item in self.itemToWidget:
self.deleteNode( item )
# Delete from view
self.canvas.delete( item )
def deleteSelection( self, _event ):
"Delete the selected item."
if self.selection is not None:
self.deleteItem( self.selection )
self.selectItem( None )
def nodeIcon( self, node, name ):
"Create a new node icon."
icon = Button( self.canvas, image=self.images[ node ],
text=name, compound='top' )
# Unfortunately bindtags wants a tuple
bindtags = [ str( self.nodeBindings ) ]
bindtags += list( icon.bindtags() )
icon.bindtags( tuple( bindtags ) )
return icon
def newNode( self, node, event ):
"Add a new node to our canvas."
c = self.canvas
x, y = c.canvasx( event.x ), c.canvasy( event.y )
self.nodeCount += 1
name = self.nodePrefixes[ node ] + str( self.nodeCount )
icon = self.nodeIcon( node, name )
item = self.canvas.create_window( x, y, anchor='c', window=icon,
tags=node )
self.widgetToItem[ icon ] = item
self.itemToWidget[ item ] = icon
self.selectItem( item )
icon.links = {}
def clickHost( self, event ):
"Add a new host to our canvas."
self.newNode( 'Host', event )
def clickSwitch( self, event ):
"Add a new switch to our canvas."
self.newNode( 'Switch', event )
def dragLink( self, event ):
"Drag a link's endpoint to another node."
if self.link is None:
return
# Since drag starts in widget, we use root coords
x = self.canvasx( event.x_root )
y = self.canvasy( event.y_root )
c = self.canvas
c.coords( self.link, self.linkx, self.linky, x, y )
def releaseLink( self, _event ):
"Give up on the current link."
if self.link is not None:
self.canvas.delete( self.link )
self.linkWidget = self.linkItem = self.link = None
# Generic node handlers
def createNodeBindings( self ):
"Create a set of bindings for nodes."
bindings = {
'<ButtonPress-1>': self.clickNode,
'<B1-Motion>': self.dragNode,
'<ButtonRelease-1>': self.releaseNode,
'<Enter>': self.enterNode,
'<Leave>': self.leaveNode,
'<Double-ButtonPress-1>': self.xterm
}
l = Label() # lightweight-ish owner for bindings
for event, binding in bindings.items():
l.bind( event, binding )
return l
def selectItem( self, item ):
"Select an item and remember old selection."
self.lastSelection = self.selection
self.selection = item
def enterNode( self, event ):
"Select node on entry."
self.selectNode( event )
def leaveNode( self, _event ):
"Restore old selection on exit."
self.selectItem( self.lastSelection )
def clickNode( self, event ):
"Node click handler."
if self.active is 'Link':
self.startLink( event )
else:
self.selectNode( event )
return 'break'
def dragNode( self, event ):
"Node drag handler."
if self.active is 'Link':
self.dragLink( event )
else:
self.dragNodeAround( event )
def releaseNode( self, event ):
"Node release handler."
if self.active is 'Link':
self.finishLink( event )
# Specific node handlers
def selectNode( self, event ):
"Select the node that was clicked on."
item = self.widgetToItem.get( event.widget, None )
self.selectItem( item )
def dragNodeAround( self, event ):
"Drag a node around on the canvas."
c = self.canvas
# Convert global to local coordinates;
# Necessary since x, y are widget-relative
x = self.canvasx( event.x_root )
y = self.canvasy( event.y_root )
w = event.widget
# Adjust node position
item = self.widgetToItem[ w ]
c.coords( item, x, y )
# Adjust link positions
for dest in w.links:
link = w.links[ dest ]
item = self.widgetToItem[ dest ]
x1, y1 = c.coords( item )
c.coords( link, x, y, x1, y1 )
def startLink( self, event ):
"Start a new link."
if event.widget not in self.widgetToItem:
# Didn't click on a node
return
w = event.widget
item = self.widgetToItem[ w ]
x, y = self.canvas.coords( item )
self.link = self.canvas.create_line( x, y, x, y, width=4,
fill='blue', tag='link' )
self.linkx, self.linky = x, y
self.linkWidget = w
self.linkItem = item
# Link bindings
# Selection still needs a bit of work overall
# Callbacks ignore event
def select( _event, link=self.link ):
"Select item on mouse entry."
self.selectItem( link )
def highlight( _event, link=self.link ):
"Highlight item on mouse entry."
# self.selectItem( link )
self.canvas.itemconfig( link, fill='green' )
def unhighlight( _event, link=self.link ):
"Unhighlight item on mouse exit."
self.canvas.itemconfig( link, fill='blue' )
# self.selectItem( None )
self.canvas.tag_bind( self.link, '<Enter>', highlight )
self.canvas.tag_bind( self.link, '<Leave>', unhighlight )
self.canvas.tag_bind( self.link, '<ButtonPress-1>', select )
def finishLink( self, event ):
"Finish creating a link"
if self.link is None:
return
source = self.linkWidget
c = self.canvas
# Since we dragged from the widget, use root coords
x, y = self.canvasx( event.x_root ), self.canvasy( event.y_root )
target = self.findItem( x, y )
dest = self.itemToWidget.get( target, None )
if ( source is None or dest is None or source == dest
or dest in source.links or source in dest.links ):
self.releaseLink( event )
return
# For now, don't allow hosts to be directly linked
# stags = self.canvas.gettags( self.widgetToItem[ source ] )
# dtags = self.canvas.gettags( target )
# if 'Host' in stags and 'Host' in dtags:
# self.releaseLink( event )
# return
x, y = c.coords( target )
c.coords( self.link, self.linkx, self.linky, x, y )
self.addLink( source, dest )
# We're done
self.link = self.linkWidget = None
# Menu handlers
def about( self ):
"Display about box."
about = self.aboutBox
if about is None:
bg = 'white'
about = Toplevel( bg='white' )
about.title( 'About' )
info = self.appName + ': a simple network editor for Mini-CCNx - based on Miniedit '
warning = 'Development version - not entirely functional!'
author = 'Carlos Cabral, Jan 2013'
author2 = 'Miniedit by Bob Lantz <rlantz@cs>, April 2010'
line1 = Label( about, text=info, font='Helvetica 10 bold', bg=bg )
line2 = Label( about, text=warning, font='Helvetica 9', bg=bg )
line3 = Label( about, text=author, font='Helvetica 9', bg=bg )
line4 = Label( about, text=author2, font='Helvetica 9', bg=bg )
line1.pack( padx=20, pady=10 )
line2.pack(pady=10 )
line3.pack(pady=10 )
line4.pack(pady=10 )
hide = ( lambda about=about: about.withdraw() )
self.aboutBox = about
# Hide on close rather than destroying window
Wm.wm_protocol( about, name='WM_DELETE_WINDOW', func=hide )
# Show (existing) window
about.deiconify()
def createToolImages( self ):
"Create toolbar (and icon) images."
# Model interface
#
# Ultimately we will either want to use a topo or
# mininet object here, probably.
def addLink( self, source, dest ):
"Add link to model."
source.links[ dest ] = self.link
dest.links[ source ] = self.link
self.links[ self.link ] = ( source, dest )
def deleteLink( self, link ):
"Delete link from model."
pair = self.links.get( link, None )
if pair is not None:
source, dest = pair
del source.links[ dest ]
del dest.links[ source ]
if link is not None:
del self.links[ link ]
def deleteNode( self, item ):
"Delete node (and its links) from model."
widget = self.itemToWidget[ item ]
for link in widget.links.values():
# Delete from view and model
self.deleteItem( link )
del self.itemToWidget[ item ]
del self.widgetToItem[ widget ]
def build( self ):
"Build network based on our topology."
net = Mininet( topo=None )
# Make controller
net.addController( 'c0' )
# Make nodes
for widget in self.widgetToItem:
name = widget[ 'text' ]
tags = self.canvas.gettags( self.widgetToItem[ widget ] )
nodeNum = int( name[ 1: ] )
if 'Switch' in tags:
net.addSwitch( name )
elif 'Host' in tags:
net.addHost( name, ip=ipStr( nodeNum ) )
else:
raise Exception( "Cannot create mystery node: " + name )
# Make links
for link in self.links.values():
( src, dst ) = link
srcName, dstName = src[ 'text' ], dst[ 'text' ]
src, dst = net.nameToNode[ srcName ], net.nameToNode[ dstName ]
src.linkTo( dst )
# Build network (we have to do this separately at the moment )
net.build()
return net
def start( self ):
"Start network."
if self.net is None:
self.net = self.build()
self.net.start()
def stop( self ):
"Stop network."
if self.net is not None:
self.net.stop()
cleanUpScreens()
self.net = None
def xterm( self, _ignore=None ):
"Make an xterm when a button is pressed."
if ( self.selection is None or
self.net is None or
self.selection not in self.itemToWidget ):
return
name = self.itemToWidget[ self.selection ][ 'text' ]
if name not in self.net.nameToNode:
return
term = makeTerm( self.net.nameToNode[ name ], 'Host' )
self.net.terms.append( term )
def miniEditImages():
"Create and return images for MiniEdit."
# Image data. Git will be unhappy. However, the alternative
# is to keep track of separate binary files, which is also
# unappealing.
return {
'Select': BitmapImage(
file='/usr/include/X11/bitmaps/left_ptr' ),
'Switch' : PhotoImage( data=r"""
R0lGODlhOgArAMIEAB8aFwB7tQCb343L8P///////////////yH+GlNvZnR3YXJlOiBNaWNyb3NvZnQgT2ZmaWNlACwAAAAAOgArAAAD/ki63P4wykmrvTjr3YYfQigKH7d5Y6qmnjmBayyHg8vAAqDPaUTbowaA13OIahqcyEgEQEbIi7LIGA1FzsaSQK0QfbnH10sMa83VsqX53HLL7sgUTudR5s367F7PEq4CYDJRcngqfgshiAqAMwF3AYdWTCERjSoBjy+ZVItvMg6XIZmaEgOkSmJwlKOkkKSRlaqraaewr7ABhnqBNLmZuL+6vCzCrpvGsB9EH8m5wc7R0sbQ09bT1dOEBLbXwMjeEN7HpuO6Dt3hFObi7Ovj7d7bEOnYD+4V8PfqF/wN/lKsxZPmop6wBwaFzTsRbVvCWzYQmlMW0UKzZCUqatzICLGjx48gKyYAADs=
"""),
'Host' : PhotoImage( data=r"""
R0lGODlhKQAyAMIHAJyeoK+wsrW2uMHCxM7P0Ozt7fn5+f///yH+EUNyZWF0ZWQgd2l0aCBHSU1QACwAAAAAKQAyAAAD63i63P4wykmrvS4cwLv/IEhxRxGeKGBM3pa+X9QeBmxrT3gMNpyLrt6rgcJgisKXgIFMopaLpjMEVUinn2pQ1ImSrN8uGKCVegHn8bl8CqbV7jFbJ47H650592sX4zl6MX9ocIOBLYNvhkxtiYV8eYx0kJSEi2d7WFmSmZqRmIKeHoddoqOcoaZkqIiqq6CtqqQkrq9jnaKzaLW6Wy8DBMHCp7ClPT+ArMY2t1u9Qs3Et6k+W87KtMfW0r6x1d7P2uDYu+LLtt3nQ9ufxeXM7MkOuCnR7UTe6/jyEOqeWj/SYQEowxXBfgYPJAAAOw==
"""),
'Link': PhotoImage( data=r"""
R0lGODlhFgAWAPcAMf//////zP//mf//Zv//M///AP/M///MzP/M
mf/MZv/MM//MAP+Z//+ZzP+Zmf+ZZv+ZM/+ZAP9m//9mzP9mmf9m
Zv9mM/9mAP8z//8zzP8zmf8zZv8zM/8zAP8A//8AzP8Amf8AZv8A
M/8AAMz//8z/zMz/mcz/Zsz/M8z/AMzM/8zMzMzMmczMZszMM8zM
AMyZ/8yZzMyZmcyZZsyZM8yZAMxm/8xmzMxmmcxmZsxmM8xmAMwz
/8wzzMwzmcwzZswzM8wzAMwA/8wAzMwAmcwAZswAM8wAAJn//5n/
zJn/mZn/Zpn/M5n/AJnM/5nMzJnMmZnMZpnMM5nMAJmZ/5mZzJmZ
mZmZZpmZM5mZAJlm/5lmzJlmmZlmZplmM5lmAJkz/5kzzJkzmZkz
ZpkzM5kzAJkA/5kAzJkAmZkAZpkAM5kAAGb//2b/zGb/mWb/Zmb/
M2b/AGbM/2bMzGbMmWbMZmbMM2bMAGaZ/2aZzGaZmWaZZmaZM2aZ
AGZm/2ZmzGZmmWZmZmZmM2ZmAGYz/2YzzGYzmWYzZmYzM2YzAGYA
/2YAzGYAmWYAZmYAM2YAADP//zP/zDP/mTP/ZjP/MzP/ADPM/zPM
zDPMmTPMZjPMMzPMADOZ/zOZzDOZmTOZZjOZMzOZADNm/zNmzDNm
mTNmZjNmMzNmADMz/zMzzDMzmTMzZjMzMzMzADMA/zMAzDMAmTMA
ZjMAMzMAAAD//wD/zAD/mQD/ZgD/MwD/AADM/wDMzADMmQDMZgDM
MwDMAACZ/wCZzACZmQCZZgCZMwCZAABm/wBmzABmmQBmZgBmMwBm
AAAz/wAzzAAzmQAzZgAzMwAzAAAA/wAAzAAAmQAAZgAAM+4AAN0A
ALsAAKoAAIgAAHcAAFUAAEQAACIAABEAAADuAADdAAC7AACqAACI
AAB3AABVAABEAAAiAAARAAAA7gAA3QAAuwAAqgAAiAAAdwAAVQAA
RAAAIgAAEe7u7t3d3bu7u6qqqoiIiHd3d1VVVURERCIiIhEREQAA
ACH5BAEAAAAALAAAAAAWABYAAAhIAAEIHEiwoEGBrhIeXEgwoUKG
Cx0+hGhQoiuKBy1irChxY0GNHgeCDAlgZEiTHlFuVImRJUWXEGEy
lBmxI8mSNknm1Dnx5sCAADs=
""" )
}
if __name__ == '__main__':
setLogLevel( 'info' )
temp_file = parse_args()
app = MiniEdit(template_file=temp_file)
app.mainloop()