• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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