• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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