blob: d517b10c1526f633da01710f52f4c6d470cb3f00 [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
19import logging
Ang Li73697b32015-12-03 00:41:53 +000020
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))
Christopher Wileyede81f02016-11-01 09:15:30 -070093
94
Benny Peake85f112a2016-10-07 19:41:49 -070095class AccessPoint(object):
96 """An access point controller.
97
98 Attributes:
Benny Peake85f112a2016-10-07 19:41:49 -070099 ssh: The ssh connection to this ap.
100 ssh_settings: The ssh settings being used by the ssh conneciton.
101 dhcp_settings: The dhcp server settings being used.
102 """
103
Christopher Wiley7dae1d92016-10-31 14:47:58 -0700104 def __init__(self, ssh_settings):
Benny Peake85f112a2016-10-07 19:41:49 -0700105 """
106 Args:
Christopher Wiley7dae1d92016-10-31 14:47:58 -0700107 ssh_settings: acts.controllers.utils_lib.ssh.SshSettings instance.
Benny Peake85f112a2016-10-07 19:41:49 -0700108 """
Christopher Wiley7dae1d92016-10-31 14:47:58 -0700109 self.ssh_settings = ssh_settings
Benny Peake85f112a2016-10-07 19:41:49 -0700110 self.ssh = connection.SshConnection(self.ssh_settings)
111
Christopher Wileyede81f02016-11-01 09:15:30 -0700112 # Singleton utilities for running various commands.
Benny Peake85f112a2016-10-07 19:41:49 -0700113 self._ip_cmd = ip.LinuxIpCommand(self.ssh)
114 self._route_cmd = route.LinuxRouteCommand(self.ssh)
115
Christopher Wileyede81f02016-11-01 09:15:30 -0700116 # A map from network interface name to _ApInstance objects representing
117 # the hostapd instance running against the interface.
118 self._aps = dict()
119
Benny Peake85f112a2016-10-07 19:41:49 -0700120 def __del__(self):
121 self.close()
122
Joe Brennan88279af2017-02-15 18:11:53 -0800123 def start_ap(self, hostapd_config, additional_parameters=None):
Benny Peake85f112a2016-10-07 19:41:49 -0700124 """Starts as an ap using a set of configurations.
125
126 This will start an ap on this host. To start an ap the controller
127 selects a network interface to use based on the configs given. It then
128 will start up hostapd on that interface. Next a subnet is created for
129 the network interface and dhcp server is refreshed to give out ips
130 for that subnet for any device that connects through that interface.
Ang Li73697b32015-12-03 00:41:53 +0000131
132 Args:
Benny Peake85f112a2016-10-07 19:41:49 -0700133 hostapd_config: hostapd_config.HostapdConfig, The configurations
134 to use when starting up the ap.
Joe Brennan88279af2017-02-15 18:11:53 -0800135 additional_parameters: A dicitonary of parameters that can sent
136 directly into the hostapd config file. This
137 can be used for debugging and or adding one
138 off parameters into the config.
Ang Li73697b32015-12-03 00:41:53 +0000139
140 Returns:
Benny Peake85f112a2016-10-07 19:41:49 -0700141 An identifier for the ap being run. This identifier can be used
142 later by this controller to control the ap.
Ang Li73697b32015-12-03 00:41:53 +0000143
144 Raises:
Benny Peake85f112a2016-10-07 19:41:49 -0700145 Error: When the ap can't be brought up.
Ang Li73697b32015-12-03 00:41:53 +0000146 """
Christopher Wileyede81f02016-11-01 09:15:30 -0700147 # Right now, we hardcode that a frequency maps to a particular
148 # network interface. This is true of the hardware we're running
149 # against right now, but in general, we'll want to do some
150 # dynamic discovery of interface capabilities. See b/32582843
Benny Peake85f112a2016-10-07 19:41:49 -0700151 if hostapd_config.frequency < 5000:
Christopher Wileyede81f02016-11-01 09:15:30 -0700152 interface = _AP_2GHZ_INTERFACE
153 subnet = _AP_2GHZ_SUBNET
Benny Peake85f112a2016-10-07 19:41:49 -0700154 else:
Christopher Wileyede81f02016-11-01 09:15:30 -0700155 interface = _AP_5GHZ_INTERFACE
156 subnet = _AP_5GHZ_SUBNET
157
Joe Brennan88279af2017-02-15 18:11:53 -0800158 # In order to handle dhcp servers on any interface, the initiation of
159 # the dhcp server must be done after the wlan interfaces are figured
160 # out as opposed to being in __init__
161 self._dhcp = dhcp_server.DhcpServer(self.ssh, interface=interface)
162
163 # For multi bssid configurations the mac address
164 # of the wireless interface needs to have enough space to mask out
165 # up to 8 different mac addresses. The easiest way to do this
166 # is to set the last byte to 0. While technically this could
167 # cause a duplicate mac address it is unlikely and will allow for
168 # one radio to have up to 8 APs on the interface. The check ensures
169 # backwards compatibility since if someone has set the bssid on purpose
170 # the bssid will not be changed from what the user set.
171 if not hostapd_config.bssid:
172 cmd = "ifconfig %s|grep ether|awk -F' ' '{print $2}'" % interface
173 interface_mac = self.ssh.run(cmd)
174 interface_mac = interface_mac.stdout[:-1] + '0'
175 hostapd_config.bssid = interface_mac
176
Christopher Wileyede81f02016-11-01 09:15:30 -0700177 if interface in self._aps:
178 raise ValueError('No WiFi interface available for AP on '
179 'channel %d' % hostapd_config.channel)
180
181 apd = hostapd.Hostapd(self.ssh, interface)
Joe Brennan88279af2017-02-15 18:11:53 -0800182 new_instance = _ApInstance(hostapd=apd, subnet=subnet)
Christopher Wileyede81f02016-11-01 09:15:30 -0700183 self._aps[interface] = new_instance
184
185 # Turn off the DHCP server, we're going to change its settings.
186 self._dhcp.stop()
187 # Clear all routes to prevent old routes from interfering.
188 self._route_cmd.clear_routes(net_interface=interface)
189
Joe Brennan88279af2017-02-15 18:11:53 -0800190 if hostapd_config.bss_lookup:
191 # The dhcp_bss dictionary is created to hold the key/value
192 # pair of the interface name and the ip scope that will be
193 # used for the particular interface. The a, b, c, d
194 # variables below are the octets for the ip address. The
195 # third octet is then incremented for each interface that
196 # is requested. This part is designed to bring up the
197 # hostapd interfaces and not the DHCP servers for each
198 # interface.
199 dhcp_bss = {}
200 counter = 1
201 for bss in hostapd_config.bss_lookup:
202 self._route_cmd.clear_routes(net_interface=str(bss))
203 if interface is _AP_2GHZ_INTERFACE:
204 starting_ip_range = _AP_2GHZ_SUBNET_STR
205 else:
206 starting_ip_range = _AP_5GHZ_SUBNET_STR
207 a, b, c, d = starting_ip_range.split('.')
208 dhcp_bss[bss] = dhcp_config.Subnet(
209 ipaddress.ip_network('%s.%s.%s.%s' % (a, b, str(
210 int(c) + counter), d)))
211 counter = counter + 1
212
213 apd.start(hostapd_config, additional_parameters=additional_parameters)
214
215 # The DHCP serer requires interfaces to have ips and routes before
216 # the server will come up.
Christopher Wileyede81f02016-11-01 09:15:30 -0700217 interface_ip = ipaddress.ip_interface(
218 '%s/%s' % (subnet.router, subnet.network.netmask))
219 self._ip_cmd.set_ipv4_address(interface, interface_ip)
Joe Brennan88279af2017-02-15 18:11:53 -0800220 if hostapd_config.bss_lookup:
221 # This loop goes through each interface that was setup for
222 # hostapd and assigns the DHCP scopes that were defined but
223 # not used during the hostapd loop above. The k and v
224 # variables represent the interface name, k, and dhcp info, v.
225 for k, v in dhcp_bss.items():
226 bss_interface_ip = ipaddress.ip_interface(
227 '%s/%s' %
228 (dhcp_bss[k].router, dhcp_bss[k].network.netmask))
229 self._ip_cmd.set_ipv4_address(str(k), bss_interface_ip)
Ang Li73697b32015-12-03 00:41:53 +0000230
Christopher Wileyede81f02016-11-01 09:15:30 -0700231 # Restart the DHCP server with our updated list of subnets.
Joe Brennan88279af2017-02-15 18:11:53 -0800232 configured_subnets = [x.subnet for x in self._aps.values()]
233 if hostapd_config.bss_lookup:
234 for k, v in dhcp_bss.items():
235 configured_subnets.append(v)
236
237 self._dhcp.start(config=dhcp_config.DhcpConfig(configured_subnets))
Benny Peake85f112a2016-10-07 19:41:49 -0700238
Christopher Wileyede81f02016-11-01 09:15:30 -0700239 return interface
Benny Peake85f112a2016-10-07 19:41:49 -0700240
241 def stop_ap(self, identifier):
242 """Stops a running ap on this controller.
Ang Li73697b32015-12-03 00:41:53 +0000243
244 Args:
Benny Peake85f112a2016-10-07 19:41:49 -0700245 identifier: The identify of the ap that should be taken down.
Ang Li73697b32015-12-03 00:41:53 +0000246 """
Joe Brennan88279af2017-02-15 18:11:53 -0800247
Christopher Wileyede81f02016-11-01 09:15:30 -0700248 if identifier not in self._aps:
Benny Peake85f112a2016-10-07 19:41:49 -0700249 raise ValueError('Invalid identifer %s given' % identifier)
Ang Li73697b32015-12-03 00:41:53 +0000250
Joe Brennan88279af2017-02-15 18:11:53 -0800251 instance = self._aps.get(identifier)
Ang Li73697b32015-12-03 00:41:53 +0000252
Christopher Wileyede81f02016-11-01 09:15:30 -0700253 instance.hostapd.stop()
254 self._dhcp.stop()
Benny Peake85f112a2016-10-07 19:41:49 -0700255 self._ip_cmd.clear_ipv4_addresses(identifier)
Ang Li73697b32015-12-03 00:41:53 +0000256
Benny Peake85f112a2016-10-07 19:41:49 -0700257 # DHCP server needs to refresh in order to tear down the subnet no
258 # longer being used. In the event that all interfaces are torn down
259 # then an exception gets thrown. We need to catch this exception and
260 # check that all interfaces should actually be down.
Joe Brennan88279af2017-02-15 18:11:53 -0800261 configured_subnets = [x.subnet for x in self._aps.values()]
Christopher Wileyede81f02016-11-01 09:15:30 -0700262 if configured_subnets:
263 self._dhcp.start(dhcp_config.DhcpConfig(configured_subnets))
Benny Peake85f112a2016-10-07 19:41:49 -0700264
265 def stop_all_aps(self):
266 """Stops all running aps on this device."""
Joe Brennan88279af2017-02-15 18:11:53 -0800267
268 for ap in self._aps.keys():
269 try:
270 self.stop_ap(ap)
271 except dhcp_server.NoInterfaceError as e:
272 pass
Benny Peake85f112a2016-10-07 19:41:49 -0700273
274 def close(self):
275 """Called to take down the entire access point.
276
277 When called will stop all aps running on this host, shutdown the dhcp
278 server, and stop the ssh conneciton.
Ang Li73697b32015-12-03 00:41:53 +0000279 """
Joe Brennan88279af2017-02-15 18:11:53 -0800280
281 if self._aps:
282 self.stop_all_aps()
283 self._dhcp.stop()
Ang Li73697b32015-12-03 00:41:53 +0000284
Benny Peake85f112a2016-10-07 19:41:49 -0700285 self.ssh.close()