blob: 2b70be61f1c59c9af930310628282cd9a8b373a6 [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 )
542 hostPair[ 1 ].addRoutes()
543 filters = []
544 if srcMac:
545 filters.append( "ether src host %s" % srcMac )
546 if srcIPs[0]:
547 filters.append( "ip src host %s" % srcIPs[0] )
548 hostPair[ 1 ].startFilter( ifaceName=dstIface, pktFilter=" and ".join(filters) )
549 hostPair[ 0 ].startScapy( ifaceName=srcIface )
550 hostPair[ 0 ].addRoutes()
551 hostPair[ 0 ].buildEther( src=srcMac, dst=dstMac )
552 if VLAN:
553 hostPair[ 0 ].buildVLAN( vlan=VLAN )
554 hostPair[ 0 ].buildIP( src=srcIPs[0], dst=dstIPs[0] )
555 hostPair[ 0 ].buildICMP( vlan=VLAN )
556 hostPair[ 0 ].sendPacket( iface=srcIface )
Jon Hall43060f62020-06-23 13:13:33 -0700557
Siddeshde1d1692021-09-15 18:12:57 +0000558 waiting = not hostPair[ 1 ].checkFilter()
559 if not waiting:
560 pingResult = main.FALSE
561 packets = hostPair[ 1 ].readPackets()
562 main.log.warn( repr( packets ) )
563 for packet in packets.splitlines():
564 main.log.debug( packet )
565 if srcIPs[0] in packet:
566 pingResult = main.TRUE
567 else:
568 main.log.warn( "Did not receive packets, killing filter" )
569 kill = hostPair[ 1 ].killFilter()
570 main.log.debug( kill )
571 hostPair[ 1 ].handle.sendline( "" )
572 hostPair[ 1 ].handle.expect( hostPair[ 1 ].scapyPrompt )
573 main.log.debug( hostPair[ 1 ].handle.before )
574 # One of the host to host pair is unreachable
575 pingResult = main.FALSE
576 hostPair[ 0 ].stopScapy()
577 hostPair[ 1 ].stopScapy()
578 if pingResult:
579 pingResponse += hostPair[ 1 ].options[ 'shortName' ]
580 if VLAN:
581 pingResponse += "." + str( VLAN )
582 else:
583 pingResponse += "X"
584 # One of the host to host pair is unreachable
585 isReachable = main.FALSE
586 failedPings += 1
587 pingResponse += "\n"
Jon Hall43060f62020-06-23 13:13:33 -0700588 else:
Siddeshde1d1692021-09-15 18:12:57 +0000589 pingResponse += hostPair[ 0 ].options[ 'shortName' ] + " -> "
Jon Hall43060f62020-06-23 13:13:33 -0700590 pingResult = hostPair[ 0 ].ping( ipDst, interface=srcIface, wait=int( wait ) )
Siddeshde1d1692021-09-15 18:12:57 +0000591 if pingResult:
592 pingResponse += hostPair[ 1 ].options[ 'shortName' ]
593 if VLAN:
594 pingResponse += "." + str( VLAN )
595 else:
596 pingResponse += "X"
597 # One of the host to host pair is unreachable
598 isReachable = main.FALSE
599 failedPings += 1
600 pingResponse += "\n"
You Wang84f981d2018-01-12 16:11:50 -0800601 main.log.info( pingResponse + "Failed pings: " + str( failedPings ) )
602 return isReachable
603 except Exception:
604 main.log.exception( self.name + ": Uncaught exception!" )
605 main.cleanAndExit()
606
Jon Hall43060f62020-06-23 13:13:33 -0700607 def pingallHostsUnidirectional( self, srcList, dstList, ipv6=False, wait=1, acceptableFailed=0, useScapy=False ):
608 """
609 Verify ping from each host in srcList to each host in dstList
610
611 acceptableFailed: max number of acceptable failed pings
612
613 Returns main.TRUE if all src hosts can reach all dst hosts
614 Returns main.FALSE if one or more of src hosts cannot reach one or more of dst hosts
615 """
616 try:
617 main.log.info( "Verifying ping from each src host to each dst host" )
618
619 srcComponentList = []
620 for hostName in srcList:
621 hostComponent = self.hosts[ hostName ]
622 if hostComponent:
623 main.log.debug( repr( hostComponent ) )
624 srcComponentList.append( hostComponent )
625 dstComponentList = []
626 for hostName in dstList:
627 hostComponent = self.hosts[ hostName ]
628 if hostComponent:
629 main.log.debug( repr( hostComponent ) )
630 dstComponentList.append( hostComponent )
631
632 isReachable = main.TRUE
633 wait = int( wait )
634 cmd = " ping" + ("6" if ipv6 else "") + " -c 1 -i 1 -W " + str( wait ) + " "
635 pingResponse = "Ping output:\n"
636 failedPingsTotal = 0
637 for srcHost in srcComponentList:
638 pingResponse += str( str( srcHost.shortName ) + " -> " )
639 for dstHost in dstComponentList:
640 failedPings = 0
Jon Hall06fd0df2021-01-25 15:50:06 -0800641 dstIP = dstHost.ip_address
Jon Hall43060f62020-06-23 13:13:33 -0700642 assert dstIP, "Not able to get IP address of host {}".format( dstHost )
643 for iface in srcHost.interfaces:
644 # FIXME This only works if one iface name is configured
645 # NOTE: We can use an IP with -I instead of an interface name as well
646 name = iface.get( 'name' )
647 if name:
648 cmd += " -I %s " % name
649
650 if useScapy:
651 while failedPings <= acceptableFailed:
652 main.log.debug( "Pinging from " + str( srcHost.shortName ) + " to " + str( dstHost.shortName ) )
653 # Use scapy to send and recieve packets
654 dstHost.startFilter()
Jon Hall627b1572020-12-01 12:01:15 -0800655 srcHost.buildEther( src=srcHost.interfaces[0].get( 'mac'), dst=dstHost.interfaces[0].get( 'mac') )
Jon Hall43060f62020-06-23 13:13:33 -0700656 srcHost.sendPacket()
657 output = dstHost.checkFilter()
658 main.log.debug( output )
659 if output:
Jon Hall9b0de1f2020-08-24 15:38:04 -0700660 # TODO: parse output?
Jon Hall43060f62020-06-23 13:13:33 -0700661 packets = dstHost.readPackets()
662 for packet in packets.splitlines():
663 main.log.debug( packet )
664 pingResponse += " " + str( dstHost.shortName )
665 break
666 else:
667 kill = dstHost.killFilter()
668 main.log.debug( kill )
669 dstHost.handle.sendline( "" )
670 dstHost.handle.expect( dstHost.scapyPrompt )
671 main.log.debug( dstHost.handle.before )
672 failedPings += 1
673 time.sleep(1)
674 if failedPings > acceptableFailed:
675 # One of the host to host pair is unreachable
676 pingResponse += " X"
677 isReachable = main.FALSE
678 failedPingsTotal += 1
679
680 else:
681 pingCmd = cmd + str( dstIP )
682 while failedPings <= acceptableFailed:
683 main.log.debug( "Pinging from " + str( srcHost.shortName ) + " to " + str( dstHost.shortName ) )
684 self.handle.sendline( pingCmd )
685 self.handle.expect( self.prompt, timeout=wait + 5 )
686 response = self.handle.before
687 if re.search( ',\s0\%\spacket\sloss', response ):
688 pingResponse += " " + str( dstHost.shortName )
689 break
690 else:
691 failedPings += 1
692 time.sleep(1)
693 if failedPings > acceptableFailed:
694 # One of the host to host pair is unreachable
695 pingResponse += " X"
696 isReachable = main.FALSE
697 failedPingsTotal += 1
698 pingResponse += "\n"
699 main.log.info( pingResponse + "Failed pings: " + str( failedPingsTotal ) )
700 return isReachable
701 except AssertionError:
702 main.log.exception( "" )
703 return main.FALSE
704 except pexpect.TIMEOUT:
705 main.log.exception( self.name + ": TIMEOUT exception" )
706 response = self.handle.before
707 # NOTE: Send ctrl-c to make sure command is stopped
708 self.exitFromCmd( [ "Interrupt", self.prompt ] )
709 response += self.handle.before + self.handle.after
710 self.handle.sendline( "" )
711 self.handle.expect( self.prompt )
712 response += self.handle.before + self.handle.after
713 main.log.debug( response )
714 return main.FALSE
715 except pexpect.EOF:
716 main.log.error( self.name + ": EOF exception found" )
717 main.log.error( self.name + ": " + self.handle.before )
718 main.cleanAndExit()
719 except Exception:
720 main.log.exception( self.name + ": Uncaught exception!" )
721 main.cleanAndExit()
722
You Wang84f981d2018-01-12 16:11:50 -0800723 def iperftcp( self, host1="h1", host2="h2", timeout=6 ):
724 '''
725 Creates an iperf TCP test between two hosts. Returns main.TRUE if test results
726 are valid.
727 Optional:
728 timeout: The defualt timeout is 6 sec to allow enough time for a successful test to complete,
729 and short enough to stop an unsuccessful test from quiting and cleaning up mininet.
730 '''
731 main.log.info( self.name + ": Simple iperf TCP test between two hosts" )
732 # TODO: complete this function
733 return main.TRUE
You Wang4cc61912018-08-28 10:10:58 -0700734
735 def update( self ):
736 return main.TRUE
737
738 def verifyHostIp( self, hostList=[], prefix="", update=False ):
739 """
740 Description:
741 Verify that all hosts have IP address assigned to them
742 Optional:
743 hostList: If specified, verifications only happen to the hosts
744 in hostList
745 prefix: at least one of the ip address assigned to the host
746 needs to have the specified prefix
747 Returns:
748 main.TRUE if all hosts have specific IP address assigned;
749 main.FALSE otherwise
750 """
751 try:
You Wang4cc61912018-08-28 10:10:58 -0700752 if not hostList:
You Wangb1665b52019-02-01 15:49:48 -0800753 hostList = self.hosts.keys()
754 for hostName, hostComponent in self.hosts.items():
You Wang4cc61912018-08-28 10:10:58 -0700755 if hostName not in hostList:
756 continue
757 ipList = []
758 ipa = hostComponent.ip()
759 ipv4Pattern = r'inet ((?:[0-9]{1,3}\.){3}[0-9]{1,3})/'
760 ipList += re.findall( ipv4Pattern, ipa )
761 # It's tricky to make regex for IPv6 addresses and this one is simplified
762 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})/'
763 ipList += re.findall( ipv6Pattern, ipa )
764 main.log.debug( self.name + ": IP list on host " + str( hostName ) + ": " + str( ipList ) )
765 if not ipList:
766 main.log.warn( self.name + ": Failed to discover any IP addresses on host " + str( hostName ) )
767 else:
768 if not any( ip.startswith( str( prefix ) ) for ip in ipList ):
769 main.log.warn( self.name + ": None of the IPs on host " + str( hostName ) + " has prefix " + str( prefix ) )
770 else:
771 main.log.debug( self.name + ": Found matching IP on host " + str( hostName ) )
772 hostList.remove( hostName )
773 return main.FALSE if hostList else main.TRUE
774 except KeyError:
You Wangb1665b52019-02-01 15:49:48 -0800775 main.log.exception( self.name + ": host data not as expected: " + self.hosts.keys() )
You Wang4cc61912018-08-28 10:10:58 -0700776 return None
777 except pexpect.EOF:
778 main.log.error( self.name + ": EOF exception found" )
779 main.log.error( self.name + ": " + self.handle.before )
780 main.cleanAndExit()
781 except Exception:
782 main.log.exception( self.name + ": Uncaught exception" )
783 return None
784
785 def addRoute( self, host, dstIP, interface, ipv6=False ):
786 """
787 Add a route to host
788 Ex: h1 route add -host 224.2.0.1 h1-eth0
789 """
790 try:
791 if ipv6:
792 cmd = "sudo route -A inet6 add "
793 else:
794 cmd = "sudo route add -host "
795 cmd += str( dstIP ) + " " + str( interface )
796 response = self.runCmdOnHost( host, cmd )
797 main.log.debug( "response = " + response )
798 return main.TRUE
799 except pexpect.TIMEOUT:
800 main.log.error( self.name + ": TIMEOUT exception found" )
801 main.log.error( self.name + ": " + self.handle.before )
802 main.cleanAndExit()
803 except pexpect.EOF:
804 main.log.error( self.name + ": EOF exception found" )
805 main.log.error( self.name + ": " + self.handle.before )
806 return main.FALSE
807 except Exception:
808 main.log.exception( self.name + ": Uncaught exception!" )
809 main.cleanAndExit()
You Wang0fc21702018-11-02 17:49:18 -0700810
Jon Hall627b1572020-12-01 12:01:15 -0800811 def getIPAddress( self, host, iface=None, proto='IPV4' ):
You Wang0fc21702018-11-02 17:49:18 -0700812 """
813 Returns IP address of the host
814 """
Jon Hall32c90f32021-06-24 16:32:44 -0700815 hostComponent = self.hosts[ host ]
816 if hostComponent:
817 return hostComponent.getIPAddress( iface=iface, proto=proto )
You Wang0fc21702018-11-02 17:49:18 -0700818 else:
Jon Hall32c90f32021-06-24 16:32:44 -0700819 main.log.warn( self.name + ": Could not find host with short name '%s'" % host )
You Wang0fc21702018-11-02 17:49:18 -0700820 return None
You Wangb1665b52019-02-01 15:49:48 -0800821
822 def getLinkRandom( self, timeout=60, nonCut=True, excludeNodes=[], skipLinks=[] ):
823 """
824 Randomly get a link from network topology.
825 If nonCut is True, it gets a list of non-cut links (the deletion
826 of a non-cut link will not increase the number of connected
827 component of a graph) and randomly returns one of them, otherwise
828 it just randomly returns one link from all current links.
829 excludeNodes will be passed to getLinks and getGraphDict method.
830 Any link that has either end included in skipLinks will be excluded.
831 Returns the link as a list, e.g. [ 's1', 's2' ].
832 """
You Wangb1665b52019-02-01 15:49:48 -0800833 candidateLinks = []
834 try:
835 if not nonCut:
836 links = self.getLinks( timeout=timeout, excludeNodes=excludeNodes )
837 assert len( links ) != 0
838 for link in links:
839 # Exclude host-switch link
840 if link[ 'node1' ].startswith( 'h' ) or link[ 'node2' ].startswith( 'h' ):
841 continue
842 candidateLinks.append( [ link[ 'node1' ], link[ 'node2' ] ] )
843 else:
844 graphDict = self.getGraphDict( timeout=timeout, useId=False,
845 excludeNodes=excludeNodes )
846 if graphDict is None:
847 return None
848 self.graph.update( graphDict )
849 candidateLinks = self.graph.getNonCutEdges()
850 candidateLinks = [ link for link in candidateLinks
851 if link[0] not in skipLinks and link[1] not in skipLinks ]
852 if candidateLinks is None:
853 return None
854 elif len( candidateLinks ) == 0:
855 main.log.info( self.name + ": No candidate link for deletion" )
856 return None
857 else:
858 link = random.sample( candidateLinks, 1 )
859 return link[ 0 ]
860 except KeyError:
861 main.log.exception( self.name + ": KeyError exception found" )
862 return None
863 except AssertionError:
864 main.log.exception( self.name + ": AssertionError exception found" )
865 return None
866 except Exception:
867 main.log.exception( self.name + ": Uncaught exception" )
868 return None
869
870 def getSwitchRandom( self, timeout=60, nonCut=True, excludeNodes=[], skipSwitches=[] ):
871 """
872 Randomly get a switch from network topology.
873 If nonCut is True, it gets a list of non-cut switches (the deletion
874 of a non-cut switch will not increase the number of connected
875 components of a graph) and randomly returns one of them, otherwise
876 it just randomly returns one switch from all current switches in
877 Mininet.
878 excludeNodes will be pased to getSwitches and getGraphDict method.
879 Switches specified in skipSwitches will be excluded.
880 Returns the name of the chosen switch.
881 """
You Wangb1665b52019-02-01 15:49:48 -0800882 candidateSwitches = []
883 try:
884 if not nonCut:
885 switches = self.getSwitches( timeout=timeout, excludeNodes=excludeNodes )
886 assert len( switches ) != 0
887 for switchName in switches.keys():
888 candidateSwitches.append( switchName )
889 else:
890 graphDict = self.getGraphDict( timeout=timeout, useId=False,
891 excludeNodes=excludeNodes )
892 if graphDict is None:
893 return None
894 self.graph.update( graphDict )
895 candidateSwitches = self.graph.getNonCutVertices()
896 candidateSwitches = [ switch for switch in candidateSwitches if switch not in skipSwitches ]
897 if candidateSwitches is None:
898 return None
899 elif len( candidateSwitches ) == 0:
900 main.log.info( self.name + ": No candidate switch for deletion" )
901 return None
902 else:
903 switch = random.sample( candidateSwitches, 1 )
904 return switch[ 0 ]
905 except KeyError:
906 main.log.exception( self.name + ": KeyError exception found" )
907 return None
908 except AssertionError:
909 main.log.exception( self.name + ": AssertionError exception found" )
910 return None
911 except Exception:
912 main.log.exception( self.name + ": Uncaught exception" )
913 return None
914
915 def getGraphDict( self, timeout=60, useId=True, includeHost=False,
916 excludeNodes=[] ):
917 """
918 Return a dictionary which describes the latest network topology data as a
919 graph.
920 An example of the dictionary:
921 { vertex1: { 'edges': ..., 'name': ..., 'protocol': ... },
922 vertex2: { 'edges': ..., 'name': ..., 'protocol': ... } }
923 Each vertex should at least have an 'edges' attribute which describes the
924 adjacency information. The value of 'edges' attribute is also represented by
925 a dictionary, which maps each edge (identified by the neighbor vertex) to a
926 list of attributes.
927 An example of the edges dictionary:
928 'edges': { vertex2: { 'port': ..., 'weight': ... },
929 vertex3: { 'port': ..., 'weight': ... } }
930 If useId == True, dpid/mac will be used instead of names to identify
931 vertices, which is helpful when e.g. comparing network topology with ONOS
932 topology.
933 If includeHost == True, all hosts (and host-switch links) will be included
934 in topology data.
935 excludeNodes will be passed to getSwitches and getLinks methods to exclude
936 unexpected switches and links.
937 """
938 # TODO: support excludeNodes
939 graphDict = {}
940 try:
941 links = self.getLinks( timeout=timeout, excludeNodes=excludeNodes )
942 portDict = {}
943 switches = self.getSwitches( excludeNodes=excludeNodes )
944 if includeHost:
945 hosts = self.getHosts()
946 for link in links:
947 # TODO: support 'includeHost' argument
948 if link[ 'node1' ].startswith( 'h' ) or link[ 'node2' ].startswith( 'h' ):
949 continue
950 nodeName1 = link[ 'node1' ]
951 nodeName2 = link[ 'node2' ]
952 if not self.switches[ nodeName1 ].isup or not self.switches[ nodeName2 ].isup:
953 continue
954 port1 = link[ 'port1' ]
955 port2 = link[ 'port2' ]
956 # Loop for two nodes
957 for i in range( 2 ):
958 portIndex = port1
959 if useId:
960 node1 = 'of:' + str( switches[ nodeName1 ][ 'dpid' ] )
961 node2 = 'of:' + str( switches[ nodeName2 ][ 'dpid' ] )
962 else:
963 node1 = nodeName1
964 node2 = nodeName2
965 if node1 not in graphDict.keys():
966 if useId:
967 graphDict[ node1 ] = { 'edges': {},
968 'dpid': switches[ nodeName1 ][ 'dpid' ],
969 'name': nodeName1,
970 'ports': switches[ nodeName1 ][ 'ports' ],
971 'swClass': switches[ nodeName1 ][ 'swClass' ],
972 'pid': switches[ nodeName1 ][ 'pid' ],
973 'options': switches[ nodeName1 ][ 'options' ] }
974 else:
975 graphDict[ node1 ] = { 'edges': {} }
976 else:
977 # Assert node2 is not connected to any current links of node1
978 # assert node2 not in graphDict[ node1 ][ 'edges' ].keys()
979 pass
980 for port in switches[ nodeName1 ][ 'ports' ]:
981 if port[ 'of_port' ] == str( portIndex ):
982 # Use -1 as index for disabled port
983 if port[ 'enabled' ]:
984 graphDict[ node1 ][ 'edges' ][ node2 ] = { 'port': portIndex }
985 else:
986 graphDict[ node1 ][ 'edges' ][ node2 ] = { 'port': -1 }
987 # Swap two nodes/ports
988 nodeName1, nodeName2 = nodeName2, nodeName1
989 port1, port2 = port2, port1
990 # Remove links with disabled ports
991 linksToRemove = []
992 for node, edges in graphDict.items():
993 for neighbor, port in edges[ 'edges' ].items():
994 if port[ 'port' ] == -1:
995 linksToRemove.append( ( node, neighbor ) )
996 for node1, node2 in linksToRemove:
997 for i in range( 2 ):
998 if graphDict.get( node1 )[ 'edges' ].get( node2 ):
999 graphDict[ node1 ][ 'edges' ].pop( node2 )
1000 node1, node2 = node2, node1
1001 return graphDict
1002 except KeyError:
1003 main.log.exception( self.name + ": KeyError exception found" )
1004 return None
1005 except AssertionError:
1006 main.log.exception( self.name + ": AssertionError exception found" )
1007 return None
1008 except pexpect.EOF:
1009 main.log.error( self.name + ": EOF exception found" )
1010 main.log.error( self.name + ": " + self.handle.before )
1011 main.cleanAndExit()
1012 except Exception:
1013 main.log.exception( self.name + ": Uncaught exception" )
1014 return None
1015
1016 def switch( self, **switchargs ):
1017 """
1018 start/stop a switch
1019 """
1020 args = utilities.parse_args( [ "SW", "OPTION" ], **switchargs )
1021 sw = args[ "SW" ] if args[ "SW" ] is not None else ""
1022 option = args[ "OPTION" ] if args[ "OPTION" ] is not None else ""
1023 try:
1024 switchComponent = self.switches[ sw ]
1025 if option == 'stop':
1026 switchComponent.stopOfAgent()
1027 elif option == 'start':
1028 switchComponent.startOfAgent()
1029 else:
1030 main.log.warn( self.name + ": Unknown switch command" )
1031 return main.FALSE
1032 return main.TRUE
1033 except KeyError:
1034 main.log.error( self.name + ": Not able to find switch [}".format( sw ) )
1035 except pexpect.TIMEOUT:
1036 main.log.error( self.name + ": TIMEOUT exception found" )
1037 main.log.error( self.name + ": " + self.handle.before )
1038 return None
1039 except pexpect.EOF:
1040 main.log.error( self.name + ": EOF exception found" )
1041 main.log.error( self.name + ": " + self.handle.before )
1042 main.cleanAndExit()
1043 except Exception:
1044 main.log.exception( self.name + ": Uncaught exception" )
1045 main.cleanAndExit()
1046
1047 def discoverHosts( self, hostList=[], wait=1000, dstIp="6.6.6.6", dstIp6="1020::3fe" ):
1048 '''
1049 Hosts in hostList will do a single ARP/ND to a non-existent address for ONOS to
1050 discover them. A host will use arping/ndisc6 to send ARP/ND depending on if it
1051 has IPv4/IPv6 addresses configured.
1052 Optional:
1053 hostList: a list of names of the hosts that need to be discovered. If not
1054 specified mininet will send ping from all the hosts
1055 wait: timeout for ARP/ND in milliseconds
1056 dstIp: destination address used by IPv4 hosts
1057 dstIp6: destination address used by IPv6 hosts
1058 Returns:
1059 main.TRUE if all packets were successfully sent. Otherwise main.FALSE
1060 '''
1061 try:
1062 hosts = self.getHosts()
1063 if not hostList:
1064 hostList = hosts.keys()
1065 discoveryResult = main.TRUE
1066 for host in hostList:
1067 flushCmd = ""
1068 cmd = ""
Jon Hall627b1572020-12-01 12:01:15 -08001069 intf = hosts[host]['interfaces'][0].get( 'name' )
1070 hostIp = self.getIPAddress( host, iface=intf ) or hosts[host]['interfaces'][0].get( 'ips', False )
1071 if hostIp:
You Wangb1665b52019-02-01 15:49:48 -08001072 flushCmd = "sudo ip neigh flush all"
Jon Hall43060f62020-06-23 13:13:33 -07001073 intfStr = "-i {}".format( intf ) if intf else ""
Jon Hall627b1572020-12-01 12:01:15 -08001074 srcIp = "-S {}".format( hostIp if isinstance( hostIp, types.StringType ) else hostIp[0] ) if intf else ""
1075 cmd = "sudo arping -c 1 -w {} {} {} {}".format( wait, intfStr, srcIp, dstIp )
You Wangb1665b52019-02-01 15:49:48 -08001076 main.log.debug( "Sending IPv4 arping from host {}".format( host ) )
1077 elif self.getIPAddress( host, proto='IPV6' ):
1078 flushCmd = "sudo ip -6 neigh flush all"
1079 intf = hosts[host]['interfaces'][0]['name']
1080 cmd = "ndisc6 -r 1 -w {} {} {}".format( wait, dstIp6, intf )
1081 main.log.debug( "Sending IPv6 ND from host {}".format( host ) )
1082 else:
1083 main.log.warn( "No IP addresses configured on host {}, skipping discovery".format( host ) )
1084 discoveryResult = main.FALSE
1085 if cmd:
1086 self.runCmdOnHost( host, flushCmd )
1087 self.runCmdOnHost( host, cmd )
1088 return discoveryResult
1089 except Exception:
1090 main.log.exception( self.name + ": Uncaught exception!" )
1091 main.cleanAndExit()