blob: a2f284061032bd2eae2a7f91101c3d4da338046d [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright 2016 - Google, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import collections
import ipaddress
import os
import time
from acts import logger
from acts import utils
from acts.controllers import pdu
from acts.controllers.ap_lib import ap_get_interface
from acts.controllers.ap_lib import ap_iwconfig
from acts.controllers.ap_lib import bridge_interface
from acts.controllers.ap_lib import dhcp_config
from acts.controllers.ap_lib import dhcp_server
from acts.controllers.ap_lib import hostapd
from acts.controllers.ap_lib import hostapd_ap_preset
from acts.controllers.ap_lib import hostapd_constants
from acts.controllers.ap_lib import hostapd_config
from acts.controllers.utils_lib.commands import ip
from acts.controllers.utils_lib.commands import route
from acts.controllers.utils_lib.commands import shell
from acts.controllers.utils_lib.ssh import connection
from acts.controllers.utils_lib.ssh import settings
from acts.libs.proc import job
MOBLY_CONTROLLER_CONFIG_NAME = 'AccessPoint'
ACTS_CONTROLLER_REFERENCE_NAME = 'access_points'
_BRCTL = 'brctl'
LIFETIME = 180
PROC_NET_SNMP6 = '/proc/net/snmp6'
SCAPY_INSTALL_COMMAND = 'sudo python setup.py install'
RA_MULTICAST_ADDR = '33:33:00:00:00:01'
RA_SCRIPT = 'sendra.py'
def create(configs):
"""Creates ap controllers from a json config.
Creates an ap controller from either a list, or a single
element. The element can either be just the hostname or a dictionary
containing the hostname and username of the ap to connect to over ssh.
Args:
The json configs that represent this controller.
Returns:
A new AccessPoint.
"""
return [AccessPoint(c) for c in configs]
def destroy(aps):
"""Destroys a list of access points.
Args:
aps: The list of access points to destroy.
"""
for ap in aps:
ap.close()
def get_info(aps):
"""Get information on a list of access points.
Args:
aps: A list of AccessPoints.
Returns:
A list of all aps hostname.
"""
return [ap.ssh_settings.hostname for ap in aps]
def setup_ap(access_point,
profile_name,
channel,
ssid,
mode=None,
preamble=None,
beacon_interval=None,
dtim_period=None,
frag_threshold=None,
rts_threshold=None,
force_wmm=None,
hidden=False,
security=None,
pmf_support=None,
additional_ap_parameters=None,
password=None,
n_capabilities=None,
ac_capabilities=None,
vht_bandwidth=None,
setup_bridge=False):
"""Creates a hostapd profile and runs it on an ap. This is a convenience
function that allows us to start an ap with a single function, without first
creating a hostapd config.
Args:
access_point: An ACTS access_point controller
profile_name: The profile name of one of the hostapd ap presets.
channel: What channel to set the AP to.
preamble: Whether to set short or long preamble (True or False)
beacon_interval: The beacon interval (int)
dtim_period: Length of dtim period (int)
frag_threshold: Fragmentation threshold (int)
rts_threshold: RTS threshold (int)
force_wmm: Enable WMM or not (True or False)
hidden: Advertise the SSID or not (True or False)
security: What security to enable.
pmf_support: int, whether pmf is not disabled, enabled, or required
additional_ap_parameters: Additional parameters to send the AP.
password: Password to connect to WLAN if necessary.
check_connectivity: Whether to check for internet connectivity.
Returns:
An identifier for each ssid being started. These identifiers can be
used later by this controller to control the ap.
Raises:
Error: When the ap can't be brought up.
"""
ap = hostapd_ap_preset.create_ap_preset(profile_name=profile_name,
iface_wlan_2g=access_point.wlan_2g,
iface_wlan_5g=access_point.wlan_5g,
channel=channel,
ssid=ssid,
mode=mode,
short_preamble=preamble,
beacon_interval=beacon_interval,
dtim_period=dtim_period,
frag_threshold=frag_threshold,
rts_threshold=rts_threshold,
force_wmm=force_wmm,
hidden=hidden,
bss_settings=[],
security=security,
pmf_support=pmf_support,
n_capabilities=n_capabilities,
ac_capabilities=ac_capabilities,
vht_bandwidth=vht_bandwidth)
return access_point.start_ap(
hostapd_config=ap,
setup_bridge=setup_bridge,
additional_parameters=additional_ap_parameters)
class Error(Exception):
"""Error raised when there is a problem with the access point."""
_ApInstance = collections.namedtuple('_ApInstance', ['hostapd', 'subnet'])
# These ranges were split this way since each physical radio can have up
# to 8 SSIDs so for the 2GHz radio the DHCP range will be
# 192.168.1 - 8 and the 5Ghz radio will be 192.168.9 - 16
_AP_2GHZ_SUBNET_STR_DEFAULT = '192.168.1.0/24'
_AP_5GHZ_SUBNET_STR_DEFAULT = '192.168.9.0/24'
# The last digit of the ip for the bridge interface
BRIDGE_IP_LAST = '100'
class AccessPoint(object):
"""An access point controller.
Attributes:
ssh: The ssh connection to this ap.
ssh_settings: The ssh settings being used by the ssh connection.
dhcp_settings: The dhcp server settings being used.
"""
def __init__(self, configs):
"""
Args:
configs: configs for the access point from config file.
"""
self.ssh_settings = settings.from_config(configs['ssh_config'])
self.log = logger.create_logger(lambda msg: '[Access Point|%s] %s' %
(self.ssh_settings.hostname, msg))
self.device_pdu_config = configs.get('PduDevice', None)
self.identifier = self.ssh_settings.hostname
if 'ap_subnet' in configs:
self._AP_2G_SUBNET_STR = configs['ap_subnet']['2g']
self._AP_5G_SUBNET_STR = configs['ap_subnet']['5g']
else:
self._AP_2G_SUBNET_STR = _AP_2GHZ_SUBNET_STR_DEFAULT
self._AP_5G_SUBNET_STR = _AP_5GHZ_SUBNET_STR_DEFAULT
self._AP_2G_SUBNET = dhcp_config.Subnet(
ipaddress.ip_network(self._AP_2G_SUBNET_STR))
self._AP_5G_SUBNET = dhcp_config.Subnet(
ipaddress.ip_network(self._AP_5G_SUBNET_STR))
self.ssh = connection.SshConnection(self.ssh_settings)
# Singleton utilities for running various commands.
self._ip_cmd = ip.LinuxIpCommand(self.ssh)
self._route_cmd = route.LinuxRouteCommand(self.ssh)
# A map from network interface name to _ApInstance objects representing
# the hostapd instance running against the interface.
self._aps = dict()
self._dhcp = None
self._dhcp_bss = dict()
self.bridge = bridge_interface.BridgeInterface(self)
self.interfaces = ap_get_interface.ApInterfaces(self)
self.iwconfig = ap_iwconfig.ApIwconfig(self)
# Get needed interface names and initialize the unneccessary ones.
self.wan = self.interfaces.get_wan_interface()
self.wlan = self.interfaces.get_wlan_interface()
self.wlan_2g = self.wlan[0]
self.wlan_5g = self.wlan[1]
self.lan = self.interfaces.get_lan_interface()
self._initial_ap()
self.scapy_install_path = None
self.setup_bridge = False
def _initial_ap(self):
"""Initial AP interfaces.
Bring down hostapd if instance is running, bring down all bridge
interfaces.
"""
# This is necessary for Gale/Whirlwind flashed with dev channel image
# Unused interfaces such as existing hostapd daemon, guest, mesh
# interfaces need to be brought down as part of the AP initialization
# process, otherwise test would fail.
try:
self.ssh.run('stop wpasupplicant')
except job.Error:
self.log.info('No wpasupplicant running')
try:
self.ssh.run('stop hostapd')
except job.Error:
self.log.info('No hostapd running')
# Bring down all wireless interfaces
for iface in self.wlan:
WLAN_DOWN = 'ifconfig {} down'.format(iface)
self.ssh.run(WLAN_DOWN)
# Bring down all bridge interfaces
bridge_interfaces = self.interfaces.get_bridge_interface()
if bridge_interfaces:
for iface in bridge_interfaces:
BRIDGE_DOWN = 'ifconfig {} down'.format(iface)
BRIDGE_DEL = 'brctl delbr {}'.format(iface)
self.ssh.run(BRIDGE_DOWN)
self.ssh.run(BRIDGE_DEL)
def start_ap(self,
hostapd_config,
setup_bridge=False,
additional_parameters=None):
"""Starts as an ap using a set of configurations.
This will start an ap on this host. To start an ap the controller
selects a network interface to use based on the configs given. It then
will start up hostapd on that interface. Next a subnet is created for
the network interface and dhcp server is refreshed to give out ips
for that subnet for any device that connects through that interface.
Args:
hostapd_config: hostapd_config.HostapdConfig, The configurations
to use when starting up the ap.
setup_bridge: Whether to bridge the LAN interface WLAN interface.
Only one WLAN interface can be bridged with the LAN interface
and none of the guest networks can be bridged.
additional_parameters: A dictionary of parameters that can sent
directly into the hostapd config file. This can be used for
debugging and or adding one off parameters into the config.
Returns:
An identifier for each ssid being started. These identifiers can be
used later by this controller to control the ap.
Raises:
Error: When the ap can't be brought up.
"""
if hostapd_config.frequency < 5000:
interface = self.wlan_2g
subnet = self._AP_2G_SUBNET
else:
interface = self.wlan_5g
subnet = self._AP_5G_SUBNET
# In order to handle dhcp servers on any interface, the initiation of
# the dhcp server must be done after the wlan interfaces are figured
# out as opposed to being in __init__
self._dhcp = dhcp_server.DhcpServer(self.ssh, interface=interface)
# For multi bssid configurations the mac address
# of the wireless interface needs to have enough space to mask out
# up to 8 different mac addresses. So in for one interface the range is
# hex 0-7 and for the other the range is hex 8-f.
interface_mac_orig = None
cmd = "ifconfig %s|grep ether|awk -F' ' '{print $2}'" % interface
interface_mac_orig = self.ssh.run(cmd)
if interface == self.wlan_5g:
hostapd_config.bssid = interface_mac_orig.stdout[:-1] + '0'
last_octet = 1
if interface == self.wlan_2g:
hostapd_config.bssid = interface_mac_orig.stdout[:-1] + '8'
last_octet = 9
if interface in self._aps:
raise ValueError('No WiFi interface available for AP on '
'channel %d' % hostapd_config.channel)
apd = hostapd.Hostapd(self.ssh, interface)
new_instance = _ApInstance(hostapd=apd, subnet=subnet)
self._aps[interface] = new_instance
# Turn off the DHCP server, we're going to change its settings.
self.stop_dhcp()
# Clear all routes to prevent old routes from interfering.
self._route_cmd.clear_routes(net_interface=interface)
self._dhcp_bss = dict()
if hostapd_config.bss_lookup:
# The self._dhcp_bss dictionary is created to hold the key/value
# pair of the interface name and the ip scope that will be
# used for the particular interface. The a, b, c, d
# variables below are the octets for the ip address. The
# third octet is then incremented for each interface that
# is requested. This part is designed to bring up the
# hostapd interfaces and not the DHCP servers for each
# interface.
counter = 1
for bss in hostapd_config.bss_lookup:
if interface_mac_orig:
hostapd_config.bss_lookup[bss].bssid = (
interface_mac_orig.stdout[:-1] + hex(last_octet)[-1:])
self._route_cmd.clear_routes(net_interface=str(bss))
if interface is self.wlan_2g:
starting_ip_range = self._AP_2G_SUBNET_STR
else:
starting_ip_range = self._AP_5G_SUBNET_STR
a, b, c, d = starting_ip_range.split('.')
self._dhcp_bss[bss] = dhcp_config.Subnet(
ipaddress.ip_network('%s.%s.%s.%s' %
(a, b, str(int(c) + counter), d)))
counter = counter + 1
last_octet = last_octet + 1
apd.start(hostapd_config, additional_parameters=additional_parameters)
# The DHCP serer requires interfaces to have ips and routes before
# the server will come up.
interface_ip = ipaddress.ip_interface(
'%s/%s' % (subnet.router, subnet.network.netmask))
if setup_bridge is True:
bridge_interface_name = 'eth_test'
self.create_bridge(bridge_interface_name, [interface, self.lan])
self._ip_cmd.set_ipv4_address(bridge_interface_name, interface_ip)
else:
self._ip_cmd.set_ipv4_address(interface, interface_ip)
if hostapd_config.bss_lookup:
# This loop goes through each interface that was setup for
# hostapd and assigns the DHCP scopes that were defined but
# not used during the hostapd loop above. The k and v
# variables represent the interface name, k, and dhcp info, v.
for k, v in self._dhcp_bss.items():
bss_interface_ip = ipaddress.ip_interface(
'%s/%s' % (self._dhcp_bss[k].router,
self._dhcp_bss[k].network.netmask))
self._ip_cmd.set_ipv4_address(str(k), bss_interface_ip)
# Restart the DHCP server with our updated list of subnets.
configured_subnets = self.get_configured_subnets()
dhcp_conf = dhcp_config.DhcpConfig(subnets=configured_subnets)
self.start_dhcp(dhcp_conf=dhcp_conf)
self.start_nat()
bss_interfaces = [bss for bss in hostapd_config.bss_lookup]
bss_interfaces.append(interface)
return bss_interfaces
def get_configured_subnets(self):
"""Get the list of configured subnets on the access point.
This allows consumers of the access point objects create custom DHCP
configs with the correct subnets.
Returns: a list of dhcp_config.Subnet objects
"""
configured_subnets = [x.subnet for x in self._aps.values()]
for k, v in self._dhcp_bss.items():
configured_subnets.append(v)
return configured_subnets
def start_dhcp(self, dhcp_conf):
"""Start a DHCP server for the specified subnets.
This allows consumers of the access point objects to control DHCP.
Args:
dhcp_conf: A dhcp_config.DhcpConfig object.
Raises:
Error: Raised when a dhcp server error is found.
"""
self._dhcp.start(config=dhcp_conf)
def stop_dhcp(self):
"""Stop DHCP for this AP object.
This allows consumers of the access point objects to control DHCP.
"""
self._dhcp.stop()
def get_dhcp_logs(self):
"""Get DHCP logs for this AP object.
This allows consumers of the access point objects to validate DHCP
behavior.
"""
return self._dhcp.get_logs()
def get_hostapd_logs(self):
"""Get hostapd logs for all interfaces on AP object.
This allows consumers of the access point objects to validate hostapd
behavior.
Returns: A dict with {interface: log} from hostapd instances.
"""
hostapd_logs = dict()
for identifier in self._aps:
hostapd_logs[identifier] = self._aps.get(
identifier).hostapd.pull_logs()
return hostapd_logs
def start_nat(self):
"""Start NAT on the AP.
This allows consumers of the access point objects to enable NAT
on the AP.
Note that this is currently a global setting, since we don't
have per-interface masquerade rules.
"""
# The following three commands are needed to enable NAT between
# the WAN and LAN/WLAN ports. This means anyone connecting to the
# WLAN/LAN ports will be able to access the internet if the WAN port
# is connected to the internet.
self.ssh.run('iptables -t nat -F')
self.ssh.run('iptables -t nat -A POSTROUTING -o %s -j MASQUERADE' %
self.wan)
self.ssh.run('echo 1 > /proc/sys/net/ipv4/ip_forward')
self.ssh.run('echo 1 > /proc/sys/net/ipv6/conf/all/forwarding')
def stop_nat(self):
"""Stop NAT on the AP.
This allows consumers of the access point objects to disable NAT on the
AP.
Note that this is currently a global setting, since we don't have
per-interface masquerade rules.
"""
self.ssh.run('iptables -t nat -F')
self.ssh.run('echo 0 > /proc/sys/net/ipv4/ip_forward')
self.ssh.run('echo 0 > /proc/sys/net/ipv6/conf/all/forwarding')
def create_bridge(self, bridge_name, interfaces):
"""Create the specified bridge and bridge the specified interfaces.
Args:
bridge_name: The name of the bridge to create.
interfaces: A list of interfaces to add to the bridge.
"""
# Create the bridge interface
self.ssh.run(
'brctl addbr {bridge_name}'.format(bridge_name=bridge_name))
for interface in interfaces:
self.ssh.run('brctl addif {bridge_name} {interface}'.format(
bridge_name=bridge_name, interface=interface))
self.ssh.run(
'ip link set {bridge_name} up'.format(bridge_name=bridge_name))
def remove_bridge(self, bridge_name):
"""Removes the specified bridge
Args:
bridge_name: The name of the bridge to remove.
"""
# Check if the bridge exists.
#
# Cases where it may not are if we failed to initialize properly
#
# Or if we're doing 2.4Ghz and 5Ghz SSIDs and we've already torn
# down the bridge once, but we got called for each band.
result = self.ssh.run(
'brctl show {bridge_name}'.format(bridge_name=bridge_name),
ignore_status=True)
# If the bridge exists, we'll get an exit_status of 0, indicating
# success, so we can continue and remove the bridge.
if result.exit_status == 0:
self.ssh.run('ip link set {bridge_name} down'.format(
bridge_name=bridge_name))
self.ssh.run(
'brctl delbr {bridge_name}'.format(bridge_name=bridge_name))
def get_bssid_from_ssid(self, ssid, band):
"""Gets the BSSID from a provided SSID
Args:
ssid: An SSID string.
band: 2G or 5G Wifi band.
Returns: The BSSID if on the AP or None if SSID could not be found.
"""
if band == hostapd_constants.BAND_2G:
interfaces = [self.wlan_2g, ssid]
else:
interfaces = [self.wlan_5g, ssid]
# Get the interface name associated with the given ssid.
for interface in interfaces:
cmd = "iw dev %s info|grep ssid|awk -F' ' '{print $2}'" % (
str(interface))
iw_output = self.ssh.run(cmd)
if 'command failed: No such device' in iw_output.stderr:
continue
else:
# If the configured ssid is equal to the given ssid, we found
# the right interface.
if iw_output.stdout == ssid:
cmd = "iw dev %s info|grep addr|awk -F' ' '{print $2}'" % (
str(interface))
iw_output = self.ssh.run(cmd)
return iw_output.stdout
return None
def stop_ap(self, identifier):
"""Stops a running ap on this controller.
Args:
identifier: The identify of the ap that should be taken down.
"""
if identifier not in list(self._aps.keys()):
raise ValueError('Invalid identifier %s given' % identifier)
instance = self._aps.get(identifier)
instance.hostapd.stop()
try:
self.stop_dhcp()
except dhcp_server.NoInterfaceError:
pass
self._ip_cmd.clear_ipv4_addresses(identifier)
del self._aps[identifier]
bridge_interfaces = self.interfaces.get_bridge_interface()
if bridge_interfaces:
for iface in bridge_interfaces:
BRIDGE_DOWN = 'ifconfig {} down'.format(iface)
BRIDGE_DEL = 'brctl delbr {}'.format(iface)
self.ssh.run(BRIDGE_DOWN)
self.ssh.run(BRIDGE_DEL)
def stop_all_aps(self):
"""Stops all running aps on this device."""
for ap in list(self._aps.keys()):
self.stop_ap(ap)
def close(self):
"""Called to take down the entire access point.
When called will stop all aps running on this host, shutdown the dhcp
server, and stop the ssh connection.
"""
if self._aps:
self.stop_all_aps()
self.ssh.close()
def generate_bridge_configs(self, channel):
"""Generate a list of configs for a bridge between LAN and WLAN.
Args:
channel: the channel WLAN interface is brought up on
iface_lan: the LAN interface to bridge
Returns:
configs: tuple containing iface_wlan, iface_lan and bridge_ip
"""
if channel < 15:
iface_wlan = self.wlan_2g
subnet_str = self._AP_2G_SUBNET_STR
else:
iface_wlan = self.wlan_5g
subnet_str = self._AP_5G_SUBNET_STR
iface_lan = self.lan
a, b, c, _ = subnet_str.strip('/24').split('.')
bridge_ip = "%s.%s.%s.%s" % (a, b, c, BRIDGE_IP_LAST)
configs = (iface_wlan, iface_lan, bridge_ip)
return configs
def install_scapy(self, scapy_path, send_ra_path):
"""Install scapy
Args:
scapy_path: path where scapy tar file is located on server
send_ra_path: path where sendra path is located on server
"""
self.scapy_install_path = self.ssh.run('mktemp -d').stdout.rstrip()
self.log.info("Scapy install path: %s" % self.scapy_install_path)
self.ssh.send_file(scapy_path, self.scapy_install_path)
self.ssh.send_file(send_ra_path, self.scapy_install_path)
scapy = os.path.join(self.scapy_install_path,
scapy_path.split('/')[-1])
untar_res = self.ssh.run('tar -xvf %s -C %s' %
(scapy, self.scapy_install_path))
instl_res = self.ssh.run(
'cd %s; %s' % (self.scapy_install_path, SCAPY_INSTALL_COMMAND))
def cleanup_scapy(self):
""" Cleanup scapy """
if self.scapy_install_path:
cmd = 'rm -rf %s' % self.scapy_install_path
self.log.info("Cleaning up scapy %s" % cmd)
output = self.ssh.run(cmd)
self.scapy_install_path = None
def send_ra(self,
iface,
mac=RA_MULTICAST_ADDR,
interval=1,
count=None,
lifetime=LIFETIME,
rtt=0):
"""Invoke scapy and send RA to the device.
Args:
iface: string of the WiFi interface to use for sending packets.
mac: string HWAddr/MAC address to send the packets to.
interval: int Time to sleep between consecutive packets.
count: int Number of packets to be sent.
lifetime: int original RA's router lifetime in seconds.
rtt: retrans timer of the RA packet
"""
scapy_command = os.path.join(self.scapy_install_path, RA_SCRIPT)
options = ' -m %s -i %d -c %d -l %d -in %s -rtt %s' % (
mac, interval, count, lifetime, iface, rtt)
self.log.info("Scapy cmd: %s" % scapy_command + options)
res = self.ssh.run(scapy_command + options)
def get_icmp6intype134(self):
"""Read the value of Icmp6InType134 and return integer.
Returns:
Integer value >0 if grep is successful; 0 otherwise.
"""
ra_count_str = self.ssh.run('grep Icmp6InType134 %s || true' %
PROC_NET_SNMP6).stdout
if ra_count_str:
return int(ra_count_str.split()[1])
def ping(self,
dest_ip,
count=3,
interval=1000,
timeout=1000,
size=56,
additional_ping_params=None):
"""Pings from AP to dest_ip, returns dict of ping stats (see utils.ping)
"""
return utils.ping(self.ssh,
dest_ip,
count=count,
interval=interval,
timeout=timeout,
size=size,
additional_ping_params=additional_ping_params)
def can_ping(self,
dest_ip,
count=1,
interval=1000,
timeout=1000,
size=56,
additional_ping_params=None):
"""Returns whether ap can ping dest_ip (see utils.can_ping)"""
return utils.can_ping(self.ssh,
dest_ip,
count=count,
interval=interval,
timeout=timeout,
size=size,
additional_ping_params=additional_ping_params)
def hard_power_cycle(self,
pdus,
unreachable_timeout=30,
ping_timeout=60,
ssh_timeout=30,
hostapd_configs=None):
"""Kills, then restores power to AccessPoint, verifying it goes down and
comes back online cleanly.
Args:
pdus: list, PduDevices in the testbed
unreachable_timeout: int, time to wait for AccessPoint to become
unreachable
ping_timeout: int, time to wait for AccessPoint to responsd to pings
ssh_timeout: int, time to wait for AccessPoint to allow SSH
hostapd_configs (optional): list, containing hostapd settings. If
present, these networks will be spun up after the AP has
rebooted. This list can either contain HostapdConfig objects, or
dictionaries with the start_ap params
(i.e { 'hostapd_config': <HostapdConfig>,
'setup_bridge': <bool>,
'additional_parameters': <dict> } ).
Raise:
Error, if no PduDevice is provided in AccessPoint config.
ConnectionError, if AccessPoint fails to go offline or come back.
"""
if not self.device_pdu_config:
raise Error('No PduDevice provided in AccessPoint config.')
if hostapd_configs is None:
hostapd_configs = []
self.log.info('Power cycling AccessPoint (%s)' %
self.ssh_settings.hostname)
ap_pdu, ap_pdu_port = pdu.get_pdu_port_for_device(
self.device_pdu_config, pdus)
self.log.info('Killing power to AccessPoint (%s)' %
self.ssh_settings.hostname)
ap_pdu.off(str(ap_pdu_port))
self.log.info('Verifying AccessPoint is unreachable.')
timeout = time.time() + unreachable_timeout
while time.time() < timeout:
if not utils.can_ping(job, self.ssh_settings.hostname):
self.log.info('AccessPoint is unreachable as expected.')
break
else:
self.log.debug(
'AccessPoint is still responding to pings. Retrying in 1 '
'second.')
time.sleep(1)
else:
raise ConnectionError('Failed to bring down AccessPoint (%s)' %
self.ssh_settings.hostname)
self._aps.clear()
self.log.info('Restoring power to AccessPoint (%s)' %
self.ssh_settings.hostname)
ap_pdu.on(str(ap_pdu_port))
self.log.info('Waiting for AccessPoint to respond to pings.')
timeout = time.time() + ping_timeout
while time.time() < timeout:
if utils.can_ping(job, self.ssh_settings.hostname):
self.log.info('AccessPoint responded to pings.')
break
else:
self.log.debug('AccessPoint is not responding to pings. '
'Retrying in 1 second.')
time.sleep(1)
else:
raise ConnectionError('Timed out waiting for AccessPoint (%s) to '
'respond to pings.' %
self.ssh_settings.hostname)
self.log.info('Waiting for AccessPoint to allow ssh connection.')
timeout = time.time() + ssh_timeout
while time.time() < timeout:
try:
self.ssh.run('echo')
except connection.Error:
self.log.debug('AccessPoint is not allowing ssh connection. '
'Retrying in 1 second.')
time.sleep(1)
else:
self.log.info('AccessPoint available via ssh.')
break
else:
raise ConnectionError('Timed out waiting for AccessPoint (%s) to '
'allow ssh connection.' %
self.ssh_settings.hostname)
# Allow 5 seconds for OS to finish getting set up
time.sleep(5)
self._initial_ap()
self.log.info('AccessPoint (%s) power cycled successfully.' %
self.ssh_settings.hostname)
for settings in hostapd_configs:
if type(settings) == hostapd_config.HostapdConfig:
config = settings
setup_bridge = False
additional_parameters = None
elif type(settings) == dict:
config = settings['hostapd_config']
setup_bridge = settings.get('setup_bridge', False)
additional_parameters = settings.get('additional_parameters',
None)
else:
raise TypeError(
'Items in hostapd_configs list must either be '
'hostapd.HostapdConfig objects or dictionaries.')
self.log.info('Restarting network (%s) on AccessPoint.' %
config.ssid)
self.start_ap(config,
setup_bridge=setup_bridge,
additional_parameters=additional_parameters)
def channel_switch(self, identifier, channel_num):
"""Switch to a different channel on the given AP."""
if identifier not in list(self._aps.keys()):
raise ValueError('Invalid identifier %s given' % identifier)
instance = self._aps.get(identifier)
self.log.info('channel switch to channel {}'.format(channel_num))
instance.hostapd.channel_switch(channel_num)
def get_current_channel(self, identifier):
"""Find the current channel on the given AP."""
if identifier not in list(self._aps.keys()):
raise ValueError('Invalid identifier %s given' % identifier)
instance = self._aps.get(identifier)
return instance.hostapd.get_current_channel()