You Wang | 84f981d | 2018-01-12 16:11:50 -0800 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | """ |
| 3 | Copyright 2018 Open Networking Foundation (ONF) |
| 4 | |
| 5 | Please refer questions to either the onos test mailing list at <onos-test@onosproject.org>, |
| 6 | the System Testing Plans and Results wiki page at <https://wiki.onosproject.org/x/voMg>, |
| 7 | or 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 | |
| 23 | This driver is used to interact with a physical network that SDN controller is controlling. |
| 24 | |
| 25 | Please refer questions to either the onos test mailing list at <onos-test@onosproject.org>, |
| 26 | the System Testing Plans and Results wiki page at <https://wiki.onosproject.org/x/voMg>, |
| 27 | or the System Testing Guide page at <https://wiki.onosproject.org/x/WYQg> |
| 28 | |
| 29 | """ |
| 30 | import pexpect |
| 31 | import os |
| 32 | import types |
| 33 | from drivers.common.clidriver import CLI |
| 34 | |
| 35 | class NetworkDriver( CLI ): |
| 36 | |
| 37 | def __init__( self ): |
| 38 | """ |
| 39 | switches: a dictionary that maps switch names to components |
| 40 | hosts: a dictionary that maps host names to components |
| 41 | """ |
| 42 | self.name = None |
| 43 | self.home = None |
| 44 | self.handle = None |
| 45 | self.switches = {} |
| 46 | self.hosts = {} |
| 47 | super( NetworkDriver, self ).__init__() |
| 48 | |
| 49 | def checkOptions( self, var, defaultVar ): |
| 50 | if var is None or var == "": |
| 51 | return defaultVar |
| 52 | return var |
| 53 | |
| 54 | def connect( self, **connectargs ): |
| 55 | """ |
| 56 | Creates ssh handle for the SDN network "bench". |
| 57 | NOTE: |
| 58 | The ip_address would come from the topo file using the host tag, the |
| 59 | value can be an environment variable as well as a "localhost" to get |
| 60 | the ip address needed to ssh to the "bench" |
| 61 | """ |
| 62 | try: |
| 63 | for key in connectargs: |
| 64 | vars( self )[ key ] = connectargs[ key ] |
| 65 | self.name = self.options[ 'name' ] |
| 66 | try: |
| 67 | if os.getenv( str( self.ip_address ) ) is not None: |
| 68 | self.ip_address = os.getenv( str( self.ip_address ) ) |
| 69 | else: |
| 70 | main.log.info( self.name + |
| 71 | ": Trying to connect to " + |
| 72 | self.ip_address ) |
| 73 | except KeyError: |
| 74 | main.log.info( "Invalid host name," + |
| 75 | " connecting to local host instead" ) |
| 76 | self.ip_address = 'localhost' |
| 77 | except Exception as inst: |
| 78 | main.log.error( "Uncaught exception: " + str( inst ) ) |
| 79 | |
| 80 | self.handle = super( NetworkDriver, self ).connect( |
| 81 | user_name=self.user_name, |
| 82 | ip_address=self.ip_address, |
| 83 | port=self.port, |
| 84 | pwd=self.pwd ) |
| 85 | |
| 86 | if self.handle: |
| 87 | main.log.info( "Connected to network bench node" ) |
| 88 | return self.handle |
| 89 | else: |
| 90 | main.log.info( "Failed to create handle" ) |
| 91 | return main.FALSE |
| 92 | except pexpect.EOF: |
| 93 | main.log.error( self.name + ": EOF exception found" ) |
| 94 | main.log.error( self.name + ": " + self.handle.before ) |
| 95 | main.cleanAndExit() |
| 96 | except Exception: |
| 97 | main.log.exception( self.name + ": Uncaught exception!" ) |
| 98 | main.cleanAndExit() |
| 99 | |
| 100 | def disconnect( self ): |
| 101 | """ |
| 102 | Called when test is complete to disconnect the handle. |
| 103 | """ |
| 104 | response = main.TRUE |
| 105 | try: |
| 106 | if self.handle: |
| 107 | self.handle.sendline( "exit" ) |
| 108 | self.handle.expect( "closed" ) |
| 109 | except pexpect.EOF: |
| 110 | main.log.error( self.name + ": EOF exception found" ) |
| 111 | main.log.error( self.name + ": " + self.handle.before ) |
| 112 | except Exception: |
| 113 | main.log.exception( self.name + ": Connection failed to the host" ) |
| 114 | response = main.FALSE |
| 115 | return response |
| 116 | |
| 117 | def connectToNet( self ): |
| 118 | """ |
| 119 | Connect to an existing physical network by getting information |
| 120 | of all switch and host components created |
| 121 | """ |
| 122 | try: |
| 123 | for key, value in main.componentDictionary.items(): |
| 124 | if hasattr( main, key ): |
Pier | 6a0c4de | 2018-03-18 16:01:30 -0700 | [diff] [blame] | 125 | if value[ 'type' ] in [ 'MininetSwitchDriver', 'OFDPASwitchDriver' ]: |
You Wang | 84f981d | 2018-01-12 16:11:50 -0800 | [diff] [blame] | 126 | self.switches[ key ] = getattr( main, key ) |
Pier | 6a0c4de | 2018-03-18 16:01:30 -0700 | [diff] [blame] | 127 | elif value[ 'type' ] in [ 'MininetHostDriver', 'HostDriver' ]: |
You Wang | 84f981d | 2018-01-12 16:11:50 -0800 | [diff] [blame] | 128 | self.hosts[ key ] = getattr( main, key ) |
| 129 | return main.TRUE |
| 130 | except Exception: |
| 131 | main.log.error( self.name + ": failed to connect to network" ) |
| 132 | return main.FALSE |
| 133 | |
| 134 | def getHosts( self, verbose=False, updateTimeout=1000 ): |
| 135 | """ |
| 136 | Return a dictionary which maps short names to host data |
| 137 | """ |
| 138 | hosts = {} |
| 139 | try: |
| 140 | for hostComponent in self.hosts.values(): |
Pier | 6a0c4de | 2018-03-18 16:01:30 -0700 | [diff] [blame] | 141 | # TODO: return more host data |
You Wang | 84f981d | 2018-01-12 16:11:50 -0800 | [diff] [blame] | 142 | hosts[ hostComponent.options[ 'shortName' ] ] = {} |
| 143 | except Exception: |
| 144 | main.log.error( self.name + ": host component not as expected" ) |
| 145 | return hosts |
| 146 | |
| 147 | def getMacAddress( self, host ): |
| 148 | """ |
| 149 | Return MAC address of a host |
| 150 | """ |
| 151 | import re |
| 152 | try: |
| 153 | hostComponent = self.getHostComponentByShortName( host ) |
| 154 | response = hostComponent.ifconfig() |
| 155 | pattern = r'HWaddr\s([0-9A-F]{2}[:-]){5}([0-9A-F]{2})' |
| 156 | macAddressSearch = re.search( pattern, response, re.I ) |
| 157 | macAddress = macAddressSearch.group().split( " " )[ 1 ] |
| 158 | main.log.info( self.name + ": Mac-Address of Host " + host + " is " + macAddress ) |
| 159 | return macAddress |
| 160 | except Exception: |
| 161 | main.log.error( self.name + ": failed to get host MAC address" ) |
| 162 | |
| 163 | def getSwitchComponentByShortName( self, shortName ): |
| 164 | """ |
| 165 | Get switch component by its short name i.e. "s1" |
| 166 | """ |
| 167 | for switchComponent in self.switches.values(): |
| 168 | if switchComponent.options[ 'shortName' ] == shortName: |
| 169 | return switchComponent |
| 170 | main.log.warn( self.name + ": failed to find switch component by name " + shortName ) |
| 171 | return None |
| 172 | |
| 173 | def getHostComponentByShortName( self, shortName ): |
| 174 | """ |
| 175 | Get host component by its short name i.e. "h1" |
| 176 | """ |
| 177 | for hostComponent in self.hosts.values(): |
| 178 | if hostComponent.options[ 'shortName' ] == shortName: |
| 179 | return hostComponent |
| 180 | main.log.warn( self.name + ": failed to find host component by name " + shortName ) |
| 181 | return None |
| 182 | |
| 183 | def assignSwController( self, sw, ip, port="6653", ptcp="" ): |
| 184 | """ |
| 185 | Description: |
| 186 | Assign switches to the controllers |
| 187 | Required: |
| 188 | sw - Short name of the switch specified in the .topo file, e.g. "s1". |
| 189 | It can also be a list of switch names. |
| 190 | ip - Ip addresses of controllers. This can be a list or a string. |
| 191 | Optional: |
| 192 | port - ONOS use port 6653, if no list of ports is passed, then |
| 193 | the all the controller will use 6653 as their port number |
| 194 | ptcp - ptcp number, This can be a string or a list that has |
| 195 | the same length as switch. This is optional and not required |
| 196 | when using ovs switches. |
| 197 | NOTE: If switches and ptcp are given in a list type they should have the |
| 198 | same length and should be in the same order, Eg. sw=[ 's1' ... n ] |
| 199 | ptcp=[ '6637' ... n ], s1 has ptcp number 6637 and so on. |
| 200 | |
| 201 | Return: |
| 202 | Returns main.TRUE if switches are correctly assigned to controllers, |
| 203 | otherwise it will return main.FALSE or an appropriate exception(s) |
| 204 | """ |
| 205 | switchList = [] |
| 206 | ptcpList = None |
| 207 | try: |
| 208 | if isinstance( sw, types.StringType ): |
| 209 | switchList.append( sw ) |
| 210 | if ptcp: |
| 211 | if isinstance( ptcp, types.StringType ): |
| 212 | ptcpList = [ ptcp ] |
| 213 | elif isinstance( ptcp, types.ListType ): |
| 214 | main.log.error( self.name + ": Only one switch is " + |
| 215 | "being set and multiple PTCP is " + |
| 216 | "being passed " ) |
| 217 | return main.FALSE |
| 218 | else: |
| 219 | main.log.error( self.name + ": Invalid PTCP" ) |
| 220 | return main.FALSE |
| 221 | |
| 222 | elif isinstance( sw, types.ListType ): |
| 223 | switchList = sw |
| 224 | if ptcp: |
| 225 | if isinstance( ptcp, types.ListType ): |
| 226 | if len( ptcp ) != len( sw ): |
| 227 | main.log.error( self.name + ": PTCP length = " + |
| 228 | str( len( ptcp ) ) + |
| 229 | " is not the same as switch" + |
| 230 | " length = " + |
| 231 | str( len( sw ) ) ) |
| 232 | return main.FALSE |
| 233 | else: |
| 234 | ptcpList = ptcp |
| 235 | else: |
| 236 | main.log.error( self.name + ": Invalid PTCP" ) |
| 237 | return main.FALSE |
| 238 | else: |
| 239 | main.log.error( self.name + ": Invalid switch type " ) |
| 240 | return main.FALSE |
| 241 | |
| 242 | assignResult = main.TRUE |
| 243 | index = 0 |
| 244 | for switch in switchList: |
| 245 | assigned = False |
| 246 | switchComponent = self.getSwitchComponentByShortName( switch ) |
| 247 | if switchComponent: |
| 248 | ptcp = ptcpList[ index ] if ptcpList else "" |
| 249 | assignResult = assignResult and switchComponent.assignSwController( ip=ip, port=port, ptcp=ptcp ) |
| 250 | assigned = True |
| 251 | if not assigned: |
| 252 | main.log.error( self.name + ": Not able to find switch " + switch ) |
| 253 | assignResult = main.FALSE |
| 254 | index += 1 |
| 255 | return assignResult |
| 256 | |
| 257 | except Exception: |
| 258 | main.log.exception( self.name + ": Uncaught exception!" ) |
| 259 | main.cleanAndExit() |
| 260 | |
| 261 | def pingall( self, protocol="IPv4", timeout=300, shortCircuit=False, acceptableFailed=0 ): |
| 262 | """ |
| 263 | Description: |
| 264 | Verifies the reachability of the hosts using ping command. |
| 265 | Optional: |
| 266 | protocol - use ping6 command if specified as "IPv6" |
| 267 | timeout( seconds ) - How long to wait before breaking the pingall |
| 268 | shortCircuit - Break the pingall based on the number of failed hosts ping |
| 269 | acceptableFailed - Set the number of acceptable failed pings for the |
| 270 | function to still return main.TRUE |
| 271 | Returns: |
| 272 | main.TRUE if pingall completes with no pings dropped |
| 273 | otherwise main.FALSE |
| 274 | """ |
| 275 | import time |
| 276 | import itertools |
| 277 | try: |
| 278 | timeout = int( timeout ) |
| 279 | main.log.info( self.name + ": Checking reachabilty to the hosts using ping" ) |
| 280 | failedPings = 0 |
| 281 | returnValue = main.TRUE |
| 282 | ipv6 = True if protocol == "IPv6" else False |
| 283 | startTime = time.time() |
| 284 | hostPairs = itertools.permutations( list( self.hosts.values() ), 2 ) |
| 285 | for hostPair in list( hostPairs ): |
| 286 | ipDst = hostPair[ 1 ].options[ 'ip6' ] if ipv6 else hostPair[ 1 ].options[ 'ip' ] |
| 287 | pingResult = hostPair[ 0 ].ping( ipDst, ipv6=ipv6 ) |
| 288 | returnValue = returnValue and pingResult |
| 289 | if ( time.time() - startTime ) > timeout: |
| 290 | returnValue = main.FALSE |
| 291 | main.log.error( self.name + |
| 292 | ": Aborting pingall - " + |
| 293 | "Function took too long " ) |
| 294 | break |
| 295 | if not pingResult: |
| 296 | failedPings = failedPings + 1 |
| 297 | if failedPings > acceptableFailed: |
| 298 | returnValue = main.FALSE |
| 299 | if shortCircuit: |
| 300 | main.log.error( self.name + |
| 301 | ": Aborting pingall - " |
| 302 | + str( failedPings ) + |
| 303 | " pings failed" ) |
| 304 | break |
| 305 | return returnValue |
| 306 | except Exception: |
| 307 | main.log.exception( self.name + ": Uncaught exception!" ) |
| 308 | main.cleanAndExit() |
| 309 | |
| 310 | def pingallHosts( self, hostList, wait=1 ): |
| 311 | """ |
| 312 | Ping all specified IPv4 hosts |
| 313 | |
| 314 | Acceptable hostList: |
| 315 | - [ 'h1','h2','h3','h4' ] |
| 316 | |
| 317 | Returns main.TRUE if all hosts specified can reach |
| 318 | each other |
| 319 | |
| 320 | Returns main.FALSE if one or more of hosts specified |
| 321 | cannot reach each other""" |
| 322 | import time |
| 323 | import itertools |
| 324 | hostComponentList = [] |
| 325 | for hostName in hostList: |
| 326 | hostComponent = self.getHostComponentByShortName( hostName ) |
| 327 | if hostComponent: |
| 328 | hostComponentList.append( hostComponent ) |
| 329 | try: |
| 330 | main.log.info( "Testing reachability between specified hosts" ) |
| 331 | isReachable = main.TRUE |
| 332 | pingResponse = "IPv4 ping across specified hosts\n" |
| 333 | failedPings = 0 |
| 334 | hostPairs = itertools.permutations( list( hostComponentList ), 2 ) |
| 335 | for hostPair in list( hostPairs ): |
| 336 | pingResponse += hostPair[ 0 ].options[ 'shortName' ] + " -> " |
| 337 | ipDst = hostPair[ 1 ].options[ 'ip6' ] if ipv6 else hostPair[ 1 ].options[ 'ip' ] |
| 338 | pingResult = hostPair[ 0 ].ping( ipDst, wait=int( wait ) ) |
| 339 | if pingResult: |
| 340 | pingResponse += hostPair[ 1 ].options[ 'shortName' ] |
| 341 | else: |
| 342 | pingResponse += "X" |
| 343 | # One of the host to host pair is unreachable |
| 344 | isReachable = main.FALSE |
| 345 | failedPings += 1 |
| 346 | pingResponse += "\n" |
| 347 | main.log.info( pingResponse + "Failed pings: " + str( failedPings ) ) |
| 348 | return isReachable |
| 349 | except Exception: |
| 350 | main.log.exception( self.name + ": Uncaught exception!" ) |
| 351 | main.cleanAndExit() |
| 352 | |
| 353 | def iperftcp( self, host1="h1", host2="h2", timeout=6 ): |
| 354 | ''' |
| 355 | Creates an iperf TCP test between two hosts. Returns main.TRUE if test results |
| 356 | are valid. |
| 357 | Optional: |
| 358 | timeout: The defualt timeout is 6 sec to allow enough time for a successful test to complete, |
| 359 | and short enough to stop an unsuccessful test from quiting and cleaning up mininet. |
| 360 | ''' |
| 361 | main.log.info( self.name + ": Simple iperf TCP test between two hosts" ) |
| 362 | # TODO: complete this function |
| 363 | return main.TRUE |