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