Home | History | Annotate | Download | only in infra_ServoDiagnosis
      1 # Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import logging
      6 
      7 from autotest_lib.client.common_lib import error
      8 from autotest_lib.client.common_lib.cros import servo_afe_board_map
      9 from autotest_lib.server import test
     10 from autotest_lib.server.cros.servo import servo
     11 
     12 
     13 def _successful(result_value):
     14     return result_value and not isinstance(result_value, Exception)
     15 
     16 
     17 class _DiagnosticTest(object):
     18     """Data needed to handle one diagnostic test on a Servo host.
     19 
     20     The class encapsulates two basic elements:
     21      1. A pre-requisite test that must have passed.  The
     22         pre-requisite is recorded as a key in the results
     23         dictionary.
     24      2. A function that performs the actual diagnostic test.
     25 
     26     All tests have the implicit pre-requisite that the servo host
     27     can be reached on the network via ping.
     28 
     29     Pre-requisites are meant to capture relationships of the form
     30     "if test X cant't pass, test Y will always fail".  Typically,
     31     that means that test X tests a capability used by test Y.
     32 
     33     This implementation is a bit naive:  It assumes only a single
     34     pre-requisite, and it assumes the only outcome is a simple
     35     pass/fail.  The design also doesn't account for relationships
     36     of the form "if test X fails, run test Y to try and distinguish
     37     possible causes".
     38 
     39     """
     40 
     41     def __init__(self, prerequisite, get_result):
     42         self._prerequisite = prerequisite
     43         self._get_result = get_result
     44 
     45     def can_run(self, results):
     46         """Return whether this test's pre-requisite is satisfied.
     47 
     48         @param results The results dictionary with the status of
     49                        this test's pre-requisite.
     50 
     51         """
     52         if self._prerequisite is None:
     53             return True
     54         return _successful(results[self._prerequisite])
     55 
     56     def run_diagnostic(self, servo_host, servod):
     57         """Run the diagnostic test, and return the result.
     58 
     59         The test receives ServoHost and Servo objects to be tested;
     60         typically a single test uses one or the other, but not both.
     61 
     62         @param servo_host A ServoHost object to be the target of the
     63                           test.
     64         @param servod     A Servo object to be the target of the
     65                           test.
     66         @return If the test returns normally, return its result.  If
     67                 the test raises an exception, return the exception.
     68 
     69         """
     70         try:
     71             return self._get_result(servo_host, servod)
     72         except Exception as e:
     73             return e
     74 
     75 
     76 def _ssh_test(servo_host, servod):
     77     """Test whether the servo host answers to ssh.
     78 
     79     This test serves as a basic pre-requisite for tests that
     80     use ssh to test other conditions.
     81 
     82     Pre-requisite: There are no pre-requisites for this test aside
     83     from the implicit pre-requisite that the host answer to ping.
     84 
     85     @param servo_host The ServoHost object to talk to via ssh.
     86     @param servod     Ignored.
     87 
     88     """
     89     return servo_host.is_up()
     90 
     91 
     92 def _servod_connect(servo_host, servod):
     93     """Test whether connection to servod succeeds.
     94 
     95     This tests the connection to the target servod with a simple
     96     method call.  As a side-effect, all hardware signals are
     97     initialized to default values.
     98 
     99     This function always returns success.  The test can only fail if
    100     the underlying call to servo raises an exception.
    101 
    102     Pre-requisite: There are no pre-requisites for this test aside
    103     from the implicit pre-requisite that the host answer to ping.
    104 
    105     @return `True`
    106 
    107     """
    108     # TODO(jrbarnette) We need to protect this call so that it
    109     # will time out if servod doesn't respond.
    110     servod.initialize_dut()
    111     return True
    112 
    113 
    114 def _pwr_button_test(servo_host, servod):
    115     """Test whether the 'pwr_button' signal is correct.
    116 
    117     This tests whether the state of the 'pwr_button' signal is
    118     'release'.  When the servo flex cable is not attached, the
    119     signal will be stuck at 'press'.
    120 
    121     Pre-requisite:  This test depends on successful initialization
    122     of servod.
    123 
    124     Rationale:  The initialization step sets 'pwr_button' to
    125     'release', which is required to justify the expectations of this
    126     test.  Also, if initialization fails, we can reasonably expect
    127     that all communication with servod will fail.
    128 
    129     @param servo_host Ignored.
    130     @param servod     The Servo object to be tested.
    131 
    132     """
    133     return servod.get('pwr_button') == 'release'
    134 
    135 
    136 def _lid_test(servo_host, servod):
    137     """Test whether the 'lid_open' signal is correct.
    138 
    139     This tests whether the state of the 'lid_open' signal has a
    140     correct value.  There is a manual switch on the servo board; if
    141     that switch is set wrong, the signal will be stuck at 'no'.
    142     Working units may return a setting of 'yes' (meaning the lid is
    143     open) or 'not_applicable' (meaning the device has no lid).
    144 
    145     Pre-requisite:  This test depends on the 'pwr_button' test.
    146 
    147     Rationale:  If the 'pwr_button' test fails, the flex cable may
    148     be disconnected, which means any servo operation to read a
    149     hardware signal will fail.
    150 
    151     @param servo_host Ignored.
    152     @param servod     The Servo object to be tested.
    153 
    154     """
    155     return servod.get('lid_open') != 'no'
    156 
    157 
    158 def _command_test(servo_host, command):
    159     """Utility to return the output of a command on a servo host.
    160 
    161     The command is expected to produce at most one line of
    162     output.  A trailing newline, if any, is stripped.
    163 
    164     @return Output from the command with the trailing newline
    165             removed.
    166 
    167     """
    168     return servo_host.run(command).stdout.strip('\n')
    169 
    170 
    171 def _brillo_test(servo_host, servod):
    172     """Get the version of Brillo running on the servo host.
    173 
    174     Reads the setting of CHROMEOS_RELEASE_VERSION from
    175     /etc/lsb-release on the servo host.  An empty string will
    176     returned if there is no such setting.
    177 
    178     Pre-requisite:  This test depends on the ssh test.
    179 
    180     @param servo_host The ServoHost object to be queried.
    181     @param servod     Ignored.
    182 
    183     @return Returns a Brillo version number or an empty string.
    184 
    185     """
    186     command = ('sed "s/CHROMEOS_RELEASE_VERSION=//p ; d" '
    187                    '/etc/lsb-release')
    188     return _command_test(servo_host, command)
    189 
    190 
    191 def _board_test(servo_host, servod):
    192     """Get the board for which the servo is configured.
    193 
    194     Reads the setting of BOARD from /var/lib/servod/config.  An
    195     empty string is returned if the board is unconfigured.
    196 
    197     Pre-requisite:  This test depends on the brillo version test.
    198 
    199     Rationale: The /var/lib/servod/config file is used by the servod
    200     upstart job, which is specific to Brillo servo builds.  This
    201     test has no meaning if the target servo host isn't running
    202     Brillo.
    203 
    204     @param servo_host The ServoHost object to be queried.
    205     @param servod     Ignored.
    206 
    207     @return The confgured board or an empty string.
    208 
    209     """
    210     command = ('CONFIG=/var/lib/servod/config\n'
    211                '[ -f $CONFIG ] && . $CONFIG && echo $BOARD')
    212     return _command_test(servo_host, command)
    213 
    214 
    215 def _servod_test(servo_host, servod):
    216     """Get the status of the servod upstart job.
    217 
    218     Ask upstart for the status of the 'servod' job.  Return whether
    219     the job is reported running.
    220 
    221     Pre-requisite:  This test depends on the brillo version test.
    222 
    223     Rationale: The servod upstart job is specific to Brillo servo
    224     builds.  This test has no meaning if the target servo host isn't
    225     running Brillo.
    226 
    227     @param servo_host The ServoHost object to be queried.
    228     @param servod     Ignored.
    229 
    230     @return `True` if the job is running, or `False` otherwise.
    231 
    232     """
    233     command = 'status servod | sed "s/,.*//"'
    234     return _command_test(servo_host, command) == 'servod start/running'
    235 
    236 
    237 _DIAGNOSTICS_LIST = [
    238     ('ssh_responds',
    239         _DiagnosticTest(None, _ssh_test)),
    240     ('servod_connect',
    241         _DiagnosticTest(None, _servod_connect)),
    242     ('pwr_button',
    243         _DiagnosticTest('servod_connect', _pwr_button_test)),
    244     ('lid_open',
    245         _DiagnosticTest('pwr_button', _lid_test)),
    246     ('brillo_version',
    247         _DiagnosticTest('ssh_responds', _brillo_test)),
    248     ('board',
    249         _DiagnosticTest('brillo_version', _board_test)),
    250     ('servod',
    251         _DiagnosticTest('brillo_version', _servod_test)),
    252 ]
    253 
    254 
    255 class infra_ServoDiagnosis(test.test):
    256     """Test a servo and diagnose common failures."""
    257 
    258     version = 1
    259 
    260     def _run_results(self, servo_host, servod):
    261         results = {}
    262         for key, tester in _DIAGNOSTICS_LIST:
    263             if tester.can_run(results):
    264                 results[key] = tester.run_diagnostic(servo_host, servod)
    265                 logging.info('Test %s result %s', key, results[key])
    266             else:
    267                 results[key] = None
    268                 logging.info('Skipping %s', key)
    269         return results
    270 
    271     def run_once(self, host):
    272         """Test and diagnose the servo for the given host.
    273 
    274         @param host Host object for a DUT with Servo.
    275 
    276         """
    277         # TODO(jrbarnette):  Need to handle ping diagnoses:
    278         #   + Specifically report if servo host isn't a lab host.
    279         #   + Specifically report if servo host is in lab but
    280         #     doesn't respond to ping.
    281         servo_host = host._servo_host
    282         servod = host.servo
    283         if servod is None:
    284             servod = servo.Servo(servo_host)
    285         results = self._run_results(servo_host, servod)
    286 
    287         if not _successful(results['ssh_responds']):
    288             raise error.TestFail('ssh connection to %s failed' %
    289                                      servo_host.hostname)
    290 
    291         if not _successful(results['brillo_version']):
    292             raise error.TestFail('Servo host %s is not running Brillo' %
    293                                      servo_host.hostname)
    294 
    295         # Make sure servo board matches DUT label
    296         board = host._get_board_from_afe()
    297         board = servo_afe_board_map.map_afe_board_to_servo_board(board)
    298         if (board and results['board'] is not None and
    299                 board != results['board']):
    300             logging.info('AFE says board should be %s', board)
    301             if results['servod']:
    302                 servo_host.run('stop servod', ignore_status=True)
    303             servo_host.run('start servod BOARD=%s' % board)
    304             results = self._run_results(servo_host, servod)
    305 
    306         # TODO(jrbarnette): The brillo update check currently
    307         # lives in ServoHost; it needs to move here.
    308 
    309         # Repair actions:
    310         #   if servod is dead or running but not working
    311         #     reboot and re-run results
    312 
    313         if (not _successful(results['servod']) or
    314                 not _successful(results['servod_connect'])):
    315             # TODO(jrbarnette):  For now, allow reboot failures to
    316             # raise their exceptions up the stack.  This event
    317             # shouldn't happen, so smarter handling should wait
    318             # until we have a use case to guide the requirements.
    319             servo_host.reboot()
    320             results = self._run_results(servo_host, servod)
    321             if not _successful(results['servod']):
    322                 # write result value to log
    323                 raise error.TestFail('servod failed to start on %s' %
    324                                          servo_host.hostname)
    325 
    326             if not _successful(results['servod_connect']):
    327                 raise error.TestFail('Servo failure on %s' %
    328                                          servo_host.hostname)
    329 
    330         if not _successful(results['pwr_button']):
    331             raise error.TestFail('Stuck power button on %s' %
    332                                      servo_host.hostname)
    333 
    334         if not _successful(results['lid_open']):
    335             raise error.TestFail('Lid stuck closed on %s' %
    336                                      servo_host.hostname)
    337