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