tturney | 1bdf77d | 2015-12-28 17:46:13 -0800 | [diff] [blame] | 1 | #!/usr/bin/env python3.4 |
Ang Li | 73697b3 | 2015-12-03 00:41:53 +0000 | [diff] [blame] | 2 | # |
tturney | 1bdf77d | 2015-12-28 17:46:13 -0800 | [diff] [blame] | 3 | # Copyright 2016 - Google, Inc. |
Ang Li | 73697b3 | 2015-12-03 00:41:53 +0000 | [diff] [blame] | 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 | |
Christopher Wiley | ede81f0 | 2016-11-01 09:15:30 -0700 | [diff] [blame] | 17 | import collections |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 18 | import ipaddress |
| 19 | import logging |
Ang Li | 73697b3 | 2015-12-03 00:41:53 +0000 | [diff] [blame] | 20 | |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 21 | from acts.controllers.ap_lib import dhcp_config |
| 22 | from acts.controllers.ap_lib import dhcp_server |
| 23 | from acts.controllers.ap_lib import hostapd |
| 24 | from acts.controllers.ap_lib import hostapd_config |
| 25 | from acts.controllers.utils_lib.commands import ip |
| 26 | from acts.controllers.utils_lib.commands import route |
Joe Brennan | 88279af | 2017-02-15 18:11:53 -0800 | [diff] [blame^] | 27 | from acts.controllers.utils_lib.commands import shell |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 28 | from acts.controllers.utils_lib.ssh import connection |
| 29 | from acts.controllers.utils_lib.ssh import settings |
| 30 | |
| 31 | ACTS_CONTROLLER_CONFIG_NAME = 'AccessPoint' |
Benny Peake | 0f5049b | 2016-10-28 15:57:01 -0700 | [diff] [blame] | 32 | ACTS_CONTROLLER_REFERENCE_NAME = 'access_points' |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 33 | |
Ang Li | 51df045 | 2016-06-07 10:58:16 -0700 | [diff] [blame] | 34 | |
Ang Li | a11f2cf | 2016-05-05 11:54:40 -0700 | [diff] [blame] | 35 | def create(configs): |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 36 | """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 Brennan | 88279af | 2017-02-15 18:11:53 -0800 | [diff] [blame^] | 48 | return [ |
| 49 | AccessPoint(settings.from_config(c['ssh_config'])) for c in configs |
| 50 | ] |
Ang Li | 73697b3 | 2015-12-03 00:41:53 +0000 | [diff] [blame] | 51 | |
Ang Li | 51df045 | 2016-06-07 10:58:16 -0700 | [diff] [blame] | 52 | |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 53 | def destroy(aps): |
| 54 | """Destroys a list of access points. |
Ang Li | 73697b3 | 2015-12-03 00:41:53 +0000 | [diff] [blame] | 55 | |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 56 | Args: |
| 57 | aps: The list of access points to destroy. |
Ang Li | 73697b3 | 2015-12-03 00:41:53 +0000 | [diff] [blame] | 58 | """ |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 59 | for ap in aps: |
| 60 | ap.close() |
Ang Li | 73697b3 | 2015-12-03 00:41:53 +0000 | [diff] [blame] | 61 | |
Ang Li | 73697b3 | 2015-12-03 00:41:53 +0000 | [diff] [blame] | 62 | |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 63 | def get_info(aps): |
| 64 | """Get information on a list of access points. |
Ang Li | 73697b3 | 2015-12-03 00:41:53 +0000 | [diff] [blame] | 65 | |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 66 | Args: |
| 67 | aps: A list of AccessPoints. |
| 68 | |
| 69 | Returns: |
| 70 | A list of all aps hostname. |
| 71 | """ |
Christopher Wiley | 7dae1d9 | 2016-10-31 14:47:58 -0700 | [diff] [blame] | 72 | return [ap.ssh_settings.hostname for ap in aps] |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 73 | |
| 74 | |
| 75 | class Error(Exception): |
| 76 | """Error raised when there is a problem with the access point.""" |
| 77 | |
| 78 | |
Joe Brennan | 88279af | 2017-02-15 18:11:53 -0800 | [diff] [blame^] | 79 | _ApInstance = collections.namedtuple('_ApInstance', ['hostapd', 'subnet']) |
Christopher Wiley | ede81f0 | 2016-11-01 09:15:30 -0700 | [diff] [blame] | 80 | |
| 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 Brennan | 88279af | 2017-02-15 18:11:53 -0800 | [diff] [blame^] | 86 | # 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 Wiley | ede81f0 | 2016-11-01 09:15:30 -0700 | [diff] [blame] | 93 | |
| 94 | |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 95 | class AccessPoint(object): |
| 96 | """An access point controller. |
| 97 | |
| 98 | Attributes: |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 99 | 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 Wiley | 7dae1d9 | 2016-10-31 14:47:58 -0700 | [diff] [blame] | 104 | def __init__(self, ssh_settings): |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 105 | """ |
| 106 | Args: |
Christopher Wiley | 7dae1d9 | 2016-10-31 14:47:58 -0700 | [diff] [blame] | 107 | ssh_settings: acts.controllers.utils_lib.ssh.SshSettings instance. |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 108 | """ |
Christopher Wiley | 7dae1d9 | 2016-10-31 14:47:58 -0700 | [diff] [blame] | 109 | self.ssh_settings = ssh_settings |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 110 | self.ssh = connection.SshConnection(self.ssh_settings) |
| 111 | |
Christopher Wiley | ede81f0 | 2016-11-01 09:15:30 -0700 | [diff] [blame] | 112 | # Singleton utilities for running various commands. |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 113 | self._ip_cmd = ip.LinuxIpCommand(self.ssh) |
| 114 | self._route_cmd = route.LinuxRouteCommand(self.ssh) |
| 115 | |
Christopher Wiley | ede81f0 | 2016-11-01 09:15:30 -0700 | [diff] [blame] | 116 | # A map from network interface name to _ApInstance objects representing |
| 117 | # the hostapd instance running against the interface. |
| 118 | self._aps = dict() |
| 119 | |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 120 | def __del__(self): |
| 121 | self.close() |
| 122 | |
Joe Brennan | 88279af | 2017-02-15 18:11:53 -0800 | [diff] [blame^] | 123 | def start_ap(self, hostapd_config, additional_parameters=None): |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 124 | """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 Li | 73697b3 | 2015-12-03 00:41:53 +0000 | [diff] [blame] | 131 | |
| 132 | Args: |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 133 | hostapd_config: hostapd_config.HostapdConfig, The configurations |
| 134 | to use when starting up the ap. |
Joe Brennan | 88279af | 2017-02-15 18:11:53 -0800 | [diff] [blame^] | 135 | 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 Li | 73697b3 | 2015-12-03 00:41:53 +0000 | [diff] [blame] | 139 | |
| 140 | Returns: |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 141 | An identifier for the ap being run. This identifier can be used |
| 142 | later by this controller to control the ap. |
Ang Li | 73697b3 | 2015-12-03 00:41:53 +0000 | [diff] [blame] | 143 | |
| 144 | Raises: |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 145 | Error: When the ap can't be brought up. |
Ang Li | 73697b3 | 2015-12-03 00:41:53 +0000 | [diff] [blame] | 146 | """ |
Christopher Wiley | ede81f0 | 2016-11-01 09:15:30 -0700 | [diff] [blame] | 147 | # 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 Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 151 | if hostapd_config.frequency < 5000: |
Christopher Wiley | ede81f0 | 2016-11-01 09:15:30 -0700 | [diff] [blame] | 152 | interface = _AP_2GHZ_INTERFACE |
| 153 | subnet = _AP_2GHZ_SUBNET |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 154 | else: |
Christopher Wiley | ede81f0 | 2016-11-01 09:15:30 -0700 | [diff] [blame] | 155 | interface = _AP_5GHZ_INTERFACE |
| 156 | subnet = _AP_5GHZ_SUBNET |
| 157 | |
Joe Brennan | 88279af | 2017-02-15 18:11:53 -0800 | [diff] [blame^] | 158 | # 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 Wiley | ede81f0 | 2016-11-01 09:15:30 -0700 | [diff] [blame] | 177 | 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 Brennan | 88279af | 2017-02-15 18:11:53 -0800 | [diff] [blame^] | 182 | new_instance = _ApInstance(hostapd=apd, subnet=subnet) |
Christopher Wiley | ede81f0 | 2016-11-01 09:15:30 -0700 | [diff] [blame] | 183 | 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 Brennan | 88279af | 2017-02-15 18:11:53 -0800 | [diff] [blame^] | 190 | 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 Wiley | ede81f0 | 2016-11-01 09:15:30 -0700 | [diff] [blame] | 217 | interface_ip = ipaddress.ip_interface( |
| 218 | '%s/%s' % (subnet.router, subnet.network.netmask)) |
| 219 | self._ip_cmd.set_ipv4_address(interface, interface_ip) |
Joe Brennan | 88279af | 2017-02-15 18:11:53 -0800 | [diff] [blame^] | 220 | 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 Li | 73697b3 | 2015-12-03 00:41:53 +0000 | [diff] [blame] | 230 | |
Christopher Wiley | ede81f0 | 2016-11-01 09:15:30 -0700 | [diff] [blame] | 231 | # Restart the DHCP server with our updated list of subnets. |
Joe Brennan | 88279af | 2017-02-15 18:11:53 -0800 | [diff] [blame^] | 232 | 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 Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 238 | |
Christopher Wiley | ede81f0 | 2016-11-01 09:15:30 -0700 | [diff] [blame] | 239 | return interface |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 240 | |
| 241 | def stop_ap(self, identifier): |
| 242 | """Stops a running ap on this controller. |
Ang Li | 73697b3 | 2015-12-03 00:41:53 +0000 | [diff] [blame] | 243 | |
| 244 | Args: |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 245 | identifier: The identify of the ap that should be taken down. |
Ang Li | 73697b3 | 2015-12-03 00:41:53 +0000 | [diff] [blame] | 246 | """ |
Joe Brennan | 88279af | 2017-02-15 18:11:53 -0800 | [diff] [blame^] | 247 | |
Christopher Wiley | ede81f0 | 2016-11-01 09:15:30 -0700 | [diff] [blame] | 248 | if identifier not in self._aps: |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 249 | raise ValueError('Invalid identifer %s given' % identifier) |
Ang Li | 73697b3 | 2015-12-03 00:41:53 +0000 | [diff] [blame] | 250 | |
Joe Brennan | 88279af | 2017-02-15 18:11:53 -0800 | [diff] [blame^] | 251 | instance = self._aps.get(identifier) |
Ang Li | 73697b3 | 2015-12-03 00:41:53 +0000 | [diff] [blame] | 252 | |
Christopher Wiley | ede81f0 | 2016-11-01 09:15:30 -0700 | [diff] [blame] | 253 | instance.hostapd.stop() |
| 254 | self._dhcp.stop() |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 255 | self._ip_cmd.clear_ipv4_addresses(identifier) |
Ang Li | 73697b3 | 2015-12-03 00:41:53 +0000 | [diff] [blame] | 256 | |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 257 | # 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 Brennan | 88279af | 2017-02-15 18:11:53 -0800 | [diff] [blame^] | 261 | configured_subnets = [x.subnet for x in self._aps.values()] |
Christopher Wiley | ede81f0 | 2016-11-01 09:15:30 -0700 | [diff] [blame] | 262 | if configured_subnets: |
| 263 | self._dhcp.start(dhcp_config.DhcpConfig(configured_subnets)) |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 264 | |
| 265 | def stop_all_aps(self): |
| 266 | """Stops all running aps on this device.""" |
Joe Brennan | 88279af | 2017-02-15 18:11:53 -0800 | [diff] [blame^] | 267 | |
| 268 | for ap in self._aps.keys(): |
| 269 | try: |
| 270 | self.stop_ap(ap) |
| 271 | except dhcp_server.NoInterfaceError as e: |
| 272 | pass |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 273 | |
| 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 Li | 73697b3 | 2015-12-03 00:41:53 +0000 | [diff] [blame] | 279 | """ |
Joe Brennan | 88279af | 2017-02-15 18:11:53 -0800 | [diff] [blame^] | 280 | |
| 281 | if self._aps: |
| 282 | self.stop_all_aps() |
| 283 | self._dhcp.stop() |
Ang Li | 73697b3 | 2015-12-03 00:41:53 +0000 | [diff] [blame] | 284 | |
Benny Peake | 85f112a | 2016-10-07 19:41:49 -0700 | [diff] [blame] | 285 | self.ssh.close() |