#!/usr/bin/python ''' Notes: This file contains classes and methods useful for integrating LincOE with Mininet, such as startOE, stopOE, OpticalLink, and OpticalSwitch - $ONOS_ROOT ust be set - Need to run with sudo -E to preserve ONOS_ROOT env var - We assume LINC-Config-Generator is named LINC-Config-Generator - We also assume linc-oe is named linc-oe - LINC-config-generator and linc-oe must be subdirectories of the user's home directory TODO ----------- - clean up files after runtime - maybe save the old files in a separate directory? - modify script to allow startOE to run before net.start() - add ONOS as a controller in script Usage: ------------ - import OpticalLink and OpticalSwitch from this module - import startOE and stopOE from this module - create topology as you would a normal topology. when to an optical switch with topo.addLink, always specify cls=OpticalLink - when creating an optical switch, use cls=OpticalSwitch in topo.addSwitch - for annotations on links and switches, a dictionary must be passed in as the annotations argument - startOE must be run AFTER net.start() with net as an argument. - stopOE can be run at any time I created a separate function to start lincOE to avoid subclassing Mininet. In case anyone wants to write something that DOES subclass Mininet, I thought I would outline how: If we want an object that starts lincOE within the mininet class itself, we need to add another object to Mininet that contains all of the json object information for each switch. We would still subclass switch and link, but these classes would basically be dummy classes that store their own json information in the Mininet class object. We may also change the default switch class to add it's tap interfaces from lincOE during startup. The start() method for mininet would grab all of the information from these switches and links, write configuration files for lincOE using the json module, start lincOE, then run the start methodfor each switch. The new start() method for each switch would parse through the sys.config file that was created and find the tap interface it needs to connect to, similar to the findTap function that I currently use. After all of the controllers and switches have been started, the new Mininet start() method should also push the Topology configuration file to ONOS. ''' import re import json import os from time import sleep from mininet.node import Switch, RemoteController from mininet.topo import Topo from mininet.util import quietRun from mininet.net import Mininet from mininet.log import setLogLevel, info, error, warn from mininet.link import Link, Intf from mininet.cli import CLI class OpticalSwitch( Switch ): def __init__( self, name, dpid=None, allowed=True, switchType='ROADM', annotations={}, **params ): params[ 'inNamespace' ] = False Switch.__init__( self, name, dpid=dpid, **params ) self.name = name self.annotations = annotations self.allowed = allowed self.switchType = switchType self.configDict = {} # dictionary that holds all of the JSON configuration data def start( self, *opts, **params ): '''Instead of starting a virtual switch, we build the JSON dictionary for the emulated optical switch''' self.configDict[ 'uri' ] = 'of:' + self.dpid self.configDict[ 'annotations' ] = self.annotations self.configDict[ 'annotations' ].setdefault( 'name', self.name ) self.configDict[ 'hw' ] = 'OE' self.configDict[ 'mfr' ] = 'Linc' self.configDict[ 'mac' ] = 'ffffffffffff' + self.dpid[-2] + self.dpid[-1] self.configDict[ 'type' ] = self.switchType self.configDict[ 'ports' ] = [] for port, intf in self.intfs.items(): if intf.name == 'lo': continue else: self.configDict[ 'ports' ].append( intf.json() ) def json( self ): "return json configuration dictionary for switch" return self.configDict def terminate( self ): pass class OpticalLink( Link ): def __init__( self, node1, node2, port1=None, port2=None, allowed=True, intfName1=None, intfName2=None, linkType='OPTICAL', annotations={}, speed1=0, speed2=0, **params ): "Creates a dummy link without a virtual ethernet pair." self.allowed = allowed self.annotations = annotations self.linkType = linkType params1 = { 'speed': speed1 } params2 = { 'speed': speed2 } if isinstance( node1, OpticalSwitch ): cls1 = OpticalIntf else: cls1 = Intf # bad hack to stop error message from appearing when we try to set up intf in a packet switch, # and there is no interface there( because we do not run makeIntfPair ). This way, we just set lo up intfName1 = 'lo' if isinstance( node2, OpticalSwitch ): cls2 = OpticalIntf else: cls2 = Intf intfName2 = 'lo' Link.__init__( self, node1, node2, port1=port1, port2=port2, intfName1=intfName1, intfName2=intfName2, cls1=cls1, cls2=cls2, params1=params1, params2=params2 ) @classmethod def makeIntfPair( _cls, intfName1, intfName2, *args, **kwargs ): pass def json( self ): "build and return the json configuration dictionary for this link" configData = {} configData[ 'src' ] = ( 'of:' + self.intf1.node.dpid + '/%s' % self.intf1.node.ports[ self.intf1 ] ) configData[ 'dst' ] = ( 'of:' + self.intf2.node.dpid + '/%s' % self.intf2.node.ports[ self.intf2 ] ) configData[ 'type' ] = self.linkType configData[ 'annotations' ] = self.annotations return configData class OpticalIntf( Intf ): def __init__( self, name=None, node=None, speed=0, port=None, link=None, **params ): self.node = node self.speed = speed self.port = port self.link = link self.name = name node.addIntf( self, port=port ) self.params = params self.ip = None def json( self ): "build and return the JSON information for this interface( not used right now )" configDict = {} configDict[ 'port' ] = self.port configDict[ 'speed' ] = self.speed configDict[ 'type' ] = 'FIBER' return configDict def config( self, *args, **kwargs ): "dont configure a dummy interface" pass def switchJSON( switch ): "Returns the json configuration for a packet switch" configDict = {} configDict[ 'uri' ] = 'of:' + switch.dpid configDict[ 'mac' ] = quietRun( 'cat /sys/class/net/%s/address' % switch.name ).strip( '\n' ).translate( None, ':' ) configDict[ 'hw' ] = 'PK' # FIXME what about OVS? configDict[ 'mfr' ] = 'Linc' # FIXME what about OVS? configDict[ 'type' ] = 'SWITCH' # FIXME what about OVS? annotations = switch.params.get( 'annotations', {} ) annotations.setdefault( 'name', switch.name ) configDict[ 'annotations' ] = annotations ports = [] for port, intf in switch.intfs.items(): if intf.name == 'lo': continue portDict = {} portDict[ 'port' ] = port portDict[ 'type' ] = 'FIBER' if isinstance( intf.link, OpticalLink ) else 'COPPER' intfList = [ intf.link.intf1, intf.link.intf2 ] intfList.remove( intf ) portDict[ 'speed' ] = intfList[ 0 ].speed if isinstance( intf.link, OpticalLink ) else 0 ports.append( portDict ) configDict[ 'ports' ] = ports return configDict def startOE( net ): "Start the LINC optical emulator within a mininet instance" opticalJSON = {} linkConfig = [] devices = [] for switch in net.switches: if isinstance( switch, OpticalSwitch ): devices.append( switch.json() ) else: devices.append( switchJSON( switch ) ) opticalJSON[ 'devices' ] = devices for link in net.links: if isinstance( link, OpticalLink ) : linkConfig.append( link.json() ) opticalJSON[ 'links' ] = linkConfig try: onosDir = os.environ[ 'ONOS_ROOT' ] except: onosDir = findDir( 'onos' ) if not onosDir: error( 'Please set ONOS_ROOT environment variable!\n' ) return False else: os.environ[ 'ONOS_ROOT' ] = onosDir info( '*** Writing Topology.json file\n' ) with open( 'Topology.json', 'w' ) as outfile: json.dump( opticalJSON, outfile, indent=4, separators=(',', ': ') ) info( '*** Converting Topology.json to linc-oe format (TopoConfig.json) file\n' ) output = quietRun( '%s/tools/test/bin/onos-oecfg ./Topology.json > TopoConfig.json' % onosDir, shell=True ) if output: error( '***ERROR: Error creating topology file: %s ' % output + '\n' ) return False info( '*** Creating sys.config...\n' ) configGen = findDir( 'LINC-config-generator' ) if not configGen: error( "***ERROR: Could not find LINC-config-generator in user's home directory\n" ) return False output = quietRun( '%s/config_generator TopoConfig.json %s/sys.config.template %s %s' % ( configGen, configGen, net.controllers[ 0 ].ip, net.controllers[ 0 ].port ), shell=True ) if output: error( '***ERROR: Error creating sys.config file: %s\n' % output ) return False info ('*** Setting multiple controllers in sys.config...\n' ) searchStr = '\[{"Switch.*$' ctrlStr = '' for index in range(len(net.controllers)): ctrlStr += '{"Switch%d-Controller","%s",%d,tcp},' % (index, net.controllers[index].ip, net.controllers[index].port) replaceStr = '[%s]},' % ctrlStr[:-1] # Cut off last comma sedCmd = 'sed -i \'s/%s/%s/\' sys.config' % (searchStr, replaceStr) output = quietRun( sedCmd, shell=True ) info( '*** Copying sys.config to linc-oe directory: ', output + '\n' ) lincDir = findDir( 'linc-oe' ) if not lincDir: error( "***ERROR: Could not find linc-oe in user's home directory\n" ) return False output = quietRun( 'cp -v sys.config %s/rel/linc/releases/1.0/' % lincDir, shell=True ).strip( '\n' ) info( output + '\n' ) info( '*** Starting linc OE...\n' ) output = quietRun( '%s/rel/linc/bin/linc start' % lincDir, shell=True ) if output: error( '***ERROR: LINC-OE: %s' % output + '\n' ) quietRun( '%s/rel/linc/bin/linc stop' % lincDir, shell=True ) return False info( '*** Waiting for linc-oe to start...\n' ) waitStarted( net ) info( '*** Adding cross-connect (tap) interfaces to packet switches...\n' ) for link in net.links: if isinstance( link, OpticalLink ): if link.annotations[ 'optical.type' ] == 'cross-connect': for intf in [ link.intf1, link.intf2 ]: if not isinstance( intf, OpticalIntf ): intfList = [ intf.link.intf1, intf.link.intf2 ] intfList.remove( intf ) intf2 = intfList[ 0 ] intf.node.attach( findTap( intf2.node, intf2.node.ports[ intf2 ] ) ) info( '*** Press ENTER to push Topology.json to onos...\n' ) raw_input() # FIXME... we should eventually remove this info( '*** Pushing Topology.json to ONOS\n' ) output = quietRun( '%s/tools/test/bin/onos-topo-cfg %s Topology.json' % ( onosDir, net.controllers[ 0 ].ip ), shell=True ) # successful output contains the two characters '{}' # if there is more output than this, there is an issue if output.strip( '{}' ): warn( '***WARNING: Could not push topology file to ONOS: %s' % output ) def waitStarted( net, timeout=None ): "wait until all tap interfaces are available" tapCount = 0 time = 0 for link in net.links: if isinstance( link, OpticalLink ): if link.annotations[ 'optical.type' ] == 'cross-connect': tapCount += 1 while True: if str( tapCount ) == quietRun( 'ip addr | grep tap | wc -l', shell=True ).strip( '\n' ): return True if timeout: if time >= timeout: error( '***ERROR: Linc OE did not start within %s seconds' % timeout ) return False time += .5 sleep( .5 ) def stopOE(): "stop the optical emulator" info( '*** Stopping linc OE...\n' ) lincDir = findDir( 'linc-oe' ) quietRun( '%s/rel/linc/bin/linc stop' % lincDir, shell=True ) def findDir( directory ): "finds and returns the path of any directory in the user's home directory" user = findUser() homeDir = '/home/' + user Dir = quietRun( 'find %s -maxdepth 1 -name %s -type d' % ( homeDir, directory ) ).strip( '\n' ) DirList = Dir.split( '\n' ) if not Dir: return None elif len( DirList ) > 1 : warn( '***WARNING: Found multiple instances of %s; using %s\n' % ( directory, DirList[ 0 ] ) ) return DirList[ 0 ] else: return Dir def findUser(): "Try to return logged-in (usually non-root) user" try: # If we're running sudo return os.environ[ 'SUDO_USER' ] except: try: # Logged-in user (if we have a tty) return quietRun( 'who am i' ).split()[ 0 ] except: # Give up and return effective user return quietRun( 'whoami' ) def findTap( node, port, path=None ): '''utility function to parse through a sys.config file to find tap interfaces for a switch''' switch=False portLine = '' intfLines = [] if path is None: lincDir = findDir( 'linc-oe' ) if not lincDir: error( '***ERROR: Could not find linc-oe in users home directory\n' ) return None path = '%s/rel/linc/releases/1.0/sys.config' % lincDir with open( path ) as f: for line in f: if 'tap' in line: intfLines.append( line ) if node.dpid in line.translate( None, ':' ): switch=True continue if switch: if 'switch' in line: switch = False if 'port_no,%s}' % port in line: portLine = line break if portLine: m = re.search( 'port,\d+', portLine ) port = m.group( 0 ).split( ',' )[ 1 ] else: error( '***ERROR: Could not find any ports in sys.config\n' ) return for intfLine in intfLines: if 'port,%s' % port in intfLine: return re.findall( 'tap\d+', intfLine )[ 0 ] class MininetOE( Mininet ): "Mininet with Linc-OE support (starts and stops linc-oe)" def start( self ): Mininet.start( self ) startOE( self ) def stop( self ): Mininet.stop( self ) stopOE() def addControllers( self, controllers ): i = 0 for ctrl in controllers: self.addController( RemoteController( 'c%d' % i, ip=ctrl ) ) class OpticalTestTopo( Topo ): def build( self ): opticalAnn = { 'optical.waves': 80, 'optical.type': "WDM", 'durable': True } switchAnn = { 'bandwidth': 100000, 'optical.type': 'cross-connect', 'durable': True } h1 = self.addHost( 'h1' ) h2 = self.addHost( 'h2' ) s1 = self.addSwitch( 's1' ) s2 = self.addSwitch( 's2' ) O4 = self.addSwitch( 'O4', cls=OpticalSwitch ) O5 = self.addSwitch( 'O5', cls=OpticalSwitch ) O6 = self.addSwitch( 'O6', cls=OpticalSwitch ) self.addLink( O4, O5, cls=OpticalLink, annotations=opticalAnn ) self.addLink( O5, O6, cls=OpticalLink, annotations=opticalAnn ) self.addLink( s1, O4, cls=OpticalLink, annotations=switchAnn ) self.addLink( s2, O6, cls=OpticalLink, annotations=switchAnn ) self.addLink( h1, s1 ) self.addLink( h2, s2 ) if __name__ == '__main__': import sys if len( sys.argv ) >= 2: controllers = sys.argv[1:] else: print 'Usage: ./opticalUtils.py ()+' print 'Using localhost...\n' controllers = [ '127.0.0.1' ] setLogLevel( 'info' ) net = MininetOE( topo=OpticalTestTopo(), controller=None, autoSetMacs=True ) net.addControllers( controllers ) net.start() CLI( net ) net.stop()