blob: b3d9b4b634d2cc962730cea505a35bdfdfa386a1 [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
23Currently you meed to specify a custom switch class
24because 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
49from mininet.util import quietRun, errRun, waitListening
50from 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
62KarafPort = 8101 # ssh port indicating karaf is running
63
64def 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
76HOME = ONOS_ROOT = KARAF_ROOT = ONOS_HOME = ONOS_USER = None
77ONOS_APPS = ONOS_WEB_USER = ONOS_WEB_PASS = ONOS_TAR = None
78
79def 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
111def 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
129tarDefaultPath = 'buck-out/gen/tools/package/onos-package/onos.tar.gz'
130
131def 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
156def 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
191class 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
262class 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
321class 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
331class ONOSOVSSwitch( ONOSSwitchMixin, OVSSwitch ):
332 "OVSSwitch that can connect to an ONOSCluster"
333 pass
334
335class ONOSUserSwitch( ONOSSwitchMixin, UserSwitch):
336 "UserSwitch that can connect to an ONOSCluster"
337 pass
338
339
340### Ugly utility routines
341
342def 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
350def 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
364OldCLI = CLI
365
366class 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
404CLI = ONOSCLI
405
406controllers = { 'onos': ONOSCluster, 'default': ONOSCluster }
407
408# XXX Hack to change default controller as above doesn't work
409findController = lambda: ONOSCluster
410
411switches = { 'onos': ONOSOVSSwitch,
412 'onosovs': ONOSOVSSwitch,
413 'onosuser': ONOSUserSwitch,
414 'default': ONOSOVSSwitch }
415
416if __name__ == '__main__':
417 if len( argv ) != 2:
418 test( 3 )
419 else:
420 test( int( argv[ 1 ] ) )