1# Copyright 2016 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 5import logging 6import time 7 8import common 9from autotest_lib.client.common_lib import hosts 10from autotest_lib.server.cros.dynamic_suite import frontend_wrappers 11from autotest_lib.server.hosts import repair 12 13 14class _UpdateVerifier(hosts.Verifier): 15 """ 16 Verifier to trigger a servo host update, if necessary. 17 18 The operation doesn't wait for the update to complete and is 19 considered a success whether or not the servo is currently 20 up-to-date. 21 """ 22 23 def verify(self, host): 24 # First, only run this verifier if the host is in the physical lab. 25 # Secondly, skip if the test is being run by test_that, because subnet 26 # restrictions can cause the update to fail. 27 if host.is_in_lab() and host.job and host.job.in_lab: 28 host.update_image(wait_for_update=False) 29 30 @property 31 def description(self): 32 return 'servo host software is up-to-date' 33 34 35class _ConfigVerifier(hosts.Verifier): 36 """ 37 Base verifier for the servo config file verifiers. 38 """ 39 40 CONFIG_FILE = '/var/lib/servod/config' 41 ATTR = '' 42 43 @staticmethod 44 def _get_config_val(host, config_file, attr): 45 """ 46 Get the `attr` for `host` from `config_file`. 47 48 @param host Host to be checked for `config_file`. 49 @param config_file Path to the config file to be tested. 50 @param attr Attribute to get from config file. 51 52 @return The attr val as set in the config file, or `None` if 53 the file was absent. 54 """ 55 getboard = ('CONFIG=%s ; [ -f $CONFIG ] && ' 56 '. $CONFIG && echo $%s' % (config_file, attr)) 57 attr_val = host.run(getboard, ignore_status=True).stdout 58 return attr_val.strip('\n') if attr_val else None 59 60 @staticmethod 61 def _validate_attr(host, val, expected_val, attr, config_file): 62 """ 63 Check that the attr setting is valid for the host. 64 65 This presupposes that a valid config file was found. Raise an 66 execption if: 67 * There was no attr setting from the file (i.e. the setting 68 is an empty string), or 69 * The attr setting is valid, the attr is known, 70 and the setting doesn't match the DUT. 71 72 @param host Host to be checked for `config_file`. 73 @param val Value to be tested. 74 @param expected_val Expected value. 75 @param attr Attribute we're validating. 76 @param config_file Path to the config file to be tested. 77 """ 78 if not val: 79 raise hosts.AutoservVerifyError( 80 'config file %s exists, but %s ' 81 'is not set' % (attr, config_file)) 82 if expected_val is not None and val != expected_val: 83 raise hosts.AutoservVerifyError( 84 '%s is %s; it should be %s' % (attr, val, expected_val)) 85 86 87 def _get_configs(self, host): 88 """ 89 Return all the config files to check. 90 91 @param host Host object. 92 93 @return The list of config files to check. 94 """ 95 # TODO(jrbarnette): Testing `CONFIG_FILE` without a port number 96 # is a legacy. Ideally, we would force all servos in the lab to 97 # update, and then remove this case. 98 config_list = ['%s_%d' % (self.CONFIG_FILE, host.servo_port)] 99 if host.servo_port == host.DEFAULT_PORT: 100 config_list.append(self.CONFIG_FILE) 101 return config_list 102 103 @property 104 def description(self): 105 return 'servo %s setting is correct' % self.ATTR 106 107 108class _SerialConfigVerifier(_ConfigVerifier): 109 """ 110 Verifier for the servo SERIAL configuration. 111 """ 112 113 ATTR = 'SERIAL' 114 115 def verify(self, host): 116 """ 117 Test whether the `host` has a `SERIAL` setting configured. 118 119 This tests the config file names used by the `servod` upstart 120 job for a valid setting of the `SERIAL` variable. The following 121 conditions raise errors: 122 * The SERIAL setting doesn't match the DUT's entry in the AFE 123 database. 124 * There is no config file. 125 """ 126 if not host.is_cros_host(): 127 return 128 # Not all servo hosts will have a servo serial so don't verify if it's 129 # not set. 130 if host.servo_serial is None: 131 return 132 for config in self._get_configs(host): 133 serialval = self._get_config_val(host, config, self.ATTR) 134 if serialval is not None: 135 self._validate_attr(host, serialval, host.servo_serial, 136 self.ATTR, config) 137 return 138 msg = 'Servo serial is unconfigured; should be %s' % host.servo_serial 139 raise hosts.AutoservVerifyError(msg) 140 141 142 143class _BoardConfigVerifier(_ConfigVerifier): 144 """ 145 Verifier for the servo BOARD configuration. 146 """ 147 148 ATTR = 'BOARD' 149 150 def verify(self, host): 151 """ 152 Test whether the `host` has a `BOARD` setting configured. 153 154 This tests the config file names used by the `servod` upstart 155 job for a valid setting of the `BOARD` variable. The following 156 conditions raise errors: 157 * A config file exists, but the content contains no setting 158 for BOARD. 159 * The BOARD setting doesn't match the DUT's entry in the AFE 160 database. 161 * There is no config file. 162 """ 163 if not host.is_cros_host(): 164 return 165 for config in self._get_configs(host): 166 boardval = self._get_config_val(host, config, self.ATTR) 167 if boardval is not None: 168 self._validate_attr(host, boardval, host.servo_board, self.ATTR, 169 config) 170 return 171 msg = 'Servo board is unconfigured' 172 if host.servo_board is not None: 173 msg += '; should be %s' % host.servo_board 174 raise hosts.AutoservVerifyError(msg) 175 176 177class _ServodJobVerifier(hosts.Verifier): 178 """ 179 Verifier to check that the `servod` upstart job is running. 180 """ 181 182 def verify(self, host): 183 if not host.is_cros_host(): 184 return 185 status_cmd = 'status servod PORT=%d' % host.servo_port 186 job_status = host.run(status_cmd, ignore_status=True).stdout 187 if 'start/running' not in job_status: 188 raise hosts.AutoservVerifyError( 189 'servod not running on %s port %d' % 190 (host.hostname, host.servo_port)) 191 192 @property 193 def description(self): 194 return 'servod upstart job is running' 195 196 197class _ServodConnectionVerifier(hosts.Verifier): 198 """ 199 Verifier to check that we can connect to `servod`. 200 201 This tests the connection to the target servod service with a simple 202 method call. As a side-effect, all servo signals are initialized to 203 default values. 204 205 N.B. Initializing servo signals is necessary because the power 206 button and lid switch verifiers both test against expected initial 207 values. 208 """ 209 210 def verify(self, host): 211 host.connect_servo() 212 213 @property 214 def description(self): 215 return 'servod service is taking calls' 216 217 218class _PowerButtonVerifier(hosts.Verifier): 219 """ 220 Verifier to check sanity of the `pwr_button` signal. 221 222 Tests that the `pwr_button` signal shows the power button has been 223 released. When `pwr_button` is stuck at `press`, it commonly 224 indicates that the ribbon cable is disconnected. 225 """ 226 # TODO (crbug.com/646593) - Remove list below once servo has been updated 227 # with a dummy pwr_button signal. 228 _BOARDS_WO_PWR_BUTTON = ['arkham', 'storm', 'whirlwind', 'gale'] 229 230 def verify(self, host): 231 if host.servo_board in self._BOARDS_WO_PWR_BUTTON: 232 return 233 button = host.get_servo().get('pwr_button') 234 if button != 'release': 235 raise hosts.AutoservVerifyError( 236 'Check ribbon cable: \'pwr_button\' is stuck') 237 238 @property 239 def description(self): 240 return 'pwr_button control is normal' 241 242 243class _LidVerifier(hosts.Verifier): 244 """ 245 Verifier to check sanity of the `lid_open` signal. 246 """ 247 248 def verify(self, host): 249 lid_open = host.get_servo().get('lid_open') 250 if lid_open != 'yes' and lid_open != 'not_applicable': 251 raise hosts.AutoservVerifyError( 252 'Check lid switch: lid_open is %s' % lid_open) 253 254 @property 255 def description(self): 256 return 'lid_open control is normal' 257 258 259class _RestartServod(hosts.RepairAction): 260 """Restart `servod` with the proper BOARD setting.""" 261 262 def repair(self, host): 263 if not host.is_cros_host(): 264 raise hosts.AutoservRepairError( 265 'Can\'t restart servod: not running ' 266 'embedded Chrome OS.') 267 host.run('stop servod PORT=%d || true' % host.servo_port) 268 serial = 'SERIAL=%s' % host.servo_serial if host.servo_serial else '' 269 if host.servo_board: 270 host.run('start servod BOARD=%s PORT=%d %s' % 271 (host.servo_board, host.servo_port, serial)) 272 else: 273 # TODO(jrbarnette): It remains to be seen whether 274 # this action is the right thing to do... 275 logging.warning('Board for DUT is unknown; starting ' 276 'servod assuming a pre-configured ' 277 'board.') 278 host.run('start servod PORT=%d %s' % (host.servo_port, serial)) 279 # There's a lag between when `start servod` completes and when 280 # the _ServodConnectionVerifier trigger can actually succeed. 281 # The call to time.sleep() below gives time to make sure that 282 # the trigger won't fail after we return. 283 # 284 # The delay selection was based on empirical testing against 285 # servo V3 on a desktop: 286 # + 10 seconds was usually too slow; 11 seconds was 287 # usually fast enough. 288 # + So, the 20 second delay is about double what we 289 # expect to need. 290 time.sleep(20) 291 292 293 @property 294 def description(self): 295 return 'Start servod with the proper config settings.' 296 297 298class _ServoRebootRepair(repair.RebootRepair): 299 """ 300 Reboot repair action that also waits for an update. 301 302 This is the same as the standard `RebootRepair`, but for 303 a servo host, if there's a pending update, we wait for that 304 to complete before rebooting. This should ensure that the 305 servo is up-to-date after reboot. 306 """ 307 308 def repair(self, host): 309 if host.is_localhost() or not host.is_cros_host(): 310 raise hosts.AutoservRepairError( 311 'Target servo is not a test lab servo') 312 host.update_image(wait_for_update=True) 313 afe = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10) 314 dut_list = host.get_attached_duts(afe) 315 if len(dut_list) > 1: 316 host.schedule_synchronized_reboot(dut_list, afe, force_reboot=True) 317 else: 318 super(_ServoRebootRepair, self).repair(host) 319 320 @property 321 def description(self): 322 return 'Wait for update, then reboot servo host.' 323 324 325def create_servo_repair_strategy(): 326 """ 327 Return a `RepairStrategy` for a `ServoHost`. 328 """ 329 config = ['brd_config', 'ser_config'] 330 verify_dag = [ 331 (repair.SshVerifier, 'servo_ssh', []), 332 (_UpdateVerifier, 'update', ['servo_ssh']), 333 (_BoardConfigVerifier, 'brd_config', ['servo_ssh']), 334 (_SerialConfigVerifier, 'ser_config', ['servo_ssh']), 335 (_ServodJobVerifier, 'job', config), 336 (_ServodConnectionVerifier, 'servod', ['job']), 337 (_PowerButtonVerifier, 'pwr_button', ['servod']), 338 (_LidVerifier, 'lid_open', ['servod']), 339 # TODO(jrbarnette): We want a verifier for whether there's 340 # a working USB stick plugged into the servo. However, 341 # although we always want to log USB stick problems, we don't 342 # want to fail the servo because we don't want a missing USB 343 # stick to prevent, say, power cycling the DUT. 344 # 345 # So, it may be that the right fix is to put diagnosis into 346 # ServoInstallRepair rather than add a verifier. 347 ] 348 349 servod_deps = ['job', 'servod', 'pwr_button', 'lid_open'] 350 repair_actions = [ 351 (repair.RPMCycleRepair, 'rpm', [], ['servo_ssh']), 352 (_RestartServod, 'restart', ['servo_ssh'], config + servod_deps), 353 (_ServoRebootRepair, 'reboot', ['servo_ssh'], servod_deps), 354 ] 355 return hosts.RepairStrategy(verify_dag, repair_actions) 356