1# Copyright (c) 2013 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# Expects to be run in an environment with sudo and no interactive password 6# prompt, such as within the Chromium OS development chroot. 7 8 9"""This file provides core logic for servo verify/repair process.""" 10 11 12import httplib 13import logging 14import socket 15import xmlrpclib 16import os 17 18from autotest_lib.client.bin import utils 19from autotest_lib.client.common_lib import error 20from autotest_lib.client.common_lib import global_config 21from autotest_lib.client.common_lib import hosts 22from autotest_lib.client.common_lib import lsbrelease_utils 23from autotest_lib.client.common_lib.cros import dev_server 24from autotest_lib.client.common_lib.cros import retry 25from autotest_lib.client.common_lib.cros.network import ping_runner 26from autotest_lib.client.cros import constants as client_constants 27from autotest_lib.server import afe_utils 28from autotest_lib.server import site_utils as server_utils 29from autotest_lib.server.cros import autoupdater 30from autotest_lib.server.cros.dynamic_suite import frontend_wrappers 31from autotest_lib.server.cros.servo import servo 32from autotest_lib.server.hosts import servo_repair 33from autotest_lib.server.hosts import ssh_host 34from autotest_lib.site_utils.rpm_control_system import rpm_client 35 36try: 37 from chromite.lib import metrics 38except ImportError: 39 metrics = utils.metrics_mock 40 41 42# Names of the host attributes in the database that represent the values for 43# the servo_host and servo_port for a servo connected to the DUT. 44SERVO_HOST_ATTR = 'servo_host' 45SERVO_PORT_ATTR = 'servo_port' 46SERVO_BOARD_ATTR = 'servo_board' 47# Model is inferred from host labels. 48SERVO_MODEL_ATTR = 'servo_model' 49SERVO_SERIAL_ATTR = 'servo_serial' 50SERVO_ATTR_KEYS = ( 51 SERVO_BOARD_ATTR, 52 SERVO_HOST_ATTR, 53 SERVO_PORT_ATTR, 54 SERVO_SERIAL_ATTR, 55) 56 57_CONFIG = global_config.global_config 58ENABLE_SSH_TUNNEL_FOR_SERVO = _CONFIG.get_config_value( 59 'CROS', 'enable_ssh_tunnel_for_servo', type=bool, default=False) 60 61AUTOTEST_BASE = _CONFIG.get_config_value( 62 'SCHEDULER', 'drone_installation_directory', 63 default='/usr/local/autotest') 64 65 66class ServoHost(ssh_host.SSHHost): 67 """Host class for a host that controls a servo, e.g. beaglebone.""" 68 69 DEFAULT_PORT = int(os.getenv('SERVOD_PORT', '9999')) 70 71 # Timeout for initializing servo signals. 72 INITIALIZE_SERVO_TIMEOUT_SECS = 60 73 74 # Ready test function 75 SERVO_READY_METHOD = 'get_version' 76 77 REBOOT_CMD = 'sleep 1; reboot & sleep 10; reboot -f' 78 79 80 def _initialize(self, servo_host='localhost', 81 servo_port=DEFAULT_PORT, servo_board=None, 82 servo_model=None, servo_serial=None, is_in_lab=None, 83 *args, **dargs): 84 """Initialize a ServoHost instance. 85 86 A ServoHost instance represents a host that controls a servo. 87 88 @param servo_host: Name of the host where the servod process 89 is running. 90 @param servo_port: Port the servod process is listening on. Defaults 91 to the SERVOD_PORT environment variable if set, 92 otherwise 9999. 93 @param servo_board: Board that the servo is connected to. 94 @param servo_model: Model that the servo is connected to. 95 @param is_in_lab: True if the servo host is in Cros Lab. Default is set 96 to None, for which utils.host_is_in_lab_zone will be 97 called to check if the servo host is in Cros lab. 98 99 """ 100 super(ServoHost, self)._initialize(hostname=servo_host, 101 *args, **dargs) 102 self.servo_port = int(servo_port) 103 self.servo_board = servo_board 104 self.servo_model = servo_model 105 self.servo_serial = servo_serial 106 self._servo = None 107 self._repair_strategy = ( 108 servo_repair.create_servo_repair_strategy()) 109 self._is_localhost = (self.hostname == 'localhost') 110 if self._is_localhost: 111 self._is_in_lab = False 112 elif is_in_lab is None: 113 self._is_in_lab = utils.host_is_in_lab_zone(self.hostname) 114 else: 115 self._is_in_lab = is_in_lab 116 117 # Commands on the servo host must be run by the superuser. 118 # Our account on a remote host is root, but if our target is 119 # localhost then we might be running unprivileged. If so, 120 # `sudo` will have to be added to the commands. 121 if self._is_localhost: 122 self._sudo_required = utils.system_output('id -u') != '0' 123 else: 124 self._sudo_required = False 125 126 127 def connect_servo(self): 128 """Establish a connection to the servod server on this host. 129 130 Initializes `self._servo` and then verifies that all network 131 connections are working. This will create an ssh tunnel if 132 it's required. 133 134 As a side effect of testing the connection, all signals on the 135 target servo are reset to default values, and the USB stick is 136 set to the neutral (off) position. 137 """ 138 servo_obj = servo.Servo(servo_host=self, servo_serial=self.servo_serial) 139 timeout, _ = retry.timeout( 140 servo_obj.initialize_dut, 141 timeout_sec=self.INITIALIZE_SERVO_TIMEOUT_SECS) 142 if timeout: 143 raise hosts.AutoservVerifyError( 144 'Servo initialize timed out.') 145 self._servo = servo_obj 146 147 148 def disconnect_servo(self): 149 """Disconnect our servo if it exists. 150 151 If we've previously successfully connected to our servo, 152 disconnect any established ssh tunnel, and set `self._servo` 153 back to `None`. 154 """ 155 if self._servo: 156 # N.B. This call is safe even without a tunnel: 157 # rpc_server_tracker.disconnect() silently ignores 158 # unknown ports. 159 self.rpc_server_tracker.disconnect(self.servo_port) 160 self._servo = None 161 162 163 def is_in_lab(self): 164 """Check whether the servo host is a lab device. 165 166 @returns: True if the servo host is in Cros Lab, otherwise False. 167 168 """ 169 return self._is_in_lab 170 171 172 def is_localhost(self): 173 """Checks whether the servo host points to localhost. 174 175 @returns: True if it points to localhost, otherwise False. 176 177 """ 178 return self._is_localhost 179 180 181 def get_servod_server_proxy(self): 182 """Return a proxy that can be used to communicate with servod server. 183 184 @returns: An xmlrpclib.ServerProxy that is connected to the servod 185 server on the host. 186 """ 187 if ENABLE_SSH_TUNNEL_FOR_SERVO and not self.is_localhost(): 188 return self.rpc_server_tracker.xmlrpc_connect( 189 None, self.servo_port, 190 ready_test_name=self.SERVO_READY_METHOD, 191 timeout_seconds=60, 192 request_timeout_seconds=3600) 193 else: 194 remote = 'http://%s:%s' % (self.hostname, self.servo_port) 195 return xmlrpclib.ServerProxy(remote) 196 197 198 def is_cros_host(self): 199 """Check if a servo host is running chromeos. 200 201 @return: True if the servo host is running chromeos. 202 False if it isn't, or we don't have enough information. 203 """ 204 try: 205 result = self.run('grep -q CHROMEOS /etc/lsb-release', 206 ignore_status=True, timeout=10) 207 except (error.AutoservRunError, error.AutoservSSHTimeout): 208 return False 209 return result.exit_status == 0 210 211 212 def make_ssh_command(self, user='root', port=22, opts='', hosts_file=None, 213 connect_timeout=None, alive_interval=None, 214 alive_count_max=None, connection_attempts=None): 215 """Override default make_ssh_command to use tuned options. 216 217 Tuning changes: 218 - ConnectTimeout=30; maximum of 30 seconds allowed for an SSH 219 connection failure. Consistency with remote_access.py. 220 221 - ServerAliveInterval=180; which causes SSH to ping connection every 222 180 seconds. In conjunction with ServerAliveCountMax ensures 223 that if the connection dies, Autotest will bail out quickly. 224 225 - ServerAliveCountMax=3; consistency with remote_access.py. 226 227 - ConnectAttempts=4; reduce flakiness in connection errors; 228 consistency with remote_access.py. 229 230 - UserKnownHostsFile=/dev/null; we don't care about the keys. 231 232 - SSH protocol forced to 2; needed for ServerAliveInterval. 233 234 @param user User name to use for the ssh connection. 235 @param port Port on the target host to use for ssh connection. 236 @param opts Additional options to the ssh command. 237 @param hosts_file Ignored. 238 @param connect_timeout Ignored. 239 @param alive_interval Ignored. 240 @param alive_count_max Ignored. 241 @param connection_attempts Ignored. 242 243 @returns: An ssh command with the requested settings. 244 245 """ 246 options = ' '.join([opts, '-o Protocol=2']) 247 return super(ServoHost, self).make_ssh_command( 248 user=user, port=port, opts=options, hosts_file='/dev/null', 249 connect_timeout=30, alive_interval=180, alive_count_max=3, 250 connection_attempts=4) 251 252 253 def _make_scp_cmd(self, sources, dest): 254 """Format scp command. 255 256 Given a list of source paths and a destination path, produces the 257 appropriate scp command for encoding it. Remote paths must be 258 pre-encoded. Overrides _make_scp_cmd in AbstractSSHHost 259 to allow additional ssh options. 260 261 @param sources: A list of source paths to copy from. 262 @param dest: Destination path to copy to. 263 264 @returns: An scp command that copies |sources| on local machine to 265 |dest| on the remote servo host. 266 267 """ 268 command = ('scp -rq %s -o BatchMode=yes -o StrictHostKeyChecking=no ' 269 '-o UserKnownHostsFile=/dev/null -P %d %s "%s"') 270 return command % (self.master_ssh_option, 271 self.port, ' '.join(sources), dest) 272 273 274 def run(self, command, timeout=3600, ignore_status=False, 275 stdout_tee=utils.TEE_TO_LOGS, stderr_tee=utils.TEE_TO_LOGS, 276 connect_timeout=30, ssh_failure_retry_ok=False, 277 options='', stdin=None, verbose=True, args=()): 278 """Run a command on the servo host. 279 280 Extends method `run` in SSHHost. If the servo host is a remote device, 281 it will call `run` in SSHost without changing anything. 282 If the servo host is 'localhost', it will call utils.system_output. 283 284 @param command: The command line string. 285 @param timeout: Time limit in seconds before attempting to 286 kill the running process. The run() function 287 will take a few seconds longer than 'timeout' 288 to complete if it has to kill the process. 289 @param ignore_status: Do not raise an exception, no matter 290 what the exit code of the command is. 291 @param stdout_tee/stderr_tee: Where to tee the stdout/stderr. 292 @param connect_timeout: SSH connection timeout (in seconds) 293 Ignored if host is 'localhost'. 294 @param options: String with additional ssh command options 295 Ignored if host is 'localhost'. 296 @param ssh_failure_retry_ok: when True and ssh connection failure is 297 suspected, OK to retry command (but not 298 compulsory, and likely not needed here) 299 @param stdin: Stdin to pass (a string) to the executed command. 300 @param verbose: Log the commands. 301 @param args: Sequence of strings to pass as arguments to command by 302 quoting them in " and escaping their contents if necessary. 303 304 @returns: A utils.CmdResult object. 305 306 @raises AutoservRunError if the command failed. 307 @raises AutoservSSHTimeout SSH connection has timed out. Only applies 308 when servo host is not 'localhost'. 309 310 """ 311 run_args = {'command': command, 'timeout': timeout, 312 'ignore_status': ignore_status, 'stdout_tee': stdout_tee, 313 'stderr_tee': stderr_tee, 'stdin': stdin, 314 'verbose': verbose, 'args': args} 315 if self.is_localhost(): 316 if self._sudo_required: 317 run_args['command'] = 'sudo -n sh -c "%s"' % utils.sh_escape( 318 command) 319 try: 320 return utils.run(**run_args) 321 except error.CmdError as e: 322 logging.error(e) 323 raise error.AutoservRunError('command execution error', 324 e.result_obj) 325 else: 326 run_args['connect_timeout'] = connect_timeout 327 run_args['options'] = options 328 return super(ServoHost, self).run(**run_args) 329 330 331 def _get_release_version(self): 332 """Get the value of attribute CHROMEOS_RELEASE_VERSION from lsb-release. 333 334 @returns The version string in lsb-release, under attribute 335 CHROMEOS_RELEASE_VERSION. 336 """ 337 lsb_release_content = self.run( 338 'cat "%s"' % client_constants.LSB_RELEASE).stdout.strip() 339 return lsbrelease_utils.get_chromeos_release_version( 340 lsb_release_content=lsb_release_content) 341 342 343 def get_attached_duts(self, afe): 344 """Gather a list of duts that use this servo host. 345 346 @param afe: afe instance. 347 348 @returns list of duts. 349 """ 350 return afe.get_hosts_by_attribute( 351 attribute=SERVO_HOST_ATTR, value=self.hostname) 352 353 354 def get_board(self): 355 """Determine the board for this servo host. 356 357 @returns a string representing this servo host's board. 358 """ 359 return lsbrelease_utils.get_current_board( 360 lsb_release_content=self.run('cat /etc/lsb-release').stdout) 361 362 363 def reboot(self, *args, **dargs): 364 """Reboot using special servo host reboot command.""" 365 super(ServoHost, self).reboot(reboot_cmd=self.REBOOT_CMD, 366 *args, **dargs) 367 368 369 def _maybe_reboot_post_upgrade(self, updater): 370 """Reboot this servo host if an upgrade is waiting. 371 372 If the host has successfully downloaded and finalized a new 373 build, reboot. 374 375 @param updater: a ChromiumOSUpdater instance for checking 376 whether reboot is needed. 377 """ 378 if updater.check_update_status() != autoupdater.UPDATER_NEED_REBOOT: 379 return 380 381 if self._needs_synchronized_reboot(): 382 logging.info('Servohost requies synchronized reboot, which is no' 383 ' longer supported. Manually reboot servohost instead.' 384 ' See crbug/848528') 385 return 386 387 self._reboot_post_upgrade() 388 389 390 def _needs_synchronized_reboot(self): 391 """Does this servohost need synchronized reboot across multiple DUTs""" 392 # TODO(pprabhu) Use HostInfo in this check instead of hitting AFE. 393 afe = frontend_wrappers.RetryingAFE( 394 timeout_min=5, delay_sec=10, 395 server=server_utils.get_global_afe_hostname()) 396 dut_list = self.get_attached_duts(afe) 397 return len(dut_list) > 1 398 399 400 def _reboot_post_upgrade(self): 401 """Reboot this servo host because an upgrade is waiting.""" 402 logging.info('Rebooting servo host %s from build %s', self.hostname, 403 self._get_release_version()) 404 # Tell the reboot() call not to wait for completion. 405 # Otherwise, the call will log reboot failure if servo does 406 # not come back. The logged reboot failure will lead to 407 # test job failure. If the test does not require servo, we 408 # don't want servo failure to fail the test with error: 409 # `Host did not return from reboot` in status.log. 410 self.reboot(fastsync=True, wait=False) 411 412 # We told the reboot() call not to wait, but we need to wait 413 # for the reboot before we continue. Alas. The code from 414 # here below is basically a copy of Host.wait_for_restart(), 415 # with the logging bits ripped out, so that they can't cause 416 # the failure logging problem described above. 417 # 418 # The black stain that this has left on my soul can never be 419 # erased. 420 old_boot_id = self.get_boot_id() 421 if not self.wait_down(timeout=self.WAIT_DOWN_REBOOT_TIMEOUT, 422 warning_timer=self.WAIT_DOWN_REBOOT_WARNING, 423 old_boot_id=old_boot_id): 424 raise error.AutoservHostError( 425 'servo host %s failed to shut down.' % 426 self.hostname) 427 if self.wait_up(timeout=120): 428 logging.info('servo host %s back from reboot, with build %s', 429 self.hostname, self._get_release_version()) 430 else: 431 raise error.AutoservHostError( 432 'servo host %s failed to come back from reboot.' % 433 self.hostname) 434 435 436 def update_image(self, wait_for_update=False): 437 """Update the image on the servo host, if needed. 438 439 This method recognizes the following cases: 440 * If the Host is not running Chrome OS, do nothing. 441 * If a previously triggered update is now complete, reboot 442 to the new version. 443 * If the host is processing a previously triggered update, 444 do nothing. 445 * If the host is running a version of Chrome OS different 446 from the default for servo Hosts, trigger an update, but 447 don't wait for it to complete. 448 449 @param wait_for_update If an update needs to be applied and 450 this is true, then don't return until the update is 451 downloaded and finalized, and the host rebooted. 452 @raises dev_server.DevServerException: If all the devservers are down. 453 @raises site_utils.ParseBuildNameException: If the devserver returns 454 an invalid build name. 455 @raises AutoservRunError: If the update_engine_client isn't present on 456 the host, and the host is a cros_host. 457 458 """ 459 # servod could be running in a Ubuntu workstation. 460 if not self.is_cros_host(): 461 logging.info('Not attempting an update, either %s is not running ' 462 'chromeos or we cannot find enough information about ' 463 'the host.', self.hostname) 464 return 465 466 if lsbrelease_utils.is_moblab(): 467 logging.info('Not attempting an update, %s is running moblab.', 468 self.hostname) 469 return 470 471 target_build = afe_utils.get_stable_cros_image_name(self.get_board()) 472 target_build_number = server_utils.ParseBuildName( 473 target_build)[3] 474 # For servo image staging, we want it as more widely distributed as 475 # possible, so that devservers' load can be evenly distributed. So use 476 # hostname instead of target_build as hash. 477 ds = dev_server.ImageServer.resolve(self.hostname, 478 hostname=self.hostname) 479 url = ds.get_update_url(target_build) 480 481 updater = autoupdater.ChromiumOSUpdater(update_url=url, host=self) 482 self._maybe_reboot_post_upgrade(updater) 483 current_build_number = self._get_release_version() 484 status = updater.check_update_status() 485 update_pending = True 486 if status in autoupdater.UPDATER_PROCESSING_UPDATE: 487 logging.info('servo host %s already processing an update, update ' 488 'engine client status=%s', self.hostname, status) 489 elif status == autoupdater.UPDATER_NEED_REBOOT: 490 return 491 elif current_build_number != target_build_number: 492 logging.info('Using devserver url: %s to trigger update on ' 493 'servo host %s, from %s to %s', url, self.hostname, 494 current_build_number, target_build_number) 495 try: 496 ds.stage_artifacts(target_build, 497 artifacts=['full_payload']) 498 except Exception as e: 499 logging.error('Staging artifacts failed: %s', str(e)) 500 logging.error('Abandoning update for this cycle.') 501 else: 502 try: 503 updater.trigger_update() 504 except autoupdater.RootFSUpdateError as e: 505 trigger_download_status = 'failed with %s' % str(e) 506 metrics.Counter('chromeos/autotest/servo/' 507 'rootfs_update_failed').increment() 508 else: 509 trigger_download_status = 'passed' 510 logging.info('Triggered download and update %s for %s, ' 511 'update engine currently in status %s', 512 trigger_download_status, self.hostname, 513 updater.check_update_status()) 514 else: 515 logging.info('servo host %s does not require an update.', 516 self.hostname) 517 update_pending = False 518 519 if update_pending and wait_for_update: 520 logging.info('Waiting for servo update to complete.') 521 self.run('update_engine_client --follow', ignore_status=True) 522 523 524 def verify(self, silent=False): 525 """Update the servo host and verify it's in a good state. 526 527 @param silent If true, suppress logging in `status.log`. 528 """ 529 message = 'Beginning verify for servo host %s port %s serial %s' 530 message %= (self.hostname, self.servo_port, self.servo_serial) 531 self.record('INFO', None, None, message) 532 try: 533 self._repair_strategy.verify(self, silent) 534 except: 535 self.disconnect_servo() 536 raise 537 538 539 def repair(self, silent=False): 540 """Attempt to repair servo host. 541 542 @param silent If true, suppress logging in `status.log`. 543 """ 544 message = 'Beginning repair for servo host %s port %s serial %s' 545 message %= (self.hostname, self.servo_port, self.servo_serial) 546 self.record('INFO', None, None, message) 547 try: 548 self._repair_strategy.repair(self, silent) 549 except: 550 self.disconnect_servo() 551 raise 552 553 554 def has_power(self): 555 """Return whether or not the servo host is powered by PoE.""" 556 # TODO(fdeng): See crbug.com/302791 557 # For now, assume all servo hosts in the lab have power. 558 return self.is_in_lab() 559 560 561 def power_cycle(self): 562 """Cycle power to this host via PoE if it is a lab device. 563 564 @raises AutoservRepairError if it fails to power cycle the 565 servo host. 566 567 """ 568 if self.has_power(): 569 try: 570 rpm_client.set_power(self, 'CYCLE') 571 except (socket.error, xmlrpclib.Error, 572 httplib.BadStatusLine, 573 rpm_client.RemotePowerException) as e: 574 raise hosts.AutoservRepairError( 575 'Power cycling %s failed: %s' % (self.hostname, e), 576 'power_cycle_via_rpm_failed' 577 ) 578 else: 579 logging.info('Skipping power cycling, not a lab device.') 580 581 582 def get_servo(self): 583 """Get the cached servo.Servo object. 584 585 @return: a servo.Servo object. 586 """ 587 return self._servo 588 589 590 def close(self): 591 """Close the associated servo and the host object.""" 592 if self._servo: 593 # In some cases when we run as lab-tools, the job object is None. 594 if self.job and not self._servo.uart_logs_dir: 595 self._servo.uart_logs_dir = self.job.resultdir 596 self._servo.close() 597 598 super(ServoHost, self).close() 599 600 601def make_servo_hostname(dut_hostname): 602 """Given a DUT's hostname, return the hostname of its servo. 603 604 @param dut_hostname: hostname of a DUT. 605 606 @return hostname of the DUT's servo. 607 608 """ 609 host_parts = dut_hostname.split('.') 610 host_parts[0] = host_parts[0] + '-servo' 611 return '.'.join(host_parts) 612 613 614def servo_host_is_up(servo_hostname): 615 """Given a servo host name, return if it's up or not. 616 617 @param servo_hostname: hostname of the servo host. 618 619 @return True if it's up, False otherwise 620 """ 621 # Technically, this duplicates the SSH ping done early in the servo 622 # proxy initialization code. However, this ping ends in a couple 623 # seconds when if fails, rather than the 60 seconds it takes to decide 624 # that an SSH ping has timed out. Specifically, that timeout happens 625 # when our servo DNS name resolves, but there is no host at that IP. 626 logging.info('Pinging servo host at %s', servo_hostname) 627 ping_config = ping_runner.PingConfig( 628 servo_hostname, count=3, 629 ignore_result=True, ignore_status=True) 630 return ping_runner.PingRunner().ping(ping_config).received > 0 631 632 633def _map_afe_board_to_servo_board(afe_board): 634 """Map a board we get from the AFE to a servo appropriate value. 635 636 Many boards are identical to other boards for servo's purposes. 637 This function makes that mapping. 638 639 @param afe_board string board name received from AFE. 640 @return board we expect servo to have. 641 642 """ 643 KNOWN_SUFFIXES = ['-freon', '_freon', '_moblab', '-cheets'] 644 BOARD_MAP = {'gizmo': 'panther'} 645 mapped_board = afe_board 646 if afe_board in BOARD_MAP: 647 mapped_board = BOARD_MAP[afe_board] 648 else: 649 for suffix in KNOWN_SUFFIXES: 650 if afe_board.endswith(suffix): 651 mapped_board = afe_board[0:-len(suffix)] 652 break 653 if mapped_board != afe_board: 654 logging.info('Mapping AFE board=%s to %s', afe_board, mapped_board) 655 return mapped_board 656 657 658def get_servo_args_for_host(dut_host): 659 """Return servo data associated with a given DUT. 660 661 @param dut_host Instance of `Host` on which to find the servo 662 attributes. 663 @return `servo_args` dict with host and an optional port. 664 """ 665 info = dut_host.host_info_store.get() 666 servo_args = {k: v for k, v in info.attributes.iteritems() 667 if k in SERVO_ATTR_KEYS} 668 669 if SERVO_PORT_ATTR in servo_args: 670 try: 671 servo_args[SERVO_PORT_ATTR] = int(servo_args[SERVO_PORT_ATTR]) 672 except ValueError: 673 logging.error('servo port is not an int: %s', 674 servo_args[SERVO_PORT_ATTR]) 675 # Reset servo_args because we don't want to use an invalid port. 676 servo_args.pop(SERVO_HOST_ATTR, None) 677 678 if info.board: 679 servo_args[SERVO_BOARD_ATTR] = _map_afe_board_to_servo_board(info.board) 680 if info.model: 681 servo_args[SERVO_MODEL_ATTR] = info.model 682 return servo_args if SERVO_HOST_ATTR in servo_args else None 683 684 685def _tweak_args_for_ssp_moblab(servo_args): 686 if servo_args[SERVO_HOST_ATTR] in ['localhost', '127.0.0.1']: 687 servo_args[SERVO_HOST_ATTR] = _CONFIG.get_config_value( 688 'SSP', 'host_container_ip', type=str, default=None) 689 690 691def create_servo_host(dut, servo_args, try_lab_servo=False, 692 try_servo_repair=False): 693 """Create a ServoHost object for a given DUT, if appropriate. 694 695 This function attempts to create and verify or repair a `ServoHost` 696 object for a servo connected to the given `dut`, subject to various 697 constraints imposed by the parameters: 698 * When the `servo_args` parameter is not `None`, a servo 699 host must be created, and must be checked with `repair()`. 700 * Otherwise, if a servo exists in the lab and `try_lab_servo` is 701 true: 702 * If `try_servo_repair` is true, then create a servo host and 703 check it with `repair()`. 704 * Otherwise, if the servo responds to `ping` then create a 705 servo host and check it with `verify()`. 706 707 In cases where `servo_args` was not `None`, repair failure 708 exceptions are passed back to the caller; otherwise, exceptions 709 are logged and then discarded. Note that this only happens in cases 710 where we're called from a test (not special task) control file that 711 has an explicit dependency on servo. In that case, we require that 712 repair not write to `status.log`, so as to avoid polluting test 713 results. 714 715 TODO(jrbarnette): The special handling for servo in test control 716 files is a thorn in my flesh; I dearly hope to see it cut out before 717 my retirement. 718 719 Parameters for a servo host consist of a host name, port number, and 720 DUT board, and are determined from one of these sources, in order of 721 priority: 722 * Servo attributes from the `dut` parameter take precedence over 723 all other sources of information. 724 * If a DNS entry for the servo based on the DUT hostname exists in 725 the CrOS lab network, that hostname is used with the default 726 port and the DUT's board. 727 * If no other options are found, the parameters will be taken 728 from the `servo_args` dict passed in from the caller. 729 730 @param dut An instance of `Host` from which to take 731 servo parameters (if available). 732 @param servo_args A dictionary with servo parameters to use if 733 they can't be found from `dut`. If this 734 argument is supplied, unrepaired exceptions 735 from `verify()` will be passed back to the 736 caller. 737 @param try_lab_servo If not true, servo host creation will be 738 skipped unless otherwise required by the 739 caller. 740 @param try_servo_repair If true, check a servo host with 741 `repair()` instead of `verify()`. 742 743 @returns: A ServoHost object or None. See comments above. 744 745 """ 746 servo_dependency = servo_args is not None 747 if dut is not None and (try_lab_servo or servo_dependency): 748 servo_args_override = get_servo_args_for_host(dut) 749 if servo_args_override is not None: 750 if utils.in_moblab_ssp(): 751 _tweak_args_for_ssp_moblab(servo_args_override) 752 logging.debug( 753 'Overriding provided servo_args (%s) with arguments' 754 ' determined from the host (%s)', 755 servo_args, 756 servo_args_override, 757 ) 758 servo_args = servo_args_override 759 760 if servo_args is None: 761 logging.debug('No servo_args provided, and failed to find overrides.') 762 return None 763 if SERVO_HOST_ATTR not in servo_args: 764 logging.debug('%s attribute missing from servo_args: %s', 765 SERVO_HOST_ATTR, servo_args) 766 return None 767 if (not servo_dependency and not try_servo_repair and 768 not servo_host_is_up(servo_args[SERVO_HOST_ATTR])): 769 logging.debug('ServoHost is not up.') 770 return None 771 772 newhost = ServoHost( 773 is_in_lab=(servo_args 774 and server_utils.host_in_lab( 775 servo_args[SERVO_HOST_ATTR])), 776 **servo_args 777 ) 778 # Note that the logic of repair() includes everything done 779 # by verify(). It's sufficient to call one or the other; 780 # we don't need both. 781 if servo_dependency: 782 newhost.repair(silent=True) 783 return newhost 784 785 if try_servo_repair: 786 try: 787 newhost.repair() 788 except Exception: 789 logging.exception('servo repair failed for %s', newhost.hostname) 790 else: 791 try: 792 newhost.verify() 793 except Exception: 794 logging.exception('servo verify failed for %s', newhost.hostname) 795 return newhost 796