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 dev_server 19from autotest_lib.client.common_lib.cros import retry 20from autotest_lib.client.cros import constants as client_constants 21from autotest_lib.client.cros import cros_ui 22from autotest_lib.server import afe_utils 23from autotest_lib.server import utils as server_utils 24from autotest_lib.server.cros import provision 25from autotest_lib.server.cros.dynamic_suite import constants as ds_constants 26from autotest_lib.server.cros.dynamic_suite import tools, frontend_wrappers 27from autotest_lib.server.cros.servo import plankton 28from autotest_lib.server.hosts import abstract_ssh 29from autotest_lib.server.hosts import base_label 30from autotest_lib.server.hosts import chameleon_host 31from autotest_lib.server.hosts import cros_label 32from autotest_lib.server.hosts import cros_repair 33from autotest_lib.server.hosts import plankton_host 34from autotest_lib.server.hosts import servo_host 35from autotest_lib.site_utils.rpm_control_system import rpm_client 36 37# In case cros_host is being ran via SSP on an older Moblab version with an 38# older chromite version. 39try: 40 from chromite.lib import metrics 41except ImportError: 42 metrics = utils.metrics_mock 43 44 45CONFIG = global_config.global_config 46 47 48class FactoryImageCheckerException(error.AutoservError): 49 """Exception raised when an image is a factory image.""" 50 pass 51 52 53class CrosHost(abstract_ssh.AbstractSSHHost): 54 """Chromium OS specific subclass of Host.""" 55 56 VERSION_PREFIX = provision.CROS_VERSION_PREFIX 57 58 _AFE = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10) 59 60 # Timeout values (in seconds) associated with various Chrome OS 61 # state changes. 62 # 63 # In general, a good rule of thumb is that the timeout can be up 64 # to twice the typical measured value on the slowest platform. 65 # The times here have not necessarily been empirically tested to 66 # meet this criterion. 67 # 68 # SLEEP_TIMEOUT: Time to allow for suspend to memory. 69 # RESUME_TIMEOUT: Time to allow for resume after suspend, plus 70 # time to restart the netwowrk. 71 # SHUTDOWN_TIMEOUT: Time to allow for shut down. 72 # BOOT_TIMEOUT: Time to allow for boot from power off. Among 73 # other things, this must account for the 30 second dev-mode 74 # screen delay, time to start the network on the DUT, and the 75 # ssh timeout of 120 seconds. 76 # USB_BOOT_TIMEOUT: Time to allow for boot from a USB device, 77 # including the 30 second dev-mode delay and time to start the 78 # network. 79 # INSTALL_TIMEOUT: Time to allow for chromeos-install. 80 # POWERWASH_BOOT_TIMEOUT: Time to allow for a reboot that 81 # includes powerwash. 82 83 SLEEP_TIMEOUT = 2 84 RESUME_TIMEOUT = 10 85 SHUTDOWN_TIMEOUT = 10 86 BOOT_TIMEOUT = 150 87 USB_BOOT_TIMEOUT = 300 88 INSTALL_TIMEOUT = 480 89 POWERWASH_BOOT_TIMEOUT = 60 90 91 # Minimum OS version that supports server side packaging. Older builds may 92 # not have server side package built or with Autotest code change to support 93 # server-side packaging. 94 MIN_VERSION_SUPPORT_SSP = CONFIG.get_config_value( 95 'AUTOSERV', 'min_version_support_ssp', type=int) 96 97 # REBOOT_TIMEOUT: How long to wait for a reboot. 98 # 99 # We have a long timeout to ensure we don't flakily fail due to other 100 # issues. Shorter timeouts are vetted in platform_RebootAfterUpdate. 101 # TODO(sbasi - crbug.com/276094) Restore to 5 mins once the 'host did not 102 # return from reboot' bug is solved. 103 REBOOT_TIMEOUT = 480 104 105 # _USB_POWER_TIMEOUT: Time to allow for USB to power toggle ON and OFF. 106 # _POWER_CYCLE_TIMEOUT: Time to allow for manual power cycle. 107 _USB_POWER_TIMEOUT = 5 108 _POWER_CYCLE_TIMEOUT = 10 109 110 _RPM_HOSTNAME_REGEX = ('chromeos(\d+)(-row(\d+))?-rack(\d+[a-z]*)' 111 '-host(\d+)') 112 113 # Constants used in ping_wait_up() and ping_wait_down(). 114 # 115 # _PING_WAIT_COUNT is the approximate number of polling 116 # cycles to use when waiting for a host state change. 117 # 118 # _PING_STATUS_DOWN and _PING_STATUS_UP are names used 119 # for arguments to the internal _ping_wait_for_status() 120 # method. 121 _PING_WAIT_COUNT = 40 122 _PING_STATUS_DOWN = False 123 _PING_STATUS_UP = True 124 125 # Allowed values for the power_method argument. 126 127 # POWER_CONTROL_RPM: Passed as default arg for power_off/on/cycle() methods. 128 # POWER_CONTROL_SERVO: Used in set_power() and power_cycle() methods. 129 # POWER_CONTROL_MANUAL: Used in set_power() and power_cycle() methods. 130 POWER_CONTROL_RPM = 'RPM' 131 POWER_CONTROL_SERVO = 'servoj10' 132 POWER_CONTROL_MANUAL = 'manual' 133 134 POWER_CONTROL_VALID_ARGS = (POWER_CONTROL_RPM, 135 POWER_CONTROL_SERVO, 136 POWER_CONTROL_MANUAL) 137 138 _RPM_OUTLET_CHANGED = 'outlet_changed' 139 140 # URL pattern to download firmware image. 141 _FW_IMAGE_URL_PATTERN = CONFIG.get_config_value( 142 'CROS', 'firmware_url_pattern', type=str) 143 144 145 @staticmethod 146 def check_host(host, timeout=10): 147 """ 148 Check if the given host is a chrome-os host. 149 150 @param host: An ssh host representing a device. 151 @param timeout: The timeout for the run command. 152 153 @return: True if the host device is chromeos. 154 155 """ 156 try: 157 result = host.run( 158 'grep -q CHROMEOS /etc/lsb-release && ' 159 '! test -f /mnt/stateful_partition/.android_tester && ' 160 '! grep -q moblab /etc/lsb-release', 161 ignore_status=True, timeout=timeout) 162 if result.exit_status == 0: 163 lsb_release_content = host.run( 164 'grep CHROMEOS_RELEASE_BOARD /etc/lsb-release', 165 timeout=timeout).stdout 166 return not ( 167 lsbrelease_utils.is_jetstream( 168 lsb_release_content=lsb_release_content) or 169 lsbrelease_utils.is_gce_board( 170 lsb_release_content=lsb_release_content)) 171 172 except (error.AutoservRunError, error.AutoservSSHTimeout): 173 return False 174 175 return False 176 177 178 @staticmethod 179 def get_chameleon_arguments(args_dict): 180 """Extract chameleon options from `args_dict` and return the result. 181 182 Recommended usage: 183 ~~~~~~~~ 184 args_dict = utils.args_to_dict(args) 185 chameleon_args = hosts.CrosHost.get_chameleon_arguments(args_dict) 186 host = hosts.create_host(machine, chameleon_args=chameleon_args) 187 ~~~~~~~~ 188 189 @param args_dict Dictionary from which to extract the chameleon 190 arguments. 191 """ 192 return {key: args_dict[key] 193 for key in ('chameleon_host', 'chameleon_port') 194 if key in args_dict} 195 196 197 @staticmethod 198 def get_plankton_arguments(args_dict): 199 """Extract chameleon options from `args_dict` and return the result. 200 201 Recommended usage: 202 ~~~~~~~~ 203 args_dict = utils.args_to_dict(args) 204 plankton_args = hosts.CrosHost.get_plankton_arguments(args_dict) 205 host = hosts.create_host(machine, plankton_args=plankton_args) 206 ~~~~~~~~ 207 208 @param args_dict Dictionary from which to extract the plankton 209 arguments. 210 """ 211 return {key: args_dict[key] 212 for key in ('plankton_host', 'plankton_port') 213 if key in args_dict} 214 215 216 @staticmethod 217 def get_servo_arguments(args_dict): 218 """Extract servo options from `args_dict` and return the result. 219 220 Recommended usage: 221 ~~~~~~~~ 222 args_dict = utils.args_to_dict(args) 223 servo_args = hosts.CrosHost.get_servo_arguments(args_dict) 224 host = hosts.create_host(machine, servo_args=servo_args) 225 ~~~~~~~~ 226 227 @param args_dict Dictionary from which to extract the servo 228 arguments. 229 """ 230 servo_attrs = (servo_host.SERVO_HOST_ATTR, 231 servo_host.SERVO_PORT_ATTR, 232 servo_host.SERVO_BOARD_ATTR, 233 servo_host.SERVO_MODEL_ATTR) 234 servo_args = {key: args_dict[key] 235 for key in servo_attrs 236 if key in args_dict} 237 return ( 238 None 239 if servo_host.SERVO_HOST_ATTR in servo_args 240 and not servo_args[servo_host.SERVO_HOST_ATTR] 241 else servo_args) 242 243 244 def _initialize(self, hostname, chameleon_args=None, servo_args=None, 245 plankton_args=None, try_lab_servo=False, 246 try_servo_repair=False, 247 ssh_verbosity_flag='', ssh_options='', 248 *args, **dargs): 249 """Initialize superclasses, |self.chameleon|, and |self.servo|. 250 251 This method will attempt to create the test-assistant object 252 (chameleon/servo) when it is needed by the test. Check 253 the docstring of chameleon_host.create_chameleon_host and 254 servo_host.create_servo_host for how this is determined. 255 256 @param hostname: Hostname of the dut. 257 @param chameleon_args: A dictionary that contains args for creating 258 a ChameleonHost. See chameleon_host for details. 259 @param servo_args: A dictionary that contains args for creating 260 a ServoHost object. See servo_host for details. 261 @param try_lab_servo: When true, indicates that an attempt should 262 be made to create a ServoHost for a DUT in 263 the test lab, even if not required by 264 `servo_args`. See servo_host for details. 265 @param try_servo_repair: If a servo host is created, check it 266 with `repair()` rather than `verify()`. 267 See servo_host for details. 268 @param ssh_verbosity_flag: String, to pass to the ssh command to control 269 verbosity. 270 @param ssh_options: String, other ssh options to pass to the ssh 271 command. 272 """ 273 super(CrosHost, self)._initialize(hostname=hostname, 274 *args, **dargs) 275 self._repair_strategy = cros_repair.create_cros_repair_strategy() 276 self.labels = base_label.LabelRetriever(cros_label.CROS_LABELS) 277 # self.env is a dictionary of environment variable settings 278 # to be exported for commands run on the host. 279 # LIBC_FATAL_STDERR_ can be useful for diagnosing certain 280 # errors that might happen. 281 self.env['LIBC_FATAL_STDERR_'] = '1' 282 self._ssh_verbosity_flag = ssh_verbosity_flag 283 self._ssh_options = ssh_options 284 self.set_servo_host( 285 servo_host.create_servo_host( 286 dut=self, servo_args=servo_args, 287 try_lab_servo=try_lab_servo, 288 try_servo_repair=try_servo_repair)) 289 290 # TODO(waihong): Do the simplication on Chameleon too. 291 self._chameleon_host = chameleon_host.create_chameleon_host( 292 dut=self.hostname, chameleon_args=chameleon_args) 293 # Add plankton host if plankton args were added on command line 294 self._plankton_host = plankton_host.create_plankton_host(plankton_args) 295 296 if self._chameleon_host: 297 self.chameleon = self._chameleon_host.create_chameleon_board() 298 else: 299 self.chameleon = None 300 301 if self._plankton_host: 302 self.plankton_servo = self._plankton_host.get_servo() 303 logging.info('plankton_servo: %r', self.plankton_servo) 304 # Create the plankton object used to access the ec uart 305 self.plankton = plankton.Plankton(self.plankton_servo, 306 self._plankton_host.get_servod_server_proxy()) 307 else: 308 self.plankton = None 309 310 311 def get_cros_repair_image_name(self): 312 info = self.host_info_store.get() 313 if not info.board: 314 raise error.AutoservError('Cannot obtain repair image name. ' 315 'No board label value found') 316 return afe_utils.get_stable_cros_image_name(info.board) 317 318 319 def host_version_prefix(self, image): 320 """Return version label prefix. 321 322 In case the CrOS provisioning version is something other than the 323 standard CrOS version e.g. CrOS TH version, this function will 324 find the prefix from provision.py. 325 326 @param image: The image name to find its version prefix. 327 @returns: A prefix string for the image type. 328 """ 329 return provision.get_version_label_prefix(image) 330 331 332 def verify_job_repo_url(self, tag=''): 333 """ 334 Make sure job_repo_url of this host is valid. 335 336 Eg: The job_repo_url "http://lmn.cd.ab.xyx:8080/static/\ 337 lumpy-release/R29-4279.0.0/autotest/packages" claims to have the 338 autotest package for lumpy-release/R29-4279.0.0. If this isn't the case, 339 download and extract it. If the devserver embedded in the url is 340 unresponsive, update the job_repo_url of the host after staging it on 341 another devserver. 342 343 @param job_repo_url: A url pointing to the devserver where the autotest 344 package for this build should be staged. 345 @param tag: The tag from the server job, in the format 346 <job_id>-<user>/<hostname>, or <hostless> for a server job. 347 348 @raises DevServerException: If we could not resolve a devserver. 349 @raises AutoservError: If we're unable to save the new job_repo_url as 350 a result of choosing a new devserver because the old one failed to 351 respond to a health check. 352 @raises urllib2.URLError: If the devserver embedded in job_repo_url 353 doesn't respond within the timeout. 354 """ 355 info = self.host_info_store.get() 356 job_repo_url = info.attributes.get(ds_constants.JOB_REPO_URL, '') 357 if not job_repo_url: 358 logging.warning('No job repo url set on host %s', self.hostname) 359 return 360 361 logging.info('Verifying job repo url %s', job_repo_url) 362 devserver_url, image_name = tools.get_devserver_build_from_package_url( 363 job_repo_url) 364 365 ds = dev_server.ImageServer(devserver_url) 366 367 logging.info('Staging autotest artifacts for %s on devserver %s', 368 image_name, ds.url()) 369 370 start_time = time.time() 371 ds.stage_artifacts(image_name, ['autotest_packages']) 372 stage_time = time.time() - start_time 373 374 # Record how much of the verification time comes from a devserver 375 # restage. If we're doing things right we should not see multiple 376 # devservers for a given board/build/branch path. 377 try: 378 board, build_type, branch = server_utils.ParseBuildName( 379 image_name)[:3] 380 except server_utils.ParseBuildNameException: 381 pass 382 else: 383 devserver = devserver_url[ 384 devserver_url.find('/') + 2:devserver_url.rfind(':')] 385 stats_key = { 386 'board': board, 387 'build_type': build_type, 388 'branch': branch, 389 'devserver': devserver.replace('.', '_'), 390 } 391 392 monarch_fields = { 393 'board': board, 394 'build_type': build_type, 395 # TODO(akeshet): To be consistent with most other metrics, 396 # consider changing the following field to be named 397 # 'milestone'. 398 'branch': branch, 399 'dev_server': devserver, 400 } 401 metrics.Counter( 402 'chromeos/autotest/provision/verify_url' 403 ).increment(fields=monarch_fields) 404 metrics.SecondsDistribution( 405 'chromeos/autotest/provision/verify_url_duration' 406 ).add(stage_time, fields=monarch_fields) 407 408 409 def stage_server_side_package(self, image=None): 410 """Stage autotest server-side package on devserver. 411 412 @param image: Full path of an OS image to install or a build name. 413 414 @return: A url to the autotest server-side package. 415 416 @raise: error.AutoservError if fail to locate the build to test with, or 417 fail to stage server-side package. 418 """ 419 # If enable_drone_in_restricted_subnet is False, do not set hostname 420 # in devserver.resolve call, so a devserver in non-restricted subnet 421 # is picked to stage autotest server package for drone to download. 422 hostname = self.hostname 423 if not server_utils.ENABLE_DRONE_IN_RESTRICTED_SUBNET: 424 hostname = None 425 if image: 426 image_name = tools.get_build_from_image(image) 427 if not image_name: 428 raise error.AutoservError( 429 'Failed to parse build name from %s' % image) 430 ds = dev_server.ImageServer.resolve(image_name, hostname) 431 else: 432 info = self.host_info_store.get() 433 job_repo_url = info.attributes.get(ds_constants.JOB_REPO_URL, '') 434 if job_repo_url: 435 devserver_url, image_name = ( 436 tools.get_devserver_build_from_package_url(job_repo_url)) 437 # If enable_drone_in_restricted_subnet is True, use the 438 # existing devserver. Otherwise, resolve a new one in 439 # non-restricted subnet. 440 if server_utils.ENABLE_DRONE_IN_RESTRICTED_SUBNET: 441 ds = dev_server.ImageServer(devserver_url) 442 else: 443 ds = dev_server.ImageServer.resolve(image_name) 444 elif info.build is not None: 445 ds = dev_server.ImageServer.resolve(info.build, hostname) 446 image_name = info.build 447 else: 448 raise error.AutoservError( 449 'Failed to stage server-side package. The host has ' 450 'no job_report_url attribute or version label.') 451 452 # Get the OS version of the build, for any build older than 453 # MIN_VERSION_SUPPORT_SSP, server side packaging is not supported. 454 match = re.match('.*/R\d+-(\d+)\.', image_name) 455 if match and int(match.group(1)) < self.MIN_VERSION_SUPPORT_SSP: 456 raise error.AutoservError( 457 'Build %s is older than %s. Server side packaging is ' 458 'disabled.' % (image_name, self.MIN_VERSION_SUPPORT_SSP)) 459 460 ds.stage_artifacts(image_name, ['autotest_server_package']) 461 return '%s/static/%s/%s' % (ds.url(), image_name, 462 'autotest_server_package.tar.bz2') 463 464 465 def stage_image_for_servo(self, image_name=None, artifact='test_image'): 466 """Stage a build on a devserver and return the update_url. 467 468 @param image_name: a name like lumpy-release/R27-3837.0.0 469 @param artifact: a string like 'test_image'. Requests 470 appropriate image to be staged. 471 @returns an update URL like: 472 http://172.22.50.205:8082/update/lumpy-release/R27-3837.0.0 473 """ 474 if not image_name: 475 image_name = self.get_cros_repair_image_name() 476 logging.info('Staging build for servo install: %s', image_name) 477 devserver = dev_server.ImageServer.resolve(image_name, self.hostname) 478 devserver.stage_artifacts(image_name, [artifact]) 479 if artifact == 'test_image': 480 return devserver.get_test_image_url(image_name) 481 elif artifact == 'recovery_image': 482 return devserver.get_recovery_image_url(image_name) 483 else: 484 raise error.AutoservError("Bad artifact!") 485 486 487 def stage_factory_image_for_servo(self, image_name): 488 """Stage a build on a devserver and return the update_url. 489 490 @param image_name: a name like <baord>/4262.204.0 491 492 @return: An update URL, eg: 493 http://<devserver>/static/canary-channel/\ 494 <board>/4262.204.0/factory_test/chromiumos_factory_image.bin 495 496 @raises: ValueError if the factory artifact name is missing from 497 the config. 498 499 """ 500 if not image_name: 501 logging.error('Need an image_name to stage a factory image.') 502 return 503 504 factory_artifact = CONFIG.get_config_value( 505 'CROS', 'factory_artifact', type=str, default='') 506 if not factory_artifact: 507 raise ValueError('Cannot retrieve the factory artifact name from ' 508 'autotest config, and hence cannot stage factory ' 509 'artifacts.') 510 511 logging.info('Staging build for servo install: %s', image_name) 512 devserver = dev_server.ImageServer.resolve(image_name, self.hostname) 513 devserver.stage_artifacts( 514 image_name, 515 [factory_artifact], 516 archive_url=None) 517 518 return tools.factory_image_url_pattern() % (devserver.url(), image_name) 519 520 521 def prepare_for_update(self): 522 """Prepares the DUT for an update. 523 524 Subclasses may override this to perform any special actions 525 required before updating. 526 """ 527 pass 528 529 530 def _clear_fw_version_labels(self, rw_only): 531 """Clear firmware version labels from the machine. 532 533 @param rw_only: True to only clear fwrw_version; otherewise, clear 534 both fwro_version and fwrw_version. 535 """ 536 labels = self._AFE.get_labels( 537 name__startswith=provision.FW_RW_VERSION_PREFIX, 538 host__hostname=self.hostname) 539 if not rw_only: 540 labels = labels + self._AFE.get_labels( 541 name__startswith=provision.FW_RO_VERSION_PREFIX, 542 host__hostname=self.hostname) 543 for label in labels: 544 label.remove_hosts(hosts=[self.hostname]) 545 546 547 def _add_fw_version_label(self, build, rw_only): 548 """Add firmware version label to the machine. 549 550 @param build: Build of firmware. 551 @param rw_only: True to only add fwrw_version; otherwise, add both 552 fwro_version and fwrw_version. 553 554 """ 555 fw_label = provision.fwrw_version_to_label(build) 556 self._AFE.run('label_add_hosts', id=fw_label, hosts=[self.hostname]) 557 if not rw_only: 558 fw_label = provision.fwro_version_to_label(build) 559 self._AFE.run('label_add_hosts', id=fw_label, hosts=[self.hostname]) 560 561 562 def firmware_install(self, build=None, rw_only=False): 563 """Install firmware to the DUT. 564 565 Use stateful update if the DUT is already running the same build. 566 Stateful update does not update kernel and tends to run much faster 567 than a full reimage. If the DUT is running a different build, or it 568 failed to do a stateful update, full update, including kernel update, 569 will be applied to the DUT. 570 571 Once a host enters firmware_install its fw[ro|rw]_version label will 572 be removed. After the firmware is updated successfully, a new 573 fw[ro|rw]_version label will be added to the host. 574 575 @param build: The build version to which we want to provision the 576 firmware of the machine, 577 e.g. 'link-firmware/R22-2695.1.144'. 578 @param rw_only: True to only install firmware to its RW portions. Keep 579 the RO portions unchanged. 580 581 TODO(dshi): After bug 381718 is fixed, update here with corresponding 582 exceptions that could be raised. 583 584 """ 585 if not self.servo: 586 raise error.TestError('Host %s does not have servo.' % 587 self.hostname) 588 589 # Get the DUT board name from AFE. 590 info = self.host_info_store.get() 591 board = info.board 592 593 if board is None or board == '': 594 board = self.servo.get_board() 595 596 # If build is not set, try to install firmware from stable CrOS. 597 if not build: 598 build = afe_utils.get_stable_faft_version(board) 599 if not build: 600 raise error.TestError( 601 'Failed to find stable firmware build for %s.', 602 self.hostname) 603 logging.info('Will install firmware from build %s.', build) 604 605 ds = dev_server.ImageServer.resolve(build, self.hostname) 606 ds.stage_artifacts(build, ['firmware']) 607 608 tmpd = autotemp.tempdir(unique_id='fwimage') 609 try: 610 fwurl = self._FW_IMAGE_URL_PATTERN % (ds.url(), build) 611 local_tarball = os.path.join(tmpd.name, os.path.basename(fwurl)) 612 ds.download_file(fwurl, local_tarball) 613 614 self._clear_fw_version_labels(rw_only) 615 self.servo.program_firmware(board, local_tarball, rw_only) 616 if utils.host_is_in_lab_zone(self.hostname): 617 self._add_fw_version_label(build, rw_only) 618 finally: 619 tmpd.clean() 620 621 622 def servo_install(self, image_url=None, usb_boot_timeout=USB_BOOT_TIMEOUT, 623 install_timeout=INSTALL_TIMEOUT): 624 """ 625 Re-install the OS on the DUT by: 626 1) installing a test image on a USB storage device attached to the Servo 627 board, 628 2) booting that image in recovery mode, and then 629 3) installing the image with chromeos-install. 630 631 @param image_url: If specified use as the url to install on the DUT. 632 otherwise boot the currently staged image on the USB stick. 633 @param usb_boot_timeout: The usb_boot_timeout to use during reimage. 634 Factory images need a longer usb_boot_timeout than regular 635 cros images. 636 @param install_timeout: The timeout to use when installing the chromeos 637 image. Factory images need a longer install_timeout. 638 639 @raises AutoservError if the image fails to boot. 640 641 """ 642 logging.info('Downloading image to USB, then booting from it. Usb boot ' 643 'timeout = %s', usb_boot_timeout) 644 with metrics.SecondsTimer( 645 'chromeos/autotest/provision/servo_install/boot_duration'): 646 self.servo.install_recovery_image(image_url) 647 if not self.wait_up(timeout=usb_boot_timeout): 648 raise hosts.AutoservRepairError( 649 'DUT failed to boot from USB after %d seconds' % 650 usb_boot_timeout, 'failed_to_reboot') 651 652 # The new chromeos-tpm-recovery has been merged since R44-7073.0.0. 653 # In old CrOS images, this command fails. Skip the error. 654 logging.info('Resetting the TPM status') 655 try: 656 self.run('chromeos-tpm-recovery') 657 except error.AutoservRunError: 658 logging.warn('chromeos-tpm-recovery is too old.') 659 660 661 with metrics.SecondsTimer( 662 'chromeos/autotest/provision/servo_install/install_duration'): 663 logging.info('Installing image through chromeos-install.') 664 self.run('chromeos-install --yes', timeout=install_timeout) 665 self.halt() 666 667 logging.info('Power cycling DUT through servo.') 668 self.servo.get_power_state_controller().power_off() 669 self.servo.switch_usbkey('off') 670 # N.B. The Servo API requires that we use power_on() here 671 # for two reasons: 672 # 1) After turning on a DUT in recovery mode, you must turn 673 # it off and then on with power_on() once more to 674 # disable recovery mode (this is a Parrot specific 675 # requirement). 676 # 2) After power_off(), the only way to turn on is with 677 # power_on() (this is a Storm specific requirement). 678 self.servo.get_power_state_controller().power_on() 679 680 logging.info('Waiting for DUT to come back up.') 681 if not self.wait_up(timeout=self.BOOT_TIMEOUT): 682 raise error.AutoservError('DUT failed to reboot installed ' 683 'test image after %d seconds' % 684 self.BOOT_TIMEOUT) 685 686 687 def set_servo_host(self, host): 688 """Set our servo host member, and associated servo. 689 690 @param host Our new `ServoHost`. 691 """ 692 self._servo_host = host 693 if self._servo_host is not None: 694 self.servo = self._servo_host.get_servo() 695 else: 696 self.servo = None 697 698 699 def repair_servo(self): 700 """ 701 Confirm that servo is initialized and verified. 702 703 If the servo object is missing, attempt to repair the servo 704 host. Repair failures are passed back to the caller. 705 706 @raise AutoservError: If there is no servo host for this CrOS 707 host. 708 """ 709 if self.servo: 710 return 711 if not self._servo_host: 712 raise error.AutoservError('No servo host for %s.' % 713 self.hostname) 714 self._servo_host.repair() 715 self.servo = self._servo_host.get_servo() 716 717 718 def repair(self): 719 """Attempt to get the DUT to pass `self.verify()`. 720 721 This overrides the base class function for repair; it does 722 not call back to the parent class, but instead relies on 723 `self._repair_strategy` to coordinate the verification and 724 repair steps needed to get the DUT working. 725 """ 726 message = 'Beginning repair for host %s board %s model %s' 727 info = self.host_info_store.get() 728 message %= (self.hostname, info.board, info.model) 729 self.record('INFO', None, None, message) 730 self._repair_strategy.repair(self) 731 732 733 def close(self): 734 """Close connection.""" 735 super(CrosHost, self).close() 736 if self._chameleon_host: 737 self._chameleon_host.close() 738 739 if self._servo_host: 740 self._servo_host.close() 741 742 743 def get_power_supply_info(self): 744 """Get the output of power_supply_info. 745 746 power_supply_info outputs the info of each power supply, e.g., 747 Device: Line Power 748 online: no 749 type: Mains 750 voltage (V): 0 751 current (A): 0 752 Device: Battery 753 state: Discharging 754 percentage: 95.9276 755 technology: Li-ion 756 757 Above output shows two devices, Line Power and Battery, with details of 758 each device listed. This function parses the output into a dictionary, 759 with key being the device name, and value being a dictionary of details 760 of the device info. 761 762 @return: The dictionary of power_supply_info, e.g., 763 {'Line Power': {'online': 'yes', 'type': 'main'}, 764 'Battery': {'vendor': 'xyz', 'percentage': '100'}} 765 @raise error.AutoservRunError if power_supply_info tool is not found in 766 the DUT. Caller should handle this error to avoid false failure 767 on verification. 768 """ 769 result = self.run('power_supply_info').stdout.strip() 770 info = {} 771 device_name = None 772 device_info = {} 773 for line in result.split('\n'): 774 pair = [v.strip() for v in line.split(':')] 775 if len(pair) != 2: 776 continue 777 if pair[0] == 'Device': 778 if device_name: 779 info[device_name] = device_info 780 device_name = pair[1] 781 device_info = {} 782 else: 783 device_info[pair[0]] = pair[1] 784 if device_name and not device_name in info: 785 info[device_name] = device_info 786 return info 787 788 789 def get_battery_percentage(self): 790 """Get the battery percentage. 791 792 @return: The percentage of battery level, value range from 0-100. Return 793 None if the battery info cannot be retrieved. 794 """ 795 try: 796 info = self.get_power_supply_info() 797 logging.info(info) 798 return float(info['Battery']['percentage']) 799 except (KeyError, ValueError, error.AutoservRunError): 800 return None 801 802 803 def is_ac_connected(self): 804 """Check if the dut has power adapter connected and charging. 805 806 @return: True if power adapter is connected and charging. 807 """ 808 try: 809 info = self.get_power_supply_info() 810 return info['Line Power']['online'] == 'yes' 811 except (KeyError, error.AutoservRunError): 812 return None 813 814 815 def _cleanup_poweron(self): 816 """Special cleanup method to make sure hosts always get power back.""" 817 info = self.host_info_store.get() 818 if self._RPM_OUTLET_CHANGED not in info.attributes: 819 return 820 logging.debug('This host has recently interacted with the RPM' 821 ' Infrastructure. Ensuring power is on.') 822 try: 823 self.power_on() 824 self._remove_rpm_changed_tag() 825 except rpm_client.RemotePowerException: 826 logging.error('Failed to turn Power On for this host after ' 827 'cleanup through the RPM Infrastructure.') 828 829 battery_percentage = self.get_battery_percentage() 830 if battery_percentage and battery_percentage < 50: 831 raise 832 elif self.is_ac_connected(): 833 logging.info('The device has power adapter connected and ' 834 'charging. No need to try to turn RPM on ' 835 'again.') 836 self._remove_rpm_changed_tag() 837 logging.info('Battery level is now at %s%%. The device may ' 838 'still have enough power to run test, so no ' 839 'exception will be raised.', battery_percentage) 840 841 842 def _remove_rpm_changed_tag(self): 843 info = self.host_info_store.get() 844 del info.attributes[self._RPM_OUTLET_CHANGED] 845 self.host_info_store.commit(info) 846 847 848 def _add_rpm_changed_tag(self): 849 info = self.host_info_store.get() 850 info.attributes[self._RPM_OUTLET_CHANGED] = 'true' 851 self.host_info_store.commit(info) 852 853 854 855 def _is_factory_image(self): 856 """Checks if the image on the DUT is a factory image. 857 858 @return: True if the image on the DUT is a factory image. 859 False otherwise. 860 """ 861 result = self.run('[ -f /root/.factory_test ]', ignore_status=True) 862 return result.exit_status == 0 863 864 865 def _restart_ui(self): 866 """Restart the Chrome UI. 867 868 @raises: FactoryImageCheckerException for factory images, since 869 we cannot attempt to restart ui on them. 870 error.AutoservRunError for any other type of error that 871 occurs while restarting ui. 872 """ 873 if self._is_factory_image(): 874 raise FactoryImageCheckerException('Cannot restart ui on factory ' 875 'images') 876 877 # TODO(jrbarnette): The command to stop/start the ui job 878 # should live inside cros_ui, too. However that would seem 879 # to imply interface changes to the existing start()/restart() 880 # functions, which is a bridge too far (for now). 881 prompt = cros_ui.get_chrome_session_ident(self) 882 self.run('stop ui; start ui') 883 cros_ui.wait_for_chrome_ready(prompt, self) 884 885 886 def _start_powerd_if_needed(self): 887 """Start powerd if it isn't already running.""" 888 self.run('start powerd', ignore_status=True) 889 890 891 def _get_lsb_release_content(self): 892 """Return the content of lsb-release file of host.""" 893 return self.run( 894 'cat "%s"' % client_constants.LSB_RELEASE).stdout.strip() 895 896 897 def get_release_version(self): 898 """Get the value of attribute CHROMEOS_RELEASE_VERSION from lsb-release. 899 900 @returns The version string in lsb-release, under attribute 901 CHROMEOS_RELEASE_VERSION. 902 """ 903 return lsbrelease_utils.get_chromeos_release_version( 904 lsb_release_content=self._get_lsb_release_content()) 905 906 907 def get_release_builder_path(self): 908 """Get the value of CHROMEOS_RELEASE_BUILDER_PATH from lsb-release. 909 910 @returns The version string in lsb-release, under attribute 911 CHROMEOS_RELEASE_BUILDER_PATH. 912 """ 913 return lsbrelease_utils.get_chromeos_release_builder_path( 914 lsb_release_content=self._get_lsb_release_content()) 915 916 917 def get_chromeos_release_milestone(self): 918 """Get the value of attribute CHROMEOS_RELEASE_BUILD_TYPE 919 from lsb-release. 920 921 @returns The version string in lsb-release, under attribute 922 CHROMEOS_RELEASE_BUILD_TYPE. 923 """ 924 return lsbrelease_utils.get_chromeos_release_milestone( 925 lsb_release_content=self._get_lsb_release_content()) 926 927 928 def verify_cros_version_label(self): 929 """ Make sure host's cros-version label match the actual image in dut. 930 931 Remove any cros-version: label that doesn't match that installed in 932 the dut. 933 934 @param raise_error: Set to True to raise exception if any mismatch found 935 936 @raise error.AutoservError: If any mismatch between cros-version label 937 and the build installed in dut is found. 938 """ 939 labels = self._AFE.get_labels( 940 name__startswith=ds_constants.VERSION_PREFIX, 941 host__hostname=self.hostname) 942 mismatch_found = False 943 if labels: 944 # Ask the DUT for its canonical image name. This will be in 945 # a form like this: kevin-release/R66-10405.0.0 946 release_builder_path = self.get_release_builder_path() 947 host_list = [self.hostname] 948 for label in labels: 949 # Remove any cros-version label that does not match 950 # the DUT's installed image. 951 # 952 # TODO(jrbarnette): We make exceptions for certain 953 # known cases where the version label will not match the 954 # original CHROMEOS_RELEASE_BUILDER_PATH setting: 955 # * Tests for the `arc-presubmit` pool append 956 # "-cheetsth" to the label. 957 # * Moblab use cases based on `cros stage` store images 958 # under a name with the string "-custom" embedded. 959 # It's not reliable to match such an image name to the 960 # label. 961 label_version = label.name[len(ds_constants.VERSION_PREFIX):] 962 if '-custom' in label_version: 963 continue 964 if label_version.endswith('-cheetsth'): 965 label_version = label_version[:-len('-cheetsth')] 966 if label_version != release_builder_path: 967 logging.warn( 968 'cros-version label "%s" does not match ' 969 'release_builder_path %s. Removing the label.', 970 label.name, release_builder_path) 971 label.remove_hosts(hosts=host_list) 972 mismatch_found = True 973 if mismatch_found: 974 raise error.AutoservError('The host has wrong cros-version label.') 975 976 977 def cleanup_services(self): 978 """Reinitializes the device for cleanup. 979 980 Subclasses may override this to customize the cleanup method. 981 982 To indicate failure of the reset, the implementation may raise 983 any of: 984 error.AutoservRunError 985 error.AutotestRunError 986 FactoryImageCheckerException 987 988 @raises error.AutoservRunError 989 @raises error.AutotestRunError 990 @raises error.FactoryImageCheckerException 991 """ 992 self._restart_ui() 993 self._start_powerd_if_needed() 994 995 996 def cleanup(self): 997 """Cleanup state on device.""" 998 self.run('rm -f %s' % client_constants.CLEANUP_LOGS_PAUSED_FILE) 999 try: 1000 self.cleanup_services() 1001 except (error.AutotestRunError, error.AutoservRunError, 1002 FactoryImageCheckerException): 1003 logging.warning('Unable to restart ui, rebooting device.') 1004 # Since restarting the UI fails fall back to normal Autotest 1005 # cleanup routines, i.e. reboot the machine. 1006 super(CrosHost, self).cleanup() 1007 # Check if the rpm outlet was manipulated. 1008 if self.has_power(): 1009 self._cleanup_poweron() 1010 self.verify_cros_version_label() 1011 1012 1013 def reboot(self, **dargs): 1014 """ 1015 This function reboots the site host. The more generic 1016 RemoteHost.reboot() performs sync and sleeps for 5 1017 seconds. This is not necessary for Chrome OS devices as the 1018 sync should be finished in a short time during the reboot 1019 command. 1020 """ 1021 if 'reboot_cmd' not in dargs: 1022 reboot_timeout = dargs.get('reboot_timeout', 10) 1023 dargs['reboot_cmd'] = ('sleep 1; ' 1024 'reboot & sleep %d; ' 1025 'reboot -f' % reboot_timeout) 1026 # Enable fastsync to avoid running extra sync commands before reboot. 1027 if 'fastsync' not in dargs: 1028 dargs['fastsync'] = True 1029 1030 dargs['board'] = self.host_info_store.get().board 1031 # Record who called us 1032 orig = sys._getframe(1).f_code 1033 metric_fields = {'board' : dargs['board'], 1034 'dut_host_name' : self.hostname, 1035 'success' : True} 1036 metric_debug_fields = {'board' : dargs['board'], 1037 'caller' : "%s:%s" % (orig.co_filename, 1038 orig.co_name), 1039 'success' : True, 1040 'error' : ''} 1041 1042 t0 = time.time() 1043 try: 1044 super(CrosHost, self).reboot(**dargs) 1045 except Exception as e: 1046 metric_fields['success'] = False 1047 metric_debug_fields['success'] = False 1048 metric_debug_fields['error'] = type(e).__name__ 1049 raise 1050 finally: 1051 duration = int(time.time() - t0) 1052 metrics.Counter( 1053 'chromeos/autotest/autoserv/reboot_count').increment( 1054 fields=metric_fields) 1055 metrics.Counter( 1056 'chromeos/autotest/autoserv/reboot_debug').increment( 1057 fields=metric_debug_fields) 1058 metrics.SecondsDistribution( 1059 'chromeos/autotest/autoserv/reboot_duration').add( 1060 duration, fields=metric_fields) 1061 1062 1063 def suspend(self, suspend_time=60, 1064 suspend_cmd=None, allow_early_resume=False): 1065 """ 1066 This function suspends the site host. 1067 1068 @param suspend_time: How long to suspend as integer seconds. 1069 @param suspend_cmd: Suspend command to execute. 1070 @param allow_early_resume: If False and if device resumes before 1071 |suspend_time|, throw an error. 1072 1073 @exception AutoservSuspendError Host resumed earlier than 1074 |suspend_time|. 1075 """ 1076 1077 if suspend_cmd is None: 1078 suspend_cmd = ' && '.join([ 1079 'echo 0 > /sys/class/rtc/rtc0/wakealarm', 1080 'echo +%d > /sys/class/rtc/rtc0/wakealarm' % suspend_time, 1081 'powerd_dbus_suspend --delay=0']) 1082 super(CrosHost, self).suspend(suspend_time, suspend_cmd, 1083 allow_early_resume); 1084 1085 1086 def upstart_status(self, service_name): 1087 """Check the status of an upstart init script. 1088 1089 @param service_name: Service to look up. 1090 1091 @returns True if the service is running, False otherwise. 1092 """ 1093 return 'start/running' in self.run('status %s' % service_name, 1094 ignore_status=True).stdout 1095 1096 def upstart_stop(self, service_name): 1097 """Stops an upstart job if it's running. 1098 1099 @param service_name: Service to stop 1100 1101 @returns True if service has been stopped or was already stopped 1102 False otherwise. 1103 """ 1104 if not self.upstart_status(service_name): 1105 return True 1106 1107 result = self.run('stop %s' % service_name, ignore_status=True) 1108 if result.exit_status != 0: 1109 return False 1110 return True 1111 1112 def upstart_restart(self, service_name): 1113 """Restarts (or starts) an upstart job. 1114 1115 @param service_name: Service to start/restart 1116 1117 @returns True if service has been started/restarted, False otherwise. 1118 """ 1119 cmd = 'start' 1120 if self.upstart_status(service_name): 1121 cmd = 'restart' 1122 cmd = cmd + ' %s' % service_name 1123 result = self.run(cmd) 1124 if result.exit_status != 0: 1125 return False 1126 return True 1127 1128 def verify_software(self): 1129 """Verify working software on a Chrome OS system. 1130 1131 Tests for the following conditions: 1132 1. All conditions tested by the parent version of this 1133 function. 1134 2. Sufficient space in /mnt/stateful_partition. 1135 3. Sufficient space in /mnt/stateful_partition/encrypted. 1136 4. update_engine answers a simple status request over DBus. 1137 1138 """ 1139 super(CrosHost, self).verify_software() 1140 default_kilo_inodes_required = CONFIG.get_config_value( 1141 'SERVER', 'kilo_inodes_required', type=int, default=100) 1142 board = self.get_board().replace(ds_constants.BOARD_PREFIX, '') 1143 kilo_inodes_required = CONFIG.get_config_value( 1144 'SERVER', 'kilo_inodes_required_%s' % board, 1145 type=int, default=default_kilo_inodes_required) 1146 self.check_inodes('/mnt/stateful_partition', kilo_inodes_required) 1147 self.check_diskspace( 1148 '/mnt/stateful_partition', 1149 CONFIG.get_config_value( 1150 'SERVER', 'gb_diskspace_required', type=float, 1151 default=20.0)) 1152 encrypted_stateful_path = '/mnt/stateful_partition/encrypted' 1153 # Not all targets build with encrypted stateful support. 1154 if self.path_exists(encrypted_stateful_path): 1155 self.check_diskspace( 1156 encrypted_stateful_path, 1157 CONFIG.get_config_value( 1158 'SERVER', 'gb_encrypted_diskspace_required', type=float, 1159 default=0.1)) 1160 1161 self.wait_for_system_services() 1162 1163 # Factory images don't run update engine, 1164 # goofy controls dbus on these DUTs. 1165 if not self._is_factory_image(): 1166 self.run('update_engine_client --status') 1167 1168 self.verify_cros_version_label() 1169 1170 1171 @retry.retry(error.AutoservError, timeout_min=5, delay_sec=10) 1172 def wait_for_system_services(self): 1173 """Waits for system-services to be running. 1174 1175 Sometimes, update_engine will take a while to update firmware, so we 1176 should give this some time to finish. See crbug.com/765686#c38 for 1177 details. 1178 """ 1179 if not self.upstart_status('system-services'): 1180 raise error.AutoservError('Chrome failed to reach login. ' 1181 'System services not running.') 1182 1183 1184 def verify(self): 1185 """Verify Chrome OS system is in good state.""" 1186 message = 'Beginning verify for host %s board %s model %s' 1187 info = self.host_info_store.get() 1188 message %= (self.hostname, info.board, info.model) 1189 self.record('INFO', None, None, message) 1190 self._repair_strategy.verify(self) 1191 1192 1193 def make_ssh_command(self, user='root', port=22, opts='', hosts_file=None, 1194 connect_timeout=None, alive_interval=None, 1195 alive_count_max=None, connection_attempts=None): 1196 """Override default make_ssh_command to use options tuned for Chrome OS. 1197 1198 Tuning changes: 1199 - ConnectTimeout=30; maximum of 30 seconds allowed for an SSH 1200 connection failure. Consistency with remote_access.sh. 1201 1202 - ServerAliveInterval=900; which causes SSH to ping connection every 1203 900 seconds. In conjunction with ServerAliveCountMax ensures 1204 that if the connection dies, Autotest will bail out. 1205 Originally tried 60 secs, but saw frequent job ABORTS where 1206 the test completed successfully. Later increased from 180 seconds to 1207 900 seconds to account for tests where the DUT is suspended for 1208 longer periods of time. 1209 1210 - ServerAliveCountMax=3; consistency with remote_access.sh. 1211 1212 - ConnectAttempts=4; reduce flakiness in connection errors; 1213 consistency with remote_access.sh. 1214 1215 - UserKnownHostsFile=/dev/null; we don't care about the keys. 1216 Host keys change with every new installation, don't waste 1217 memory/space saving them. 1218 1219 - SSH protocol forced to 2; needed for ServerAliveInterval. 1220 1221 @param user User name to use for the ssh connection. 1222 @param port Port on the target host to use for ssh connection. 1223 @param opts Additional options to the ssh command. 1224 @param hosts_file Ignored. 1225 @param connect_timeout Ignored. 1226 @param alive_interval Ignored. 1227 @param alive_count_max Ignored. 1228 @param connection_attempts Ignored. 1229 """ 1230 options = ' '.join([opts, '-o Protocol=2']) 1231 return super(CrosHost, self).make_ssh_command( 1232 user=user, port=port, opts=options, hosts_file='/dev/null', 1233 connect_timeout=30, alive_interval=900, alive_count_max=3, 1234 connection_attempts=4) 1235 1236 1237 def syslog(self, message, tag='autotest'): 1238 """Logs a message to syslog on host. 1239 1240 @param message String message to log into syslog 1241 @param tag String tag prefix for syslog 1242 1243 """ 1244 self.run('logger -t "%s" "%s"' % (tag, message)) 1245 1246 1247 def _ping_check_status(self, status): 1248 """Ping the host once, and return whether it has a given status. 1249 1250 @param status Check the ping status against this value. 1251 @return True iff `status` and the result of ping are the same 1252 (i.e. both True or both False). 1253 1254 """ 1255 ping_val = utils.ping(self.hostname, tries=1, deadline=1) 1256 return not (status ^ (ping_val == 0)) 1257 1258 def _ping_wait_for_status(self, status, timeout): 1259 """Wait for the host to have a given status (UP or DOWN). 1260 1261 Status is checked by polling. Polling will not last longer 1262 than the number of seconds in `timeout`. The polling 1263 interval will be long enough that only approximately 1264 _PING_WAIT_COUNT polling cycles will be executed, subject 1265 to a maximum interval of about one minute. 1266 1267 @param status Waiting will stop immediately if `ping` of the 1268 host returns this status. 1269 @param timeout Poll for at most this many seconds. 1270 @return True iff the host status from `ping` matched the 1271 requested status at the time of return. 1272 1273 """ 1274 # _ping_check_status() takes about 1 second, hence the 1275 # "- 1" in the formula below. 1276 # FIXME: if the ping command errors then _ping_check_status() 1277 # returns instantly. If timeout is also smaller than twice 1278 # _PING_WAIT_COUNT then the while loop below forks many 1279 # thousands of ping commands (see /tmp/test_that_results_XXXXX/ 1280 # /results-1-logging_YYY.ZZZ/debug/autoserv.DEBUG) and hogs one 1281 # CPU core for 60 seconds. 1282 poll_interval = min(int(timeout / self._PING_WAIT_COUNT), 60) - 1 1283 end_time = time.time() + timeout 1284 while time.time() <= end_time: 1285 if self._ping_check_status(status): 1286 return True 1287 if poll_interval > 0: 1288 time.sleep(poll_interval) 1289 1290 # The last thing we did was sleep(poll_interval), so it may 1291 # have been too long since the last `ping`. Check one more 1292 # time, just to be sure. 1293 return self._ping_check_status(status) 1294 1295 def ping_wait_up(self, timeout): 1296 """Wait for the host to respond to `ping`. 1297 1298 N.B. This method is not a reliable substitute for 1299 `wait_up()`, because a host that responds to ping will not 1300 necessarily respond to ssh. This method should only be used 1301 if the target DUT can be considered functional even if it 1302 can't be reached via ssh. 1303 1304 @param timeout Minimum time to allow before declaring the 1305 host to be non-responsive. 1306 @return True iff the host answered to ping before the timeout. 1307 1308 """ 1309 return self._ping_wait_for_status(self._PING_STATUS_UP, timeout) 1310 1311 def ping_wait_down(self, timeout): 1312 """Wait until the host no longer responds to `ping`. 1313 1314 This function can be used as a slightly faster version of 1315 `wait_down()`, by avoiding potentially long ssh timeouts. 1316 1317 @param timeout Minimum time to allow for the host to become 1318 non-responsive. 1319 @return True iff the host quit answering ping before the 1320 timeout. 1321 1322 """ 1323 return self._ping_wait_for_status(self._PING_STATUS_DOWN, timeout) 1324 1325 def test_wait_for_sleep(self, sleep_timeout=None): 1326 """Wait for the client to enter low-power sleep mode. 1327 1328 The test for "is asleep" can't distinguish a system that is 1329 powered off; to confirm that the unit was asleep, it is 1330 necessary to force resume, and then call 1331 `test_wait_for_resume()`. 1332 1333 This function is expected to be called from a test as part 1334 of a sequence like the following: 1335 1336 ~~~~~~~~ 1337 boot_id = host.get_boot_id() 1338 # trigger sleep on the host 1339 host.test_wait_for_sleep() 1340 # trigger resume on the host 1341 host.test_wait_for_resume(boot_id) 1342 ~~~~~~~~ 1343 1344 @param sleep_timeout time limit in seconds to allow the host sleep. 1345 1346 @exception TestFail The host did not go to sleep within 1347 the allowed time. 1348 """ 1349 if sleep_timeout is None: 1350 sleep_timeout = self.SLEEP_TIMEOUT 1351 1352 if not self.ping_wait_down(timeout=sleep_timeout): 1353 raise error.TestFail( 1354 'client failed to sleep after %d seconds' % sleep_timeout) 1355 1356 1357 def test_wait_for_resume(self, old_boot_id, resume_timeout=None): 1358 """Wait for the client to resume from low-power sleep mode. 1359 1360 The `old_boot_id` parameter should be the value from 1361 `get_boot_id()` obtained prior to entering sleep mode. A 1362 `TestFail` exception is raised if the boot id changes. 1363 1364 See @ref test_wait_for_sleep for more on this function's 1365 usage. 1366 1367 @param old_boot_id A boot id value obtained before the 1368 target host went to sleep. 1369 @param resume_timeout time limit in seconds to allow the host up. 1370 1371 @exception TestFail The host did not respond within the 1372 allowed time. 1373 @exception TestFail The host responded, but the boot id test 1374 indicated a reboot rather than a sleep 1375 cycle. 1376 """ 1377 if resume_timeout is None: 1378 resume_timeout = self.RESUME_TIMEOUT 1379 1380 if not self.wait_up(timeout=resume_timeout): 1381 raise error.TestFail( 1382 'client failed to resume from sleep after %d seconds' % 1383 resume_timeout) 1384 else: 1385 new_boot_id = self.get_boot_id() 1386 if new_boot_id != old_boot_id: 1387 logging.error('client rebooted (old boot %s, new boot %s)', 1388 old_boot_id, new_boot_id) 1389 raise error.TestFail( 1390 'client rebooted, but sleep was expected') 1391 1392 1393 def test_wait_for_shutdown(self, shutdown_timeout=None): 1394 """Wait for the client to shut down. 1395 1396 The test for "has shut down" can't distinguish a system that 1397 is merely asleep; to confirm that the unit was down, it is 1398 necessary to force boot, and then call test_wait_for_boot(). 1399 1400 This function is expected to be called from a test as part 1401 of a sequence like the following: 1402 1403 ~~~~~~~~ 1404 boot_id = host.get_boot_id() 1405 # trigger shutdown on the host 1406 host.test_wait_for_shutdown() 1407 # trigger boot on the host 1408 host.test_wait_for_boot(boot_id) 1409 ~~~~~~~~ 1410 1411 @param shutdown_timeout time limit in seconds to allow the host down. 1412 @exception TestFail The host did not shut down within the 1413 allowed time. 1414 """ 1415 if shutdown_timeout is None: 1416 shutdown_timeout = self.SHUTDOWN_TIMEOUT 1417 1418 if not self.ping_wait_down(timeout=shutdown_timeout): 1419 raise error.TestFail( 1420 'client failed to shut down after %d seconds' % 1421 shutdown_timeout) 1422 1423 1424 def test_wait_for_boot(self, old_boot_id=None): 1425 """Wait for the client to boot from cold power. 1426 1427 The `old_boot_id` parameter should be the value from 1428 `get_boot_id()` obtained prior to shutting down. A 1429 `TestFail` exception is raised if the boot id does not 1430 change. The boot id test is omitted if `old_boot_id` is not 1431 specified. 1432 1433 See @ref test_wait_for_shutdown for more on this function's 1434 usage. 1435 1436 @param old_boot_id A boot id value obtained before the 1437 shut down. 1438 1439 @exception TestFail The host did not respond within the 1440 allowed time. 1441 @exception TestFail The host responded, but the boot id test 1442 indicated that there was no reboot. 1443 """ 1444 if not self.wait_up(timeout=self.REBOOT_TIMEOUT): 1445 raise error.TestFail( 1446 'client failed to reboot after %d seconds' % 1447 self.REBOOT_TIMEOUT) 1448 elif old_boot_id: 1449 if self.get_boot_id() == old_boot_id: 1450 logging.error('client not rebooted (boot %s)', 1451 old_boot_id) 1452 raise error.TestFail( 1453 'client is back up, but did not reboot') 1454 1455 1456 @staticmethod 1457 def check_for_rpm_support(hostname): 1458 """For a given hostname, return whether or not it is powered by an RPM. 1459 1460 @param hostname: hostname to check for rpm support. 1461 1462 @return None if this host does not follows the defined naming format 1463 for RPM powered DUT's in the lab. If it does follow the format, 1464 it returns a regular expression MatchObject instead. 1465 """ 1466 return re.match(CrosHost._RPM_HOSTNAME_REGEX, hostname) 1467 1468 1469 def has_power(self): 1470 """For this host, return whether or not it is powered by an RPM. 1471 1472 @return True if this host is in the CROS lab and follows the defined 1473 naming format. 1474 """ 1475 return CrosHost.check_for_rpm_support(self.hostname) 1476 1477 1478 def _set_power(self, state, power_method): 1479 """Sets the power to the host via RPM, Servo or manual. 1480 1481 @param state Specifies which power state to set to DUT 1482 @param power_method Specifies which method of power control to 1483 use. By default "RPM" will be used. Valid values 1484 are the strings "RPM", "manual", "servoj10". 1485 1486 """ 1487 ACCEPTABLE_STATES = ['ON', 'OFF'] 1488 1489 if state.upper() not in ACCEPTABLE_STATES: 1490 raise error.TestError('State must be one of: %s.' 1491 % (ACCEPTABLE_STATES,)) 1492 1493 if power_method == self.POWER_CONTROL_SERVO: 1494 logging.info('Setting servo port J10 to %s', state) 1495 self.servo.set('prtctl3_pwren', state.lower()) 1496 time.sleep(self._USB_POWER_TIMEOUT) 1497 elif power_method == self.POWER_CONTROL_MANUAL: 1498 logging.info('You have %d seconds to set the AC power to %s.', 1499 self._POWER_CYCLE_TIMEOUT, state) 1500 time.sleep(self._POWER_CYCLE_TIMEOUT) 1501 else: 1502 if not self.has_power(): 1503 raise error.TestFail('DUT does not have RPM connected.') 1504 self._add_rpm_changed_tag() 1505 rpm_client.set_power(self, state.upper(), timeout_mins=5) 1506 1507 1508 def power_off(self, power_method=POWER_CONTROL_RPM): 1509 """Turn off power to this host via RPM, Servo or manual. 1510 1511 @param power_method Specifies which method of power control to 1512 use. By default "RPM" will be used. Valid values 1513 are the strings "RPM", "manual", "servoj10". 1514 1515 """ 1516 self._set_power('OFF', power_method) 1517 1518 1519 def power_on(self, power_method=POWER_CONTROL_RPM): 1520 """Turn on power to this host via RPM, Servo or manual. 1521 1522 @param power_method Specifies which method of power control to 1523 use. By default "RPM" will be used. Valid values 1524 are the strings "RPM", "manual", "servoj10". 1525 1526 """ 1527 self._set_power('ON', power_method) 1528 1529 1530 def power_cycle(self, power_method=POWER_CONTROL_RPM): 1531 """Cycle power to this host by turning it OFF, then ON. 1532 1533 @param power_method Specifies which method of power control to 1534 use. By default "RPM" will be used. Valid values 1535 are the strings "RPM", "manual", "servoj10". 1536 1537 """ 1538 if power_method in (self.POWER_CONTROL_SERVO, 1539 self.POWER_CONTROL_MANUAL): 1540 self.power_off(power_method=power_method) 1541 time.sleep(self._POWER_CYCLE_TIMEOUT) 1542 self.power_on(power_method=power_method) 1543 else: 1544 self._add_rpm_changed_tag() 1545 rpm_client.set_power(self, 'CYCLE') 1546 1547 1548 def get_platform(self): 1549 """Determine the correct platform label for this host. 1550 1551 @returns a string representing this host's platform. 1552 """ 1553 release_info = utils.parse_cmd_output('cat /etc/lsb-release', 1554 run_method=self.run) 1555 unibuild = release_info.get('CHROMEOS_RELEASE_UNIBUILD') == '1' 1556 platform = '' 1557 if unibuild: 1558 cmd = 'mosys platform model' 1559 result = self.run(command=cmd, ignore_status=True) 1560 if result.exit_status == 0: 1561 platform = result.stdout.strip() 1562 1563 if not platform: 1564 # Look at the firmware for non-unibuild cases or if mosys fails. 1565 crossystem = utils.Crossystem(self) 1566 crossystem.init() 1567 # Extract fwid value and use the leading part as the platform id. 1568 # fwid generally follow the format of {platform}.{firmware version} 1569 # Example: Alex.X.YYY.Z or Google_Alex.X.YYY.Z 1570 platform = crossystem.fwid().split('.')[0].lower() 1571 # Newer platforms start with 'Google_' while the older ones do not. 1572 platform = platform.replace('google_', '') 1573 return platform 1574 1575 1576 def get_architecture(self): 1577 """Determine the correct architecture label for this host. 1578 1579 @returns a string representing this host's architecture. 1580 """ 1581 crossystem = utils.Crossystem(self) 1582 crossystem.init() 1583 return crossystem.arch() 1584 1585 1586 def get_chrome_version(self): 1587 """Gets the Chrome version number and milestone as strings. 1588 1589 Invokes "chrome --version" to get the version number and milestone. 1590 1591 @return A tuple (chrome_ver, milestone) where "chrome_ver" is the 1592 current Chrome version number as a string (in the form "W.X.Y.Z") 1593 and "milestone" is the first component of the version number 1594 (the "W" from "W.X.Y.Z"). If the version number cannot be parsed 1595 in the "W.X.Y.Z" format, the "chrome_ver" will be the full output 1596 of "chrome --version" and the milestone will be the empty string. 1597 1598 """ 1599 version_string = self.run(client_constants.CHROME_VERSION_COMMAND).stdout 1600 return utils.parse_chrome_version(version_string) 1601 1602 1603 def get_ec_version(self): 1604 """Get the ec version as strings. 1605 1606 @returns a string representing this host's ec version. 1607 """ 1608 command = 'mosys ec info -s fw_version' 1609 result = self.run(command, ignore_status=True) 1610 if result.exit_status != 0: 1611 return '' 1612 return result.stdout.strip() 1613 1614 1615 def get_firmware_version(self): 1616 """Get the firmware version as strings. 1617 1618 @returns a string representing this host's firmware version. 1619 """ 1620 crossystem = utils.Crossystem(self) 1621 crossystem.init() 1622 return crossystem.fwid() 1623 1624 1625 def get_hardware_revision(self): 1626 """Get the hardware revision as strings. 1627 1628 @returns a string representing this host's hardware revision. 1629 """ 1630 command = 'mosys platform version' 1631 result = self.run(command, ignore_status=True) 1632 if result.exit_status != 0: 1633 return '' 1634 return result.stdout.strip() 1635 1636 1637 def get_kernel_version(self): 1638 """Get the kernel version as strings. 1639 1640 @returns a string representing this host's kernel version. 1641 """ 1642 return self.run('uname -r').stdout.strip() 1643 1644 1645 def get_cpu_name(self): 1646 """Get the cpu name as strings. 1647 1648 @returns a string representing this host's cpu name. 1649 """ 1650 1651 # Try get cpu name from device tree first 1652 if self.path_exists('/proc/device-tree/compatible'): 1653 command = ' | '.join( 1654 ["sed -e 's/\\x0/\\n/g' /proc/device-tree/compatible", 1655 'tail -1']) 1656 return self.run(command).stdout.strip().replace(',', ' ') 1657 1658 # Get cpu name from uname -p 1659 command = 'uname -p' 1660 ret = self.run(command).stdout.strip() 1661 1662 # 'uname -p' return variant of unknown or amd64 or x86_64 or i686 1663 # Try get cpu name from /proc/cpuinfo instead 1664 if re.match("unknown|amd64|[ix][0-9]?86(_64)?", ret, re.IGNORECASE): 1665 command = "grep model.name /proc/cpuinfo | cut -f 2 -d: | head -1" 1666 self = self.run(command).stdout.strip() 1667 1668 # Remove bloat from CPU name, for example 1669 # Intel(R) Core(TM) i5-7Y57 CPU @ 1.20GHz -> Intel Core i5-7Y57 1670 # Intel(R) Xeon(R) CPU E5-2690 v4 @ 2.60GHz -> Intel Xeon E5-2690 v4 1671 # AMD A10-7850K APU with Radeon(TM) R7 Graphics -> AMD A10-7850K 1672 # AMD GX-212JC SOC with Radeon(TM) R2E Graphics -> AMD GX-212JC 1673 trim_re = r' (@|processor|apu|soc|radeon).*|\(.*?\)| cpu' 1674 return re.sub(trim_re, '', ret, flags=re.IGNORECASE) 1675 1676 1677 def get_screen_resolution(self): 1678 """Get the screen(s) resolution as strings. 1679 In case of more than 1 monitor, return resolution for each monitor 1680 separate with plus sign. 1681 1682 @returns a string representing this host's screen(s) resolution. 1683 """ 1684 command = 'for f in /sys/class/drm/*/*/modes; do head -1 $f; done' 1685 ret = self.run(command, ignore_status=True) 1686 # We might have Chromebox without a screen 1687 if ret.exit_status != 0: 1688 return '' 1689 return ret.stdout.strip().replace('\n', '+') 1690 1691 1692 def get_mem_total_gb(self): 1693 """Get total memory available in the system in GiB (2^20). 1694 1695 @returns an integer representing total memory 1696 """ 1697 mem_total_kb = self.read_from_meminfo('MemTotal') 1698 kb_in_gb = float(2 ** 20) 1699 return int(round(mem_total_kb / kb_in_gb)) 1700 1701 1702 def get_disk_size_gb(self): 1703 """Get size of disk in GB (10^9) 1704 1705 @returns an integer representing size of disk, 0 in Error Case 1706 """ 1707 command = 'grep $(rootdev -s -d | cut -f3 -d/)$ /proc/partitions' 1708 result = self.run(command, ignore_status=True) 1709 if result.exit_status != 0: 1710 return 0 1711 _, _, block, _ = re.split(r' +', result.stdout.strip()) 1712 byte_per_block = 1024.0 1713 disk_kb_in_gb = 1e9 1714 return int(int(block) * byte_per_block / disk_kb_in_gb + 0.5) 1715 1716 1717 def get_battery_size(self): 1718 """Get size of battery in Watt-hour via sysfs 1719 1720 This method assumes that battery support voltage_min_design and 1721 charge_full_design sysfs. 1722 1723 @returns a float representing Battery size, 0 if error. 1724 """ 1725 # sysfs report data in micro scale 1726 battery_scale = 1e6 1727 1728 command = 'cat /sys/class/power_supply/*/voltage_min_design' 1729 result = self.run(command, ignore_status=True) 1730 if result.exit_status != 0: 1731 return 0 1732 voltage = float(result.stdout.strip()) / battery_scale 1733 1734 command = 'cat /sys/class/power_supply/*/charge_full_design' 1735 result = self.run(command, ignore_status=True) 1736 if result.exit_status != 0: 1737 return 0 1738 amphereHour = float(result.stdout.strip()) / battery_scale 1739 1740 return voltage * amphereHour 1741 1742 1743 def get_low_battery_shutdown_percent(self): 1744 """Get the percent-based low-battery shutdown threshold. 1745 1746 @returns a float representing low-battery shutdown percent, 0 if error. 1747 """ 1748 ret = 0.0 1749 try: 1750 command = 'check_powerd_config --low_battery_shutdown_percent' 1751 ret = float(self.run(command).stdout) 1752 except error.CmdError: 1753 logging.debug("Can't run %s", command) 1754 except ValueError: 1755 logging.debug("Didn't get number from %s", command) 1756 1757 return ret 1758 1759 1760 def has_hammer(self): 1761 """Check whether DUT has hammer device or not. 1762 1763 @returns boolean whether device has hammer or not 1764 """ 1765 command = 'grep Hammer /sys/bus/usb/devices/*/product' 1766 return self.run(command, ignore_status=True).exit_status == 0 1767 1768 1769 def is_chrome_switch_present(self, switch): 1770 """Returns True if the specified switch was provided to Chrome. 1771 1772 @param switch The chrome switch to search for. 1773 """ 1774 1775 command = 'pgrep -x -f -c "/opt/google/chrome/chrome.*%s.*"' % switch 1776 return self.run(command, ignore_status=True).exit_status == 0 1777 1778 1779 def oobe_triggers_update(self): 1780 """Returns True if this host has an OOBE flow during which 1781 it will perform an update check and perhaps an update. 1782 One example of such a flow is Hands-Off Zero-Touch Enrollment. 1783 As more such flows are developed, code handling them needs 1784 to be added here. 1785 1786 @return Boolean indicating whether this host's OOBE triggers an update. 1787 """ 1788 return self.is_chrome_switch_present( 1789 '--enterprise-enable-zero-touch-enrollment=hands-off') 1790 1791 1792 # TODO(kevcheng): change this to just return the board without the 1793 # 'board:' prefix and fix up all the callers. Also look into removing the 1794 # need for this method. 1795 def get_board(self): 1796 """Determine the correct board label for this host. 1797 1798 @returns a string representing this host's board. 1799 """ 1800 release_info = utils.parse_cmd_output('cat /etc/lsb-release', 1801 run_method=self.run) 1802 return (ds_constants.BOARD_PREFIX + 1803 release_info['CHROMEOS_RELEASE_BOARD']) 1804 1805 def get_channel(self): 1806 """Determine the correct channel label for this host. 1807 1808 @returns: a string represeting this host's build channel. 1809 (stable, dev, beta). None on fail. 1810 """ 1811 return lsbrelease_utils.get_chromeos_channel( 1812 lsb_release_content=self._get_lsb_release_content()) 1813 1814 def get_power_supply(self): 1815 """ 1816 Determine what type of power supply the host has 1817 1818 @returns a string representing this host's power supply. 1819 'power:battery' when the device has a battery intended for 1820 extended use 1821 'power:AC_primary' when the device has a battery not intended 1822 for extended use (for moving the machine, etc) 1823 'power:AC_only' when the device has no battery at all. 1824 """ 1825 psu = self.run(command='mosys psu type', ignore_status=True) 1826 if psu.exit_status: 1827 # The psu command for mosys is not included for all platforms. The 1828 # assumption is that the device will have a battery if the command 1829 # is not found. 1830 return 'power:battery' 1831 1832 psu_str = psu.stdout.strip() 1833 if psu_str == 'unknown': 1834 return None 1835 1836 return 'power:%s' % psu_str 1837 1838 1839 def has_battery(self): 1840 """Determine if DUT has a battery. 1841 1842 Returns: 1843 Boolean, False if known not to have battery, True otherwise. 1844 """ 1845 rv = True 1846 power_supply = self.get_power_supply() 1847 if power_supply == 'power:battery': 1848 _NO_BATTERY_BOARD_TYPE = ['CHROMEBOX', 'CHROMEBIT', 'CHROMEBASE'] 1849 board_type = self.get_board_type() 1850 if board_type in _NO_BATTERY_BOARD_TYPE: 1851 logging.warn('Do NOT believe type %s has battery. ' 1852 'See debug for mosys details', board_type) 1853 psu = self.system_output('mosys -vvvv psu type', 1854 ignore_status=True) 1855 logging.debug(psu) 1856 rv = False 1857 elif power_supply == 'power:AC_only': 1858 rv = False 1859 1860 return rv 1861 1862 1863 def get_servo(self): 1864 """Determine if the host has a servo attached. 1865 1866 If the host has a working servo attached, it should have a servo label. 1867 1868 @return: string 'servo' if the host has servo attached. Otherwise, 1869 returns None. 1870 """ 1871 return 'servo' if self._servo_host else None 1872 1873 1874 def has_internal_display(self): 1875 """Determine if the device under test is equipped with an internal 1876 display. 1877 1878 @return: 'internal_display' if one is present; None otherwise. 1879 """ 1880 from autotest_lib.client.cros.graphics import graphics_utils 1881 from autotest_lib.client.common_lib import utils as common_utils 1882 1883 def __system_output(cmd): 1884 return self.run(cmd).stdout 1885 1886 def __read_file(remote_path): 1887 return self.run('cat %s' % remote_path).stdout 1888 1889 # Hijack the necessary client functions so that we can take advantage 1890 # of the client lib here. 1891 # FIXME: find a less hacky way than this 1892 original_system_output = utils.system_output 1893 original_read_file = common_utils.read_file 1894 utils.system_output = __system_output 1895 common_utils.read_file = __read_file 1896 try: 1897 return ('internal_display' if graphics_utils.has_internal_display() 1898 else None) 1899 finally: 1900 utils.system_output = original_system_output 1901 common_utils.read_file = original_read_file 1902 1903 1904 def is_boot_from_usb(self): 1905 """Check if DUT is boot from USB. 1906 1907 @return: True if DUT is boot from usb. 1908 """ 1909 device = self.run('rootdev -s -d').stdout.strip() 1910 removable = int(self.run('cat /sys/block/%s/removable' % 1911 os.path.basename(device)).stdout.strip()) 1912 return removable == 1 1913 1914 1915 def read_from_meminfo(self, key): 1916 """Return the memory info from /proc/meminfo 1917 1918 @param key: meminfo requested 1919 1920 @return the memory value as a string 1921 1922 """ 1923 meminfo = self.run('grep %s /proc/meminfo' % key).stdout.strip() 1924 logging.debug('%s', meminfo) 1925 return int(re.search(r'\d+', meminfo).group(0)) 1926 1927 1928 def get_cpu_arch(self): 1929 """Returns CPU arch of the device. 1930 1931 @return CPU architecture of the DUT. 1932 """ 1933 # Add CPUs by following logic in client/bin/utils.py. 1934 if self.run("grep '^flags.*:.* lm .*' /proc/cpuinfo", 1935 ignore_status=True).stdout: 1936 return 'x86_64' 1937 if self.run("grep -Ei 'ARM|CPU implementer' /proc/cpuinfo", 1938 ignore_status=True).stdout: 1939 return 'arm' 1940 return 'i386' 1941 1942 1943 def get_board_type(self): 1944 """ 1945 Get the DUT's device type from /etc/lsb-release. 1946 DEVICETYPE can be one of CHROMEBOX, CHROMEBASE, CHROMEBOOK or more. 1947 1948 @return value of DEVICETYPE param from lsb-release. 1949 """ 1950 device_type = self.run('grep DEVICETYPE /etc/lsb-release', 1951 ignore_status=True).stdout 1952 if device_type: 1953 return device_type.split('=')[-1].strip() 1954 return '' 1955 1956 1957 def get_arc_version(self): 1958 """Return ARC version installed on the DUT. 1959 1960 @returns ARC version as string if the CrOS build has ARC, else None. 1961 """ 1962 arc_version = self.run('grep CHROMEOS_ARC_VERSION /etc/lsb-release', 1963 ignore_status=True).stdout 1964 if arc_version: 1965 return arc_version.split('=')[-1].strip() 1966 return None 1967 1968 1969 def get_os_type(self): 1970 return 'cros' 1971 1972 1973 def get_labels(self): 1974 """Return the detected labels on the host.""" 1975 return self.labels.get_labels(self) 1976