blob: 96a17f09decff9433e776cd42fc5a7f5f1aca128 [file] [log] [blame]
Bob Lantz087b5d92016-05-06 11:39:04 -07001#!/usr/bin/python
2
3"""
4onos.py: ONOS cluster and control network in Mininet
5
6With onos.py, you can use Mininet to create a complete
7ONOS network, including an ONOS cluster with a modeled
8control network as well as the usual data nework.
9
10This is intended to be useful for distributed ONOS
11development and testing in the case that you require
12a modeled control network.
13
14Invocation (using OVS as default switch):
15
16mn --custom onos.py --controller onos,3 --topo torus,4,4
17
18Or with the user switch (or CPqD if installed):
19
20mn --custom onos.py --controller onos,3 \
21 --switch onosuser --topo torus,4,4
22
Bob Lantzbb37d872016-05-16 16:26:13 -070023Currently you meed to use a custom switch class
Bob Lantz087b5d92016-05-06 11:39:04 -070024because Mininet's Switch() class does't (yet?) handle
25controllers with multiple IP addresses directly.
26
27The classes may also be imported and used via Mininet's
28python API.
29
30Bugs/Gripes:
31- We need --switch onosuser for the user switch because
32 Switch() doesn't currently handle Controller objects
33 with multiple IP addresses.
34- ONOS startup and configuration is painful/undocumented.
35- Too many ONOS environment vars - do we need them all?
36- ONOS cluster startup is very, very slow. If Linux can
37 boot in 4 seconds, why can't ONOS?
38- It's a pain to mess with the control network from the
39 CLI
40- Setting a default controller for Mininet should be easier
41"""
42
43from mininet.node import Controller, OVSSwitch, UserSwitch
44from mininet.nodelib import LinuxBridge
45from mininet.net import Mininet
46from mininet.topo import SingleSwitchTopo, Topo
47from mininet.log import setLogLevel, info
48from mininet.cli import CLI
Bob Lantz1451d722016-05-17 14:40:07 -070049from mininet.util import quietRun, waitListening
Bob Lantz087b5d92016-05-06 11:39:04 -070050from mininet.clean import killprocs
51from mininet.examples.controlnet import MininetFacade
52
53from os import environ
54from os.path import dirname, join, isfile
55from sys import argv
56from glob import glob
57import time
58
59
60### ONOS Environment
61
Bob Lantzbb37d872016-05-16 16:26:13 -070062KarafPort = 8101 # ssh port indicating karaf is running
63GUIPort = 8181 # GUI/REST port
64OpenFlowPort = 6653 # OpenFlow port
Bob Lantz087b5d92016-05-06 11:39:04 -070065
66def defaultUser():
67 "Return a reasonable default user"
68 if 'SUDO_USER' in environ:
69 return environ[ 'SUDO_USER' ]
70 try:
71 user = quietRun( 'who am i' ).split()[ 0 ]
72 except:
73 user = 'nobody'
74 return user
75
Bob Lantz087b5d92016-05-06 11:39:04 -070076# Module vars, initialized below
Bob Lantz4b51d5c2016-05-27 14:47:38 -070077HOME = ONOS_ROOT = ONOS_USER = None
Bob Lantz087b5d92016-05-06 11:39:04 -070078ONOS_APPS = ONOS_WEB_USER = ONOS_WEB_PASS = ONOS_TAR = None
79
80def initONOSEnv():
81 """Initialize ONOS environment (and module) variables
82 This is ugly and painful, but they have to be set correctly
83 in order for the onos-setup-karaf script to work.
84 nodes: list of ONOS nodes
85 returns: ONOS environment variable dict"""
86 # pylint: disable=global-statement
Bob Lantz4b51d5c2016-05-27 14:47:38 -070087 global HOME, ONOS_ROOT, ONOS_USER
Bob Lantz087b5d92016-05-06 11:39:04 -070088 global ONOS_APPS, ONOS_WEB_USER, ONOS_WEB_PASS
89 env = {}
90 def sd( var, val ):
91 "Set default value for environment variable"
92 env[ var ] = environ.setdefault( var, val )
93 return env[ var ]
Bob Lantz4b51d5c2016-05-27 14:47:38 -070094 assert environ[ 'HOME' ]
Bob Lantz087b5d92016-05-06 11:39:04 -070095 HOME = sd( 'HOME', environ[ 'HOME' ] )
Bob Lantz087b5d92016-05-06 11:39:04 -070096 ONOS_ROOT = sd( 'ONOS_ROOT', join( HOME, 'onos' ) )
Bob Lantz087b5d92016-05-06 11:39:04 -070097 environ[ 'ONOS_USER' ] = defaultUser()
98 ONOS_USER = sd( 'ONOS_USER', defaultUser() )
99 ONOS_APPS = sd( 'ONOS_APPS',
100 'drivers,openflow,fwd,proxyarp,mobility' )
101 # ONOS_WEB_{USER,PASS} isn't respected by onos-karaf:
102 environ.update( ONOS_WEB_USER='karaf', ONOS_WEB_PASS='karaf' )
103 ONOS_WEB_USER = sd( 'ONOS_WEB_USER', 'karaf' )
104 ONOS_WEB_PASS = sd( 'ONOS_WEB_PASS', 'karaf' )
105 return env
106
107
108def updateNodeIPs( env, nodes ):
109 "Update env dict and environ with node IPs"
110 # Get rid of stale junk
111 for var in 'ONOS_NIC', 'ONOS_CELL', 'ONOS_INSTANCES':
112 env[ var ] = ''
113 for var in environ.keys():
114 if var.startswith( 'OC' ):
115 env[ var ] = ''
116 for index, node in enumerate( nodes, 1 ):
117 var = 'OC%d' % index
118 env[ var ] = node.IP()
119 env[ 'OCI' ] = env[ 'OCN' ] = env[ 'OC1' ]
120 env[ 'ONOS_INSTANCES' ] = '\n'.join(
121 node.IP() for node in nodes )
122 environ.update( env )
123 return env
124
125
126tarDefaultPath = 'buck-out/gen/tools/package/onos-package/onos.tar.gz'
127
Bob Lantz1451d722016-05-17 14:40:07 -0700128def unpackONOS( destDir='/tmp', run=quietRun ):
Bob Lantz087b5d92016-05-06 11:39:04 -0700129 "Unpack ONOS and return its location"
130 global ONOS_TAR
131 environ.setdefault( 'ONOS_TAR', join( ONOS_ROOT, tarDefaultPath ) )
132 ONOS_TAR = environ[ 'ONOS_TAR' ]
133 tarPath = ONOS_TAR
134 if not isfile( tarPath ):
135 raise Exception( 'Missing ONOS tarball %s - run buck build onos?'
136 % tarPath )
137 info( '(unpacking %s)' % destDir)
Bob Lantz1451d722016-05-17 14:40:07 -0700138 cmds = ( 'mkdir -p "%s" && cd "%s" && tar xzf "%s"'
Bob Lantz087b5d92016-05-06 11:39:04 -0700139 % ( destDir, destDir, tarPath) )
Bob Lantz1451d722016-05-17 14:40:07 -0700140 run( cmds, shell=True, verbose=True )
141 # We can use quietRun for this usually
142 tarOutput = quietRun( 'tar tzf "%s" | head -1' % tarPath, shell=True)
143 tarOutput = tarOutput.split()[ 0 ].strip()
144 assert '/' in tarOutput
145 onosDir = join( destDir, dirname( tarOutput ) )
Bob Lantz087b5d92016-05-06 11:39:04 -0700146 # Add symlink to log file
Bob Lantz1451d722016-05-17 14:40:07 -0700147 run( 'cd %s; ln -s onos*/apache* karaf;'
148 'ln -s karaf/data/log/karaf.log log' % destDir,
149 shell=True )
Bob Lantz087b5d92016-05-06 11:39:04 -0700150 return onosDir
151
152
153### Mininet classes
154
155def RenamedTopo( topo, *args, **kwargs ):
156 """Return specialized topo with renamed hosts
157 topo: topo class/class name to specialize
158 args, kwargs: topo args
159 sold: old switch name prefix (default 's')
160 snew: new switch name prefix
161 hold: old host name prefix (default 'h')
162 hnew: new host name prefix
163 This may be used from the mn command, e.g.
164 mn --topo renamed,single,spref=sw,hpref=host"""
165 sold = kwargs.pop( 'sold', 's' )
166 hold = kwargs.pop( 'hold', 'h' )
167 snew = kwargs.pop( 'snew', 'cs' )
168 hnew = kwargs.pop( 'hnew' ,'ch' )
169 topos = {} # TODO: use global TOPOS dict
170 if isinstance( topo, str ):
171 # Look up in topo directory - this allows us to
172 # use RenamedTopo from the command line!
173 if topo in topos:
174 topo = topos.get( topo )
175 else:
176 raise Exception( 'Unknown topo name: %s' % topo )
177 # pylint: disable=no-init
178 class RenamedTopoCls( topo ):
179 "Topo subclass with renamed nodes"
180 def addNode( self, name, *args, **kwargs ):
181 "Add a node, renaming if necessary"
182 if name.startswith( sold ):
183 name = snew + name[ len( sold ): ]
184 elif name.startswith( hold ):
185 name = hnew + name[ len( hold ): ]
186 return topo.addNode( self, name, *args, **kwargs )
187 return RenamedTopoCls( *args, **kwargs )
188
189
190class ONOSNode( Controller ):
191 "ONOS cluster node"
192
Bob Lantz087b5d92016-05-06 11:39:04 -0700193 def __init__( self, name, **kwargs ):
194 kwargs.update( inNamespace=True )
195 Controller.__init__( self, name, **kwargs )
196 self.dir = '/tmp/%s' % self.name
Bob Lantz4b51d5c2016-05-27 14:47:38 -0700197 self.client = self.dir + '/karaf/bin/client'
Bob Lantz087b5d92016-05-06 11:39:04 -0700198 self.ONOS_HOME = '/tmp'
199
200 # pylint: disable=arguments-differ
201
Bob Lantz569bbec2016-06-03 18:51:16 -0700202 def start( self, env, nodes=() ):
Bob Lantz087b5d92016-05-06 11:39:04 -0700203 """Start ONOS on node
Bob Lantz569bbec2016-06-03 18:51:16 -0700204 env: environment var dict
205 nodes: all nodes in cluster"""
Bob Lantz087b5d92016-05-06 11:39:04 -0700206 env = dict( env )
207 self.cmd( 'rm -rf', self.dir )
Bob Lantz1451d722016-05-17 14:40:07 -0700208 self.ONOS_HOME = unpackONOS( self.dir, run=self.ucmd )
Bob Lantz087b5d92016-05-06 11:39:04 -0700209 env.update( ONOS_HOME=self.ONOS_HOME )
210 self.updateEnv( env )
211 karafbin = glob( '%s/apache*/bin' % self.ONOS_HOME )[ 0 ]
212 onosbin = join( ONOS_ROOT, 'tools/test/bin' )
213 self.cmd( 'export PATH=%s:%s:$PATH' % ( onosbin, karafbin ) )
214 self.cmd( 'cd', self.ONOS_HOME )
Bob Lantz1451d722016-05-17 14:40:07 -0700215 self.ucmd( 'mkdir -p config && '
Bob Lantz569bbec2016-06-03 18:51:16 -0700216 'onos-gen-partitions config/cluster.json',
217 ' '.join( node.IP() for node in nodes ) )
Bob Lantz087b5d92016-05-06 11:39:04 -0700218 info( '(starting %s)' % self )
219 service = join( self.ONOS_HOME, 'bin/onos-service' )
Bob Lantz1451d722016-05-17 14:40:07 -0700220 self.ucmd( service, 'server 1>../onos.log 2>../onos.log'
221 ' & echo $! > onos.pid; ln -s `pwd`/onos.pid ..' )
Bob Lantz4b51d5c2016-05-27 14:47:38 -0700222 self.onosPid = int( self.cmd( 'cat onos.pid' ).strip() )
Bob Lantz087b5d92016-05-06 11:39:04 -0700223
224 # pylint: enable=arguments-differ
225
226 def stop( self ):
227 # XXX This will kill all karafs - too bad!
228 self.cmd( 'pkill -HUP -f karaf.jar && wait' )
229 self.cmd( 'rm -rf', self.dir )
230
Bob Lantz4b51d5c2016-05-27 14:47:38 -0700231 def isRunning( self ):
232 "Is our ONOS process still running?"
233 cmd = 'ps -p %d >/dev/null 2>&1 && echo "running" || echo "not running"'
234 return self.cmd( cmd % self.onosPid ) == 'running'
235
236 def sanityCheck( self ):
237 "Check whether we've quit or are running out of memory"
238 if not self.isRunning():
239 raise Exception( 'ONOS node %s has died' % self.name )
240
Bob Lantz087b5d92016-05-06 11:39:04 -0700241 def waitStarted( self ):
242 "Wait until we've really started"
243 info( '(checking: karaf' )
244 while True:
Bob Lantz1451d722016-05-17 14:40:07 -0700245 status = self.ucmd( 'karaf status' ).lower()
Bob Lantz087b5d92016-05-06 11:39:04 -0700246 if 'running' in status and 'not running' not in status:
247 break
248 info( '.' )
249 time.sleep( 1 )
250 info( ' ssh-port' )
Bob Lantzbb37d872016-05-16 16:26:13 -0700251 waitListening( server=self, port=KarafPort )
Bob Lantz087b5d92016-05-06 11:39:04 -0700252 info( ' openflow-port' )
Bob Lantzbb37d872016-05-16 16:26:13 -0700253 waitListening( server=self, port=OpenFlowPort )
Bob Lantz087b5d92016-05-06 11:39:04 -0700254 info( ' client' )
255 while True:
Bob Lantzbb37d872016-05-16 16:26:13 -0700256 result = quietRun( 'echo apps -a | %s -h %s' %
257 ( self.client, self.IP() ), shell=True )
Bob Lantz087b5d92016-05-06 11:39:04 -0700258 if 'openflow' in result:
259 break
260 info( '.' )
261 time.sleep( 1 )
262 info( ')\n' )
263
264 def updateEnv( self, envDict ):
265 "Update environment variables"
Bob Lantz569bbec2016-06-03 18:51:16 -0700266 cmd = ';'.join( ( 'export %s="%s"' % ( var, val )
267 if val else 'unset %s' % var )
Bob Lantz087b5d92016-05-06 11:39:04 -0700268 for var, val in envDict.iteritems() )
269 self.cmd( cmd )
270
Bob Lantz1451d722016-05-17 14:40:07 -0700271 def ucmd( self, *args, **_kwargs ):
272 "Run command as $ONOS_USER using sudo -E -u"
273 if ONOS_USER != 'root': # don't bother with sudo
274 args = [ "sudo -E -u $ONOS_USER PATH=$PATH "
275 "bash -c '%s'" % ' '.join( args ) ]
276 return self.cmd( *args )
277
Bob Lantz087b5d92016-05-06 11:39:04 -0700278
279class ONOSCluster( Controller ):
280 "ONOS Cluster"
281 def __init__( self, *args, **kwargs ):
282 """name: (first parameter)
283 *args: topology class parameters
284 ipBase: IP range for ONOS nodes
Bob Lantzbb37d872016-05-16 16:26:13 -0700285 forward: default port forwarding list,
Bob Lantz087b5d92016-05-06 11:39:04 -0700286 topo: topology class or instance
287 **kwargs: additional topology parameters"""
288 args = list( args )
289 name = args.pop( 0 )
290 topo = kwargs.pop( 'topo', None )
Bob Lantz4b51d5c2016-05-27 14:47:38 -0700291 nat = kwargs.pop( 'nat', 'nat0' )
Bob Lantz087b5d92016-05-06 11:39:04 -0700292 # Default: single switch with 1 ONOS node
293 if not topo:
294 topo = SingleSwitchTopo
295 if not args:
296 args = ( 1, )
297 if not isinstance( topo, Topo ):
298 topo = RenamedTopo( topo, *args, hnew='onos', **kwargs )
Bob Lantzbb37d872016-05-16 16:26:13 -0700299 self.ipBase = kwargs.pop( 'ipBase', '192.168.123.0/24' )
300 self.forward = kwargs.pop( 'forward',
301 [ KarafPort, GUIPort, OpenFlowPort ] )
Bob Lantz087b5d92016-05-06 11:39:04 -0700302 super( ONOSCluster, self ).__init__( name, inNamespace=False )
303 fixIPTables()
304 self.env = initONOSEnv()
Bob Lantzbb37d872016-05-16 16:26:13 -0700305 self.net = Mininet( topo=topo, ipBase=self.ipBase,
Bob Lantz087b5d92016-05-06 11:39:04 -0700306 host=ONOSNode, switch=LinuxBridge,
307 controller=None )
Bob Lantz4b51d5c2016-05-27 14:47:38 -0700308 if nat:
309 self.net.addNAT( nat ).configDefault()
Bob Lantz087b5d92016-05-06 11:39:04 -0700310 updateNodeIPs( self.env, self.nodes() )
311 self._remoteControllers = []
312
313 def start( self ):
314 "Start up ONOS cluster"
Bob Lantz087b5d92016-05-06 11:39:04 -0700315 info( '*** ONOS_APPS = %s\n' % ONOS_APPS )
316 self.net.start()
317 for node in self.nodes():
Bob Lantz569bbec2016-06-03 18:51:16 -0700318 node.start( self.env, self.nodes() )
Bob Lantz087b5d92016-05-06 11:39:04 -0700319 info( '\n' )
Bob Lantzbb37d872016-05-16 16:26:13 -0700320 self.configPortForwarding( ports=self.forward, action='A' )
Bob Lantz087b5d92016-05-06 11:39:04 -0700321 self.waitStarted()
322 return
323
324 def waitStarted( self ):
325 "Wait until all nodes have started"
326 startTime = time.time()
327 for node in self.nodes():
328 info( node )
329 node.waitStarted()
Bob Lantzbb37d872016-05-16 16:26:13 -0700330 info( '*** Waited %.2f seconds for ONOS startup' %
331 ( time.time() - startTime ) )
Bob Lantz087b5d92016-05-06 11:39:04 -0700332
333 def stop( self ):
334 "Shut down ONOS cluster"
Bob Lantzbb37d872016-05-16 16:26:13 -0700335 self.configPortForwarding( ports=self.forward, action='D' )
Bob Lantz087b5d92016-05-06 11:39:04 -0700336 for node in self.nodes():
337 node.stop()
338 self.net.stop()
339
340 def nodes( self ):
341 "Return list of ONOS nodes"
342 return [ h for h in self.net.hosts if isinstance( h, ONOSNode ) ]
343
Bob Lantzbb37d872016-05-16 16:26:13 -0700344 def configPortForwarding( self, ports=[], intf='eth0', action='A' ):
345 """Start or stop ports on intf to all nodes
346 action: A=add/start, D=delete/stop (default: A)"""
347 for port in ports:
348 for index, node in enumerate( self.nodes() ):
349 ip, inport = node.IP(), port + index
350 # Configure a destination NAT rule
351 cmd = ( 'iptables -t nat -{action} PREROUTING -t nat '
352 '-i {intf} -p tcp --dport {inport} '
353 '-j DNAT --to-destination {ip}:{port}' )
354 self.cmd( cmd.format( **locals() ) )
355
Bob Lantz087b5d92016-05-06 11:39:04 -0700356
357class ONOSSwitchMixin( object ):
358 "Mixin for switches that connect to an ONOSCluster"
359 def start( self, controllers ):
360 "Connect to ONOSCluster"
361 self.controllers = controllers
362 assert ( len( controllers ) is 1 and
363 isinstance( controllers[ 0 ], ONOSCluster ) )
364 clist = controllers[ 0 ].nodes()
365 return super( ONOSSwitchMixin, self ).start( clist )
366
367class ONOSOVSSwitch( ONOSSwitchMixin, OVSSwitch ):
368 "OVSSwitch that can connect to an ONOSCluster"
369 pass
370
371class ONOSUserSwitch( ONOSSwitchMixin, UserSwitch):
372 "UserSwitch that can connect to an ONOSCluster"
373 pass
374
375
376### Ugly utility routines
377
378def fixIPTables():
379 "Fix LinuxBridge warning"
380 for s in 'arp', 'ip', 'ip6':
381 quietRun( 'sysctl net.bridge.bridge-nf-call-%stables=0' % s )
382
383
384### Test code
385
386def test( serverCount ):
387 "Test this setup"
388 setLogLevel( 'info' )
389 net = Mininet( topo=SingleSwitchTopo( 3 ),
390 controller=[ ONOSCluster( 'c0', serverCount ) ],
391 switch=ONOSOVSSwitch )
392 net.start()
393 net.waitConnected()
394 CLI( net )
395 net.stop()
396
397
398### CLI Extensions
399
400OldCLI = CLI
401
402class ONOSCLI( OldCLI ):
403 "CLI Extensions for ONOS"
404
405 prompt = 'mininet-onos> '
406
407 def __init__( self, net, **kwargs ):
408 c0 = net.controllers[ 0 ]
409 if isinstance( c0, ONOSCluster ):
410 net = MininetFacade( net, cnet=c0.net )
411 OldCLI.__init__( self, net, **kwargs )
412
Bob Lantz4b51d5c2016-05-27 14:47:38 -0700413 def onos1( self ):
414 "Helper function: return default ONOS node"
415 return self.mn.controllers[ 0 ].net.hosts[ 0 ]
416
Bob Lantz087b5d92016-05-06 11:39:04 -0700417 def do_onos( self, line ):
418 "Send command to ONOS CLI"
419 c0 = self.mn.controllers[ 0 ]
420 if isinstance( c0, ONOSCluster ):
421 # cmdLoop strips off command name 'onos'
422 if line.startswith( ':' ):
423 line = 'onos' + line
Bob Lantz4b51d5c2016-05-27 14:47:38 -0700424 onos1 = self.onos1().name
425 if line:
426 line = '"%s"' % line
427 cmd = '%s client -h %s %s' % ( onos1, onos1, line )
Bob Lantz087b5d92016-05-06 11:39:04 -0700428 quietRun( 'stty -echo' )
429 self.default( cmd )
430 quietRun( 'stty echo' )
431
432 def do_wait( self, line ):
433 "Wait for switches to connect"
434 self.mn.waitConnected()
435
436 def do_balance( self, line ):
437 "Balance switch mastership"
438 self.do_onos( ':balance-masters' )
439
440 def do_log( self, line ):
Bob Lantz4b51d5c2016-05-27 14:47:38 -0700441 "Run tail -f /tmp/onos1/log; press control-C to stop"
442 self.default( self.onos1().name, 'tail -f /tmp/%s/log' % self.onos1() )
Bob Lantz087b5d92016-05-06 11:39:04 -0700443
444
445### Exports for bin/mn
446
447CLI = ONOSCLI
448
449controllers = { 'onos': ONOSCluster, 'default': ONOSCluster }
450
451# XXX Hack to change default controller as above doesn't work
452findController = lambda: ONOSCluster
453
454switches = { 'onos': ONOSOVSSwitch,
455 'onosovs': ONOSOVSSwitch,
456 'onosuser': ONOSUserSwitch,
457 'default': ONOSOVSSwitch }
458
Bob Lantzbb37d872016-05-16 16:26:13 -0700459# Null topology so we can control an external/hardware network
460topos = { 'none': Topo }
461
Bob Lantz087b5d92016-05-06 11:39:04 -0700462if __name__ == '__main__':
463 if len( argv ) != 2:
464 test( 3 )
465 else:
466 test( int( argv[ 1 ] ) )