blob: 968973dbfc16accd2664f899999577812f8b2fa9 [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
Jon Hall43060f62020-06-23 13:13:33 -070034import time
35import itertools
36import random
You Wang84f981d2018-01-12 16:11:50 -080037from drivers.common.clidriver import CLI
You Wangb1665b52019-02-01 15:49:48 -080038from core.graph import Graph
You Wang84f981d2018-01-12 16:11:50 -080039
40class NetworkDriver( CLI ):
41
42 def __init__( self ):
43 """
44 switches: a dictionary that maps switch names to components
45 hosts: a dictionary that maps host names to components
46 """
47 self.name = None
48 self.home = None
49 self.handle = None
50 self.switches = {}
51 self.hosts = {}
You Wangb1665b52019-02-01 15:49:48 -080052 self.links = {}
You Wang84f981d2018-01-12 16:11:50 -080053 super( NetworkDriver, self ).__init__()
You Wangb1665b52019-02-01 15:49:48 -080054 self.graph = Graph()
You Wang84f981d2018-01-12 16:11:50 -080055
56 def checkOptions( self, var, defaultVar ):
57 if var is None or var == "":
58 return defaultVar
59 return var
60
61 def connect( self, **connectargs ):
62 """
63 Creates ssh handle for the SDN network "bench".
64 NOTE:
65 The ip_address would come from the topo file using the host tag, the
66 value can be an environment variable as well as a "localhost" to get
67 the ip address needed to ssh to the "bench"
68 """
69 try:
70 for key in connectargs:
71 vars( self )[ key ] = connectargs[ key ]
72 self.name = self.options[ 'name' ]
73 try:
74 if os.getenv( str( self.ip_address ) ) is not None:
75 self.ip_address = os.getenv( str( self.ip_address ) )
76 else:
77 main.log.info( self.name +
78 ": Trying to connect to " +
79 self.ip_address )
80 except KeyError:
81 main.log.info( "Invalid host name," +
82 " connecting to local host instead" )
83 self.ip_address = 'localhost'
84 except Exception as inst:
85 main.log.error( "Uncaught exception: " + str( inst ) )
86
87 self.handle = super( NetworkDriver, self ).connect(
88 user_name=self.user_name,
89 ip_address=self.ip_address,
90 port=self.port,
91 pwd=self.pwd )
92
93 if self.handle:
94 main.log.info( "Connected to network bench node" )
95 return self.handle
96 else:
97 main.log.info( "Failed to create handle" )
98 return main.FALSE
99 except pexpect.EOF:
100 main.log.error( self.name + ": EOF exception found" )
101 main.log.error( self.name + ": " + self.handle.before )
102 main.cleanAndExit()
103 except Exception:
104 main.log.exception( self.name + ": Uncaught exception!" )
105 main.cleanAndExit()
106
107 def disconnect( self ):
108 """
109 Called when test is complete to disconnect the handle.
110 """
111 response = main.TRUE
112 try:
113 if self.handle:
114 self.handle.sendline( "exit" )
115 self.handle.expect( "closed" )
116 except pexpect.EOF:
117 main.log.error( self.name + ": EOF exception found" )
118 main.log.error( self.name + ": " + self.handle.before )
119 except Exception:
120 main.log.exception( self.name + ": Connection failed to the host" )
121 response = main.FALSE
122 return response
123
124 def connectToNet( self ):
125 """
126 Connect to an existing physical network by getting information
127 of all switch and host components created
128 """
129 try:
130 for key, value in main.componentDictionary.items():
131 if hasattr( main, key ):
Jon Hall43060f62020-06-23 13:13:33 -0700132 if value[ 'type' ] in [ 'MininetSwitchDriver', 'OFDPASwitchDriver', 'StratumOSSwitchDriver' ]:
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.switches[ shortName ] = getattr( main, localName )
Pier6a0c4de2018-03-18 16:01:30 -0700138 elif value[ 'type' ] in [ 'MininetHostDriver', 'HostDriver' ]:
You Wang4cc61912018-08-28 10:10:58 -0700139 component = getattr( main, key )
140 shortName = component.options[ 'shortName' ]
141 localName = self.name + "-" + shortName
142 self.copyComponent( key, localName )
143 self.hosts[ shortName ] = getattr( main, localName )
144 main.log.debug( self.name + ": found switches: {}".format( self.switches ) )
145 main.log.debug( self.name + ": found hosts: {}".format( self.hosts ) )
You Wang84f981d2018-01-12 16:11:50 -0800146 return main.TRUE
147 except Exception:
148 main.log.error( self.name + ": failed to connect to network" )
149 return main.FALSE
150
You Wang4cc61912018-08-28 10:10:58 -0700151 def disconnectFromNet( self ):
You Wang84f981d2018-01-12 16:11:50 -0800152 """
You Wang4cc61912018-08-28 10:10:58 -0700153 Disconnect from the physical network connected
You Wang84f981d2018-01-12 16:11:50 -0800154 """
You Wang84f981d2018-01-12 16:11:50 -0800155 try:
You Wang4cc61912018-08-28 10:10:58 -0700156 for key, value in main.componentDictionary.items():
157 if hasattr( main, key ) and key.startswith( self.name + "-" ):
158 self.removeComponent( key )
159 self.switches = {}
160 self.hosts = {}
161 return main.TRUE
You Wang84f981d2018-01-12 16:11:50 -0800162 except Exception:
You Wang4cc61912018-08-28 10:10:58 -0700163 main.log.error( self.name + ": failed to disconnect from network" )
164 return main.FALSE
165
166 def copyComponent( self, name, newName ):
167 """
168 Copy the component initialized from the .topo file
169 The copied components are only supposed to be called within this driver
170 Required:
171 name: name of the component to be copied
172 newName: name of the new component
173 """
174 try:
175 main.componentDictionary[ newName ] = main.componentDictionary[ name ].copy()
176 main.componentInit( newName )
177 except Exception:
178 main.log.exception( self.name + ": Uncaught exception!" )
179 main.cleanAndExit()
180
181 def removeHostComponent( self, name ):
182 """
183 Remove host component
184 Required:
185 name: name of the component to be removed
186 """
187 try:
188 self.removeComponent( name )
189 except Exception:
190 main.log.exception( self.name + ": Uncaught exception!" )
191 main.cleanAndExit()
192
193 def removeComponent( self, name ):
194 """
195 Remove host/switch component
196 Required:
197 name: name of the component to be removed
198 """
199 try:
200 component = getattr( main, name )
201 except AttributeError:
202 main.log.error( "Component " + name + " does not exist." )
203 return main.FALSE
204 try:
205 # Disconnect from component
206 component.disconnect()
207 # Delete component
208 delattr( main, name )
209 # Delete component from ComponentDictionary
210 del( main.componentDictionary[ name ] )
211 return main.TRUE
212 except Exception:
213 main.log.exception( self.name + ": Uncaught exception!" )
214 main.cleanAndExit()
215
You Wang0fc21702018-11-02 17:49:18 -0700216 def createHostComponent( self, name ):
You Wang4cc61912018-08-28 10:10:58 -0700217 """
You Wang0fc21702018-11-02 17:49:18 -0700218 Creates host component with the same parameters as the one copied to local.
You Wang4cc61912018-08-28 10:10:58 -0700219 Arguments:
220 name - The string of the name of this component. The new component
221 will be assigned to main.<name> .
222 In addition, main.<name>.name = str( name )
223 """
224 try:
225 # look to see if this component already exists
226 getattr( main, name )
227 except AttributeError:
228 # namespace is clear, creating component
229 localName = self.name + "-" + name
230 main.componentDictionary[ name ] = main.componentDictionary[ localName ].copy()
231 main.componentInit( name )
232 except Exception:
233 main.log.exception( self.name + ": Uncaught exception!" )
234 main.cleanAndExit()
235 else:
236 # namespace is not clear!
237 main.log.error( name + " component already exists!" )
238 main.cleanAndExit()
239
240 def connectInbandHosts( self ):
241 """
242 Connect to hosts using data plane IPs specified
243 """
244 result = main.TRUE
245 try:
246 for hostName, hostComponent in self.hosts.items():
247 if hostComponent.options[ 'inband' ] == 'True':
248 main.log.info( self.name + ": connecting inband host " + hostName )
249 result = hostComponent.connectInband() and result
250 return result
251 except Exception:
252 main.log.error( self.name + ": failed to connect to inband hosts" )
253 return main.FALSE
254
255 def disconnectInbandHosts( self ):
256 """
257 Terminate the connections to hosts using data plane IPs
258 """
259 result = main.TRUE
260 try:
261 for hostName, hostComponent in self.hosts.items():
262 if hostComponent.options[ 'inband' ] == 'True':
263 main.log.info( self.name + ": disconnecting inband host " + hostName )
264 result = hostComponent.disconnectInband() and result
265 return result
266 except Exception:
267 main.log.error( self.name + ": failed to disconnect inband hosts" )
268 return main.FALSE
269
You Wangb1665b52019-02-01 15:49:48 -0800270 def getSwitches( self, timeout=60, excludeNodes=[], includeStopped=False ):
You Wang4cc61912018-08-28 10:10:58 -0700271 """
You Wangb1665b52019-02-01 15:49:48 -0800272 Return a dictionary which maps short names to switch data
273 If includeStopped is True, stopped switches will also be included
You Wang4cc61912018-08-28 10:10:58 -0700274 """
You Wangb1665b52019-02-01 15:49:48 -0800275 switches = {}
276 for switchName, switchComponent in self.switches.items():
277 if switchName in excludeNodes:
278 continue
279 if not includeStopped and not switchComponent.isup:
280 continue
Jon Hall43060f62020-06-23 13:13:33 -0700281 try:
282 dpid = switchComponent.dpid
283 except AttributeError:
284 main.log.warn( "Switch has no dpid, ignore this if not an OpenFlow switch" )
285 dpid = "0x0"
286 dpid = dpid.replace( '0x', '' ).zfill( 16 )
You Wangb1665b52019-02-01 15:49:48 -0800287 ports = switchComponent.ports
288 swClass = 'Unknown'
289 pid = None
290 options = None
291 switches[ switchName ] = { "dpid": dpid,
292 "ports": ports,
293 "swClass": swClass,
294 "pid": pid,
295 "options": options }
296 return switches
You Wang4cc61912018-08-28 10:10:58 -0700297
You Wangb1665b52019-02-01 15:49:48 -0800298 def getHosts( self, hostClass=None ):
You Wang4cc61912018-08-28 10:10:58 -0700299 """
You Wangb1665b52019-02-01 15:49:48 -0800300 Return a dictionary which maps short names to host data
You Wang4cc61912018-08-28 10:10:58 -0700301 """
You Wangb1665b52019-02-01 15:49:48 -0800302 hosts = {}
303 for hostName, hostComponent in self.hosts.items():
304 interfaces = hostComponent.interfaces
305 hosts[ hostName ] = { "interfaces": interfaces }
306 return hosts
307
308 def updateLinks( self, timeout=60, excludeNodes=[] ):
309 """
310 Update self.links by getting up-to-date port information from
311 switches
312 """
313 # TODO: also inlcude switch-to-host links
314 self.links = {}
315 for node1 in self.switches.keys():
316 if node1 in excludeNodes:
317 continue
318 self.links[ node1 ] = {}
319 self.switches[ node1 ].updatePorts()
320 for port in self.switches[ node1 ].ports:
321 if not port[ 'enabled' ]:
322 continue
323 node2 = getattr( main, port[ 'node2' ] ).shortName
324 if node2 in excludeNodes:
325 continue
326 port1 = port[ 'of_port' ]
327 port2 = port[ 'port2' ]
328 if not self.links[ node1 ].get( node2 ):
329 self.links[ node1 ][ node2 ] = {}
330 # Check if this link already exists
331 if self.links.get( node2 ):
332 if self.links[ node2 ].get( node1 ):
333 if self.links[ node2 ].get( node1 ).get( port2 ):
334 assert self.links[ node2 ][ node1 ][ port2 ] == port1
335 continue
336 self.links[ node1 ][ node2 ][ port1 ] = port2
337
338 def getLinks( self, timeout=60, excludeNodes=[] ):
339 """
340 Return a list of links specify both node names and port numbers
341 """
342 self.updateLinks( timeout=timeout, excludeNodes=excludeNodes )
343 links = []
344 for node1, nodeLinks in self.links.items():
345 for node2, ports in nodeLinks.items():
346 for port1, port2 in ports.items():
347 links.append( { 'node1': node1, 'node2': node2,
348 'port1': port1, 'port2': port2 } )
349 return links
You Wang84f981d2018-01-12 16:11:50 -0800350
351 def getMacAddress( self, host ):
352 """
353 Return MAC address of a host
354 """
You Wang84f981d2018-01-12 16:11:50 -0800355 try:
You Wangb1665b52019-02-01 15:49:48 -0800356 hostComponent = self.hosts[ host ]
You Wang84f981d2018-01-12 16:11:50 -0800357 response = hostComponent.ifconfig()
358 pattern = r'HWaddr\s([0-9A-F]{2}[:-]){5}([0-9A-F]{2})'
359 macAddressSearch = re.search( pattern, response, re.I )
360 macAddress = macAddressSearch.group().split( " " )[ 1 ]
361 main.log.info( self.name + ": Mac-Address of Host " + host + " is " + macAddress )
362 return macAddress
363 except Exception:
364 main.log.error( self.name + ": failed to get host MAC address" )
365
You Wang4cc61912018-08-28 10:10:58 -0700366 def runCmdOnHost( self, hostName, cmd ):
You Wang84f981d2018-01-12 16:11:50 -0800367 """
You Wang4cc61912018-08-28 10:10:58 -0700368 Run shell command on specified host and return output
369 Required:
370 hostName: name of the host e.g. "h1"
371 cmd: command to run on the host
You Wang84f981d2018-01-12 16:11:50 -0800372 """
You Wangb1665b52019-02-01 15:49:48 -0800373 hostComponent = self.hosts[ hostName ]
You Wang4cc61912018-08-28 10:10:58 -0700374 if hostComponent:
375 return hostComponent.command( cmd )
You Wang84f981d2018-01-12 16:11:50 -0800376 return None
377
378 def assignSwController( self, sw, ip, port="6653", ptcp="" ):
379 """
380 Description:
381 Assign switches to the controllers
382 Required:
383 sw - Short name of the switch specified in the .topo file, e.g. "s1".
384 It can also be a list of switch names.
385 ip - Ip addresses of controllers. This can be a list or a string.
386 Optional:
387 port - ONOS use port 6653, if no list of ports is passed, then
388 the all the controller will use 6653 as their port number
389 ptcp - ptcp number, This can be a string or a list that has
390 the same length as switch. This is optional and not required
391 when using ovs switches.
392 NOTE: If switches and ptcp are given in a list type they should have the
393 same length and should be in the same order, Eg. sw=[ 's1' ... n ]
394 ptcp=[ '6637' ... n ], s1 has ptcp number 6637 and so on.
395
396 Return:
397 Returns main.TRUE if switches are correctly assigned to controllers,
398 otherwise it will return main.FALSE or an appropriate exception(s)
399 """
400 switchList = []
401 ptcpList = None
402 try:
403 if isinstance( sw, types.StringType ):
404 switchList.append( sw )
405 if ptcp:
406 if isinstance( ptcp, types.StringType ):
407 ptcpList = [ ptcp ]
408 elif isinstance( ptcp, types.ListType ):
409 main.log.error( self.name + ": Only one switch is " +
410 "being set and multiple PTCP is " +
411 "being passed " )
412 return main.FALSE
413 else:
414 main.log.error( self.name + ": Invalid PTCP" )
415 return main.FALSE
416
417 elif isinstance( sw, types.ListType ):
418 switchList = sw
419 if ptcp:
420 if isinstance( ptcp, types.ListType ):
421 if len( ptcp ) != len( sw ):
422 main.log.error( self.name + ": PTCP length = " +
423 str( len( ptcp ) ) +
424 " is not the same as switch" +
425 " length = " +
426 str( len( sw ) ) )
427 return main.FALSE
428 else:
429 ptcpList = ptcp
430 else:
431 main.log.error( self.name + ": Invalid PTCP" )
432 return main.FALSE
433 else:
434 main.log.error( self.name + ": Invalid switch type " )
435 return main.FALSE
436
437 assignResult = main.TRUE
438 index = 0
439 for switch in switchList:
440 assigned = False
You Wang4cc61912018-08-28 10:10:58 -0700441 switchComponent = self.switches[ switch ]
You Wang84f981d2018-01-12 16:11:50 -0800442 if switchComponent:
443 ptcp = ptcpList[ index ] if ptcpList else ""
444 assignResult = assignResult and switchComponent.assignSwController( ip=ip, port=port, ptcp=ptcp )
445 assigned = True
446 if not assigned:
447 main.log.error( self.name + ": Not able to find switch " + switch )
448 assignResult = main.FALSE
449 index += 1
450 return assignResult
451
452 except Exception:
453 main.log.exception( self.name + ": Uncaught exception!" )
454 main.cleanAndExit()
455
456 def pingall( self, protocol="IPv4", timeout=300, shortCircuit=False, acceptableFailed=0 ):
457 """
458 Description:
459 Verifies the reachability of the hosts using ping command.
460 Optional:
461 protocol - use ping6 command if specified as "IPv6"
462 timeout( seconds ) - How long to wait before breaking the pingall
463 shortCircuit - Break the pingall based on the number of failed hosts ping
464 acceptableFailed - Set the number of acceptable failed pings for the
465 function to still return main.TRUE
466 Returns:
467 main.TRUE if pingall completes with no pings dropped
468 otherwise main.FALSE
469 """
You Wang84f981d2018-01-12 16:11:50 -0800470 try:
471 timeout = int( timeout )
472 main.log.info( self.name + ": Checking reachabilty to the hosts using ping" )
473 failedPings = 0
474 returnValue = main.TRUE
475 ipv6 = True if protocol == "IPv6" else False
476 startTime = time.time()
You Wangb1665b52019-02-01 15:49:48 -0800477 hostPairs = itertools.permutations( list( self.hosts.values() ), 2 )
You Wang84f981d2018-01-12 16:11:50 -0800478 for hostPair in list( hostPairs ):
479 ipDst = hostPair[ 1 ].options[ 'ip6' ] if ipv6 else hostPair[ 1 ].options[ 'ip' ]
480 pingResult = hostPair[ 0 ].ping( ipDst, ipv6=ipv6 )
481 returnValue = returnValue and pingResult
482 if ( time.time() - startTime ) > timeout:
483 returnValue = main.FALSE
484 main.log.error( self.name +
485 ": Aborting pingall - " +
486 "Function took too long " )
487 break
488 if not pingResult:
489 failedPings = failedPings + 1
490 if failedPings > acceptableFailed:
491 returnValue = main.FALSE
492 if shortCircuit:
493 main.log.error( self.name +
494 ": Aborting pingall - "
495 + str( failedPings ) +
496 " pings failed" )
497 break
498 return returnValue
499 except Exception:
500 main.log.exception( self.name + ": Uncaught exception!" )
501 main.cleanAndExit()
502
Jon Hall43060f62020-06-23 13:13:33 -0700503 def pingallHosts( self, hostList, ipv6=False, wait=1, useScapy=False ):
You Wang84f981d2018-01-12 16:11:50 -0800504 """
505 Ping all specified IPv4 hosts
506
507 Acceptable hostList:
508 - [ 'h1','h2','h3','h4' ]
509
510 Returns main.TRUE if all hosts specified can reach
511 each other
512
513 Returns main.FALSE if one or more of hosts specified
514 cannot reach each other"""
You Wang84f981d2018-01-12 16:11:50 -0800515 hostComponentList = []
516 for hostName in hostList:
You Wangb1665b52019-02-01 15:49:48 -0800517 hostComponent = self.hosts[ hostName ]
You Wang84f981d2018-01-12 16:11:50 -0800518 if hostComponent:
519 hostComponentList.append( hostComponent )
520 try:
521 main.log.info( "Testing reachability between specified hosts" )
522 isReachable = main.TRUE
523 pingResponse = "IPv4 ping across specified hosts\n"
524 failedPings = 0
525 hostPairs = itertools.permutations( list( hostComponentList ), 2 )
526 for hostPair in list( hostPairs ):
527 pingResponse += hostPair[ 0 ].options[ 'shortName' ] + " -> "
Jon Hall43060f62020-06-23 13:13:33 -0700528 ipDst = hostPair[ 1 ].options.get( 'ip6', hostPair[ 1 ].options[ 'ip' ] ) if ipv6 else hostPair[ 1 ].options[ 'ip' ]
529 srcIface = hostPair[ 0 ].interfaces[0].get( 'name' )
530 dstIface = hostPair[ 1 ].interfaces[0].get( 'name' )
531 srcMac = hostPair[0].interfaces[0].get( 'mac' )
532 dstMac = hostPair[1].interfaces[0].get( 'mac' )
533 if useScapy:
534 main.log.debug( "Pinging from " + str( hostPair[ 0 ].shortName ) + " to " + str( hostPair[ 1 ].shortName ) )
535 srcIPs = hostPair[ 0 ].interfaces[0].get( 'ips' )
536 dstIPs = hostPair[ 1 ].interfaces[0].get( 'ips' )
537 # Use scapy to send and recieve packets
538 hostPair[ 1 ].startScapy( ifaceName=dstIface )
539 hostPair[ 1 ].addRoutes()
540 hostPair[ 1 ].startFilter( ifaceName=dstIface, pktFilter="ether host %s and ip host %s" % ( srcMac, srcIPs[0] ) )
541
542 hostPair[ 0 ].startScapy( ifaceName=srcIface )
543 hostPair[ 0 ].addRoutes()
544 hostPair[ 0 ].buildEther( dst=dstMac )
545 hostPair[ 0 ].buildIP( src=srcIPs, dst=dstIPs )
546 hostPair[ 0 ].buildICMP( )
547 hostPair[ 0 ].sendPacket( iface=srcIface )
548
549 waiting = not hostPair[ 1 ].checkFilter()
550 if not waiting:
551 pingResult = main.FALSE
552 packets = hostPair[ 1 ].readPackets()
553 main.log.warn( packets )
554 for packet in packets.splitlines():
555 main.log.debug( packet )
556 if srcIPs[0] in packet:
557 pingResult = main.TRUE
558 else:
559 main.log.warn( "Did not receive packets, killing filter" )
560 kill = hostPair[ 1 ].killFilter()
561 main.log.debug( kill )
562 hostPair[ 1 ].handle.sendline( "" )
563 hostPair[ 1 ].handle.expect( hostPair[ 1 ].scapyPrompt )
564 main.log.debug( hostPair[ 1 ].handle.before )
565 # One of the host to host pair is unreachable
566 pingResult = main.FALSE
567 hostPair[ 0 ].stopScapy()
568 hostPair[ 1 ].stopScapy()
569 else:
570 pingResult = hostPair[ 0 ].ping( ipDst, interface=srcIface, wait=int( wait ) )
You Wang84f981d2018-01-12 16:11:50 -0800571 if pingResult:
572 pingResponse += hostPair[ 1 ].options[ 'shortName' ]
573 else:
574 pingResponse += "X"
575 # One of the host to host pair is unreachable
576 isReachable = main.FALSE
577 failedPings += 1
578 pingResponse += "\n"
579 main.log.info( pingResponse + "Failed pings: " + str( failedPings ) )
580 return isReachable
581 except Exception:
582 main.log.exception( self.name + ": Uncaught exception!" )
583 main.cleanAndExit()
584
Jon Hall43060f62020-06-23 13:13:33 -0700585 def pingallHostsUnidirectional( self, srcList, dstList, ipv6=False, wait=1, acceptableFailed=0, useScapy=False ):
586 """
587 Verify ping from each host in srcList to each host in dstList
588
589 acceptableFailed: max number of acceptable failed pings
590
591 Returns main.TRUE if all src hosts can reach all dst hosts
592 Returns main.FALSE if one or more of src hosts cannot reach one or more of dst hosts
593 """
594 try:
595 main.log.info( "Verifying ping from each src host to each dst host" )
596
597 srcComponentList = []
598 for hostName in srcList:
599 hostComponent = self.hosts[ hostName ]
600 if hostComponent:
601 main.log.debug( repr( hostComponent ) )
602 srcComponentList.append( hostComponent )
603 dstComponentList = []
604 for hostName in dstList:
605 hostComponent = self.hosts[ hostName ]
606 if hostComponent:
607 main.log.debug( repr( hostComponent ) )
608 dstComponentList.append( hostComponent )
609
610 isReachable = main.TRUE
611 wait = int( wait )
612 cmd = " ping" + ("6" if ipv6 else "") + " -c 1 -i 1 -W " + str( wait ) + " "
613 pingResponse = "Ping output:\n"
614 failedPingsTotal = 0
615 for srcHost in srcComponentList:
616 pingResponse += str( str( srcHost.shortName ) + " -> " )
617 for dstHost in dstComponentList:
618 failedPings = 0
619 dstIP = dstHost.ip
620 assert dstIP, "Not able to get IP address of host {}".format( dstHost )
621 for iface in srcHost.interfaces:
622 # FIXME This only works if one iface name is configured
623 # NOTE: We can use an IP with -I instead of an interface name as well
624 name = iface.get( 'name' )
625 if name:
626 cmd += " -I %s " % name
627
628 if useScapy:
629 while failedPings <= acceptableFailed:
630 main.log.debug( "Pinging from " + str( srcHost.shortName ) + " to " + str( dstHost.shortName ) )
631 # Use scapy to send and recieve packets
632 dstHost.startFilter()
633 srcHost.buildEther( dst=srcHost.interfaces[0].get( 'mac') )
634 srcHost.sendPacket()
635 output = dstHost.checkFilter()
636 main.log.debug( output )
637 if output:
638 #TODO: parse output?
639 packets = dstHost.readPackets()
640 for packet in packets.splitlines():
641 main.log.debug( packet )
642 pingResponse += " " + str( dstHost.shortName )
643 break
644 else:
645 kill = dstHost.killFilter()
646 main.log.debug( kill )
647 dstHost.handle.sendline( "" )
648 dstHost.handle.expect( dstHost.scapyPrompt )
649 main.log.debug( dstHost.handle.before )
650 failedPings += 1
651 time.sleep(1)
652 if failedPings > acceptableFailed:
653 # One of the host to host pair is unreachable
654 pingResponse += " X"
655 isReachable = main.FALSE
656 failedPingsTotal += 1
657
658 else:
659 pingCmd = cmd + str( dstIP )
660 while failedPings <= acceptableFailed:
661 main.log.debug( "Pinging from " + str( srcHost.shortName ) + " to " + str( dstHost.shortName ) )
662 self.handle.sendline( pingCmd )
663 self.handle.expect( self.prompt, timeout=wait + 5 )
664 response = self.handle.before
665 if re.search( ',\s0\%\spacket\sloss', response ):
666 pingResponse += " " + str( dstHost.shortName )
667 break
668 else:
669 failedPings += 1
670 time.sleep(1)
671 if failedPings > acceptableFailed:
672 # One of the host to host pair is unreachable
673 pingResponse += " X"
674 isReachable = main.FALSE
675 failedPingsTotal += 1
676 pingResponse += "\n"
677 main.log.info( pingResponse + "Failed pings: " + str( failedPingsTotal ) )
678 return isReachable
679 except AssertionError:
680 main.log.exception( "" )
681 return main.FALSE
682 except pexpect.TIMEOUT:
683 main.log.exception( self.name + ": TIMEOUT exception" )
684 response = self.handle.before
685 # NOTE: Send ctrl-c to make sure command is stopped
686 self.exitFromCmd( [ "Interrupt", self.prompt ] )
687 response += self.handle.before + self.handle.after
688 self.handle.sendline( "" )
689 self.handle.expect( self.prompt )
690 response += self.handle.before + self.handle.after
691 main.log.debug( response )
692 return main.FALSE
693 except pexpect.EOF:
694 main.log.error( self.name + ": EOF exception found" )
695 main.log.error( self.name + ": " + self.handle.before )
696 main.cleanAndExit()
697 except Exception:
698 main.log.exception( self.name + ": Uncaught exception!" )
699 main.cleanAndExit()
700
You Wang84f981d2018-01-12 16:11:50 -0800701 def iperftcp( self, host1="h1", host2="h2", timeout=6 ):
702 '''
703 Creates an iperf TCP test between two hosts. Returns main.TRUE if test results
704 are valid.
705 Optional:
706 timeout: The defualt timeout is 6 sec to allow enough time for a successful test to complete,
707 and short enough to stop an unsuccessful test from quiting and cleaning up mininet.
708 '''
709 main.log.info( self.name + ": Simple iperf TCP test between two hosts" )
710 # TODO: complete this function
711 return main.TRUE
You Wang4cc61912018-08-28 10:10:58 -0700712
713 def update( self ):
714 return main.TRUE
715
716 def verifyHostIp( self, hostList=[], prefix="", update=False ):
717 """
718 Description:
719 Verify that all hosts have IP address assigned to them
720 Optional:
721 hostList: If specified, verifications only happen to the hosts
722 in hostList
723 prefix: at least one of the ip address assigned to the host
724 needs to have the specified prefix
725 Returns:
726 main.TRUE if all hosts have specific IP address assigned;
727 main.FALSE otherwise
728 """
729 try:
You Wang4cc61912018-08-28 10:10:58 -0700730 if not hostList:
You Wangb1665b52019-02-01 15:49:48 -0800731 hostList = self.hosts.keys()
732 for hostName, hostComponent in self.hosts.items():
You Wang4cc61912018-08-28 10:10:58 -0700733 if hostName not in hostList:
734 continue
735 ipList = []
736 ipa = hostComponent.ip()
737 ipv4Pattern = r'inet ((?:[0-9]{1,3}\.){3}[0-9]{1,3})/'
738 ipList += re.findall( ipv4Pattern, ipa )
739 # It's tricky to make regex for IPv6 addresses and this one is simplified
740 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})/'
741 ipList += re.findall( ipv6Pattern, ipa )
742 main.log.debug( self.name + ": IP list on host " + str( hostName ) + ": " + str( ipList ) )
743 if not ipList:
744 main.log.warn( self.name + ": Failed to discover any IP addresses on host " + str( hostName ) )
745 else:
746 if not any( ip.startswith( str( prefix ) ) for ip in ipList ):
747 main.log.warn( self.name + ": None of the IPs on host " + str( hostName ) + " has prefix " + str( prefix ) )
748 else:
749 main.log.debug( self.name + ": Found matching IP on host " + str( hostName ) )
750 hostList.remove( hostName )
751 return main.FALSE if hostList else main.TRUE
752 except KeyError:
You Wangb1665b52019-02-01 15:49:48 -0800753 main.log.exception( self.name + ": host data not as expected: " + self.hosts.keys() )
You Wang4cc61912018-08-28 10:10:58 -0700754 return None
755 except pexpect.EOF:
756 main.log.error( self.name + ": EOF exception found" )
757 main.log.error( self.name + ": " + self.handle.before )
758 main.cleanAndExit()
759 except Exception:
760 main.log.exception( self.name + ": Uncaught exception" )
761 return None
762
763 def addRoute( self, host, dstIP, interface, ipv6=False ):
764 """
765 Add a route to host
766 Ex: h1 route add -host 224.2.0.1 h1-eth0
767 """
768 try:
769 if ipv6:
770 cmd = "sudo route -A inet6 add "
771 else:
772 cmd = "sudo route add -host "
773 cmd += str( dstIP ) + " " + str( interface )
774 response = self.runCmdOnHost( host, cmd )
775 main.log.debug( "response = " + response )
776 return main.TRUE
777 except pexpect.TIMEOUT:
778 main.log.error( self.name + ": TIMEOUT exception found" )
779 main.log.error( self.name + ": " + self.handle.before )
780 main.cleanAndExit()
781 except pexpect.EOF:
782 main.log.error( self.name + ": EOF exception found" )
783 main.log.error( self.name + ": " + self.handle.before )
784 return main.FALSE
785 except Exception:
786 main.log.exception( self.name + ": Uncaught exception!" )
787 main.cleanAndExit()
You Wang0fc21702018-11-02 17:49:18 -0700788
789 def getIPAddress( self, host, proto='IPV4' ):
790 """
791 Returns IP address of the host
792 """
793 response = self.runCmdOnHost( host, "ifconfig" )
794 pattern = ''
795 if proto == 'IPV4':
796 pattern = "inet\s(\d+\.\d+\.\d+\.\d+)\s\snetmask"
797 else:
798 pattern = "inet6\s([\w,:]*)/\d+\s\sprefixlen"
799 ipAddressSearch = re.search( pattern, response )
800 if not ipAddressSearch:
801 return None
802 main.log.info(
803 self.name +
804 ": IP-Address of Host " +
805 host +
806 " is " +
807 ipAddressSearch.group( 1 ) )
808 return ipAddressSearch.group( 1 )
You Wangb1665b52019-02-01 15:49:48 -0800809
810 def getLinkRandom( self, timeout=60, nonCut=True, excludeNodes=[], skipLinks=[] ):
811 """
812 Randomly get a link from network topology.
813 If nonCut is True, it gets a list of non-cut links (the deletion
814 of a non-cut link will not increase the number of connected
815 component of a graph) and randomly returns one of them, otherwise
816 it just randomly returns one link from all current links.
817 excludeNodes will be passed to getLinks and getGraphDict method.
818 Any link that has either end included in skipLinks will be excluded.
819 Returns the link as a list, e.g. [ 's1', 's2' ].
820 """
You Wangb1665b52019-02-01 15:49:48 -0800821 candidateLinks = []
822 try:
823 if not nonCut:
824 links = self.getLinks( timeout=timeout, excludeNodes=excludeNodes )
825 assert len( links ) != 0
826 for link in links:
827 # Exclude host-switch link
828 if link[ 'node1' ].startswith( 'h' ) or link[ 'node2' ].startswith( 'h' ):
829 continue
830 candidateLinks.append( [ link[ 'node1' ], link[ 'node2' ] ] )
831 else:
832 graphDict = self.getGraphDict( timeout=timeout, useId=False,
833 excludeNodes=excludeNodes )
834 if graphDict is None:
835 return None
836 self.graph.update( graphDict )
837 candidateLinks = self.graph.getNonCutEdges()
838 candidateLinks = [ link for link in candidateLinks
839 if link[0] not in skipLinks and link[1] not in skipLinks ]
840 if candidateLinks is None:
841 return None
842 elif len( candidateLinks ) == 0:
843 main.log.info( self.name + ": No candidate link for deletion" )
844 return None
845 else:
846 link = random.sample( candidateLinks, 1 )
847 return link[ 0 ]
848 except KeyError:
849 main.log.exception( self.name + ": KeyError exception found" )
850 return None
851 except AssertionError:
852 main.log.exception( self.name + ": AssertionError exception found" )
853 return None
854 except Exception:
855 main.log.exception( self.name + ": Uncaught exception" )
856 return None
857
858 def getSwitchRandom( self, timeout=60, nonCut=True, excludeNodes=[], skipSwitches=[] ):
859 """
860 Randomly get a switch from network topology.
861 If nonCut is True, it gets a list of non-cut switches (the deletion
862 of a non-cut switch will not increase the number of connected
863 components of a graph) and randomly returns one of them, otherwise
864 it just randomly returns one switch from all current switches in
865 Mininet.
866 excludeNodes will be pased to getSwitches and getGraphDict method.
867 Switches specified in skipSwitches will be excluded.
868 Returns the name of the chosen switch.
869 """
You Wangb1665b52019-02-01 15:49:48 -0800870 candidateSwitches = []
871 try:
872 if not nonCut:
873 switches = self.getSwitches( timeout=timeout, excludeNodes=excludeNodes )
874 assert len( switches ) != 0
875 for switchName in switches.keys():
876 candidateSwitches.append( switchName )
877 else:
878 graphDict = self.getGraphDict( timeout=timeout, useId=False,
879 excludeNodes=excludeNodes )
880 if graphDict is None:
881 return None
882 self.graph.update( graphDict )
883 candidateSwitches = self.graph.getNonCutVertices()
884 candidateSwitches = [ switch for switch in candidateSwitches if switch not in skipSwitches ]
885 if candidateSwitches is None:
886 return None
887 elif len( candidateSwitches ) == 0:
888 main.log.info( self.name + ": No candidate switch for deletion" )
889 return None
890 else:
891 switch = random.sample( candidateSwitches, 1 )
892 return switch[ 0 ]
893 except KeyError:
894 main.log.exception( self.name + ": KeyError exception found" )
895 return None
896 except AssertionError:
897 main.log.exception( self.name + ": AssertionError exception found" )
898 return None
899 except Exception:
900 main.log.exception( self.name + ": Uncaught exception" )
901 return None
902
903 def getGraphDict( self, timeout=60, useId=True, includeHost=False,
904 excludeNodes=[] ):
905 """
906 Return a dictionary which describes the latest network topology data as a
907 graph.
908 An example of the dictionary:
909 { vertex1: { 'edges': ..., 'name': ..., 'protocol': ... },
910 vertex2: { 'edges': ..., 'name': ..., 'protocol': ... } }
911 Each vertex should at least have an 'edges' attribute which describes the
912 adjacency information. The value of 'edges' attribute is also represented by
913 a dictionary, which maps each edge (identified by the neighbor vertex) to a
914 list of attributes.
915 An example of the edges dictionary:
916 'edges': { vertex2: { 'port': ..., 'weight': ... },
917 vertex3: { 'port': ..., 'weight': ... } }
918 If useId == True, dpid/mac will be used instead of names to identify
919 vertices, which is helpful when e.g. comparing network topology with ONOS
920 topology.
921 If includeHost == True, all hosts (and host-switch links) will be included
922 in topology data.
923 excludeNodes will be passed to getSwitches and getLinks methods to exclude
924 unexpected switches and links.
925 """
926 # TODO: support excludeNodes
927 graphDict = {}
928 try:
929 links = self.getLinks( timeout=timeout, excludeNodes=excludeNodes )
930 portDict = {}
931 switches = self.getSwitches( excludeNodes=excludeNodes )
932 if includeHost:
933 hosts = self.getHosts()
934 for link in links:
935 # TODO: support 'includeHost' argument
936 if link[ 'node1' ].startswith( 'h' ) or link[ 'node2' ].startswith( 'h' ):
937 continue
938 nodeName1 = link[ 'node1' ]
939 nodeName2 = link[ 'node2' ]
940 if not self.switches[ nodeName1 ].isup or not self.switches[ nodeName2 ].isup:
941 continue
942 port1 = link[ 'port1' ]
943 port2 = link[ 'port2' ]
944 # Loop for two nodes
945 for i in range( 2 ):
946 portIndex = port1
947 if useId:
948 node1 = 'of:' + str( switches[ nodeName1 ][ 'dpid' ] )
949 node2 = 'of:' + str( switches[ nodeName2 ][ 'dpid' ] )
950 else:
951 node1 = nodeName1
952 node2 = nodeName2
953 if node1 not in graphDict.keys():
954 if useId:
955 graphDict[ node1 ] = { 'edges': {},
956 'dpid': switches[ nodeName1 ][ 'dpid' ],
957 'name': nodeName1,
958 'ports': switches[ nodeName1 ][ 'ports' ],
959 'swClass': switches[ nodeName1 ][ 'swClass' ],
960 'pid': switches[ nodeName1 ][ 'pid' ],
961 'options': switches[ nodeName1 ][ 'options' ] }
962 else:
963 graphDict[ node1 ] = { 'edges': {} }
964 else:
965 # Assert node2 is not connected to any current links of node1
966 # assert node2 not in graphDict[ node1 ][ 'edges' ].keys()
967 pass
968 for port in switches[ nodeName1 ][ 'ports' ]:
969 if port[ 'of_port' ] == str( portIndex ):
970 # Use -1 as index for disabled port
971 if port[ 'enabled' ]:
972 graphDict[ node1 ][ 'edges' ][ node2 ] = { 'port': portIndex }
973 else:
974 graphDict[ node1 ][ 'edges' ][ node2 ] = { 'port': -1 }
975 # Swap two nodes/ports
976 nodeName1, nodeName2 = nodeName2, nodeName1
977 port1, port2 = port2, port1
978 # Remove links with disabled ports
979 linksToRemove = []
980 for node, edges in graphDict.items():
981 for neighbor, port in edges[ 'edges' ].items():
982 if port[ 'port' ] == -1:
983 linksToRemove.append( ( node, neighbor ) )
984 for node1, node2 in linksToRemove:
985 for i in range( 2 ):
986 if graphDict.get( node1 )[ 'edges' ].get( node2 ):
987 graphDict[ node1 ][ 'edges' ].pop( node2 )
988 node1, node2 = node2, node1
989 return graphDict
990 except KeyError:
991 main.log.exception( self.name + ": KeyError exception found" )
992 return None
993 except AssertionError:
994 main.log.exception( self.name + ": AssertionError exception found" )
995 return None
996 except pexpect.EOF:
997 main.log.error( self.name + ": EOF exception found" )
998 main.log.error( self.name + ": " + self.handle.before )
999 main.cleanAndExit()
1000 except Exception:
1001 main.log.exception( self.name + ": Uncaught exception" )
1002 return None
1003
1004 def switch( self, **switchargs ):
1005 """
1006 start/stop a switch
1007 """
1008 args = utilities.parse_args( [ "SW", "OPTION" ], **switchargs )
1009 sw = args[ "SW" ] if args[ "SW" ] is not None else ""
1010 option = args[ "OPTION" ] if args[ "OPTION" ] is not None else ""
1011 try:
1012 switchComponent = self.switches[ sw ]
1013 if option == 'stop':
1014 switchComponent.stopOfAgent()
1015 elif option == 'start':
1016 switchComponent.startOfAgent()
1017 else:
1018 main.log.warn( self.name + ": Unknown switch command" )
1019 return main.FALSE
1020 return main.TRUE
1021 except KeyError:
1022 main.log.error( self.name + ": Not able to find switch [}".format( sw ) )
1023 except pexpect.TIMEOUT:
1024 main.log.error( self.name + ": TIMEOUT exception found" )
1025 main.log.error( self.name + ": " + self.handle.before )
1026 return None
1027 except pexpect.EOF:
1028 main.log.error( self.name + ": EOF exception found" )
1029 main.log.error( self.name + ": " + self.handle.before )
1030 main.cleanAndExit()
1031 except Exception:
1032 main.log.exception( self.name + ": Uncaught exception" )
1033 main.cleanAndExit()
1034
1035 def discoverHosts( self, hostList=[], wait=1000, dstIp="6.6.6.6", dstIp6="1020::3fe" ):
1036 '''
1037 Hosts in hostList will do a single ARP/ND to a non-existent address for ONOS to
1038 discover them. A host will use arping/ndisc6 to send ARP/ND depending on if it
1039 has IPv4/IPv6 addresses configured.
1040 Optional:
1041 hostList: a list of names of the hosts that need to be discovered. If not
1042 specified mininet will send ping from all the hosts
1043 wait: timeout for ARP/ND in milliseconds
1044 dstIp: destination address used by IPv4 hosts
1045 dstIp6: destination address used by IPv6 hosts
1046 Returns:
1047 main.TRUE if all packets were successfully sent. Otherwise main.FALSE
1048 '''
1049 try:
1050 hosts = self.getHosts()
1051 if not hostList:
1052 hostList = hosts.keys()
1053 discoveryResult = main.TRUE
1054 for host in hostList:
1055 flushCmd = ""
1056 cmd = ""
Jon Hall43060f62020-06-23 13:13:33 -07001057 if self.getIPAddress( host ) or hosts[host]['interfaces'][0].get( 'ips', False ) :
You Wangb1665b52019-02-01 15:49:48 -08001058 flushCmd = "sudo ip neigh flush all"
Jon Hall43060f62020-06-23 13:13:33 -07001059 intf = hosts[host]['interfaces'][0].get( 'name' )
1060 intfStr = "-i {}".format( intf ) if intf else ""
1061 cmd = "sudo arping -c 1 -w {} {} {}".format( wait, intfStr, dstIp )
You Wangb1665b52019-02-01 15:49:48 -08001062 main.log.debug( "Sending IPv4 arping from host {}".format( host ) )
1063 elif self.getIPAddress( host, proto='IPV6' ):
1064 flushCmd = "sudo ip -6 neigh flush all"
1065 intf = hosts[host]['interfaces'][0]['name']
1066 cmd = "ndisc6 -r 1 -w {} {} {}".format( wait, dstIp6, intf )
1067 main.log.debug( "Sending IPv6 ND from host {}".format( host ) )
1068 else:
1069 main.log.warn( "No IP addresses configured on host {}, skipping discovery".format( host ) )
1070 discoveryResult = main.FALSE
1071 if cmd:
1072 self.runCmdOnHost( host, flushCmd )
1073 self.runCmdOnHost( host, cmd )
1074 return discoveryResult
1075 except Exception:
1076 main.log.exception( self.name + ": Uncaught exception!" )
1077 main.cleanAndExit()