1# Copyright (c) 2012 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 os 7import re 8import sys 9import time 10 11import common 12from autotest_lib.client.bin import utils 13from autotest_lib.client.common_lib import autotemp 14from autotest_lib.client.common_lib import error 15from autotest_lib.client.common_lib import global_config 16from autotest_lib.client.common_lib import hosts 17from autotest_lib.client.common_lib import lsbrelease_utils 18from autotest_lib.client.common_lib.cros import autoupdater 19from autotest_lib.client.common_lib.cros import dev_server 20from autotest_lib.client.common_lib.cros.graphite import autotest_es 21from autotest_lib.client.cros import constants as client_constants 22from autotest_lib.client.cros import cros_ui 23from autotest_lib.client.cros.audio import cras_utils 24from autotest_lib.client.cros.input_playback import input_playback 25from autotest_lib.client.cros.video import constants as video_test_constants 26from autotest_lib.server import afe_utils 27from autotest_lib.server import autoserv_parser 28from autotest_lib.server import autotest 29from autotest_lib.server import constants 30from autotest_lib.server import utils as server_utils 31from autotest_lib.server.cros import provision 32from autotest_lib.server.cros.dynamic_suite import constants as ds_constants 33from autotest_lib.server.cros.dynamic_suite import tools, frontend_wrappers 34from autotest_lib.server.cros.faft.config.config import Config as FAFTConfig 35from autotest_lib.server.cros.servo import plankton 36from autotest_lib.server.hosts import abstract_ssh 37from autotest_lib.server.hosts import base_label 38from autotest_lib.server.hosts import cros_label 39from autotest_lib.server.hosts import chameleon_host 40from autotest_lib.server.hosts import cros_repair 41from autotest_lib.server.hosts import plankton_host 42from autotest_lib.server.hosts import servo_host 43from autotest_lib.site_utils.rpm_control_system import rpm_client 44 45# In case cros_host is being ran via SSP on an older Moblab version with an 46# older chromite version. 47try: 48 from chromite.lib import metrics 49except ImportError: 50 metrics = utils.metrics_mock 51 52 53CONFIG = global_config.global_config 54ENABLE_DEVSERVER_TRIGGER_AUTO_UPDATE = CONFIG.get_config_value( 55 'CROS', 'enable_devserver_trigger_auto_update', type=bool, 56 default=False) 57 58LUCID_SLEEP_BOARDS = ['samus', 'lulu'] 59 60 61class FactoryImageCheckerException(error.AutoservError): 62 """Exception raised when an image is a factory image.""" 63 pass 64 65 66class CrosHost(abstract_ssh.AbstractSSHHost): 67 """Chromium OS specific subclass of Host.""" 68 69 VERSION_PREFIX = provision.CROS_VERSION_PREFIX 70 71 _parser = autoserv_parser.autoserv_parser 72 _AFE = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10) 73 support_devserver_provision = ENABLE_DEVSERVER_TRIGGER_AUTO_UPDATE 74 75 # Timeout values (in seconds) associated with various Chrome OS 76 # state changes. 77 # 78 # In general, a good rule of thumb is that the timeout can be up 79 # to twice the typical measured value on the slowest platform. 80 # The times here have not necessarily been empirically tested to 81 # meet this criterion. 82 # 83 # SLEEP_TIMEOUT: Time to allow for suspend to memory. 84 # RESUME_TIMEOUT: Time to allow for resume after suspend, plus 85 # time to restart the netwowrk. 86 # SHUTDOWN_TIMEOUT: Time to allow for shut down. 87 # BOOT_TIMEOUT: Time to allow for boot from power off. Among 88 # other things, this must account for the 30 second dev-mode 89 # screen delay, time to start the network on the DUT, and the 90 # ssh timeout of 120 seconds. 91 # USB_BOOT_TIMEOUT: Time to allow for boot from a USB device, 92 # including the 30 second dev-mode delay and time to start the 93 # network. 94 # INSTALL_TIMEOUT: Time to allow for chromeos-install. 95 # POWERWASH_BOOT_TIMEOUT: Time to allow for a reboot that 96 # includes powerwash. 97 98 SLEEP_TIMEOUT = 2 99 RESUME_TIMEOUT = 10 100 SHUTDOWN_TIMEOUT = 10 101 BOOT_TIMEOUT = 150 102 USB_BOOT_TIMEOUT = 300 103 INSTALL_TIMEOUT = 480 104 POWERWASH_BOOT_TIMEOUT = 60 105 106 # Minimum OS version that supports server side packaging. Older builds may 107 # not have server side package built or with Autotest code change to support 108 # server-side packaging. 109 MIN_VERSION_SUPPORT_SSP = CONFIG.get_config_value( 110 'AUTOSERV', 'min_version_support_ssp', type=int) 111 112 # REBOOT_TIMEOUT: How long to wait for a reboot. 113 # 114 # We have a long timeout to ensure we don't flakily fail due to other 115 # issues. Shorter timeouts are vetted in platform_RebootAfterUpdate. 116 # TODO(sbasi - crbug.com/276094) Restore to 5 mins once the 'host did not 117 # return from reboot' bug is solved. 118 REBOOT_TIMEOUT = 480 119 120 # _USB_POWER_TIMEOUT: Time to allow for USB to power toggle ON and OFF. 121 # _POWER_CYCLE_TIMEOUT: Time to allow for manual power cycle. 122 _USB_POWER_TIMEOUT = 5 123 _POWER_CYCLE_TIMEOUT = 10 124 125 _RPM_RECOVERY_BOARDS = CONFIG.get_config_value('CROS', 126 'rpm_recovery_boards', type=str).split(',') 127 128 _LAB_MACHINE_FILE = '/mnt/stateful_partition/.labmachine' 129 _RPM_HOSTNAME_REGEX = ('chromeos(\d+)(-row(\d+))?-rack(\d+[a-z]*)' 130 '-host(\d+)') 131 _LIGHTSENSOR_FILES = [ "in_illuminance0_input", 132 "in_illuminance_input", 133 "in_illuminance0_raw", 134 "in_illuminance_raw", 135 "illuminance0_input"] 136 _LIGHTSENSOR_SEARCH_DIR = '/sys/bus/iio/devices' 137 138 # Constants used in ping_wait_up() and ping_wait_down(). 139 # 140 # _PING_WAIT_COUNT is the approximate number of polling 141 # cycles to use when waiting for a host state change. 142 # 143 # _PING_STATUS_DOWN and _PING_STATUS_UP are names used 144 # for arguments to the internal _ping_wait_for_status() 145 # method. 146 _PING_WAIT_COUNT = 40 147 _PING_STATUS_DOWN = False 148 _PING_STATUS_UP = True 149 150 # Allowed values for the power_method argument. 151 152 # POWER_CONTROL_RPM: Passed as default arg for power_off/on/cycle() methods. 153 # POWER_CONTROL_SERVO: Used in set_power() and power_cycle() methods. 154 # POWER_CONTROL_MANUAL: Used in set_power() and power_cycle() methods. 155 POWER_CONTROL_RPM = 'RPM' 156 POWER_CONTROL_SERVO = 'servoj10' 157 POWER_CONTROL_MANUAL = 'manual' 158 159 POWER_CONTROL_VALID_ARGS = (POWER_CONTROL_RPM, 160 POWER_CONTROL_SERVO, 161 POWER_CONTROL_MANUAL) 162 163 _RPM_OUTLET_CHANGED = 'outlet_changed' 164 165 # URL pattern to download firmware image. 166 _FW_IMAGE_URL_PATTERN = CONFIG.get_config_value( 167 'CROS', 'firmware_url_pattern', type=str) 168 169 170 # A flag file to indicate provision failures. The file is created 171 # at the start of any AU procedure (see `machine_install()`). The 172 # file's location in stateful means that on successul update it will 173 # be removed. Thus, if this file exists, it indicates that we've 174 # tried and failed in a previous attempt to update. 175 PROVISION_FAILED = '/var/tmp/provision_failed' 176 177 178 @staticmethod 179 def check_host(host, timeout=10): 180 """ 181 Check if the given host is a chrome-os host. 182 183 @param host: An ssh host representing a device. 184 @param timeout: The timeout for the run command. 185 186 @return: True if the host device is chromeos. 187 188 """ 189 try: 190 result = host.run( 191 'grep -q CHROMEOS /etc/lsb-release && ' 192 '! test -f /mnt/stateful_partition/.android_tester && ' 193 '! grep -q moblab /etc/lsb-release', 194 ignore_status=True, timeout=timeout) 195 except (error.AutoservRunError, error.AutoservSSHTimeout): 196 return False 197 return result.exit_status == 0 198 199 200 @staticmethod 201 def get_chameleon_arguments(args_dict): 202 """Extract chameleon options from `args_dict` and return the result. 203 204 Recommended usage: 205 ~~~~~~~~ 206 args_dict = utils.args_to_dict(args) 207 chameleon_args = hosts.CrosHost.get_chameleon_arguments(args_dict) 208 host = hosts.create_host(machine, chameleon_args=chameleon_args) 209 ~~~~~~~~ 210 211 @param args_dict Dictionary from which to extract the chameleon 212 arguments. 213 """ 214 return {key: args_dict[key] 215 for key in ('chameleon_host', 'chameleon_port') 216 if key in args_dict} 217 218 219 @staticmethod 220 def get_plankton_arguments(args_dict): 221 """Extract chameleon options from `args_dict` and return the result. 222 223 Recommended usage: 224 ~~~~~~~~ 225 args_dict = utils.args_to_dict(args) 226 plankton_args = hosts.CrosHost.get_plankton_arguments(args_dict) 227 host = hosts.create_host(machine, plankton_args=plankton_args) 228 ~~~~~~~~ 229 230 @param args_dict Dictionary from which to extract the plankton 231 arguments. 232 """ 233 return {key: args_dict[key] 234 for key in ('plankton_host', 'plankton_port') 235 if key in args_dict} 236 237 238 @staticmethod 239 def get_servo_arguments(args_dict): 240 """Extract servo options from `args_dict` and return the result. 241 242 Recommended usage: 243 ~~~~~~~~ 244 args_dict = utils.args_to_dict(args) 245 servo_args = hosts.CrosHost.get_servo_arguments(args_dict) 246 host = hosts.create_host(machine, servo_args=servo_args) 247 ~~~~~~~~ 248 249 @param args_dict Dictionary from which to extract the servo 250 arguments. 251 """ 252 servo_attrs = (servo_host.SERVO_HOST_ATTR, 253 servo_host.SERVO_PORT_ATTR, 254 servo_host.SERVO_BOARD_ATTR) 255 return {key: args_dict[key] 256 for key in servo_attrs 257 if key in args_dict} 258 259 260 def _initialize(self, hostname, chameleon_args=None, servo_args=None, 261 plankton_args=None, try_lab_servo=False, 262 try_servo_repair=False, 263 ssh_verbosity_flag='', ssh_options='', 264 *args, **dargs): 265 """Initialize superclasses, |self.chameleon|, and |self.servo|. 266 267 This method will attempt to create the test-assistant object 268 (chameleon/servo) when it is needed by the test. Check 269 the docstring of chameleon_host.create_chameleon_host and 270 servo_host.create_servo_host for how this is determined. 271 272 @param hostname: Hostname of the dut. 273 @param chameleon_args: A dictionary that contains args for creating 274 a ChameleonHost. See chameleon_host for details. 275 @param servo_args: A dictionary that contains args for creating 276 a ServoHost object. See servo_host for details. 277 @param try_lab_servo: When true, indicates that an attempt should 278 be made to create a ServoHost for a DUT in 279 the test lab, even if not required by 280 `servo_args`. See servo_host for details. 281 @param try_servo_repair: If a servo host is created, check it 282 with `repair()` rather than `verify()`. 283 See servo_host for details. 284 @param ssh_verbosity_flag: String, to pass to the ssh command to control 285 verbosity. 286 @param ssh_options: String, other ssh options to pass to the ssh 287 command. 288 """ 289 super(CrosHost, self)._initialize(hostname=hostname, 290 *args, **dargs) 291 self._repair_strategy = cros_repair.create_cros_repair_strategy() 292 self.labels = base_label.LabelRetriever(cros_label.CROS_LABELS) 293 # self.env is a dictionary of environment variable settings 294 # to be exported for commands run on the host. 295 # LIBC_FATAL_STDERR_ can be useful for diagnosing certain 296 # errors that might happen. 297 self.env['LIBC_FATAL_STDERR_'] = '1' 298 self._ssh_verbosity_flag = ssh_verbosity_flag 299 self._ssh_options = ssh_options 300 # TODO(fdeng): We need to simplify the 301 # process of servo and servo_host initialization. 302 # crbug.com/298432 303 self._servo_host = servo_host.create_servo_host( 304 dut=self, servo_args=servo_args, 305 try_lab_servo=try_lab_servo, 306 try_servo_repair=try_servo_repair) 307 if self._servo_host is not None: 308 self.servo = self._servo_host.get_servo() 309 else: 310 self.servo = None 311 312 # TODO(waihong): Do the simplication on Chameleon too. 313 self._chameleon_host = chameleon_host.create_chameleon_host( 314 dut=self.hostname, chameleon_args=chameleon_args) 315 # Add plankton host if plankton args were added on command line 316 self._plankton_host = plankton_host.create_plankton_host(plankton_args) 317 318 if self._chameleon_host: 319 self.chameleon = self._chameleon_host.create_chameleon_board() 320 else: 321 self.chameleon = None 322 323 if self._plankton_host: 324 self.plankton_servo = self._plankton_host.get_servo() 325 logging.info('plankton_servo: %r', self.plankton_servo) 326 # Create the plankton object used to access the ec uart 327 self.plankton = plankton.Plankton(self.plankton_servo, 328 self._plankton_host.get_servod_server_proxy()) 329 else: 330 self.plankton = None 331 332 333 def _get_cros_repair_image_name(self): 334 info = self.host_info_store.get() 335 if info.board is None: 336 raise error.AutoservError('Cannot obtain repair image name. ' 337 'No board label value found') 338 339 return afe_utils.get_stable_cros_image_name(info.board) 340 341 342 def verify_job_repo_url(self, tag=''): 343 """ 344 Make sure job_repo_url of this host is valid. 345 346 Eg: The job_repo_url "http://lmn.cd.ab.xyx:8080/static/\ 347 lumpy-release/R29-4279.0.0/autotest/packages" claims to have the 348 autotest package for lumpy-release/R29-4279.0.0. If this isn't the case, 349 download and extract it. If the devserver embedded in the url is 350 unresponsive, update the job_repo_url of the host after staging it on 351 another devserver. 352 353 @param job_repo_url: A url pointing to the devserver where the autotest 354 package for this build should be staged. 355 @param tag: The tag from the server job, in the format 356 <job_id>-<user>/<hostname>, or <hostless> for a server job. 357 358 @raises DevServerException: If we could not resolve a devserver. 359 @raises AutoservError: If we're unable to save the new job_repo_url as 360 a result of choosing a new devserver because the old one failed to 361 respond to a health check. 362 @raises urllib2.URLError: If the devserver embedded in job_repo_url 363 doesn't respond within the timeout. 364 """ 365 job_repo_url = afe_utils.get_host_attribute(self, 366 ds_constants.JOB_REPO_URL) 367 if not job_repo_url: 368 logging.warning('No job repo url set on host %s', self.hostname) 369 return 370 371 logging.info('Verifying job repo url %s', job_repo_url) 372 devserver_url, image_name = tools.get_devserver_build_from_package_url( 373 job_repo_url) 374 375 ds = dev_server.ImageServer(devserver_url) 376 377 logging.info('Staging autotest artifacts for %s on devserver %s', 378 image_name, ds.url()) 379 380 start_time = time.time() 381 ds.stage_artifacts(image_name, ['autotest_packages']) 382 stage_time = time.time() - start_time 383 384 # Record how much of the verification time comes from a devserver 385 # restage. If we're doing things right we should not see multiple 386 # devservers for a given board/build/branch path. 387 try: 388 board, build_type, branch = server_utils.ParseBuildName( 389 image_name)[:3] 390 except server_utils.ParseBuildNameException: 391 pass 392 else: 393 devserver = devserver_url[ 394 devserver_url.find('/') + 2:devserver_url.rfind(':')] 395 stats_key = { 396 'board': board, 397 'build_type': build_type, 398 'branch': branch, 399 'devserver': devserver.replace('.', '_'), 400 } 401 402 monarch_fields = { 403 'board': board, 404 'build_type': build_type, 405 # TODO(akeshet): To be consistent with most other metrics, 406 # consider changing the following field to be named 407 # 'milestone'. 408 'branch': branch, 409 'dev_server': devserver, 410 } 411 metrics.Counter( 412 'chromeos/autotest/provision/verify_url' 413 ).increment(fields=monarch_fields) 414 metrics.SecondsDistribution( 415 'chromeos/autotest/provision/verify_url_duration' 416 ).add(stage_time, fields=monarch_fields) 417 418 419 def stage_server_side_package(self, image=None): 420 """Stage autotest server-side package on devserver. 421 422 @param image: Full path of an OS image to install or a build name. 423 424 @return: A url to the autotest server-side package. 425 426 @raise: error.AutoservError if fail to locate the build to test with, or 427 fail to stage server-side package. 428 """ 429 # If enable_drone_in_restricted_subnet is False, do not set hostname 430 # in devserver.resolve call, so a devserver in non-restricted subnet 431 # is picked to stage autotest server package for drone to download. 432 hostname = self.hostname 433 if not server_utils.ENABLE_DRONE_IN_RESTRICTED_SUBNET: 434 hostname = None 435 if image: 436 image_name = tools.get_build_from_image(image) 437 if not image_name: 438 raise error.AutoservError( 439 'Failed to parse build name from %s' % image) 440 ds = dev_server.ImageServer.resolve(image_name, hostname) 441 else: 442 job_repo_url = afe_utils.get_host_attribute( 443 self, ds_constants.JOB_REPO_URL) 444 if job_repo_url: 445 devserver_url, image_name = ( 446 tools.get_devserver_build_from_package_url(job_repo_url)) 447 # If enable_drone_in_restricted_subnet is True, use the 448 # existing devserver. Otherwise, resolve a new one in 449 # non-restricted subnet. 450 if server_utils.ENABLE_DRONE_IN_RESTRICTED_SUBNET: 451 ds = dev_server.ImageServer(devserver_url) 452 else: 453 ds = dev_server.ImageServer.resolve(image_name) 454 elif info.build is not None: 455 ds = dev_server.ImageServer.resolve(info.build, hostname) 456 else: 457 raise error.AutoservError( 458 'Failed to stage server-side package. The host has ' 459 'no job_report_url attribute or version label.') 460 461 # Get the OS version of the build, for any build older than 462 # MIN_VERSION_SUPPORT_SSP, server side packaging is not supported. 463 match = re.match('.*/R\d+-(\d+)\.', image_name) 464 if match and int(match.group(1)) < self.MIN_VERSION_SUPPORT_SSP: 465 raise error.AutoservError( 466 'Build %s is older than %s. Server side packaging is ' 467 'disabled.' % (image_name, self.MIN_VERSION_SUPPORT_SSP)) 468 469 ds.stage_artifacts(image_name, ['autotest_server_package']) 470 return '%s/static/%s/%s' % (ds.url(), image_name, 471 'autotest_server_package.tar.bz2') 472 473 474 def _try_stateful_update(self, update_url, force_update, updater): 475 """Try to use stateful update to initialize DUT. 476 477 When DUT is already running the same version that machine_install 478 tries to install, stateful update is a much faster way to clean up 479 the DUT for testing, compared to a full reimage. It is implemeted 480 by calling autoupdater.run_update, but skipping updating root, as 481 updating the kernel is time consuming and not necessary. 482 483 @param update_url: url of the image. 484 @param force_update: Set to True to update the image even if the DUT 485 is running the same version. 486 @param updater: ChromiumOSUpdater instance used to update the DUT. 487 @returns: True if the DUT was updated with stateful update. 488 489 """ 490 # Stop service ap-update-manager to prevent rebooting during autoupdate. 491 # The service is used in jetstream boards, but not other CrOS devices. 492 self.run('sudo stop ap-update-manager', ignore_status=True) 493 494 # TODO(jrbarnette): Yes, I hate this re.match() test case. 495 # It's better than the alternative: see crbug.com/360944. 496 image_name = autoupdater.url_to_image_name(update_url) 497 release_pattern = r'^.*-release/R[0-9]+-[0-9]+\.[0-9]+\.0$' 498 if not re.match(release_pattern, image_name): 499 return False 500 if not updater.check_version(): 501 return False 502 if not force_update: 503 logging.info('Canceling stateful update because the new and ' 504 'old versions are the same.') 505 return False 506 # Following folders should be rebuilt after stateful update. 507 # A test file is used to confirm each folder gets rebuilt after 508 # the stateful update. 509 folders_to_check = ['/var', '/home', '/mnt/stateful_partition'] 510 test_file = '.test_file_to_be_deleted' 511 for folder in folders_to_check: 512 touch_path = os.path.join(folder, test_file) 513 self.run('touch %s' % touch_path) 514 515 updater.run_update(update_root=False) 516 517 # Reboot to complete stateful update. 518 self.reboot(timeout=self.REBOOT_TIMEOUT, wait=True) 519 520 # After stateful update and a reboot, all of the test_files shouldn't 521 # exist any more. Otherwise the stateful update is failed. 522 return not any( 523 self.path_exists(os.path.join(folder, test_file)) 524 for folder in folders_to_check) 525 526 527 def _post_update_processing(self, updater, expected_kernel=None): 528 """After the DUT is updated, confirm machine_install succeeded. 529 530 @param updater: ChromiumOSUpdater instance used to update the DUT. 531 @param expected_kernel: kernel expected to be active after reboot, 532 or `None` to skip rollback checking. 533 534 """ 535 # Touch the lab machine file to leave a marker that 536 # distinguishes this image from other test images. 537 # Afterwards, we must re-run the autoreboot script because 538 # it depends on the _LAB_MACHINE_FILE. 539 autoreboot_cmd = ('FILE="%s" ; [ -f "$FILE" ] || ' 540 '( touch "$FILE" ; start autoreboot )') 541 self.run(autoreboot_cmd % self._LAB_MACHINE_FILE) 542 updater.verify_boot_expectations( 543 expected_kernel, rollback_message= 544 'Build %s failed to boot on %s; system rolled back to previous ' 545 'build' % (updater.update_version, self.hostname)) 546 # Check that we've got the build we meant to install. 547 if not updater.check_version_to_confirm_install(): 548 raise autoupdater.ChromiumOSError( 549 'Failed to update %s to build %s; found build ' 550 '%s instead' % (self.hostname, 551 updater.update_version, 552 self.get_release_version())) 553 554 logging.debug('Cleaning up old autotest directories.') 555 try: 556 installed_autodir = autotest.Autotest.get_installed_autodir(self) 557 self.run('rm -rf ' + installed_autodir) 558 except autotest.AutodirNotFoundError: 559 logging.debug('No autotest installed directory found.') 560 561 562 def _stage_image_for_update(self, image_name=None): 563 """Stage a build on a devserver and return the update_url and devserver. 564 565 @param image_name: a name like lumpy-release/R27-3837.0.0 566 @returns a tuple with an update URL like: 567 http://172.22.50.205:8082/update/lumpy-release/R27-3837.0.0 568 and the devserver instance. 569 """ 570 if not image_name: 571 image_name = self._get_cros_repair_image_name() 572 logging.info('Staging build for AU: %s', image_name) 573 devserver = dev_server.ImageServer.resolve(image_name, self.hostname) 574 devserver.trigger_download(image_name, synchronous=False) 575 return (tools.image_url_pattern() % (devserver.url(), image_name), 576 devserver) 577 578 579 def stage_image_for_servo(self, image_name=None): 580 """Stage a build on a devserver and return the update_url. 581 582 @param image_name: a name like lumpy-release/R27-3837.0.0 583 @returns an update URL like: 584 http://172.22.50.205:8082/update/lumpy-release/R27-3837.0.0 585 """ 586 if not image_name: 587 image_name = self._get_cros_repair_image_name() 588 logging.info('Staging build for servo install: %s', image_name) 589 devserver = dev_server.ImageServer.resolve(image_name, self.hostname) 590 devserver.stage_artifacts(image_name, ['test_image']) 591 return devserver.get_test_image_url(image_name) 592 593 594 def stage_factory_image_for_servo(self, image_name): 595 """Stage a build on a devserver and return the update_url. 596 597 @param image_name: a name like <baord>/4262.204.0 598 599 @return: An update URL, eg: 600 http://<devserver>/static/canary-channel/\ 601 <board>/4262.204.0/factory_test/chromiumos_factory_image.bin 602 603 @raises: ValueError if the factory artifact name is missing from 604 the config. 605 606 """ 607 if not image_name: 608 logging.error('Need an image_name to stage a factory image.') 609 return 610 611 factory_artifact = CONFIG.get_config_value( 612 'CROS', 'factory_artifact', type=str, default='') 613 if not factory_artifact: 614 raise ValueError('Cannot retrieve the factory artifact name from ' 615 'autotest config, and hence cannot stage factory ' 616 'artifacts.') 617 618 logging.info('Staging build for servo install: %s', image_name) 619 devserver = dev_server.ImageServer.resolve(image_name, self.hostname) 620 devserver.stage_artifacts( 621 image_name, 622 [factory_artifact], 623 archive_url=None) 624 625 return tools.factory_image_url_pattern() % (devserver.url(), image_name) 626 627 628 def _get_au_monarch_fields(self, devserver, build): 629 """Form monarch fields by given devserve & build for auto-update. 630 631 @param devserver: the devserver (ImageServer instance) for auto-update. 632 @param build: the build to be updated. 633 634 @return A dictionary of monach fields. 635 """ 636 try: 637 board, build_type, milestone, _ = server_utils.ParseBuildName(build) 638 except server_utils.ParseBuildNameException: 639 logging.warning('Unable to parse build name %s. Something is ' 640 'likely broken, but will continue anyway.', 641 build) 642 board, build_type, milestone = ('', '', '') 643 644 monarch_fields = { 645 'dev_server': devserver.hostname, 646 'board': board, 647 'build_type': build_type, 648 'milestone': milestone 649 } 650 return monarch_fields 651 652 653 def machine_install_by_devserver(self, update_url=None, force_update=False, 654 local_devserver=False, repair=False, 655 force_full_update=False): 656 """Ultiize devserver to install the DUT. 657 658 (TODO) crbugs.com/627269: The logic in this function has some overlap 659 with those in function machine_install. The merge will be done later, 660 not in the same CL. 661 662 @param update_url: The update_url or build for the host to update. 663 @param force_update: Force an update even if the version installed 664 is the same. Default:False 665 @param local_devserver: Used by test_that to allow people to 666 use their local devserver. Default: False 667 @param repair: Forces update to repair image. Implies force_update. 668 @param force_full_update: If True, do not attempt to run stateful 669 update, force a full reimage. If False, try stateful update 670 first when the dut is already installed with the same version. 671 @raises autoupdater.ChromiumOSError 672 673 @returns A tuple of (image_name, host_attributes). 674 image_name is the name of image installed, e.g., 675 veyron_jerry-release/R50-7871.0.0 676 host_attributes is a dictionary of (attribute, value), which 677 can be saved to afe_host_attributes table in database. This 678 method returns a dictionary with a single entry of 679 `job_repo_url`: repo_url, where repo_url is a devserver url to 680 autotest packages. 681 """ 682 if repair: 683 update_url = self._get_cros_repair_image_name() 684 force_update = True 685 686 if not update_url and not self._parser.options.image: 687 raise error.AutoservError( 688 'There is no update URL, nor a method to get one.') 689 690 if not update_url and self._parser.options.image: 691 update_url = self._parser.options.image 692 693 694 # Get build from parameter or AFE. 695 # If the build is not a URL, let devserver to stage it first. 696 # Otherwise, choose a devserver to trigger auto-update. 697 build = None 698 devserver = None 699 logging.debug('Resolving a devserver for auto-update') 700 previously_resolved = False 701 if update_url.startswith('http://'): 702 build = autoupdater.url_to_image_name(update_url) 703 previously_resolved = True 704 else: 705 build = update_url 706 devserver = dev_server.resolve(build, self.hostname) 707 server_name = devserver.hostname 708 709 monarch_fields = self._get_au_monarch_fields(devserver, build) 710 711 if previously_resolved: 712 # Make sure devserver for Auto-Update has staged the build. 713 if server_name not in update_url: 714 logging.debug('Resolved to devserver that does not match ' 715 'update_url. The previously resolved devserver ' 716 'must be unhealthy. Switching to use devserver %s,' 717 ' and re-staging.', 718 server_name) 719 logging.info('Staging build for AU: %s', update_url) 720 devserver.trigger_download(build, synchronous=False) 721 c = metrics.Counter( 722 'chromeos/autotest/provision/failover_download') 723 c.increment(fields=monarch_fields) 724 else: 725 logging.info('Staging build for AU: %s', update_url) 726 devserver.trigger_download(build, synchronous=False) 727 c = metrics.Counter('chromeos/autotest/provision/trigger_download') 728 c.increment(fields=monarch_fields) 729 730 # Report provision stats. 731 install_with_dev_counter = metrics.Counter( 732 'chromeos/autotest/provision/install_with_devserver') 733 install_with_dev_counter.increment(fields=monarch_fields) 734 logging.debug('Resolved devserver for auto-update: %s', devserver.url()) 735 736 # and other metrics from this function. 737 metrics.Counter('chromeos/autotest/provision/resolve' 738 ).increment(fields=monarch_fields) 739 740 success, retryable = devserver.auto_update( 741 self.hostname, build, log_dir=self.job.resultdir, 742 force_update=force_update, full_update=force_full_update) 743 if not success and retryable: 744 # It indicates that last provision failed due to devserver load 745 # issue, so another devserver is resolved to kick off provision 746 # job once again and only once. 747 logging.debug('Provision failed due to devserver issue,' 748 'retry it with another devserver.') 749 750 # Check first whether this DUT is completely offline. If so, skip 751 # the following provision tries. 752 logging.debug('Checking whether host %s is online.', self.hostname) 753 if utils.ping(self.hostname, tries=1, deadline=1) == 0: 754 devserver = dev_server.resolve( 755 build, self.hostname, ban_list=[devserver.url()]) 756 devserver.trigger_download(build, synchronous=False) 757 monarch_fields = self._get_au_monarch_fields(devserver, build) 758 logging.debug('Retry auto_update: resolved devserver for ' 759 'auto-update: %s', devserver.url()) 760 761 # Add metrics 762 install_with_dev_counter.increment(fields=monarch_fields) 763 c = metrics.Counter( 764 'chromeos/autotest/provision/retry_by_devserver') 765 monarch_fields['last_devserver'] = server_name 766 monarch_fields['host'] = self.hostname 767 c.increment(fields=monarch_fields) 768 769 devserver.auto_update( 770 self.hostname, build, log_dir=self.job.resultdir, 771 force_update=force_update, full_update=force_full_update) 772 773 774 # The reason to resolve a new devserver in function machine_install 775 # is mostly because that the update_url there may has a strange format, 776 # and it's hard to parse the devserver url from it. 777 # Since we already resolve a devserver to trigger auto-update, the same 778 # devserver is used to form JOB_REPO_URL here. Verified in local test. 779 repo_url = tools.get_package_url(devserver.url(), build) 780 return build, {ds_constants.JOB_REPO_URL: repo_url} 781 782 783 def machine_install(self, update_url=None, force_update=False, 784 local_devserver=False, repair=False, 785 force_full_update=False): 786 """Install the DUT. 787 788 Use stateful update if the DUT is already running the same build. 789 Stateful update does not update kernel and tends to run much faster 790 than a full reimage. If the DUT is running a different build, or it 791 failed to do a stateful update, full update, including kernel update, 792 will be applied to the DUT. 793 794 Once a host enters machine_install its host attribute job_repo_url 795 (used for package install) will be removed and then updated. 796 797 @param update_url: The url to use for the update 798 pattern: http://$devserver:###/update/$build 799 If update_url is None and repair is True we will install the 800 stable image listed in afe_stable_versions table. If the table 801 is not setup, global_config value under CROS.stable_cros_version 802 will be used instead. 803 @param force_update: Force an update even if the version installed 804 is the same. Default:False 805 @param local_devserver: Used by test_that to allow people to 806 use their local devserver. Default: False 807 @param repair: Forces update to repair image. Implies force_update. 808 @param force_full_update: If True, do not attempt to run stateful 809 update, force a full reimage. If False, try stateful update 810 first when the dut is already installed with the same version. 811 @raises autoupdater.ChromiumOSError 812 813 @returns A tuple of (image_name, host_attributes). 814 image_name is the name of image installed, e.g., 815 veyron_jerry-release/R50-7871.0.0 816 host_attributes is a dictionary of (attribute, value), which 817 can be saved to afe_host_attributes table in database. This 818 method returns a dictionary with a single entry of 819 `job_repo_url`: repo_url, where repo_url is a devserver url to 820 autotest packages. 821 """ 822 devserver = None 823 if repair: 824 update_url, devserver = self._stage_image_for_update() 825 force_update = True 826 827 if not update_url and not self._parser.options.image: 828 raise error.AutoservError( 829 'There is no update URL, nor a method to get one.') 830 831 if not update_url and self._parser.options.image: 832 # This is the base case where we have no given update URL i.e. 833 # dynamic suites logic etc. This is the most flexible case where we 834 # can serve an update from any of our fleet of devservers. 835 requested_build = self._parser.options.image 836 if not requested_build.startswith('http://'): 837 logging.debug('Update will be staged for this installation') 838 update_url, devserver = self._stage_image_for_update( 839 requested_build) 840 else: 841 update_url = requested_build 842 843 logging.debug('Update URL is %s', update_url) 844 845 # Report provision stats. 846 server_name = dev_server.get_hostname(update_url) 847 (metrics.Counter('chromeos/autotest/provision/install') 848 .increment(fields={'devserver': server_name})) 849 850 # Create a file to indicate if provision fails. The file will be removed 851 # by stateful update or full install. 852 self.run('touch %s' % self.PROVISION_FAILED) 853 854 update_complete = False 855 updater = autoupdater.ChromiumOSUpdater( 856 update_url, host=self, local_devserver=local_devserver) 857 if not force_full_update: 858 try: 859 # If the DUT is already running the same build, try stateful 860 # update first as it's much quicker than a full re-image. 861 update_complete = self._try_stateful_update( 862 update_url, force_update, updater) 863 except Exception as e: 864 logging.exception(e) 865 866 inactive_kernel = None 867 if update_complete or (not force_update and updater.check_version()): 868 logging.info('Install complete without full update') 869 else: 870 logging.info('DUT requires full update.') 871 self.reboot(timeout=self.REBOOT_TIMEOUT, wait=True) 872 # Stop service ap-update-manager to prevent rebooting during 873 # autoupdate. The service is used in jetstream boards, but not other 874 # CrOS devices. 875 self.run('sudo stop ap-update-manager', ignore_status=True) 876 877 num_of_attempts = provision.FLAKY_DEVSERVER_ATTEMPTS 878 879 while num_of_attempts > 0: 880 num_of_attempts -= 1 881 try: 882 updater.run_update() 883 except Exception: 884 logging.warn('Autoupdate did not complete.') 885 # Do additional check for the devserver health. Ideally, 886 # the autoupdater.py could raise an exception when it 887 # detected network flake but that would require 888 # instrumenting the update engine and parsing it log. 889 if (num_of_attempts <= 0 or 890 devserver is None or 891 dev_server.ImageServer.devserver_healthy( 892 devserver.url())): 893 raise 894 895 logging.warn('Devserver looks unhealthy. Trying another') 896 update_url, devserver = self._stage_image_for_update( 897 requested_build) 898 logging.debug('New Update URL is %s', update_url) 899 updater = autoupdater.ChromiumOSUpdater( 900 update_url, host=self, 901 local_devserver=local_devserver) 902 else: 903 break 904 905 # Give it some time in case of IO issues. 906 time.sleep(10) 907 908 # Figure out active and inactive kernel. 909 active_kernel, inactive_kernel = updater.get_kernel_state() 910 911 # Ensure inactive kernel has higher priority than active. 912 if (updater.get_kernel_priority(inactive_kernel) 913 < updater.get_kernel_priority(active_kernel)): 914 raise autoupdater.ChromiumOSError( 915 'Update failed. The priority of the inactive kernel' 916 ' partition is less than that of the active kernel' 917 ' partition.') 918 919 # Updater has returned successfully; reboot the host. 920 # 921 # Regarding the 'crossystem' command: In some cases, the 922 # TPM gets into a state such that it fails verification. 923 # We don't know why. However, this call papers over the 924 # problem by clearing the TPM during the reboot. 925 # 926 # We ignore failures from 'crossystem'. Although failure 927 # here is unexpected, and could signal a bug, the point 928 # of the exercise is to paper over problems; allowing 929 # this to fail would defeat the purpose. 930 self.run('crossystem clear_tpm_owner_request=1', 931 ignore_status=True) 932 self.reboot(timeout=self.REBOOT_TIMEOUT, wait=True) 933 934 self._post_update_processing(updater, inactive_kernel) 935 image_name = autoupdater.url_to_image_name(update_url) 936 # update_url is different from devserver url needed to stage autotest 937 # packages, therefore, resolve a new devserver url here. 938 devserver_url = dev_server.ImageServer.resolve(image_name, 939 self.hostname).url() 940 repo_url = tools.get_package_url(devserver_url, image_name) 941 return image_name, {ds_constants.JOB_REPO_URL: repo_url} 942 943 944 def _clear_fw_version_labels(self, rw_only): 945 """Clear firmware version labels from the machine. 946 947 @param rw_only: True to only clear fwrw_version; otherewise, clear 948 both fwro_version and fwrw_version. 949 """ 950 labels = self._AFE.get_labels( 951 name__startswith=provision.FW_RW_VERSION_PREFIX, 952 host__hostname=self.hostname) 953 if not rw_only: 954 labels = labels + self._AFE.get_labels( 955 name__startswith=provision.FW_RO_VERSION_PREFIX, 956 host__hostname=self.hostname) 957 for label in labels: 958 label.remove_hosts(hosts=[self.hostname]) 959 960 961 def _add_fw_version_label(self, build, rw_only): 962 """Add firmware version label to the machine. 963 964 @param build: Build of firmware. 965 @param rw_only: True to only add fwrw_version; otherwise, add both 966 fwro_version and fwrw_version. 967 968 """ 969 fw_label = provision.fwrw_version_to_label(build) 970 self._AFE.run('label_add_hosts', id=fw_label, hosts=[self.hostname]) 971 if not rw_only: 972 fw_label = provision.fwro_version_to_label(build) 973 self._AFE.run('label_add_hosts', id=fw_label, hosts=[self.hostname]) 974 975 976 def firmware_install(self, build=None, rw_only=False): 977 """Install firmware to the DUT. 978 979 Use stateful update if the DUT is already running the same build. 980 Stateful update does not update kernel and tends to run much faster 981 than a full reimage. If the DUT is running a different build, or it 982 failed to do a stateful update, full update, including kernel update, 983 will be applied to the DUT. 984 985 Once a host enters firmware_install its fw[ro|rw]_version label will 986 be removed. After the firmware is updated successfully, a new 987 fw[ro|rw]_version label will be added to the host. 988 989 @param build: The build version to which we want to provision the 990 firmware of the machine, 991 e.g. 'link-firmware/R22-2695.1.144'. 992 @param rw_only: True to only install firmware to its RW portions. Keep 993 the RO portions unchanged. 994 995 TODO(dshi): After bug 381718 is fixed, update here with corresponding 996 exceptions that could be raised. 997 998 """ 999 if not self.servo: 1000 raise error.TestError('Host %s does not have servo.' % 1001 self.hostname) 1002 1003 board = self.get_board().replace(ds_constants.BOARD_PREFIX, '') 1004 1005 # If build is not set, try to install firmware from stable CrOS. 1006 if not build: 1007 build = afe_utils.get_stable_faft_version(board) 1008 if not build: 1009 raise error.TestError( 1010 'Failed to find stable firmware build for %s.', 1011 self.hostname) 1012 logging.info('Will install firmware from build %s.', build) 1013 1014 config = FAFTConfig(board) 1015 if config.use_u_boot: 1016 ap_image = 'image-%s.bin' % board 1017 else: # Depthcharge platform 1018 ap_image = 'image.bin' 1019 ec_image = 'ec.bin' 1020 ds = dev_server.ImageServer.resolve(build, self.hostname) 1021 ds.stage_artifacts(build, ['firmware']) 1022 1023 tmpd = autotemp.tempdir(unique_id='fwimage') 1024 try: 1025 fwurl = self._FW_IMAGE_URL_PATTERN % (ds.url(), build) 1026 local_tarball = os.path.join(tmpd.name, os.path.basename(fwurl)) 1027 ds.download_file(fwurl, local_tarball) 1028 1029 self._clear_fw_version_labels(rw_only) 1030 if config.chrome_ec: 1031 logging.info('Will re-program EC %snow', 'RW ' if rw_only else '') 1032 server_utils.system('tar xf %s -C %s %s' % 1033 (local_tarball, tmpd.name, ec_image), 1034 timeout=60) 1035 self.servo.program_ec(os.path.join(tmpd.name, ec_image), rw_only) 1036 else: 1037 logging.info('Not a Chrome EC, ignore re-programing it') 1038 logging.info('Will re-program BIOS %snow', 'RW ' if rw_only else '') 1039 server_utils.system('tar xf %s -C %s %s' % 1040 (local_tarball, tmpd.name, ap_image), 1041 timeout=60) 1042 self.servo.program_bios(os.path.join(tmpd.name, ap_image), rw_only) 1043 self.servo.get_power_state_controller().reset() 1044 time.sleep(self.servo.BOOT_DELAY) 1045 if utils.host_is_in_lab_zone(self.hostname): 1046 self._add_fw_version_label(build, rw_only) 1047 finally: 1048 tmpd.clean() 1049 1050 1051 def show_update_engine_log(self): 1052 """Output update engine log.""" 1053 logging.debug('Dumping %s', client_constants.UPDATE_ENGINE_LOG) 1054 self.run('cat %s' % client_constants.UPDATE_ENGINE_LOG) 1055 1056 1057 def _get_board_from_afe(self): 1058 """Retrieve this host's board from its labels stored locally. 1059 1060 Looks for a host label of the form "board:<board>", and 1061 returns the "<board>" part of the label. `None` is returned 1062 if there is not a single, unique label matching the pattern. 1063 1064 @returns board from label, or `None`. 1065 """ 1066 board = afe_utils.get_board(self) 1067 if board is None: 1068 raise error.AutoservError('DUT cannot be repaired, ' 1069 'there is no board attribute.') 1070 return board 1071 1072 1073 def servo_install(self, image_url=None, usb_boot_timeout=USB_BOOT_TIMEOUT, 1074 install_timeout=INSTALL_TIMEOUT): 1075 """ 1076 Re-install the OS on the DUT by: 1077 1) installing a test image on a USB storage device attached to the Servo 1078 board, 1079 2) booting that image in recovery mode, and then 1080 3) installing the image with chromeos-install. 1081 1082 @param image_url: If specified use as the url to install on the DUT. 1083 otherwise boot the currently staged image on the USB stick. 1084 @param usb_boot_timeout: The usb_boot_timeout to use during reimage. 1085 Factory images need a longer usb_boot_timeout than regular 1086 cros images. 1087 @param install_timeout: The timeout to use when installing the chromeos 1088 image. Factory images need a longer install_timeout. 1089 1090 @raises AutoservError if the image fails to boot. 1091 1092 """ 1093 logging.info('Downloading image to USB, then booting from it. Usb boot ' 1094 'timeout = %s', usb_boot_timeout) 1095 with metrics.SecondsTimer( 1096 'chromeos/autotest/provision/servo_install/boot_duration'): 1097 self.servo.install_recovery_image(image_url) 1098 if not self.wait_up(timeout=usb_boot_timeout): 1099 raise hosts.AutoservRepairError( 1100 'DUT failed to boot from USB after %d seconds' % 1101 usb_boot_timeout) 1102 1103 # The new chromeos-tpm-recovery has been merged since R44-7073.0.0. 1104 # In old CrOS images, this command fails. Skip the error. 1105 logging.info('Resetting the TPM status') 1106 try: 1107 self.run('chromeos-tpm-recovery') 1108 except error.AutoservRunError: 1109 logging.warn('chromeos-tpm-recovery is too old.') 1110 1111 1112 with metrics.SecondsTimer( 1113 'chromeos/autotest/provision/servo_install/install_duration'): 1114 logging.info('Installing image through chromeos-install.') 1115 self.run('chromeos-install --yes', timeout=install_timeout) 1116 self.halt() 1117 1118 logging.info('Power cycling DUT through servo.') 1119 self.servo.get_power_state_controller().power_off() 1120 self.servo.switch_usbkey('off') 1121 # N.B. The Servo API requires that we use power_on() here 1122 # for two reasons: 1123 # 1) After turning on a DUT in recovery mode, you must turn 1124 # it off and then on with power_on() once more to 1125 # disable recovery mode (this is a Parrot specific 1126 # requirement). 1127 # 2) After power_off(), the only way to turn on is with 1128 # power_on() (this is a Storm specific requirement). 1129 self.servo.get_power_state_controller().power_on() 1130 1131 logging.info('Waiting for DUT to come back up.') 1132 if not self.wait_up(timeout=self.BOOT_TIMEOUT): 1133 raise error.AutoservError('DUT failed to reboot installed ' 1134 'test image after %d seconds' % 1135 self.BOOT_TIMEOUT) 1136 1137 1138 def repair_servo(self): 1139 """ 1140 Confirm that servo is initialized and verified. 1141 1142 If the servo object is missing, attempt to repair the servo 1143 host. Repair failures are passed back to the caller. 1144 1145 @raise AutoservError: If there is no servo host for this CrOS 1146 host. 1147 """ 1148 if self.servo: 1149 return 1150 if not self._servo_host: 1151 raise error.AutoservError('No servo host for %s.' % 1152 self.hostname) 1153 self._servo_host.repair() 1154 self.servo = self._servo_host.get_servo() 1155 1156 1157 def repair(self): 1158 """Attempt to get the DUT to pass `self.verify()`. 1159 1160 This overrides the base class function for repair; it does 1161 not call back to the parent class, but instead relies on 1162 `self._repair_strategy` to coordinate the verification and 1163 repair steps needed to get the DUT working. 1164 """ 1165 self._repair_strategy.repair(self) 1166 1167 1168 def close(self): 1169 super(CrosHost, self).close() 1170 if self._chameleon_host: 1171 self._chameleon_host.close() 1172 1173 if self._servo_host: 1174 self._servo_host.close() 1175 1176 1177 def get_power_supply_info(self): 1178 """Get the output of power_supply_info. 1179 1180 power_supply_info outputs the info of each power supply, e.g., 1181 Device: Line Power 1182 online: no 1183 type: Mains 1184 voltage (V): 0 1185 current (A): 0 1186 Device: Battery 1187 state: Discharging 1188 percentage: 95.9276 1189 technology: Li-ion 1190 1191 Above output shows two devices, Line Power and Battery, with details of 1192 each device listed. This function parses the output into a dictionary, 1193 with key being the device name, and value being a dictionary of details 1194 of the device info. 1195 1196 @return: The dictionary of power_supply_info, e.g., 1197 {'Line Power': {'online': 'yes', 'type': 'main'}, 1198 'Battery': {'vendor': 'xyz', 'percentage': '100'}} 1199 @raise error.AutoservRunError if power_supply_info tool is not found in 1200 the DUT. Caller should handle this error to avoid false failure 1201 on verification. 1202 """ 1203 result = self.run('power_supply_info').stdout.strip() 1204 info = {} 1205 device_name = None 1206 device_info = {} 1207 for line in result.split('\n'): 1208 pair = [v.strip() for v in line.split(':')] 1209 if len(pair) != 2: 1210 continue 1211 if pair[0] == 'Device': 1212 if device_name: 1213 info[device_name] = device_info 1214 device_name = pair[1] 1215 device_info = {} 1216 else: 1217 device_info[pair[0]] = pair[1] 1218 if device_name and not device_name in info: 1219 info[device_name] = device_info 1220 return info 1221 1222 1223 def get_battery_percentage(self): 1224 """Get the battery percentage. 1225 1226 @return: The percentage of battery level, value range from 0-100. Return 1227 None if the battery info cannot be retrieved. 1228 """ 1229 try: 1230 info = self.get_power_supply_info() 1231 logging.info(info) 1232 return float(info['Battery']['percentage']) 1233 except (KeyError, ValueError, error.AutoservRunError): 1234 return None 1235 1236 1237 def is_ac_connected(self): 1238 """Check if the dut has power adapter connected and charging. 1239 1240 @return: True if power adapter is connected and charging. 1241 """ 1242 try: 1243 info = self.get_power_supply_info() 1244 return info['Line Power']['online'] == 'yes' 1245 except (KeyError, error.AutoservRunError): 1246 return None 1247 1248 1249 def _cleanup_poweron(self): 1250 """Special cleanup method to make sure hosts always get power back.""" 1251 afe = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10) 1252 hosts = afe.get_hosts(hostname=self.hostname) 1253 if not hosts or not (self._RPM_OUTLET_CHANGED in 1254 hosts[0].attributes): 1255 return 1256 logging.debug('This host has recently interacted with the RPM' 1257 ' Infrastructure. Ensuring power is on.') 1258 try: 1259 self.power_on() 1260 afe.set_host_attribute(self._RPM_OUTLET_CHANGED, None, 1261 hostname=self.hostname) 1262 except rpm_client.RemotePowerException: 1263 logging.error('Failed to turn Power On for this host after ' 1264 'cleanup through the RPM Infrastructure.') 1265 autotest_es.post( 1266 type_str='RPM_poweron_failure', 1267 metadata={'hostname': self.hostname}) 1268 1269 battery_percentage = self.get_battery_percentage() 1270 if battery_percentage and battery_percentage < 50: 1271 raise 1272 elif self.is_ac_connected(): 1273 logging.info('The device has power adapter connected and ' 1274 'charging. No need to try to turn RPM on ' 1275 'again.') 1276 afe.set_host_attribute(self._RPM_OUTLET_CHANGED, None, 1277 hostname=self.hostname) 1278 logging.info('Battery level is now at %s%%. The device may ' 1279 'still have enough power to run test, so no ' 1280 'exception will be raised.', battery_percentage) 1281 1282 1283 def _is_factory_image(self): 1284 """Checks if the image on the DUT is a factory image. 1285 1286 @return: True if the image on the DUT is a factory image. 1287 False otherwise. 1288 """ 1289 result = self.run('[ -f /root/.factory_test ]', ignore_status=True) 1290 return result.exit_status == 0 1291 1292 1293 def _restart_ui(self): 1294 """Restart the Chrome UI. 1295 1296 @raises: FactoryImageCheckerException for factory images, since 1297 we cannot attempt to restart ui on them. 1298 error.AutoservRunError for any other type of error that 1299 occurs while restarting ui. 1300 """ 1301 if self._is_factory_image(): 1302 raise FactoryImageCheckerException('Cannot restart ui on factory ' 1303 'images') 1304 1305 # TODO(jrbarnette): The command to stop/start the ui job 1306 # should live inside cros_ui, too. However that would seem 1307 # to imply interface changes to the existing start()/restart() 1308 # functions, which is a bridge too far (for now). 1309 prompt = cros_ui.get_chrome_session_ident(self) 1310 self.run('stop ui; start ui') 1311 cros_ui.wait_for_chrome_ready(prompt, self) 1312 1313 1314 def get_release_version(self): 1315 """Get the value of attribute CHROMEOS_RELEASE_VERSION from lsb-release. 1316 1317 @returns The version string in lsb-release, under attribute 1318 CHROMEOS_RELEASE_VERSION. 1319 """ 1320 lsb_release_content = self.run( 1321 'cat "%s"' % client_constants.LSB_RELEASE).stdout.strip() 1322 return lsbrelease_utils.get_chromeos_release_version( 1323 lsb_release_content=lsb_release_content) 1324 1325 1326 def verify_cros_version_label(self): 1327 """ Make sure host's cros-version label match the actual image in dut. 1328 1329 Remove any cros-version: label that doesn't match that installed in 1330 the dut. 1331 1332 @param raise_error: Set to True to raise exception if any mismatch found 1333 1334 @raise error.AutoservError: If any mismatch between cros-version label 1335 and the build installed in dut is found. 1336 """ 1337 labels = self._AFE.get_labels( 1338 name__startswith=ds_constants.VERSION_PREFIX, 1339 host__hostname=self.hostname) 1340 mismatch_found = False 1341 if labels: 1342 # Get CHROMEOS_RELEASE_VERSION from lsb-release, e.g., 6908.0.0. 1343 # Note that it's different from cros-version label, which has 1344 # builder and branch info, e.g., 1345 # cros-version:peppy-release/R43-6908.0.0 1346 release_version = self.get_release_version() 1347 host_list = [self.hostname] 1348 for label in labels: 1349 # Remove any cros-version label that does not match 1350 # release_version. 1351 build_version = label.name[len(ds_constants.VERSION_PREFIX):] 1352 if not utils.version_match(build_version, release_version): 1353 logging.warn('cros-version label "%s" does not match ' 1354 'release version %s. Removing the label.', 1355 label.name, release_version) 1356 label.remove_hosts(hosts=host_list) 1357 mismatch_found = True 1358 if mismatch_found: 1359 autotest_es.post(use_http=True, 1360 type_str='cros_version_label_mismatch', 1361 metadata={'hostname': self.hostname}) 1362 raise error.AutoservError('The host has wrong cros-version label.') 1363 1364 1365 def cleanup(self): 1366 self.run('rm -f %s' % client_constants.CLEANUP_LOGS_PAUSED_FILE) 1367 try: 1368 self._restart_ui() 1369 except (error.AutotestRunError, error.AutoservRunError, 1370 FactoryImageCheckerException): 1371 logging.warning('Unable to restart ui, rebooting device.') 1372 # Since restarting the UI fails fall back to normal Autotest 1373 # cleanup routines, i.e. reboot the machine. 1374 super(CrosHost, self).cleanup() 1375 # Check if the rpm outlet was manipulated. 1376 if self.has_power(): 1377 self._cleanup_poweron() 1378 self.verify_cros_version_label() 1379 1380 1381 def reboot(self, **dargs): 1382 """ 1383 This function reboots the site host. The more generic 1384 RemoteHost.reboot() performs sync and sleeps for 5 1385 seconds. This is not necessary for Chrome OS devices as the 1386 sync should be finished in a short time during the reboot 1387 command. 1388 """ 1389 if 'reboot_cmd' not in dargs: 1390 reboot_timeout = dargs.get('reboot_timeout', 10) 1391 dargs['reboot_cmd'] = ('sleep 1; ' 1392 'reboot & sleep %d; ' 1393 'reboot -f' % reboot_timeout) 1394 # Enable fastsync to avoid running extra sync commands before reboot. 1395 if 'fastsync' not in dargs: 1396 dargs['fastsync'] = True 1397 1398 # For purposes of logging reboot times: 1399 # Get the board name i.e. 'daisy_spring' 1400 board_fullname = self.get_board() 1401 1402 # Strip the prefix and add it to dargs. 1403 dargs['board'] = board_fullname[board_fullname.find(':')+1:] 1404 # Record who called us 1405 orig = sys._getframe(1).f_code 1406 metric_fields = {'board' : dargs['board'], 1407 'dut_host_name' : self.hostname, 1408 'success' : True} 1409 metric_debug_fields = {'board' : dargs['board'], 1410 'caller' : "%s:%s" % (orig.co_filename, orig.co_name), 1411 'success' : True, 1412 'error' : ''} 1413 1414 t0 = time.time() 1415 try: 1416 super(CrosHost, self).reboot(**dargs) 1417 except Exception as e: 1418 metric_fields['success'] = False 1419 metric_debug_fields['success'] = False 1420 metric_debug_fields['error'] = type(e).__name__ 1421 raise 1422 finally: 1423 duration = int(time.time() - t0) 1424 metrics.Counter( 1425 'chromeos/autotest/autoserv/reboot_count').increment( 1426 fields=metric_fields) 1427 metrics.Counter( 1428 'chromeos/autotest/autoserv/reboot_debug').increment( 1429 fields=metric_debug_fields) 1430 metrics.SecondsDistribution( 1431 'chromeos/autotest/autoserv/reboot_duration').add( 1432 duration, fields=metric_fields) 1433 1434 1435 def suspend(self, **dargs): 1436 """ 1437 This function suspends the site host. 1438 """ 1439 suspend_time = dargs.get('suspend_time', 60) 1440 dargs['timeout'] = suspend_time 1441 if 'suspend_cmd' not in dargs: 1442 dargs['suspend_cmd'] = ' && '.join([ 1443 'echo 0 > /sys/class/rtc/rtc0/wakealarm', 1444 'echo +%d > /sys/class/rtc/rtc0/wakealarm' % suspend_time, 1445 'powerd_dbus_suspend --delay=0']) 1446 super(CrosHost, self).suspend(**dargs) 1447 1448 1449 def upstart_status(self, service_name): 1450 """Check the status of an upstart init script. 1451 1452 @param service_name: Service to look up. 1453 1454 @returns True if the service is running, False otherwise. 1455 """ 1456 return self.run('status %s | grep start/running' % 1457 service_name).stdout.strip() != '' 1458 1459 1460 def verify_software(self): 1461 """Verify working software on a Chrome OS system. 1462 1463 Tests for the following conditions: 1464 1. All conditions tested by the parent version of this 1465 function. 1466 2. Sufficient space in /mnt/stateful_partition. 1467 3. Sufficient space in /mnt/stateful_partition/encrypted. 1468 4. update_engine answers a simple status request over DBus. 1469 1470 """ 1471 super(CrosHost, self).verify_software() 1472 default_kilo_inodes_required = CONFIG.get_config_value( 1473 'SERVER', 'kilo_inodes_required', type=int, default=100) 1474 board = self.get_board().replace(ds_constants.BOARD_PREFIX, '') 1475 kilo_inodes_required = CONFIG.get_config_value( 1476 'SERVER', 'kilo_inodes_required_%s' % board, 1477 type=int, default=default_kilo_inodes_required) 1478 self.check_inodes('/mnt/stateful_partition', kilo_inodes_required) 1479 self.check_diskspace( 1480 '/mnt/stateful_partition', 1481 CONFIG.get_config_value( 1482 'SERVER', 'gb_diskspace_required', type=float, 1483 default=20.0)) 1484 encrypted_stateful_path = '/mnt/stateful_partition/encrypted' 1485 # Not all targets build with encrypted stateful support. 1486 if self.path_exists(encrypted_stateful_path): 1487 self.check_diskspace( 1488 encrypted_stateful_path, 1489 CONFIG.get_config_value( 1490 'SERVER', 'gb_encrypted_diskspace_required', type=float, 1491 default=0.1)) 1492 1493 if not self.upstart_status('system-services'): 1494 raise error.AutoservError('Chrome failed to reach login. ' 1495 'System services not running.') 1496 1497 # Factory images don't run update engine, 1498 # goofy controls dbus on these DUTs. 1499 if not self._is_factory_image(): 1500 self.run('update_engine_client --status') 1501 1502 self.verify_cros_version_label() 1503 1504 1505 def verify(self): 1506 self._repair_strategy.verify(self) 1507 1508 1509 def make_ssh_command(self, user='root', port=22, opts='', hosts_file=None, 1510 connect_timeout=None, alive_interval=None): 1511 """Override default make_ssh_command to use options tuned for Chrome OS. 1512 1513 Tuning changes: 1514 - ConnectTimeout=30; maximum of 30 seconds allowed for an SSH 1515 connection failure. Consistency with remote_access.sh. 1516 1517 - ServerAliveInterval=900; which causes SSH to ping connection every 1518 900 seconds. In conjunction with ServerAliveCountMax ensures 1519 that if the connection dies, Autotest will bail out. 1520 Originally tried 60 secs, but saw frequent job ABORTS where 1521 the test completed successfully. Later increased from 180 seconds to 1522 900 seconds to account for tests where the DUT is suspended for 1523 longer periods of time. 1524 1525 - ServerAliveCountMax=3; consistency with remote_access.sh. 1526 1527 - ConnectAttempts=4; reduce flakiness in connection errors; 1528 consistency with remote_access.sh. 1529 1530 - UserKnownHostsFile=/dev/null; we don't care about the keys. 1531 Host keys change with every new installation, don't waste 1532 memory/space saving them. 1533 1534 - SSH protocol forced to 2; needed for ServerAliveInterval. 1535 1536 @param user User name to use for the ssh connection. 1537 @param port Port on the target host to use for ssh connection. 1538 @param opts Additional options to the ssh command. 1539 @param hosts_file Ignored. 1540 @param connect_timeout Ignored. 1541 @param alive_interval Ignored. 1542 """ 1543 base_command = ('/usr/bin/ssh -a -x %s %s %s' 1544 ' -o StrictHostKeyChecking=no' 1545 ' -o UserKnownHostsFile=/dev/null -o BatchMode=yes' 1546 ' -o ConnectTimeout=30 -o ServerAliveInterval=900' 1547 ' -o ServerAliveCountMax=3 -o ConnectionAttempts=4' 1548 ' -o Protocol=2 -l %s -p %d') 1549 return base_command % (self._ssh_verbosity_flag, self._ssh_options, 1550 opts, user, port) 1551 def syslog(self, message, tag='autotest'): 1552 """Logs a message to syslog on host. 1553 1554 @param message String message to log into syslog 1555 @param tag String tag prefix for syslog 1556 1557 """ 1558 self.run('logger -t "%s" "%s"' % (tag, message)) 1559 1560 1561 def _ping_check_status(self, status): 1562 """Ping the host once, and return whether it has a given status. 1563 1564 @param status Check the ping status against this value. 1565 @return True iff `status` and the result of ping are the same 1566 (i.e. both True or both False). 1567 1568 """ 1569 ping_val = utils.ping(self.hostname, tries=1, deadline=1) 1570 return not (status ^ (ping_val == 0)) 1571 1572 def _ping_wait_for_status(self, status, timeout): 1573 """Wait for the host to have a given status (UP or DOWN). 1574 1575 Status is checked by polling. Polling will not last longer 1576 than the number of seconds in `timeout`. The polling 1577 interval will be long enough that only approximately 1578 _PING_WAIT_COUNT polling cycles will be executed, subject 1579 to a maximum interval of about one minute. 1580 1581 @param status Waiting will stop immediately if `ping` of the 1582 host returns this status. 1583 @param timeout Poll for at most this many seconds. 1584 @return True iff the host status from `ping` matched the 1585 requested status at the time of return. 1586 1587 """ 1588 # _ping_check_status() takes about 1 second, hence the 1589 # "- 1" in the formula below. 1590 # FIXME: if the ping command errors then _ping_check_status() 1591 # returns instantly. If timeout is also smaller than twice 1592 # _PING_WAIT_COUNT then the while loop below forks many 1593 # thousands of ping commands (see /tmp/test_that_results_XXXXX/ 1594 # /results-1-logging_YYY.ZZZ/debug/autoserv.DEBUG) and hogs one 1595 # CPU core for 60 seconds. 1596 poll_interval = min(int(timeout / self._PING_WAIT_COUNT), 60) - 1 1597 end_time = time.time() + timeout 1598 while time.time() <= end_time: 1599 if self._ping_check_status(status): 1600 return True 1601 if poll_interval > 0: 1602 time.sleep(poll_interval) 1603 1604 # The last thing we did was sleep(poll_interval), so it may 1605 # have been too long since the last `ping`. Check one more 1606 # time, just to be sure. 1607 return self._ping_check_status(status) 1608 1609 def ping_wait_up(self, timeout): 1610 """Wait for the host to respond to `ping`. 1611 1612 N.B. This method is not a reliable substitute for 1613 `wait_up()`, because a host that responds to ping will not 1614 necessarily respond to ssh. This method should only be used 1615 if the target DUT can be considered functional even if it 1616 can't be reached via ssh. 1617 1618 @param timeout Minimum time to allow before declaring the 1619 host to be non-responsive. 1620 @return True iff the host answered to ping before the timeout. 1621 1622 """ 1623 return self._ping_wait_for_status(self._PING_STATUS_UP, timeout) 1624 1625 def ping_wait_down(self, timeout): 1626 """Wait until the host no longer responds to `ping`. 1627 1628 This function can be used as a slightly faster version of 1629 `wait_down()`, by avoiding potentially long ssh timeouts. 1630 1631 @param timeout Minimum time to allow for the host to become 1632 non-responsive. 1633 @return True iff the host quit answering ping before the 1634 timeout. 1635 1636 """ 1637 return self._ping_wait_for_status(self._PING_STATUS_DOWN, timeout) 1638 1639 def test_wait_for_sleep(self, sleep_timeout=None): 1640 """Wait for the client to enter low-power sleep mode. 1641 1642 The test for "is asleep" can't distinguish a system that is 1643 powered off; to confirm that the unit was asleep, it is 1644 necessary to force resume, and then call 1645 `test_wait_for_resume()`. 1646 1647 This function is expected to be called from a test as part 1648 of a sequence like the following: 1649 1650 ~~~~~~~~ 1651 boot_id = host.get_boot_id() 1652 # trigger sleep on the host 1653 host.test_wait_for_sleep() 1654 # trigger resume on the host 1655 host.test_wait_for_resume(boot_id) 1656 ~~~~~~~~ 1657 1658 @param sleep_timeout time limit in seconds to allow the host sleep. 1659 1660 @exception TestFail The host did not go to sleep within 1661 the allowed time. 1662 """ 1663 if sleep_timeout is None: 1664 sleep_timeout = self.SLEEP_TIMEOUT 1665 1666 if not self.ping_wait_down(timeout=sleep_timeout): 1667 raise error.TestFail( 1668 'client failed to sleep after %d seconds' % sleep_timeout) 1669 1670 1671 def test_wait_for_resume(self, old_boot_id, resume_timeout=None): 1672 """Wait for the client to resume from low-power sleep mode. 1673 1674 The `old_boot_id` parameter should be the value from 1675 `get_boot_id()` obtained prior to entering sleep mode. A 1676 `TestFail` exception is raised if the boot id changes. 1677 1678 See @ref test_wait_for_sleep for more on this function's 1679 usage. 1680 1681 @param old_boot_id A boot id value obtained before the 1682 target host went to sleep. 1683 @param resume_timeout time limit in seconds to allow the host up. 1684 1685 @exception TestFail The host did not respond within the 1686 allowed time. 1687 @exception TestFail The host responded, but the boot id test 1688 indicated a reboot rather than a sleep 1689 cycle. 1690 """ 1691 if resume_timeout is None: 1692 resume_timeout = self.RESUME_TIMEOUT 1693 1694 if not self.wait_up(timeout=resume_timeout): 1695 raise error.TestFail( 1696 'client failed to resume from sleep after %d seconds' % 1697 resume_timeout) 1698 else: 1699 new_boot_id = self.get_boot_id() 1700 if new_boot_id != old_boot_id: 1701 logging.error('client rebooted (old boot %s, new boot %s)', 1702 old_boot_id, new_boot_id) 1703 raise error.TestFail( 1704 'client rebooted, but sleep was expected') 1705 1706 1707 def test_wait_for_shutdown(self, shutdown_timeout=None): 1708 """Wait for the client to shut down. 1709 1710 The test for "has shut down" can't distinguish a system that 1711 is merely asleep; to confirm that the unit was down, it is 1712 necessary to force boot, and then call test_wait_for_boot(). 1713 1714 This function is expected to be called from a test as part 1715 of a sequence like the following: 1716 1717 ~~~~~~~~ 1718 boot_id = host.get_boot_id() 1719 # trigger shutdown on the host 1720 host.test_wait_for_shutdown() 1721 # trigger boot on the host 1722 host.test_wait_for_boot(boot_id) 1723 ~~~~~~~~ 1724 1725 @param shutdown_timeout time limit in seconds to allow the host down. 1726 @exception TestFail The host did not shut down within the 1727 allowed time. 1728 """ 1729 if shutdown_timeout is None: 1730 shutdown_timeout = self.SHUTDOWN_TIMEOUT 1731 1732 if not self.ping_wait_down(timeout=shutdown_timeout): 1733 raise error.TestFail( 1734 'client failed to shut down after %d seconds' % 1735 shutdown_timeout) 1736 1737 1738 def test_wait_for_boot(self, old_boot_id=None): 1739 """Wait for the client to boot from cold power. 1740 1741 The `old_boot_id` parameter should be the value from 1742 `get_boot_id()` obtained prior to shutting down. A 1743 `TestFail` exception is raised if the boot id does not 1744 change. The boot id test is omitted if `old_boot_id` is not 1745 specified. 1746 1747 See @ref test_wait_for_shutdown for more on this function's 1748 usage. 1749 1750 @param old_boot_id A boot id value obtained before the 1751 shut down. 1752 1753 @exception TestFail The host did not respond within the 1754 allowed time. 1755 @exception TestFail The host responded, but the boot id test 1756 indicated that there was no reboot. 1757 """ 1758 if not self.wait_up(timeout=self.REBOOT_TIMEOUT): 1759 raise error.TestFail( 1760 'client failed to reboot after %d seconds' % 1761 self.REBOOT_TIMEOUT) 1762 elif old_boot_id: 1763 if self.get_boot_id() == old_boot_id: 1764 logging.error('client not rebooted (boot %s)', 1765 old_boot_id) 1766 raise error.TestFail( 1767 'client is back up, but did not reboot') 1768 1769 1770 @staticmethod 1771 def check_for_rpm_support(hostname): 1772 """For a given hostname, return whether or not it is powered by an RPM. 1773 1774 @param hostname: hostname to check for rpm support. 1775 1776 @return None if this host does not follows the defined naming format 1777 for RPM powered DUT's in the lab. If it does follow the format, 1778 it returns a regular expression MatchObject instead. 1779 """ 1780 return re.match(CrosHost._RPM_HOSTNAME_REGEX, hostname) 1781 1782 1783 def has_power(self): 1784 """For this host, return whether or not it is powered by an RPM. 1785 1786 @return True if this host is in the CROS lab and follows the defined 1787 naming format. 1788 """ 1789 return CrosHost.check_for_rpm_support(self.hostname) 1790 1791 1792 def _set_power(self, state, power_method): 1793 """Sets the power to the host via RPM, Servo or manual. 1794 1795 @param state Specifies which power state to set to DUT 1796 @param power_method Specifies which method of power control to 1797 use. By default "RPM" will be used. Valid values 1798 are the strings "RPM", "manual", "servoj10". 1799 1800 """ 1801 ACCEPTABLE_STATES = ['ON', 'OFF'] 1802 1803 if state.upper() not in ACCEPTABLE_STATES: 1804 raise error.TestError('State must be one of: %s.' 1805 % (ACCEPTABLE_STATES,)) 1806 1807 if power_method == self.POWER_CONTROL_SERVO: 1808 logging.info('Setting servo port J10 to %s', state) 1809 self.servo.set('prtctl3_pwren', state.lower()) 1810 time.sleep(self._USB_POWER_TIMEOUT) 1811 elif power_method == self.POWER_CONTROL_MANUAL: 1812 logging.info('You have %d seconds to set the AC power to %s.', 1813 self._POWER_CYCLE_TIMEOUT, state) 1814 time.sleep(self._POWER_CYCLE_TIMEOUT) 1815 else: 1816 if not self.has_power(): 1817 raise error.TestFail('DUT does not have RPM connected.') 1818 afe = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10) 1819 afe.set_host_attribute(self._RPM_OUTLET_CHANGED, True, 1820 hostname=self.hostname) 1821 rpm_client.set_power(self.hostname, state.upper(), timeout_mins=5) 1822 1823 1824 def power_off(self, power_method=POWER_CONTROL_RPM): 1825 """Turn off power to this host via RPM, Servo or manual. 1826 1827 @param power_method Specifies which method of power control to 1828 use. By default "RPM" will be used. Valid values 1829 are the strings "RPM", "manual", "servoj10". 1830 1831 """ 1832 self._set_power('OFF', power_method) 1833 1834 1835 def power_on(self, power_method=POWER_CONTROL_RPM): 1836 """Turn on power to this host via RPM, Servo or manual. 1837 1838 @param power_method Specifies which method of power control to 1839 use. By default "RPM" will be used. Valid values 1840 are the strings "RPM", "manual", "servoj10". 1841 1842 """ 1843 self._set_power('ON', power_method) 1844 1845 1846 def power_cycle(self, power_method=POWER_CONTROL_RPM): 1847 """Cycle power to this host by turning it OFF, then ON. 1848 1849 @param power_method Specifies which method of power control to 1850 use. By default "RPM" will be used. Valid values 1851 are the strings "RPM", "manual", "servoj10". 1852 1853 """ 1854 if power_method in (self.POWER_CONTROL_SERVO, 1855 self.POWER_CONTROL_MANUAL): 1856 self.power_off(power_method=power_method) 1857 time.sleep(self._POWER_CYCLE_TIMEOUT) 1858 self.power_on(power_method=power_method) 1859 else: 1860 rpm_client.set_power(self.hostname, 'CYCLE') 1861 1862 1863 def get_platform(self): 1864 """Determine the correct platform label for this host. 1865 1866 @returns a string representing this host's platform. 1867 """ 1868 crossystem = utils.Crossystem(self) 1869 crossystem.init() 1870 # Extract fwid value and use the leading part as the platform id. 1871 # fwid generally follow the format of {platform}.{firmware version} 1872 # Example: Alex.X.YYY.Z or Google_Alex.X.YYY.Z 1873 platform = crossystem.fwid().split('.')[0].lower() 1874 # Newer platforms start with 'Google_' while the older ones do not. 1875 return platform.replace('google_', '') 1876 1877 1878 def get_architecture(self): 1879 """Determine the correct architecture label for this host. 1880 1881 @returns a string representing this host's architecture. 1882 """ 1883 crossystem = utils.Crossystem(self) 1884 crossystem.init() 1885 return crossystem.arch() 1886 1887 1888 def get_chrome_version(self): 1889 """Gets the Chrome version number and milestone as strings. 1890 1891 Invokes "chrome --version" to get the version number and milestone. 1892 1893 @return A tuple (chrome_ver, milestone) where "chrome_ver" is the 1894 current Chrome version number as a string (in the form "W.X.Y.Z") 1895 and "milestone" is the first component of the version number 1896 (the "W" from "W.X.Y.Z"). If the version number cannot be parsed 1897 in the "W.X.Y.Z" format, the "chrome_ver" will be the full output 1898 of "chrome --version" and the milestone will be the empty string. 1899 1900 """ 1901 version_string = self.run(client_constants.CHROME_VERSION_COMMAND).stdout 1902 return utils.parse_chrome_version(version_string) 1903 1904 1905 # TODO(kevcheng): change this to just return the board without the 1906 # 'board:' prefix and fix up all the callers. Also look into removing the 1907 # need for this method. 1908 def get_board(self): 1909 """Determine the correct board label for this host. 1910 1911 @returns a string representing this host's board. 1912 """ 1913 release_info = utils.parse_cmd_output('cat /etc/lsb-release', 1914 run_method=self.run) 1915 return (ds_constants.BOARD_PREFIX + 1916 release_info['CHROMEOS_RELEASE_BOARD']) 1917 1918 1919 1920 def has_lightsensor(self): 1921 """Determine the correct board label for this host. 1922 1923 @returns the string 'lightsensor' if this host has a lightsensor or 1924 None if it does not. 1925 """ 1926 search_cmd = "find -L %s -maxdepth 4 | egrep '%s'" % ( 1927 self._LIGHTSENSOR_SEARCH_DIR, '|'.join(self._LIGHTSENSOR_FILES)) 1928 try: 1929 # Run the search cmd following the symlinks. Stderr_tee is set to 1930 # None as there can be a symlink loop, but the command will still 1931 # execute correctly with a few messages printed to stderr. 1932 self.run(search_cmd, stdout_tee=None, stderr_tee=None) 1933 return 'lightsensor' 1934 except error.AutoservRunError: 1935 # egrep exited with a return code of 1 meaning none of the possible 1936 # lightsensor files existed. 1937 return None 1938 1939 1940 def has_bluetooth(self): 1941 """Determine the correct board label for this host. 1942 1943 @returns the string 'bluetooth' if this host has bluetooth or 1944 None if it does not. 1945 """ 1946 try: 1947 self.run('test -d /sys/class/bluetooth/hci0') 1948 # test exited with a return code of 0. 1949 return 'bluetooth' 1950 except error.AutoservRunError: 1951 # test exited with a return code 1 meaning the directory did not 1952 # exist. 1953 return None 1954 1955 1956 def get_accels(self): 1957 """ 1958 Determine the type of accelerometers on this host. 1959 1960 @returns a string representing this host's accelerometer type. 1961 At present, it only returns "accel:cros-ec", for accelerometers 1962 attached to a Chrome OS EC, or none, if no accelerometers. 1963 """ 1964 # Check to make sure we have ectool 1965 rv = self.run('which ectool', ignore_status=True) 1966 if rv.exit_status: 1967 logging.info("No ectool cmd found, assuming no EC accelerometers") 1968 return None 1969 1970 # Check that the EC supports the motionsense command 1971 rv = self.run('ectool motionsense', ignore_status=True) 1972 if rv.exit_status: 1973 logging.info("EC does not support motionsense command " 1974 "assuming no EC accelerometers") 1975 return None 1976 1977 # Check that EC motion sensors are active 1978 active = self.run('ectool motionsense active').stdout.split('\n') 1979 if active[0] == "0": 1980 logging.info("Motion sense inactive, assuming no EC accelerometers") 1981 return None 1982 1983 logging.info("EC accelerometers found") 1984 return 'accel:cros-ec' 1985 1986 1987 def has_chameleon(self): 1988 """Determine if a Chameleon connected to this host. 1989 1990 @returns a list containing two strings ('chameleon' and 1991 'chameleon:' + label, e.g. 'chameleon:hdmi') if this host 1992 has a Chameleon or None if it has not. 1993 """ 1994 if self._chameleon_host: 1995 return ['chameleon', 'chameleon:' + self.chameleon.get_label()] 1996 else: 1997 return None 1998 1999 2000 def has_loopback_dongle(self): 2001 """Determine if an audio loopback dongle is plugged to this host. 2002 2003 @returns 'audio_loopback_dongle' when there is an audio loopback dongle 2004 plugged to this host. 2005 None when there is no audio loopback dongle 2006 plugged to this host. 2007 """ 2008 nodes_info = self.run(command=cras_utils.get_cras_nodes_cmd(), 2009 ignore_status=True).stdout 2010 if (cras_utils.node_type_is_plugged('HEADPHONE', nodes_info) and 2011 cras_utils.node_type_is_plugged('MIC', nodes_info)): 2012 return 'audio_loopback_dongle' 2013 else: 2014 return None 2015 2016 2017 def get_power_supply(self): 2018 """ 2019 Determine what type of power supply the host has 2020 2021 @returns a string representing this host's power supply. 2022 'power:battery' when the device has a battery intended for 2023 extended use 2024 'power:AC_primary' when the device has a battery not intended 2025 for extended use (for moving the machine, etc) 2026 'power:AC_only' when the device has no battery at all. 2027 """ 2028 psu = self.run(command='mosys psu type', ignore_status=True) 2029 if psu.exit_status: 2030 # The psu command for mosys is not included for all platforms. The 2031 # assumption is that the device will have a battery if the command 2032 # is not found. 2033 return 'power:battery' 2034 2035 psu_str = psu.stdout.strip() 2036 if psu_str == 'unknown': 2037 return None 2038 2039 return 'power:%s' % psu_str 2040 2041 2042 def get_storage(self): 2043 """ 2044 Determine the type of boot device for this host. 2045 2046 Determine if the internal device is SCSI or dw_mmc device. 2047 Then check that it is SSD or HDD or eMMC or something else. 2048 2049 @returns a string representing this host's internal device type. 2050 'storage:ssd' when internal device is solid state drive 2051 'storage:hdd' when internal device is hard disk drive 2052 'storage:mmc' when internal device is mmc drive 2053 None When internal device is something else or 2054 when we are unable to determine the type 2055 """ 2056 # The output should be /dev/mmcblk* for SD/eMMC or /dev/sd* for scsi 2057 rootdev_cmd = ' '.join(['. /usr/sbin/write_gpt.sh;', 2058 '. /usr/share/misc/chromeos-common.sh;', 2059 'load_base_vars;', 2060 'get_fixed_dst_drive']) 2061 rootdev = self.run(command=rootdev_cmd, ignore_status=True) 2062 if rootdev.exit_status: 2063 logging.info("Fail to run %s", rootdev_cmd) 2064 return None 2065 rootdev_str = rootdev.stdout.strip() 2066 2067 if not rootdev_str: 2068 return None 2069 2070 rootdev_base = os.path.basename(rootdev_str) 2071 2072 mmc_pattern = '/dev/mmcblk[0-9]' 2073 if re.match(mmc_pattern, rootdev_str): 2074 # Use type to determine if the internal device is eMMC or somthing 2075 # else. We can assume that MMC is always an internal device. 2076 type_cmd = 'cat /sys/block/%s/device/type' % rootdev_base 2077 type = self.run(command=type_cmd, ignore_status=True) 2078 if type.exit_status: 2079 logging.info("Fail to run %s", type_cmd) 2080 return None 2081 type_str = type.stdout.strip() 2082 2083 if type_str == 'MMC': 2084 return 'storage:mmc' 2085 2086 scsi_pattern = '/dev/sd[a-z]+' 2087 if re.match(scsi_pattern, rootdev.stdout): 2088 # Read symlink for /sys/block/sd* to determine if the internal 2089 # device is connected via ata or usb. 2090 link_cmd = 'readlink /sys/block/%s' % rootdev_base 2091 link = self.run(command=link_cmd, ignore_status=True) 2092 if link.exit_status: 2093 logging.info("Fail to run %s", link_cmd) 2094 return None 2095 link_str = link.stdout.strip() 2096 if 'usb' in link_str: 2097 return None 2098 2099 # Read rotation to determine if the internal device is ssd or hdd. 2100 rotate_cmd = str('cat /sys/block/%s/queue/rotational' 2101 % rootdev_base) 2102 rotate = self.run(command=rotate_cmd, ignore_status=True) 2103 if rotate.exit_status: 2104 logging.info("Fail to run %s", rotate_cmd) 2105 return None 2106 rotate_str = rotate.stdout.strip() 2107 2108 rotate_dict = {'0':'storage:ssd', '1':'storage:hdd'} 2109 return rotate_dict.get(rotate_str) 2110 2111 # All other internal device / error case will always fall here 2112 return None 2113 2114 2115 def get_servo(self): 2116 """Determine if the host has a servo attached. 2117 2118 If the host has a working servo attached, it should have a servo label. 2119 2120 @return: string 'servo' if the host has servo attached. Otherwise, 2121 returns None. 2122 """ 2123 return 'servo' if self._servo_host else None 2124 2125 2126 def get_video_labels(self): 2127 """Run /usr/local/bin/avtest_label_detect to get a list of video labels. 2128 2129 Sample output of avtest_label_detect: 2130 Detected label: hw_video_acc_vp8 2131 Detected label: webcam 2132 2133 @return: A list of labels detected by tool avtest_label_detect. 2134 """ 2135 try: 2136 result = self.run('/usr/local/bin/avtest_label_detect').stdout 2137 return re.findall('^Detected label: (\w+)$', result, re.M) 2138 except error.AutoservRunError: 2139 # The tool is not installed. 2140 return [] 2141 2142 2143 def is_video_glitch_detection_supported(self): 2144 """ Determine if a board under test is supported for video glitch 2145 detection tests. 2146 2147 @return: 'video_glitch_detection' if board is supported, None otherwise. 2148 """ 2149 board = self.get_board().replace(ds_constants.BOARD_PREFIX, '') 2150 2151 if board in video_test_constants.SUPPORTED_BOARDS: 2152 return 'video_glitch_detection' 2153 2154 return None 2155 2156 2157 def get_touch(self): 2158 """ 2159 Determine whether board under test has a touchpad or touchscreen. 2160 2161 @return: A list of some combination of 'touchscreen' and 'touchpad', 2162 depending on what is present on the device. 2163 2164 """ 2165 labels = [] 2166 looking_for = ['touchpad', 'touchscreen'] 2167 player = input_playback.InputPlayback() 2168 input_events = self.run('ls /dev/input/event*').stdout.strip().split() 2169 filename = '/tmp/touch_labels' 2170 for event in input_events: 2171 self.run('evtest %s > %s' % (event, filename), timeout=1, 2172 ignore_timeout=True) 2173 properties = self.run('cat %s' % filename).stdout 2174 input_type = player._determine_input_type(properties) 2175 if input_type in looking_for: 2176 labels.append(input_type) 2177 looking_for.remove(input_type) 2178 if len(looking_for) == 0: 2179 break 2180 self.run('rm %s' % filename) 2181 2182 return labels 2183 2184 2185 def has_internal_display(self): 2186 """Determine if the device under test is equipped with an internal 2187 display. 2188 2189 @return: 'internal_display' if one is present; None otherwise. 2190 """ 2191 from autotest_lib.client.cros.graphics import graphics_utils 2192 from autotest_lib.client.common_lib import utils as common_utils 2193 2194 def __system_output(cmd): 2195 return self.run(cmd).stdout 2196 2197 def __read_file(remote_path): 2198 return self.run('cat %s' % remote_path).stdout 2199 2200 # Hijack the necessary client functions so that we can take advantage 2201 # of the client lib here. 2202 # FIXME: find a less hacky way than this 2203 original_system_output = utils.system_output 2204 original_read_file = common_utils.read_file 2205 utils.system_output = __system_output 2206 common_utils.read_file = __read_file 2207 try: 2208 return ('internal_display' if graphics_utils.has_internal_display() 2209 else None) 2210 finally: 2211 utils.system_output = original_system_output 2212 common_utils.read_file = original_read_file 2213 2214 2215 def has_lucid_sleep_support(self): 2216 """Determine if the device under test has support for lucid sleep. 2217 2218 @return 'lucidsleep' if this board supports lucid sleep; None otherwise 2219 """ 2220 board = self.get_board().replace(ds_constants.BOARD_PREFIX, '') 2221 return 'lucidsleep' if board in LUCID_SLEEP_BOARDS else None 2222 2223 2224 def is_boot_from_usb(self): 2225 """Check if DUT is boot from USB. 2226 2227 @return: True if DUT is boot from usb. 2228 """ 2229 device = self.run('rootdev -s -d').stdout.strip() 2230 removable = int(self.run('cat /sys/block/%s/removable' % 2231 os.path.basename(device)).stdout.strip()) 2232 return removable == 1 2233 2234 2235 def read_from_meminfo(self, key): 2236 """Return the memory info from /proc/meminfo 2237 2238 @param key: meminfo requested 2239 2240 @return the memory value as a string 2241 2242 """ 2243 meminfo = self.run('grep %s /proc/meminfo' % key).stdout.strip() 2244 logging.debug('%s', meminfo) 2245 return int(re.search(r'\d+', meminfo).group(0)) 2246 2247 2248 def get_cpu_arch(self): 2249 """Returns CPU arch of the device. 2250 2251 @return CPU architecture of the DUT. 2252 """ 2253 # Add CPUs by following logic in client/bin/base_utils.py. 2254 if self.run("grep '^flags.*:.* lm .*' /proc/cpuinfo", 2255 ignore_status=True).stdout: 2256 return 'x86_64' 2257 if self.run("grep -Ei 'ARM|CPU implementer' /proc/cpuinfo", 2258 ignore_status=True).stdout: 2259 return 'arm' 2260 return 'i386' 2261 2262 2263 def get_board_type(self): 2264 """ 2265 Get the DUT's device type from /etc/lsb-release. 2266 DEVICETYPE can be one of CHROMEBOX, CHROMEBASE, CHROMEBOOK or more. 2267 2268 @return value of DEVICETYPE param from lsb-release. 2269 """ 2270 device_type = self.run('grep DEVICETYPE /etc/lsb-release', 2271 ignore_status=True).stdout 2272 if device_type: 2273 return device_type.split('=')[-1].strip() 2274 return '' 2275 2276 2277 def get_os_type(self): 2278 return 'cros' 2279 2280 2281 def enable_adb_testing(self): 2282 """Mark this host as an adb tester.""" 2283 self.run('touch %s' % constants.ANDROID_TESTER_FILEFLAG) 2284 2285 2286 def get_labels(self): 2287 """Return the detected labels on the host.""" 2288 return self.labels.get_labels(self) 2289 2290 2291 def update_labels(self): 2292 """Update the labels on the host.""" 2293 self.labels.update_labels(self) 2294