Merge "Fixed a couple Fuchsia Device excemptions and add get_info to attenuator"
diff --git a/acts/framework/acts/controllers/attenuator.py b/acts/framework/acts/controllers/attenuator.py
index aa84801..9d99c2d 100644
--- a/acts/framework/acts/controllers/attenuator.py
+++ b/acts/framework/acts/controllers/attenuator.py
@@ -48,17 +48,16 @@
                 logging.error('Attempt %s to open connection to attenuator '
                               'failed: %s' % (attempt_number, e))
                 if attempt_number == _ATTENUATOR_OPEN_RETRIES:
-                    ping_output = job.run(
-                        'ping %s -c 1 -w 1' % ip_address, ignore_status=True)
+                    ping_output = job.run('ping %s -c 1 -w 1' % ip_address,
+                                          ignore_status=True)
                     if ping_output.exit_status == 1:
-                        logging.error(
-                            'Unable to ping attenuator at %s' % ip_address)
+                        logging.error('Unable to ping attenuator at %s' %
+                                      ip_address)
                     else:
-                        logging.error(
-                            'Able to ping attenuator at %s' % ip_address)
-                        job.run(
-                            'echo "q" | telnet %s %s' % (ip_address, port),
-                            ignore_status=True)
+                        logging.error('Able to ping attenuator at %s' %
+                                      ip_address)
+                        job.run('echo "q" | telnet %s %s' % (ip_address, port),
+                                ignore_status=True)
                     raise
         for i in range(inst_cnt):
             attn = Attenuator(attn_inst, idx=i)
@@ -72,13 +71,31 @@
     return objs
 
 
+def get_info(attenuators):
+    """Get information on a list of Attenuator objects.
+
+    Args:
+        attenuators: A list of Attenuator objects.
+
+    Returns:
+        A list of dict, each representing info for Attenuator objects.
+    """
+    device_info = []
+    for attenuator in attenuators:
+        info = {
+            "Address": attenuator.instrument.address,
+            "Attenuator_Port": attenuator.idx
+        }
+        device_info.append(info)
+    return device_info
+
+
 def destroy(objs):
     for attn in objs:
         attn.instrument.close()
 
 
-def get_attenuators_for_device(device_attenuator_configs,
-                               attenuators,
+def get_attenuators_for_device(device_attenuator_configs, attenuators,
                                attenuator_key):
     """Gets the list of attenuators associated to a specified device and builds
     a list of the attenuator objects associated to the ip address in the
@@ -139,11 +156,12 @@
         for attenuator_port in device_attenuator_config[attenuator_key]:
             for attenuator in attenuators:
                 if (attenuator.instrument.address ==
-                        device_attenuator_config['Address'] and
-                        attenuator.idx is attenuator_port):
+                        device_attenuator_config['Address']
+                        and attenuator.idx is attenuator_port):
                     attenuator_list.append(attenuator)
     return attenuator_list
 
+
 """Classes for accessing, managing, and manipulating attenuators.
 
 Users will instantiate a specific child class, but almost all operation should
@@ -244,7 +262,6 @@
     the physical implementation and allows the user to think only of attenuators
     regardless of their location.
     """
-
     def __init__(self, instrument, idx=0, offset=0):
         """This is the constructor for Attenuator
 
@@ -313,7 +330,6 @@
     convenience to the user and avoid re-implementation of helper functions and
     small loops scattered throughout user code.
     """
-
     def __init__(self, name=''):
         """This constructor for AttenuatorGroup
 
diff --git a/acts/framework/acts/controllers/fuchsia_device.py b/acts/framework/acts/controllers/fuchsia_device.py
index fd4aecd..998927d 100644
--- a/acts/framework/acts/controllers/fuchsia_device.py
+++ b/acts/framework/acts/controllers/fuchsia_device.py
@@ -14,6 +14,7 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
+import backoff
 import json
 import logging
 import platform
@@ -22,6 +23,7 @@
 import re
 import requests
 import subprocess
+import socket
 import time
 
 from acts import context
@@ -146,7 +148,6 @@
         log: A logger object.
         port: The TCP port number of the Fuchsia device.
     """
-
     def __init__(self, fd_conf_data):
         """
         Args:
@@ -230,13 +231,13 @@
         # Init server
         self.init_server_connection()
 
-    def init_server_connection(self, retry_count=3):
-        """Initializes HTTP connection with SL4F server.
-
-        Args:
-            retry_count: How many time to retry connecting assuming a
-                known error.
-        """
+    @backoff.on_exception(
+        backoff.constant,
+        (ConnectionRefusedError, requests.exceptions.ConnectionError),
+        interval=1.5,
+        max_tries=4)
+    def init_server_connection(self):
+        """Initializes HTTP connection with SL4F server."""
         self.log.debug("Initialziing server connection")
         init_data = json.dumps({
             "jsonrpc": "2.0",
@@ -246,28 +247,8 @@
                 "client_id": self.client_id
             }
         })
-        retry_counter = 0
-        while retry_counter < retry_count:
-            try:
-                requests.get(url=self.init_address, data=init_data)
-                retry_counter = retry_count + 1
-            except ConnectionRefusedError:
-                self.log.info('Connection Refused Error.  '
-                              'Retrying in 1 second.')
-                e = ConnectionRefusedError('Connection Refused Error.')
-                retry_counter += 1
-                time.sleep(1)
-            except requests.exceptions.ConnectionError:
-                self.log.info('Requests ConnectionError.  '
-                              'Retrying in 1 second.')
-                e = requests.exceptions.ConnectionError('Requests '
-                                                        'ConnectionError')
-                retry_counter += 1
-                time.sleep(1)
-            except Exception as e:
-                raise e
-        if retry_counter is retry_count:
-            raise e
+
+        requests.get(url=self.init_address, data=init_data)
         self.test_counter += 1
 
     def build_id(self, test_id):
@@ -315,7 +296,8 @@
         elif os_type == 'Linux':
             timeout_flag = '-W'
         else:
-            raise ValueError('Invalid OS.  Only Linux and MacOS are supported.')
+            raise ValueError(
+                'Invalid OS.  Only Linux and MacOS are supported.')
         ping_command = ['ping', '%s' % timeout_flag, '1', '-c', '1', self.ip]
         self.clean_up()
         self.log.info('Rebooting FuchsiaDevice %s' % self.ip)
@@ -331,7 +313,7 @@
         self.log.info('Waiting for FuchsiaDevice %s to come back up.' %
                       self.ip)
         self.log.debug('Waiting for FuchsiaDevice %s to stop responding'
-                      ' to pings.' % self.ip)
+                       ' to pings.' % self.ip)
         while True:
             initial_ping_status_code = subprocess.call(
                 ping_command,
@@ -340,28 +322,26 @@
             if initial_ping_status_code != 1:
                 break
             else:
-                initial_ping_elapsed_time = (
-                        time.time() - initial_ping_start_time)
+                initial_ping_elapsed_time = (time.time() -
+                                             initial_ping_start_time)
                 if initial_ping_elapsed_time > timeout:
                     try:
                         uptime = (int(
                             self.send_command_ssh(
                                 'clock --monotonic',
-                                timeout=
-                                FUCHSIA_RECONNECT_AFTER_REBOOT_TIME).stdout)
-                                / FUCHSIA_TIME_IN_NANOSECONDS)
+                                timeout=FUCHSIA_RECONNECT_AFTER_REBOOT_TIME).
+                            stdout) / FUCHSIA_TIME_IN_NANOSECONDS)
                     except Exception as e:
-                        self.log.debug('Unable to retrieve uptime from device.')
+                        self.log.info('Unable to retrieve uptime from device.')
                     # Device failed to restart within the specified period.
                     # Restart the services so other tests can continue.
                     self.start_services()
                     self.init_server_connection()
-                    raise TimeoutError('Waited %s seconds, and FuchsiaDevice %s'
-                                       ' never stopped responding to pings.'
-                                       ' Uptime reported as %s' %
-                                       (initial_ping_elapsed_time,
-                                        self.ip,
-                                        str(uptime)))
+                    raise TimeoutError(
+                        'Waited %s seconds, and FuchsiaDevice %s'
+                        ' never stopped responding to pings.'
+                        ' Uptime reported as %s' %
+                        (initial_ping_elapsed_time, self.ip, str(uptime)))
 
         start_time = time.time()
         self.log.debug('Waiting for FuchsiaDevice %s to start responding '
@@ -377,8 +357,8 @@
                 raise TimeoutError('Waited %s seconds, and FuchsiaDevice %s'
                                    'did not repond to a ping.' %
                                    (elapsed_time, self.ip))
-        self.log.debug('Received a ping back in %s seconds.'
-                       % str(time.time() - start_time))
+        self.log.debug('Received a ping back in %s seconds.' %
+                       str(time.time() - start_time))
         # Wait 5 seconds after receiving a ping packet to just to let
         # the OS get everything up and running.
         time.sleep(10)
@@ -656,15 +636,17 @@
                            disconnect_response.get("error"))
             return False
 
-    def start_services(self, skip_sl4f=False, retry_count=3):
+    @backoff.on_exception(backoff.constant,
+                          (FuchsiaSyslogError, socket.timeout),
+                          interval=1.5,
+                          max_tries=4)
+    def start_services(self, skip_sl4f=False):
         """Starts long running services on the Fuchsia device.
 
         1. Start SL4F if not skipped.
 
         Args:
             skip_sl4f: Does not attempt to start SL4F if True.
-            retry_count: How many time to retry connecting assuming a
-                known error.
         """
         self.log.debug("Attempting to start Fuchsia device services on %s." %
                        self.ip)
@@ -672,24 +654,9 @@
             self.log_process = start_syslog(self.serial, self.log_path,
                                             self.ip, self.ssh_username,
                                             self.ssh_config)
-            retry_counter = 0
-            while retry_counter < retry_count:
-                if ENABLE_LOG_LISTENER:
-                    try:
-                        self.log_process.start()
-                        retry_counter = retry_count + 1
-                    except FuchsiaSyslogError:
-                        self.log.info('Fuchsia Syslog Error.  '
-                                      'Retrying in 1 second.')
-                        e = FuchsiaSyslogError('Fuchsia Syslog Error')
-                        retry_counter += 1
-                        time.sleep(1)
-                    except Exception as e:
-                        raise e
-                else:
-                    retry_counter = retry_count + 1
-            if retry_counter is retry_count:
-                raise e
+
+            if ENABLE_LOG_LISTENER:
+                self.log_process.start()
 
             if not skip_sl4f:
                 self.control_daemon("sl4f.cmx", "start")
diff --git a/acts/framework/acts/controllers/fuchsia_lib/utils_lib.py b/acts/framework/acts/controllers/fuchsia_lib/utils_lib.py
index 6456a08..56fd4c2 100644
--- a/acts/framework/acts/controllers/fuchsia_lib/utils_lib.py
+++ b/acts/framework/acts/controllers/fuchsia_lib/utils_lib.py
@@ -14,9 +14,11 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
+import backoff
 import os
 import logging
 import paramiko
+import socket
 import time
 
 logging.getLogger("paramiko").setLevel(logging.WARNING)
@@ -51,11 +53,17 @@
     raise Exception('No valid ssh key type found', exceptions)
 
 
+@backoff.on_exception(
+    backoff.constant,
+    (paramiko.ssh_exception.SSHException,
+     paramiko.ssh_exception.AuthenticationException, socket.timeout,
+     socket.error, ConnectionRefusedError, ConnectionResetError),
+    interval=1.5,
+    max_tries=4)
 def create_ssh_connection(ip_address,
                           ssh_username,
                           ssh_config,
-                          connect_timeout=30,
-                          retry_count=3):
+                          connect_timeout=30):
     """Creates and ssh connection to a Fuchsia device
 
     Args:
@@ -63,8 +71,6 @@
         ssh_username: Username for ssh server.
         ssh_config: ssh_config location for the ssh server.
         connect_timeout: Timeout value for connecting to ssh_server.
-        retry_count: How many time to retry connecting assuming a
-            known error.
 
     Returns:
         A paramiko ssh object
@@ -72,31 +78,12 @@
     ssh_key = get_private_key(ip_address=ip_address, ssh_config=ssh_config)
     ssh_client = paramiko.SSHClient()
     ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-    retry_counter = 0
-    while retry_counter < retry_count:
-        try:
-            ssh_client.connect(hostname=ip_address,
-                               username=ssh_username,
-                               allow_agent=False,
-                               pkey=ssh_key,
-                               timeout=connect_timeout,
-                               banner_timeout=200)
-            retry_counter = retry_count + 1
-        except paramiko.ssh_exception.SSHException:
-            logging.info('Paramiko SSHException.  Retrying in 1 second.')
-            e = paramiko.ssh_exception.SSHException('Paramiko SSHException')
-            time.sleep(1)
-            retry_counter =+ 1
-        except ConnectionRefusedError:
-            logging.info('Connection Refused Error.  Retrying in 1 second.')
-            e = ConnectionRefusedError('Connection Refused Error')
-            time.sleep(1)
-            retry_counter =+ 1
-        except Exception as e:
-            raise e
-    if retry_counter is retry_count:
-        raise e
-
+    ssh_client.connect(hostname=ip_address,
+                       username=ssh_username,
+                       allow_agent=False,
+                       pkey=ssh_key,
+                       timeout=connect_timeout,
+                       banner_timeout=200)
     return ssh_client
 
 
diff --git a/acts/framework/setup.py b/acts/framework/setup.py
index 21b9dfe..59210f9 100755
--- a/acts/framework/setup.py
+++ b/acts/framework/setup.py
@@ -23,6 +23,7 @@
 import sys
 
 install_requires = [
+    'backoff',
     # Future needs to have a newer version that contains urllib.
     'future>=0.16.0',
     'mock',