blob: 6e66f26da3418c9ec62bb04243ee2ea09afe7208 [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
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
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
132def unpackONOS( destDir='/tmp' ):
133 "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)
142 cmds = ( 'mkdir -p "%s" && cd "%s" && tar xvzf "%s"'
143 % ( destDir, destDir, tarPath) )
144 out, _err, _code = errRun( cmds, shell=True, verbose=True )
145 first = out.split( '\n' )[ 0 ]
146 assert '/' in first
147 onosDir = join( destDir, dirname( first ) )
148 # Add symlink to log file
149 quietRun( 'cd %s; ln -s onos*/apache* karaf;'
150 'ln -s karaf/data/log/karaf.log log' % destDir,
151 shell=True )
152 return onosDir
153
154
155### Mininet classes
156
157def RenamedTopo( topo, *args, **kwargs ):
158 """Return specialized topo with renamed hosts
159 topo: topo class/class name to specialize
160 args, kwargs: topo args
161 sold: old switch name prefix (default 's')
162 snew: new switch name prefix
163 hold: old host name prefix (default 'h')
164 hnew: new host name prefix
165 This may be used from the mn command, e.g.
166 mn --topo renamed,single,spref=sw,hpref=host"""
167 sold = kwargs.pop( 'sold', 's' )
168 hold = kwargs.pop( 'hold', 'h' )
169 snew = kwargs.pop( 'snew', 'cs' )
170 hnew = kwargs.pop( 'hnew' ,'ch' )
171 topos = {} # TODO: use global TOPOS dict
172 if isinstance( topo, str ):
173 # Look up in topo directory - this allows us to
174 # use RenamedTopo from the command line!
175 if topo in topos:
176 topo = topos.get( topo )
177 else:
178 raise Exception( 'Unknown topo name: %s' % topo )
179 # pylint: disable=no-init
180 class RenamedTopoCls( topo ):
181 "Topo subclass with renamed nodes"
182 def addNode( self, name, *args, **kwargs ):
183 "Add a node, renaming if necessary"
184 if name.startswith( sold ):
185 name = snew + name[ len( sold ): ]
186 elif name.startswith( hold ):
187 name = hnew + name[ len( hold ): ]
188 return topo.addNode( self, name, *args, **kwargs )
189 return RenamedTopoCls( *args, **kwargs )
190
191
192class ONOSNode( Controller ):
193 "ONOS cluster node"
194
195 # Default karaf client location
196 client = '/tmp/onos1/karaf/bin/client'
197
198 def __init__( self, name, **kwargs ):
199 kwargs.update( inNamespace=True )
200 Controller.__init__( self, name, **kwargs )
201 self.dir = '/tmp/%s' % self.name
202 # Satisfy pylint
203 self.ONOS_HOME = '/tmp'
204
205 # pylint: disable=arguments-differ
206
207 def start( self, env ):
208 """Start ONOS on node
209 env: environment var dict"""
210 env = dict( env )
211 self.cmd( 'rm -rf', self.dir )
212 self.ONOS_HOME = unpackONOS( self.dir )
213 env.update( ONOS_HOME=self.ONOS_HOME )
214 self.updateEnv( env )
215 karafbin = glob( '%s/apache*/bin' % self.ONOS_HOME )[ 0 ]
216 onosbin = join( ONOS_ROOT, 'tools/test/bin' )
217 self.cmd( 'export PATH=%s:%s:$PATH' % ( onosbin, karafbin ) )
218 self.cmd( 'cd', self.ONOS_HOME )
219 self.cmd( 'mkdir -p config && '
220 'onos-gen-partitions config/cluster.json' )
221 info( '(starting %s)' % self )
222 service = join( self.ONOS_HOME, 'bin/onos-service' )
223 self.cmd( service, 'server 1>../onos.log 2>../onos.log &' )
224 self.cmd( 'echo $! > onos.pid' )
225
226 # pylint: enable=arguments-differ
227
228 def stop( self ):
229 # XXX This will kill all karafs - too bad!
230 self.cmd( 'pkill -HUP -f karaf.jar && wait' )
231 self.cmd( 'rm -rf', self.dir )
232
233 def waitStarted( self ):
234 "Wait until we've really started"
235 info( '(checking: karaf' )
236 while True:
237 status = self.cmd( 'karaf status' ).lower()
238 if 'running' in status and 'not running' not in status:
239 break
240 info( '.' )
241 time.sleep( 1 )
242 info( ' ssh-port' )
Bob Lantzbb37d872016-05-16 16:26:13 -0700243 waitListening( server=self, port=KarafPort )
Bob Lantz087b5d92016-05-06 11:39:04 -0700244 info( ' openflow-port' )
Bob Lantzbb37d872016-05-16 16:26:13 -0700245 waitListening( server=self, port=OpenFlowPort )
Bob Lantz087b5d92016-05-06 11:39:04 -0700246 info( ' client' )
247 while True:
Bob Lantzbb37d872016-05-16 16:26:13 -0700248 result = quietRun( 'echo apps -a | %s -h %s' %
249 ( self.client, self.IP() ), shell=True )
Bob Lantz087b5d92016-05-06 11:39:04 -0700250 if 'openflow' in result:
251 break
252 info( '.' )
253 time.sleep( 1 )
254 info( ')\n' )
255
256 def updateEnv( self, envDict ):
257 "Update environment variables"
258 cmd = ';'.join( 'export %s="%s"' % ( var, val )
259 for var, val in envDict.iteritems() )
260 self.cmd( cmd )
261
262
263class ONOSCluster( Controller ):
264 "ONOS Cluster"
265 def __init__( self, *args, **kwargs ):
266 """name: (first parameter)
267 *args: topology class parameters
268 ipBase: IP range for ONOS nodes
Bob Lantzbb37d872016-05-16 16:26:13 -0700269 forward: default port forwarding list,
Bob Lantz087b5d92016-05-06 11:39:04 -0700270 topo: topology class or instance
271 **kwargs: additional topology parameters"""
272 args = list( args )
273 name = args.pop( 0 )
274 topo = kwargs.pop( 'topo', None )
275 # Default: single switch with 1 ONOS node
276 if not topo:
277 topo = SingleSwitchTopo
278 if not args:
279 args = ( 1, )
280 if not isinstance( topo, Topo ):
281 topo = RenamedTopo( topo, *args, hnew='onos', **kwargs )
Bob Lantzbb37d872016-05-16 16:26:13 -0700282 self.ipBase = kwargs.pop( 'ipBase', '192.168.123.0/24' )
283 self.forward = kwargs.pop( 'forward',
284 [ KarafPort, GUIPort, OpenFlowPort ] )
Bob Lantz087b5d92016-05-06 11:39:04 -0700285 super( ONOSCluster, self ).__init__( name, inNamespace=False )
286 fixIPTables()
287 self.env = initONOSEnv()
Bob Lantzbb37d872016-05-16 16:26:13 -0700288 self.net = Mininet( topo=topo, ipBase=self.ipBase,
Bob Lantz087b5d92016-05-06 11:39:04 -0700289 host=ONOSNode, switch=LinuxBridge,
290 controller=None )
291 self.net.addNAT().configDefault()
292 updateNodeIPs( self.env, self.nodes() )
293 self._remoteControllers = []
294
295 def start( self ):
296 "Start up ONOS cluster"
297 killprocs( 'karaf.jar' )
298 info( '*** ONOS_APPS = %s\n' % ONOS_APPS )
299 self.net.start()
300 for node in self.nodes():
301 node.start( self.env )
302 info( '\n' )
Bob Lantzbb37d872016-05-16 16:26:13 -0700303 self.configPortForwarding( ports=self.forward, action='A' )
Bob Lantz087b5d92016-05-06 11:39:04 -0700304 self.waitStarted()
305 return
306
307 def waitStarted( self ):
308 "Wait until all nodes have started"
309 startTime = time.time()
310 for node in self.nodes():
311 info( node )
312 node.waitStarted()
Bob Lantzbb37d872016-05-16 16:26:13 -0700313 info( '*** Waited %.2f seconds for ONOS startup' %
314 ( time.time() - startTime ) )
Bob Lantz087b5d92016-05-06 11:39:04 -0700315
316 def stop( self ):
317 "Shut down ONOS cluster"
Bob Lantzbb37d872016-05-16 16:26:13 -0700318 self.configPortForwarding( ports=self.forward, action='D' )
Bob Lantz087b5d92016-05-06 11:39:04 -0700319 for node in self.nodes():
320 node.stop()
321 self.net.stop()
322
323 def nodes( self ):
324 "Return list of ONOS nodes"
325 return [ h for h in self.net.hosts if isinstance( h, ONOSNode ) ]
326
Bob Lantzbb37d872016-05-16 16:26:13 -0700327 def configPortForwarding( self, ports=[], intf='eth0', action='A' ):
328 """Start or stop ports on intf to all nodes
329 action: A=add/start, D=delete/stop (default: A)"""
330 for port in ports:
331 for index, node in enumerate( self.nodes() ):
332 ip, inport = node.IP(), port + index
333 # Configure a destination NAT rule
334 cmd = ( 'iptables -t nat -{action} PREROUTING -t nat '
335 '-i {intf} -p tcp --dport {inport} '
336 '-j DNAT --to-destination {ip}:{port}' )
337 self.cmd( cmd.format( **locals() ) )
338
Bob Lantz087b5d92016-05-06 11:39:04 -0700339
340class ONOSSwitchMixin( object ):
341 "Mixin for switches that connect to an ONOSCluster"
342 def start( self, controllers ):
343 "Connect to ONOSCluster"
344 self.controllers = controllers
345 assert ( len( controllers ) is 1 and
346 isinstance( controllers[ 0 ], ONOSCluster ) )
347 clist = controllers[ 0 ].nodes()
348 return super( ONOSSwitchMixin, self ).start( clist )
349
350class ONOSOVSSwitch( ONOSSwitchMixin, OVSSwitch ):
351 "OVSSwitch that can connect to an ONOSCluster"
352 pass
353
354class ONOSUserSwitch( ONOSSwitchMixin, UserSwitch):
355 "UserSwitch that can connect to an ONOSCluster"
356 pass
357
358
359### Ugly utility routines
360
361def fixIPTables():
362 "Fix LinuxBridge warning"
363 for s in 'arp', 'ip', 'ip6':
364 quietRun( 'sysctl net.bridge.bridge-nf-call-%stables=0' % s )
365
366
367### Test code
368
369def test( serverCount ):
370 "Test this setup"
371 setLogLevel( 'info' )
372 net = Mininet( topo=SingleSwitchTopo( 3 ),
373 controller=[ ONOSCluster( 'c0', serverCount ) ],
374 switch=ONOSOVSSwitch )
375 net.start()
376 net.waitConnected()
377 CLI( net )
378 net.stop()
379
380
381### CLI Extensions
382
383OldCLI = CLI
384
385class ONOSCLI( OldCLI ):
386 "CLI Extensions for ONOS"
387
388 prompt = 'mininet-onos> '
389
390 def __init__( self, net, **kwargs ):
391 c0 = net.controllers[ 0 ]
392 if isinstance( c0, ONOSCluster ):
393 net = MininetFacade( net, cnet=c0.net )
394 OldCLI.__init__( self, net, **kwargs )
395
396 def do_onos( self, line ):
397 "Send command to ONOS CLI"
398 c0 = self.mn.controllers[ 0 ]
399 if isinstance( c0, ONOSCluster ):
400 # cmdLoop strips off command name 'onos'
401 if line.startswith( ':' ):
402 line = 'onos' + line
403 cmd = 'onos1 client -h onos1 ' + line
404 quietRun( 'stty -echo' )
405 self.default( cmd )
406 quietRun( 'stty echo' )
407
408 def do_wait( self, line ):
409 "Wait for switches to connect"
410 self.mn.waitConnected()
411
412 def do_balance( self, line ):
413 "Balance switch mastership"
414 self.do_onos( ':balance-masters' )
415
416 def do_log( self, line ):
417 "Run tail -f /tmp/onos1/log on onos1; press control-C to stop"
418 self.default( 'onos1 tail -f /tmp/onos1/log' )
419
420
421### Exports for bin/mn
422
423CLI = ONOSCLI
424
425controllers = { 'onos': ONOSCluster, 'default': ONOSCluster }
426
427# XXX Hack to change default controller as above doesn't work
428findController = lambda: ONOSCluster
429
430switches = { 'onos': ONOSOVSSwitch,
431 'onosovs': ONOSOVSSwitch,
432 'onosuser': ONOSUserSwitch,
433 'default': ONOSOVSSwitch }
434
Bob Lantzbb37d872016-05-16 16:26:13 -0700435# Null topology so we can control an external/hardware network
436topos = { 'none': Topo }
437
Bob Lantz087b5d92016-05-06 11:39:04 -0700438if __name__ == '__main__':
439 if len( argv ) != 2:
440 test( 3 )
441 else:
442 test( int( argv[ 1 ] ) )