blob: d517b10c1526f633da01710f52f4c6d470cb3f00 [file] [log] [blame]
#!/usr/bin/env python3.4
#
# 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 logging
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_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
ACTS_CONTROLLER_CONFIG_NAME = 'AccessPoint'
ACTS_CONTROLLER_REFERENCE_NAME = 'access_points'
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(settings.from_config(c['ssh_config'])) 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]
class Error(Exception):
"""Error raised when there is a problem with the access point."""
_ApInstance = collections.namedtuple('_ApInstance', ['hostapd', 'subnet'])
# We use these today as part of a hardcoded mapping of interface name to
# capabilities. However, medium term we need to start inspecting
# interfaces to determine their capabilities.
_AP_2GHZ_INTERFACE = 'wlan0'
_AP_5GHZ_INTERFACE = 'wlan1'
# 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 = '192.168.1.0/24'
_AP_5GHZ_SUBNET_STR = '192.168.9.0/24'
_AP_2GHZ_SUBNET = dhcp_config.Subnet(ipaddress.ip_network(_AP_2GHZ_SUBNET_STR))
_AP_5GHZ_SUBNET = dhcp_config.Subnet(ipaddress.ip_network(_AP_5GHZ_SUBNET_STR))
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 conneciton.
dhcp_settings: The dhcp server settings being used.
"""
def __init__(self, ssh_settings):
"""
Args:
ssh_settings: acts.controllers.utils_lib.ssh.SshSettings instance.
"""
self.ssh_settings = ssh_settings
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()
def __del__(self):
self.close()
def start_ap(self, hostapd_config, 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.
additional_parameters: A dicitonary 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 the ap being run. This identifier can be used
later by this controller to control the ap.
Raises:
Error: When the ap can't be brought up.
"""
# Right now, we hardcode that a frequency maps to a particular
# network interface. This is true of the hardware we're running
# against right now, but in general, we'll want to do some
# dynamic discovery of interface capabilities. See b/32582843
if hostapd_config.frequency < 5000:
interface = _AP_2GHZ_INTERFACE
subnet = _AP_2GHZ_SUBNET
else:
interface = _AP_5GHZ_INTERFACE
subnet = _AP_5GHZ_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. The easiest way to do this
# is to set the last byte to 0. While technically this could
# cause a duplicate mac address it is unlikely and will allow for
# one radio to have up to 8 APs on the interface. The check ensures
# backwards compatibility since if someone has set the bssid on purpose
# the bssid will not be changed from what the user set.
if not hostapd_config.bssid:
cmd = "ifconfig %s|grep ether|awk -F' ' '{print $2}'" % interface
interface_mac = self.ssh.run(cmd)
interface_mac = interface_mac.stdout[:-1] + '0'
hostapd_config.bssid = interface_mac
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._dhcp.stop()
# Clear all routes to prevent old routes from interfering.
self._route_cmd.clear_routes(net_interface=interface)
if hostapd_config.bss_lookup:
# The 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.
dhcp_bss = {}
counter = 1
for bss in hostapd_config.bss_lookup:
self._route_cmd.clear_routes(net_interface=str(bss))
if interface is _AP_2GHZ_INTERFACE:
starting_ip_range = _AP_2GHZ_SUBNET_STR
else:
starting_ip_range = _AP_5GHZ_SUBNET_STR
a, b, c, d = starting_ip_range.split('.')
dhcp_bss[bss] = dhcp_config.Subnet(
ipaddress.ip_network('%s.%s.%s.%s' % (a, b, str(
int(c) + counter), d)))
counter = counter + 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))
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 dhcp_bss.items():
bss_interface_ip = ipaddress.ip_interface(
'%s/%s' %
(dhcp_bss[k].router, 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 = [x.subnet for x in self._aps.values()]
if hostapd_config.bss_lookup:
for k, v in dhcp_bss.items():
configured_subnets.append(v)
self._dhcp.start(config=dhcp_config.DhcpConfig(configured_subnets))
return interface
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 self._aps:
raise ValueError('Invalid identifer %s given' % identifier)
instance = self._aps.get(identifier)
instance.hostapd.stop()
self._dhcp.stop()
self._ip_cmd.clear_ipv4_addresses(identifier)
# DHCP server needs to refresh in order to tear down the subnet no
# longer being used. In the event that all interfaces are torn down
# then an exception gets thrown. We need to catch this exception and
# check that all interfaces should actually be down.
configured_subnets = [x.subnet for x in self._aps.values()]
if configured_subnets:
self._dhcp.start(dhcp_config.DhcpConfig(configured_subnets))
def stop_all_aps(self):
"""Stops all running aps on this device."""
for ap in self._aps.keys():
try:
self.stop_ap(ap)
except dhcp_server.NoInterfaceError as e:
pass
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 conneciton.
"""
if self._aps:
self.stop_all_aps()
self._dhcp.stop()
self.ssh.close()