blob: a8ea506161a01d72309a3fd3f0be021fbfe115c5 [file] [log] [blame]
tturney1bdf77d2015-12-28 17:46:13 -08001#!/usr/bin/env python3.4
Ang Li73697b32015-12-03 00:41:53 +00002#
tturney1bdf77d2015-12-28 17:46:13 -08003# Copyright 2016 - Google, Inc.
Ang Li73697b32015-12-03 00:41:53 +00004#
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
Christopher Wileyede81f02016-11-01 09:15:30 -070017import collections
Benny Peake85f112a2016-10-07 19:41:49 -070018import ipaddress
Ang Li73697b32015-12-03 00:41:53 +000019
Qi Jiangc3e87852017-10-04 22:45:52 -070020from acts.controllers.ap_lib import bridge_interface
Benny Peake85f112a2016-10-07 19:41:49 -070021from acts.controllers.ap_lib import dhcp_config
22from acts.controllers.ap_lib import dhcp_server
23from acts.controllers.ap_lib import hostapd
24from acts.controllers.ap_lib import hostapd_config
25from acts.controllers.utils_lib.commands import ip
26from acts.controllers.utils_lib.commands import route
Joe Brennan88279af2017-02-15 18:11:53 -080027from acts.controllers.utils_lib.commands import shell
Benny Peake85f112a2016-10-07 19:41:49 -070028from acts.controllers.utils_lib.ssh import connection
29from acts.controllers.utils_lib.ssh import settings
30
31ACTS_CONTROLLER_CONFIG_NAME = 'AccessPoint'
Benny Peake0f5049b2016-10-28 15:57:01 -070032ACTS_CONTROLLER_REFERENCE_NAME = 'access_points'
Benny Peake85f112a2016-10-07 19:41:49 -070033
Ang Li51df0452016-06-07 10:58:16 -070034
Ang Lia11f2cf2016-05-05 11:54:40 -070035def create(configs):
Benny Peake85f112a2016-10-07 19:41:49 -070036 """Creates ap controllers from a json config.
37
38 Creates an ap controller from either a list, or a single
39 element. The element can either be just the hostname or a dictionary
40 containing the hostname and username of the ap to connect to over ssh.
41
42 Args:
43 The json configs that represent this controller.
44
45 Returns:
46 A new AccessPoint.
47 """
Joe Brennan88279af2017-02-15 18:11:53 -080048 return [
49 AccessPoint(settings.from_config(c['ssh_config'])) for c in configs
50 ]
Ang Li73697b32015-12-03 00:41:53 +000051
Ang Li51df0452016-06-07 10:58:16 -070052
Benny Peake85f112a2016-10-07 19:41:49 -070053def destroy(aps):
54 """Destroys a list of access points.
Ang Li73697b32015-12-03 00:41:53 +000055
Benny Peake85f112a2016-10-07 19:41:49 -070056 Args:
57 aps: The list of access points to destroy.
Ang Li73697b32015-12-03 00:41:53 +000058 """
Benny Peake85f112a2016-10-07 19:41:49 -070059 for ap in aps:
60 ap.close()
Ang Li73697b32015-12-03 00:41:53 +000061
Ang Li73697b32015-12-03 00:41:53 +000062
Benny Peake85f112a2016-10-07 19:41:49 -070063def get_info(aps):
64 """Get information on a list of access points.
Ang Li73697b32015-12-03 00:41:53 +000065
Benny Peake85f112a2016-10-07 19:41:49 -070066 Args:
67 aps: A list of AccessPoints.
68
69 Returns:
70 A list of all aps hostname.
71 """
Christopher Wiley7dae1d92016-10-31 14:47:58 -070072 return [ap.ssh_settings.hostname for ap in aps]
Benny Peake85f112a2016-10-07 19:41:49 -070073
74
75class Error(Exception):
76 """Error raised when there is a problem with the access point."""
77
78
Joe Brennan88279af2017-02-15 18:11:53 -080079_ApInstance = collections.namedtuple('_ApInstance', ['hostapd', 'subnet'])
Christopher Wileyede81f02016-11-01 09:15:30 -070080
81# We use these today as part of a hardcoded mapping of interface name to
82# capabilities. However, medium term we need to start inspecting
83# interfaces to determine their capabilities.
84_AP_2GHZ_INTERFACE = 'wlan0'
85_AP_5GHZ_INTERFACE = 'wlan1'
Joe Brennan88279af2017-02-15 18:11:53 -080086# These ranges were split this way since each physical radio can have up
87# to 8 SSIDs so for the 2GHz radio the DHCP range will be
88# 192.168.1 - 8 and the 5Ghz radio will be 192.168.9 - 16
89_AP_2GHZ_SUBNET_STR = '192.168.1.0/24'
90_AP_5GHZ_SUBNET_STR = '192.168.9.0/24'
91_AP_2GHZ_SUBNET = dhcp_config.Subnet(ipaddress.ip_network(_AP_2GHZ_SUBNET_STR))
92_AP_5GHZ_SUBNET = dhcp_config.Subnet(ipaddress.ip_network(_AP_5GHZ_SUBNET_STR))
Qi Jiangc3e87852017-10-04 22:45:52 -070093LAN_INTERFACE = 'eth1'
94# The last digit of the ip for the bridge interface
95BRIDGE_IP_LAST = '100'
Christopher Wileyede81f02016-11-01 09:15:30 -070096
97
Benny Peake85f112a2016-10-07 19:41:49 -070098class AccessPoint(object):
99 """An access point controller.
100
101 Attributes:
Benny Peake85f112a2016-10-07 19:41:49 -0700102 ssh: The ssh connection to this ap.
103 ssh_settings: The ssh settings being used by the ssh conneciton.
104 dhcp_settings: The dhcp server settings being used.
105 """
106
Christopher Wiley7dae1d92016-10-31 14:47:58 -0700107 def __init__(self, ssh_settings):
Benny Peake85f112a2016-10-07 19:41:49 -0700108 """
109 Args:
Christopher Wiley7dae1d92016-10-31 14:47:58 -0700110 ssh_settings: acts.controllers.utils_lib.ssh.SshSettings instance.
Benny Peake85f112a2016-10-07 19:41:49 -0700111 """
Christopher Wiley7dae1d92016-10-31 14:47:58 -0700112 self.ssh_settings = ssh_settings
Benny Peake85f112a2016-10-07 19:41:49 -0700113 self.ssh = connection.SshConnection(self.ssh_settings)
114
Christopher Wileyede81f02016-11-01 09:15:30 -0700115 # Singleton utilities for running various commands.
Benny Peake85f112a2016-10-07 19:41:49 -0700116 self._ip_cmd = ip.LinuxIpCommand(self.ssh)
117 self._route_cmd = route.LinuxRouteCommand(self.ssh)
118
Christopher Wileyede81f02016-11-01 09:15:30 -0700119 # A map from network interface name to _ApInstance objects representing
120 # the hostapd instance running against the interface.
121 self._aps = dict()
Qi Jiangc3e87852017-10-04 22:45:52 -0700122 self.bridge = bridge_interface.BridgeInterface(self.ssh)
Christopher Wileyede81f02016-11-01 09:15:30 -0700123
Joe Brennan88279af2017-02-15 18:11:53 -0800124 def start_ap(self, hostapd_config, additional_parameters=None):
Benny Peake85f112a2016-10-07 19:41:49 -0700125 """Starts as an ap using a set of configurations.
126
127 This will start an ap on this host. To start an ap the controller
128 selects a network interface to use based on the configs given. It then
129 will start up hostapd on that interface. Next a subnet is created for
130 the network interface and dhcp server is refreshed to give out ips
131 for that subnet for any device that connects through that interface.
Ang Li73697b32015-12-03 00:41:53 +0000132
133 Args:
Benny Peake85f112a2016-10-07 19:41:49 -0700134 hostapd_config: hostapd_config.HostapdConfig, The configurations
135 to use when starting up the ap.
Joe Brennan88279af2017-02-15 18:11:53 -0800136 additional_parameters: A dicitonary of parameters that can sent
137 directly into the hostapd config file. This
138 can be used for debugging and or adding one
139 off parameters into the config.
Ang Li73697b32015-12-03 00:41:53 +0000140
141 Returns:
Benny Peake85f112a2016-10-07 19:41:49 -0700142 An identifier for the ap being run. This identifier can be used
143 later by this controller to control the ap.
Ang Li73697b32015-12-03 00:41:53 +0000144
145 Raises:
Benny Peake85f112a2016-10-07 19:41:49 -0700146 Error: When the ap can't be brought up.
Ang Li73697b32015-12-03 00:41:53 +0000147 """
Christopher Wileyede81f02016-11-01 09:15:30 -0700148 # Right now, we hardcode that a frequency maps to a particular
149 # network interface. This is true of the hardware we're running
150 # against right now, but in general, we'll want to do some
151 # dynamic discovery of interface capabilities. See b/32582843
Benny Peake85f112a2016-10-07 19:41:49 -0700152 if hostapd_config.frequency < 5000:
Christopher Wileyede81f02016-11-01 09:15:30 -0700153 interface = _AP_2GHZ_INTERFACE
154 subnet = _AP_2GHZ_SUBNET
Benny Peake85f112a2016-10-07 19:41:49 -0700155 else:
Christopher Wileyede81f02016-11-01 09:15:30 -0700156 interface = _AP_5GHZ_INTERFACE
157 subnet = _AP_5GHZ_SUBNET
158
Joe Brennan88279af2017-02-15 18:11:53 -0800159 # In order to handle dhcp servers on any interface, the initiation of
160 # the dhcp server must be done after the wlan interfaces are figured
161 # out as opposed to being in __init__
162 self._dhcp = dhcp_server.DhcpServer(self.ssh, interface=interface)
163
164 # For multi bssid configurations the mac address
165 # of the wireless interface needs to have enough space to mask out
166 # up to 8 different mac addresses. The easiest way to do this
167 # is to set the last byte to 0. While technically this could
168 # cause a duplicate mac address it is unlikely and will allow for
Bindu Mahadev2ef0c3e2017-07-13 15:39:55 -0700169 # one radio to have up to 8 APs on the interface.
Joe Brennan3d00b2a2017-04-13 14:27:05 -0700170 interface_mac_orig = None
Bindu Mahadev2ef0c3e2017-07-13 15:39:55 -0700171 cmd = "ifconfig %s|grep ether|awk -F' ' '{print $2}'" % interface
172 interface_mac_orig = self.ssh.run(cmd)
173 hostapd_config.bssid = interface_mac_orig.stdout[:-1] + '0'
Joe Brennan88279af2017-02-15 18:11:53 -0800174
Christopher Wileyede81f02016-11-01 09:15:30 -0700175 if interface in self._aps:
176 raise ValueError('No WiFi interface available for AP on '
177 'channel %d' % hostapd_config.channel)
178
179 apd = hostapd.Hostapd(self.ssh, interface)
Joe Brennan88279af2017-02-15 18:11:53 -0800180 new_instance = _ApInstance(hostapd=apd, subnet=subnet)
Christopher Wileyede81f02016-11-01 09:15:30 -0700181 self._aps[interface] = new_instance
182
183 # Turn off the DHCP server, we're going to change its settings.
184 self._dhcp.stop()
185 # Clear all routes to prevent old routes from interfering.
186 self._route_cmd.clear_routes(net_interface=interface)
187
Joe Brennan88279af2017-02-15 18:11:53 -0800188 if hostapd_config.bss_lookup:
189 # The dhcp_bss dictionary is created to hold the key/value
190 # pair of the interface name and the ip scope that will be
191 # used for the particular interface. The a, b, c, d
192 # variables below are the octets for the ip address. The
193 # third octet is then incremented for each interface that
194 # is requested. This part is designed to bring up the
195 # hostapd interfaces and not the DHCP servers for each
196 # interface.
197 dhcp_bss = {}
198 counter = 1
199 for bss in hostapd_config.bss_lookup:
Bindu Mahadev2ef0c3e2017-07-13 15:39:55 -0700200 if interface_mac_orig:
201 hostapd_config.bss_lookup[
202 bss].bssid = interface_mac_orig.stdout[:-1] + str(
203 counter)
Joe Brennan88279af2017-02-15 18:11:53 -0800204 self._route_cmd.clear_routes(net_interface=str(bss))
205 if interface is _AP_2GHZ_INTERFACE:
206 starting_ip_range = _AP_2GHZ_SUBNET_STR
207 else:
208 starting_ip_range = _AP_5GHZ_SUBNET_STR
209 a, b, c, d = starting_ip_range.split('.')
210 dhcp_bss[bss] = dhcp_config.Subnet(
211 ipaddress.ip_network('%s.%s.%s.%s' % (a, b, str(
212 int(c) + counter), d)))
213 counter = counter + 1
214
215 apd.start(hostapd_config, additional_parameters=additional_parameters)
216
217 # The DHCP serer requires interfaces to have ips and routes before
218 # the server will come up.
Christopher Wileyede81f02016-11-01 09:15:30 -0700219 interface_ip = ipaddress.ip_interface(
220 '%s/%s' % (subnet.router, subnet.network.netmask))
221 self._ip_cmd.set_ipv4_address(interface, interface_ip)
Joe Brennan88279af2017-02-15 18:11:53 -0800222 if hostapd_config.bss_lookup:
223 # This loop goes through each interface that was setup for
224 # hostapd and assigns the DHCP scopes that were defined but
225 # not used during the hostapd loop above. The k and v
226 # variables represent the interface name, k, and dhcp info, v.
227 for k, v in dhcp_bss.items():
228 bss_interface_ip = ipaddress.ip_interface(
markdr85a5e1a2017-07-17 14:11:34 -0700229 '%s/%s' % (dhcp_bss[k].router,
230 dhcp_bss[k].network.netmask))
Joe Brennan88279af2017-02-15 18:11:53 -0800231 self._ip_cmd.set_ipv4_address(str(k), bss_interface_ip)
Ang Li73697b32015-12-03 00:41:53 +0000232
Christopher Wileyede81f02016-11-01 09:15:30 -0700233 # Restart the DHCP server with our updated list of subnets.
Joe Brennan88279af2017-02-15 18:11:53 -0800234 configured_subnets = [x.subnet for x in self._aps.values()]
235 if hostapd_config.bss_lookup:
236 for k, v in dhcp_bss.items():
237 configured_subnets.append(v)
238
239 self._dhcp.start(config=dhcp_config.DhcpConfig(configured_subnets))
Benny Peake85f112a2016-10-07 19:41:49 -0700240
Christopher Wileyede81f02016-11-01 09:15:30 -0700241 return interface
Benny Peake85f112a2016-10-07 19:41:49 -0700242
Joe Brennan3d00b2a2017-04-13 14:27:05 -0700243 def get_bssid_from_ssid(self, ssid):
244 """Gets the BSSID from a provided SSID
245
246 Args:
247 ssid: An SSID string
Bindu Mahadev9bc52e62017-06-28 17:01:12 -0700248 Returns: The BSSID if on the AP or None if SSID could not be found.
Joe Brennan3d00b2a2017-04-13 14:27:05 -0700249 """
250
Bindu Mahadev9bc52e62017-06-28 17:01:12 -0700251 interfaces = [_AP_2GHZ_INTERFACE, _AP_5GHZ_INTERFACE, ssid]
252 # Get the interface name associated with the given ssid.
253 for interface in interfaces:
markdr85a5e1a2017-07-17 14:11:34 -0700254 cmd = "iw dev %s info|grep ssid|awk -F' ' '{print $2}'" % (
Bindu Mahadev9bc52e62017-06-28 17:01:12 -0700255 str(interface))
256 iw_output = self.ssh.run(cmd)
257 if 'command failed: No such device' in iw_output.stderr:
258 continue
259 else:
260 # If the configured ssid is equal to the given ssid, we found
261 # the right interface.
262 if iw_output.stdout == ssid:
markdr85a5e1a2017-07-17 14:11:34 -0700263 cmd = "iw dev %s info|grep addr|awk -F' ' '{print $2}'" % (
Bindu Mahadev9bc52e62017-06-28 17:01:12 -0700264 str(interface))
265 iw_output = self.ssh.run(cmd)
266 return iw_output.stdout
267 return None
Joe Brennan3d00b2a2017-04-13 14:27:05 -0700268
Benny Peake85f112a2016-10-07 19:41:49 -0700269 def stop_ap(self, identifier):
270 """Stops a running ap on this controller.
Ang Li73697b32015-12-03 00:41:53 +0000271
272 Args:
Benny Peake85f112a2016-10-07 19:41:49 -0700273 identifier: The identify of the ap that should be taken down.
Ang Li73697b32015-12-03 00:41:53 +0000274 """
Joe Brennan88279af2017-02-15 18:11:53 -0800275
Qi Jiang293bc992017-09-14 17:15:20 -0700276 if identifier not in list(self._aps.keys()):
Benny Peake85f112a2016-10-07 19:41:49 -0700277 raise ValueError('Invalid identifer %s given' % identifier)
Ang Li73697b32015-12-03 00:41:53 +0000278
Joe Brennan88279af2017-02-15 18:11:53 -0800279 instance = self._aps.get(identifier)
Ang Li73697b32015-12-03 00:41:53 +0000280
Christopher Wileyede81f02016-11-01 09:15:30 -0700281 instance.hostapd.stop()
282 self._dhcp.stop()
Benny Peake85f112a2016-10-07 19:41:49 -0700283 self._ip_cmd.clear_ipv4_addresses(identifier)
Ang Li73697b32015-12-03 00:41:53 +0000284
Benny Peake85f112a2016-10-07 19:41:49 -0700285 # DHCP server needs to refresh in order to tear down the subnet no
286 # longer being used. In the event that all interfaces are torn down
287 # then an exception gets thrown. We need to catch this exception and
288 # check that all interfaces should actually be down.
Joe Brennan88279af2017-02-15 18:11:53 -0800289 configured_subnets = [x.subnet for x in self._aps.values()]
Qi Jiangaa818ba2017-09-16 17:09:47 -0700290 del self._aps[identifier]
Christopher Wileyede81f02016-11-01 09:15:30 -0700291 if configured_subnets:
292 self._dhcp.start(dhcp_config.DhcpConfig(configured_subnets))
Benny Peake85f112a2016-10-07 19:41:49 -0700293
294 def stop_all_aps(self):
295 """Stops all running aps on this device."""
Joe Brennan88279af2017-02-15 18:11:53 -0800296
Benny Peake7336fa42017-08-17 19:30:35 -0700297 for ap in list(self._aps.keys()):
Joe Brennan88279af2017-02-15 18:11:53 -0800298 try:
299 self.stop_ap(ap)
300 except dhcp_server.NoInterfaceError as e:
301 pass
Benny Peake85f112a2016-10-07 19:41:49 -0700302
303 def close(self):
304 """Called to take down the entire access point.
305
306 When called will stop all aps running on this host, shutdown the dhcp
307 server, and stop the ssh conneciton.
Ang Li73697b32015-12-03 00:41:53 +0000308 """
Joe Brennan88279af2017-02-15 18:11:53 -0800309
310 if self._aps:
311 self.stop_all_aps()
markdr85a5e1a2017-07-17 14:11:34 -0700312 self.ssh.close()
Qi Jiangc3e87852017-10-04 22:45:52 -0700313
314 def generate_bridge_configs(self, channel, iface_lan=LAN_INTERFACE):
315 """Generate a list of configs for a bridge between LAN and WLAN.
316
317 Args:
318 channel: the channel WLAN interface is brought up on
319 iface_lan: the LAN interface to bridge
320 Returns:
321 configs: tuple containing iface_wlan, iface_lan and bridge_ip
322 """
323
324 if channel < 15:
325 iface_wlan = _AP_2GHZ_INTERFACE
326 subnet_str = _AP_2GHZ_SUBNET_STR
327 else:
328 iface_wlan = _AP_5GHZ_INTERFACE
329 subnet_str = _AP_5GHZ_SUBNET_STR
330
331 iface_lan = iface_lan
332
333 a, b, c, d = subnet_str.strip('/24').split('.')
334 bridge_ip = "%s.%s.%s.%s" % (a, b, c, BRIDGE_IP_LAST)
335
336 configs = (iface_wlan, iface_lan, bridge_ip)
337
338 return configs