1"""Controller for Open WRT access point."""
2
3import ast
4import logging
5import random
6import re
7import time
8
9from acts import logger
10from acts import signals
11from acts.controllers.ap_lib import hostapd_constants
12from acts.controllers.openwrt_lib import network_settings
13from acts.controllers.openwrt_lib import openwrt_authentication
14from acts.controllers.openwrt_lib import wireless_config
15from acts.controllers.openwrt_lib import wireless_settings_applier
16from acts.controllers.openwrt_lib.openwrt_constants import OpenWrtModelMap as modelmap
17from acts.controllers.openwrt_lib.openwrt_constants import OpenWrtWifiSetting
18from acts.controllers.openwrt_lib.openwrt_constants import SYSTEM_INFO_CMD
19from acts.controllers.utils_lib.ssh import connection
20from acts.controllers.utils_lib.ssh import settings
21import yaml
22
23
24MOBLY_CONTROLLER_CONFIG_NAME = "OpenWrtAP"
25ACTS_CONTROLLER_REFERENCE_NAME = "access_points"
26OPEN_SECURITY = "none"
27PSK1_SECURITY = "psk"
28PSK_SECURITY = "psk2"
29WEP_SECURITY = "wep"
30ENT_SECURITY = "wpa2"
31OWE_SECURITY = "owe"
32SAE_SECURITY = "sae"
33SAEMIXED_SECURITY = "sae-mixed"
34ENABLE_RADIO = "0"
35PMF_ENABLED = 2
36WIFI_2G = "wifi2g"
37WIFI_5G = "wifi5g"
38WAIT_TIME = 20
39DEFAULT_RADIOS = ("radio0", "radio1")
40
41
42def create(configs):
43  """Creates ap controllers from a json config.
44
45  Creates an ap controller from either a list, or a single element. The element
46  can either be just the hostname or a dictionary containing the hostname and
47  username of the AP to connect to over SSH.
48
49  Args:
50    configs: The json configs that represent this controller.
51
52  Returns:
53    AccessPoint object
54
55  Example:
56    Below is the config file entry for OpenWrtAP as a list. A testbed can have
57    1 or more APs to configure. Each AP has a "ssh_config" key to provide SSH
58    login information. OpenWrtAP#__init__() uses this to create SSH object.
59
60      "OpenWrtAP": [
61        {
62          "ssh_config": {
63            "user" : "root",
64            "host" : "192.168.1.1"
65          }
66        },
67        {
68          "ssh_config": {
69            "user" : "root",
70            "host" : "192.168.1.2"
71          }
72        }
73      ]
74  """
75  return [OpenWrtAP(c) for c in configs]
76
77
78def destroy(aps):
79  """Destroys a list of AccessPoints.
80
81  Args:
82    aps: The list of AccessPoints to destroy.
83  """
84  for ap in aps:
85    ap.close()
86    ap.close_ssh()
87
88
89def get_info(aps):
90  """Get information on a list of access points.
91
92  Args:
93    aps: A list of AccessPoints.
94
95  Returns:
96    A list of all aps hostname.
97  """
98  return [ap.ssh_settings.hostname for ap in aps]
99
100
101class OpenWrtAP(object):
102  """An AccessPoint controller.
103
104  Attributes:
105    ssh: The ssh connection to the AP.
106    ssh_settings: The ssh settings being used by the ssh connection.
107    log: Logging object for AccessPoint.
108    wireless_setting: object holding wireless configuration.
109    network_setting: Object for network configuration.
110    model: OpenWrt HW model.
111    radios: Fit interface for test.
112  """
113
114  def __init__(self, config):
115    """Initialize AP."""
116    try:
117      self.ssh_settings = settings.from_config(config["ssh_config"])
118      self.ssh = connection.SshConnection(self.ssh_settings)
119      self.ssh.setup_master_ssh()
120    except connection.Error:
121      logging.info("OpenWrt AP instance is not initialized, use SSH Auth...")
122      openwrt_auth = openwrt_authentication.OpenWrtAuth(
123        self.ssh_settings.hostname)
124      openwrt_auth.generate_rsa_key()
125      openwrt_auth.send_public_key_to_remote_host()
126      self.ssh_settings.identity_file = openwrt_auth.private_key_file
127      self.ssh = connection.SshConnection(self.ssh_settings)
128      self.ssh.setup_master_ssh()
129    self.log = logger.create_logger(
130      lambda msg: "[OpenWrtAP|%s] %s" % (self.ssh_settings.hostname, msg))
131    self.wireless_setting = None
132    self.network_setting = network_settings.NetworkSettings(
133        self.ssh, self.ssh_settings, self.log)
134    self.model = self.get_model_name()
135    self.log.info("OpenWrt AP: %s has been initiated." % self.model)
136    if self.model in modelmap.__dict__:
137      self.radios = modelmap.__dict__[self.model]
138    else:
139      self.radios = DEFAULT_RADIOS
140
141  def configure_ap(self, wifi_configs, channel_2g, channel_5g):
142    """Configure AP with the required settings.
143
144    Each test class inherits WifiBaseTest. Based on the test, we may need to
145    configure PSK, WEP, OPEN, ENT networks on 2G and 5G bands in any
146    combination. We call WifiBaseTest methods get_psk_network(),
147    get_open_network(), get_wep_network() and get_ent_network() to create
148    dictionaries which contains this information. 'wifi_configs' is a list of
149    such dictionaries. Example below configures 2 WiFi networks - 1 PSK 2G and
150    1 Open 5G on one AP. configure_ap() is called from WifiBaseTest to
151    configure the APs.
152
153    wifi_configs = [
154      {
155        '2g': {
156          'SSID': '2g_AkqXWPK4',
157          'security': 'psk2',
158          'password': 'YgYuXqDO9H',
159          'hiddenSSID': False
160        },
161      },
162      {
163        '5g': {
164          'SSID': '5g_8IcMR1Sg',
165          'security': 'none',
166          'hiddenSSID': False
167        },
168      }
169    ]
170
171    Args:
172      wifi_configs: list of network settings for 2G and 5G bands.
173      channel_2g: channel for 2G band.
174      channel_5g: channel for 5G band.
175    """
176    # generate wifi configs to configure
177    wireless_configs = self.generate_wireless_configs(wifi_configs)
178    self.wireless_setting = wireless_settings_applier.WirelessSettingsApplier(
179        self.ssh, wireless_configs, channel_2g, channel_5g, self.radios[1], self.radios[0])
180    self.wireless_setting.apply_wireless_settings()
181
182  def start_ap(self):
183    """Starts the AP with the settings in /etc/config/wireless."""
184    self.log.info("wifi up")
185    self.ssh.run("wifi up")
186    curr_time = time.time()
187    while time.time() < curr_time + WAIT_TIME:
188      if self.get_wifi_status():
189        return
190      time.sleep(3)
191    if not self.get_wifi_status():
192      raise ValueError("Failed to turn on WiFi on the AP.")
193
194  def stop_ap(self):
195    """Stops the AP."""
196    self.log.info("wifi down")
197    self.ssh.run("wifi down")
198    curr_time = time.time()
199    while time.time() < curr_time + WAIT_TIME:
200      if not self.get_wifi_status():
201        return
202      time.sleep(3)
203    if self.get_wifi_status():
204      raise ValueError("Failed to turn off WiFi on the AP.")
205
206  def get_bssids_for_wifi_networks(self):
207    """Get BSSIDs for wifi networks configured.
208
209    Returns:
210      Dictionary of SSID - BSSID map for both bands.
211    """
212    bssid_map = {"2g": {}, "5g": {}}
213    for radio in self.radios:
214      ssid_ifname_map = self.get_ifnames_for_ssids(radio)
215      if radio == self.radios[0]:
216        for ssid, ifname in ssid_ifname_map.items():
217          bssid_map["5g"][ssid] = self.get_bssid(ifname)
218      elif radio == self.radios[1]:
219        for ssid, ifname in ssid_ifname_map.items():
220          bssid_map["2g"][ssid] = self.get_bssid(ifname)
221    return bssid_map
222
223  def get_ifnames_for_ssids(self, radio):
224    """Get interfaces for wifi networks.
225
226    Args:
227      radio: 2g or 5g radio get the bssids from.
228
229    Returns:
230      dictionary of ssid - ifname mappings.
231    """
232    ssid_ifname_map = {}
233    str_output = self.ssh.run("wifi status %s" % radio).stdout
234    wifi_status = yaml.load(str_output.replace("\t", "").replace("\n", ""),
235                            Loader=yaml.SafeLoader)
236    wifi_status = wifi_status[radio]
237    if wifi_status["up"]:
238      interfaces = wifi_status["interfaces"]
239      for config in interfaces:
240        ssid = config["config"]["ssid"]
241        ifname = config["ifname"]
242        ssid_ifname_map[ssid] = ifname
243    return ssid_ifname_map
244
245  def get_bssid(self, ifname):
246    """Get MAC address from an interface.
247
248    Args:
249      ifname: interface name of the corresponding MAC.
250
251    Returns:
252      BSSID of the interface.
253    """
254    ifconfig = self.ssh.run("ifconfig %s" % ifname).stdout
255    mac_addr = ifconfig.split("\n")[0].split()[-1]
256    return mac_addr
257
258  def set_wpa_encryption(self, encryption):
259    """Set different encryptions to wpa or wpa2.
260
261    Args:
262      encryption: ccmp, tkip, or ccmp+tkip.
263    """
264    str_output = self.ssh.run("wifi status").stdout
265    wifi_status = yaml.load(str_output.replace("\t", "").replace("\n", ""),
266                            Loader=yaml.SafeLoader)
267
268    # Counting how many interface are enabled.
269    total_interface = 0
270    for radio in self.radios:
271      num_interface = len(wifi_status[radio]["interfaces"])
272      total_interface += num_interface
273
274    # Iterates every interface to get and set wpa encryption.
275    default_extra_interface = 2
276    for i in range(total_interface + default_extra_interface):
277      origin_encryption = self.ssh.run(
278          "uci get wireless.@wifi-iface[{}].encryption".format(i)).stdout
279      origin_psk_pattern = re.match(r"psk\b", origin_encryption)
280      target_psk_pattern = re.match(r"psk\b", encryption)
281      origin_psk2_pattern = re.match(r"psk2\b", origin_encryption)
282      target_psk2_pattern = re.match(r"psk2\b", encryption)
283
284      if origin_psk_pattern == target_psk_pattern:
285        self.ssh.run(
286            "uci set wireless.@wifi-iface[{}].encryption={}".format(
287                i, encryption))
288
289      if origin_psk2_pattern == target_psk2_pattern:
290        self.ssh.run(
291            "uci set wireless.@wifi-iface[{}].encryption={}".format(
292                i, encryption))
293
294    self.ssh.run("uci commit wireless")
295    self.ssh.run("wifi")
296
297  def set_password(self, pwd_5g=None, pwd_2g=None):
298    """Set password for individual interface.
299
300    Args:
301        pwd_5g: 8 ~ 63 chars, ascii letters and digits password for 5g network.
302        pwd_2g: 8 ~ 63 chars, ascii letters and digits password for 2g network.
303    """
304    if pwd_5g:
305      if len(pwd_5g) < 8 or len(pwd_5g) > 63:
306        self.log.error("Password must be 8~63 characters long")
307      # Only accept ascii letters and digits
308      elif not re.match("^[A-Za-z0-9]*$", pwd_5g):
309        self.log.error("Password must only contains ascii letters and digits")
310      else:
311        self.ssh.run(
312            "uci set wireless.@wifi-iface[{}].key={}".format(3, pwd_5g))
313        self.log.info("Set 5G password to :{}".format(pwd_5g))
314
315    if pwd_2g:
316      if len(pwd_2g) < 8 or len(pwd_2g) > 63:
317        self.log.error("Password must be 8~63 characters long")
318      # Only accept ascii letters and digits
319      elif not re.match("^[A-Za-z0-9]*$", pwd_2g):
320        self.log.error("Password must only contains ascii letters and digits")
321      else:
322        self.ssh.run(
323            "uci set wireless.@wifi-iface[{}].key={}".format(2, pwd_2g))
324        self.log.info("Set 2G password to :{}".format(pwd_2g))
325
326    self.ssh.run("uci commit wireless")
327    self.ssh.run("wifi")
328
329  def set_ssid(self, ssid_5g=None, ssid_2g=None):
330    """Set SSID for individual interface.
331
332    Args:
333        ssid_5g: 8 ~ 63 chars for 5g network.
334        ssid_2g: 8 ~ 63 chars for 2g network.
335    """
336    if ssid_5g:
337      if len(ssid_5g) < 8 or len(ssid_5g) > 63:
338        self.log.error("SSID must be 8~63 characters long")
339      # Only accept ascii letters and digits
340      else:
341        self.ssh.run(
342            "uci set wireless.@wifi-iface[{}].ssid={}".format(3, ssid_5g))
343        self.log.info("Set 5G SSID to :{}".format(ssid_5g))
344
345    if ssid_2g:
346      if len(ssid_2g) < 8 or len(ssid_2g) > 63:
347        self.log.error("SSID must be 8~63 characters long")
348      # Only accept ascii letters and digits
349      else:
350        self.ssh.run(
351            "uci set wireless.@wifi-iface[{}].ssid={}".format(2, ssid_2g))
352        self.log.info("Set 2G SSID to :{}".format(ssid_2g))
353
354    self.ssh.run("uci commit wireless")
355    self.ssh.run("wifi")
356
357  def generate_mobility_domain(self):
358    """Generate 4-character hexadecimal ID.
359
360    Returns:
361      String; a 4-character hexadecimal ID.
362    """
363    md = "{:04x}".format(random.getrandbits(16))
364    self.log.info("Mobility Domain ID: {}".format(md))
365    return md
366
367  def enable_80211r(self, iface, md):
368    """Enable 802.11r for one single radio.
369
370    Args:
371      iface: index number of wifi-iface.
372              2: radio1
373              3: radio0
374      md: mobility domain. a 4-character hexadecimal ID.
375    Raises:
376      TestSkip if 2g or 5g radio is not up or 802.11r is not enabled.
377    """
378    str_output = self.ssh.run("wifi status").stdout
379    wifi_status = yaml.load(str_output.replace("\t", "").replace("\n", ""),
380                            Loader=yaml.SafeLoader)
381    # Check if the radio is up.
382    if iface == OpenWrtWifiSetting.IFACE_2G:
383      if wifi_status[self.radios[1]]["up"]:
384        self.log.info("2g network is ENABLED")
385      else:
386        raise signals.TestSkip("2g network is NOT ENABLED")
387    elif iface == OpenWrtWifiSetting.IFACE_5G:
388      if wifi_status[self.radios[0]]["up"]:
389        self.log.info("5g network is ENABLED")
390      else:
391        raise signals.TestSkip("5g network is NOT ENABLED")
392
393    # Setup 802.11r.
394    self.ssh.run(
395        "uci set wireless.@wifi-iface[{}].ieee80211r='1'".format(iface))
396    self.ssh.run(
397        "uci set wireless.@wifi-iface[{}].ft_psk_generate_local='1'"
398        .format(iface))
399    self.ssh.run(
400        "uci set wireless.@wifi-iface[{}].mobility_domain='{}'"
401        .format(iface, md))
402    self.ssh.run(
403        "uci commit wireless")
404    self.ssh.run("wifi")
405
406    # Check if 802.11r is enabled.
407    result = self.ssh.run(
408        "uci get wireless.@wifi-iface[{}].ieee80211r".format(iface)).stdout
409    if result == "1":
410      self.log.info("802.11r is ENABLED")
411    else:
412      raise signals.TestSkip("802.11r is NOT ENABLED")
413
414  def generate_wireless_configs(self, wifi_configs):
415    """Generate wireless configs to configure.
416
417    Converts wifi_configs from configure_ap() to a list of 'WirelessConfig'
418    objects. Each object represents a wifi network to configure on the AP.
419
420    Args:
421      wifi_configs: Network list of different security types and bands.
422
423    Returns:
424      wireless configuration for openwrt AP.
425    """
426    num_2g = 1
427    num_5g = 1
428    wireless_configs = []
429
430    for i in range(len(wifi_configs)):
431      if hostapd_constants.BAND_2G in wifi_configs[i]:
432        config = wifi_configs[i][hostapd_constants.BAND_2G]
433        if config["security"] == PSK_SECURITY:
434          wireless_configs.append(
435              wireless_config.WirelessConfig("%s%s" % (WIFI_2G, num_2g),
436                                             config["SSID"],
437                                             config["security"],
438                                             hostapd_constants.BAND_2G,
439                                             password=config["password"],
440                                             hidden=config["hiddenSSID"],
441                                             ieee80211w=config["ieee80211w"]))
442        elif config["security"] == PSK1_SECURITY:
443          wireless_configs.append(
444              wireless_config.WirelessConfig("%s%s" % (WIFI_2G, num_2g),
445                                             config["SSID"],
446                                             config["security"],
447                                             hostapd_constants.BAND_2G,
448                                             password=config["password"],
449                                             hidden=config["hiddenSSID"],
450                                             ieee80211w=config["ieee80211w"]))
451        elif config["security"] == WEP_SECURITY:
452          wireless_configs.append(
453              wireless_config.WirelessConfig("%s%s" % (WIFI_2G, num_2g),
454                                             config["SSID"],
455                                             config["security"],
456                                             hostapd_constants.BAND_2G,
457                                             wep_key=config["wepKeys"][0],
458                                             hidden=config["hiddenSSID"]))
459        elif config["security"] == OPEN_SECURITY:
460          wireless_configs.append(
461              wireless_config.WirelessConfig("%s%s" % (WIFI_2G, num_2g),
462                                             config["SSID"],
463                                             config["security"],
464                                             hostapd_constants.BAND_2G,
465                                             hidden=config["hiddenSSID"]))
466        elif config["security"] == OWE_SECURITY:
467          wireless_configs.append(
468              wireless_config.WirelessConfig("%s%s" % (WIFI_2G, num_2g),
469                                             config["SSID"],
470                                             config["security"],
471                                             hostapd_constants.BAND_2G,
472                                             hidden=config["hiddenSSID"],
473                                             ieee80211w=PMF_ENABLED))
474        elif config["security"] == SAE_SECURITY:
475          wireless_configs.append(
476              wireless_config.WirelessConfig("%s%s" % (WIFI_2G, num_2g),
477                                             config["SSID"],
478                                             config["security"],
479                                             hostapd_constants.BAND_2G,
480                                             password=config["password"],
481                                             hidden=config["hiddenSSID"],
482                                             ieee80211w=PMF_ENABLED))
483        elif config["security"] == SAEMIXED_SECURITY:
484          wireless_configs.append(
485              wireless_config.WirelessConfig("%s%s" % (WIFI_2G, num_2g),
486                                             config["SSID"],
487                                             config["security"],
488                                             hostapd_constants.BAND_2G,
489                                             password=config["password"],
490                                             hidden=config["hiddenSSID"],
491                                             ieee80211w=config["ieee80211w"]))
492        elif config["security"] == ENT_SECURITY:
493          wireless_configs.append(
494              wireless_config.WirelessConfig(
495                  "%s%s" % (WIFI_2G, num_2g),
496                  config["SSID"],
497                  config["security"],
498                  hostapd_constants.BAND_2G,
499                  radius_server_ip=config["radius_server_ip"],
500                  radius_server_port=config["radius_server_port"],
501                  radius_server_secret=config["radius_server_secret"],
502                  hidden=config["hiddenSSID"]))
503        num_2g += 1
504      if hostapd_constants.BAND_5G in wifi_configs[i]:
505        config = wifi_configs[i][hostapd_constants.BAND_5G]
506        if config["security"] == PSK_SECURITY:
507          wireless_configs.append(
508              wireless_config.WirelessConfig("%s%s" % (WIFI_5G, num_5g),
509                                             config["SSID"],
510                                             config["security"],
511                                             hostapd_constants.BAND_5G,
512                                             password=config["password"],
513                                             hidden=config["hiddenSSID"],
514                                             ieee80211w=config["ieee80211w"]))
515        elif config["security"] == PSK1_SECURITY:
516          wireless_configs.append(
517              wireless_config.WirelessConfig("%s%s" % (WIFI_5G, num_5g),
518                                             config["SSID"],
519                                             config["security"],
520                                             hostapd_constants.BAND_5G,
521                                             password=config["password"],
522                                             hidden=config["hiddenSSID"],
523                                             ieee80211w=config["ieee80211w"]))
524        elif config["security"] == WEP_SECURITY:
525          wireless_configs.append(
526              wireless_config.WirelessConfig("%s%s" % (WIFI_5G, num_5g),
527                                             config["SSID"],
528                                             config["security"],
529                                             hostapd_constants.BAND_5G,
530                                             wep_key=config["wepKeys"][0],
531                                             hidden=config["hiddenSSID"]))
532        elif config["security"] == OPEN_SECURITY:
533          wireless_configs.append(
534              wireless_config.WirelessConfig("%s%s" % (WIFI_5G, num_5g),
535                                             config["SSID"],
536                                             config["security"],
537                                             hostapd_constants.BAND_5G,
538                                             hidden=config["hiddenSSID"]))
539        elif config["security"] == OWE_SECURITY:
540          wireless_configs.append(
541              wireless_config.WirelessConfig("%s%s" % (WIFI_5G, num_5g),
542                                             config["SSID"],
543                                             config["security"],
544                                             hostapd_constants.BAND_5G,
545                                             hidden=config["hiddenSSID"],
546                                             ieee80211w=PMF_ENABLED))
547        elif config["security"] == SAE_SECURITY:
548          wireless_configs.append(
549              wireless_config.WirelessConfig("%s%s" % (WIFI_5G, num_5g),
550                                             config["SSID"],
551                                             config["security"],
552                                             hostapd_constants.BAND_5G,
553                                             password=config["password"],
554                                             hidden=config["hiddenSSID"],
555                                             ieee80211w=PMF_ENABLED))
556        elif config["security"] == SAEMIXED_SECURITY:
557          wireless_configs.append(
558              wireless_config.WirelessConfig("%s%s" % (WIFI_5G, num_5g),
559                                             config["SSID"],
560                                             config["security"],
561                                             hostapd_constants.BAND_5G,
562                                             password=config["password"],
563                                             hidden=config["hiddenSSID"],
564                                             ieee80211w=config["ieee80211w"]))
565        elif config["security"] == ENT_SECURITY:
566          wireless_configs.append(
567              wireless_config.WirelessConfig(
568                  "%s%s" % (WIFI_5G, num_5g),
569                  config["SSID"],
570                  config["security"],
571                  hostapd_constants.BAND_5G,
572                  radius_server_ip=config["radius_server_ip"],
573                  radius_server_port=config["radius_server_port"],
574                  radius_server_secret=config["radius_server_secret"],
575                  hidden=config["hiddenSSID"]))
576        num_5g += 1
577
578    return wireless_configs
579
580  def get_wifi_network(self, security=None, band=None):
581    """Return first match wifi interface's config.
582
583    Args:
584      security: psk2 or none
585      band: '2g' or '5g'
586
587    Returns:
588      A dict contains match wifi interface's config.
589    """
590
591    for wifi_iface in self.wireless_setting.wireless_configs:
592      match_list = []
593      wifi_network = wifi_iface.__dict__
594      if security:
595        match_list.append(security == wifi_network["security"])
596      if band:
597        match_list.append(band == wifi_network["band"])
598
599      if all(match_list):
600        wifi_network["SSID"] = wifi_network["ssid"]
601        if not wifi_network["password"]:
602          del wifi_network["password"]
603        return wifi_network
604    return None
605
606  def get_wifi_status(self):
607    """Check if radios are up. Default are 2G and 5G bands.
608
609    Returns:
610      True if both radios are up. False if not.
611    """
612    status = True
613    for radio in self.radios:
614      try:
615        str_output = self.ssh.run("wifi status %s" % radio).stdout
616        wifi_status = yaml.load(str_output.replace("\t", "").replace("\n", ""),
617                                Loader=yaml.SafeLoader)
618        status = wifi_status[radio]["up"] and status
619      except:
620        self.log.info("Failed to make ssh connection to the OpenWrt")
621        return False
622    return status
623
624  def verify_wifi_status(self, timeout=20):
625    """Ensure wifi interfaces are ready.
626
627    Args:
628      timeout: An integer that is the number of times to try
629               wait for interface ready.
630    Returns:
631      True if both radios are up. False if not.
632    """
633    start_time = time.time()
634    end_time = start_time + timeout
635    while time.time() < end_time:
636      if self.get_wifi_status():
637        return True
638      time.sleep(1)
639    return False
640
641  def get_model_name(self):
642    """Get Openwrt model name.
643
644    Returns:
645      A string include device brand and model. e.g. NETGEAR_R8000
646    """
647    out = self.ssh.run(SYSTEM_INFO_CMD).stdout.split("\n")
648    for line in out:
649      if "board_name" in line:
650        line = (line.split()[1].strip("\",").split(","))
651        board_name = "_".join(map(lambda i: i.upper(), line))
652        filter_list = "-"
653        for i in filter_list:
654          board_name = str(board_name.replace(i, "_"))
655        return board_name
656    self.log.info("Failed to retrieve OpenWrt model information.")
657    return None
658
659  def get_version(self):
660    """Get Openwrt version.
661
662    Returns:
663      A string with version number.
664    """
665    out = self.ssh.run(SYSTEM_INFO_CMD).stdout
666    return ast.literal_eval(out)["release"]["version"]
667
668  def is_version_under_20(self):
669    """Boolean if version under 20."""
670    return int(self.get_version().split(".")[0]) < 20
671
672  def close(self):
673    """Reset wireless and network settings to default and stop AP."""
674    try:
675      self.network_setting.cleanup_network_settings()
676    except AttributeError as e:
677      self.log.warning("OpenWrtAP object has no attribute 'network_setting'")
678    if self.wireless_setting:
679      self.wireless_setting.cleanup_wireless_settings()
680
681  def close_ssh(self):
682    """Close SSH connection to AP."""
683    self.ssh.close()
684
685  def reboot(self):
686    """Reboot Openwrt."""
687    self.ssh.run("reboot")
688