blob: 64d5a527aaa95dd74239e748d70286bb6da4a008 [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
202 def start( self, env ):
203 """Start ONOS on node
204 env: environment var dict"""
205 env = dict( env )
206 self.cmd( 'rm -rf', self.dir )
Bob Lantz1451d722016-05-17 14:40:07 -0700207 self.ONOS_HOME = unpackONOS( self.dir, run=self.ucmd )
Bob Lantz087b5d92016-05-06 11:39:04 -0700208 env.update( ONOS_HOME=self.ONOS_HOME )
209 self.updateEnv( env )
210 karafbin = glob( '%s/apache*/bin' % self.ONOS_HOME )[ 0 ]
211 onosbin = join( ONOS_ROOT, 'tools/test/bin' )
212 self.cmd( 'export PATH=%s:%s:$PATH' % ( onosbin, karafbin ) )
213 self.cmd( 'cd', self.ONOS_HOME )
Bob Lantz1451d722016-05-17 14:40:07 -0700214 self.ucmd( 'mkdir -p config && '
215 'onos-gen-partitions config/cluster.json' )
Bob Lantz087b5d92016-05-06 11:39:04 -0700216 info( '(starting %s)' % self )
217 service = join( self.ONOS_HOME, 'bin/onos-service' )
Bob Lantz1451d722016-05-17 14:40:07 -0700218 self.ucmd( service, 'server 1>../onos.log 2>../onos.log'
219 ' & echo $! > onos.pid; ln -s `pwd`/onos.pid ..' )
Bob Lantz4b51d5c2016-05-27 14:47:38 -0700220 self.onosPid = int( self.cmd( 'cat onos.pid' ).strip() )
Bob Lantz087b5d92016-05-06 11:39:04 -0700221
222 # pylint: enable=arguments-differ
223
224 def stop( self ):
225 # XXX This will kill all karafs - too bad!
226 self.cmd( 'pkill -HUP -f karaf.jar && wait' )
227 self.cmd( 'rm -rf', self.dir )
228
Bob Lantz4b51d5c2016-05-27 14:47:38 -0700229 def isRunning( self ):
230 "Is our ONOS process still running?"
231 cmd = 'ps -p %d >/dev/null 2>&1 && echo "running" || echo "not running"'
232 return self.cmd( cmd % self.onosPid ) == 'running'
233
234 def sanityCheck( self ):
235 "Check whether we've quit or are running out of memory"
236 if not self.isRunning():
237 raise Exception( 'ONOS node %s has died' % self.name )
238
Bob Lantz087b5d92016-05-06 11:39:04 -0700239 def waitStarted( self ):
240 "Wait until we've really started"
241 info( '(checking: karaf' )
242 while True:
Bob Lantz1451d722016-05-17 14:40:07 -0700243 status = self.ucmd( 'karaf status' ).lower()
Bob Lantz087b5d92016-05-06 11:39:04 -0700244 if 'running' in status and 'not running' not in status:
245 break
246 info( '.' )
247 time.sleep( 1 )
248 info( ' ssh-port' )
Bob Lantzbb37d872016-05-16 16:26:13 -0700249 waitListening( server=self, port=KarafPort )
Bob Lantz087b5d92016-05-06 11:39:04 -0700250 info( ' openflow-port' )
Bob Lantzbb37d872016-05-16 16:26:13 -0700251 waitListening( server=self, port=OpenFlowPort )
Bob Lantz087b5d92016-05-06 11:39:04 -0700252 info( ' client' )
253 while True:
Bob Lantzbb37d872016-05-16 16:26:13 -0700254 result = quietRun( 'echo apps -a | %s -h %s' %
255 ( self.client, self.IP() ), shell=True )
Bob Lantz087b5d92016-05-06 11:39:04 -0700256 if 'openflow' in result:
257 break
258 info( '.' )
259 time.sleep( 1 )
260 info( ')\n' )
261
262 def updateEnv( self, envDict ):
263 "Update environment variables"
264 cmd = ';'.join( 'export %s="%s"' % ( var, val )
265 for var, val in envDict.iteritems() )
266 self.cmd( cmd )
267
Bob Lantz1451d722016-05-17 14:40:07 -0700268 def ucmd( self, *args, **_kwargs ):
269 "Run command as $ONOS_USER using sudo -E -u"
270 if ONOS_USER != 'root': # don't bother with sudo
271 args = [ "sudo -E -u $ONOS_USER PATH=$PATH "
272 "bash -c '%s'" % ' '.join( args ) ]
273 return self.cmd( *args )
274
Bob Lantz087b5d92016-05-06 11:39:04 -0700275
276class ONOSCluster( Controller ):
277 "ONOS Cluster"
278 def __init__( self, *args, **kwargs ):
279 """name: (first parameter)
280 *args: topology class parameters
281 ipBase: IP range for ONOS nodes
Bob Lantzbb37d872016-05-16 16:26:13 -0700282 forward: default port forwarding list,
Bob Lantz087b5d92016-05-06 11:39:04 -0700283 topo: topology class or instance
284 **kwargs: additional topology parameters"""
285 args = list( args )
286 name = args.pop( 0 )
287 topo = kwargs.pop( 'topo', None )
Bob Lantz4b51d5c2016-05-27 14:47:38 -0700288 nat = kwargs.pop( 'nat', 'nat0' )
Bob Lantz087b5d92016-05-06 11:39:04 -0700289 # Default: single switch with 1 ONOS node
290 if not topo:
291 topo = SingleSwitchTopo
292 if not args:
293 args = ( 1, )
294 if not isinstance( topo, Topo ):
295 topo = RenamedTopo( topo, *args, hnew='onos', **kwargs )
Bob Lantzbb37d872016-05-16 16:26:13 -0700296 self.ipBase = kwargs.pop( 'ipBase', '192.168.123.0/24' )
297 self.forward = kwargs.pop( 'forward',
298 [ KarafPort, GUIPort, OpenFlowPort ] )
Bob Lantz087b5d92016-05-06 11:39:04 -0700299 super( ONOSCluster, self ).__init__( name, inNamespace=False )
300 fixIPTables()
301 self.env = initONOSEnv()
Bob Lantzbb37d872016-05-16 16:26:13 -0700302 self.net = Mininet( topo=topo, ipBase=self.ipBase,
Bob Lantz087b5d92016-05-06 11:39:04 -0700303 host=ONOSNode, switch=LinuxBridge,
304 controller=None )
Bob Lantz4b51d5c2016-05-27 14:47:38 -0700305 if nat:
306 self.net.addNAT( nat ).configDefault()
Bob Lantz087b5d92016-05-06 11:39:04 -0700307 updateNodeIPs( self.env, self.nodes() )
308 self._remoteControllers = []
309
310 def start( self ):
311 "Start up ONOS cluster"
Bob Lantz087b5d92016-05-06 11:39:04 -0700312 info( '*** ONOS_APPS = %s\n' % ONOS_APPS )
313 self.net.start()
314 for node in self.nodes():
315 node.start( self.env )
316 info( '\n' )
Bob Lantzbb37d872016-05-16 16:26:13 -0700317 self.configPortForwarding( ports=self.forward, action='A' )
Bob Lantz087b5d92016-05-06 11:39:04 -0700318 self.waitStarted()
319 return
320
321 def waitStarted( self ):
322 "Wait until all nodes have started"
323 startTime = time.time()
324 for node in self.nodes():
325 info( node )
326 node.waitStarted()
Bob Lantzbb37d872016-05-16 16:26:13 -0700327 info( '*** Waited %.2f seconds for ONOS startup' %
328 ( time.time() - startTime ) )
Bob Lantz087b5d92016-05-06 11:39:04 -0700329
330 def stop( self ):
331 "Shut down ONOS cluster"
Bob Lantzbb37d872016-05-16 16:26:13 -0700332 self.configPortForwarding( ports=self.forward, action='D' )
Bob Lantz087b5d92016-05-06 11:39:04 -0700333 for node in self.nodes():
334 node.stop()
335 self.net.stop()
336
337 def nodes( self ):
338 "Return list of ONOS nodes"
339 return [ h for h in self.net.hosts if isinstance( h, ONOSNode ) ]
340
Bob Lantzbb37d872016-05-16 16:26:13 -0700341 def configPortForwarding( self, ports=[], intf='eth0', action='A' ):
342 """Start or stop ports on intf to all nodes
343 action: A=add/start, D=delete/stop (default: A)"""
344 for port in ports:
345 for index, node in enumerate( self.nodes() ):
346 ip, inport = node.IP(), port + index
347 # Configure a destination NAT rule
348 cmd = ( 'iptables -t nat -{action} PREROUTING -t nat '
349 '-i {intf} -p tcp --dport {inport} '
350 '-j DNAT --to-destination {ip}:{port}' )
351 self.cmd( cmd.format( **locals() ) )
352
Bob Lantz087b5d92016-05-06 11:39:04 -0700353
354class ONOSSwitchMixin( object ):
355 "Mixin for switches that connect to an ONOSCluster"
356 def start( self, controllers ):
357 "Connect to ONOSCluster"
358 self.controllers = controllers
359 assert ( len( controllers ) is 1 and
360 isinstance( controllers[ 0 ], ONOSCluster ) )
361 clist = controllers[ 0 ].nodes()
362 return super( ONOSSwitchMixin, self ).start( clist )
363
364class ONOSOVSSwitch( ONOSSwitchMixin, OVSSwitch ):
365 "OVSSwitch that can connect to an ONOSCluster"
366 pass
367
368class ONOSUserSwitch( ONOSSwitchMixin, UserSwitch):
369 "UserSwitch that can connect to an ONOSCluster"
370 pass
371
372
373### Ugly utility routines
374
375def fixIPTables():
376 "Fix LinuxBridge warning"
377 for s in 'arp', 'ip', 'ip6':
378 quietRun( 'sysctl net.bridge.bridge-nf-call-%stables=0' % s )
379
380
381### Test code
382
383def test( serverCount ):
384 "Test this setup"
385 setLogLevel( 'info' )
386 net = Mininet( topo=SingleSwitchTopo( 3 ),
387 controller=[ ONOSCluster( 'c0', serverCount ) ],
388 switch=ONOSOVSSwitch )
389 net.start()
390 net.waitConnected()
391 CLI( net )
392 net.stop()
393
394
395### CLI Extensions
396
397OldCLI = CLI
398
399class ONOSCLI( OldCLI ):
400 "CLI Extensions for ONOS"
401
402 prompt = 'mininet-onos> '
403
404 def __init__( self, net, **kwargs ):
405 c0 = net.controllers[ 0 ]
406 if isinstance( c0, ONOSCluster ):
407 net = MininetFacade( net, cnet=c0.net )
408 OldCLI.__init__( self, net, **kwargs )
409
Bob Lantz4b51d5c2016-05-27 14:47:38 -0700410 def onos1( self ):
411 "Helper function: return default ONOS node"
412 return self.mn.controllers[ 0 ].net.hosts[ 0 ]
413
Bob Lantz087b5d92016-05-06 11:39:04 -0700414 def do_onos( self, line ):
415 "Send command to ONOS CLI"
416 c0 = self.mn.controllers[ 0 ]
417 if isinstance( c0, ONOSCluster ):
418 # cmdLoop strips off command name 'onos'
419 if line.startswith( ':' ):
420 line = 'onos' + line
Bob Lantz4b51d5c2016-05-27 14:47:38 -0700421 onos1 = self.onos1().name
422 if line:
423 line = '"%s"' % line
424 cmd = '%s client -h %s %s' % ( onos1, onos1, line )
Bob Lantz087b5d92016-05-06 11:39:04 -0700425 quietRun( 'stty -echo' )
426 self.default( cmd )
427 quietRun( 'stty echo' )
428
429 def do_wait( self, line ):
430 "Wait for switches to connect"
431 self.mn.waitConnected()
432
433 def do_balance( self, line ):
434 "Balance switch mastership"
435 self.do_onos( ':balance-masters' )
436
437 def do_log( self, line ):
Bob Lantz4b51d5c2016-05-27 14:47:38 -0700438 "Run tail -f /tmp/onos1/log; press control-C to stop"
439 self.default( self.onos1().name, 'tail -f /tmp/%s/log' % self.onos1() )
Bob Lantz087b5d92016-05-06 11:39:04 -0700440
441
442### Exports for bin/mn
443
444CLI = ONOSCLI
445
446controllers = { 'onos': ONOSCluster, 'default': ONOSCluster }
447
448# XXX Hack to change default controller as above doesn't work
449findController = lambda: ONOSCluster
450
451switches = { 'onos': ONOSOVSSwitch,
452 'onosovs': ONOSOVSSwitch,
453 'onosuser': ONOSUserSwitch,
454 'default': ONOSOVSSwitch }
455
Bob Lantzbb37d872016-05-16 16:26:13 -0700456# Null topology so we can control an external/hardware network
457topos = { 'none': Topo }
458
Bob Lantz087b5d92016-05-06 11:39:04 -0700459if __name__ == '__main__':
460 if len( argv ) != 2:
461 test( 3 )
462 else:
463 test( int( argv[ 1 ] ) )