1#!/usr/bin/env python3 2# 3# Copyright 2016 - Google, Inc. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17import collections 18import ipaddress 19import os 20import time 21 22from acts import logger 23from acts import utils 24 25from acts.controllers import pdu 26from acts.controllers.ap_lib import ap_get_interface 27from acts.controllers.ap_lib import ap_iwconfig 28from acts.controllers.ap_lib import bridge_interface 29from acts.controllers.ap_lib import dhcp_config 30from acts.controllers.ap_lib import dhcp_server 31from acts.controllers.ap_lib import hostapd 32from acts.controllers.ap_lib import hostapd_ap_preset 33from acts.controllers.ap_lib import hostapd_constants 34from acts.controllers.ap_lib import hostapd_config 35from acts.controllers.utils_lib.commands import ip 36from acts.controllers.utils_lib.commands import route 37from acts.controllers.utils_lib.commands import shell 38from acts.controllers.utils_lib.ssh import connection 39from acts.controllers.utils_lib.ssh import settings 40from acts.libs.proc import job 41 42MOBLY_CONTROLLER_CONFIG_NAME = 'AccessPoint' 43ACTS_CONTROLLER_REFERENCE_NAME = 'access_points' 44_BRCTL = 'brctl' 45 46LIFETIME = 180 47PROC_NET_SNMP6 = '/proc/net/snmp6' 48SCAPY_INSTALL_COMMAND = 'sudo python setup.py install' 49RA_MULTICAST_ADDR = '33:33:00:00:00:01' 50RA_SCRIPT = 'sendra.py' 51 52 53def create(configs): 54 """Creates ap controllers from a json config. 55 56 Creates an ap controller from either a list, or a single 57 element. The element can either be just the hostname or a dictionary 58 containing the hostname and username of the ap to connect to over ssh. 59 60 Args: 61 The json configs that represent this controller. 62 63 Returns: 64 A new AccessPoint. 65 """ 66 return [AccessPoint(c) for c in configs] 67 68 69def destroy(aps): 70 """Destroys a list of access points. 71 72 Args: 73 aps: The list of access points to destroy. 74 """ 75 for ap in aps: 76 ap.close() 77 78 79def get_info(aps): 80 """Get information on a list of access points. 81 82 Args: 83 aps: A list of AccessPoints. 84 85 Returns: 86 A list of all aps hostname. 87 """ 88 return [ap.ssh_settings.hostname for ap in aps] 89 90 91def setup_ap(access_point, 92 profile_name, 93 channel, 94 ssid, 95 mode=None, 96 preamble=None, 97 beacon_interval=None, 98 dtim_period=None, 99 frag_threshold=None, 100 rts_threshold=None, 101 force_wmm=None, 102 hidden=False, 103 security=None, 104 pmf_support=None, 105 additional_ap_parameters=None, 106 password=None, 107 n_capabilities=None, 108 ac_capabilities=None, 109 vht_bandwidth=None, 110 setup_bridge=False): 111 """Creates a hostapd profile and runs it on an ap. This is a convenience 112 function that allows us to start an ap with a single function, without first 113 creating a hostapd config. 114 115 Args: 116 access_point: An ACTS access_point controller 117 profile_name: The profile name of one of the hostapd ap presets. 118 channel: What channel to set the AP to. 119 preamble: Whether to set short or long preamble (True or False) 120 beacon_interval: The beacon interval (int) 121 dtim_period: Length of dtim period (int) 122 frag_threshold: Fragmentation threshold (int) 123 rts_threshold: RTS threshold (int) 124 force_wmm: Enable WMM or not (True or False) 125 hidden: Advertise the SSID or not (True or False) 126 security: What security to enable. 127 pmf_support: int, whether pmf is not disabled, enabled, or required 128 additional_ap_parameters: Additional parameters to send the AP. 129 password: Password to connect to WLAN if necessary. 130 check_connectivity: Whether to check for internet connectivity. 131 """ 132 ap = hostapd_ap_preset.create_ap_preset(profile_name=profile_name, 133 iface_wlan_2g=access_point.wlan_2g, 134 iface_wlan_5g=access_point.wlan_5g, 135 channel=channel, 136 ssid=ssid, 137 mode=mode, 138 short_preamble=preamble, 139 beacon_interval=beacon_interval, 140 dtim_period=dtim_period, 141 frag_threshold=frag_threshold, 142 rts_threshold=rts_threshold, 143 force_wmm=force_wmm, 144 hidden=hidden, 145 bss_settings=[], 146 security=security, 147 pmf_support=pmf_support, 148 n_capabilities=n_capabilities, 149 ac_capabilities=ac_capabilities, 150 vht_bandwidth=vht_bandwidth) 151 access_point.start_ap(hostapd_config=ap, 152 setup_bridge=setup_bridge, 153 additional_parameters=additional_ap_parameters) 154 155 156class Error(Exception): 157 """Error raised when there is a problem with the access point.""" 158 159 160_ApInstance = collections.namedtuple('_ApInstance', ['hostapd', 'subnet']) 161 162# These ranges were split this way since each physical radio can have up 163# to 8 SSIDs so for the 2GHz radio the DHCP range will be 164# 192.168.1 - 8 and the 5Ghz radio will be 192.168.9 - 16 165_AP_2GHZ_SUBNET_STR_DEFAULT = '192.168.1.0/24' 166_AP_5GHZ_SUBNET_STR_DEFAULT = '192.168.9.0/24' 167 168# The last digit of the ip for the bridge interface 169BRIDGE_IP_LAST = '100' 170 171 172class AccessPoint(object): 173 """An access point controller. 174 175 Attributes: 176 ssh: The ssh connection to this ap. 177 ssh_settings: The ssh settings being used by the ssh connection. 178 dhcp_settings: The dhcp server settings being used. 179 """ 180 def __init__(self, configs): 181 """ 182 Args: 183 configs: configs for the access point from config file. 184 """ 185 self.ssh_settings = settings.from_config(configs['ssh_config']) 186 self.log = logger.create_logger(lambda msg: '[Access Point|%s] %s' % 187 (self.ssh_settings.hostname, msg)) 188 self.device_pdu_config = configs.get('PduDevice', None) 189 self.identifier = self.ssh_settings.hostname 190 191 if 'ap_subnet' in configs: 192 self._AP_2G_SUBNET_STR = configs['ap_subnet']['2g'] 193 self._AP_5G_SUBNET_STR = configs['ap_subnet']['5g'] 194 else: 195 self._AP_2G_SUBNET_STR = _AP_2GHZ_SUBNET_STR_DEFAULT 196 self._AP_5G_SUBNET_STR = _AP_5GHZ_SUBNET_STR_DEFAULT 197 198 self._AP_2G_SUBNET = dhcp_config.Subnet( 199 ipaddress.ip_network(self._AP_2G_SUBNET_STR)) 200 self._AP_5G_SUBNET = dhcp_config.Subnet( 201 ipaddress.ip_network(self._AP_5G_SUBNET_STR)) 202 203 self.ssh = connection.SshConnection(self.ssh_settings) 204 205 # Singleton utilities for running various commands. 206 self._ip_cmd = ip.LinuxIpCommand(self.ssh) 207 self._route_cmd = route.LinuxRouteCommand(self.ssh) 208 209 # A map from network interface name to _ApInstance objects representing 210 # the hostapd instance running against the interface. 211 self._aps = dict() 212 self._dhcp = None 213 self._dhcp_bss = dict() 214 self.bridge = bridge_interface.BridgeInterface(self) 215 self.interfaces = ap_get_interface.ApInterfaces(self) 216 self.iwconfig = ap_iwconfig.ApIwconfig(self) 217 218 # Get needed interface names and initialize the unneccessary ones. 219 self.wan = self.interfaces.get_wan_interface() 220 self.wlan = self.interfaces.get_wlan_interface() 221 self.wlan_2g = self.wlan[0] 222 self.wlan_5g = self.wlan[1] 223 self.lan = self.interfaces.get_lan_interface() 224 self._initial_ap() 225 self.scapy_install_path = None 226 self.setup_bridge = False 227 228 def _initial_ap(self): 229 """Initial AP interfaces. 230 231 Bring down hostapd if instance is running, bring down all bridge 232 interfaces. 233 """ 234 # This is necessary for Gale/Whirlwind flashed with dev channel image 235 # Unused interfaces such as existing hostapd daemon, guest, mesh 236 # interfaces need to be brought down as part of the AP initialization 237 # process, otherwise test would fail. 238 try: 239 self.ssh.run('stop wpasupplicant') 240 except job.Error: 241 self.log.info('No wpasupplicant running') 242 try: 243 self.ssh.run('stop hostapd') 244 except job.Error: 245 self.log.info('No hostapd running') 246 # Bring down all wireless interfaces 247 for iface in self.wlan: 248 WLAN_DOWN = 'ifconfig {} down'.format(iface) 249 self.ssh.run(WLAN_DOWN) 250 # Bring down all bridge interfaces 251 bridge_interfaces = self.interfaces.get_bridge_interface() 252 if bridge_interfaces: 253 for iface in bridge_interfaces: 254 BRIDGE_DOWN = 'ifconfig {} down'.format(iface) 255 BRIDGE_DEL = 'brctl delbr {}'.format(iface) 256 self.ssh.run(BRIDGE_DOWN) 257 self.ssh.run(BRIDGE_DEL) 258 259 def start_ap(self, 260 hostapd_config, 261 setup_bridge=False, 262 additional_parameters=None): 263 """Starts as an ap using a set of configurations. 264 265 This will start an ap on this host. To start an ap the controller 266 selects a network interface to use based on the configs given. It then 267 will start up hostapd on that interface. Next a subnet is created for 268 the network interface and dhcp server is refreshed to give out ips 269 for that subnet for any device that connects through that interface. 270 271 Args: 272 hostapd_config: hostapd_config.HostapdConfig, The configurations 273 to use when starting up the ap. 274 setup_bridge: Whether to bridge the LAN interface WLAN interface. 275 Only one WLAN interface can be bridged with the LAN interface 276 and none of the guest networks can be bridged. 277 additional_parameters: A dictionary of parameters that can sent 278 directly into the hostapd config file. This can be used for 279 debugging and or adding one off parameters into the config. 280 281 Returns: 282 An identifier for each ssid being started. These identifiers can be 283 used later by this controller to control the ap. 284 285 Raises: 286 Error: When the ap can't be brought up. 287 """ 288 if hostapd_config.frequency < 5000: 289 interface = self.wlan_2g 290 subnet = self._AP_2G_SUBNET 291 else: 292 interface = self.wlan_5g 293 subnet = self._AP_5G_SUBNET 294 295 # In order to handle dhcp servers on any interface, the initiation of 296 # the dhcp server must be done after the wlan interfaces are figured 297 # out as opposed to being in __init__ 298 self._dhcp = dhcp_server.DhcpServer(self.ssh, interface=interface) 299 300 # For multi bssid configurations the mac address 301 # of the wireless interface needs to have enough space to mask out 302 # up to 8 different mac addresses. So in for one interface the range is 303 # hex 0-7 and for the other the range is hex 8-f. 304 interface_mac_orig = None 305 cmd = "ifconfig %s|grep ether|awk -F' ' '{print $2}'" % interface 306 interface_mac_orig = self.ssh.run(cmd) 307 if interface == self.wlan_5g: 308 hostapd_config.bssid = interface_mac_orig.stdout[:-1] + '0' 309 last_octet = 1 310 if interface == self.wlan_2g: 311 hostapd_config.bssid = interface_mac_orig.stdout[:-1] + '8' 312 last_octet = 9 313 if interface in self._aps: 314 raise ValueError('No WiFi interface available for AP on ' 315 'channel %d' % hostapd_config.channel) 316 317 apd = hostapd.Hostapd(self.ssh, interface) 318 new_instance = _ApInstance(hostapd=apd, subnet=subnet) 319 self._aps[interface] = new_instance 320 321 # Turn off the DHCP server, we're going to change its settings. 322 self.stop_dhcp() 323 # Clear all routes to prevent old routes from interfering. 324 self._route_cmd.clear_routes(net_interface=interface) 325 326 if hostapd_config.bss_lookup: 327 # The self._dhcp_bss dictionary is created to hold the key/value 328 # pair of the interface name and the ip scope that will be 329 # used for the particular interface. The a, b, c, d 330 # variables below are the octets for the ip address. The 331 # third octet is then incremented for each interface that 332 # is requested. This part is designed to bring up the 333 # hostapd interfaces and not the DHCP servers for each 334 # interface. 335 self._dhcp_bss = dict() 336 counter = 1 337 for bss in hostapd_config.bss_lookup: 338 if interface_mac_orig: 339 hostapd_config.bss_lookup[bss].bssid = ( 340 interface_mac_orig.stdout[:-1] + hex(last_octet)[-1:]) 341 self._route_cmd.clear_routes(net_interface=str(bss)) 342 if interface is self.wlan_2g: 343 starting_ip_range = self._AP_2G_SUBNET_STR 344 else: 345 starting_ip_range = self._AP_5G_SUBNET_STR 346 a, b, c, d = starting_ip_range.split('.') 347 self._dhcp_bss[bss] = dhcp_config.Subnet( 348 ipaddress.ip_network('%s.%s.%s.%s' % 349 (a, b, str(int(c) + counter), d))) 350 counter = counter + 1 351 last_octet = last_octet + 1 352 353 apd.start(hostapd_config, additional_parameters=additional_parameters) 354 355 # The DHCP serer requires interfaces to have ips and routes before 356 # the server will come up. 357 interface_ip = ipaddress.ip_interface( 358 '%s/%s' % (subnet.router, subnet.network.netmask)) 359 if setup_bridge is True: 360 bridge_interface_name = 'br_lan' 361 self.create_bridge(bridge_interface_name, [interface, self.lan]) 362 self._ip_cmd.set_ipv4_address(bridge_interface_name, interface_ip) 363 else: 364 self._ip_cmd.set_ipv4_address(interface, interface_ip) 365 if hostapd_config.bss_lookup: 366 # This loop goes through each interface that was setup for 367 # hostapd and assigns the DHCP scopes that were defined but 368 # not used during the hostapd loop above. The k and v 369 # variables represent the interface name, k, and dhcp info, v. 370 for k, v in self._dhcp_bss.items(): 371 bss_interface_ip = ipaddress.ip_interface( 372 '%s/%s' % (self._dhcp_bss[k].router, 373 self._dhcp_bss[k].network.netmask)) 374 self._ip_cmd.set_ipv4_address(str(k), bss_interface_ip) 375 376 # Restart the DHCP server with our updated list of subnets. 377 configured_subnets = [x.subnet for x in self._aps.values()] 378 if hostapd_config.bss_lookup: 379 for k, v in self._dhcp_bss.items(): 380 configured_subnets.append(v) 381 382 self.start_dhcp(subnets=configured_subnets) 383 self.start_nat() 384 385 bss_interfaces = [bss for bss in hostapd_config.bss_lookup] 386 bss_interfaces.append(interface) 387 388 return bss_interfaces 389 390 def start_dhcp(self, subnets): 391 """Start a DHCP server for the specified subnets. 392 393 This allows consumers of the access point objects to control DHCP. 394 395 Args: 396 subnets: A list of Subnets. 397 """ 398 return self._dhcp.start(config=dhcp_config.DhcpConfig(subnets)) 399 400 def stop_dhcp(self): 401 """Stop DHCP for this AP object. 402 403 This allows consumers of the access point objects to control DHCP. 404 """ 405 return self._dhcp.stop() 406 407 def start_nat(self): 408 """Start NAT on the AP. 409 410 This allows consumers of the access point objects to enable NAT 411 on the AP. 412 413 Note that this is currently a global setting, since we don't 414 have per-interface masquerade rules. 415 """ 416 # The following three commands are needed to enable NAT between 417 # the WAN and LAN/WLAN ports. This means anyone connecting to the 418 # WLAN/LAN ports will be able to access the internet if the WAN port 419 # is connected to the internet. 420 self.ssh.run('iptables -t nat -F') 421 self.ssh.run('iptables -t nat -A POSTROUTING -o %s -j MASQUERADE' % 422 self.wan) 423 self.ssh.run('echo 1 > /proc/sys/net/ipv4/ip_forward') 424 self.ssh.run('echo 1 > /proc/sys/net/ipv6/conf/all/forwarding') 425 426 def stop_nat(self): 427 """Stop NAT on the AP. 428 429 This allows consumers of the access point objects to disable NAT on the 430 AP. 431 432 Note that this is currently a global setting, since we don't have 433 per-interface masquerade rules. 434 """ 435 self.ssh.run('iptables -t nat -F') 436 self.ssh.run('echo 0 > /proc/sys/net/ipv4/ip_forward') 437 self.ssh.run('echo 0 > /proc/sys/net/ipv6/conf/all/forwarding') 438 439 def create_bridge(self, bridge_name, interfaces): 440 """Create the specified bridge and bridge the specified interfaces. 441 442 Args: 443 bridge_name: The name of the bridge to create. 444 interfaces: A list of interfaces to add to the bridge. 445 """ 446 447 # Create the bridge interface 448 self.ssh.run( 449 'brctl addbr {bridge_name}'.format(bridge_name=bridge_name)) 450 451 for interface in interfaces: 452 self.ssh.run('brctl addif {bridge_name} {interface}'.format( 453 bridge_name=bridge_name, interface=interface)) 454 455 def remove_bridge(self, bridge_name): 456 """Removes the specified bridge 457 458 Args: 459 bridge_name: The name of the bridge to remove. 460 """ 461 # Check if the bridge exists. 462 # 463 # Cases where it may not are if we failed to initialize properly 464 # 465 # Or if we're doing 2.4Ghz and 5Ghz SSIDs and we've already torn 466 # down the bridge once, but we got called for each band. 467 result = self.ssh.run( 468 'brctl show {bridge_name}'.format(bridge_name=bridge_name), 469 ignore_status=True) 470 471 # If the bridge exists, we'll get an exit_status of 0, indicating 472 # success, so we can continue and remove the bridge. 473 if result.exit_status == 0: 474 self.ssh.run('ip link set {bridge_name} down'.format( 475 bridge_name=bridge_name)) 476 self.ssh.run( 477 'brctl delbr {bridge_name}'.format(bridge_name=bridge_name)) 478 479 def get_bssid_from_ssid(self, ssid, band): 480 """Gets the BSSID from a provided SSID 481 482 Args: 483 ssid: An SSID string. 484 band: 2G or 5G Wifi band. 485 Returns: The BSSID if on the AP or None if SSID could not be found. 486 """ 487 if band == hostapd_constants.BAND_2G: 488 interfaces = [self.wlan_2g, ssid] 489 else: 490 interfaces = [self.wlan_5g, ssid] 491 492 # Get the interface name associated with the given ssid. 493 for interface in interfaces: 494 cmd = "iw dev %s info|grep ssid|awk -F' ' '{print $2}'" % ( 495 str(interface)) 496 iw_output = self.ssh.run(cmd) 497 if 'command failed: No such device' in iw_output.stderr: 498 continue 499 else: 500 # If the configured ssid is equal to the given ssid, we found 501 # the right interface. 502 if iw_output.stdout == ssid: 503 cmd = "iw dev %s info|grep addr|awk -F' ' '{print $2}'" % ( 504 str(interface)) 505 iw_output = self.ssh.run(cmd) 506 return iw_output.stdout 507 return None 508 509 def stop_ap(self, identifier): 510 """Stops a running ap on this controller. 511 512 Args: 513 identifier: The identify of the ap that should be taken down. 514 """ 515 516 if identifier not in list(self._aps.keys()): 517 raise ValueError('Invalid identifier %s given' % identifier) 518 519 instance = self._aps.get(identifier) 520 521 instance.hostapd.stop() 522 try: 523 self.stop_dhcp() 524 except dhcp_server.NoInterfaceError: 525 pass 526 self._ip_cmd.clear_ipv4_addresses(identifier) 527 528 del self._aps[identifier] 529 bridge_interfaces = self.interfaces.get_bridge_interface() 530 if bridge_interfaces: 531 for iface in bridge_interfaces: 532 BRIDGE_DOWN = 'ifconfig {} down'.format(iface) 533 BRIDGE_DEL = 'brctl delbr {}'.format(iface) 534 self.ssh.run(BRIDGE_DOWN) 535 self.ssh.run(BRIDGE_DEL) 536 537 def stop_all_aps(self): 538 """Stops all running aps on this device.""" 539 540 for ap in list(self._aps.keys()): 541 self.stop_ap(ap) 542 543 def close(self): 544 """Called to take down the entire access point. 545 546 When called will stop all aps running on this host, shutdown the dhcp 547 server, and stop the ssh connection. 548 """ 549 550 if self._aps: 551 self.stop_all_aps() 552 self.ssh.close() 553 554 def generate_bridge_configs(self, channel): 555 """Generate a list of configs for a bridge between LAN and WLAN. 556 557 Args: 558 channel: the channel WLAN interface is brought up on 559 iface_lan: the LAN interface to bridge 560 Returns: 561 configs: tuple containing iface_wlan, iface_lan and bridge_ip 562 """ 563 564 if channel < 15: 565 iface_wlan = self.wlan_2g 566 subnet_str = self._AP_2G_SUBNET_STR 567 else: 568 iface_wlan = self.wlan_5g 569 subnet_str = self._AP_5G_SUBNET_STR 570 571 iface_lan = self.lan 572 573 a, b, c, _ = subnet_str.strip('/24').split('.') 574 bridge_ip = "%s.%s.%s.%s" % (a, b, c, BRIDGE_IP_LAST) 575 576 configs = (iface_wlan, iface_lan, bridge_ip) 577 578 return configs 579 580 def install_scapy(self, scapy_path, send_ra_path): 581 """Install scapy 582 583 Args: 584 scapy_path: path where scapy tar file is located on server 585 send_ra_path: path where sendra path is located on server 586 """ 587 self.scapy_install_path = self.ssh.run('mktemp -d').stdout.rstrip() 588 self.log.info("Scapy install path: %s" % self.scapy_install_path) 589 self.ssh.send_file(scapy_path, self.scapy_install_path) 590 self.ssh.send_file(send_ra_path, self.scapy_install_path) 591 592 scapy = os.path.join(self.scapy_install_path, 593 scapy_path.split('/')[-1]) 594 595 untar_res = self.ssh.run('tar -xvf %s -C %s' % 596 (scapy, self.scapy_install_path)) 597 598 instl_res = self.ssh.run( 599 'cd %s; %s' % (self.scapy_install_path, SCAPY_INSTALL_COMMAND)) 600 601 def cleanup_scapy(self): 602 """ Cleanup scapy """ 603 if self.scapy_install_path: 604 cmd = 'rm -rf %s' % self.scapy_install_path 605 self.log.info("Cleaning up scapy %s" % cmd) 606 output = self.ssh.run(cmd) 607 self.scapy_install_path = None 608 609 def send_ra(self, 610 iface, 611 mac=RA_MULTICAST_ADDR, 612 interval=1, 613 count=None, 614 lifetime=LIFETIME, 615 rtt=0): 616 """Invoke scapy and send RA to the device. 617 618 Args: 619 iface: string of the WiFi interface to use for sending packets. 620 mac: string HWAddr/MAC address to send the packets to. 621 interval: int Time to sleep between consecutive packets. 622 count: int Number of packets to be sent. 623 lifetime: int original RA's router lifetime in seconds. 624 rtt: retrans timer of the RA packet 625 """ 626 scapy_command = os.path.join(self.scapy_install_path, RA_SCRIPT) 627 options = ' -m %s -i %d -c %d -l %d -in %s -rtt %s' % ( 628 mac, interval, count, lifetime, iface, rtt) 629 self.log.info("Scapy cmd: %s" % scapy_command + options) 630 res = self.ssh.run(scapy_command + options) 631 632 def get_icmp6intype134(self): 633 """Read the value of Icmp6InType134 and return integer. 634 635 Returns: 636 Integer value >0 if grep is successful; 0 otherwise. 637 """ 638 ra_count_str = self.ssh.run('grep Icmp6InType134 %s || true' % 639 PROC_NET_SNMP6).stdout 640 if ra_count_str: 641 return int(ra_count_str.split()[1]) 642 643 def ping(self, 644 dest_ip, 645 count=3, 646 interval=1000, 647 timeout=1000, 648 size=56, 649 additional_ping_params=None): 650 """Pings from AP to dest_ip, returns dict of ping stats (see utils.ping) 651 """ 652 return utils.ping(self.ssh, 653 dest_ip, 654 count=count, 655 interval=interval, 656 timeout=timeout, 657 size=size, 658 additional_ping_params=additional_ping_params) 659 660 def can_ping(self, 661 dest_ip, 662 count=1, 663 interval=1000, 664 timeout=1000, 665 size=56, 666 additional_ping_params=None): 667 """Returns whether ap can ping dest_ip (see utils.can_ping)""" 668 return utils.can_ping(self.ssh, 669 dest_ip, 670 count=count, 671 interval=interval, 672 timeout=timeout, 673 size=size, 674 additional_ping_params=additional_ping_params) 675 676 def hard_power_cycle(self, 677 pdus, 678 unreachable_timeout=30, 679 ping_timeout=60, 680 ssh_timeout=30, 681 hostapd_configs=None): 682 """Kills, then restores power to AccessPoint, verifying it goes down and 683 comes back online cleanly. 684 685 Args: 686 pdus: list, PduDevices in the testbed 687 unreachable_timeout: int, time to wait for AccessPoint to become 688 unreachable 689 ping_timeout: int, time to wait for AccessPoint to responsd to pings 690 ssh_timeout: int, time to wait for AccessPoint to allow SSH 691 hostapd_configs (optional): list, containing hostapd settings. If 692 present, these networks will be spun up after the AP has 693 rebooted. This list can either contain HostapdConfig objects, or 694 dictionaries with the start_ap params 695 (i.e { 'hostapd_config': <HostapdConfig>, 696 'setup_bridge': <bool>, 697 'additional_parameters': <dict> } ). 698 Raise: 699 Error, if no PduDevice is provided in AccessPoint config. 700 ConnectionError, if AccessPoint fails to go offline or come back. 701 """ 702 if not self.device_pdu_config: 703 raise Error('No PduDevice provided in AccessPoint config.') 704 705 if hostapd_configs is None: 706 hostapd_configs = [] 707 708 self.log.info('Power cycling AccessPoint (%s)' % 709 self.ssh_settings.hostname) 710 ap_pdu, ap_pdu_port = pdu.get_pdu_port_for_device( 711 self.device_pdu_config, pdus) 712 713 self.log.info('Killing power to AccessPoint (%s)' % 714 self.ssh_settings.hostname) 715 ap_pdu.off(str(ap_pdu_port)) 716 717 self.log.info('Verifying AccessPoint is unreachable.') 718 timeout = time.time() + unreachable_timeout 719 while time.time() < timeout: 720 if not utils.can_ping(job, self.ssh_settings.hostname): 721 self.log.info('AccessPoint is unreachable as expected.') 722 break 723 else: 724 self.log.debug( 725 'AccessPoint is still responding to pings. Retrying in 1 ' 726 'second.') 727 time.sleep(1) 728 else: 729 raise ConnectionError('Failed to bring down AccessPoint (%s)' % 730 self.ssh_settings.hostname) 731 self._aps.clear() 732 733 self.log.info('Restoring power to AccessPoint (%s)' % 734 self.ssh_settings.hostname) 735 ap_pdu.on(str(ap_pdu_port)) 736 737 self.log.info('Waiting for AccessPoint to respond to pings.') 738 timeout = time.time() + ping_timeout 739 while time.time() < timeout: 740 if utils.can_ping(job, self.ssh_settings.hostname): 741 self.log.info('AccessPoint responded to pings.') 742 break 743 else: 744 self.log.debug('AccessPoint is not responding to pings. ' 745 'Retrying in 1 second.') 746 time.sleep(1) 747 else: 748 raise ConnectionError('Timed out waiting for AccessPoint (%s) to ' 749 'respond to pings.' % 750 self.ssh_settings.hostname) 751 752 self.log.info('Waiting for AccessPoint to allow ssh connection.') 753 timeout = time.time() + ssh_timeout 754 while time.time() < timeout: 755 try: 756 self.ssh.run('echo') 757 except connection.Error: 758 self.log.debug('AccessPoint is not allowing ssh connection. ' 759 'Retrying in 1 second.') 760 time.sleep(1) 761 else: 762 self.log.info('AccessPoint available via ssh.') 763 break 764 else: 765 raise ConnectionError('Timed out waiting for AccessPoint (%s) to ' 766 'allow ssh connection.' % 767 self.ssh_settings.hostname) 768 769 # Allow 5 seconds for OS to finish getting set up 770 time.sleep(5) 771 self._initial_ap() 772 self.log.info('AccessPoint (%s) power cycled successfully.' % 773 self.ssh_settings.hostname) 774 775 for settings in hostapd_configs: 776 if type(settings) == hostapd_config.HostapdConfig: 777 config = settings 778 setup_bridge = False 779 additional_parameters = None 780 781 elif type(settings) == dict: 782 config = settings['hostapd_config'] 783 setup_bridge = settings.get('setup_bridge', False) 784 additional_parameters = settings.get('additional_parameters', 785 None) 786 else: 787 raise TypeError( 788 'Items in hostapd_configs list must either be ' 789 'hostapd.HostapdConfig objects or dictionaries.') 790 791 self.log.info('Restarting network (%s) on AccessPoint.' % 792 config.ssid) 793 self.start_ap(config, 794 setup_bridge=setup_bridge, 795 additional_parameters=additional_parameters) 796