• 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    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