blob: f0d7f8b0b03e32e47eaec33bcb3349f3eab62359 [file] [log] [blame]
You Wang84f981d2018-01-12 16:11:50 -08001#!/usr/bin/env python
2"""
3Copyright 2018 Open Networking Foundation (ONF)
4
5Please refer questions to either the onos test mailing list at <onos-test@onosproject.org>,
6the System Testing Plans and Results wiki page at <https://wiki.onosproject.org/x/voMg>,
7or the System Testing Guide page at <https://wiki.onosproject.org/x/WYQg>
8
9 TestON is free software: you can redistribute it and/or modify
10 it under the terms of the GNU General Public License as published by
11 the Free Software Foundation, either version 2 of the License, or
12 (at your option) any later version.
13
14 TestON is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
18
19 You should have received a copy of the GNU General Public License
20 along with TestON. If not, see <http://www.gnu.org/licenses/>.
21
22
23This driver is used to interact with a physical network that SDN controller is controlling.
24
25Please refer questions to either the onos test mailing list at <onos-test@onosproject.org>,
26the System Testing Plans and Results wiki page at <https://wiki.onosproject.org/x/voMg>,
27or the System Testing Guide page at <https://wiki.onosproject.org/x/WYQg>
28
29"""
30import pexpect
31import os
You Wang4cc61912018-08-28 10:10:58 -070032import re
You Wang84f981d2018-01-12 16:11:50 -080033import types
34from drivers.common.clidriver import CLI
35
36class NetworkDriver( CLI ):
37
38 def __init__( self ):
39 """
40 switches: a dictionary that maps switch names to components
41 hosts: a dictionary that maps host names to components
42 """
43 self.name = None
44 self.home = None
45 self.handle = None
46 self.switches = {}
47 self.hosts = {}
48 super( NetworkDriver, self ).__init__()
49
50 def checkOptions( self, var, defaultVar ):
51 if var is None or var == "":
52 return defaultVar
53 return var
54
55 def connect( self, **connectargs ):
56 """
57 Creates ssh handle for the SDN network "bench".
58 NOTE:
59 The ip_address would come from the topo file using the host tag, the
60 value can be an environment variable as well as a "localhost" to get
61 the ip address needed to ssh to the "bench"
62 """
63 try:
64 for key in connectargs:
65 vars( self )[ key ] = connectargs[ key ]
66 self.name = self.options[ 'name' ]
67 try:
68 if os.getenv( str( self.ip_address ) ) is not None:
69 self.ip_address = os.getenv( str( self.ip_address ) )
70 else:
71 main.log.info( self.name +
72 ": Trying to connect to " +
73 self.ip_address )
74 except KeyError:
75 main.log.info( "Invalid host name," +
76 " connecting to local host instead" )
77 self.ip_address = 'localhost'
78 except Exception as inst:
79 main.log.error( "Uncaught exception: " + str( inst ) )
80
81 self.handle = super( NetworkDriver, self ).connect(
82 user_name=self.user_name,
83 ip_address=self.ip_address,
84 port=self.port,
85 pwd=self.pwd )
86
87 if self.handle:
88 main.log.info( "Connected to network bench node" )
89 return self.handle
90 else:
91 main.log.info( "Failed to create handle" )
92 return main.FALSE
93 except pexpect.EOF:
94 main.log.error( self.name + ": EOF exception found" )
95 main.log.error( self.name + ": " + self.handle.before )
96 main.cleanAndExit()
97 except Exception:
98 main.log.exception( self.name + ": Uncaught exception!" )
99 main.cleanAndExit()
100
101 def disconnect( self ):
102 """
103 Called when test is complete to disconnect the handle.
104 """
105 response = main.TRUE
106 try:
107 if self.handle:
108 self.handle.sendline( "exit" )
109 self.handle.expect( "closed" )
110 except pexpect.EOF:
111 main.log.error( self.name + ": EOF exception found" )
112 main.log.error( self.name + ": " + self.handle.before )
113 except Exception:
114 main.log.exception( self.name + ": Connection failed to the host" )
115 response = main.FALSE
116 return response
117
118 def connectToNet( self ):
119 """
120 Connect to an existing physical network by getting information
121 of all switch and host components created
122 """
123 try:
124 for key, value in main.componentDictionary.items():
125 if hasattr( main, key ):
Pier6a0c4de2018-03-18 16:01:30 -0700126 if value[ 'type' ] in [ 'MininetSwitchDriver', 'OFDPASwitchDriver' ]:
You Wang4cc61912018-08-28 10:10:58 -0700127 component = getattr( main, key )
128 shortName = component.options[ 'shortName' ]
129 localName = self.name + "-" + shortName
130 self.copyComponent( key, localName )
131 self.switches[ shortName ] = getattr( main, localName )
Pier6a0c4de2018-03-18 16:01:30 -0700132 elif value[ 'type' ] in [ 'MininetHostDriver', 'HostDriver' ]:
You Wang4cc61912018-08-28 10:10:58 -0700133 component = getattr( main, key )
134 shortName = component.options[ 'shortName' ]
135 localName = self.name + "-" + shortName
136 self.copyComponent( key, localName )
137 self.hosts[ shortName ] = getattr( main, localName )
138 main.log.debug( self.name + ": found switches: {}".format( self.switches ) )
139 main.log.debug( self.name + ": found hosts: {}".format( self.hosts ) )
You Wang84f981d2018-01-12 16:11:50 -0800140 return main.TRUE
141 except Exception:
142 main.log.error( self.name + ": failed to connect to network" )
143 return main.FALSE
144
You Wang4cc61912018-08-28 10:10:58 -0700145 def disconnectFromNet( self ):
You Wang84f981d2018-01-12 16:11:50 -0800146 """
You Wang4cc61912018-08-28 10:10:58 -0700147 Disconnect from the physical network connected
You Wang84f981d2018-01-12 16:11:50 -0800148 """
You Wang84f981d2018-01-12 16:11:50 -0800149 try:
You Wang4cc61912018-08-28 10:10:58 -0700150 for key, value in main.componentDictionary.items():
151 if hasattr( main, key ) and key.startswith( self.name + "-" ):
152 self.removeComponent( key )
153 self.switches = {}
154 self.hosts = {}
155 return main.TRUE
You Wang84f981d2018-01-12 16:11:50 -0800156 except Exception:
You Wang4cc61912018-08-28 10:10:58 -0700157 main.log.error( self.name + ": failed to disconnect from network" )
158 return main.FALSE
159
160 def copyComponent( self, name, newName ):
161 """
162 Copy the component initialized from the .topo file
163 The copied components are only supposed to be called within this driver
164 Required:
165 name: name of the component to be copied
166 newName: name of the new component
167 """
168 try:
169 main.componentDictionary[ newName ] = main.componentDictionary[ name ].copy()
170 main.componentInit( newName )
171 except Exception:
172 main.log.exception( self.name + ": Uncaught exception!" )
173 main.cleanAndExit()
174
175 def removeHostComponent( self, name ):
176 """
177 Remove host component
178 Required:
179 name: name of the component to be removed
180 """
181 try:
182 self.removeComponent( name )
183 except Exception:
184 main.log.exception( self.name + ": Uncaught exception!" )
185 main.cleanAndExit()
186
187 def removeComponent( self, name ):
188 """
189 Remove host/switch component
190 Required:
191 name: name of the component to be removed
192 """
193 try:
194 component = getattr( main, name )
195 except AttributeError:
196 main.log.error( "Component " + name + " does not exist." )
197 return main.FALSE
198 try:
199 # Disconnect from component
200 component.disconnect()
201 # Delete component
202 delattr( main, name )
203 # Delete component from ComponentDictionary
204 del( main.componentDictionary[ name ] )
205 return main.TRUE
206 except Exception:
207 main.log.exception( self.name + ": Uncaught exception!" )
208 main.cleanAndExit()
209
210 def createComponent( self, name ):
211 """
212 Creates switch/host component with the same parameters as the one copied to local.
213 Arguments:
214 name - The string of the name of this component. The new component
215 will be assigned to main.<name> .
216 In addition, main.<name>.name = str( name )
217 """
218 try:
219 # look to see if this component already exists
220 getattr( main, name )
221 except AttributeError:
222 # namespace is clear, creating component
223 localName = self.name + "-" + name
224 main.componentDictionary[ name ] = main.componentDictionary[ localName ].copy()
225 main.componentInit( name )
226 except Exception:
227 main.log.exception( self.name + ": Uncaught exception!" )
228 main.cleanAndExit()
229 else:
230 # namespace is not clear!
231 main.log.error( name + " component already exists!" )
232 main.cleanAndExit()
233
234 def connectInbandHosts( self ):
235 """
236 Connect to hosts using data plane IPs specified
237 """
238 result = main.TRUE
239 try:
240 for hostName, hostComponent in self.hosts.items():
241 if hostComponent.options[ 'inband' ] == 'True':
242 main.log.info( self.name + ": connecting inband host " + hostName )
243 result = hostComponent.connectInband() and result
244 return result
245 except Exception:
246 main.log.error( self.name + ": failed to connect to inband hosts" )
247 return main.FALSE
248
249 def disconnectInbandHosts( self ):
250 """
251 Terminate the connections to hosts using data plane IPs
252 """
253 result = main.TRUE
254 try:
255 for hostName, hostComponent in self.hosts.items():
256 if hostComponent.options[ 'inband' ] == 'True':
257 main.log.info( self.name + ": disconnecting inband host " + hostName )
258 result = hostComponent.disconnectInband() and result
259 return result
260 except Exception:
261 main.log.error( self.name + ": failed to disconnect inband hosts" )
262 return main.FALSE
263
264 def getSwitches( self ):
265 """
266 Return a dictionary which maps short names to switch component
267 """
268 return self.switches
269
270 def getHosts( self ):
271 """
272 Return a dictionary which maps short names to host component
273 """
274 return self.hosts
You Wang84f981d2018-01-12 16:11:50 -0800275
276 def getMacAddress( self, host ):
277 """
278 Return MAC address of a host
279 """
280 import re
281 try:
You Wang4cc61912018-08-28 10:10:58 -0700282 hosts = self.getHosts()
283 hostComponent = hosts[ host ]
You Wang84f981d2018-01-12 16:11:50 -0800284 response = hostComponent.ifconfig()
285 pattern = r'HWaddr\s([0-9A-F]{2}[:-]){5}([0-9A-F]{2})'
286 macAddressSearch = re.search( pattern, response, re.I )
287 macAddress = macAddressSearch.group().split( " " )[ 1 ]
288 main.log.info( self.name + ": Mac-Address of Host " + host + " is " + macAddress )
289 return macAddress
290 except Exception:
291 main.log.error( self.name + ": failed to get host MAC address" )
292
You Wang4cc61912018-08-28 10:10:58 -0700293 def runCmdOnHost( self, hostName, cmd ):
You Wang84f981d2018-01-12 16:11:50 -0800294 """
You Wang4cc61912018-08-28 10:10:58 -0700295 Run shell command on specified host and return output
296 Required:
297 hostName: name of the host e.g. "h1"
298 cmd: command to run on the host
You Wang84f981d2018-01-12 16:11:50 -0800299 """
You Wang4cc61912018-08-28 10:10:58 -0700300 hosts = self.getHosts()
301 hostComponent = hosts[ hostName ]
302 if hostComponent:
303 return hostComponent.command( cmd )
You Wang84f981d2018-01-12 16:11:50 -0800304 return None
305
306 def assignSwController( self, sw, ip, port="6653", ptcp="" ):
307 """
308 Description:
309 Assign switches to the controllers
310 Required:
311 sw - Short name of the switch specified in the .topo file, e.g. "s1".
312 It can also be a list of switch names.
313 ip - Ip addresses of controllers. This can be a list or a string.
314 Optional:
315 port - ONOS use port 6653, if no list of ports is passed, then
316 the all the controller will use 6653 as their port number
317 ptcp - ptcp number, This can be a string or a list that has
318 the same length as switch. This is optional and not required
319 when using ovs switches.
320 NOTE: If switches and ptcp are given in a list type they should have the
321 same length and should be in the same order, Eg. sw=[ 's1' ... n ]
322 ptcp=[ '6637' ... n ], s1 has ptcp number 6637 and so on.
323
324 Return:
325 Returns main.TRUE if switches are correctly assigned to controllers,
326 otherwise it will return main.FALSE or an appropriate exception(s)
327 """
328 switchList = []
329 ptcpList = None
330 try:
331 if isinstance( sw, types.StringType ):
332 switchList.append( sw )
333 if ptcp:
334 if isinstance( ptcp, types.StringType ):
335 ptcpList = [ ptcp ]
336 elif isinstance( ptcp, types.ListType ):
337 main.log.error( self.name + ": Only one switch is " +
338 "being set and multiple PTCP is " +
339 "being passed " )
340 return main.FALSE
341 else:
342 main.log.error( self.name + ": Invalid PTCP" )
343 return main.FALSE
344
345 elif isinstance( sw, types.ListType ):
346 switchList = sw
347 if ptcp:
348 if isinstance( ptcp, types.ListType ):
349 if len( ptcp ) != len( sw ):
350 main.log.error( self.name + ": PTCP length = " +
351 str( len( ptcp ) ) +
352 " is not the same as switch" +
353 " length = " +
354 str( len( sw ) ) )
355 return main.FALSE
356 else:
357 ptcpList = ptcp
358 else:
359 main.log.error( self.name + ": Invalid PTCP" )
360 return main.FALSE
361 else:
362 main.log.error( self.name + ": Invalid switch type " )
363 return main.FALSE
364
365 assignResult = main.TRUE
366 index = 0
367 for switch in switchList:
368 assigned = False
You Wang4cc61912018-08-28 10:10:58 -0700369 switchComponent = self.switches[ switch ]
You Wang84f981d2018-01-12 16:11:50 -0800370 if switchComponent:
371 ptcp = ptcpList[ index ] if ptcpList else ""
372 assignResult = assignResult and switchComponent.assignSwController( ip=ip, port=port, ptcp=ptcp )
373 assigned = True
374 if not assigned:
375 main.log.error( self.name + ": Not able to find switch " + switch )
376 assignResult = main.FALSE
377 index += 1
378 return assignResult
379
380 except Exception:
381 main.log.exception( self.name + ": Uncaught exception!" )
382 main.cleanAndExit()
383
384 def pingall( self, protocol="IPv4", timeout=300, shortCircuit=False, acceptableFailed=0 ):
385 """
386 Description:
387 Verifies the reachability of the hosts using ping command.
388 Optional:
389 protocol - use ping6 command if specified as "IPv6"
390 timeout( seconds ) - How long to wait before breaking the pingall
391 shortCircuit - Break the pingall based on the number of failed hosts ping
392 acceptableFailed - Set the number of acceptable failed pings for the
393 function to still return main.TRUE
394 Returns:
395 main.TRUE if pingall completes with no pings dropped
396 otherwise main.FALSE
397 """
398 import time
399 import itertools
400 try:
401 timeout = int( timeout )
402 main.log.info( self.name + ": Checking reachabilty to the hosts using ping" )
403 failedPings = 0
404 returnValue = main.TRUE
405 ipv6 = True if protocol == "IPv6" else False
406 startTime = time.time()
You Wang4cc61912018-08-28 10:10:58 -0700407 hosts = self.getHosts()
408 hostPairs = itertools.permutations( list( hosts.values() ), 2 )
You Wang84f981d2018-01-12 16:11:50 -0800409 for hostPair in list( hostPairs ):
410 ipDst = hostPair[ 1 ].options[ 'ip6' ] if ipv6 else hostPair[ 1 ].options[ 'ip' ]
411 pingResult = hostPair[ 0 ].ping( ipDst, ipv6=ipv6 )
412 returnValue = returnValue and pingResult
413 if ( time.time() - startTime ) > timeout:
414 returnValue = main.FALSE
415 main.log.error( self.name +
416 ": Aborting pingall - " +
417 "Function took too long " )
418 break
419 if not pingResult:
420 failedPings = failedPings + 1
421 if failedPings > acceptableFailed:
422 returnValue = main.FALSE
423 if shortCircuit:
424 main.log.error( self.name +
425 ": Aborting pingall - "
426 + str( failedPings ) +
427 " pings failed" )
428 break
429 return returnValue
430 except Exception:
431 main.log.exception( self.name + ": Uncaught exception!" )
432 main.cleanAndExit()
433
434 def pingallHosts( self, hostList, wait=1 ):
435 """
436 Ping all specified IPv4 hosts
437
438 Acceptable hostList:
439 - [ 'h1','h2','h3','h4' ]
440
441 Returns main.TRUE if all hosts specified can reach
442 each other
443
444 Returns main.FALSE if one or more of hosts specified
445 cannot reach each other"""
446 import time
447 import itertools
448 hostComponentList = []
You Wang4cc61912018-08-28 10:10:58 -0700449 hosts = self.getHosts()
You Wang84f981d2018-01-12 16:11:50 -0800450 for hostName in hostList:
You Wang4cc61912018-08-28 10:10:58 -0700451 hostComponent = hosts[ hostName ]
You Wang84f981d2018-01-12 16:11:50 -0800452 if hostComponent:
453 hostComponentList.append( hostComponent )
454 try:
455 main.log.info( "Testing reachability between specified hosts" )
456 isReachable = main.TRUE
457 pingResponse = "IPv4 ping across specified hosts\n"
458 failedPings = 0
459 hostPairs = itertools.permutations( list( hostComponentList ), 2 )
460 for hostPair in list( hostPairs ):
461 pingResponse += hostPair[ 0 ].options[ 'shortName' ] + " -> "
462 ipDst = hostPair[ 1 ].options[ 'ip6' ] if ipv6 else hostPair[ 1 ].options[ 'ip' ]
463 pingResult = hostPair[ 0 ].ping( ipDst, wait=int( wait ) )
464 if pingResult:
465 pingResponse += hostPair[ 1 ].options[ 'shortName' ]
466 else:
467 pingResponse += "X"
468 # One of the host to host pair is unreachable
469 isReachable = main.FALSE
470 failedPings += 1
471 pingResponse += "\n"
472 main.log.info( pingResponse + "Failed pings: " + str( failedPings ) )
473 return isReachable
474 except Exception:
475 main.log.exception( self.name + ": Uncaught exception!" )
476 main.cleanAndExit()
477
478 def iperftcp( self, host1="h1", host2="h2", timeout=6 ):
479 '''
480 Creates an iperf TCP test between two hosts. Returns main.TRUE if test results
481 are valid.
482 Optional:
483 timeout: The defualt timeout is 6 sec to allow enough time for a successful test to complete,
484 and short enough to stop an unsuccessful test from quiting and cleaning up mininet.
485 '''
486 main.log.info( self.name + ": Simple iperf TCP test between two hosts" )
487 # TODO: complete this function
488 return main.TRUE
You Wang4cc61912018-08-28 10:10:58 -0700489
490 def update( self ):
491 return main.TRUE
492
493 def verifyHostIp( self, hostList=[], prefix="", update=False ):
494 """
495 Description:
496 Verify that all hosts have IP address assigned to them
497 Optional:
498 hostList: If specified, verifications only happen to the hosts
499 in hostList
500 prefix: at least one of the ip address assigned to the host
501 needs to have the specified prefix
502 Returns:
503 main.TRUE if all hosts have specific IP address assigned;
504 main.FALSE otherwise
505 """
506 try:
507 hosts = self.getHosts()
508 if not hostList:
509 hostList = hosts.keys()
510 for hostName, hostComponent in hosts.items():
511 if hostName not in hostList:
512 continue
513 ipList = []
514 ipa = hostComponent.ip()
515 ipv4Pattern = r'inet ((?:[0-9]{1,3}\.){3}[0-9]{1,3})/'
516 ipList += re.findall( ipv4Pattern, ipa )
517 # It's tricky to make regex for IPv6 addresses and this one is simplified
518 ipv6Pattern = r'inet6 ((?:[0-9a-fA-F]{1,4})?(?:[:0-9a-fA-F]{1,4}){1,7}(?:::)?(?:[:0-9a-fA-F]{1,4}){1,7})/'
519 ipList += re.findall( ipv6Pattern, ipa )
520 main.log.debug( self.name + ": IP list on host " + str( hostName ) + ": " + str( ipList ) )
521 if not ipList:
522 main.log.warn( self.name + ": Failed to discover any IP addresses on host " + str( hostName ) )
523 else:
524 if not any( ip.startswith( str( prefix ) ) for ip in ipList ):
525 main.log.warn( self.name + ": None of the IPs on host " + str( hostName ) + " has prefix " + str( prefix ) )
526 else:
527 main.log.debug( self.name + ": Found matching IP on host " + str( hostName ) )
528 hostList.remove( hostName )
529 return main.FALSE if hostList else main.TRUE
530 except KeyError:
531 main.log.exception( self.name + ": host data not as expected: " + hosts )
532 return None
533 except pexpect.EOF:
534 main.log.error( self.name + ": EOF exception found" )
535 main.log.error( self.name + ": " + self.handle.before )
536 main.cleanAndExit()
537 except Exception:
538 main.log.exception( self.name + ": Uncaught exception" )
539 return None
540
541 def addRoute( self, host, dstIP, interface, ipv6=False ):
542 """
543 Add a route to host
544 Ex: h1 route add -host 224.2.0.1 h1-eth0
545 """
546 try:
547 if ipv6:
548 cmd = "sudo route -A inet6 add "
549 else:
550 cmd = "sudo route add -host "
551 cmd += str( dstIP ) + " " + str( interface )
552 response = self.runCmdOnHost( host, cmd )
553 main.log.debug( "response = " + response )
554 return main.TRUE
555 except pexpect.TIMEOUT:
556 main.log.error( self.name + ": TIMEOUT exception found" )
557 main.log.error( self.name + ": " + self.handle.before )
558 main.cleanAndExit()
559 except pexpect.EOF:
560 main.log.error( self.name + ": EOF exception found" )
561 main.log.error( self.name + ": " + self.handle.before )
562 return main.FALSE
563 except Exception:
564 main.log.exception( self.name + ": Uncaught exception!" )
565 main.cleanAndExit()