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