1#!/usr/bin/env python3
2#
3#   Copyright 2017 - The Android Open Source Project
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 fcntl
18import os
19import selenium
20import splinter
21import time
22from acts import logger
23from acts.controllers import access_point
24from acts.controllers.ap_lib import bridge_interface
25from acts.controllers.ap_lib import hostapd_security
26from acts.controllers.ap_lib import hostapd_ap_preset
27
28BROWSER_WAIT_SHORT = 1
29BROWSER_WAIT_MED = 3
30BROWSER_WAIT_LONG = 30
31BROWSER_WAIT_EXTRA_LONG = 60
32
33
34def create(configs):
35    """Factory method for retail AP class.
36
37    Args:
38        configs: list of dicts containing ap settings. ap settings must contain
39        the following: brand, model, ip_address, username and password
40    """
41    SUPPORTED_APS = {
42        ("Netgear", "R7000"): "NetgearR7000AP",
43        ("Netgear", "R7000NA"): "NetgearR7000NAAP",
44        ("Netgear", "R7500"): "NetgearR7500AP",
45        ("Netgear", "R7800"): "NetgearR7800AP",
46        ("Netgear", "R8000"): "NetgearR8000AP",
47        ("Netgear", "R8500"): "NetgearR8500AP",
48        ("Netgear", "RAX"): "NetgearRAXAP",
49        ("Google", "Wifi"): "GoogleWifiAP"
50    }
51    objs = []
52    for config in configs:
53        try:
54            ap_class_name = SUPPORTED_APS[(config["brand"], config["model"])]
55            ap_class = globals()[ap_class_name]
56        except KeyError:
57            raise KeyError("Invalid retail AP brand and model combination.")
58        objs.append(ap_class(config))
59    return objs
60
61
62def detroy(objs):
63    for obj in objs:
64        obj.teardown()
65
66
67class BlockingBrowser(splinter.driver.webdriver.chrome.WebDriver):
68    """Class that implements a blocking browser session on top of selenium.
69
70    The class inherits from and builds upon splinter/selenium's webdriver class
71    and makes sure that only one such webdriver is active on a machine at any
72    single time. The class ensures single session operation using a lock file.
73    The class is to be used within context managers (e.g. with statements) to
74    ensure locks are always properly released.
75    """
76    def __init__(self, headless, timeout):
77        """Constructor for BlockingBrowser class.
78
79        Args:
80            headless: boolean to control visible/headless browser operation
81            timeout: maximum time allowed to launch browser
82        """
83        self.log = logger.create_tagged_trace_logger("ChromeDriver")
84        self.chrome_options = splinter.driver.webdriver.chrome.Options()
85        self.chrome_options.add_argument("--no-proxy-server")
86        self.chrome_options.add_argument("--no-sandbox")
87        self.chrome_options.add_argument("--allow-running-insecure-content")
88        self.chrome_options.add_argument("--ignore-certificate-errors")
89        self.chrome_capabilities = selenium.webdriver.common.desired_capabilities.DesiredCapabilities.CHROME.copy(
90        )
91        self.chrome_capabilities["acceptSslCerts"] = True
92        self.chrome_capabilities["acceptInsecureCerts"] = True
93        if headless:
94            self.chrome_options.add_argument("--headless")
95            self.chrome_options.add_argument("--disable-gpu")
96        self.lock_file_path = "/usr/local/bin/chromedriver"
97        self.timeout = timeout
98
99    def __enter__(self):
100        """Entry context manager for BlockingBrowser.
101
102        The enter context manager for BlockingBrowser attempts to lock the
103        browser file. If successful, it launches and returns a chromedriver
104        session. If an exception occurs while starting the browser, the lock
105        file is released.
106        """
107        self.lock_file = open(self.lock_file_path, "r")
108        start_time = time.time()
109        while time.time() < start_time + self.timeout:
110            try:
111                fcntl.flock(self.lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
112            except BlockingIOError:
113                time.sleep(BROWSER_WAIT_SHORT)
114                continue
115            try:
116                self.driver = selenium.webdriver.Chrome(
117                    options=self.chrome_options,
118                    desired_capabilities=self.chrome_capabilities)
119                self.element_class = splinter.driver.webdriver.WebDriverElement
120                self._cookie_manager = splinter.driver.webdriver.cookie_manager.CookieManager(
121                    self.driver)
122                super(splinter.driver.webdriver.chrome.WebDriver,
123                      self).__init__(2)
124                return super(BlockingBrowser, self).__enter__()
125            except:
126                fcntl.flock(self.lock_file, fcntl.LOCK_UN)
127                self.lock_file.close()
128                raise RuntimeError("Error starting browser. "
129                                   "Releasing lock file.")
130        raise TimeoutError("Could not start chrome browser in time.")
131
132    def __exit__(self, exc_type, exc_value, traceback):
133        """Exit context manager for BlockingBrowser.
134
135        The exit context manager simply calls the parent class exit and
136        releases the lock file.
137        """
138        try:
139            super(BlockingBrowser, self).__exit__(exc_type, exc_value,
140                                                  traceback)
141        except:
142            raise RuntimeError("Failed to quit browser. Releasing lock file.")
143        finally:
144            fcntl.flock(self.lock_file, fcntl.LOCK_UN)
145            self.lock_file.close()
146
147    def restart(self):
148        """Method to restart browser session without releasing lock file."""
149        self.quit()
150        self.__enter__()
151
152    def visit_persistent(self,
153                         url,
154                         page_load_timeout,
155                         num_tries,
156                         backup_url="about:blank",
157                         check_for_element=None):
158        """Method to visit webpages and retry upon failure.
159
160        The function visits a web page and checks the the resulting URL matches
161        the intended URL, i.e. no redirects have happened
162
163        Args:
164            url: the intended url
165            page_load_timeout: timeout for page visits
166            num_tries: number of tries before url is declared unreachable
167            backup_url: url to visit if first url is not reachable. This can be
168            used to simply refresh the browser and try again or to re-login to
169            the AP
170            check_for_element: element id to check for existence on page
171        """
172        self.driver.set_page_load_timeout(page_load_timeout)
173        for idx in range(num_tries):
174            try:
175                self.visit(url)
176            except:
177                self.restart()
178
179            page_reached = self.url.split("/")[-1] == url.split("/")[-1]
180            if check_for_element:
181                time.sleep(BROWSER_WAIT_MED)
182                element = self.find_by_id(check_for_element)
183                if not element:
184                    page_reached = 0
185            if page_reached:
186                break
187            else:
188                try:
189                    self.visit(backup_url)
190                except:
191                    self.restart()
192
193            if idx == num_tries - 1:
194                self.log.error("URL unreachable. Current URL: {}".format(
195                    self.url))
196                raise RuntimeError("URL unreachable.")
197
198
199class WifiRetailAP(object):
200    """Base class implementation for retail ap.
201
202    Base class provides functions whose implementation is shared by all aps.
203    If some functions such as set_power not supported by ap, checks will raise
204    exceptions.
205    """
206    def __init__(self, ap_settings):
207        self.ap_settings = ap_settings.copy()
208        self.log = logger.create_tagged_trace_logger("AccessPoint|{}".format(
209            self._get_control_ip_address()))
210        # Lock AP
211        if self.ap_settings.get('lock_ap', 0):
212            self.lock_timeout = self.ap_settings.get('lock_timeout', 3600)
213            self._lock_ap()
214
215    def teardown(self):
216        """Function to perform destroy operations."""
217        self._unlock_ap()
218
219    def reset(self):
220        """Function that resets AP.
221
222        Function implementation is AP dependent and intended to perform any
223        necessary reset operations as part of controller destroy.
224        """
225        pass
226
227    def read_ap_settings(self):
228        """Function that reads current ap settings.
229
230        Function implementation is AP dependent and thus base class raises exception
231        if function not implemented in child class.
232        """
233        raise NotImplementedError
234
235    def validate_ap_settings(self):
236        """Function to validate ap settings.
237
238        This function compares the actual ap settings read from the web GUI
239        with the assumed settings saved in the AP object. When called after AP
240        configuration, this method helps ensure that our configuration was
241        successful.
242        Note: Calling this function updates the stored ap_settings
243
244        Raises:
245            ValueError: If read AP settings do not match stored settings.
246        """
247        assumed_ap_settings = self.ap_settings.copy()
248        actual_ap_settings = self.read_ap_settings()
249        if assumed_ap_settings != actual_ap_settings:
250            self.log.warning(
251                "Discrepancy in AP settings. Some settings may have been overwritten."
252            )
253
254    def configure_ap(self, **config_flags):
255        """Function that configures ap based on values of ap_settings.
256
257        Function implementation is AP dependent and thus base class raises exception
258        if function not implemented in child class.
259
260        Args:
261            config_flags: optional configuration flags
262        """
263        raise NotImplementedError
264
265    def set_region(self, region):
266        """Function that sets AP region.
267
268        This function sets the region for the AP. Note that this may overwrite
269        channel and bandwidth settings in cases where the new region does not
270        support the current wireless configuration.
271
272        Args:
273            region: string indicating AP region
274        """
275        self.log.warning("Updating region may overwrite wireless settings.")
276        setting_to_update = {"region": region}
277        self.update_ap_settings(setting_to_update)
278
279    def set_radio_on_off(self, network, status):
280        """Function that turns the radio on or off.
281
282        Args:
283            network: string containing network identifier (2G, 5G_1, 5G_2)
284            status: boolean indicating on or off (0: off, 1: on)
285        """
286        setting_to_update = {"status_{}".format(network): int(status)}
287        self.update_ap_settings(setting_to_update)
288
289    def set_ssid(self, network, ssid):
290        """Function that sets network SSID.
291
292        Args:
293            network: string containing network identifier (2G, 5G_1, 5G_2)
294            ssid: string containing ssid
295        """
296        setting_to_update = {"ssid_{}".format(network): str(ssid)}
297        self.update_ap_settings(setting_to_update)
298
299    def set_channel(self, network, channel):
300        """Function that sets network channel.
301
302        Args:
303            network: string containing network identifier (2G, 5G_1, 5G_2)
304            channel: string or int containing channel
305        """
306        setting_to_update = {"channel_{}".format(network): str(channel)}
307        self.update_ap_settings(setting_to_update)
308
309    def set_bandwidth(self, network, bandwidth):
310        """Function that sets network bandwidth/mode.
311
312        Args:
313            network: string containing network identifier (2G, 5G_1, 5G_2)
314            bandwidth: string containing mode, e.g. 11g, VHT20, VHT40, VHT80.
315        """
316        setting_to_update = {"bandwidth_{}".format(network): str(bandwidth)}
317        self.update_ap_settings(setting_to_update)
318
319    def set_power(self, network, power):
320        """Function that sets network transmit power.
321
322        Args:
323            network: string containing network identifier (2G, 5G_1, 5G_2)
324            power: string containing power level, e.g., 25%, 100%
325        """
326        setting_to_update = {"power_{}".format(network): str(power)}
327        self.update_ap_settings(setting_to_update)
328
329    def set_security(self, network, security_type, *password):
330        """Function that sets network security setting and password.
331
332        Args:
333            network: string containing network identifier (2G, 5G_1, 5G_2)
334            security: string containing security setting, e.g., WPA2-PSK
335            password: optional argument containing password
336        """
337        if (len(password) == 1) and (type(password[0]) == str):
338            setting_to_update = {
339                "security_type_{}".format(network): str(security_type),
340                "password_{}".format(network): str(password[0])
341            }
342        else:
343            setting_to_update = {
344                "security_type_{}".format(network): str(security_type)
345            }
346        self.update_ap_settings(setting_to_update)
347
348    def set_rate(self):
349        """Function that configures rate used by AP.
350
351        Function implementation is not supported by most APs and thus base
352        class raises exception if function not implemented in child class.
353        """
354        raise NotImplementedError
355
356    def update_ap_settings(self, dict_settings={}, **named_settings):
357        """Function to update settings of existing AP.
358
359        Function copies arguments into ap_settings and calls configure_retail_ap
360        to apply them.
361
362        Args:
363            *dict_settings accepts single dictionary of settings to update
364            **named_settings accepts named settings to update
365            Note: dict and named_settings cannot contain the same settings.
366        """
367        settings_to_update = dict(dict_settings, **named_settings)
368        if len(settings_to_update) != len(dict_settings) + len(named_settings):
369            raise KeyError("The following keys were passed twice: {}".format(
370                (set(dict_settings.keys()).intersection(
371                    set(named_settings.keys())))))
372        if not set(settings_to_update.keys()).issubset(
373                set(self.ap_settings.keys())):
374            raise KeyError(
375                "The following settings are invalid for this AP: {}".format(
376                    set(settings_to_update.keys()).difference(
377                        set(self.ap_settings.keys()))))
378
379        updates_requested = False
380        status_toggle_flag = False
381        for setting, value in settings_to_update.items():
382            if self.ap_settings[setting] != value:
383                self.ap_settings[setting] = value
384                if "status" in setting:
385                    status_toggle_flag = True
386                updates_requested = True
387
388        if updates_requested:
389            self.configure_ap(status_toggled=status_toggle_flag)
390
391    def band_lookup_by_channel(self, channel):
392        """Function that gives band name by channel number.
393
394        Args:
395            channel: channel number to lookup
396        Returns:
397            band: name of band which this channel belongs to on this ap
398        """
399        for key, value in self.channel_band_map.items():
400            if channel in value:
401                return key
402        raise ValueError("Invalid channel passed in argument.")
403
404    def _get_control_ip_address(self):
405        """Function to get AP's Control Interface IP address."""
406        if "ssh_config" in self.ap_settings.keys():
407            return self.ap_settings["ssh_config"]["host"]
408        else:
409            return self.ap_settings["ip_address"]
410
411    def _lock_ap(self):
412        """Function to lock the ap while tests are running."""
413        self.lock_file_path = "/tmp/{}_{}_{}.lock".format(
414            self.ap_settings['brand'], self.ap_settings['model'],
415            self._get_control_ip_address())
416        if not os.path.exists(self.lock_file_path):
417            with open(self.lock_file_path, 'w'):
418                pass
419        self.lock_file = open(self.lock_file_path, "r")
420        start_time = time.time()
421        self.log.info('Trying to acquire AP lock.')
422        while time.time() < start_time + self.lock_timeout:
423            try:
424                fcntl.flock(self.lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
425            except BlockingIOError:
426                time.sleep(BROWSER_WAIT_SHORT)
427                continue
428            self.log.info('AP lock acquired.')
429            return
430        raise RuntimeError("Could not lock AP in time.")
431
432    def _unlock_ap(self):
433        """Function to unlock the AP when tests are done."""
434        self.log.info('Releasing AP lock.')
435        if hasattr(self, "lock_file"):
436            fcntl.flock(self.lock_file, fcntl.LOCK_UN)
437            self.lock_file.close()
438
439
440class NetgearR7000AP(WifiRetailAP):
441    """Class that implements Netgear R7000 AP."""
442    def __init__(self, ap_settings):
443        super().__init__(ap_settings)
444        self.init_gui_data()
445        # Read and update AP settings
446        self.read_ap_settings()
447        if not set(ap_settings.items()).issubset(self.ap_settings.items()):
448            self.update_ap_settings(ap_settings)
449
450    def init_gui_data(self):
451        """Function to initialize data used while interacting with web GUI"""
452        self.config_page = (
453            "{protocol}://{username}:{password}@"
454            "{ip_address}:{port}/WLG_wireless_dual_band_r10.htm").format(
455                protocol=self.ap_settings["protocol"],
456                username=self.ap_settings["admin_username"],
457                password=self.ap_settings["admin_password"],
458                ip_address=self.ap_settings["ip_address"],
459                port=self.ap_settings["port"])
460        self.config_page_nologin = (
461            "{protocol}://{ip_address}:{port}/"
462            "WLG_wireless_dual_band_r10.htm").format(
463                protocol=self.ap_settings["protocol"],
464                ip_address=self.ap_settings["ip_address"],
465                port=self.ap_settings["port"])
466        self.config_page_advanced = (
467            "{protocol}://{username}:{password}@"
468            "{ip_address}:{port}/WLG_adv_dual_band2.htm").format(
469                protocol=self.ap_settings["protocol"],
470                username=self.ap_settings["admin_username"],
471                password=self.ap_settings["admin_password"],
472                ip_address=self.ap_settings["ip_address"],
473                port=self.ap_settings["port"])
474        self.networks = ["2G", "5G_1"]
475        self.channel_band_map = {
476            "2G": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
477            "5G_1": [
478                36, 40, 44, 48, 52, 56, 60, 64, 100, 104, 108, 112, 116, 120,
479                124, 128, 132, 136, 140, 149, 153, 157, 161, 165
480            ]
481        }
482        self.region_map = {
483            "1": "Africa",
484            "2": "Asia",
485            "3": "Australia",
486            "4": "Canada",
487            "5": "Europe",
488            "6": "Israel",
489            "7": "Japan",
490            "8": "Korea",
491            "9": "Mexico",
492            "10": "South America",
493            "11": "United States",
494            "12": "Middle East(Algeria/Syria/Yemen)",
495            "14": "Russia",
496            "16": "China",
497            "17": "India",
498            "18": "Malaysia",
499            "19": "Middle East(Iran/Labanon/Qatar)",
500            "20": "Middle East(Turkey/Egypt/Tunisia/Kuwait)",
501            "21": "Middle East(Saudi Arabia)",
502            "22": "Middle East(United Arab Emirates)",
503            "23": "Singapore",
504            "24": "Taiwan"
505        }
506        self.config_page_fields = {
507            "region": "WRegion",
508            ("2G", "status"): "enable_ap",
509            ("5G_1", "status"): "enable_ap_an",
510            ("2G", "ssid"): "ssid",
511            ("5G_1", "ssid"): "ssid_an",
512            ("2G", "channel"): "w_channel",
513            ("5G_1", "channel"): "w_channel_an",
514            ("2G", "bandwidth"): "opmode",
515            ("5G_1", "bandwidth"): "opmode_an",
516            ("2G", "power"): "enable_tpc",
517            ("5G_1", "power"): "enable_tpc_an",
518            ("2G", "security_type"): "security_type",
519            ("5G_1", "security_type"): "security_type_an",
520            ("2G", "password"): "passphrase",
521            ("5G_1", "password"): "passphrase_an"
522        }
523        self.bw_mode_values = {
524            "g and b": "11g",
525            "145Mbps": "VHT20",
526            "300Mbps": "VHT40",
527            "HT80": "VHT80"
528        }
529        self.power_mode_values = {
530            "1": "100%",
531            "2": "75%",
532            "3": "50%",
533            "4": "25%"
534        }
535        self.bw_mode_text = {
536            "11g": "Up to 54 Mbps",
537            "VHT20": "Up to 289 Mbps",
538            "VHT40": "Up to 600 Mbps",
539            "VHT80": "Up to 1300 Mbps"
540        }
541
542    def read_ap_settings(self):
543        """Function to read ap settings."""
544        with BlockingBrowser(self.ap_settings["headless_browser"],
545                             900) as browser:
546            # Visit URL
547            browser.visit_persistent(self.config_page, BROWSER_WAIT_MED, 10)
548
549            for key, value in self.config_page_fields.items():
550                if "status" in key:
551                    browser.visit_persistent(self.config_page_advanced,
552                                             BROWSER_WAIT_MED, 10)
553                    config_item = browser.find_by_name(value)
554                    self.ap_settings["{}_{}".format(key[1], key[0])] = int(
555                        config_item.first.checked)
556                    browser.visit_persistent(self.config_page,
557                                             BROWSER_WAIT_MED, 10)
558                else:
559                    config_item = browser.find_by_name(value)
560                    if "bandwidth" in key:
561                        self.ap_settings["{}_{}".format(
562                            key[1], key[0])] = self.bw_mode_values[
563                                config_item.first.value]
564                    elif "power" in key:
565                        self.ap_settings["{}_{}".format(
566                            key[1], key[0])] = self.power_mode_values[
567                                config_item.first.value]
568                    elif "region" in key:
569                        self.ap_settings["region"] = self.region_map[
570                            config_item.first.value]
571                    elif "security_type" in key:
572                        for item in config_item:
573                            if item.checked:
574                                self.ap_settings["{}_{}".format(
575                                    key[1], key[0])] = item.value
576                    else:
577                        config_item = browser.find_by_name(value)
578                        self.ap_settings["{}_{}".format(
579                            key[1], key[0])] = config_item.first.value
580        return self.ap_settings.copy()
581
582    def configure_ap(self, **config_flags):
583        """Function to configure ap wireless settings."""
584        # Turn radios on or off
585        if config_flags["status_toggled"]:
586            self.configure_radio_on_off()
587        # Configure radios
588        with BlockingBrowser(self.ap_settings["headless_browser"],
589                             900) as browser:
590            # Visit URL
591            browser.visit_persistent(self.config_page, BROWSER_WAIT_MED, 10)
592            browser.visit_persistent(self.config_page_nologin,
593                                     BROWSER_WAIT_MED, 10, self.config_page)
594
595            # Update region, and power/bandwidth for each network
596            config_item = browser.find_by_name(
597                self.config_page_fields["region"]).first
598            config_item.select_by_text(self.ap_settings["region"])
599            for key, value in self.config_page_fields.items():
600                if "power" in key:
601                    config_item = browser.find_by_name(value).first
602                    config_item.select_by_text(self.ap_settings["{}_{}".format(
603                        key[1], key[0])])
604                elif "bandwidth" in key:
605                    config_item = browser.find_by_name(value).first
606                    try:
607                        config_item.select_by_text(
608                            self.bw_mode_text[self.ap_settings["{}_{}".format(
609                                key[1], key[0])]])
610                    except AttributeError:
611                        self.log.warning(
612                            "Cannot select bandwidth. Keeping AP default.")
613
614            # Update security settings (passwords updated only if applicable)
615            for key, value in self.config_page_fields.items():
616                if "security_type" in key:
617                    browser.choose(
618                        value, self.ap_settings["{}_{}".format(key[1],
619                                                               key[0])])
620                    if self.ap_settings["{}_{}".format(key[1],
621                                                       key[0])] == "WPA2-PSK":
622                        config_item = browser.find_by_name(
623                            self.config_page_fields[(key[0],
624                                                     "password")]).first
625                        config_item.fill(self.ap_settings["{}_{}".format(
626                            "password", key[0])])
627
628            # Update SSID and channel for each network
629            # NOTE: Update ordering done as such as workaround for R8000
630            # wherein channel and SSID get overwritten when some other
631            # variables are changed. However, region does have to be set before
632            # channel in all cases.
633            for key, value in self.config_page_fields.items():
634                if "ssid" in key:
635                    config_item = browser.find_by_name(value).first
636                    config_item.fill(self.ap_settings["{}_{}".format(
637                        key[1], key[0])])
638                elif "channel" in key:
639                    config_item = browser.find_by_name(value).first
640                    try:
641                        config_item.select(self.ap_settings["{}_{}".format(
642                            key[1], key[0])])
643                        time.sleep(BROWSER_WAIT_SHORT)
644                    except AttributeError:
645                        self.log.warning(
646                            "Cannot select channel. Keeping AP default.")
647                    try:
648                        alert = browser.get_alert()
649                        alert.accept()
650                    except:
651                        pass
652
653            time.sleep(BROWSER_WAIT_SHORT)
654            browser.find_by_name("Apply").first.click()
655            time.sleep(BROWSER_WAIT_SHORT)
656            try:
657                alert = browser.get_alert()
658                alert.accept()
659                time.sleep(BROWSER_WAIT_SHORT)
660            except:
661                time.sleep(BROWSER_WAIT_SHORT)
662            browser.visit_persistent(self.config_page, BROWSER_WAIT_EXTRA_LONG,
663                                     10)
664
665    def configure_radio_on_off(self):
666        """Helper configuration function to turn radios on/off."""
667        with BlockingBrowser(self.ap_settings["headless_browser"],
668                             900) as browser:
669            # Visit URL
670            browser.visit_persistent(self.config_page, BROWSER_WAIT_MED, 10)
671            browser.visit_persistent(self.config_page_advanced,
672                                     BROWSER_WAIT_MED, 10)
673
674            # Turn radios on or off
675            for key, value in self.config_page_fields.items():
676                if "status" in key:
677                    config_item = browser.find_by_name(value).first
678                    if self.ap_settings["{}_{}".format(key[1], key[0])]:
679                        config_item.check()
680                    else:
681                        config_item.uncheck()
682
683            time.sleep(BROWSER_WAIT_SHORT)
684            browser.find_by_name("Apply").first.click()
685            time.sleep(BROWSER_WAIT_EXTRA_LONG)
686            browser.visit_persistent(self.config_page, BROWSER_WAIT_EXTRA_LONG,
687                                     10)
688
689
690class NetgearR7000NAAP(NetgearR7000AP):
691    """Class that implements Netgear R7000 NA AP."""
692    def init_gui_data(self):
693        """Function to initialize data used while interacting with web GUI"""
694        super.init_gui_data()
695        self.region_map["11"] = "North America"
696
697
698class NetgearR7500AP(WifiRetailAP):
699    """Class that implements Netgear R7500 AP."""
700    def __init__(self, ap_settings):
701        super().__init__(ap_settings)
702        self.init_gui_data()
703        # Read and update AP settings
704        self.read_ap_settings()
705        if not set(ap_settings.items()).issubset(self.ap_settings.items()):
706            self.update_ap_settings(ap_settings)
707
708    def init_gui_data(self):
709        """Function to initialize data used while interacting with web GUI"""
710        self.config_page = ("{protocol}://{username}:{password}@"
711                            "{ip_address}:{port}/index.htm").format(
712                                protocol=self.ap_settings["protocol"],
713                                username=self.ap_settings["admin_username"],
714                                password=self.ap_settings["admin_password"],
715                                ip_address=self.ap_settings["ip_address"],
716                                port=self.ap_settings["port"])
717        self.config_page_advanced = (
718            "{protocol}://{username}:{password}@"
719            "{ip_address}:{port}/adv_index.htm").format(
720                protocol=self.ap_settings["protocol"],
721                username=self.ap_settings["admin_username"],
722                password=self.ap_settings["admin_password"],
723                ip_address=self.ap_settings["ip_address"],
724                port=self.ap_settings["port"])
725        self.networks = ["2G", "5G_1"]
726        self.channel_band_map = {
727            "2G": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
728            "5G_1": [
729                36, 40, 44, 48, 52, 56, 60, 64, 100, 104, 108, 112, 116, 120,
730                124, 128, 132, 136, 140, 149, 153, 157, 161, 165
731            ]
732        }
733        self.config_page_fields = {
734            "region": "WRegion",
735            ("2G", "status"): "enable_ap",
736            ("5G_1", "status"): "enable_ap_an",
737            ("2G", "ssid"): "ssid",
738            ("5G_1", "ssid"): "ssid_an",
739            ("2G", "channel"): "w_channel",
740            ("5G_1", "channel"): "w_channel_an",
741            ("2G", "bandwidth"): "opmode",
742            ("5G_1", "bandwidth"): "opmode_an",
743            ("2G", "security_type"): "security_type",
744            ("5G_1", "security_type"): "security_type_an",
745            ("2G", "password"): "passphrase",
746            ("5G_1", "password"): "passphrase_an"
747        }
748        self.region_map = {
749            "0": "Africa",
750            "1": "Asia",
751            "2": "Australia",
752            "3": "Canada",
753            "4": "Europe",
754            "5": "Israel",
755            "6": "Japan",
756            "7": "Korea",
757            "8": "Mexico",
758            "9": "South America",
759            "10": "United States",
760            "11": "China",
761            "12": "India",
762            "13": "Malaysia",
763            "14": "Middle East(Algeria/Syria/Yemen)",
764            "15": "Middle East(Iran/Labanon/Qatar)",
765            "16": "Middle East(Turkey/Egypt/Tunisia/Kuwait)",
766            "17": "Middle East(Saudi Arabia)",
767            "18": "Middle East(United Arab Emirates)",
768            "19": "Russia",
769            "20": "Singapore",
770            "21": "Taiwan"
771        }
772        self.bw_mode_text_2g = {
773            "11g": "Up to 54 Mbps",
774            "VHT20": "Up to 289 Mbps",
775            "VHT40": "Up to 600 Mbps"
776        }
777        self.bw_mode_text_5g = {
778            "VHT20": "Up to 347 Mbps",
779            "VHT40": "Up to 800 Mbps",
780            "VHT80": "Up to 1733 Mbps"
781        }
782        self.bw_mode_values = {
783            "1": "11g",
784            "2": "VHT20",
785            "3": "VHT40",
786            "7": "VHT20",
787            "8": "VHT40",
788            "9": "VHT80"
789        }
790
791    def read_ap_settings(self):
792        """Function to read ap wireless settings."""
793        # Get radio status (on/off)
794        self.read_radio_on_off()
795        # Get radio configuration. Note that if both radios are off, the below
796        # code will result in an error
797        with BlockingBrowser(self.ap_settings["headless_browser"],
798                             900) as browser:
799            browser.visit_persistent(self.config_page,
800                                     BROWSER_WAIT_MED,
801                                     10,
802                                     check_for_element="wireless")
803            wireless_button = browser.find_by_id("wireless").first
804            wireless_button.click()
805            time.sleep(BROWSER_WAIT_MED)
806
807            with browser.get_iframe("formframe") as iframe:
808                for key, value in self.config_page_fields.items():
809                    if "bandwidth" in key:
810                        config_item = iframe.find_by_name(value).first
811                        self.ap_settings["{}_{}".format(
812                            key[1],
813                            key[0])] = self.bw_mode_values[config_item.value]
814                    elif "region" in key:
815                        config_item = iframe.find_by_name(value).first
816                        self.ap_settings["region"] = self.region_map[
817                            config_item.value]
818                    elif "password" in key:
819                        try:
820                            config_item = iframe.find_by_name(value).first
821                            self.ap_settings["{}_{}".format(
822                                key[1], key[0])] = config_item.value
823                            self.ap_settings["{}_{}".format(
824                                "security_type", key[0])] = "WPA2-PSK"
825                        except:
826                            self.ap_settings["{}_{}".format(
827                                key[1], key[0])] = "defaultpassword"
828                            self.ap_settings["{}_{}".format(
829                                "security_type", key[0])] = "Disable"
830                    elif ("channel" in key) or ("ssid" in key):
831                        config_item = iframe.find_by_name(value).first
832                        self.ap_settings["{}_{}".format(
833                            key[1], key[0])] = config_item.value
834                    else:
835                        pass
836        return self.ap_settings.copy()
837
838    def configure_ap(self, **config_flags):
839        """Function to configure ap wireless settings."""
840        # Turn radios on or off
841        if config_flags["status_toggled"]:
842            self.configure_radio_on_off()
843        # Configure radios
844        with BlockingBrowser(self.ap_settings["headless_browser"],
845                             900) as browser:
846            browser.visit_persistent(self.config_page,
847                                     BROWSER_WAIT_MED,
848                                     10,
849                                     check_for_element="wireless")
850            wireless_button = browser.find_by_id("wireless").first
851            wireless_button.click()
852            time.sleep(BROWSER_WAIT_MED)
853
854            with browser.get_iframe("formframe") as iframe:
855                # Update AP region. Must be done before channel setting
856                config_item = iframe.find_by_name(
857                    self.config_page_fields["region"]).first
858                config_item.select_by_text(self.ap_settings["region"])
859                # Update wireless settings for each network
860                for key, value in self.config_page_fields.items():
861                    if "ssid" in key:
862                        config_item = iframe.find_by_name(value).first
863                        config_item.fill(self.ap_settings["{}_{}".format(
864                            key[1], key[0])])
865                    elif "channel" in key:
866                        channel_string = "0" * (int(self.ap_settings[
867                            "{}_{}".format(key[1], key[0])]) < 10) + str(
868                                self.ap_settings["{}_{}".format(
869                                    key[1], key[0])]) + "(DFS)" * (48 < int(
870                                        self.ap_settings["{}_{}".format(
871                                            key[1], key[0])]) < 149)
872                        config_item = iframe.find_by_name(value).first
873                        try:
874                            config_item.select_by_text(channel_string)
875                        except AttributeError:
876                            self.log.warning(
877                                "Cannot select channel. Keeping AP default.")
878                    elif key == ("2G", "bandwidth"):
879                        config_item = iframe.find_by_name(value).first
880                        try:
881                            config_item.select_by_text(
882                                str(self.bw_mode_text_2g[self.ap_settings[
883                                    "{}_{}".format(key[1], key[0])]]))
884                        except AttributeError:
885                            self.log.warning(
886                                "Cannot select bandwidth. Keeping AP default.")
887                    elif key == ("5G_1", "bandwidth"):
888                        config_item = iframe.find_by_name(value).first
889                        try:
890                            config_item.select_by_text(
891                                str(self.bw_mode_text_5g[self.ap_settings[
892                                    "{}_{}".format(key[1], key[0])]]))
893                        except AttributeError:
894                            self.log.warning(
895                                "Cannot select bandwidth. Keeping AP default.")
896                # Update passwords for WPA2-PSK protected networks
897                # (Must be done after security type is selected)
898                for key, value in self.config_page_fields.items():
899                    if "security_type" in key:
900                        iframe.choose(
901                            value,
902                            self.ap_settings["{}_{}".format(key[1], key[0])])
903                        if self.ap_settings["{}_{}".format(
904                                key[1], key[0])] == "WPA2-PSK":
905                            config_item = iframe.find_by_name(
906                                self.config_page_fields[(key[0],
907                                                         "password")]).first
908                            config_item.fill(self.ap_settings["{}_{}".format(
909                                "password", key[0])])
910
911                apply_button = iframe.find_by_name("Apply")
912                apply_button[0].click()
913                time.sleep(BROWSER_WAIT_SHORT)
914                try:
915                    alert = browser.get_alert()
916                    alert.accept()
917                except:
918                    pass
919                time.sleep(BROWSER_WAIT_SHORT)
920                try:
921                    alert = browser.get_alert()
922                    alert.accept()
923                except:
924                    pass
925                time.sleep(BROWSER_WAIT_SHORT)
926            time.sleep(BROWSER_WAIT_EXTRA_LONG)
927            browser.visit_persistent(self.config_page, BROWSER_WAIT_EXTRA_LONG,
928                                     10)
929
930    def configure_radio_on_off(self):
931        """Helper configuration function to turn radios on/off."""
932        with BlockingBrowser(self.ap_settings["headless_browser"],
933                             900) as browser:
934            browser.visit_persistent(self.config_page, BROWSER_WAIT_MED, 10)
935            browser.visit_persistent(self.config_page_advanced,
936                                     BROWSER_WAIT_MED,
937                                     10,
938                                     check_for_element="advanced_bt")
939            advanced_button = browser.find_by_id("advanced_bt").first
940            advanced_button.click()
941            time.sleep(BROWSER_WAIT_MED)
942            wireless_button = browser.find_by_id("wladv").first
943            wireless_button.click()
944            time.sleep(BROWSER_WAIT_MED)
945
946            with browser.get_iframe("formframe") as iframe:
947                # Turn radios on or off
948                for key, value in self.config_page_fields.items():
949                    if "status" in key:
950                        config_item = iframe.find_by_name(value).first
951                        if self.ap_settings["{}_{}".format(key[1], key[0])]:
952                            config_item.check()
953                        else:
954                            config_item.uncheck()
955
956                time.sleep(BROWSER_WAIT_SHORT)
957                browser.find_by_name("Apply").first.click()
958                time.sleep(BROWSER_WAIT_EXTRA_LONG)
959                browser.visit_persistent(self.config_page,
960                                         BROWSER_WAIT_EXTRA_LONG, 10)
961
962    def read_radio_on_off(self):
963        """Helper configuration function to read radio status."""
964        with BlockingBrowser(self.ap_settings["headless_browser"],
965                             900) as browser:
966            browser.visit_persistent(self.config_page, BROWSER_WAIT_MED, 10)
967            browser.visit_persistent(self.config_page_advanced,
968                                     BROWSER_WAIT_MED,
969                                     10,
970                                     check_for_element="advanced_bt")
971            advanced_button = browser.find_by_id("advanced_bt").first
972            advanced_button.click()
973            time.sleep(BROWSER_WAIT_SHORT)
974            wireless_button = browser.find_by_id("wladv").first
975            wireless_button.click()
976            time.sleep(BROWSER_WAIT_MED)
977
978            with browser.get_iframe("formframe") as iframe:
979                # Turn radios on or off
980                for key, value in self.config_page_fields.items():
981                    if "status" in key:
982                        config_item = iframe.find_by_name(value).first
983                        self.ap_settings["{}_{}".format(key[1], key[0])] = int(
984                            config_item.checked)
985
986
987class NetgearR7800AP(NetgearR7500AP):
988    """Class that implements Netgear R7800 AP.
989
990    Since most of the class' implementation is shared with the R7500, this
991    class inherits from NetgearR7500AP and simply redefines config parameters
992    """
993    def __init__(self, ap_settings):
994        super().__init__(ap_settings)
995        self.init_gui_data()
996        # Overwrite minor differences from R7500 AP
997        self.bw_mode_text_2g["VHT20"] = "Up to 347 Mbps"
998        # Read and update AP settings
999        self.read_ap_settings()
1000        if not set(ap_settings.items()).issubset(self.ap_settings.items()):
1001            self.update_ap_settings(ap_settings)
1002
1003
1004class NetgearR8000AP(NetgearR7000AP):
1005    """Class that implements Netgear R8000 AP.
1006
1007    Since most of the class' implementation is shared with the R7000, this
1008    class inherits from NetgearR7000AP and simply redefines config parameters
1009    """
1010    def init_gui_data(self):
1011        super().init_gui_data()
1012        # Overwrite minor differences from R7000 AP
1013        self.config_page = (
1014            "{protocol}://{username}:{password}@"
1015            "{ip_address}:{port}/WLG_wireless_dual_band_r8000.htm").format(
1016                protocol=self.ap_settings["protocol"],
1017                username=self.ap_settings["admin_username"],
1018                password=self.ap_settings["admin_password"],
1019                ip_address=self.ap_settings["ip_address"],
1020                port=self.ap_settings["port"])
1021        self.config_page_nologin = (
1022            "{protocol}://{ip_address}:{port}/"
1023            "WLG_wireless_dual_band_r8000.htm").format(
1024                protocol=self.ap_settings["protocol"],
1025                ip_address=self.ap_settings["ip_address"],
1026                port=self.ap_settings["port"])
1027        self.config_page_advanced = (
1028            "{protocol}://{username}:{password}@"
1029            "{ip_address}:{port}/WLG_adv_dual_band2_r8000.htm").format(
1030                protocol=self.ap_settings["protocol"],
1031                username=self.ap_settings["admin_username"],
1032                password=self.ap_settings["admin_password"],
1033                ip_address=self.ap_settings["ip_address"],
1034                port=self.ap_settings["port"])
1035        self.networks = ["2G", "5G_1", "5G_2"]
1036        self.channel_band_map = {
1037            "2G": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
1038            "5G_1": [36, 40, 44, 48],
1039            "5G_2": [149, 153, 157, 161, 165]
1040        }
1041        self.config_page_fields = {
1042            "region": "WRegion",
1043            ("2G", "status"): "enable_ap",
1044            ("5G_1", "status"): "enable_ap_an",
1045            ("5G_2", "status"): "enable_ap_an_2",
1046            ("2G", "ssid"): "ssid",
1047            ("5G_1", "ssid"): "ssid_an",
1048            ("5G_2", "ssid"): "ssid_an_2",
1049            ("2G", "channel"): "w_channel",
1050            ("5G_1", "channel"): "w_channel_an",
1051            ("5G_2", "channel"): "w_channel_an_2",
1052            ("2G", "bandwidth"): "opmode",
1053            ("5G_1", "bandwidth"): "opmode_an",
1054            ("5G_2", "bandwidth"): "opmode_an_2",
1055            ("2G", "security_type"): "security_type",
1056            ("5G_1", "security_type"): "security_type_an",
1057            ("5G_2", "security_type"): "security_type_an_2",
1058            ("2G", "password"): "passphrase",
1059            ("5G_1", "password"): "passphrase_an",
1060            ("5G_2", "password"): "passphrase_an_2"
1061        }
1062
1063
1064class NetgearR8500AP(NetgearR7000AP):
1065    """Class that implements Netgear R8500 AP.
1066
1067    Since most of the class' implementation is shared with the R7000, this
1068    class inherits from NetgearR7000AP and simply redefines config parameters
1069    """
1070    def __init__(self, ap_settings):
1071        super().__init__(ap_settings)
1072        self.init_gui_data()
1073        # Overwrite minor differences from R8000 AP
1074        self.config_page = (
1075            "{protocol}://{username}:{password}@"
1076            "{ip_address}:{port}/WLG_wireless_tri_band.htm").format(
1077                protocol=self.ap_settings["protocol"],
1078                username=self.ap_settings["admin_username"],
1079                password=self.ap_settings["admin_password"],
1080                ip_address=self.ap_settings["ip_address"],
1081                port=self.ap_settings["port"])
1082        self.config_page_nologin = (
1083            "{protocol}://{ip_address}:{port}/"
1084            "WLG_wireless_tri_band.htm").format(
1085                protocol=self.ap_settings["protocol"],
1086                ip_address=self.ap_settings["ip_address"],
1087                port=self.ap_settings["port"])
1088        self.config_page_advanced = (
1089            "{protocol}://{username}:{password}@"
1090            "{ip_address}:{port}/WLG_adv_tri_band2.htm").format(
1091                protocol=self.ap_settings["protocol"],
1092                username=self.ap_settings["admin_username"],
1093                password=self.ap_settings["admin_password"],
1094                ip_address=self.ap_settings["ip_address"],
1095                port=self.ap_settings["port"])
1096        self.networks = ["2G", "5G_1", "5G_2"]
1097        self.channel_band_map = {
1098            "2G": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
1099            "5G_1": [36, 40, 44, 48],
1100            "5G_2": [149, 153, 157, 161, 165]
1101        }
1102        self.config_page_fields = {
1103            "region": "WRegion",
1104            ("2G", "status"): "enable_ap",
1105            ("5G_1", "status"): "enable_ap_an",
1106            ("5G_2", "status"): "enable_ap_an_2",
1107            ("2G", "ssid"): "ssid",
1108            ("5G_1", "ssid"): "ssid_an",
1109            ("5G_2", "ssid"): "ssid_an_2",
1110            ("2G", "channel"): "w_channel",
1111            ("5G_1", "channel"): "w_channel_an",
1112            ("5G_2", "channel"): "w_channel_an_2",
1113            ("2G", "bandwidth"): "opmode",
1114            ("5G_1", "bandwidth"): "opmode_an",
1115            ("5G_2", "bandwidth"): "opmode_an_2",
1116            ("2G", "security_type"): "security_type",
1117            ("5G_1", "security_type"): "security_type_an",
1118            ("5G_2", "security_type"): "security_type_an_2",
1119            ("2G", "password"): "passphrase",
1120            ("5G_1", "password"): "passphrase_an",
1121            ("5G_2", "password"): "passphrase_an_2"
1122        }
1123        self.bw_mode_text = {
1124            "11g": "Up to 54 Mbps",
1125            "VHT20": "Up to 433 Mbps",
1126            "VHT40": "Up to 1000 Mbps",
1127            "VHT80": "Up to 2165 Mbps"
1128        }
1129        # Read and update AP settings
1130        self.read_ap_settings()
1131        if not set(ap_settings.items()).issubset(self.ap_settings.items()):
1132            self.update_ap_settings(ap_settings)
1133
1134
1135class NetgearRAXAP(NetgearR7000AP):
1136    """Class that implements Netgear RAX AP.
1137
1138    Since most of the class' implementation is shared with the R7000, this
1139    class inherits from NetgearR7000AP and simply redefines config parameters
1140    """
1141    def init_gui_data(self):
1142        super().init_gui_data()
1143        # Overwrite minor differences from R7000 AP
1144        self.config_page = (
1145            "{protocol}://{username}:{password}@"
1146            "{ip_address}:{port}/WLG_wireless_dual_band_r10.htm").format(
1147                protocol=self.ap_settings["protocol"],
1148                username=self.ap_settings["admin_username"],
1149                password=self.ap_settings["admin_password"],
1150                ip_address=self.ap_settings["ip_address"],
1151                port=self.ap_settings["port"])
1152        self.config_page_nologin = (
1153            "{protocol}://{ip_address}:{port}/"
1154            "WLG_wireless_dual_band_r10.htm").format(
1155                protocol=self.ap_settings["protocol"],
1156                ip_address=self.ap_settings["ip_address"],
1157                port=self.ap_settings["port"])
1158        self.config_page_advanced = (
1159            "{protocol}://{username}:{password}@"
1160            "{ip_address}:{port}/WLG_adv_dual_band2.htm").format(
1161                protocol=self.ap_settings["protocol"],
1162                username=self.ap_settings["admin_username"],
1163                password=self.ap_settings["admin_password"],
1164                ip_address=self.ap_settings["ip_address"],
1165                port=self.ap_settings["port"])
1166        self.networks = ["2G", "5G_1", "5G_2"]
1167        self.channel_band_map = {
1168            "2G": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
1169            "5G_1": [
1170                36, 40, 44, 48, 52, 56, 60, 64, 100, 104, 108, 112, 116, 120,
1171                124, 128, 132, 136, 140, 149, 153, 157, 161, 165
1172            ]
1173        }
1174
1175        self.bw_mode_values = {
1176            "g and b": "11g",
1177            "145Mbps": "VHT20",
1178            "300Mbps": "VHT40",
1179            "HT80": "VHT80",
1180            "HT160": "VHT160"
1181        }
1182        self.bw_mode_text = {
1183            "11g": "Up to 54 Mbps",
1184            "VHT20": "Up to 600 Mbps",
1185            "VHT40": "Up to 1200 Mbps",
1186            "VHT80": "Up to 2400 Mbps",
1187            "VHT160": "Up to 4800 Mbps"
1188        }
1189
1190
1191class GoogleWifiAP(WifiRetailAP):
1192    """ Class that implements Google Wifi AP.
1193
1194    This class is a work in progress
1195    """
1196    def __init__(self, ap_settings):
1197        super().__init__(ap_settings)
1198        # Initialize AP
1199        if self.ap_settings["status_2G"] and self.ap_settings["status_5G_1"]:
1200            raise ValueError("Error initializing Google Wifi AP. "
1201                             "Only one interface can be enabled at a time.")
1202        self.channel_band_map = {
1203            "2G": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
1204            "5G_1": [36, 40, 44, 48, 149, 153, 157, 161, 165]
1205        }
1206        self.BW_MODE_MAP = {
1207            "legacy": 20,
1208            "VHT20": 20,
1209            "VHT40": 40,
1210            "VHT80": 80
1211        }
1212        self.default_settings = {
1213            "region": "United States",
1214            "brand": "Google",
1215            "model": "Wifi",
1216            "hostapd_profile": "whirlwind",
1217            "status_2G": 0,
1218            "status_5G_1": 0,
1219            "ssid_2G": "GoogleWifi_2G",
1220            "ssid_5G_1": "GoogleWifi_5G",
1221            "channel_2G": 11,
1222            "channel_5G_1": 149,
1223            "bandwidth_2G": "VHT20",
1224            "bandwidth_5G_1": "VHT20",
1225            "power_2G": "auto",
1226            "power_5G_1": "auto",
1227            "mode_2G": None,
1228            "num_streams_2G": None,
1229            "rate_2G": "auto",
1230            "short_gi_2G": 0,
1231            "mode_5G_1": None,
1232            "num_streams_5G_1": None,
1233            "rate_5G_1": "auto",
1234            "short_gi_5G_1": 0,
1235            "security_type_2G": "Open",
1236            "security_type_5G_1": "Open",
1237            "subnet_2G": "192.168.1.0/24",
1238            "subnet_5G_1": "192.168.9.0/24",
1239            "password_2G": "password",
1240            "password_5G_1": "password"
1241        }
1242
1243        for setting in self.default_settings.keys():
1244            if setting not in self.ap_settings:
1245                self.log.debug(
1246                    "{0} not found during init. Setting {0} = {1}".format(
1247                        setting, self.default_settings[setting]))
1248                self.ap_settings[setting] = self.default_settings[setting]
1249        init_settings = self.ap_settings.copy()
1250        init_settings["ap_subnet"] = {
1251            "2g": self.ap_settings["subnet_2G"],
1252            "5g": self.ap_settings["subnet_5G_1"]
1253        }
1254        self.access_point = access_point.AccessPoint(init_settings)
1255        self.configure_ap()
1256
1257    def read_ap_settings(self):
1258        """Function that reads current ap settings."""
1259        return self.ap_settings.copy()
1260
1261    def update_ap_settings(self, dict_settings={}, **named_settings):
1262        """Function to update settings of existing AP.
1263
1264        Function copies arguments into ap_settings and calls configure_ap
1265        to apply them.
1266
1267        Args:
1268            dict_settings: single dictionary of settings to update
1269            **named_settings: named settings to update
1270            Note: dict and named_settings cannot contain the same settings.
1271        """
1272        settings_to_update = dict(dict_settings, **named_settings)
1273        if len(settings_to_update) != len(dict_settings) + len(named_settings):
1274            raise KeyError("The following keys were passed twice: {}".format(
1275                (set(dict_settings.keys()).intersection(
1276                    set(named_settings.keys())))))
1277        if not set(settings_to_update.keys()).issubset(
1278                set(self.ap_settings.keys())):
1279            raise KeyError(
1280                "The following settings are invalid for this AP: {}".format(
1281                    set(settings_to_update.keys()).difference(
1282                        set(self.ap_settings.keys()))))
1283
1284        updating_2G = any(["2G" in x for x in settings_to_update.keys()])
1285        updating_5G_1 = any(["5G_1" in x for x in settings_to_update.keys()])
1286        if updating_2G and updating_5G_1:
1287            raise ValueError(
1288                "Error updating Google WiFi AP. "
1289                "One interface can be activated and updated at a time")
1290        elif updating_2G:
1291            # If updating an interface and not explicitly setting its status,
1292            # it is assumed that the interface is to be ENABLED and updated
1293            if "status_2G" not in settings_to_update:
1294                settings_to_update["status_2G"] = 1
1295                settings_to_update["status_5G_1"] = 0
1296        elif updating_5G_1:
1297            if "status_5G_1" not in settings_to_update:
1298                settings_to_update["status_2G"] = 0
1299                settings_to_update["status_5G_1"] = 1
1300
1301        updates_requested = False
1302        for setting, value in settings_to_update.items():
1303            if self.ap_settings[setting] != value:
1304                self.ap_settings[setting] = value
1305                updates_requested = True
1306
1307        if updates_requested:
1308            self.configure_ap()
1309
1310    def configure_ap(self):
1311        """Function to configure Google Wifi."""
1312        self.log.info("Stopping Google Wifi interfaces.")
1313        self.access_point.stop_all_aps()
1314
1315        if self.ap_settings["status_2G"] == 1:
1316            network = "2G"
1317            self.log.info("Bringing up 2.4 GHz network.")
1318        elif self.ap_settings["status_5G_1"] == 1:
1319            network = "5G_1"
1320            self.log.info("Bringing up 5 GHz network.")
1321        else:
1322            return
1323
1324        bss_settings = []
1325        ssid = self.ap_settings["ssid_{}".format(network)]
1326        security_mode = self.ap_settings["security_type_{}".format(
1327            network)].lower()
1328        if "wpa" in security_mode:
1329            password = self.ap_settings["password_{}".format(network)]
1330            security = hostapd_security.Security(security_mode=security_mode,
1331                                                 password=password)
1332        else:
1333            security = hostapd_security.Security(security_mode=None,
1334                                                 password=None)
1335        channel = int(self.ap_settings["channel_{}".format(network)])
1336        bandwidth = self.BW_MODE_MAP[self.ap_settings["bandwidth_{}".format(
1337            network)]]
1338        config = hostapd_ap_preset.create_ap_preset(
1339            channel=channel,
1340            ssid=ssid,
1341            security=security,
1342            bss_settings=bss_settings,
1343            vht_bandwidth=bandwidth,
1344            profile_name=self.ap_settings["hostapd_profile"],
1345            iface_wlan_2g=self.access_point.wlan_2g,
1346            iface_wlan_5g=self.access_point.wlan_5g)
1347        config_bridge = self.access_point.generate_bridge_configs(channel)
1348        brconfigs = bridge_interface.BridgeInterfaceConfigs(
1349            config_bridge[0], "lan0", config_bridge[2])
1350        self.access_point.bridge.startup(brconfigs)
1351        self.access_point.start_ap(config)
1352        self.set_power(network, self.ap_settings["power_{}".format(network)])
1353        self.set_rate(
1354            network,
1355            mode=self.ap_settings["mode_{}".format(network)],
1356            num_streams=self.ap_settings["num_streams_{}".format(network)],
1357            rate=self.ap_settings["rate_{}".format(network)],
1358            short_gi=self.ap_settings["short_gi_{}".format(network)])
1359        self.log.info("AP started on channel {} with SSID {}".format(
1360            channel, ssid))
1361
1362    def set_power(self, network, power):
1363        """Function that sets network transmit power.
1364
1365        Args:
1366            network: string containing network identifier (2G, 5G_1, 5G_2)
1367            power: power level in dBm
1368        """
1369        if power == "auto":
1370            power_string = "auto"
1371        else:
1372            if not float(power).is_integer():
1373                self.log.info(
1374                    "Power in dBm must be an integer. Setting to {}".format(
1375                        int(power)))
1376            power = int(power)
1377            power_string = "fixed {}".format(int(power) * 100)
1378
1379        if "2G" in network:
1380            interface = self.access_point.wlan_2g
1381            self.ap_settings["power_2G"] = power
1382        elif "5G_1" in network:
1383            interface = self.access_point.wlan_5g
1384            self.ap_settings["power_5G_1"] = power
1385        self.access_point.ssh.run("iw dev {} set txpower {}".format(
1386            interface, power_string))
1387
1388    def set_rate(self,
1389                 network,
1390                 mode=None,
1391                 num_streams=None,
1392                 rate='auto',
1393                 short_gi=0):
1394        """Function that sets rate.
1395
1396        Args:
1397            network: string containing network identifier (2G, 5G_1, 5G_2)
1398            mode: string indicating the WiFi standard to use
1399            num_streams: number of MIMO streams. used only for VHT
1400            rate: data rate of MCS index to use
1401            short_gi: boolean controlling the use of short guard interval
1402        """
1403        if "2G" in network:
1404            interface = self.access_point.wlan_2g
1405            interface_short = "2.4"
1406            self.ap_settings["mode_2G"] = mode
1407            self.ap_settings["num_streams_2G"] = num_streams
1408            self.ap_settings["rate_2G"] = rate
1409            self.ap_settings["short_gi_2G"] = short_gi
1410        elif "5G_1" in network:
1411            interface = self.access_point.wlan_5g
1412            interface_short = "5"
1413            self.ap_settings["mode_5G_1"] = mode
1414            self.ap_settings["num_streams_5G_1"] = num_streams
1415            self.ap_settings["rate_5G_1"] = rate
1416            self.ap_settings["short_gi_5G_1"] = short_gi
1417
1418        if rate == "auto":
1419            cmd_string = "iw dev {0} set bitrates".format(interface)
1420        elif "legacy" in mode.lower():
1421            cmd_string = "iw dev {0} set bitrates legacy-{1} {2} ht-mcs-{1} vht-mcs-{1}".format(
1422                interface, interface_short, rate)
1423        elif "vht" in mode.lower():
1424            cmd_string = "iw dev {0} set bitrates legacy-{1} ht-mcs-{1} vht-mcs-{1} {2}:{3}".format(
1425                interface, interface_short, num_streams, rate)
1426            if short_gi:
1427                cmd_string = cmd_string + " sgi-{}".format(interface_short)
1428        elif "ht" in mode.lower():
1429            cmd_string = "iw dev {0} set bitrates legacy-{1} ht-mcs-{1} {2} vht-mcs-{1}".format(
1430                interface, interface_short, rate)
1431            if short_gi:
1432                cmd_string = cmd_string + " sgi-{}".format(interface_short)
1433        self.access_point.ssh.run(cmd_string)
1434