Merge "Set scipy<1.11 for Python versions below 3.9"
diff --git a/acts/framework/acts/controllers/openwrt_ap.py b/acts/framework/acts/controllers/openwrt_ap.py
index 1d3ad93..797172f 100644
--- a/acts/framework/acts/controllers/openwrt_ap.py
+++ b/acts/framework/acts/controllers/openwrt_ap.py
@@ -3,11 +3,13 @@
 import random
 import re
 import time
+import logging
 
 from acts import logger
 from acts import signals
 from acts.controllers.ap_lib import hostapd_constants
 from acts.controllers.openwrt_lib import network_settings
+from acts.controllers.openwrt_lib import openwrt_authentication
 from acts.controllers.openwrt_lib import wireless_config
 from acts.controllers.openwrt_lib import wireless_settings_applier
 from acts.controllers.openwrt_lib.openwrt_constants import OpenWrtModelMap as modelmap
@@ -110,14 +112,26 @@
 
   def __init__(self, config):
     """Initialize AP."""
-    self.ssh_settings = settings.from_config(config["ssh_config"])
-    self.ssh = connection.SshConnection(self.ssh_settings)
+    try:
+      self.ssh_settings = settings.from_config(config["ssh_config"])
+      self.ssh = connection.SshConnection(self.ssh_settings)
+      self.ssh.setup_master_ssh()
+    except connection.Error:
+      logging.info("OpenWrt AP instance is not initialized, use SSH Auth...")
+      openwrt_auth = openwrt_authentication.OpenWrtAuth(
+        self.ssh_settings.hostname)
+      openwrt_auth.generate_rsa_key()
+      openwrt_auth.send_public_key_to_remote_host()
+      self.ssh_settings.identity_file = openwrt_auth.private_key_file
+      self.ssh = connection.SshConnection(self.ssh_settings)
+      self.ssh.setup_master_ssh()
     self.log = logger.create_logger(
-        lambda msg: "[OpenWrtAP|%s] %s" % (self.ssh_settings.hostname, msg))
+      lambda msg: "[OpenWrtAP|%s] %s" % (self.ssh_settings.hostname, msg))
     self.wireless_setting = None
     self.network_setting = network_settings.NetworkSettings(
         self.ssh, self.ssh_settings, self.log)
     self.model = self.get_model_name()
+    self.log.info("OpenWrt AP: %s has been initiated." % self.model)
     if self.model in modelmap.__dict__:
       self.radios = modelmap.__dict__[self.model]
     else:
@@ -637,8 +651,10 @@
 
   def close(self):
     """Reset wireless and network settings to default and stop AP."""
-    if self.network_setting.config:
+    try:
       self.network_setting.cleanup_network_settings()
+    except AttributeError as e:
+      self.log.warning("OpenWrtAP object has no attribute 'network_setting'")
     if self.wireless_setting:
       self.wireless_setting.cleanup_wireless_settings()
 
@@ -649,4 +665,3 @@
   def reboot(self):
     """Reboot Openwrt."""
     self.ssh.run("reboot")
-
diff --git a/acts/framework/acts/controllers/openwrt_lib/openwrt_authentication.py b/acts/framework/acts/controllers/openwrt_lib/openwrt_authentication.py
new file mode 100644
index 0000000..bdc7b97
--- /dev/null
+++ b/acts/framework/acts/controllers/openwrt_lib/openwrt_authentication.py
@@ -0,0 +1,98 @@
+import logging
+import os
+import paramiko
+
+
+_REMOTE_PATH = '/etc/dropbear/authorized_keys'
+
+
+class OpenWrtAuth:
+  """
+  A class for managing SSH authentication for OpenWrt devices.
+  """
+  def __init__(self, hostname, username='root', password='root', port=22):
+    """
+    Initializes a new instance of the OpenWrtAuth class.
+
+    Args:
+      hostname (str): The hostname or IP address of the remote device.
+      username (str): The username for authentication.
+      password (str): The password for authentication.
+      port (int): The port number for SSH.
+
+    Attributes:
+      public_key (str): The generated public key.
+      public_key_file (str): The path to the generated public key file.
+      private_key_file (str): The path to the generated private key file.
+    """
+    self.hostname = hostname
+    self.username = username
+    self.password = password
+    self.port = port
+    self.public_key = None
+    self.public_key_file = None
+    self.private_key_file = None
+
+  def generate_rsa_key(self):
+    """
+    Generates an RSA key pair and saves it to the specified directory.
+
+    Raises:
+      ValueError: If an error occurs while generating the RSA key pair.
+      paramiko.SSHException: If an error occurs while generating the RSA key pair.
+      FileNotFoundError: If the directory for saving the private or public key does not exist.
+      PermissionError: If there is a permission error while creating the directory for saving the keys.
+      Exception: If an unexpected error occurs while generating the RSA key pair.
+    """
+    try:
+      # Generates an RSA key pair in /tmp/openwrt/ directory.
+      logging.info("Generating RSA key pair...")
+      key = paramiko.RSAKey.generate(bits=2048)
+      self.public_key = f"ssh-rsa {key.get_base64()}"
+      logging.info(f"Public key: {self.public_key}")
+
+      # Create /tmp/openwrt/ directory if it doesn't exist.
+      logging.info("Creating /tmp/openwrt/ directory...")
+      os.makedirs('/tmp/openwrt/', exist_ok=True)
+
+      # Saves the private key to a file.
+      self.private_key_file = '/tmp/openwrt/id_rsa'
+      key.write_private_key_file(self.private_key_file)
+
+      # Saves the public key to a file.
+      self.public_key_file = '/tmp/openwrt/id_rsa.pub'
+      with open(self.public_key_file, "w") as f:
+          f.write(self.public_key)
+      logging.info(f"Saved public key to file: {self.public_key_file}")
+    except (ValueError, paramiko.SSHException, PermissionError) as e:
+      logging.error(f"An error occurred while generating the RSA key pair: {e}")
+    except Exception as e:
+      logging.error(f"An unexpected error occurred while generating the RSA key pair: {e}")
+
+  def send_public_key_to_remote_host(self):
+    """
+    Uploads the public key to the remote host.
+
+    Raises:
+      paramiko.AuthenticationException: If authentication to the remote host fails.
+      paramiko.SSHException: If an SSH-related error occurs during the connection.
+      FileNotFoundError: If the public key file or the private key file does not exist.
+      Exception: If an unexpected error occurs while sending the public key.
+    """
+    try:
+      # Connects to the remote host and uploads the public key.
+      logging.info(f"Uploading public key to remote host {self.hostname}...")
+      with paramiko.SSHClient() as ssh:
+        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+        ssh.connect(hostname=self.hostname,
+                    port=self.port,
+                    username=self.username,
+                    password=self.password,
+                    key_filename='/tmp/openwrt/id_rsa')
+        with ssh.open_sftp() as sftp:
+          sftp.put(self.public_key_file, _REMOTE_PATH)
+      logging.info('Public key uploaded successfully.')
+    except (paramiko.AuthenticationException, paramiko.SSHException, FileNotFoundError) as e:
+      logging.error(f"An error occurred while sending the public key: {e}")
+    except Exception as e:
+      logging.error(f"An unexpected error occurred while sending the public key: {e}")
diff --git a/acts/framework/acts/controllers/openwrt_lib/openwrt_constants.py b/acts/framework/acts/controllers/openwrt_lib/openwrt_constants.py
index c0a408d..a29f89b 100644
--- a/acts/framework/acts/controllers/openwrt_lib/openwrt_constants.py
+++ b/acts/framework/acts/controllers/openwrt_lib/openwrt_constants.py
@@ -36,3 +36,4 @@
 
 class OpenWrtModelMap:
   NETGEAR_R8000 = ("radio2", "radio1")
+  TOTOLINK_X5000R = ("radio1", "radio0")