#!/usr/bin/python ''' Notes: This file contains classes and methods useful for integrating LincOE with Mininet, such as startOE, stopOE, LINCLink, 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 LINCLink 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=LINCLink - 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 sys import re import json import os from time import sleep import urllib2 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 # Sleep time and timeout values in seconds SLEEP_TIME = 2 TIMEOUT = 60 REST_USER = 'onos' REST_PW = 'rocks' class OpticalSwitch(Switch): """ For now, same as Switch class. """ pass class OpticalIntf(Intf): """ For now,same as Intf class. """ pass class OpticalLink(Link): """ For now, same as Link. """ pass class LINCSwitch(OpticalSwitch): """ LINCSwitch class """ # FIXME:Sometimes LINC doesn't remove pipes and on restart increase the pipe # number from erlang.pipe.1.* to erlang.pipe.2.*, so should read and write # from latest pipe files. For now we are removing all the pipes before # starting LINC. ### User Name ### user = os.getlogin() ### pipes ### readPipe = "/tmp/home/{}/linc-oe/rel/linc/erlang.pipe.1.r".format(user) writePipe = "/tmp/home/{}/linc-oe/rel/linc/erlang.pipe.1.w".format(user) ### sys.config path ### sysConfig = "/home/{}/linc-oe/rel/linc/releases/1.0/sys.config".format(user) ### method, mapping dpid to LINC switchId ### @staticmethod def dpids_to_ids(sysConfig): ''' return the dict containing switch dpids as key and LINC switch id as values ''' dpids_to_ids = {} fd = None try: with open(sysConfig, 'r', 0) as fd: switch_id = 1 for line in fd: dpid = re.search(r'([0-9A-Fa-f]{2}[:-]){7}([0-9A-Fa-f]{2})+', line, re.I) if dpid: dpids_to_ids[dpid.group().replace(':', '')] = switch_id switch_id += 1 return dpids_to_ids except: print "Error working with {}\nError: {}\n".format(sysConfig, sys.exc_info()) fd.close() return None ### dict of containing dpids as key and corresponding LINC switchId as values ### dpidsToLINCSwitchId = dpids_to_ids.__func__(sysConfig) @staticmethod def findDir(directory, userName): "finds and returns the path of any directory in the user's home directory" homeDir = '/home/' + userName 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 ### ONOS Directory ### try: onosDir = os.environ[ 'ONOS_ROOT' ] except: onosDir = findDir('onos', user) if not onosDir: error('Please set ONOS_ROOT environment variable!\n') else: os.environ[ 'ONOS_ROOT' ] = onosDir ### LINC-directory lincDir = findDir.__func__('linc-oe', user) if not lincDir: error("***ERROR: Could not find linc-oe in user's home directory\n") ### LINC config generator directory### configGen = findDir.__func__('LINC-config-generator', user) if not configGen: error("***ERROR: Could not find LINC-config-generator in user's home directory\n") # list of all the controllers controllers = None def __init__(self, name, dpid=None, allowed=True, switchType='ROADM', topo=None, annotations={}, controller=None, **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 self.crossConnects = [] self.deletedCrossConnects = [] self.controller = controller self.lincId = self._get_linc_id() # use to communicate with LINC self.lincStarted = False def start(self, *opts, **params): '''Instead of starting a virtual switch, we build the JSON dictionary for the emulated optical switch''' # TODO:Once LINC has the ability to spawn network element dynamically # we need to use this method to spawn new logical LINC switch rather then # bulding JSON. # if LINC is started then we can start and stop logical switches else create JSON if self.lincStarted: return self.start_oe() self.configDict[ 'uri' ] = 'of:' + self.dpid self.configDict[ 'annotations' ] = self.annotations self.configDict[ 'annotations' ].setdefault('name', self.name) self.configDict[ 'hw' ] = 'LINC-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()) self.lincStarted = True def stop(self, deleteIntfs=False): ''' stop the existing switch ''' # TODO:Add support for deleteIntf self.stop_oe() def dpctl( self, *args ): "Run dpctl command: ignore for now" pass def write_to_cli(self, command): ''' send command to LINC ''' fd = None try: fd = open(self.writePipe, 'w', 0) fd.write(command) fd.close() except: print "Error working with {}\nError: {}\n".format(self.writePipe, sys.exc_info()) if fd: fd.close() def read_from_cli(self): ''' read the output from the LINC CLI ''' response = None fd = None try: fd = open(self.readPipe, 'r', 0) fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK) # for non-blocking read # FIXME:Due to non-blocking read most for the time we read nothing response = fd.read() fd.close() except : # print "Error working with {}\nError: {}\n".format(self.readPipe, sys.exc_info()) if fd: fd.close() return response def _get_linc_id(self): ''' return the corresponding LINC switchId. ''' return LINCSwitch.dpidsToLINCSwitchId.get(self.dpid) #-------------------------------------------------------------------------- # LINC CLI commands #-------------------------------------------------------------------------- def start_oe(self): ''' existing LINC switch ''' #starting Switch cmd = "linc:start_switch({}).\r\n".format(self.lincId) self.write_to_cli(cmd) #hanlding taps interfaces related to the switch crossConnectJSON = {} linkConfig = [] for i in range(0,len(self.deletedCrossConnects)): crossConnect = self.deletedCrossConnects.pop() tap = None if isinstance(crossConnect.intf1.node, LINCSwitch): intf = crossConnect.intf2 tapPort = crossConnect.intf1.port else: intf = crossConnect.intf1 tapPort = crossConnect.intf2.port tap = LINCSwitch.findTap(self, tapPort) if tap: LINCSwitch.setupInts([tap]) intf.node.attach(tap) self.crossConnects.append(crossConnect) linkConfig.append(crossConnect.json()) #Sending crossConnect info to the ONOS. crossConnectJSON['links'] = linkConfig with open("crossConnect.json", 'w') as fd: json.dump(crossConnectJSON, fd, indent=4, separators=(',', ': ')) info('*** Pushing crossConnect.json to ONOS\n') output = quietRun('%s/tools/test/bin/onos-topo-cfg %s\ Topology.json' % (self.onosDir, self.controllers[ 0 ].ip), shell=True) def stop_oe(self): ''' stop the existing LINC switch ''' cmd = "linc:stop_switch({}).\r\n".format(self.lincId) self.write_to_cli(cmd) #handling taps if any for i in range(0, len(self.crossConnects)): crossConnect = self.crossConnects.pop() if isinstance(crossConnect.intf1.node, LINCSwitch): intf = crossConnect.intf2 tapPort = crossConnect.intf1.port else: intf = crossConnect.intf1 tapPort = crossConnect.intf2.port intf.node.detach(LINCSwitch.findTap(self, tapPort)) self.deletedCrossConnects.append(crossConnect) def w_port_up(self, port): ''' port_up ''' cmd = "linc:port_up({},{}).\r\n".format(self.lincId, port) self.write_to_cli(cmd) def w_port_down(self, port): ''' port_down ''' cmd = "linc:port_down({},{}).\r\n".format(self.lincId, port) self.write_to_cli(cmd) # helper functions @staticmethod 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, LINCLink) else 'COPPER' intfList = [ intf.link.intf1, intf.link.intf2 ] intfList.remove(intf) portDict[ 'speed' ] = intfList[ 0 ].speed if isinstance(intf.link, LINCLink) else 0 ports.append(portDict) configDict[ 'ports' ] = ports return configDict @staticmethod def bootOE(net): "Start the LINC optical emulator within a mininet instance" LINCSwitch.opticalJSON = {} linkConfig = [] devices = [] #setting up the controllers for LINCSwitch class LINCSwitch.controllers = net.controllers for switch in net.switches: if isinstance(switch, OpticalSwitch): devices.append(switch.json()) else: devices.append(LINCSwitch.switchJSON(switch)) LINCSwitch.opticalJSON[ 'devices' ] = devices for link in net.links: if isinstance(link, LINCLink) : linkConfig.append(link.json()) LINCSwitch.opticalJSON[ 'links' ] = linkConfig info('*** Writing Topology.json file\n') with open('Topology.json', 'w') as outfile: json.dump(LINCSwitch.opticalJSON, outfile, indent=4, separators=(',', ': ')) info('*** Converting Topology.json to linc-oe format (TopoConfig.json) file (no oecfg) \n') topoConfigJson = {}; dpIdToName = {}; topoConfigJson["switchConfig"] = LINCSwitch.getSwitchConfig(dpIdToName); topoConfigJson["linkConfig"] = LINCSwitch.getLinkConfig(dpIdToName); #Writing to TopoConfig.json with open( 'TopoConfig.json', 'w' ) as outfile: json.dump( topoConfigJson, outfile, indent=4, separators=(',', ': ') ) info('*** Creating sys.config...\n') output = quietRun('%s/config_generator TopoConfig.json %s/sys.config.template %s %s' % (LINCSwitch.configGen, LINCSwitch.configGen, LINCSwitch.controllers[ 0 ].ip, LINCSwitch.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(LINCSwitch.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') output = quietRun('cp -v sys.config %s/rel/linc/releases/1.0/' % LINCSwitch.lincDir, shell=True).strip('\n') info(output + '\n') info('*** Adding taps and bringing them up...\n') LINCSwitch.setupInts(LINCSwitch.getTaps()) info('*** removing pipes if any \n') quietRun('rm /tmp/home/%s/linc-oe/rel/linc/*' % LINCSwitch.user, shell=True) info('*** Starting linc OE...\n') output = quietRun('%s/rel/linc/bin/linc start' % LINCSwitch.lincDir, shell=True) if output: error('***ERROR: LINC-OE: %s' % output + '\n') quietRun('%s/rel/linc/bin/linc stop' % LINCSwitch.lincDir, shell=True) return False info('*** Waiting for linc-oe to start...\n') LINCSwitch.waitStarted(net) info('*** Adding cross-connect (tap) interfaces to packet switches...\n') for link in net.links: if isinstance(link, LINCLink): if link.annotations[ 'optical.type' ] == 'cross-connect': for intf in [ link.intf1, link.intf2 ]: if not isinstance(intf, LINCIntf): intfList = [ intf.link.intf1, intf.link.intf2 ] intfList.remove(intf) intf2 = intfList[ 0 ] intf.node.attach(LINCSwitch.findTap(intf2.node, intf2.node.ports[ intf2 ])) info('*** Waiting for all devices to be available in ONOS...\n') url = 'http://%s:8181/onos/v1/devices' % LINCSwitch.controllers[0].ip time = 0 # Set up password authentication pw_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm() pw_mgr.add_password(None, url, REST_USER, REST_PW) handler = urllib2.HTTPBasicAuthHandler(pw_mgr) opener = urllib2.build_opener(handler) opener.open(url) urllib2.install_opener(opener) while True: response = json.load(urllib2.urlopen(url)) devs = response.get('devices') # Wait for all devices to be registered if (len(devices) != len(devs)): continue # Wait for all devices to available available = True for d in devs: available &= d['available'] if available: break if (time >= TIMEOUT): error('***ERROR: ONOS did not register devices within %s seconds\n' % TIMEOUT) break time += SLEEP_TIME sleep(SLEEP_TIME) info('*** Pushing Topology.json to ONOS\n') for index in range(len(LINCSwitch.controllers)): output = quietRun('%s/tools/test/bin/onos-topo-cfg %s Topology.json &' % (LINCSwitch.onosDir, LINCSwitch.controllers[ index ].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\n' % output) #converts node ids to linc-oe format, with colons every two chars @staticmethod def dpId(id): nodeDpid = "" id = id.split("/", 1)[0] for i in range(3, len(id) - 1, 2): nodeDpid += (id[i:(i + 2):]) + ":" return nodeDpid[0:-1]; @staticmethod def getSwitchConfig (dpIdToName): switchConfig = []; #Iterate through all switches and convert the ROADM switches to linc-oe format for switch in LINCSwitch.opticalJSON["devices"]: if switch.get("type", "none") == "ROADM": builtSwitch = {} #set basic switch params based on annotations builtSwitch["allowed"] = True; builtSwitch["latitude"] = switch["annotations"].get("latitude", 0.0); builtSwitch["longitude"] = switch["annotations"].get("longitude", 0.0); #assumed that all switches have this entry nodeId = switch["uri"] #convert the nodeId to linc-oe format nodeDpid = LINCSwitch.dpId(nodeId); builtSwitch["name"] = switch.get("name", "none"); #keep track of the name corresponding to each switch dpid dpIdToName[nodeDpid] = builtSwitch["name"]; builtSwitch["nodeDpid"] = nodeDpid #set switch params and type builtSwitch["params"] = {}; builtSwitch["params"]["numregens"] = switch["annotations"].get("optical.regens", 0); builtSwitch["type"] = "Roadm" #append to list of switches switchConfig.append(builtSwitch); return switchConfig @staticmethod def getLinkConfig (dpIdToName): newLinkConfig = []; #Iterate through all optical links and convert them to linc-oe format for link in LINCSwitch.opticalJSON["links"]: if link.get("type", "none") == "OPTICAL": builtLink = {} #set basic link params for src and dst builtLink["allowed"] = True; builtLink["nodeDpid1"] = LINCSwitch.dpId(link["src"]) builtLink["nodeDpid2"] = LINCSwitch.dpId(link["dst"]) #set more params such as name/bandwidth/port/waves if they exist params = {} params["nodeName1"] = dpIdToName.get(builtLink["nodeDpid1"], "none") params["nodeName2"] = dpIdToName.get(builtLink["nodeDpid2"], "none") params["port1"] = int(link["src"].split("/")[1]) params["port2"] = int(link["dst"].split("/")[1]) if "bandwidth" in link["annotations"]: params["bandwidth"] = link["annotations"]["bandwidth"] if "optical.waves" in link["annotations"]: params["numWaves"] = link["annotations"]["optical.waves"] builtLink["params"] = params #set type of link (WDM or pktOpt) if link["annotations"].get("optical.type", "cross-connect") == "WDM": builtLink["type"] = "wdmLink" else: builtLink["type"] = "pktOptLink" newLinkConfig.append(builtLink); return newLinkConfig @staticmethod def waitStarted(net, timeout=TIMEOUT): "wait until all tap interfaces are available" tapCount = 0 time = 0 for link in net.links: if isinstance(link, LINCLink): 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\n' % TIMEOUT) return False time += SLEEP_TIME sleep(SLEEP_TIME) @staticmethod def shutdownOE(): "stop the optical emulator" info('*** Stopping linc OE...\n') quietRun('%s/rel/linc/bin/linc stop' % LINCSwitch.lincDir, shell=True) @staticmethod def setupInts(intfs): ''' add taps and bring them up. ''' for i in intfs: quietRun('ip tuntap add dev %s mode tap' % i) quietRun('ip link set dev %s up' % i) info('*** Intf %s set\n' % i) @staticmethod def getTaps(path=None): ''' return list of all the tops in sys.config ''' if path is None: path = '%s/rel/linc/releases/1.0/sys.config' % LINCSwitch.lincDir fd = open(path, 'r', 0) sys_data = fd.read() taps = re.findall('tap\d+', sys_data) fd.close() return taps @staticmethod 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') @staticmethod 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: path = '%s/rel/linc/releases/1.0/sys.config' % LINCSwitch.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 ] def json(self): "return json configuration dictionary for switch" return self.configDict def terminate(self): pass class LINCLink(Link): """ LINC link class """ 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 self.port1 = port1 self.port2 = port2 params1 = { 'speed': speed1 } params2 = { 'speed': speed2 } # self.isCrossConnect = True if self.annotations.get('optical.type') == 'cross-connect' else False if isinstance(node1, LINCSwitch) and isinstance(node2, LINCSwitch): self.isCrossConnect = False else: self.isCrossConnect = True if isinstance(node1, LINCSwitch): cls1 = LINCIntf if self.isCrossConnect: node1.crossConnects.append(self) 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, LINCSwitch): cls2 = LINCIntf if self.isCrossConnect: node2.crossConnects.append(self) 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 LINCIntf(OpticalIntf): """ LINC interface class """ 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 ifconfig(self, status): "configure the status" if status == "up": return self.node.w_port_up(self.port) elif status == "down": return self.node.w_port_down(self.port) class MininetOE(Mininet): "Mininet with Linc-OE support (starts and stops linc-oe)" def start(self): Mininet.start(self) LINCSwitch.bootOE(self) def stop(self): Mininet.stop(self) LINCSwitch.shutdownOE() def addControllers(self, controllers): i = 0 for ctrl in controllers: self.addController(RemoteController('c%d' % i, ip=ctrl)) i += 1 if __name__ == '__main__': pass