blob: 07994213ed6396645b6480c2389c277284c8886e [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
77HOME = ONOS_ROOT = KARAF_ROOT = ONOS_HOME = ONOS_USER = None
78ONOS_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
87 global HOME, ONOS_ROOT, KARAF_ROOT, ONOS_HOME, ONOS_USER
88 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 ]
94 HOME = sd( 'HOME', environ[ 'HOME' ] )
95 assert HOME
96 ONOS_ROOT = sd( 'ONOS_ROOT', join( HOME, 'onos' ) )
97 KARAF_ROOT = sd( 'KARAF_ROOT',
98 glob( join( HOME,
99 'Applications/apache-karaf-*' ) )[ -1 ] )
100 ONOS_HOME = sd( 'ONOS_HOME', dirname( KARAF_ROOT ) )
101 environ[ 'ONOS_USER' ] = defaultUser()
102 ONOS_USER = sd( 'ONOS_USER', defaultUser() )
103 ONOS_APPS = sd( 'ONOS_APPS',
104 'drivers,openflow,fwd,proxyarp,mobility' )
105 # ONOS_WEB_{USER,PASS} isn't respected by onos-karaf:
106 environ.update( ONOS_WEB_USER='karaf', ONOS_WEB_PASS='karaf' )
107 ONOS_WEB_USER = sd( 'ONOS_WEB_USER', 'karaf' )
108 ONOS_WEB_PASS = sd( 'ONOS_WEB_PASS', 'karaf' )
109 return env
110
111
112def updateNodeIPs( env, nodes ):
113 "Update env dict and environ with node IPs"
114 # Get rid of stale junk
115 for var in 'ONOS_NIC', 'ONOS_CELL', 'ONOS_INSTANCES':
116 env[ var ] = ''
117 for var in environ.keys():
118 if var.startswith( 'OC' ):
119 env[ var ] = ''
120 for index, node in enumerate( nodes, 1 ):
121 var = 'OC%d' % index
122 env[ var ] = node.IP()
123 env[ 'OCI' ] = env[ 'OCN' ] = env[ 'OC1' ]
124 env[ 'ONOS_INSTANCES' ] = '\n'.join(
125 node.IP() for node in nodes )
126 environ.update( env )
127 return env
128
129
130tarDefaultPath = 'buck-out/gen/tools/package/onos-package/onos.tar.gz'
131
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 Lantz1451d722016-05-17 14:40:07 -0700142 cmds = ( 'mkdir -p "%s" && cd "%s" && tar xzf "%s"'
Bob Lantz087b5d92016-05-06 11:39:04 -0700143 % ( destDir, destDir, tarPath) )
Bob Lantz1451d722016-05-17 14:40:07 -0700144 run( cmds, shell=True, verbose=True )
145 # We can use quietRun for this usually
146 tarOutput = quietRun( 'tar tzf "%s" | head -1' % tarPath, shell=True)
147 tarOutput = tarOutput.split()[ 0 ].strip()
148 assert '/' in tarOutput
149 onosDir = join( destDir, dirname( tarOutput ) )
Bob Lantz087b5d92016-05-06 11:39:04 -0700150 # Add symlink to log file
Bob Lantz1451d722016-05-17 14:40:07 -0700151 run( 'cd %s; ln -s onos*/apache* karaf;'
152 'ln -s karaf/data/log/karaf.log log' % destDir,
153 shell=True )
Bob Lantz087b5d92016-05-06 11:39:04 -0700154 return onosDir
155
156
157### Mininet classes
158
159def RenamedTopo( topo, *args, **kwargs ):
160 """Return specialized topo with renamed hosts
161 topo: topo class/class name to specialize
162 args, kwargs: topo args
163 sold: old switch name prefix (default 's')
164 snew: new switch name prefix
165 hold: old host name prefix (default 'h')
166 hnew: new host name prefix
167 This may be used from the mn command, e.g.
168 mn --topo renamed,single,spref=sw,hpref=host"""
169 sold = kwargs.pop( 'sold', 's' )
170 hold = kwargs.pop( 'hold', 'h' )
171 snew = kwargs.pop( 'snew', 'cs' )
172 hnew = kwargs.pop( 'hnew' ,'ch' )
173 topos = {} # TODO: use global TOPOS dict
174 if isinstance( topo, str ):
175 # Look up in topo directory - this allows us to
176 # use RenamedTopo from the command line!
177 if topo in topos:
178 topo = topos.get( topo )
179 else:
180 raise Exception( 'Unknown topo name: %s' % topo )
181 # pylint: disable=no-init
182 class RenamedTopoCls( topo ):
183 "Topo subclass with renamed nodes"
184 def addNode( self, name, *args, **kwargs ):
185 "Add a node, renaming if necessary"
186 if name.startswith( sold ):
187 name = snew + name[ len( sold ): ]
188 elif name.startswith( hold ):
189 name = hnew + name[ len( hold ): ]
190 return topo.addNode( self, name, *args, **kwargs )
191 return RenamedTopoCls( *args, **kwargs )
192
193
194class ONOSNode( Controller ):
195 "ONOS cluster node"
196
197 # Default karaf client location
198 client = '/tmp/onos1/karaf/bin/client'
199
200 def __init__( self, name, **kwargs ):
201 kwargs.update( inNamespace=True )
202 Controller.__init__( self, name, **kwargs )
203 self.dir = '/tmp/%s' % self.name
204 # Satisfy pylint
205 self.ONOS_HOME = '/tmp'
206
207 # pylint: disable=arguments-differ
208
209 def start( self, env ):
210 """Start ONOS on node
211 env: environment var dict"""
212 env = dict( env )
213 self.cmd( 'rm -rf', self.dir )
Bob Lantz1451d722016-05-17 14:40:07 -0700214 self.ONOS_HOME = unpackONOS( self.dir, run=self.ucmd )
Bob Lantz087b5d92016-05-06 11:39:04 -0700215 env.update( ONOS_HOME=self.ONOS_HOME )
216 self.updateEnv( env )
217 karafbin = glob( '%s/apache*/bin' % self.ONOS_HOME )[ 0 ]
218 onosbin = join( ONOS_ROOT, 'tools/test/bin' )
219 self.cmd( 'export PATH=%s:%s:$PATH' % ( onosbin, karafbin ) )
220 self.cmd( 'cd', self.ONOS_HOME )
Bob Lantz1451d722016-05-17 14:40:07 -0700221 self.ucmd( 'mkdir -p config && '
222 'onos-gen-partitions config/cluster.json' )
Bob Lantz087b5d92016-05-06 11:39:04 -0700223 info( '(starting %s)' % self )
224 service = join( self.ONOS_HOME, 'bin/onos-service' )
Bob Lantz1451d722016-05-17 14:40:07 -0700225 self.ucmd( service, 'server 1>../onos.log 2>../onos.log'
226 ' & echo $! > onos.pid; ln -s `pwd`/onos.pid ..' )
Bob Lantz087b5d92016-05-06 11:39:04 -0700227
228 # pylint: enable=arguments-differ
229
230 def stop( self ):
231 # XXX This will kill all karafs - too bad!
232 self.cmd( 'pkill -HUP -f karaf.jar && wait' )
233 self.cmd( 'rm -rf', self.dir )
234
235 def waitStarted( self ):
236 "Wait until we've really started"
237 info( '(checking: karaf' )
238 while True:
Bob Lantz1451d722016-05-17 14:40:07 -0700239 status = self.ucmd( 'karaf status' ).lower()
Bob Lantz087b5d92016-05-06 11:39:04 -0700240 if 'running' in status and 'not running' not in status:
241 break
242 info( '.' )
243 time.sleep( 1 )
244 info( ' ssh-port' )
Bob Lantzbb37d872016-05-16 16:26:13 -0700245 waitListening( server=self, port=KarafPort )
Bob Lantz087b5d92016-05-06 11:39:04 -0700246 info( ' openflow-port' )
Bob Lantzbb37d872016-05-16 16:26:13 -0700247 waitListening( server=self, port=OpenFlowPort )
Bob Lantz087b5d92016-05-06 11:39:04 -0700248 info( ' client' )
249 while True:
Bob Lantzbb37d872016-05-16 16:26:13 -0700250 result = quietRun( 'echo apps -a | %s -h %s' %
251 ( self.client, self.IP() ), shell=True )
Bob Lantz087b5d92016-05-06 11:39:04 -0700252 if 'openflow' in result:
253 break
254 info( '.' )
255 time.sleep( 1 )
256 info( ')\n' )
257
258 def updateEnv( self, envDict ):
259 "Update environment variables"
260 cmd = ';'.join( 'export %s="%s"' % ( var, val )
261 for var, val in envDict.iteritems() )
262 self.cmd( cmd )
263
Bob Lantz1451d722016-05-17 14:40:07 -0700264 def ucmd( self, *args, **_kwargs ):
265 "Run command as $ONOS_USER using sudo -E -u"
266 if ONOS_USER != 'root': # don't bother with sudo
267 args = [ "sudo -E -u $ONOS_USER PATH=$PATH "
268 "bash -c '%s'" % ' '.join( args ) ]
269 return self.cmd( *args )
270
Bob Lantz087b5d92016-05-06 11:39:04 -0700271
272class ONOSCluster( Controller ):
273 "ONOS Cluster"
274 def __init__( self, *args, **kwargs ):
275 """name: (first parameter)
276 *args: topology class parameters
277 ipBase: IP range for ONOS nodes
Bob Lantzbb37d872016-05-16 16:26:13 -0700278 forward: default port forwarding list,
Bob Lantz087b5d92016-05-06 11:39:04 -0700279 topo: topology class or instance
280 **kwargs: additional topology parameters"""
281 args = list( args )
282 name = args.pop( 0 )
283 topo = kwargs.pop( 'topo', None )
284 # Default: single switch with 1 ONOS node
285 if not topo:
286 topo = SingleSwitchTopo
287 if not args:
288 args = ( 1, )
289 if not isinstance( topo, Topo ):
290 topo = RenamedTopo( topo, *args, hnew='onos', **kwargs )
Bob Lantzbb37d872016-05-16 16:26:13 -0700291 self.ipBase = kwargs.pop( 'ipBase', '192.168.123.0/24' )
292 self.forward = kwargs.pop( 'forward',
293 [ KarafPort, GUIPort, OpenFlowPort ] )
Bob Lantz087b5d92016-05-06 11:39:04 -0700294 super( ONOSCluster, self ).__init__( name, inNamespace=False )
295 fixIPTables()
296 self.env = initONOSEnv()
Bob Lantzbb37d872016-05-16 16:26:13 -0700297 self.net = Mininet( topo=topo, ipBase=self.ipBase,
Bob Lantz087b5d92016-05-06 11:39:04 -0700298 host=ONOSNode, switch=LinuxBridge,
299 controller=None )
300 self.net.addNAT().configDefault()
301 updateNodeIPs( self.env, self.nodes() )
302 self._remoteControllers = []
303
304 def start( self ):
305 "Start up ONOS cluster"
306 killprocs( 'karaf.jar' )
307 info( '*** ONOS_APPS = %s\n' % ONOS_APPS )
308 self.net.start()
309 for node in self.nodes():
310 node.start( self.env )
311 info( '\n' )
Bob Lantzbb37d872016-05-16 16:26:13 -0700312 self.configPortForwarding( ports=self.forward, action='A' )
Bob Lantz087b5d92016-05-06 11:39:04 -0700313 self.waitStarted()
314 return
315
316 def waitStarted( self ):
317 "Wait until all nodes have started"
318 startTime = time.time()
319 for node in self.nodes():
320 info( node )
321 node.waitStarted()
Bob Lantzbb37d872016-05-16 16:26:13 -0700322 info( '*** Waited %.2f seconds for ONOS startup' %
323 ( time.time() - startTime ) )
Bob Lantz087b5d92016-05-06 11:39:04 -0700324
325 def stop( self ):
326 "Shut down ONOS cluster"
Bob Lantzbb37d872016-05-16 16:26:13 -0700327 self.configPortForwarding( ports=self.forward, action='D' )
Bob Lantz087b5d92016-05-06 11:39:04 -0700328 for node in self.nodes():
329 node.stop()
330 self.net.stop()
331
332 def nodes( self ):
333 "Return list of ONOS nodes"
334 return [ h for h in self.net.hosts if isinstance( h, ONOSNode ) ]
335
Bob Lantzbb37d872016-05-16 16:26:13 -0700336 def configPortForwarding( self, ports=[], intf='eth0', action='A' ):
337 """Start or stop ports on intf to all nodes
338 action: A=add/start, D=delete/stop (default: A)"""
339 for port in ports:
340 for index, node in enumerate( self.nodes() ):
341 ip, inport = node.IP(), port + index
342 # Configure a destination NAT rule
343 cmd = ( 'iptables -t nat -{action} PREROUTING -t nat '
344 '-i {intf} -p tcp --dport {inport} '
345 '-j DNAT --to-destination {ip}:{port}' )
346 self.cmd( cmd.format( **locals() ) )
347
Bob Lantz087b5d92016-05-06 11:39:04 -0700348
349class ONOSSwitchMixin( object ):
350 "Mixin for switches that connect to an ONOSCluster"
351 def start( self, controllers ):
352 "Connect to ONOSCluster"
353 self.controllers = controllers
354 assert ( len( controllers ) is 1 and
355 isinstance( controllers[ 0 ], ONOSCluster ) )
356 clist = controllers[ 0 ].nodes()
357 return super( ONOSSwitchMixin, self ).start( clist )
358
359class ONOSOVSSwitch( ONOSSwitchMixin, OVSSwitch ):
360 "OVSSwitch that can connect to an ONOSCluster"
361 pass
362
363class ONOSUserSwitch( ONOSSwitchMixin, UserSwitch):
364 "UserSwitch that can connect to an ONOSCluster"
365 pass
366
367
368### Ugly utility routines
369
370def fixIPTables():
371 "Fix LinuxBridge warning"
372 for s in 'arp', 'ip', 'ip6':
373 quietRun( 'sysctl net.bridge.bridge-nf-call-%stables=0' % s )
374
375
376### Test code
377
378def test( serverCount ):
379 "Test this setup"
380 setLogLevel( 'info' )
381 net = Mininet( topo=SingleSwitchTopo( 3 ),
382 controller=[ ONOSCluster( 'c0', serverCount ) ],
383 switch=ONOSOVSSwitch )
384 net.start()
385 net.waitConnected()
386 CLI( net )
387 net.stop()
388
389
390### CLI Extensions
391
392OldCLI = CLI
393
394class ONOSCLI( OldCLI ):
395 "CLI Extensions for ONOS"
396
397 prompt = 'mininet-onos> '
398
399 def __init__( self, net, **kwargs ):
400 c0 = net.controllers[ 0 ]
401 if isinstance( c0, ONOSCluster ):
402 net = MininetFacade( net, cnet=c0.net )
403 OldCLI.__init__( self, net, **kwargs )
404
405 def do_onos( self, line ):
406 "Send command to ONOS CLI"
407 c0 = self.mn.controllers[ 0 ]
408 if isinstance( c0, ONOSCluster ):
409 # cmdLoop strips off command name 'onos'
410 if line.startswith( ':' ):
411 line = 'onos' + line
412 cmd = 'onos1 client -h onos1 ' + line
413 quietRun( 'stty -echo' )
414 self.default( cmd )
415 quietRun( 'stty echo' )
416
417 def do_wait( self, line ):
418 "Wait for switches to connect"
419 self.mn.waitConnected()
420
421 def do_balance( self, line ):
422 "Balance switch mastership"
423 self.do_onos( ':balance-masters' )
424
425 def do_log( self, line ):
426 "Run tail -f /tmp/onos1/log on onos1; press control-C to stop"
427 self.default( 'onos1 tail -f /tmp/onos1/log' )
428
429
430### Exports for bin/mn
431
432CLI = ONOSCLI
433
434controllers = { 'onos': ONOSCluster, 'default': ONOSCluster }
435
436# XXX Hack to change default controller as above doesn't work
437findController = lambda: ONOSCluster
438
439switches = { 'onos': ONOSOVSSwitch,
440 'onosovs': ONOSOVSSwitch,
441 'onosuser': ONOSUserSwitch,
442 'default': ONOSOVSSwitch }
443
Bob Lantzbb37d872016-05-16 16:26:13 -0700444# Null topology so we can control an external/hardware network
445topos = { 'none': Topo }
446
Bob Lantz087b5d92016-05-06 11:39:04 -0700447if __name__ == '__main__':
448 if len( argv ) != 2:
449 test( 3 )
450 else:
451 test( int( argv[ 1 ] ) )