blob: f8e370368935d9ade663f93547ce322f2c326323 [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
Bob Lantz64382422016-06-03 22:51:39 -070047from mininet.log import setLogLevel, info, warn, error, debug
Bob Lantz087b5d92016-05-06 11:39:04 -070048from mininet.cli import CLI
Bob Lantz64382422016-06-03 22:51:39 -070049from mininet.util import quietRun, specialClass
Bob Lantz087b5d92016-05-06 11:39:04 -070050from mininet.examples.controlnet import MininetFacade
51
52from os import environ
53from os.path import dirname, join, isfile
54from sys import argv
55from glob import glob
56import time
Bob Lantz64382422016-06-03 22:51:39 -070057from functools import partial
Bob Lantz92d8e052016-06-17 16:03:58 -070058
Bob Lantz087b5d92016-05-06 11:39:04 -070059
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
Jon Halla43d0332016-08-04 15:02:23 -070065CopycatPort = 9876 # Copycat port
Carmelo Cascone3c8d3d02018-08-15 10:12:51 -070066DebugPort = 5005 # JVM debug port
Bob Lantz087b5d92016-05-06 11:39:04 -070067
68def defaultUser():
69 "Return a reasonable default user"
70 if 'SUDO_USER' in environ:
71 return environ[ 'SUDO_USER' ]
72 try:
73 user = quietRun( 'who am i' ).split()[ 0 ]
74 except:
75 user = 'nobody'
76 return user
77
Bob Lantz087b5d92016-05-06 11:39:04 -070078# Module vars, initialized below
Bob Lantz4b51d5c2016-05-27 14:47:38 -070079HOME = ONOS_ROOT = ONOS_USER = None
Lyndon Fawcettfbad2e72017-09-20 11:46:17 +000080ONOS_APPS = ONOS_WEB_USER = ONOS_WEB_PASS = ONOS_TAR = JAVA_OPTS = None
Bob Lantz087b5d92016-05-06 11:39:04 -070081
82def initONOSEnv():
83 """Initialize ONOS environment (and module) variables
84 This is ugly and painful, but they have to be set correctly
85 in order for the onos-setup-karaf script to work.
86 nodes: list of ONOS nodes
87 returns: ONOS environment variable dict"""
88 # pylint: disable=global-statement
Bob Lantz4b51d5c2016-05-27 14:47:38 -070089 global HOME, ONOS_ROOT, ONOS_USER
Bob Lantz087b5d92016-05-06 11:39:04 -070090 global ONOS_APPS, ONOS_WEB_USER, ONOS_WEB_PASS
91 env = {}
92 def sd( var, val ):
93 "Set default value for environment variable"
94 env[ var ] = environ.setdefault( var, val )
95 return env[ var ]
Bob Lantz4b51d5c2016-05-27 14:47:38 -070096 assert environ[ 'HOME' ]
Bob Lantz087b5d92016-05-06 11:39:04 -070097 HOME = sd( 'HOME', environ[ 'HOME' ] )
Bob Lantz087b5d92016-05-06 11:39:04 -070098 ONOS_ROOT = sd( 'ONOS_ROOT', join( HOME, 'onos' ) )
Bob Lantz087b5d92016-05-06 11:39:04 -070099 environ[ 'ONOS_USER' ] = defaultUser()
100 ONOS_USER = sd( 'ONOS_USER', defaultUser() )
101 ONOS_APPS = sd( 'ONOS_APPS',
Sean Condonbf7ff4f2019-03-17 16:18:42 +0000102 'gui,drivers,openflow,fwd,proxyarp,mobility' )
Lyndon Fawcettfbad2e72017-09-20 11:46:17 +0000103 JAVA_OPTS = sd( 'JAVA_OPTS', '-Xms128m -Xmx512m' )
Bob Lantz087b5d92016-05-06 11:39:04 -0700104 # ONOS_WEB_{USER,PASS} isn't respected by onos-karaf:
105 environ.update( ONOS_WEB_USER='karaf', ONOS_WEB_PASS='karaf' )
106 ONOS_WEB_USER = sd( 'ONOS_WEB_USER', 'karaf' )
107 ONOS_WEB_PASS = sd( 'ONOS_WEB_PASS', 'karaf' )
108 return env
109
110
111def updateNodeIPs( env, nodes ):
112 "Update env dict and environ with node IPs"
113 # Get rid of stale junk
Thomas Vachuska5d80e452019-03-21 14:10:13 -0700114 for var in 'ONOS_CELL', 'ONOS_INSTANCES':
Bob Lantz087b5d92016-05-06 11:39:04 -0700115 env[ var ] = ''
116 for var in environ.keys():
117 if var.startswith( 'OC' ):
118 env[ var ] = ''
119 for index, node in enumerate( nodes, 1 ):
120 var = 'OC%d' % index
121 env[ var ] = node.IP()
Jon Hall25485592016-08-19 13:56:14 -0700122 if nodes:
123 env[ 'OCI' ] = env[ 'OCN' ] = env[ 'OC1' ]
Bob Lantz087b5d92016-05-06 11:39:04 -0700124 env[ 'ONOS_INSTANCES' ] = '\n'.join(
125 node.IP() for node in nodes )
126 environ.update( env )
127 return env
128
129
Bogdan45932c72018-11-06 15:28:00 +0000130tarDefaultPath = "bazel-out/k8-fastbuild/bin/onos.tar.gz"
Bob Lantz087b5d92016-05-06 11:39:04 -0700131
Bob Lantz1451d722016-05-17 14:40:07 -0700132def unpackONOS( destDir='/tmp', run=quietRun ):
Bob Lantz087b5d92016-05-06 11:39:04 -0700133 "Unpack ONOS and return its location"
134 global ONOS_TAR
135 environ.setdefault( 'ONOS_TAR', join( ONOS_ROOT, tarDefaultPath ) )
136 ONOS_TAR = environ[ 'ONOS_TAR' ]
137 tarPath = ONOS_TAR
138 if not isfile( tarPath ):
139 raise Exception( 'Missing ONOS tarball %s - run buck build onos?'
140 % tarPath )
141 info( '(unpacking %s)' % destDir)
Bob Lantza2ccaa52016-06-29 18:26:06 -0700142 success = '*** SUCCESS ***'
143 cmds = ( 'mkdir -p "%s" && cd "%s" && tar xzf "%s" && echo "%s"'
144 % ( destDir, destDir, tarPath, success ) )
145 result = run( cmds, shell=True, verbose=True )
146 if success not in result:
147 raise Exception( 'Failed to unpack ONOS archive %s in %s:\n%s\n' %
148 ( tarPath, destDir, result ) )
Bob Lantz1451d722016-05-17 14:40:07 -0700149 # We can use quietRun for this usually
150 tarOutput = quietRun( 'tar tzf "%s" | head -1' % tarPath, shell=True)
151 tarOutput = tarOutput.split()[ 0 ].strip()
152 assert '/' in tarOutput
153 onosDir = join( destDir, dirname( tarOutput ) )
Bob Lantz087b5d92016-05-06 11:39:04 -0700154 # Add symlink to log file
Bob Lantz1451d722016-05-17 14:40:07 -0700155 run( 'cd %s; ln -s onos*/apache* karaf;'
156 'ln -s karaf/data/log/karaf.log log' % destDir,
157 shell=True )
Bob Lantz087b5d92016-05-06 11:39:04 -0700158 return onosDir
159
160
Bob Lantz9ba19dc2016-06-13 20:22:07 -0700161def waitListening( server, port=80, callback=None, sleepSecs=.5,
162 proc='java' ):
163 "Simplified netstat version of waitListening"
164 while True:
165 lines = server.cmd( 'netstat -natp' ).strip().split( '\n' )
166 entries = [ line.split() for line in lines ]
167 portstr = ':%s' % port
168 listening = [ entry for entry in entries
169 if len( entry ) > 6 and portstr in entry[ 3 ]
170 and proc in entry[ 6 ] ]
171 if listening:
172 break
Bob Lantz64382422016-06-03 22:51:39 -0700173 info( '.' )
174 if callback:
175 callback()
176 time.sleep( sleepSecs )
Bob Lantz64382422016-06-03 22:51:39 -0700177
178
Bob Lantz087b5d92016-05-06 11:39:04 -0700179### Mininet classes
180
181def RenamedTopo( topo, *args, **kwargs ):
182 """Return specialized topo with renamed hosts
183 topo: topo class/class name to specialize
184 args, kwargs: topo args
185 sold: old switch name prefix (default 's')
186 snew: new switch name prefix
187 hold: old host name prefix (default 'h')
188 hnew: new host name prefix
189 This may be used from the mn command, e.g.
190 mn --topo renamed,single,spref=sw,hpref=host"""
191 sold = kwargs.pop( 'sold', 's' )
192 hold = kwargs.pop( 'hold', 'h' )
193 snew = kwargs.pop( 'snew', 'cs' )
194 hnew = kwargs.pop( 'hnew' ,'ch' )
195 topos = {} # TODO: use global TOPOS dict
196 if isinstance( topo, str ):
197 # Look up in topo directory - this allows us to
198 # use RenamedTopo from the command line!
199 if topo in topos:
200 topo = topos.get( topo )
201 else:
202 raise Exception( 'Unknown topo name: %s' % topo )
203 # pylint: disable=no-init
204 class RenamedTopoCls( topo ):
205 "Topo subclass with renamed nodes"
206 def addNode( self, name, *args, **kwargs ):
207 "Add a node, renaming if necessary"
208 if name.startswith( sold ):
209 name = snew + name[ len( sold ): ]
210 elif name.startswith( hold ):
211 name = hnew + name[ len( hold ): ]
212 return topo.addNode( self, name, *args, **kwargs )
213 return RenamedTopoCls( *args, **kwargs )
214
215
Bob Lantz8e576252016-08-29 14:05:59 -0700216# We accept objects that "claim" to be a particular class,
217# since the class definitions can be both execed (--custom) and
218# imported (in another custom file), which breaks isinstance().
219# In order for this to work properly, a class should not be
220# renamed so as to inappropriately omit or include the class
221# name text. Note that mininet.util.specialClass renames classes
222# by adding the specialized parameter names and values.
223
224def isONOSNode( obj ):
225 "Does obj claim to be some kind of ONOSNode?"
226 return ( isinstance( obj, ONOSNode) or
227 'ONOSNode' in type( obj ).__name__ )
228
229def isONOSCluster( obj ):
230 "Does obj claim to be some kind of ONOSCluster?"
231 return ( isinstance( obj, ONOSCluster ) or
232 'ONOSCluster' in type( obj ).__name__ )
233
234
Bob Lantz087b5d92016-05-06 11:39:04 -0700235class ONOSNode( Controller ):
236 "ONOS cluster node"
237
Bob Lantz087b5d92016-05-06 11:39:04 -0700238 def __init__( self, name, **kwargs ):
Bob Lantz64382422016-06-03 22:51:39 -0700239 "alertAction: exception|ignore|warn|exit (exception)"
Bob Lantz087b5d92016-05-06 11:39:04 -0700240 kwargs.update( inNamespace=True )
Bob Lantz64382422016-06-03 22:51:39 -0700241 self.alertAction = kwargs.pop( 'alertAction', 'exception' )
Bob Lantz087b5d92016-05-06 11:39:04 -0700242 Controller.__init__( self, name, **kwargs )
243 self.dir = '/tmp/%s' % self.name
Bob Lantz4b51d5c2016-05-27 14:47:38 -0700244 self.client = self.dir + '/karaf/bin/client'
Bob Lantz087b5d92016-05-06 11:39:04 -0700245 self.ONOS_HOME = '/tmp'
Bob Lantz55562ea2016-06-17 18:19:03 -0700246 self.cmd( 'rm -rf', self.dir )
247 self.ONOS_HOME = unpackONOS( self.dir, run=self.ucmd )
Jon Hall25485592016-08-19 13:56:14 -0700248 self.ONOS_ROOT = ONOS_ROOT
Bob Lantz087b5d92016-05-06 11:39:04 -0700249
250 # pylint: disable=arguments-differ
251
jaegonkim4e360492018-02-17 08:44:35 +0900252
jaegonkima1988f32018-04-09 22:52:26 +0900253 def start( self, env=None, nodes=(), debug=False ):
Bob Lantz087b5d92016-05-06 11:39:04 -0700254 """Start ONOS on node
Bob Lantz569bbec2016-06-03 18:51:16 -0700255 env: environment var dict
256 nodes: all nodes in cluster"""
jaegonkim4e360492018-02-17 08:44:35 +0900257 if env is not None:
258 self.genStartConfig(env, nodes)
259 self.cmd( 'cd', self.ONOS_HOME )
jaegonkima1988f32018-04-09 22:52:26 +0900260 info( '(starting %s debug: %s)' % ( self, debug ) )
jaegonkim4e360492018-02-17 08:44:35 +0900261 service = join( self.ONOS_HOME, 'bin/onos-service' )
jaegonkima1988f32018-04-09 22:52:26 +0900262 if debug:
263 self.ucmd( service, 'server debug 1>../onos.log 2>../onos.log'
jaegonkim4e360492018-02-17 08:44:35 +0900264 ' & echo $! > onos.pid; ln -s `pwd`/onos.pid ..' )
jaegonkima1988f32018-04-09 22:52:26 +0900265 else:
266 self.ucmd( service, 'server 1>../onos.log 2>../onos.log'
267 ' & echo $! > onos.pid; ln -s `pwd`/onos.pid ..' )
jaegonkim4e360492018-02-17 08:44:35 +0900268 self.onosPid = int( self.cmd( 'cat onos.pid' ).strip() )
269 self.warningCount = 0
270
271 # pylint: disable=arguments-differ
272
273 def genStartConfig( self, env, nodes=() ):
274 """generate a start config"""
Bob Lantz087b5d92016-05-06 11:39:04 -0700275 env = dict( env )
Bob Lantz087b5d92016-05-06 11:39:04 -0700276 env.update( ONOS_HOME=self.ONOS_HOME )
277 self.updateEnv( env )
278 karafbin = glob( '%s/apache*/bin' % self.ONOS_HOME )[ 0 ]
279 onosbin = join( ONOS_ROOT, 'tools/test/bin' )
280 self.cmd( 'export PATH=%s:%s:$PATH' % ( onosbin, karafbin ) )
281 self.cmd( 'cd', self.ONOS_HOME )
Bob Lantz1451d722016-05-17 14:40:07 -0700282 self.ucmd( 'mkdir -p config && '
Bogdan45932c72018-11-06 15:28:00 +0000283 'onos-gen-default-cluster config/cluster.json --nodes ',
Bob Lantz569bbec2016-06-03 18:51:16 -0700284 ' '.join( node.IP() for node in nodes ) )
Bob Lantz087b5d92016-05-06 11:39:04 -0700285
Carmelo Casconec52a4b12016-10-24 16:47:17 +0200286 def intfsDown( self ):
287 """Bring all interfaces down"""
288 for intf in self.intfs.values():
289 cmdOutput = intf.ifconfig( 'down' )
290 # no output indicates success
291 if cmdOutput:
292 error( "Error setting %s down: %s " % ( intf.name, cmdOutput ) )
293
294 def intfsUp( self ):
295 """Bring all interfaces up"""
296 for intf in self.intfs.values():
297 cmdOutput = intf.ifconfig( 'up' )
298 if cmdOutput:
299 error( "Error setting %s up: %s " % ( intf.name, cmdOutput ) )
300
jaegonkim4e360492018-02-17 08:44:35 +0900301 def kill( self ):
302 """Kill ONOS process"""
303 self.cmd( 'kill %d && wait' % ( self.onosPid ) )
304
Bob Lantz087b5d92016-05-06 11:39:04 -0700305 def stop( self ):
306 # XXX This will kill all karafs - too bad!
307 self.cmd( 'pkill -HUP -f karaf.jar && wait' )
308 self.cmd( 'rm -rf', self.dir )
309
Bob Lantz64382422016-06-03 22:51:39 -0700310 def sanityAlert( self, *args ):
311 "Alert to raise on sanityCheck failure"
312 info( '\n' )
313 if self.alertAction == 'exception':
314 raise Exception( *args )
315 if self.alertAction == 'warn':
316 warn( *args + ( '\n', ) )
317 elif self.alertAction == 'exit':
318 error( '***', *args +
319 ( '\nExiting. Run "sudo mn -c" to clean up.\n', ) )
320 exit( 1 )
321
Bob Lantz4b51d5c2016-05-27 14:47:38 -0700322 def isRunning( self ):
323 "Is our ONOS process still running?"
Bob Lantz64382422016-06-03 22:51:39 -0700324 cmd = ( 'ps -p %d >/dev/null 2>&1 && echo "running" ||'
325 'echo "not running"' )
326 return self.cmd( cmd % self.onosPid ).strip() == 'running'
Bob Lantz4b51d5c2016-05-27 14:47:38 -0700327
Bob Lantz64382422016-06-03 22:51:39 -0700328 def checkLog( self ):
329 "Return log file errors and warnings"
330 log = join( self.dir, 'log' )
Bob Lantzc96e2582016-06-13 18:57:04 -0700331 errors, warnings = [], []
Bob Lantz64382422016-06-03 22:51:39 -0700332 if isfile( log ):
333 lines = open( log ).read().split( '\n' )
334 errors = [ line for line in lines if 'ERROR' in line ]
335 warnings = [ line for line in lines if 'WARN'in line ]
336 return errors, warnings
337
338 def memAvailable( self ):
339 "Return available memory in KB (or -1 if we can't tell)"
340 lines = open( '/proc/meminfo' ).read().strip().split( '\n' )
341 entries = map( str.split, lines )
342 index = { entry[ 0 ]: entry for entry in entries }
343 # Check MemAvailable if present
344 default = ( None, '-1', 'kB' )
345 _name, count, unit = index.get( 'MemAvailable:', default )
346 if unit.lower() == 'kb':
347 return int( count )
348 return -1
349
350 def sanityCheck( self, lowMem=100000 ):
351 """Check whether we've quit or are running out of memory
352 lowMem: low memory threshold in KB (100000)"""
353 # Are we still running?
Bob Lantz4b51d5c2016-05-27 14:47:38 -0700354 if not self.isRunning():
Bob Lantz64382422016-06-03 22:51:39 -0700355 self.sanityAlert( 'ONOS node %s has died' % self.name )
356 # Are there errors in the log file?
357 errors, warnings = self.checkLog()
358 if errors:
359 self.sanityAlert( 'ONOS startup errors:\n<<%s>>' %
360 '\n'.join( errors ) )
361 warningCount = len( warnings )
362 if warnings and warningCount > self.warningCount:
363 warn( '(%d warnings)' % len( warnings ) )
364 self.warningCount = warningCount
365 # Are we running out of memory?
366 mem = self.memAvailable()
367 if mem > 0 and mem < lowMem:
368 self.sanityAlert( 'Running out of memory (only %d KB available)'
369 % mem )
Bob Lantz4b51d5c2016-05-27 14:47:38 -0700370
Bob Lantz087b5d92016-05-06 11:39:04 -0700371 def waitStarted( self ):
372 "Wait until we've really started"
373 info( '(checking: karaf' )
374 while True:
Bob Lantz1451d722016-05-17 14:40:07 -0700375 status = self.ucmd( 'karaf status' ).lower()
Bob Lantz087b5d92016-05-06 11:39:04 -0700376 if 'running' in status and 'not running' not in status:
377 break
378 info( '.' )
Bob Lantz64382422016-06-03 22:51:39 -0700379 self.sanityCheck()
Bob Lantz087b5d92016-05-06 11:39:04 -0700380 time.sleep( 1 )
381 info( ' ssh-port' )
Bob Lantz64382422016-06-03 22:51:39 -0700382 waitListening( server=self, port=KarafPort, callback=self.sanityCheck )
Carmelo Casconec3baa4c2017-11-21 00:06:11 -0800383 info( ' protocol' )
Bob Lantz087b5d92016-05-06 11:39:04 -0700384 while True:
Bob Lantz9ba19dc2016-06-13 20:22:07 -0700385 result = quietRun( '%s -h %s "apps -a"' %
Bob Lantzbb37d872016-05-16 16:26:13 -0700386 ( self.client, self.IP() ), shell=True )
Carmelo Casconec3baa4c2017-11-21 00:06:11 -0800387 if 'openflow' in result or 'p4runtime' in result:
Bob Lantz087b5d92016-05-06 11:39:04 -0700388 break
389 info( '.' )
Bob Lantz64382422016-06-03 22:51:39 -0700390 self.sanityCheck()
Bob Lantz087b5d92016-05-06 11:39:04 -0700391 time.sleep( 1 )
Jon Halla43d0332016-08-04 15:02:23 -0700392 info( ' node-status' )
393 while True:
394 result = quietRun( '%s -h %s "nodes"' %
395 ( self.client, self.IP() ), shell=True )
Carmelo Casconec3baa4c2017-11-21 00:06:11 -0800396 nodeStr = 'id=%s, address=%s:%s, state=READY' %\
Jon Halla43d0332016-08-04 15:02:23 -0700397 ( self.IP(), self.IP(), CopycatPort )
398 if nodeStr in result:
399 break
Bogdan45932c72018-11-06 15:28:00 +0000400
Brian O'Connor25b239c2018-11-08 23:39:32 +0000401 # just break if state is active
Bogdan45932c72018-11-06 15:28:00 +0000402 if "state=ACTIVE" in result:
403 break
404
Jon Halla43d0332016-08-04 15:02:23 -0700405 info( '.' )
406 self.sanityCheck()
407 time.sleep( 1 )
Bob Lantz087b5d92016-05-06 11:39:04 -0700408 info( ')\n' )
409
410 def updateEnv( self, envDict ):
411 "Update environment variables"
Bob Lantz569bbec2016-06-03 18:51:16 -0700412 cmd = ';'.join( ( 'export %s="%s"' % ( var, val )
413 if val else 'unset %s' % var )
Bob Lantz087b5d92016-05-06 11:39:04 -0700414 for var, val in envDict.iteritems() )
415 self.cmd( cmd )
416
Bob Lantz1451d722016-05-17 14:40:07 -0700417 def ucmd( self, *args, **_kwargs ):
418 "Run command as $ONOS_USER using sudo -E -u"
419 if ONOS_USER != 'root': # don't bother with sudo
420 args = [ "sudo -E -u $ONOS_USER PATH=$PATH "
421 "bash -c '%s'" % ' '.join( args ) ]
422 return self.cmd( *args )
423
Bob Lantz087b5d92016-05-06 11:39:04 -0700424
425class ONOSCluster( Controller ):
426 "ONOS Cluster"
Bob Lantz57635162016-08-29 15:13:54 -0700427 # Offset for port forwarding
428 portOffset = 0
Bob Lantz087b5d92016-05-06 11:39:04 -0700429 def __init__( self, *args, **kwargs ):
430 """name: (first parameter)
431 *args: topology class parameters
432 ipBase: IP range for ONOS nodes
Bob Lantz57635162016-08-29 15:13:54 -0700433 forward: default port forwarding list
434 portOffset: offset to port base (optional)
Bob Lantz087b5d92016-05-06 11:39:04 -0700435 topo: topology class or instance
Bob Lantz64382422016-06-03 22:51:39 -0700436 nodeOpts: ONOSNode options
jaegonkima1988f32018-04-09 22:52:26 +0900437 debug: enabling debug mode or not
Bob Lantz57635162016-08-29 15:13:54 -0700438 **kwargs: additional topology parameters
439 By default, multiple ONOSClusters will increment
440 the portOffset automatically; alternately, it can
441 be specified explicitly.
442 """
Bob Lantz087b5d92016-05-06 11:39:04 -0700443 args = list( args )
444 name = args.pop( 0 )
445 topo = kwargs.pop( 'topo', None )
Bob Lantz930138e2016-06-23 18:53:19 -0700446 self.nat = kwargs.pop( 'nat', 'nat0' )
Bob Lantz64382422016-06-03 22:51:39 -0700447 nodeOpts = kwargs.pop( 'nodeOpts', {} )
Bob Lantz57635162016-08-29 15:13:54 -0700448 self.portOffset = kwargs.pop( 'portOffset', ONOSCluster.portOffset )
Jon Hall9b238ae2016-08-09 13:47:43 -0700449 # Pass in kwargs to the ONOSNodes instead of the cluster
450 "alertAction: exception|ignore|warn|exit (exception)"
451 alertAction = kwargs.pop( 'alertAction', None )
452 if alertAction:
453 nodeOpts[ 'alertAction'] = alertAction
Bob Lantz087b5d92016-05-06 11:39:04 -0700454 # Default: single switch with 1 ONOS node
455 if not topo:
456 topo = SingleSwitchTopo
457 if not args:
458 args = ( 1, )
459 if not isinstance( topo, Topo ):
460 topo = RenamedTopo( topo, *args, hnew='onos', **kwargs )
Bob Lantzbb37d872016-05-16 16:26:13 -0700461 self.ipBase = kwargs.pop( 'ipBase', '192.168.123.0/24' )
462 self.forward = kwargs.pop( 'forward',
Carmelo Cascone3c8d3d02018-08-15 10:12:51 -0700463 [ KarafPort, GUIPort, OpenFlowPort, DebugPort ] )
jaegonkima1988f32018-04-09 22:52:26 +0900464 self.debug = kwargs.pop('debug', 'False') == 'True'
465
Bob Lantz087b5d92016-05-06 11:39:04 -0700466 super( ONOSCluster, self ).__init__( name, inNamespace=False )
467 fixIPTables()
468 self.env = initONOSEnv()
Bob Lantzbb37d872016-05-16 16:26:13 -0700469 self.net = Mininet( topo=topo, ipBase=self.ipBase,
Bob Lantz64382422016-06-03 22:51:39 -0700470 host=partial( ONOSNode, **nodeOpts ),
471 switch=LinuxBridge,
Bob Lantz087b5d92016-05-06 11:39:04 -0700472 controller=None )
Bob Lantz930138e2016-06-23 18:53:19 -0700473 if self.nat:
474 self.net.addNAT( self.nat ).configDefault()
Bob Lantz087b5d92016-05-06 11:39:04 -0700475 updateNodeIPs( self.env, self.nodes() )
476 self._remoteControllers = []
Bob Lantz57635162016-08-29 15:13:54 -0700477 # Update port offset for more ONOS clusters
478 ONOSCluster.portOffset += len( self.nodes() )
Bob Lantz087b5d92016-05-06 11:39:04 -0700479
480 def start( self ):
481 "Start up ONOS cluster"
Bob Lantz087b5d92016-05-06 11:39:04 -0700482 info( '*** ONOS_APPS = %s\n' % ONOS_APPS )
483 self.net.start()
484 for node in self.nodes():
jaegonkima1988f32018-04-09 22:52:26 +0900485 node.start( self.env, self.nodes(), self.debug )
Bob Lantz087b5d92016-05-06 11:39:04 -0700486 info( '\n' )
Bob Lantzbb37d872016-05-16 16:26:13 -0700487 self.configPortForwarding( ports=self.forward, action='A' )
Bob Lantz087b5d92016-05-06 11:39:04 -0700488 self.waitStarted()
489 return
490
491 def waitStarted( self ):
492 "Wait until all nodes have started"
493 startTime = time.time()
494 for node in self.nodes():
495 info( node )
496 node.waitStarted()
Bob Lantzbb37d872016-05-16 16:26:13 -0700497 info( '*** Waited %.2f seconds for ONOS startup' %
498 ( time.time() - startTime ) )
Bob Lantz087b5d92016-05-06 11:39:04 -0700499
500 def stop( self ):
501 "Shut down ONOS cluster"
Bob Lantzbb37d872016-05-16 16:26:13 -0700502 self.configPortForwarding( ports=self.forward, action='D' )
Bob Lantz087b5d92016-05-06 11:39:04 -0700503 for node in self.nodes():
504 node.stop()
505 self.net.stop()
506
507 def nodes( self ):
508 "Return list of ONOS nodes"
Bob Lantz8e576252016-08-29 14:05:59 -0700509 return [ h for h in self.net.hosts if isONOSNode( h ) ]
Bob Lantz087b5d92016-05-06 11:39:04 -0700510
Bob Lantz930138e2016-06-23 18:53:19 -0700511 def configPortForwarding( self, ports=[], action='A' ):
512 """Start or stop port forwarding (any intf) for all nodes
513 ports: list of ports to forward
Bob Lantzbb37d872016-05-16 16:26:13 -0700514 action: A=add/start, D=delete/stop (default: A)"""
Bob Lantz930138e2016-06-23 18:53:19 -0700515 self.cmd( 'iptables -' + action, 'FORWARD -d', self.ipBase,
516 '-j ACCEPT' )
Bob Lantzbb37d872016-05-16 16:26:13 -0700517 for port in ports:
518 for index, node in enumerate( self.nodes() ):
Bob Lantz57635162016-08-29 15:13:54 -0700519 ip, inport = node.IP(), port + self.portOffset + index
Bob Lantzbb37d872016-05-16 16:26:13 -0700520 # Configure a destination NAT rule
Bob Lantz930138e2016-06-23 18:53:19 -0700521 self.cmd( 'iptables -t nat -' + action,
522 'PREROUTING -t nat -p tcp --dport', inport,
523 '-j DNAT --to-destination %s:%s' % ( ip, port ) )
Bob Lantzbb37d872016-05-16 16:26:13 -0700524
jaegonkim4e360492018-02-17 08:44:35 +0900525 def getONOSNode( self, instance ):
526 """Return ONOS node which name is 'instance'
527 instance: ONOS instance name"""
528 try:
529 onos = self.net.getNodeByName( instance )
530 if isONOSNode( onos ):
531 return onos
532 else:
533 info( 'instance %s is not ONOS.\n' % instance )
534 except KeyError:
535 info( 'No such ONOS instance %s.\n' % instance )
Bob Lantz57635162016-08-29 15:13:54 -0700536
Bob Lantz087b5d92016-05-06 11:39:04 -0700537class ONOSSwitchMixin( object ):
538 "Mixin for switches that connect to an ONOSCluster"
539 def start( self, controllers ):
540 "Connect to ONOSCluster"
541 self.controllers = controllers
542 assert ( len( controllers ) is 1 and
Bob Lantz8e576252016-08-29 14:05:59 -0700543 isONOSCluster( controllers[ 0 ] ) )
Bob Lantz087b5d92016-05-06 11:39:04 -0700544 clist = controllers[ 0 ].nodes()
545 return super( ONOSSwitchMixin, self ).start( clist )
546
547class ONOSOVSSwitch( ONOSSwitchMixin, OVSSwitch ):
548 "OVSSwitch that can connect to an ONOSCluster"
549 pass
550
551class ONOSUserSwitch( ONOSSwitchMixin, UserSwitch):
552 "UserSwitch that can connect to an ONOSCluster"
553 pass
554
555
556### Ugly utility routines
557
558def fixIPTables():
559 "Fix LinuxBridge warning"
560 for s in 'arp', 'ip', 'ip6':
561 quietRun( 'sysctl net.bridge.bridge-nf-call-%stables=0' % s )
562
563
564### Test code
565
566def test( serverCount ):
567 "Test this setup"
568 setLogLevel( 'info' )
569 net = Mininet( topo=SingleSwitchTopo( 3 ),
570 controller=[ ONOSCluster( 'c0', serverCount ) ],
571 switch=ONOSOVSSwitch )
572 net.start()
573 net.waitConnected()
574 CLI( net )
575 net.stop()
576
577
578### CLI Extensions
579
580OldCLI = CLI
581
582class ONOSCLI( OldCLI ):
583 "CLI Extensions for ONOS"
584
585 prompt = 'mininet-onos> '
586
587 def __init__( self, net, **kwargs ):
Bob Lantz503a4022016-08-29 18:08:37 -0700588 clusters = [ c.net for c in net.controllers
589 if isONOSCluster( c ) ]
590 net = MininetFacade( net, *clusters )
Bob Lantz087b5d92016-05-06 11:39:04 -0700591 OldCLI.__init__( self, net, **kwargs )
592
Bob Lantz4b51d5c2016-05-27 14:47:38 -0700593 def onos1( self ):
594 "Helper function: return default ONOS node"
595 return self.mn.controllers[ 0 ].net.hosts[ 0 ]
596
Bob Lantz087b5d92016-05-06 11:39:04 -0700597 def do_onos( self, line ):
598 "Send command to ONOS CLI"
599 c0 = self.mn.controllers[ 0 ]
Bob Lantz8e576252016-08-29 14:05:59 -0700600 if isONOSCluster( c0 ):
Bob Lantz087b5d92016-05-06 11:39:04 -0700601 # cmdLoop strips off command name 'onos'
602 if line.startswith( ':' ):
603 line = 'onos' + line
Bob Lantz4b51d5c2016-05-27 14:47:38 -0700604 onos1 = self.onos1().name
605 if line:
606 line = '"%s"' % line
607 cmd = '%s client -h %s %s' % ( onos1, onos1, line )
Bob Lantz087b5d92016-05-06 11:39:04 -0700608 quietRun( 'stty -echo' )
609 self.default( cmd )
610 quietRun( 'stty echo' )
611
612 def do_wait( self, line ):
613 "Wait for switches to connect"
614 self.mn.waitConnected()
615
616 def do_balance( self, line ):
617 "Balance switch mastership"
618 self.do_onos( ':balance-masters' )
619
620 def do_log( self, line ):
Bob Lantz4b51d5c2016-05-27 14:47:38 -0700621 "Run tail -f /tmp/onos1/log; press control-C to stop"
Bob Lantz9ba19dc2016-06-13 20:22:07 -0700622 self.default( '%s tail -f /tmp/%s/log' %
623 ( self.onos1(), self.onos1() ) )
Bob Lantz087b5d92016-05-06 11:39:04 -0700624
Bob Lantz64382422016-06-03 22:51:39 -0700625 def do_status( self, line ):
626 "Return status of ONOS cluster(s)"
627 for c in self.mn.controllers:
Bob Lantz8e576252016-08-29 14:05:59 -0700628 if isONOSCluster( c ):
Bob Lantz64382422016-06-03 22:51:39 -0700629 for node in c.net.hosts:
Bob Lantz8e576252016-08-29 14:05:59 -0700630 if isONOSNode( node ):
Bob Lantz64382422016-06-03 22:51:39 -0700631 errors, warnings = node.checkLog()
632 running = ( 'Running' if node.isRunning()
633 else 'Exited' )
634 status = ''
635 if errors:
636 status += '%d ERRORS ' % len( errors )
637 if warnings:
638 status += '%d warnings' % len( warnings )
639 status = status if status else 'OK'
640 info( node, '\t', running, '\t', status, '\n' )
641
Bob Lantzc96e2582016-06-13 18:57:04 -0700642 def do_arp( self, line ):
643 "Send gratuitous arps from all data network hosts"
644 startTime = time.time()
645 try:
646 count = int( line )
647 except:
648 count = 1
649 # Technically this check should be on the host
Bob Lantzc3de5152016-06-17 17:13:57 -0700650 if '-U' not in quietRun( 'arping -h', shell=True ):
651 warn( 'Please install iputils-arping.\n' )
Bob Lantzc96e2582016-06-13 18:57:04 -0700652 return
653 # This is much faster if we do it in parallel
654 for host in self.mn.net.hosts:
655 intf = host.defaultIntf()
656 # -b: keep using broadcasts; -f: quit after 1 reply
657 # -U: gratuitous ARP update
658 host.sendCmd( 'arping -bf -c', count, '-U -I',
659 intf.name, intf.IP() )
660 for host in self.mn.net.hosts:
661 # We could check the output here if desired
662 host.waitOutput()
663 info( '.' )
664 info( '\n' )
665 elapsed = time.time() - startTime
666 debug( 'Completed in %.2f seconds\n' % elapsed )
667
Carmelo Casconec52a4b12016-10-24 16:47:17 +0200668 def onosupdown( self, cmd, instance ):
669 if not instance:
670 info( 'Provide the name of an ONOS instance.\n' )
671 return
672 c0 = self.mn.controllers[ 0 ]
673 if isONOSCluster( c0 ):
jaegonkim4e360492018-02-17 08:44:35 +0900674 onos = c0.getONOSNode( instance )
675 if onos:
676 info('Bringing %s %s...\n' % ( instance, cmd ) )
677 if cmd == 'up':
678 onos.intfsUp()
679 else:
680 onos.intfsDown()
Carmelo Casconec52a4b12016-10-24 16:47:17 +0200681
682 def do_onosdown( self, instance=None ):
683 """Disconnects an ONOS instance from the network"""
684 self.onosupdown( 'down', instance )
685
686 def do_onosup( self, instance=None ):
687 """"Connects an ONOS instance to the network"""
688 self.onosupdown( 'up', instance )
689
jaegonkim4e360492018-02-17 08:44:35 +0900690 def do_start( self, instance=None ):
691 """Start ONOS instance"""
692 if not instance:
693 info( 'Provide the name of an ONOS instance.\n' )
694 return
695 c0 = self.mn.controllers[ 0 ]
696 if isONOSCluster( c0 ):
697 onos = c0.getONOSNode( instance )
698 if onos:
699 info('Starting %s...\n' % ( instance ) )
jaegonkima1988f32018-04-09 22:52:26 +0900700 onos.start( debug=c0.debug )
jaegonkim4e360492018-02-17 08:44:35 +0900701
702 def do_kill( self, instance=None ):
703 """Kill ONOS instance"""
704 if not instance:
705 info( 'Provide the name of an ONOS instance.\n' )
706 return
707 c0 = self.mn.controllers[ 0 ]
708 if isONOSCluster( c0 ):
709 onos = c0.getONOSNode( instance )
710 if onos:
711 info('Killing %s...\n' % ( instance ) )
712 onos.kill()
Bob Lantz64382422016-06-03 22:51:39 -0700713
714# For interactive use, exit on error
715exitOnError = dict( nodeOpts={ 'alertAction': 'exit' } )
716ONOSClusterInteractive = specialClass( ONOSCluster, defaults=exitOnError )
717
Bob Lantz087b5d92016-05-06 11:39:04 -0700718
719### Exports for bin/mn
720
721CLI = ONOSCLI
Bob Lantz64382422016-06-03 22:51:39 -0700722controllers = { 'onos': ONOSClusterInteractive,
723 'default': ONOSClusterInteractive }
Bob Lantz087b5d92016-05-06 11:39:04 -0700724
725# XXX Hack to change default controller as above doesn't work
Bob Lantz64382422016-06-03 22:51:39 -0700726findController = lambda: controllers[ 'default' ]
Bob Lantz087b5d92016-05-06 11:39:04 -0700727
728switches = { 'onos': ONOSOVSSwitch,
729 'onosovs': ONOSOVSSwitch,
730 'onosuser': ONOSUserSwitch,
731 'default': ONOSOVSSwitch }
732
Bob Lantzbb37d872016-05-16 16:26:13 -0700733# Null topology so we can control an external/hardware network
734topos = { 'none': Topo }
735
Bob Lantz087b5d92016-05-06 11:39:04 -0700736if __name__ == '__main__':
737 if len( argv ) != 2:
738 test( 3 )
739 else:
740 test( int( argv[ 1 ] ) )