Bob Lantz
Committed by Gerrit Code Review

onos.py: ONOS cluster and control network modeling in Mininet

This is intended to facilitate ONOS development and testing when
you require an ONOS cluster and a modeled control network. More
information is available in the file comments/module docstring.

Change-Id: I8a7338b61bd21eb82ea4e27adbf3cea15be312ee
1 +#!/usr/bin/python
2 +
3 +"""
4 +onos.py: ONOS cluster and control network in Mininet
5 +
6 +With onos.py, you can use Mininet to create a complete
7 +ONOS network, including an ONOS cluster with a modeled
8 +control network as well as the usual data nework.
9 +
10 +This is intended to be useful for distributed ONOS
11 +development and testing in the case that you require
12 +a modeled control network.
13 +
14 +Invocation (using OVS as default switch):
15 +
16 +mn --custom onos.py --controller onos,3 --topo torus,4,4
17 +
18 +Or with the user switch (or CPqD if installed):
19 +
20 +mn --custom onos.py --controller onos,3 \
21 + --switch onosuser --topo torus,4,4
22 +
23 +Currently you meed to specify a custom switch class
24 +because Mininet's Switch() class does't (yet?) handle
25 +controllers with multiple IP addresses directly.
26 +
27 +The classes may also be imported and used via Mininet's
28 +python API.
29 +
30 +Bugs/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 +
43 +from mininet.node import Controller, OVSSwitch, UserSwitch
44 +from mininet.nodelib import LinuxBridge
45 +from mininet.net import Mininet
46 +from mininet.topo import SingleSwitchTopo, Topo
47 +from mininet.log import setLogLevel, info
48 +from mininet.cli import CLI
49 +from mininet.util import quietRun, errRun, waitListening
50 +from mininet.clean import killprocs
51 +from mininet.examples.controlnet import MininetFacade
52 +
53 +from os import environ
54 +from os.path import dirname, join, isfile
55 +from sys import argv
56 +from glob import glob
57 +import time
58 +
59 +
60 +### ONOS Environment
61 +
62 +KarafPort = 8101 # ssh port indicating karaf is running
63 +
64 +def defaultUser():
65 + "Return a reasonable default user"
66 + if 'SUDO_USER' in environ:
67 + return environ[ 'SUDO_USER' ]
68 + try:
69 + user = quietRun( 'who am i' ).split()[ 0 ]
70 + except:
71 + user = 'nobody'
72 + return user
73 +
74 +
75 +# Module vars, initialized below
76 +HOME = ONOS_ROOT = KARAF_ROOT = ONOS_HOME = ONOS_USER = None
77 +ONOS_APPS = ONOS_WEB_USER = ONOS_WEB_PASS = ONOS_TAR = None
78 +
79 +def initONOSEnv():
80 + """Initialize ONOS environment (and module) variables
81 + This is ugly and painful, but they have to be set correctly
82 + in order for the onos-setup-karaf script to work.
83 + nodes: list of ONOS nodes
84 + returns: ONOS environment variable dict"""
85 + # pylint: disable=global-statement
86 + global HOME, ONOS_ROOT, KARAF_ROOT, ONOS_HOME, ONOS_USER
87 + global ONOS_APPS, ONOS_WEB_USER, ONOS_WEB_PASS
88 + env = {}
89 + def sd( var, val ):
90 + "Set default value for environment variable"
91 + env[ var ] = environ.setdefault( var, val )
92 + return env[ var ]
93 + HOME = sd( 'HOME', environ[ 'HOME' ] )
94 + assert HOME
95 + ONOS_ROOT = sd( 'ONOS_ROOT', join( HOME, 'onos' ) )
96 + KARAF_ROOT = sd( 'KARAF_ROOT',
97 + glob( join( HOME,
98 + 'Applications/apache-karaf-*' ) )[ -1 ] )
99 + ONOS_HOME = sd( 'ONOS_HOME', dirname( KARAF_ROOT ) )
100 + environ[ 'ONOS_USER' ] = defaultUser()
101 + ONOS_USER = sd( 'ONOS_USER', defaultUser() )
102 + ONOS_APPS = sd( 'ONOS_APPS',
103 + 'drivers,openflow,fwd,proxyarp,mobility' )
104 + # 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 +
111 +def updateNodeIPs( env, nodes ):
112 + "Update env dict and environ with node IPs"
113 + # Get rid of stale junk
114 + for var in 'ONOS_NIC', 'ONOS_CELL', 'ONOS_INSTANCES':
115 + 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()
122 + env[ 'OCI' ] = env[ 'OCN' ] = env[ 'OC1' ]
123 + env[ 'ONOS_INSTANCES' ] = '\n'.join(
124 + node.IP() for node in nodes )
125 + environ.update( env )
126 + return env
127 +
128 +
129 +tarDefaultPath = 'buck-out/gen/tools/package/onos-package/onos.tar.gz'
130 +
131 +def unpackONOS( destDir='/tmp' ):
132 + "Unpack ONOS and return its location"
133 + global ONOS_TAR
134 + environ.setdefault( 'ONOS_TAR', join( ONOS_ROOT, tarDefaultPath ) )
135 + ONOS_TAR = environ[ 'ONOS_TAR' ]
136 + tarPath = ONOS_TAR
137 + if not isfile( tarPath ):
138 + raise Exception( 'Missing ONOS tarball %s - run buck build onos?'
139 + % tarPath )
140 + info( '(unpacking %s)' % destDir)
141 + cmds = ( 'mkdir -p "%s" && cd "%s" && tar xvzf "%s"'
142 + % ( destDir, destDir, tarPath) )
143 + out, _err, _code = errRun( cmds, shell=True, verbose=True )
144 + first = out.split( '\n' )[ 0 ]
145 + assert '/' in first
146 + onosDir = join( destDir, dirname( first ) )
147 + # Add symlink to log file
148 + quietRun( 'cd %s; ln -s onos*/apache* karaf;'
149 + 'ln -s karaf/data/log/karaf.log log' % destDir,
150 + shell=True )
151 + return onosDir
152 +
153 +
154 +### Mininet classes
155 +
156 +def RenamedTopo( topo, *args, **kwargs ):
157 + """Return specialized topo with renamed hosts
158 + topo: topo class/class name to specialize
159 + args, kwargs: topo args
160 + sold: old switch name prefix (default 's')
161 + snew: new switch name prefix
162 + hold: old host name prefix (default 'h')
163 + hnew: new host name prefix
164 + This may be used from the mn command, e.g.
165 + mn --topo renamed,single,spref=sw,hpref=host"""
166 + sold = kwargs.pop( 'sold', 's' )
167 + hold = kwargs.pop( 'hold', 'h' )
168 + snew = kwargs.pop( 'snew', 'cs' )
169 + hnew = kwargs.pop( 'hnew' ,'ch' )
170 + topos = {} # TODO: use global TOPOS dict
171 + if isinstance( topo, str ):
172 + # Look up in topo directory - this allows us to
173 + # use RenamedTopo from the command line!
174 + if topo in topos:
175 + topo = topos.get( topo )
176 + else:
177 + raise Exception( 'Unknown topo name: %s' % topo )
178 + # pylint: disable=no-init
179 + class RenamedTopoCls( topo ):
180 + "Topo subclass with renamed nodes"
181 + def addNode( self, name, *args, **kwargs ):
182 + "Add a node, renaming if necessary"
183 + if name.startswith( sold ):
184 + name = snew + name[ len( sold ): ]
185 + elif name.startswith( hold ):
186 + name = hnew + name[ len( hold ): ]
187 + return topo.addNode( self, name, *args, **kwargs )
188 + return RenamedTopoCls( *args, **kwargs )
189 +
190 +
191 +class ONOSNode( Controller ):
192 + "ONOS cluster node"
193 +
194 + # Default karaf client location
195 + client = '/tmp/onos1/karaf/bin/client'
196 +
197 + def __init__( self, name, **kwargs ):
198 + kwargs.update( inNamespace=True )
199 + Controller.__init__( self, name, **kwargs )
200 + self.dir = '/tmp/%s' % self.name
201 + # Satisfy pylint
202 + self.ONOS_HOME = '/tmp'
203 +
204 + # pylint: disable=arguments-differ
205 +
206 + def start( self, env ):
207 + """Start ONOS on node
208 + env: environment var dict"""
209 + env = dict( env )
210 + self.cmd( 'rm -rf', self.dir )
211 + self.ONOS_HOME = unpackONOS( self.dir )
212 + env.update( ONOS_HOME=self.ONOS_HOME )
213 + self.updateEnv( env )
214 + karafbin = glob( '%s/apache*/bin' % self.ONOS_HOME )[ 0 ]
215 + onosbin = join( ONOS_ROOT, 'tools/test/bin' )
216 + self.cmd( 'export PATH=%s:%s:$PATH' % ( onosbin, karafbin ) )
217 + self.cmd( 'cd', self.ONOS_HOME )
218 + self.cmd( 'mkdir -p config && '
219 + 'onos-gen-partitions config/cluster.json' )
220 + info( '(starting %s)' % self )
221 + service = join( self.ONOS_HOME, 'bin/onos-service' )
222 + self.cmd( service, 'server 1>../onos.log 2>../onos.log &' )
223 + self.cmd( 'echo $! > onos.pid' )
224 +
225 + # pylint: enable=arguments-differ
226 +
227 + def stop( self ):
228 + # XXX This will kill all karafs - too bad!
229 + self.cmd( 'pkill -HUP -f karaf.jar && wait' )
230 + self.cmd( 'rm -rf', self.dir )
231 +
232 + def waitStarted( self ):
233 + "Wait until we've really started"
234 + info( '(checking: karaf' )
235 + while True:
236 + status = self.cmd( 'karaf status' ).lower()
237 + if 'running' in status and 'not running' not in status:
238 + break
239 + info( '.' )
240 + time.sleep( 1 )
241 + info( ' ssh-port' )
242 + waitListening( client=self, server=self, port=8101 )
243 + info( ' openflow-port' )
244 + waitListening( server=self, port=6653 )
245 + info( ' client' )
246 + while True:
247 + result = quietRun( 'echo apps -a | %s -h %s' % ( self.client, self.IP() ),
248 + shell=True )
249 + if 'openflow' in result:
250 + break
251 + info( '.' )
252 + time.sleep( 1 )
253 + info( ')\n' )
254 +
255 + def updateEnv( self, envDict ):
256 + "Update environment variables"
257 + cmd = ';'.join( 'export %s="%s"' % ( var, val )
258 + for var, val in envDict.iteritems() )
259 + self.cmd( cmd )
260 +
261 +
262 +class ONOSCluster( Controller ):
263 + "ONOS Cluster"
264 + def __init__( self, *args, **kwargs ):
265 + """name: (first parameter)
266 + *args: topology class parameters
267 + ipBase: IP range for ONOS nodes
268 + topo: topology class or instance
269 + **kwargs: additional topology parameters"""
270 + args = list( args )
271 + name = args.pop( 0 )
272 + topo = kwargs.pop( 'topo', None )
273 + # Default: single switch with 1 ONOS node
274 + if not topo:
275 + topo = SingleSwitchTopo
276 + if not args:
277 + args = ( 1, )
278 + if not isinstance( topo, Topo ):
279 + topo = RenamedTopo( topo, *args, hnew='onos', **kwargs )
280 + ipBase = kwargs.pop( 'ipbase', '192.168.123.0/24' )
281 + super( ONOSCluster, self ).__init__( name, inNamespace=False )
282 + fixIPTables()
283 + self.env = initONOSEnv()
284 + self.net = Mininet( topo=topo, ipBase=ipBase,
285 + host=ONOSNode, switch=LinuxBridge,
286 + controller=None )
287 + self.net.addNAT().configDefault()
288 + updateNodeIPs( self.env, self.nodes() )
289 + self._remoteControllers = []
290 +
291 + def start( self ):
292 + "Start up ONOS cluster"
293 + killprocs( 'karaf.jar' )
294 + info( '*** ONOS_APPS = %s\n' % ONOS_APPS )
295 + self.net.start()
296 + for node in self.nodes():
297 + node.start( self.env )
298 + info( '\n' )
299 + self.waitStarted()
300 + return
301 +
302 + def waitStarted( self ):
303 + "Wait until all nodes have started"
304 + startTime = time.time()
305 + for node in self.nodes():
306 + info( node )
307 + node.waitStarted()
308 + info( '*** Waited %.2f seconds for ONOS startup' % ( time.time() - startTime ) )
309 +
310 + def stop( self ):
311 + "Shut down ONOS cluster"
312 + for node in self.nodes():
313 + node.stop()
314 + self.net.stop()
315 +
316 + def nodes( self ):
317 + "Return list of ONOS nodes"
318 + return [ h for h in self.net.hosts if isinstance( h, ONOSNode ) ]
319 +
320 +
321 +class ONOSSwitchMixin( object ):
322 + "Mixin for switches that connect to an ONOSCluster"
323 + def start( self, controllers ):
324 + "Connect to ONOSCluster"
325 + self.controllers = controllers
326 + assert ( len( controllers ) is 1 and
327 + isinstance( controllers[ 0 ], ONOSCluster ) )
328 + clist = controllers[ 0 ].nodes()
329 + return super( ONOSSwitchMixin, self ).start( clist )
330 +
331 +class ONOSOVSSwitch( ONOSSwitchMixin, OVSSwitch ):
332 + "OVSSwitch that can connect to an ONOSCluster"
333 + pass
334 +
335 +class ONOSUserSwitch( ONOSSwitchMixin, UserSwitch):
336 + "UserSwitch that can connect to an ONOSCluster"
337 + pass
338 +
339 +
340 +### Ugly utility routines
341 +
342 +def fixIPTables():
343 + "Fix LinuxBridge warning"
344 + for s in 'arp', 'ip', 'ip6':
345 + quietRun( 'sysctl net.bridge.bridge-nf-call-%stables=0' % s )
346 +
347 +
348 +### Test code
349 +
350 +def test( serverCount ):
351 + "Test this setup"
352 + setLogLevel( 'info' )
353 + net = Mininet( topo=SingleSwitchTopo( 3 ),
354 + controller=[ ONOSCluster( 'c0', serverCount ) ],
355 + switch=ONOSOVSSwitch )
356 + net.start()
357 + net.waitConnected()
358 + CLI( net )
359 + net.stop()
360 +
361 +
362 +### CLI Extensions
363 +
364 +OldCLI = CLI
365 +
366 +class ONOSCLI( OldCLI ):
367 + "CLI Extensions for ONOS"
368 +
369 + prompt = 'mininet-onos> '
370 +
371 + def __init__( self, net, **kwargs ):
372 + c0 = net.controllers[ 0 ]
373 + if isinstance( c0, ONOSCluster ):
374 + net = MininetFacade( net, cnet=c0.net )
375 + OldCLI.__init__( self, net, **kwargs )
376 +
377 + def do_onos( self, line ):
378 + "Send command to ONOS CLI"
379 + c0 = self.mn.controllers[ 0 ]
380 + if isinstance( c0, ONOSCluster ):
381 + # cmdLoop strips off command name 'onos'
382 + if line.startswith( ':' ):
383 + line = 'onos' + line
384 + cmd = 'onos1 client -h onos1 ' + line
385 + quietRun( 'stty -echo' )
386 + self.default( cmd )
387 + quietRun( 'stty echo' )
388 +
389 + def do_wait( self, line ):
390 + "Wait for switches to connect"
391 + self.mn.waitConnected()
392 +
393 + def do_balance( self, line ):
394 + "Balance switch mastership"
395 + self.do_onos( ':balance-masters' )
396 +
397 + def do_log( self, line ):
398 + "Run tail -f /tmp/onos1/log on onos1; press control-C to stop"
399 + self.default( 'onos1 tail -f /tmp/onos1/log' )
400 +
401 +
402 +### Exports for bin/mn
403 +
404 +CLI = ONOSCLI
405 +
406 +controllers = { 'onos': ONOSCluster, 'default': ONOSCluster }
407 +
408 +# XXX Hack to change default controller as above doesn't work
409 +findController = lambda: ONOSCluster
410 +
411 +switches = { 'onos': ONOSOVSSwitch,
412 + 'onosovs': ONOSOVSSwitch,
413 + 'onosuser': ONOSUserSwitch,
414 + 'default': ONOSOVSSwitch }
415 +
416 +if __name__ == '__main__':
417 + if len( argv ) != 2:
418 + test( 3 )
419 + else:
420 + test( int( argv[ 1 ] ) )