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