1#!/usr/bin/env python3.4 2# 3# Copyright 2016 - Google, Inc. 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 17import acts.jsonrpc as jsonrpc 18from acts.test_utils.wifi.wifi_test_utils import WifiEnums 19 20ACTS_CONTROLLER_CONFIG_NAME = "AP" 21ACTS_CONTROLLER_REFERENCE_NAME = "access_points" 22 23def create(configs, logger): 24 results = [] 25 for c in configs: 26 addr = c[Config.key_address.value] 27 port = 80 28 if Config.key_port.value in c: 29 port = c[Config.key_port.value] 30 results.append(AP(addr, port)) 31 return results 32 33def destroy(objs): 34 return 35 36class ServerError(Exception): 37 pass 38 39class ClientError(Exception): 40 pass 41 42""" 43Controller for OpenWRT routers. 44""" 45class AP(): 46 """Interface to OpenWRT using the LuCI interface. 47 48 Works via JSON-RPC over HTTP. A generic access method is provided, as well 49 as more specialized methods. 50 51 Can also call LuCI methods generically: 52 53 ap_instance.sys.loadavg() 54 ap_instance.sys.dmesg() 55 ap_instance.fs.stat("/etc/hosts") 56 """ 57 IFACE_DEFAULTS = {"mode": "ap", "disabled": "0", 58 "encryption": "psk2", "network": "lan"} 59 RADIO_DEFAULTS = {"disabled": "0"} 60 61 def __init__(self, addr, port=80): 62 self._client = jsonrpc.JSONRPCClient( 63 "http://""{}:{}/cgi-bin/luci/rpc/".format(addr, port)) 64 self.RADIO_NAMES = [] 65 keys = self._client.get_all("wireless").keys() 66 if "radio0" in keys: 67 self.RADIO_NAMES.append("radio0") 68 if "radio1" in keys: 69 self.RADIO_NAMES.append("radio1") 70 71 def section_id_lookup(self, cfg_name, key, value): 72 """Looks up the section id of a section. 73 74 Finds the section ids of the sections that have the specified key:value 75 pair in them. 76 77 Args: 78 cfg_name: Name of the configuration file to look in. 79 key: Key of the pair. 80 value: Value of the pair. 81 82 Returns: 83 A list of the section ids found. 84 """ 85 section_ids = [] 86 sections = self._client.get_all(cfg_name) 87 for section_id, section_cfg in sections.items(): 88 if key in section_cfg and section_cfg[key] == value: 89 section_ids.append(section_id) 90 return section_ids 91 92 def _section_option_lookup(self, cfg_name, conditions, *target_keys): 93 """Looks up values of options in sections that match the conditions. 94 95 To match a condition, a section needs to have all the key:value pairs 96 specified in conditions. 97 98 Args: 99 cfg_name: Name of the configuration file to look in. 100 key: Key of the pair. 101 value: Value of the pair. 102 target_key: Key of the options we want to retrieve values from. 103 104 Returns: 105 A list of the values found. 106 """ 107 results = [] 108 sections = self._client.get_all(cfg_name) 109 for section_cfg in sections.values(): 110 if self._match_conditions(conditions, section_cfg): 111 r = {} 112 for k in target_keys: 113 if k not in section_cfg: 114 break 115 r[k] = section_cfg[k] 116 if r: 117 results.append(r) 118 return results 119 120 @staticmethod 121 def _match_conditions(conds, cfg): 122 for cond in conds: 123 key, value = cond 124 if key not in cfg or cfg[key] != value: 125 return False 126 return True 127 128 def run(self, *cmd): 129 """Executes a terminal command on the AP. 130 131 Args: 132 cmd: A tuple of command strings. 133 134 Returns: 135 The terminal output of the command. 136 """ 137 return self._client.sys("exec", *cmd) 138 139 def apply_configs(self, ap_config): 140 """Applies configurations to the access point. 141 142 Reads the configuration file, adds wifi interfaces, and sets parameters 143 based on the configuration file. 144 145 Args: 146 ap_config: A dict containing the configurations for the AP. 147 """ 148 self.reset() 149 for k, v in ap_config.items(): 150 if "radio" in k: 151 self._apply_radio_configs(k, v) 152 if "network" in k: 153 # TODO(angli) Implement this. 154 pass 155 self.apply_wifi_changes() 156 157 def _apply_radio_configs(self, radio_id, radio_config): 158 """Applies conigurations on a radio of the AP. 159 160 Sets the options in the radio config. 161 Adds wifi-ifaces to this radio based on the configurations. 162 """ 163 for k, v in radio_config.items(): 164 if k == "settings": 165 self._set_options('wireless', radio_id, v, 166 self.RADIO_DEFAULTS) 167 if k == "wifi-iface": 168 for cfg in v: 169 cfg["device"] = radio_id 170 self._add_ifaces(v) 171 172 def reset(self): 173 """Resets the AP to a clean state. 174 175 Deletes all wifi-ifaces. 176 Enable all the radios. 177 """ 178 sections = self._client.get_all("wireless") 179 to_be_deleted = [] 180 for section_id in sections.keys(): 181 if section_id not in self.RADIO_NAMES: 182 to_be_deleted.append(section_id) 183 self.delete_ifaces_by_ids(to_be_deleted) 184 for r in self.RADIO_NAMES: 185 self.toggle_radio_state(r, True) 186 187 def toggle_radio_state(self, radio_name, state=None): 188 """Toggles the state of a radio. 189 190 If input state is None, toggle the state of the radio. 191 Otherwise, set the radio's state to input state. 192 State True is equivalent to 'disabled':'0' 193 194 Args: 195 radio_name: Name of the radio to change state. 196 state: State to set to, default is None. 197 198 Raises: 199 ClientError: If the radio specified does not exist on the AP. 200 """ 201 if radio_name not in self.RADIO_NAMES: 202 raise ClientError("Trying to change none-existent radio's state") 203 cur_state = self._client.get("wireless", radio_name, "disabled") 204 cur_state = True if cur_state=='0' else False 205 if state == cur_state: 206 return 207 new_state = '1' if cur_state else '0' 208 self._set_option("wireless", radio_name, "disabled", new_state) 209 self.apply_wifi_changes() 210 return 211 212 def set_ssid_state(self, ssid, state): 213 """Sets the state of ssid (turns on/off). 214 215 Args: 216 ssid: The ssid whose state is being changed. 217 state: State to set the ssid to. Enable the ssid if True, disable 218 otherwise. 219 """ 220 new_state = '0' if state else '1' 221 section_ids = self.section_id_lookup("wireless", "ssid", ssid) 222 for s_id in section_ids: 223 self._set_option("wireless", s_id, "disabled", new_state) 224 225 def get_ssids(self, conds): 226 """Gets all the ssids that match the conditions. 227 228 Params: 229 conds: An iterable of tuples, each representing a key:value pair 230 an ssid must have to be included. 231 232 Returns: 233 A list of ssids that contain all the specified key:value pairs. 234 """ 235 results = [] 236 for s in self._section_option_lookup("wireless", conds, "ssid"): 237 results.append(s["ssid"]) 238 return results 239 240 def get_active_ssids(self): 241 """Gets the ssids that are currently not disabled. 242 243 Returns: 244 A list of ssids that are currently active. 245 """ 246 conds = (("disabled", "0"),) 247 return self.get_ssids(conds) 248 249 def get_active_ssids_info(self, *keys): 250 """Gets the specified info of currently active ssids 251 252 If frequency is requested, it'll be retrieved from the radio section 253 associated with this ssid. 254 255 Params: 256 keys: Names of the fields to include in the returned info. 257 e.g. "frequency". 258 259 Returns: 260 Values of the requested info. 261 """ 262 conds = (("disabled", "0"),) 263 keys = [w.replace("frequency","device") for w in keys] 264 if "device" not in keys: 265 keys.append("device") 266 info = self._section_option_lookup("wireless", conds, "ssid", *keys) 267 results = [] 268 for i in info: 269 radio = i["device"] 270 # Skip this info the radio its ssid is on is disabled. 271 disabled = self._client.get("wireless", radio, "disabled") 272 if disabled != '0': 273 continue 274 c = int(self._client.get("wireless", radio, "channel")) 275 if radio == "radio0": 276 i["frequency"] = WifiEnums.channel_2G_to_freq[c] 277 elif radio == "radio1": 278 i["frequency"] = WifiEnums.channel_5G_to_freq[c] 279 results.append(i) 280 return results 281 282 def get_radio_option(self, key, idx=0): 283 """Gets an option from the configured settings of a radio. 284 285 Params: 286 key: Name of the option to retrieve. 287 idx: Index of the radio to retrieve the option from. Default is 0. 288 289 Returns: 290 The value of the specified option and radio. 291 """ 292 r = None 293 if idx == 0: 294 r = "radio0" 295 elif idx == 1: 296 r = "radio1" 297 return self._client.get("wireless", r, key) 298 299 def apply_wifi_changes(self): 300 """Applies committed wifi changes by restarting wifi. 301 302 Raises: 303 ServerError: Something funny happened restarting wifi on the AP. 304 """ 305 s = self._client.commit('wireless') 306 resp = self.run('wifi') 307 return resp 308 # if resp != '' or not s: 309 # raise ServerError(("Exception in refreshing wifi changes, commit" 310 # " status: ") + str(s) + ", wifi restart response: " 311 # + str(resp)) 312 313 def set_wifi_channel(self, channel, device='radio0'): 314 self.set('wireless', device, 'channel', channel) 315 316 def _add_ifaces(self, configs): 317 """Adds wifi-ifaces in the AP's wireless config based on a list of 318 configuration dict. 319 320 Args: 321 configs: A list of dicts each representing a wifi-iface config. 322 """ 323 for config in configs: 324 self._add_cfg_section('wireless', 'wifi-iface', 325 config, self.IFACE_DEFAULTS) 326 327 def _add_cfg_section(self, cfg_name, section, options, defaults=None): 328 """Adds a section in a configuration file. 329 330 Args: 331 cfg_name: Name of the config file to add a section to. 332 e.g. 'wireless'. 333 section: Type of the secion to add. e.g. 'wifi-iface'. 334 options: A dict containing all key:value pairs of the options. 335 e.g. {'ssid': 'test', 'mode': 'ap'} 336 337 Raises: 338 ServerError: Uci add call returned False. 339 """ 340 section_id = self._client.add(cfg_name, section) 341 if not section_id: 342 raise ServerError(' '.join(("Failed adding", section, "in", 343 cfg_name))) 344 self._set_options(cfg_name, section_id, options, defaults) 345 346 def _set_options(self, cfg_name, section_id, options, defaults): 347 """Sets options in a section. 348 349 Args: 350 cfg_name: Name of the config file to add a section to. 351 e.g. 'wireless'. 352 section_id: ID of the secion to add options to. e.g. 'cfg000864'. 353 options: A dict containing all key:value pairs of the options. 354 e.g. {'ssid': 'test', 'mode': 'ap'} 355 356 Raises: 357 ServerError: Uci set call returned False. 358 """ 359 # Fill the fields not defined in config with default values. 360 if defaults: 361 for k, v in defaults.items(): 362 if k not in options: 363 options[k] = v 364 # Set value pairs defined in config. 365 for k, v in options.items(): 366 self._set_option(cfg_name, section_id, k, v) 367 368 def _set_option(self, cfg_name, section_id, k, v): 369 """Sets an option in a config section. 370 371 Args: 372 cfg_name: Name of the config file the section is in. 373 e.g. 'wireless'. 374 section_id: ID of the secion to set option in. e.g. 'cfg000864'. 375 k: Name of the option. 376 v: Value to set the option to. 377 378 Raises: 379 ServerError: If the rpc called returned False. 380 """ 381 status = self._client.set(cfg_name, section_id, k, v) 382 if not status: 383 # Delete whatever was added. 384 raise ServerError(' '.join(("Failed adding option", str(k), 385 ':', str(d), "to", str(section_id)))) 386 387 def delete_ifaces_by_ids(self, ids): 388 """Delete wifi-ifaces that are specified by the ids from the AP's 389 wireless config. 390 391 Args: 392 ids: A list of ids whose wifi-iface sections to be deleted. 393 """ 394 for i in ids: 395 self._delete_cfg_section_by_id('wireless', i) 396 397 def delete_ifaces(self, key, value): 398 """Delete wifi-ifaces that contain the specified key:value pair. 399 400 Args: 401 key: Key of the pair. 402 value: Value of the pair. 403 """ 404 self._delete_cfg_sections('wireless', key, value) 405 406 def _delete_cfg_sections(self, cfg_name, key, value): 407 """Deletes config sections that have the specified key:value pair. 408 409 Finds the ids of sections that match a key:value pair in the specified 410 config file and delete the section. 411 412 Args: 413 cfg_name: Name of the config file to delete sections from. 414 e.g. 'wireless'. 415 key: Name of the option to be matched. 416 value: Value of the option to be matched. 417 418 Raises: 419 ClientError: Could not find any section that has the key:value 420 pair. 421 """ 422 section_ids = self.section_id_lookup(cfg_name, key, value) 423 if not section_ids: 424 raise ClientError(' '.join(("Could not find any section that has ", 425 key, ":", value))) 426 for section_id in section_ids: 427 self._delete_cfg_section_by_id(cfg_name, section_id) 428 429 def _delete_cfg_section_by_id(self, cfg_name, section_id): 430 """Deletes the config section with specified id. 431 432 Args: 433 cfg_name: Name of the config file to the delete a section from. 434 e.g. 'wireless'. 435 section_id: ID of the section to be deleted. e.g. 'cfg0d3777'. 436 437 Raises: 438 ServerError: Uci delete call returned False. 439 """ 440 self._client.delete(cfg_name, section_id) 441 442 def _get_iw_info(self): 443 results = [] 444 text = self.run("iw dev").replace('\t', '') 445 interfaces = text.split("Interface") 446 for intf in interfaces: 447 if len(intf.strip()) < 6: 448 # This is a PHY mark. 449 continue 450 # This is an interface line. 451 intf = intf.replace(', ', '\n') 452 lines = intf.split('\n') 453 r = {} 454 for l in lines: 455 if ' ' in l: 456 # Only the lines with space are processed. 457 k, v = l.split(' ', 1) 458 if k == "addr": 459 k = "bssid" 460 if "wlan" in v: 461 k = "interface" 462 if k == "channel": 463 vs = v.split(' ', 1) 464 v = int(vs[0]) 465 r["frequency"] = int(vs[1].split(' ', 1)[0][1:5]) 466 if k[-1] == ':': 467 k = k[:-1] 468 r[k] = v 469 results.append(r) 470 return results 471 472 def get_active_bssids_info(self, radio, *args): 473 wlan = None 474 if radio == "radio0": 475 wlan = "wlan0" 476 if radio == "radio1": 477 wlan = "wlan1" 478 infos = self._get_iw_info() 479 bssids = [] 480 for i in infos: 481 if wlan in i["interface"]: 482 r = {} 483 for k,v in i.items(): 484 if k in args: 485 r[k] = v 486 r["bssid"] = i["bssid"].upper() 487 bssids.append(r) 488 return bssids 489 490 def toggle_bssid_state(self, bssid): 491 if bssid == self.get_bssid("radio0"): 492 self.toggle_radio_state("radio0") 493 return True 494 elif bssid == self.get_bssid("radio1"): 495 self.toggle_radio_state("radio1") 496 return True 497 return False 498 499 def __getattr__(self, name): 500 return _LibCaller(self._client, name) 501 502class _LibCaller: 503 def __init__(self, client, *args): 504 self._client = client 505 self._args = args 506 507 def __getattr__(self, name): 508 return _LibCaller(self._client, *self._args+(name,)) 509 510 def __call__(self, *args): 511 return self._client.call("/".join(self._args[:-1]), 512 self._args[-1], 513 *args) 514