1# Lint as: python2, python3 2# Copyright (c) 2012 The Chromium OS Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6from __future__ import absolute_import 7from __future__ import division 8from __future__ import print_function 9 10import logging 11import os 12import re 13import sys 14import six 15import time 16 17import common 18from autotest_lib.client.bin import utils 19from autotest_lib.client.common_lib import autotemp 20from autotest_lib.client.common_lib import error 21from autotest_lib.client.common_lib import global_config 22from autotest_lib.client.common_lib import hosts 23from autotest_lib.client.common_lib import lsbrelease_utils 24from autotest_lib.client.common_lib import utils as common_utils 25from autotest_lib.client.common_lib.cros import cros_config 26from autotest_lib.client.common_lib.cros import dev_server 27from autotest_lib.client.common_lib.cros import retry 28from autotest_lib.client.cros import constants as client_constants 29from autotest_lib.client.cros import cros_ui 30from autotest_lib.server import afe_utils 31from autotest_lib.server import utils as server_utils 32from autotest_lib.server.cros import provision 33from autotest_lib.server.cros.dynamic_suite import constants as ds_constants 34from autotest_lib.server.cros.dynamic_suite import tools, frontend_wrappers 35from autotest_lib.server.cros.device_health_profile import device_health_profile 36from autotest_lib.server.cros.device_health_profile import profile_constants 37from autotest_lib.server.cros.servo import pdtester 38from autotest_lib.server.hosts import abstract_ssh 39from autotest_lib.server.hosts import base_label 40from autotest_lib.server.hosts import chameleon_host 41from autotest_lib.server.hosts import cros_constants 42from autotest_lib.server.hosts import cros_label 43from autotest_lib.server.hosts import cros_repair 44from autotest_lib.server.hosts import pdtester_host 45from autotest_lib.server.hosts import servo_host 46from autotest_lib.server.hosts import servo_constants 47from autotest_lib.site_utils.rpm_control_system import rpm_client 48from autotest_lib.site_utils.admin_audit import constants as audit_const 49from autotest_lib.site_utils.admin_audit import verifiers as audit_verify 50from six.moves import zip 51 52# In case cros_host is being ran via SSP on an older Moblab version with an 53# older chromite version. 54try: 55 from chromite.lib import metrics 56except ImportError: 57 metrics = utils.metrics_mock 58 59 60CONFIG = global_config.global_config 61 62class FactoryImageCheckerException(error.AutoservError): 63 """Exception raised when an image is a factory image.""" 64 pass 65 66 67class CrosHost(abstract_ssh.AbstractSSHHost): 68 """Chromium OS specific subclass of Host.""" 69 70 VERSION_PREFIX = provision.CROS_VERSION_PREFIX 71 72 _AFE = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10) 73 74 # Timeout values (in seconds) associated with various Chrome OS 75 # state changes. 76 # 77 # In general, a good rule of thumb is that the timeout can be up 78 # to twice the typical measured value on the slowest platform. 79 # The times here have not necessarily been empirically tested to 80 # meet this criterion. 81 # 82 # SLEEP_TIMEOUT: Time to allow for suspend to memory. 83 # RESUME_TIMEOUT: Time to allow for resume after suspend, plus 84 # time to restart the netwowrk. 85 # SHUTDOWN_TIMEOUT: Time to allow for shut down. 86 # BOOT_TIMEOUT: Time to allow for boot from power off. Among 87 # other things, this must account for the 30 second dev-mode 88 # screen delay, time to start the network on the DUT, and the 89 # ssh timeout of 120 seconds. 90 # USB_BOOT_TIMEOUT: Time to allow for boot from a USB device, 91 # including the 30 second dev-mode delay and time to start the 92 # network. 93 # INSTALL_TIMEOUT: Time to allow for chromeos-install. 94 # POWERWASH_BOOT_TIMEOUT: Time to allow for a reboot that 95 # includes powerwash. 96 97 SLEEP_TIMEOUT = 2 98 RESUME_TIMEOUT = 10 99 SHUTDOWN_TIMEOUT = 10 100 BOOT_TIMEOUT = 150 101 USB_BOOT_TIMEOUT = 300 102 INSTALL_TIMEOUT = 480 103 POWERWASH_BOOT_TIMEOUT = 60 104 105 # Minimum OS version that supports server side packaging. Older builds may 106 # not have server side package built or with Autotest code change to support 107 # server-side packaging. 108 MIN_VERSION_SUPPORT_SSP = CONFIG.get_config_value( 109 'AUTOSERV', 'min_version_support_ssp', type=int) 110 111 USE_FSFREEZE = CONFIG.get_config_value( 112 'CROS', 'enable_fs_freeze', type=bool, default=False) 113 114 # REBOOT_TIMEOUT: How long to wait for a reboot. 115 # 116 # We have a long timeout to ensure we don't flakily fail due to other 117 # issues. Shorter timeouts are vetted in platform_RebootAfterUpdate. 118 # TODO(sbasi - crbug.com/276094) Restore to 5 mins once the 'host did not 119 # return from reboot' bug is solved. 120 REBOOT_TIMEOUT = 480 121 122 # _USB_POWER_TIMEOUT: Time to allow for USB to power toggle ON and OFF. 123 # _POWER_CYCLE_TIMEOUT: Time to allow for manual power cycle. 124 # _CHANGE_SERVO_ROLE_TIMEOUT: Time to allow DUT regain network connection 125 # since changing servo role will reset USB state 126 # and causes temporary ethernet drop. 127 _USB_POWER_TIMEOUT = 5 128 _POWER_CYCLE_TIMEOUT = 10 129 _CHANGE_SERVO_ROLE_TIMEOUT = 180 130 131 _RPM_HOSTNAME_REGEX = ('chromeos(\d+)(-row(\d+))?-rack(\d+[a-z]*)' 132 '-host(\d+)') 133 134 # Constants used in ping_wait_up() and ping_wait_down(). 135 # 136 # _PING_WAIT_COUNT is the approximate number of polling 137 # cycles to use when waiting for a host state change. 138 # 139 # _PING_STATUS_DOWN and _PING_STATUS_UP are names used 140 # for arguments to the internal _ping_wait_for_status() 141 # method. 142 _PING_WAIT_COUNT = 40 143 _PING_STATUS_DOWN = False 144 _PING_STATUS_UP = True 145 146 # Allowed values for the power_method argument. 147 148 # POWER_CONTROL_RPM: Used in power_off/on/cycle() methods, default for all 149 # DUTs except those with servo_v4 CCD. 150 # POWER_CONTROL_CCD: Used in power_off/on/cycle() methods, default for all 151 # DUTs with servo_v4 CCD. 152 # POWER_CONTROL_SERVO: Used in set_power() and power_cycle() methods. 153 # POWER_CONTROL_MANUAL: Used in set_power() and power_cycle() methods. 154 POWER_CONTROL_RPM = 'RPM' 155 POWER_CONTROL_CCD = 'CCD' 156 POWER_CONTROL_SERVO = 'servoj10' 157 POWER_CONTROL_MANUAL = 'manual' 158 159 POWER_CONTROL_VALID_ARGS = (POWER_CONTROL_RPM, 160 POWER_CONTROL_CCD, 161 POWER_CONTROL_SERVO, 162 POWER_CONTROL_MANUAL) 163 164 _RPM_OUTLET_CHANGED = 'outlet_changed' 165 166 # URL pattern to download firmware image. 167 _FW_IMAGE_URL_PATTERN = CONFIG.get_config_value( 168 'CROS', 'firmware_url_pattern', type=str) 169 170 # Regular expression for extracting EC version string 171 _EC_REGEX = '(%s_\w*[-\.]\w*[-\.]\w*[-\.]\w*)' 172 173 # Regular expression for extracting BIOS version string 174 _BIOS_REGEX = '(%s\.\w*\.\w*\.\w*)' 175 176 # Command to update firmware located on DUT 177 _FW_UPDATE_CMD = 'chromeos-firmwareupdate --mode=recovery %s' 178 179 @staticmethod 180 def check_host(host, timeout=10): 181 """ 182 Check if the given host is a chrome-os host. 183 184 @param host: An ssh host representing a device. 185 @param timeout: The timeout for the run command. 186 187 @return: True if the host device is chromeos. 188 189 """ 190 try: 191 result = host.run( 192 'grep -q CHROMEOS /etc/lsb-release && ' 193 '! grep -q moblab /etc/lsb-release && ' 194 '! grep -q labstation /etc/lsb-release', 195 ignore_status=True, timeout=timeout) 196 if result.exit_status == 0: 197 lsb_release_content = host.run( 198 'grep CHROMEOS_RELEASE_BOARD /etc/lsb-release', 199 timeout=timeout).stdout 200 return not ( 201 lsbrelease_utils.is_jetstream( 202 lsb_release_content=lsb_release_content) or 203 lsbrelease_utils.is_gce_board( 204 lsb_release_content=lsb_release_content)) 205 206 except (error.AutoservRunError, error.AutoservSSHTimeout): 207 return False 208 209 return False 210 211 212 @staticmethod 213 def get_chameleon_arguments(args_dict): 214 """Extract chameleon options from `args_dict` and return the result. 215 216 Recommended usage: 217 ~~~~~~~~ 218 args_dict = utils.args_to_dict(args) 219 chameleon_args = hosts.CrosHost.get_chameleon_arguments(args_dict) 220 host = hosts.create_host(machine, chameleon_args=chameleon_args) 221 ~~~~~~~~ 222 223 @param args_dict Dictionary from which to extract the chameleon 224 arguments. 225 """ 226 chameleon_args = {key: args_dict[key] 227 for key in ('chameleon_host', 'chameleon_port') 228 if key in args_dict} 229 if 'chameleon_ssh_port' in args_dict: 230 chameleon_args['port'] = int(args_dict['chameleon_ssh_port']) 231 return chameleon_args 232 233 @staticmethod 234 def get_btpeer_arguments(args_dict): 235 """Extract btpeer options from `args_dict` and return the result. 236 237 This is used to parse details of Bluetooth peer. 238 Recommended usage: 239 ~~~~~~~~ 240 args_dict = utils.args_to_dict(args) 241 btpeer_args = hosts.CrosHost.get_btpeer_arguments(args_dict) 242 host = hosts.create_host(machine, btpeer_args=btpeer_args) 243 ~~~~~~~~ 244 245 @param args_dict: Dictionary from which to extract the btpeer 246 arguments. 247 """ 248 if 'btpeer_host_list' in args_dict: 249 result = [] 250 for btpeer in args_dict['btpeer_host_list'].split(','): 251 # IPv6 addresses including a port number should be enclosed in 252 # square brackets. 253 delimiter = ']:' if re.search(r':.*:', btpeer) else ':' 254 result.append({key: value for key,value in 255 zip(('btpeer_host','btpeer_port'), 256 btpeer.strip('[]').split(delimiter))}) 257 return result 258 else: 259 return {key: args_dict[key] 260 for key in ('btpeer_host', 'btpeer_port', 'btpeer_ssh_port') 261 if key in args_dict} 262 263 264 @staticmethod 265 def get_pdtester_arguments(args_dict): 266 """Extract chameleon options from `args_dict` and return the result. 267 268 Recommended usage: 269 ~~~~~~~~ 270 args_dict = utils.args_to_dict(args) 271 pdtester_args = hosts.CrosHost.get_pdtester_arguments(args_dict) 272 host = hosts.create_host(machine, pdtester_args=pdtester_args) 273 ~~~~~~~~ 274 275 @param args_dict Dictionary from which to extract the pdtester 276 arguments. 277 """ 278 return {key: args_dict[key] 279 for key in ('pdtester_host', 'pdtester_port') 280 if key in args_dict} 281 282 283 @staticmethod 284 def get_servo_arguments(args_dict): 285 """Extract servo options from `args_dict` and return the result. 286 287 Recommended usage: 288 ~~~~~~~~ 289 args_dict = utils.args_to_dict(args) 290 servo_args = hosts.CrosHost.get_servo_arguments(args_dict) 291 host = hosts.create_host(machine, servo_args=servo_args) 292 ~~~~~~~~ 293 294 @param args_dict Dictionary from which to extract the servo 295 arguments. 296 """ 297 servo_attrs = (servo_constants.SERVO_HOST_ATTR, 298 servo_constants.SERVO_PORT_ATTR, 299 servo_constants.SERVO_SERIAL_ATTR, 300 servo_constants.SERVO_BOARD_ATTR, 301 servo_constants.SERVO_MODEL_ATTR) 302 servo_args = {key: args_dict[key] 303 for key in servo_attrs 304 if key in args_dict} 305 return ( 306 None 307 if servo_constants.SERVO_HOST_ATTR in servo_args 308 and not servo_args[servo_constants.SERVO_HOST_ATTR] 309 else servo_args) 310 311 312 def _initialize(self, hostname, chameleon_args=None, servo_args=None, 313 pdtester_args=None, try_lab_servo=False, 314 try_servo_repair=False, ssh_verbosity_flag='', 315 ssh_options='', *args, **dargs): 316 """Initialize superclasses, |self.chameleon|, and |self.servo|. 317 318 This method will attempt to create the test-assistant object 319 (chameleon/servo) when it is needed by the test. Check 320 the docstring of chameleon_host.create_chameleon_host and 321 servo_host.create_servo_host for how this is determined. 322 323 @param hostname: Hostname of the dut. 324 @param chameleon_args: A dictionary that contains args for creating 325 a ChameleonHost. See chameleon_host for details. 326 @param servo_args: A dictionary that contains args for creating 327 a ServoHost object. See servo_host for details. 328 @param try_lab_servo: When true, indicates that an attempt should 329 be made to create a ServoHost for a DUT in 330 the test lab, even if not required by 331 `servo_args`. See servo_host for details. 332 @param try_servo_repair: If a servo host is created, check it 333 with `repair()` rather than `verify()`. 334 See servo_host for details. 335 @param ssh_verbosity_flag: String, to pass to the ssh command to control 336 verbosity. 337 @param ssh_options: String, other ssh options to pass to the ssh 338 command. 339 """ 340 super(CrosHost, self)._initialize(hostname=hostname, 341 *args, **dargs) 342 self._repair_strategy = cros_repair.create_cros_repair_strategy() 343 # hold special dut_state for repair process 344 self._device_repair_state = None 345 self.labels = base_label.LabelRetriever(cros_label.CROS_LABELS) 346 # self.env is a dictionary of environment variable settings 347 # to be exported for commands run on the host. 348 # LIBC_FATAL_STDERR_ can be useful for diagnosing certain 349 # errors that might happen. 350 self.env['LIBC_FATAL_STDERR_'] = '1' 351 self._ssh_verbosity_flag = ssh_verbosity_flag 352 self._ssh_options = ssh_options 353 self.health_profile = None 354 self._default_power_method = None 355 dut_health_profile = device_health_profile.DeviceHealthProfile( 356 hostname=self.hostname, 357 host_info=self.host_info_store.get(), 358 result_dir=self.get_result_dir()) 359 360 # TODO(otabek@): remove when b/171414073 closed 361 pingable_before_servo = self.is_up_fast(count=3) 362 if pingable_before_servo: 363 logging.info('DUT is pingable before init Servo.') 364 _servo_host, servo_state = servo_host.create_servo_host( 365 dut=self, 366 servo_args=servo_args, 367 try_lab_servo=try_lab_servo, 368 try_servo_repair=try_servo_repair, 369 dut_host_info=self.host_info_store.get(), 370 dut_health_profile=dut_health_profile) 371 if dut_health_profile.is_loaded(): 372 logging.info('Device health profile loaded.') 373 # The device profile is located in the servo_host which make it 374 # dependency. If profile is not loaded yet then we do not have it 375 # TODO(otabek@) persist device provide out of servo-host. 376 self.health_profile = dut_health_profile 377 self.set_servo_host(_servo_host, servo_state) 378 379 # TODO(otabek@): remove when b/171414073 closed 380 # Introduced to collect cases when servo made DUT not sshable 381 pingable_after_servo = self.is_up_fast(count=3) 382 if pingable_after_servo: 383 logging.info('DUT is pingable after init Servo.') 384 elif pingable_before_servo: 385 logging.info('DUT was pingable before init Servo but not now') 386 if servo_args and self._servo_host and self._servo_host.hostname: 387 # collect stats only for tests. 388 dut_ping_servo_init_data = { 389 'host': self.hostname, 390 'servo_host': self._servo_host.hostname, 391 } 392 metrics.Counter('chromeos/autotest/dut_ping_servo_init2' 393 ).increment(fields=dut_ping_servo_init_data) 394 395 # TODO(waihong): Do the simplication on Chameleon too. 396 self._chameleon_host = chameleon_host.create_chameleon_host( 397 dut=self.hostname, 398 chameleon_args=chameleon_args) 399 if self._chameleon_host: 400 self.chameleon = self._chameleon_host.create_chameleon_board() 401 else: 402 self.chameleon = None 403 404 # Bluetooth peers will be populated by the test if needed 405 self._btpeer_host_list = [] 406 self.btpeer_list = [] 407 self.btpeer = None 408 409 # Add pdtester host if pdtester args were added on command line 410 self._pdtester_host = pdtester_host.create_pdtester_host( 411 pdtester_args, self._servo_host) 412 413 if self._pdtester_host: 414 self.pdtester_servo = self._pdtester_host.get_servo() 415 logging.info('pdtester_servo: %r', self.pdtester_servo) 416 # Create the pdtester object used to access the ec uart 417 self.pdtester = pdtester.PDTester(self.pdtester_servo, 418 self._pdtester_host.get_servod_server_proxy()) 419 else: 420 self.pdtester = None 421 422 423 def initialize_btpeer(self, btpeer_args=[]): 424 """ Initialize the Bluetooth peers 425 426 Initialize Bluetooth peer devices given in the arguments. Bluetooth peer 427 is chameleon host on Raspberry Pi. 428 @param btpeer_args: A dictionary that contains args for creating 429 a ChameleonHost. See chameleon_host for details. 430 431 """ 432 logging.debug('Attempting to initialize bluetooth peers if available') 433 try: 434 if type(btpeer_args) is list: 435 btpeer_args_list = btpeer_args 436 else: 437 btpeer_args_list = [btpeer_args] 438 439 self._btpeer_host_list = chameleon_host.create_btpeer_host( 440 dut=self.hostname, btpeer_args_list=btpeer_args_list) 441 logging.debug('Bluetooth peer hosts are %s', 442 self._btpeer_host_list) 443 self.btpeer_list = [_host.create_chameleon_board() for _host in 444 self._btpeer_host_list if _host is not None] 445 446 if len(self.btpeer_list) > 0: 447 self.btpeer = self.btpeer_list[0] 448 449 logging.debug('After initialize_btpeer btpeer_list %s ' 450 'btpeer_host_list is %s and btpeer is %s', 451 self.btpeer_list, self._btpeer_host_list, 452 self.btpeer) 453 except Exception as e: 454 logging.error('Exception %s in initialize_btpeer', str(e)) 455 456 457 458 def get_cros_repair_image_name(self): 459 """Get latest stable cros image name from AFE. 460 461 Use the board name from the info store. Should that fail, try to 462 retrieve the board name from the host's installed image itself. 463 464 @returns: current stable cros image name for this host. 465 """ 466 info = self.host_info_store.get() 467 if not info.board: 468 logging.warn('No board label value found. Trying to infer ' 469 'from the host itself.') 470 try: 471 info.labels.append(self.get_board()) 472 except (error.AutoservRunError, error.AutoservSSHTimeout) as e: 473 logging.error('Also failed to get the board name from the DUT ' 474 'itself. %s.', str(e)) 475 raise error.AutoservError('Cannot determine board of the DUT' 476 ' while getting repair image name.') 477 return afe_utils.get_stable_cros_image_name_v2(info) 478 479 480 def host_version_prefix(self, image): 481 """Return version label prefix. 482 483 In case the CrOS provisioning version is something other than the 484 standard CrOS version e.g. CrOS TH version, this function will 485 find the prefix from provision.py. 486 487 @param image: The image name to find its version prefix. 488 @returns: A prefix string for the image type. 489 """ 490 return provision.get_version_label_prefix(image) 491 492 def stage_build_to_usb(self, build): 493 """Stage the current ChromeOS image on the USB stick connected to the 494 servo. 495 496 @param build: The build to download and send to USB. 497 """ 498 if not self.servo: 499 raise error.TestError('Host %s does not have servo.' % 500 self.hostname) 501 502 _, update_url = self.stage_image_for_servo(build) 503 504 try: 505 self.servo.image_to_servo_usb(update_url) 506 finally: 507 # servo.image_to_servo_usb turned the DUT off, so turn it back on 508 logging.debug('Turn DUT power back on.') 509 self.servo.get_power_state_controller().power_on() 510 511 logging.debug('ChromeOS image %s is staged on the USB stick.', 512 build) 513 514 def verify_job_repo_url(self, tag=''): 515 """ 516 Make sure job_repo_url of this host is valid. 517 518 Eg: The job_repo_url "http://lmn.cd.ab.xyx:8080/static/\ 519 lumpy-release/R29-4279.0.0/autotest/packages" claims to have the 520 autotest package for lumpy-release/R29-4279.0.0. If this isn't the case, 521 download and extract it. If the devserver embedded in the url is 522 unresponsive, update the job_repo_url of the host after staging it on 523 another devserver. 524 525 @param job_repo_url: A url pointing to the devserver where the autotest 526 package for this build should be staged. 527 @param tag: The tag from the server job, in the format 528 <job_id>-<user>/<hostname>, or <hostless> for a server job. 529 530 @raises DevServerException: If we could not resolve a devserver. 531 @raises AutoservError: If we're unable to save the new job_repo_url as 532 a result of choosing a new devserver because the old one failed to 533 respond to a health check. 534 @raises urllib2.URLError: If the devserver embedded in job_repo_url 535 doesn't respond within the timeout. 536 """ 537 info = self.host_info_store.get() 538 job_repo_url = info.attributes.get(ds_constants.JOB_REPO_URL, '') 539 if not job_repo_url: 540 logging.warning('No job repo url set on host %s', self.hostname) 541 return 542 543 logging.info('Verifying job repo url %s', job_repo_url) 544 devserver_url, image_name = tools.get_devserver_build_from_package_url( 545 job_repo_url) 546 547 ds = dev_server.ImageServer(devserver_url) 548 549 logging.info('Staging autotest artifacts for %s on devserver %s', 550 image_name, ds.url()) 551 552 start_time = time.time() 553 ds.stage_artifacts(image_name, ['autotest_packages']) 554 stage_time = time.time() - start_time 555 556 # Record how much of the verification time comes from a devserver 557 # restage. If we're doing things right we should not see multiple 558 # devservers for a given board/build/branch path. 559 try: 560 board, build_type, branch = server_utils.ParseBuildName( 561 image_name)[:3] 562 except server_utils.ParseBuildNameException: 563 pass 564 else: 565 devserver = devserver_url[ 566 devserver_url.find('/') + 2:devserver_url.rfind(':')] 567 stats_key = { 568 'board': board, 569 'build_type': build_type, 570 'branch': branch, 571 'devserver': devserver.replace('.', '_'), 572 } 573 574 monarch_fields = { 575 'board': board, 576 'build_type': build_type, 577 'branch': branch, 578 'dev_server': devserver, 579 } 580 metrics.Counter( 581 'chromeos/autotest/provision/verify_url' 582 ).increment(fields=monarch_fields) 583 metrics.SecondsDistribution( 584 'chromeos/autotest/provision/verify_url_duration' 585 ).add(stage_time, fields=monarch_fields) 586 587 588 def stage_server_side_package(self, image=None): 589 """Stage autotest server-side package on devserver. 590 591 @param image: Full path of an OS image to install or a build name. 592 593 @return: A url to the autotest server-side package. 594 595 @raise: error.AutoservError if fail to locate the build to test with, or 596 fail to stage server-side package. 597 """ 598 # If enable_drone_in_restricted_subnet is False, do not set hostname 599 # in devserver.resolve call, so a devserver in non-restricted subnet 600 # is picked to stage autotest server package for drone to download. 601 hostname = self.hostname 602 if not server_utils.ENABLE_DRONE_IN_RESTRICTED_SUBNET: 603 hostname = None 604 if image: 605 image_name = tools.get_build_from_image(image) 606 if not image_name: 607 raise error.AutoservError( 608 'Failed to parse build name from %s' % image) 609 ds = dev_server.ImageServer.resolve(image_name, hostname) 610 else: 611 info = self.host_info_store.get() 612 job_repo_url = info.attributes.get(ds_constants.JOB_REPO_URL, '') 613 if job_repo_url: 614 devserver_url, image_name = ( 615 tools.get_devserver_build_from_package_url(job_repo_url)) 616 # If enable_drone_in_restricted_subnet is True, use the 617 # existing devserver. Otherwise, resolve a new one in 618 # non-restricted subnet. 619 if server_utils.ENABLE_DRONE_IN_RESTRICTED_SUBNET: 620 ds = dev_server.ImageServer(devserver_url) 621 else: 622 ds = dev_server.ImageServer.resolve(image_name) 623 elif info.build is not None: 624 ds = dev_server.ImageServer.resolve(info.build, hostname) 625 image_name = info.build 626 else: 627 raise error.AutoservError( 628 'Failed to stage server-side package. The host has ' 629 'no job_repo_url attribute or cros-version label.') 630 631 # Get the OS version of the build, for any build older than 632 # MIN_VERSION_SUPPORT_SSP, server side packaging is not supported. 633 match = re.match('.*/R\d+-(\d+)\.', image_name) 634 if match and int(match.group(1)) < self.MIN_VERSION_SUPPORT_SSP: 635 raise error.AutoservError( 636 'Build %s is older than %s. Server side packaging is ' 637 'disabled.' % (image_name, self.MIN_VERSION_SUPPORT_SSP)) 638 639 ds.stage_artifacts(image_name, ['autotest_server_package']) 640 return '%s/static/%s/%s' % (ds.url(), image_name, 641 'autotest_server_package.tar.bz2') 642 643 644 def stage_image_for_servo(self, image_name=None, artifact='test_image'): 645 """Stage a build on a devserver and return the update_url. 646 647 @param image_name: a name like lumpy-release/R27-3837.0.0 648 @param artifact: a string like 'test_image'. Requests 649 appropriate image to be staged. 650 @returns a tuple of (image_name, URL) like 651 (lumpy-release/R27-3837.0.0, 652 http://172.22.50.205:8082/update/lumpy-release/R27-3837.0.0) 653 """ 654 if not image_name: 655 image_name = self.get_cros_repair_image_name() 656 logging.info('Staging build for servo install: %s', image_name) 657 devserver = dev_server.ImageServer.resolve(image_name, self.hostname) 658 devserver.stage_artifacts(image_name, [artifact]) 659 if artifact == 'test_image': 660 return image_name, devserver.get_test_image_url(image_name) 661 elif artifact == 'recovery_image': 662 return image_name, devserver.get_recovery_image_url(image_name) 663 else: 664 raise error.AutoservError("Bad artifact!") 665 666 667 def stage_factory_image_for_servo(self, image_name): 668 """Stage a build on a devserver and return the update_url. 669 670 @param image_name: a name like <baord>/4262.204.0 671 672 @return: An update URL, eg: 673 http://<devserver>/static/canary-channel/\ 674 <board>/4262.204.0/factory_test/chromiumos_factory_image.bin 675 676 @raises: ValueError if the factory artifact name is missing from 677 the config. 678 679 """ 680 if not image_name: 681 logging.error('Need an image_name to stage a factory image.') 682 return 683 684 factory_artifact = CONFIG.get_config_value( 685 'CROS', 'factory_artifact', type=str, default='') 686 if not factory_artifact: 687 raise ValueError('Cannot retrieve the factory artifact name from ' 688 'autotest config, and hence cannot stage factory ' 689 'artifacts.') 690 691 logging.info('Staging build for servo install: %s', image_name) 692 devserver = dev_server.ImageServer.resolve(image_name, self.hostname) 693 devserver.stage_artifacts( 694 image_name, 695 [factory_artifact], 696 archive_url=None) 697 698 return tools.factory_image_url_pattern() % (devserver.url(), image_name) 699 700 701 def prepare_for_update(self): 702 """Prepares the DUT for an update. 703 704 Subclasses may override this to perform any special actions 705 required before updating. 706 """ 707 pass 708 709 710 def _clear_fw_version_labels(self, rw_only): 711 """Clear firmware version labels from the machine. 712 713 @param rw_only: True to only clear fwrw_version; otherewise, clear 714 both fwro_version and fwrw_version. 715 """ 716 info = self.host_info_store.get() 717 info.clear_version_labels(provision.FW_RW_VERSION_PREFIX) 718 if not rw_only: 719 info.clear_version_labels(provision.FW_RO_VERSION_PREFIX) 720 self.host_info_store.commit(info) 721 722 723 def _add_fw_version_label(self, build, rw_only): 724 """Add firmware version label to the machine. 725 726 @param build: Build of firmware. 727 @param rw_only: True to only add fwrw_version; otherwise, add both 728 fwro_version and fwrw_version. 729 730 """ 731 info = self.host_info_store.get() 732 info.set_version_label(provision.FW_RW_VERSION_PREFIX, build) 733 if not rw_only: 734 info.set_version_label(provision.FW_RO_VERSION_PREFIX, build) 735 self.host_info_store.commit(info) 736 737 738 def get_latest_release_version(self, platform, ref_board=None): 739 """Search for the latest package release version from the image archive, 740 and return it. 741 742 @param platform: platform name, a.k.a. board or model 743 @param ref_board: reference board name, a.k.a. baseboard, parent 744 745 @return 'firmware-{platform}-{branch}-firmwarebranch/{release-version}/' 746 '{platform}' 747 or None if LATEST release file does not exist. 748 """ 749 750 platforms = [ platform ] 751 752 # Search the image path in reference board archive as well. 753 # For example, bob has its binary image under its reference board (gru) 754 # image archive. 755 if ref_board: 756 platforms.append(ref_board) 757 758 for board in platforms: 759 # Read 'LATEST-1.0.0' file 760 branch_dir = provision.FW_BRANCH_GLOB % board 761 latest_file = os.path.join(provision.CROS_IMAGE_ARCHIVE, branch_dir, 762 'LATEST-1.0.0') 763 764 try: 765 # The result could be one or more. 766 result = utils.system_output('gsutil ls -d ' + latest_file) 767 768 candidates = re.findall('gs://.*', result) 769 770 # Found the directory candidates. No need to check the other 771 # board name cadidates. Let's break the loop. 772 break 773 except error.CmdError: 774 # It doesn't exist. Let's move on to the next item. 775 pass 776 else: 777 logging.error('No LATEST release info is available.') 778 return None 779 780 for cand_dir in candidates: 781 result = utils.system_output('gsutil cat ' + cand_dir) 782 783 release_path = cand_dir.replace('LATEST-1.0.0', result) 784 release_path = os.path.join(release_path, platform) 785 try: 786 # Check if release_path does exist. 787 release = utils.system_output('gsutil ls -d ' + release_path) 788 # Now 'release' has a full directory path: e.g. 789 # gs://chromeos-image-archive/firmware-octopus-11297.B- 790 # firmwarebranch/RNone-1.0.0-b4395530/octopus/ 791 792 # Remove "gs://chromeos-image-archive". 793 release = release.replace(provision.CROS_IMAGE_ARCHIVE, '') 794 795 # Remove CROS_IMAGE_ARCHIVE and any surrounding '/'s. 796 return release.strip('/') 797 except error.CmdError: 798 # The directory might not exist. Let's try next candidate. 799 pass 800 else: 801 raise error.AutoservError('Cannot find the latest firmware') 802 803 @staticmethod 804 def get_version_from_image(image, version_regex): 805 """Get version string from binary image using regular expression. 806 807 @param image: Binary image to search 808 @param version_regex: Regular expression to search for 809 810 @return Version string 811 812 @raises TestFail if no version string is found in image 813 """ 814 with open(image, 'rb') as f: 815 image_data = f.read() 816 match = re.findall(version_regex, 817 image_data.decode('ISO-8859-1', errors='ignore')) 818 if match: 819 return match[0] 820 else: 821 raise error.TestFail('Failed to read version from %s.' % image) 822 823 824 def firmware_install(self, build, rw_only=False, dest=None, 825 local_tarball=None, verify_version=False, 826 try_scp=False, install_ec=True, install_bios=True, 827 board_as=None): 828 """Install firmware to the DUT. 829 830 Use stateful update if the DUT is already running the same build. 831 Stateful update does not update kernel and tends to run much faster 832 than a full reimage. If the DUT is running a different build, or it 833 failed to do a stateful update, full update, including kernel update, 834 will be applied to the DUT. 835 836 Once a host enters firmware_install its fw[ro|rw]_version label will 837 be removed. After the firmware is updated successfully, a new 838 fw[ro|rw]_version label will be added to the host. 839 840 @param build: The build version to which we want to provision the 841 firmware of the machine, 842 e.g. 'link-firmware/R22-2695.1.144'. 843 @param rw_only: True to only install firmware to its RW portions. Keep 844 the RO portions unchanged. 845 @param dest: Directory to store the firmware in. 846 @param local_tarball: Path to local firmware image for installing 847 without devserver. 848 @param verify_version: True to verify EC and BIOS versions after 849 programming firmware, default is False. 850 @param try_scp: False to always program using servo, true to try copying 851 the firmware and programming from the DUT. 852 @param install_ec: True to install EC FW, and False to skip it. 853 @param install_bios: True to install BIOS, and False to skip it. 854 @param board_as: A board name to force to use. 855 856 TODO(dshi): After bug 381718 is fixed, update here with corresponding 857 exceptions that could be raised. 858 859 """ 860 if not self.servo: 861 raise error.TestError('Host %s does not have servo.' % 862 self.hostname) 863 864 # Get the DUT board name from AFE. 865 info = self.host_info_store.get() 866 board = info.board 867 model = info.model 868 869 if board is None or board == '': 870 board = self.servo.get_board() 871 872 # if board_as argument is passed, then use it instead of the original 873 # board name. 874 if board_as: 875 board = board_as 876 877 if model is None or model == '': 878 try: 879 model = self.get_platform() 880 except Exception as e: 881 logging.warn('Dut is unresponsive: %s', str(e)) 882 883 # If local firmware path not provided fetch it from the dev server 884 tmpd = None 885 if not local_tarball: 886 logging.info('Will install firmware from build %s.', build) 887 888 try: 889 ds = dev_server.ImageServer.resolve(build, self.hostname) 890 ds.stage_artifacts(build, ['firmware']) 891 892 if not dest: 893 tmpd = autotemp.tempdir(unique_id='fwimage') 894 dest = tmpd.name 895 896 # Download firmware image 897 fwurl = self._FW_IMAGE_URL_PATTERN % (ds.url(), build) 898 local_tarball = os.path.join(dest, os.path.basename(fwurl)) 899 ds.download_file(fwurl, local_tarball) 900 except Exception as e: 901 raise error.TestError('Failed to download firmware package: %s' 902 % str(e)) 903 904 ec_image = None 905 if install_ec: 906 # Extract EC image from tarball 907 logging.info('Extracting EC image.') 908 ec_image = self.servo.extract_ec_image(board, model, local_tarball) 909 logging.info('Extracted: %s', ec_image) 910 911 bios_image = None 912 if install_bios: 913 # Extract BIOS image from tarball 914 logging.info('Extracting BIOS image.') 915 bios_image = self.servo.extract_bios_image(board, model, 916 local_tarball) 917 logging.info('Extracted: %s', bios_image) 918 919 if not bios_image and not ec_image: 920 raise error.TestError('No firmware installation was processed.') 921 922 # Clear firmware version labels 923 self._clear_fw_version_labels(rw_only) 924 925 # Install firmware from local tarball 926 try: 927 # Check if copying to DUT is enabled and DUT is available 928 if try_scp and self.is_up(): 929 # DUT is available, make temp firmware directory to store images 930 logging.info('Making temp folder.') 931 dest_folder = '/tmp/firmware' 932 self.run('mkdir -p ' + dest_folder) 933 934 fw_cmd = self._FW_UPDATE_CMD % ('--wp=1' if rw_only else '') 935 936 if bios_image: 937 # Send BIOS firmware image to DUT 938 logging.info('Sending BIOS firmware.') 939 dest_bios_path = os.path.join(dest_folder, 940 os.path.basename(bios_image)) 941 self.send_file(bios_image, dest_bios_path) 942 943 # Initialize firmware update command for BIOS image 944 fw_cmd += ' -i %s' % dest_bios_path 945 946 # Send EC firmware image to DUT when EC image was found 947 if ec_image: 948 logging.info('Sending EC firmware.') 949 dest_ec_path = os.path.join(dest_folder, 950 os.path.basename(ec_image)) 951 self.send_file(ec_image, dest_ec_path) 952 953 # Add EC image to firmware update command 954 fw_cmd += ' -e %s' % dest_ec_path 955 956 # Make sure command is allowed to finish even if ssh fails. 957 fw_cmd = "trap '' SIGHUP; %s" % fw_cmd 958 959 # Update firmware on DUT 960 logging.info('Updating firmware.') 961 try: 962 self.run(fw_cmd, options="-o LogLevel=verbose") 963 except error.AutoservRunError as e: 964 if e.result_obj.exit_status != 255: 965 raise 966 elif ec_image: 967 logging.warn("DUT network dropped during update" 968 " (often caused by EC resetting USB)") 969 else: 970 logging.error("DUT network dropped during update" 971 " (unexpected, since no EC image)") 972 raise 973 else: 974 # Host is not available, program firmware using servo 975 if ec_image: 976 self.servo.program_ec(ec_image, rw_only) 977 if bios_image: 978 self.servo.program_bios(bios_image, rw_only) 979 if utils.host_is_in_lab_zone(self.hostname): 980 self._add_fw_version_label(build, rw_only) 981 982 # Reboot and wait for DUT after installing firmware 983 logging.info('Rebooting DUT.') 984 self.servo.get_power_state_controller().reset() 985 time.sleep(self.servo.BOOT_DELAY) 986 self.test_wait_for_boot() 987 988 # When enabled verify EC and BIOS firmware version after programming 989 if verify_version: 990 # Check programmed EC firmware when EC image was found 991 if ec_image: 992 logging.info('Checking EC firmware version.') 993 dest_ec_version = self.get_ec_version() 994 ec_version_prefix = dest_ec_version.split('_', 1)[0] 995 ec_regex = self._EC_REGEX % ec_version_prefix 996 image_ec_version = self.get_version_from_image(ec_image, 997 ec_regex) 998 if dest_ec_version != image_ec_version: 999 raise error.TestFail( 1000 'Failed to update EC firmware, version %s ' 1001 '(expected %s)' % (dest_ec_version, 1002 image_ec_version)) 1003 1004 if bios_image: 1005 # Check programmed BIOS firmware against expected version 1006 logging.info('Checking BIOS firmware version.') 1007 dest_bios_version = self.get_firmware_version() 1008 bios_version_prefix = dest_bios_version.split('.', 1)[0] 1009 bios_regex = self._BIOS_REGEX % bios_version_prefix 1010 image_bios_version = self.get_version_from_image(bios_image, 1011 bios_regex) 1012 if dest_bios_version != image_bios_version: 1013 raise error.TestFail( 1014 'Failed to update BIOS, version %s ' 1015 '(expected %s)' % (dest_bios_version, 1016 image_bios_version)) 1017 finally: 1018 if tmpd: 1019 tmpd.clean() 1020 1021 1022 def servo_install(self, 1023 image_url=None, 1024 usb_boot_timeout=USB_BOOT_TIMEOUT, 1025 install_timeout=INSTALL_TIMEOUT, 1026 is_repair=False): 1027 """ 1028 Re-install the OS on the DUT by: 1029 1) installing a test image on a USB storage device attached to the Servo 1030 board, 1031 2) booting that image in recovery mode, and then 1032 3) installing the image with chromeos-install. 1033 1034 @param image_url: If specified use as the url to install on the DUT. 1035 otherwise boot the currently staged image on the USB stick. 1036 @param usb_boot_timeout: The usb_boot_timeout to use during reimage. 1037 Factory images need a longer usb_boot_timeout than regular 1038 cros images. 1039 @param install_timeout: The timeout to use when installing the chromeos 1040 image. Factory images need a longer install_timeout. 1041 @param is_repair: Indicates if the method is called from a repair task. 1042 1043 @raises AutoservError if the image fails to boot. 1044 1045 """ 1046 if image_url: 1047 logging.info('Downloading image to USB, then booting from it.' 1048 ' Usb boot timeout = %s', usb_boot_timeout) 1049 else: 1050 logging.info('Booting from USB directly. Usb boot timeout = %s', 1051 usb_boot_timeout) 1052 1053 metrics_field = {'download': bool(image_url)} 1054 metrics.Counter( 1055 'chromeos/autotest/provision/servo_install/download_image' 1056 ).increment(fields=metrics_field) 1057 1058 with metrics.SecondsTimer( 1059 'chromeos/autotest/provision/servo_install/boot_duration'): 1060 self.servo._power_state.power_off() 1061 try: 1062 self.servo.image_to_servo_usb(image_path=image_url, 1063 power_off_dut=False) 1064 except error.AutotestError as e: 1065 metrics.Counter('chromeos/autotest/repair/image_to_usb_error' 1066 ).increment( 1067 fields={'host': self.hostname or ''}) 1068 six.reraise(error.AutotestError, str(e), sys.exc_info()[2]) 1069 # Give the DUT some time to power_off if we skip 1070 # download image to usb. (crbug.com/982993) 1071 if not image_url: 1072 time.sleep(10) 1073 need_snk = self.require_snk_mode_in_recovery() 1074 self.servo.boot_in_recovery_mode(snk_mode=need_snk) 1075 if not self.wait_up(timeout=usb_boot_timeout): 1076 if need_snk: 1077 # Attempt to restore servo_v4 role to 'src' mode. 1078 self.servo.set_servo_v4_role('src') 1079 raise hosts.AutoservRepairError( 1080 'DUT failed to boot from USB after %d seconds' % 1081 usb_boot_timeout, 'failed_to_boot_pre_install') 1082 1083 # Make sure the DUT is boot from an external device. 1084 if not self.is_boot_from_external_device(): 1085 raise hosts.AutoservRepairError( 1086 'DUT is expected to boot from an external device(e.g. ' 1087 'a usb stick), however it seems still boot from an' 1088 ' internal storage.', 'boot_from_internal_storage') 1089 1090 # The new chromeos-tpm-recovery has been merged since R44-7073.0.0. 1091 # In old CrOS images, this command fails. Skip the error. 1092 logging.info('Resetting the TPM status') 1093 try: 1094 self.run('chromeos-tpm-recovery') 1095 except error.AutoservRunError: 1096 logging.warn('chromeos-tpm-recovery is too old.') 1097 1098 1099 with metrics.SecondsTimer( 1100 'chromeos/autotest/provision/servo_install/install_duration'): 1101 logging.info('Installing image through chromeos-install.') 1102 try: 1103 self.run('chromeos-install --yes',timeout=install_timeout) 1104 self.halt() 1105 except Exception as e: 1106 storage_errors = [ 1107 'No space left on device', 1108 'I/O error when trying to write primary GPT', 1109 'Input/output error while writing out', 1110 'cannot read GPT header', 1111 'can not determine destination device', 1112 'wrong fs type', 1113 'bad superblock on', 1114 ] 1115 has_error = [msg for msg in storage_errors if(msg in str(e))] 1116 if has_error: 1117 info = self.host_info_store.get() 1118 info.set_version_label( 1119 audit_const.DUT_STORAGE_STATE_PREFIX, 1120 audit_const.HW_STATE_NEED_REPLACEMENT) 1121 self.host_info_store.commit(info) 1122 self.set_device_repair_state( 1123 cros_constants.DEVICE_STATE_NEEDS_REPLACEMENT) 1124 logging.debug( 1125 'Fail install image from USB; Storage error; %s', e) 1126 raise error.AutoservError( 1127 'Failed to install image from USB due to a suspect ' 1128 'disk failure, DUT storage state changed to ' 1129 'need_replacement, please check debug log ' 1130 'for details.') 1131 else: 1132 if is_repair: 1133 # DUT will be marked for replacement if storage is bad. 1134 audit_verify.VerifyDutStorage(self).verify() 1135 1136 logging.debug('Fail install image from USB; %s', e) 1137 raise error.AutoservError( 1138 'Failed to install image from USB due to unexpected ' 1139 'error, please check debug log for details.') 1140 finally: 1141 # We need reset the DUT no matter re-install success or not, 1142 # as we don't want leave the DUT in boot from usb state. 1143 logging.info('Power cycling DUT through servo.') 1144 self.servo.get_power_state_controller().power_off() 1145 self.servo.switch_usbkey('off') 1146 if need_snk: 1147 # Attempt to restore servo_v4 role to 'src' mode. 1148 self.servo.set_servo_v4_role('src') 1149 # N.B. The Servo API requires that we use power_on() here 1150 # for two reasons: 1151 # 1) After turning on a DUT in recovery mode, you must turn 1152 # it off and then on with power_on() once more to 1153 # disable recovery mode (this is a Parrot specific 1154 # requirement). 1155 # 2) After power_off(), the only way to turn on is with 1156 # power_on() (this is a Storm specific requirement). 1157 self.servo.get_power_state_controller().power_on() 1158 1159 logging.info('Waiting for DUT to come back up.') 1160 if not self.wait_up(timeout=self.BOOT_TIMEOUT): 1161 raise hosts.AutoservRepairError('DUT failed to reboot installed ' 1162 'test image after %d seconds' % 1163 self.BOOT_TIMEOUT, 1164 'failed_to_boot_post_install') 1165 1166 1167 def set_servo_host(self, host, servo_state=None): 1168 """Set our servo host member, and associated servo. 1169 1170 @param host Our new `ServoHost`. 1171 """ 1172 self._servo_host = host 1173 self.servo_pwr_supported = None 1174 if self._servo_host is not None: 1175 self.servo = self._servo_host.get_servo() 1176 servo_state = self._servo_host.get_servo_state() 1177 self._set_smart_usbhub_label(self._servo_host.smart_usbhub) 1178 try: 1179 self.servo_pwr_supported = self.servo.has_control('power_state') 1180 except Exception as e: 1181 logging.debug( 1182 "Could not get servo power state due to {}".format(e)) 1183 else: 1184 self.servo = None 1185 self.servo_pwr_supported = False 1186 self.set_servo_type() 1187 self.set_servo_state(servo_state) 1188 self._set_servo_topology() 1189 1190 1191 def repair_servo(self): 1192 """ 1193 Confirm that servo is initialized and verified. 1194 1195 If the servo object is missing, attempt to repair the servo 1196 host. Repair failures are passed back to the caller. 1197 1198 @raise AutoservError: If there is no servo host for this CrOS 1199 host. 1200 """ 1201 if self.servo: 1202 return 1203 if not self._servo_host: 1204 raise error.AutoservError('No servo host for %s.' % 1205 self.hostname) 1206 try: 1207 self._servo_host.repair() 1208 except: 1209 raise 1210 finally: 1211 self.set_servo_host(self._servo_host) 1212 1213 1214 def set_servo_type(self): 1215 """Set servo info labels to dut host_info""" 1216 if not self.servo: 1217 logging.debug('Servo is not initialized to get servo_type.') 1218 return 1219 servo_type = self.servo.get_servo_type() 1220 if not servo_type: 1221 logging.debug('Cannot collect servo_type from servo' 1222 ' by `dut-control servo_type`! Please file a bug' 1223 ' and inform infra team as we are not expected ' 1224 ' to reach this point.') 1225 return 1226 host_info = self.host_info_store.get() 1227 prefix = servo_constants.SERVO_TYPE_LABEL_PREFIX 1228 old_type = host_info.get_label_value(prefix) 1229 if old_type == servo_type: 1230 # do not need update 1231 return 1232 host_info.set_version_label(prefix, servo_type) 1233 self.host_info_store.commit(host_info) 1234 logging.info('ServoHost: servo_type updated to %s ' 1235 '(previous: %s)', servo_type, old_type) 1236 1237 1238 def set_servo_state(self, servo_state): 1239 """Set servo info labels to dut host_info""" 1240 if servo_state is not None: 1241 host_info = self.host_info_store.get() 1242 servo_state_prefix = servo_constants.SERVO_STATE_LABEL_PREFIX 1243 old_state = host_info.get_label_value(servo_state_prefix) 1244 if old_state == servo_state: 1245 # do not need update 1246 return 1247 host_info.set_version_label(servo_state_prefix, servo_state) 1248 self.host_info_store.commit(host_info) 1249 logging.info('ServoHost: servo_state updated to %s (previous: %s)', 1250 servo_state, old_state) 1251 1252 1253 def get_servo_state(self): 1254 host_info = self.host_info_store.get() 1255 servo_state_prefix = servo_constants.SERVO_STATE_LABEL_PREFIX 1256 return host_info.get_label_value(servo_state_prefix) 1257 1258 def is_servo_in_working_state(self): 1259 """Validate servo is in WORKING state.""" 1260 servo_state = self.get_servo_state() 1261 return servo_state == servo_constants.SERVO_STATE_WORKING 1262 1263 def get_servo_usb_state(self): 1264 """Get the label value indicating the health of the USB drive. 1265 1266 @return: The label value if defined, otherwise '' (empty string). 1267 @rtype: str 1268 """ 1269 host_info = self.host_info_store.get() 1270 servo_usb_state_prefix = audit_const.SERVO_USB_STATE_PREFIX 1271 return host_info.get_label_value(servo_usb_state_prefix) 1272 1273 def is_servo_usb_usable(self): 1274 """Check if the servo USB storage device is usable for FAFT. 1275 1276 @return: False if the label indicates a state that will break FAFT. 1277 True if state is okay, or if state is not defined. 1278 @rtype: bool 1279 """ 1280 usb_state = self.get_servo_usb_state() 1281 return usb_state in ('', audit_const.HW_STATE_ACCEPTABLE, 1282 audit_const.HW_STATE_NORMAL, 1283 audit_const.HW_STATE_UNKNOWN) 1284 1285 def _set_smart_usbhub_label(self, smart_usbhub_detected): 1286 if smart_usbhub_detected is None: 1287 # skip the label update here as this indicate we wasn't able 1288 # to confirm usbhub type. 1289 return 1290 host_info = self.host_info_store.get() 1291 if (smart_usbhub_detected == 1292 (servo_constants.SMART_USBHUB_LABEL in host_info.labels)): 1293 # skip label update if current label match the truth. 1294 return 1295 if smart_usbhub_detected: 1296 logging.info('Adding %s label to host %s', 1297 servo_constants.SMART_USBHUB_LABEL, 1298 self.hostname) 1299 host_info.labels.append(servo_constants.SMART_USBHUB_LABEL) 1300 else: 1301 logging.info('Removing %s label from host %s', 1302 servo_constants.SMART_USBHUB_LABEL, 1303 self.hostname) 1304 host_info.labels.remove(servo_constants.SMART_USBHUB_LABEL) 1305 self.host_info_store.commit(host_info) 1306 1307 def repair(self): 1308 """Attempt to get the DUT to pass `self.verify()`. 1309 1310 This overrides the base class function for repair; it does 1311 not call back to the parent class, but instead relies on 1312 `self._repair_strategy` to coordinate the verification and 1313 repair steps needed to get the DUT working. 1314 """ 1315 message = 'Beginning repair for host %s board %s model %s' 1316 info = self.host_info_store.get() 1317 message %= (self.hostname, info.board, info.model) 1318 self.record('INFO', None, None, message) 1319 profile_state = profile_constants.DUT_STATE_READY 1320 # Initialize bluetooth peers 1321 self.initialize_btpeer() 1322 try: 1323 self._repair_strategy.repair(self) 1324 except hosts.AutoservVerifyDependencyError as e: 1325 # TODO(otabek): remove when finish b/174191325 1326 self._stat_if_pingable_but_not_sshable() 1327 # We don't want flag a DUT as failed if only non-critical 1328 # verifier(s) failed during the repair. 1329 if e.is_critical(): 1330 profile_state = profile_constants.DUT_STATE_REPAIR_FAILED 1331 self._reboot_labstation_if_needed() 1332 self.try_set_device_needs_manual_repair() 1333 raise 1334 finally: 1335 self.set_health_profile_dut_state(profile_state) 1336 1337 def get_verifier_state(self, tag): 1338 """Return the state of servo verifier. 1339 1340 @returns: bool or None 1341 """ 1342 return self._repair_strategy.verifier_is_good(tag) 1343 1344 def close(self): 1345 """Close connection.""" 1346 super(CrosHost, self).close() 1347 1348 if self._chameleon_host: 1349 self._chameleon_host.close() 1350 1351 if self.health_profile: 1352 try: 1353 self.health_profile.close() 1354 except Exception as e: 1355 logging.warning( 1356 'Failed to finalize device health profile; %s', e) 1357 1358 if self._servo_host: 1359 self._servo_host.close() 1360 1361 def get_power_supply_info(self): 1362 """Get the output of power_supply_info. 1363 1364 power_supply_info outputs the info of each power supply, e.g., 1365 Device: Line Power 1366 online: no 1367 type: Mains 1368 voltage (V): 0 1369 current (A): 0 1370 Device: Battery 1371 state: Discharging 1372 percentage: 95.9276 1373 technology: Li-ion 1374 1375 Above output shows two devices, Line Power and Battery, with details of 1376 each device listed. This function parses the output into a dictionary, 1377 with key being the device name, and value being a dictionary of details 1378 of the device info. 1379 1380 @return: The dictionary of power_supply_info, e.g., 1381 {'Line Power': {'online': 'yes', 'type': 'main'}, 1382 'Battery': {'vendor': 'xyz', 'percentage': '100'}} 1383 @raise error.AutoservRunError if power_supply_info tool is not found in 1384 the DUT. Caller should handle this error to avoid false failure 1385 on verification. 1386 """ 1387 result = self.run('power_supply_info').stdout.strip() 1388 info = {} 1389 device_name = None 1390 device_info = {} 1391 for line in result.split('\n'): 1392 pair = [v.strip() for v in line.split(':')] 1393 if len(pair) != 2: 1394 continue 1395 if pair[0] == 'Device': 1396 if device_name: 1397 info[device_name] = device_info 1398 device_name = pair[1] 1399 device_info = {} 1400 else: 1401 device_info[pair[0]] = pair[1] 1402 if device_name and not device_name in info: 1403 info[device_name] = device_info 1404 return info 1405 1406 1407 def get_battery_percentage(self): 1408 """Get the battery percentage. 1409 1410 @return: The percentage of battery level, value range from 0-100. Return 1411 None if the battery info cannot be retrieved. 1412 """ 1413 try: 1414 info = self.get_power_supply_info() 1415 logging.info(info) 1416 return float(info['Battery']['percentage']) 1417 except (KeyError, ValueError, error.AutoservRunError): 1418 return None 1419 1420 1421 def get_battery_state(self): 1422 """Get the battery charging state. 1423 1424 @return: A string representing the battery charging state. It can be 1425 'Charging', 'Fully charged', or 'Discharging'. 1426 """ 1427 try: 1428 info = self.get_power_supply_info() 1429 logging.info(info) 1430 return info['Battery']['state'] 1431 except (KeyError, ValueError, error.AutoservRunError): 1432 return None 1433 1434 1435 def get_battery_display_percentage(self): 1436 """Get the battery display percentage. 1437 1438 @return: The display percentage of battery level, value range from 1439 0-100. Return None if the battery info cannot be retrieved. 1440 """ 1441 try: 1442 info = self.get_power_supply_info() 1443 logging.info(info) 1444 return float(info['Battery']['display percentage']) 1445 except (KeyError, ValueError, error.AutoservRunError): 1446 return None 1447 1448 1449 def is_ac_connected(self): 1450 """Check if the dut has power adapter connected and charging. 1451 1452 @return: True if power adapter is connected and charging. 1453 """ 1454 try: 1455 info = self.get_power_supply_info() 1456 return info['Line Power']['online'] == 'yes' 1457 except (KeyError, error.AutoservRunError): 1458 return None 1459 1460 1461 def _cleanup_poweron(self): 1462 """Special cleanup method to make sure hosts always get power back.""" 1463 info = self.host_info_store.get() 1464 if self._RPM_OUTLET_CHANGED not in info.attributes: 1465 return 1466 logging.debug('This host has recently interacted with the RPM' 1467 ' Infrastructure. Ensuring power is on.') 1468 try: 1469 self.power_on() 1470 self._remove_rpm_changed_tag() 1471 except rpm_client.RemotePowerException: 1472 logging.error('Failed to turn Power On for this host after ' 1473 'cleanup through the RPM Infrastructure.') 1474 1475 battery_percentage = self.get_battery_percentage() 1476 if ( 1477 battery_percentage 1478 and battery_percentage < cros_constants.MIN_BATTERY_LEVEL): 1479 raise 1480 elif self.is_ac_connected(): 1481 logging.info('The device has power adapter connected and ' 1482 'charging. No need to try to turn RPM on ' 1483 'again.') 1484 self._remove_rpm_changed_tag() 1485 logging.info('Battery level is now at %s%%. The device may ' 1486 'still have enough power to run test, so no ' 1487 'exception will be raised.', battery_percentage) 1488 1489 1490 def _remove_rpm_changed_tag(self): 1491 info = self.host_info_store.get() 1492 del info.attributes[self._RPM_OUTLET_CHANGED] 1493 self.host_info_store.commit(info) 1494 1495 1496 def _add_rpm_changed_tag(self): 1497 info = self.host_info_store.get() 1498 info.attributes[self._RPM_OUTLET_CHANGED] = 'true' 1499 self.host_info_store.commit(info) 1500 1501 1502 1503 def _is_factory_image(self): 1504 """Checks if the image on the DUT is a factory image. 1505 1506 @return: True if the image on the DUT is a factory image. 1507 False otherwise. 1508 """ 1509 result = self.run('[ -f /root/.factory_test ]', ignore_status=True) 1510 return result.exit_status == 0 1511 1512 1513 def _restart_ui(self): 1514 """Restart the Chrome UI. 1515 1516 @raises: FactoryImageCheckerException for factory images, since 1517 we cannot attempt to restart ui on them. 1518 error.AutoservRunError for any other type of error that 1519 occurs while restarting ui. 1520 """ 1521 if self._is_factory_image(): 1522 raise FactoryImageCheckerException('Cannot restart ui on factory ' 1523 'images') 1524 1525 # TODO(jrbarnette): The command to stop/start the ui job 1526 # should live inside cros_ui, too. However that would seem 1527 # to imply interface changes to the existing start()/restart() 1528 # functions, which is a bridge too far (for now). 1529 prompt = cros_ui.get_chrome_session_ident(self) 1530 self.run('stop ui; start ui') 1531 cros_ui.wait_for_chrome_ready(prompt, self) 1532 1533 1534 def _start_powerd_if_needed(self): 1535 """Start powerd if it isn't already running.""" 1536 self.run('start powerd', ignore_status=True) 1537 1538 def _read_arc_prop_file(self, filename): 1539 for path in [ 1540 '/usr/share/arcvm/properties/', '/usr/share/arc/properties/' 1541 ]: 1542 if self.path_exists(path + filename): 1543 return utils.parse_cmd_output('cat ' + path + filename, 1544 run_method=self.run) 1545 return None 1546 1547 def _get_arc_build_info(self): 1548 """Returns a dictionary mapping build properties to their values.""" 1549 build_info = None 1550 for filename in ['build.prop', 'vendor_build.prop']: 1551 properties = self._read_arc_prop_file(filename) 1552 if properties: 1553 if build_info: 1554 build_info.update(properties) 1555 else: 1556 build_info = properties 1557 else: 1558 logging.error('Failed to find %s in device.', filename) 1559 return build_info 1560 1561 def _get_arc_primary_abi(self): 1562 """Returns the primary abi of the host.""" 1563 return self._get_arc_build_info().get('ro.product.cpu.abi') 1564 1565 def _get_arc_security_patch(self): 1566 """Returns the security patch of the host.""" 1567 return self._get_arc_build_info().get('ro.build.version.security_patch') 1568 1569 def get_arc_first_api_level(self): 1570 """Returns the security patch of the host.""" 1571 return self._get_arc_build_info().get('ro.product.first_api_level') 1572 1573 def _get_lsb_release_content(self): 1574 """Return the content of lsb-release file of host.""" 1575 return self.run( 1576 'cat "%s"' % client_constants.LSB_RELEASE).stdout.strip() 1577 1578 1579 def get_release_version(self): 1580 """Get the value of attribute CHROMEOS_RELEASE_VERSION from lsb-release. 1581 1582 @returns The version string in lsb-release, under attribute 1583 CHROMEOS_RELEASE_VERSION. 1584 """ 1585 return lsbrelease_utils.get_chromeos_release_version( 1586 lsb_release_content=self._get_lsb_release_content()) 1587 1588 1589 def get_release_builder_path(self): 1590 """Get the value of CHROMEOS_RELEASE_BUILDER_PATH from lsb-release. 1591 1592 @returns The version string in lsb-release, under attribute 1593 CHROMEOS_RELEASE_BUILDER_PATH. 1594 """ 1595 return lsbrelease_utils.get_chromeos_release_builder_path( 1596 lsb_release_content=self._get_lsb_release_content()) 1597 1598 1599 def get_chromeos_release_milestone(self): 1600 """Get the value of attribute CHROMEOS_RELEASE_BUILD_TYPE 1601 from lsb-release. 1602 1603 @returns The version string in lsb-release, under attribute 1604 CHROMEOS_RELEASE_BUILD_TYPE. 1605 """ 1606 return lsbrelease_utils.get_chromeos_release_milestone( 1607 lsb_release_content=self._get_lsb_release_content()) 1608 1609 1610 def verify_cros_version_label(self): 1611 """Verify if host's cros-version label match the actual image in dut. 1612 1613 @returns True if the label match with image in dut, otherwise False 1614 """ 1615 os_from_host = self.get_release_builder_path() 1616 info = self.host_info_store.get() 1617 os_from_label = info.get_label_value(self.VERSION_PREFIX) 1618 if not os_from_label: 1619 logging.debug('No existing %s label detected', self.VERSION_PREFIX) 1620 return True 1621 1622 # known cases where the version label will not match the 1623 # original CHROMEOS_RELEASE_BUILDER_PATH setting: 1624 # * Tests for the `arc-presubmit` append "-cheetsth" to the label. 1625 if os_from_label.endswith(provision.CHEETS_SUFFIX): 1626 logging.debug('%s label with %s suffix detected, this suffix will' 1627 ' be ignored when comparing label.', 1628 self.VERSION_PREFIX, provision.CHEETS_SUFFIX) 1629 os_from_label = os_from_label[:-len(provision.CHEETS_SUFFIX)] 1630 logging.debug('OS version from host: %s; OS verision cached in ' 1631 'label: %s', os_from_host, os_from_label) 1632 return os_from_label == os_from_host 1633 1634 1635 def cleanup_services(self): 1636 """Reinitializes the device for cleanup. 1637 1638 Subclasses may override this to customize the cleanup method. 1639 1640 To indicate failure of the reset, the implementation may raise 1641 any of: 1642 error.AutoservRunError 1643 error.AutotestRunError 1644 FactoryImageCheckerException 1645 1646 @raises error.AutoservRunError 1647 @raises error.AutotestRunError 1648 @raises error.FactoryImageCheckerException 1649 """ 1650 self._restart_ui() 1651 self._start_powerd_if_needed() 1652 1653 1654 def cleanup(self): 1655 """Cleanup state on device.""" 1656 self.run('rm -f %s' % client_constants.CLEANUP_LOGS_PAUSED_FILE) 1657 try: 1658 self.cleanup_services() 1659 except (error.AutotestRunError, error.AutoservRunError, 1660 FactoryImageCheckerException): 1661 logging.warning('Unable to restart ui.') 1662 1663 # cleanup routines, i.e. reboot the machine. 1664 super(CrosHost, self).cleanup() 1665 1666 # Check if the rpm outlet was manipulated. 1667 if self.has_power(): 1668 self._cleanup_poweron() 1669 1670 1671 def reboot(self, **dargs): 1672 """ 1673 This function reboots the site host. The more generic 1674 RemoteHost.reboot() performs sync and sleeps for 5 1675 seconds. This is not necessary for Chrome OS devices as the 1676 sync should be finished in a short time during the reboot 1677 command. 1678 """ 1679 if 'reboot_cmd' not in dargs: 1680 reboot_timeout = dargs.get('reboot_timeout', 10) 1681 dargs['reboot_cmd'] = ('sleep 1; ' 1682 'reboot & sleep %d; ' 1683 'reboot -f' % reboot_timeout) 1684 # Enable fastsync to avoid running extra sync commands before reboot. 1685 if 'fastsync' not in dargs: 1686 dargs['fastsync'] = True 1687 1688 dargs['board'] = self.host_info_store.get().board 1689 # Record who called us 1690 orig = sys._getframe(1).f_code 1691 metric_fields = {'board' : dargs['board'], 1692 'dut_host_name' : self.hostname, 1693 'success' : True} 1694 metric_debug_fields = {'board' : dargs['board'], 1695 'caller' : "%s:%s" % (orig.co_filename, 1696 orig.co_name), 1697 'success' : True, 1698 'error' : ''} 1699 1700 t0 = time.time() 1701 try: 1702 super(CrosHost, self).reboot(**dargs) 1703 except Exception as e: 1704 metric_fields['success'] = False 1705 metric_debug_fields['success'] = False 1706 metric_debug_fields['error'] = type(e).__name__ 1707 raise 1708 finally: 1709 duration = int(time.time() - t0) 1710 metrics.Counter( 1711 'chromeos/autotest/autoserv/reboot_count').increment( 1712 fields=metric_fields) 1713 metrics.Counter( 1714 'chromeos/autotest/autoserv/reboot_debug').increment( 1715 fields=metric_debug_fields) 1716 metrics.SecondsDistribution( 1717 'chromeos/autotest/autoserv/reboot_duration').add( 1718 duration, fields=metric_fields) 1719 1720 1721 def suspend(self, suspend_time=60, delay_seconds=0, 1722 suspend_cmd=None, allow_early_resume=False): 1723 """ 1724 This function suspends the site host. 1725 1726 @param suspend_time: How long to suspend as integer seconds. 1727 @param suspend_cmd: Suspend command to execute. 1728 @param allow_early_resume: If False and if device resumes before 1729 |suspend_time|, throw an error. 1730 1731 @exception AutoservSuspendError Host resumed earlier than 1732 |suspend_time|. 1733 """ 1734 1735 if suspend_cmd is None: 1736 suspend_cmd = ' && '.join([ 1737 'echo 0 > /sys/class/rtc/rtc0/wakealarm', 1738 'echo +%d > /sys/class/rtc/rtc0/wakealarm' % suspend_time, 1739 'powerd_dbus_suspend --delay=%d' % delay_seconds]) 1740 super(CrosHost, self).suspend(suspend_time, suspend_cmd, 1741 allow_early_resume); 1742 1743 1744 def upstart_status(self, service_name): 1745 """Check the status of an upstart init script. 1746 1747 @param service_name: Service to look up. 1748 1749 @returns True if the service is running, False otherwise. 1750 """ 1751 return 'start/running' in self.run('status %s' % service_name, 1752 ignore_status=True).stdout 1753 1754 def upstart_stop(self, service_name): 1755 """Stops an upstart job if it's running. 1756 1757 @param service_name: Service to stop 1758 1759 @returns True if service has been stopped or was already stopped 1760 False otherwise. 1761 """ 1762 if not self.upstart_status(service_name): 1763 return True 1764 1765 result = self.run('stop %s' % service_name, ignore_status=True) 1766 if result.exit_status != 0: 1767 return False 1768 return True 1769 1770 def upstart_restart(self, service_name): 1771 """Restarts (or starts) an upstart job. 1772 1773 @param service_name: Service to start/restart 1774 1775 @returns True if service has been started/restarted, False otherwise. 1776 """ 1777 cmd = 'start' 1778 if self.upstart_status(service_name): 1779 cmd = 'restart' 1780 cmd = cmd + ' %s' % service_name 1781 result = self.run(cmd) 1782 if result.exit_status != 0: 1783 return False 1784 return True 1785 1786 def verify_software(self): 1787 """Verify working software on a Chrome OS system. 1788 1789 Tests for the following conditions: 1790 1. All conditions tested by the parent version of this 1791 function. 1792 2. Sufficient space in /mnt/stateful_partition. 1793 3. Sufficient space in /mnt/stateful_partition/encrypted. 1794 4. update_engine answers a simple status request over DBus. 1795 1796 """ 1797 super(CrosHost, self).verify_software() 1798 default_kilo_inodes_required = CONFIG.get_config_value( 1799 'SERVER', 'kilo_inodes_required', type=int, default=100) 1800 board = self.get_board().replace(ds_constants.BOARD_PREFIX, '') 1801 kilo_inodes_required = CONFIG.get_config_value( 1802 'SERVER', 'kilo_inodes_required_%s' % board, 1803 type=int, default=default_kilo_inodes_required) 1804 self.check_inodes('/mnt/stateful_partition', kilo_inodes_required) 1805 self.check_diskspace( 1806 '/mnt/stateful_partition', 1807 CONFIG.get_config_value( 1808 'SERVER', 'gb_diskspace_required', type=float, 1809 default=20.0)) 1810 encrypted_stateful_path = '/mnt/stateful_partition/encrypted' 1811 # Not all targets build with encrypted stateful support. 1812 if self.path_exists(encrypted_stateful_path): 1813 self.check_diskspace( 1814 encrypted_stateful_path, 1815 CONFIG.get_config_value( 1816 'SERVER', 'gb_encrypted_diskspace_required', type=float, 1817 default=0.1)) 1818 1819 self.wait_for_system_services() 1820 1821 # Factory images don't run update engine, 1822 # goofy controls dbus on these DUTs. 1823 if not self._is_factory_image(): 1824 self.run('update_engine_client --status') 1825 1826 1827 @retry.retry(error.AutoservError, timeout_min=5, delay_sec=10) 1828 def wait_for_system_services(self): 1829 """Waits for system-services to be running. 1830 1831 Sometimes, update_engine will take a while to update firmware, so we 1832 should give this some time to finish. See crbug.com/765686#c38 for 1833 details. 1834 """ 1835 if not self.upstart_status('system-services'): 1836 raise error.AutoservError('Chrome failed to reach login. ' 1837 'System services not running.') 1838 1839 1840 def verify(self): 1841 """Verify Chrome OS system is in good state.""" 1842 message = 'Beginning verify for host %s board %s model %s' 1843 info = self.host_info_store.get() 1844 message %= (self.hostname, info.board, info.model) 1845 self.record('INFO', None, None, message) 1846 try: 1847 self._repair_strategy.verify(self) 1848 except hosts.AutoservVerifyDependencyError as e: 1849 # We don't want flag a DUT as failed if only non-critical 1850 # verifier(s) failed during the repair. 1851 if e.is_critical(): 1852 raise 1853 1854 1855 def make_ssh_command(self, user='root', port=22, opts='', hosts_file=None, 1856 connect_timeout=None, alive_interval=None, 1857 alive_count_max=None, connection_attempts=None): 1858 """Override default make_ssh_command to use options tuned for Chrome OS. 1859 1860 Tuning changes: 1861 - ConnectTimeout=30; maximum of 30 seconds allowed for an SSH 1862 connection failure. Consistency with remote_access.sh. 1863 1864 - ServerAliveInterval=900; which causes SSH to ping connection every 1865 900 seconds. In conjunction with ServerAliveCountMax ensures 1866 that if the connection dies, Autotest will bail out. 1867 Originally tried 60 secs, but saw frequent job ABORTS where 1868 the test completed successfully. Later increased from 180 seconds to 1869 900 seconds to account for tests where the DUT is suspended for 1870 longer periods of time. 1871 1872 - ServerAliveCountMax=3; consistency with remote_access.sh. 1873 1874 - ConnectAttempts=4; reduce flakiness in connection errors; 1875 consistency with remote_access.sh. 1876 1877 - UserKnownHostsFile=/dev/null; we don't care about the keys. 1878 Host keys change with every new installation, don't waste 1879 memory/space saving them. 1880 1881 - SSH protocol forced to 2; needed for ServerAliveInterval. 1882 1883 @param user User name to use for the ssh connection. 1884 @param port Port on the target host to use for ssh connection. 1885 @param opts Additional options to the ssh command. 1886 @param hosts_file Ignored. 1887 @param connect_timeout Ignored. 1888 @param alive_interval Ignored. 1889 @param alive_count_max Ignored. 1890 @param connection_attempts Ignored. 1891 """ 1892 options = ' '.join([opts, '-o Protocol=2']) 1893 return super(CrosHost, self).make_ssh_command( 1894 user=user, port=port, opts=options, hosts_file='/dev/null', 1895 connect_timeout=30, alive_interval=900, alive_count_max=3, 1896 connection_attempts=4) 1897 1898 1899 def syslog(self, message, tag='autotest'): 1900 """Logs a message to syslog on host. 1901 1902 @param message String message to log into syslog 1903 @param tag String tag prefix for syslog 1904 1905 """ 1906 self.run('logger -t "%s" "%s"' % (tag, message)) 1907 1908 1909 def _ping_check_status(self, status): 1910 """Ping the host once, and return whether it has a given status. 1911 1912 @param status Check the ping status against this value. 1913 @return True iff `status` and the result of ping are the same 1914 (i.e. both True or both False). 1915 1916 """ 1917 ping_val = utils.ping(self.hostname, 1918 tries=1, 1919 deadline=1, 1920 timeout=2, 1921 ignore_timeout=True) 1922 return not (status ^ (ping_val == 0)) 1923 1924 def _ping_wait_for_status(self, status, timeout): 1925 """Wait for the host to have a given status (UP or DOWN). 1926 1927 Status is checked by polling. Polling will not last longer 1928 than the number of seconds in `timeout`. The polling 1929 interval will be long enough that only approximately 1930 _PING_WAIT_COUNT polling cycles will be executed, subject 1931 to a maximum interval of about one minute. 1932 1933 @param status Waiting will stop immediately if `ping` of the 1934 host returns this status. 1935 @param timeout Poll for at most this many seconds. 1936 @return True iff the host status from `ping` matched the 1937 requested status at the time of return. 1938 1939 """ 1940 # _ping_check_status() takes about 1 second, hence the 1941 # "- 1" in the formula below. 1942 # FIXME: if the ping command errors then _ping_check_status() 1943 # returns instantly. If timeout is also smaller than twice 1944 # _PING_WAIT_COUNT then the while loop below forks many 1945 # thousands of ping commands (see /tmp/test_that_results_XXXXX/ 1946 # /results-1-logging_YYY.ZZZ/debug/autoserv.DEBUG) and hogs one 1947 # CPU core for 60 seconds. 1948 poll_interval = min(int(timeout / self._PING_WAIT_COUNT), 60) - 1 1949 end_time = time.time() + timeout 1950 while time.time() <= end_time: 1951 if self._ping_check_status(status): 1952 return True 1953 if poll_interval > 0: 1954 time.sleep(poll_interval) 1955 1956 # The last thing we did was sleep(poll_interval), so it may 1957 # have been too long since the last `ping`. Check one more 1958 # time, just to be sure. 1959 return self._ping_check_status(status) 1960 1961 def ping_wait_up(self, timeout): 1962 """Wait for the host to respond to `ping`. 1963 1964 N.B. This method is not a reliable substitute for 1965 `wait_up()`, because a host that responds to ping will not 1966 necessarily respond to ssh. This method should only be used 1967 if the target DUT can be considered functional even if it 1968 can't be reached via ssh. 1969 1970 @param timeout Minimum time to allow before declaring the 1971 host to be non-responsive. 1972 @return True iff the host answered to ping before the timeout. 1973 1974 """ 1975 return self._ping_wait_for_status(self._PING_STATUS_UP, timeout) 1976 1977 def ping_wait_down(self, timeout): 1978 """Wait until the host no longer responds to `ping`. 1979 1980 This function can be used as a slightly faster version of 1981 `wait_down()`, by avoiding potentially long ssh timeouts. 1982 1983 @param timeout Minimum time to allow for the host to become 1984 non-responsive. 1985 @return True iff the host quit answering ping before the 1986 timeout. 1987 1988 """ 1989 return self._ping_wait_for_status(self._PING_STATUS_DOWN, timeout) 1990 1991 def _is_host_port_forwarded(self): 1992 """Checks if the dut is connected over port forwarding. 1993 1994 N.B. This method does not detect all situations where port forwarding is 1995 occurring. Namely, running autotest on the dut may result in a 1996 false-positive, and port forwarding using a different machine on the 1997 same network will be a false-negative. 1998 1999 @return True if the dut is connected over port forwarding 2000 False otherwise 2001 """ 2002 is_localhost = self.hostname in ['localhost', '127.0.0.1'] 2003 is_forwarded = is_localhost and not self.is_default_port 2004 if is_forwarded: 2005 logging.info('Detected DUT connected by port forwarding') 2006 return is_forwarded 2007 2008 def test_wait_for_sleep(self, sleep_timeout=None): 2009 """Wait for the client to enter low-power sleep mode. 2010 2011 The test for "is asleep" can't distinguish a system that is 2012 powered off; to confirm that the unit was asleep, it is 2013 necessary to force resume, and then call 2014 `test_wait_for_resume()`. 2015 2016 This function is expected to be called from a test as part 2017 of a sequence like the following: 2018 2019 ~~~~~~~~ 2020 boot_id = host.get_boot_id() 2021 # trigger sleep on the host 2022 host.test_wait_for_sleep() 2023 # trigger resume on the host 2024 host.test_wait_for_resume(boot_id) 2025 ~~~~~~~~ 2026 2027 @param sleep_timeout time limit in seconds to allow the host sleep. 2028 2029 @exception TestFail The host did not go to sleep within 2030 the allowed time. 2031 """ 2032 if sleep_timeout is None: 2033 sleep_timeout = self.SLEEP_TIMEOUT 2034 2035 # If the dut is accessed over SSH port-forwarding, `ping` is not useful 2036 # for detecting the dut is down since a ping to localhost will always 2037 # succeed. In this case, fall back to wait_down() which uses SSH. 2038 if self._is_host_port_forwarded(): 2039 success = self.wait_down(timeout=sleep_timeout) 2040 else: 2041 success = self.ping_wait_down(timeout=sleep_timeout) 2042 2043 if not success: 2044 raise error.TestFail( 2045 'client failed to sleep after %d seconds' % sleep_timeout) 2046 2047 2048 def test_wait_for_resume(self, old_boot_id, resume_timeout=None): 2049 """Wait for the client to resume from low-power sleep mode. 2050 2051 The `old_boot_id` parameter should be the value from 2052 `get_boot_id()` obtained prior to entering sleep mode. A 2053 `TestFail` exception is raised if the boot id changes. 2054 2055 See @ref test_wait_for_sleep for more on this function's 2056 usage. 2057 2058 @param old_boot_id A boot id value obtained before the 2059 target host went to sleep. 2060 @param resume_timeout time limit in seconds to allow the host up. 2061 2062 @exception TestFail The host did not respond within the 2063 allowed time. 2064 @exception TestFail The host responded, but the boot id test 2065 indicated a reboot rather than a sleep 2066 cycle. 2067 """ 2068 if resume_timeout is None: 2069 resume_timeout = self.RESUME_TIMEOUT 2070 2071 if not self.wait_up(timeout=resume_timeout): 2072 raise error.TestFail( 2073 'client failed to resume from sleep after %d seconds' % 2074 resume_timeout) 2075 else: 2076 new_boot_id = self.get_boot_id() 2077 if new_boot_id != old_boot_id: 2078 logging.error('client rebooted (old boot %s, new boot %s)', 2079 old_boot_id, new_boot_id) 2080 raise error.TestFail( 2081 'client rebooted, but sleep was expected') 2082 2083 2084 def test_wait_for_shutdown(self, shutdown_timeout=None): 2085 """Wait for the client to shut down. 2086 2087 The test for "has shut down" can't distinguish a system that 2088 is merely asleep; to confirm that the unit was down, it is 2089 necessary to force boot, and then call test_wait_for_boot(). 2090 2091 This function is expected to be called from a test as part 2092 of a sequence like the following: 2093 2094 ~~~~~~~~ 2095 boot_id = host.get_boot_id() 2096 # trigger shutdown on the host 2097 host.test_wait_for_shutdown() 2098 # trigger boot on the host 2099 host.test_wait_for_boot(boot_id) 2100 ~~~~~~~~ 2101 2102 @param shutdown_timeout time limit in seconds to allow the host down. 2103 @exception TestFail The host did not shut down within the 2104 allowed time. 2105 """ 2106 if shutdown_timeout is None: 2107 shutdown_timeout = self.SHUTDOWN_TIMEOUT 2108 2109 if self._is_host_port_forwarded(): 2110 success = self.wait_down(timeout=shutdown_timeout) 2111 else: 2112 success = self.ping_wait_down(timeout=shutdown_timeout) 2113 2114 if not success: 2115 raise error.TestFail( 2116 'client failed to shut down after %d seconds' % 2117 shutdown_timeout) 2118 2119 2120 def test_wait_for_boot(self, old_boot_id=None): 2121 """Wait for the client to boot from cold power. 2122 2123 The `old_boot_id` parameter should be the value from 2124 `get_boot_id()` obtained prior to shutting down. A 2125 `TestFail` exception is raised if the boot id does not 2126 change. The boot id test is omitted if `old_boot_id` is not 2127 specified. 2128 2129 See @ref test_wait_for_shutdown for more on this function's 2130 usage. 2131 2132 @param old_boot_id A boot id value obtained before the 2133 shut down. 2134 2135 @exception TestFail The host did not respond within the 2136 allowed time. 2137 @exception TestFail The host responded, but the boot id test 2138 indicated that there was no reboot. 2139 """ 2140 if not self.wait_up(timeout=self.REBOOT_TIMEOUT): 2141 raise error.TestFail( 2142 'client failed to reboot after %d seconds' % 2143 self.REBOOT_TIMEOUT) 2144 elif old_boot_id: 2145 if self.get_boot_id() == old_boot_id: 2146 logging.error('client not rebooted (boot %s)', 2147 old_boot_id) 2148 raise error.TestFail( 2149 'client is back up, but did not reboot') 2150 2151 2152 @staticmethod 2153 def check_for_rpm_support(hostname): 2154 """For a given hostname, return whether or not it is powered by an RPM. 2155 2156 @param hostname: hostname to check for rpm support. 2157 2158 @return None if this host does not follows the defined naming format 2159 for RPM powered DUT's in the lab. If it does follow the format, 2160 it returns a regular expression MatchObject instead. 2161 """ 2162 return re.match(CrosHost._RPM_HOSTNAME_REGEX, hostname) 2163 2164 2165 def has_power(self): 2166 """For this host, return whether or not it is powered by an RPM. 2167 2168 @return True if this host is in the CROS lab and follows the defined 2169 naming format. 2170 """ 2171 return CrosHost.check_for_rpm_support(self.hostname) 2172 2173 2174 def _set_power(self, state, power_method): 2175 """Sets the power to the host via RPM, CCD, Servo or manual. 2176 2177 @param state Specifies which power state to set to DUT 2178 @param power_method Specifies which method of power control to 2179 use. By default "RPM" or "CCD" will be used based 2180 on servo type. Valid values from 2181 POWER_CONTROL_VALID_ARGS, or None to use default. 2182 2183 """ 2184 ACCEPTABLE_STATES = ['ON', 'OFF'] 2185 2186 if not power_method: 2187 power_method = self.get_default_power_method() 2188 2189 state = state.upper() 2190 if state not in ACCEPTABLE_STATES: 2191 raise error.TestError('State must be one of: %s.' 2192 % (ACCEPTABLE_STATES,)) 2193 2194 if power_method == self.POWER_CONTROL_SERVO: 2195 logging.info('Setting servo port J10 to %s', state) 2196 self.servo.set('prtctl3_pwren', state.lower()) 2197 time.sleep(self._USB_POWER_TIMEOUT) 2198 elif power_method == self.POWER_CONTROL_MANUAL: 2199 logging.info('You have %d seconds to set the AC power to %s.', 2200 self._POWER_CYCLE_TIMEOUT, state) 2201 time.sleep(self._POWER_CYCLE_TIMEOUT) 2202 elif power_method == self.POWER_CONTROL_CCD: 2203 servo_role = 'src' if state == 'ON' else 'snk' 2204 logging.info('servo ccd power pass through detected,' 2205 ' changing servo_role to %s.', servo_role) 2206 self.servo.set_servo_v4_role(servo_role) 2207 if not self.ping_wait_up(timeout=self._CHANGE_SERVO_ROLE_TIMEOUT): 2208 # Make sure we don't leave DUT with no power(servo_role=snk) 2209 # when DUT is not pingable, as we raise a exception here 2210 # that may break a power cycle in the middle. 2211 self.servo.set_servo_v4_role('src') 2212 raise error.AutoservError( 2213 'DUT failed to regain network connection after %d seconds.' 2214 % self._CHANGE_SERVO_ROLE_TIMEOUT) 2215 else: 2216 if not self.has_power(): 2217 raise error.TestFail('DUT does not have RPM connected.') 2218 self._add_rpm_changed_tag() 2219 rpm_client.set_power(self, state, timeout_mins=5) 2220 2221 2222 def power_off(self, power_method=None): 2223 """Turn off power to this host via RPM, CCD, Servo or manual. 2224 2225 @param power_method Specifies which method of power control to 2226 use. By default "RPM" or "CCD" will be used based 2227 on servo type. Valid values from 2228 POWER_CONTROL_VALID_ARGS, or None to use default. 2229 2230 """ 2231 self._sync_if_up() 2232 self._set_power('OFF', power_method) 2233 2234 def _check_supported(self): 2235 """Throw an error if dts mode control is not supported.""" 2236 if not self.servo_pwr_supported: 2237 raise error.TestFail('power_state controls not supported') 2238 2239 def _sync_if_up(self): 2240 """Run sync on the DUT and wait for completion if the DUT is up. 2241 2242 Additionally, try to sync and ignore status if its not up. 2243 2244 Useful prior to reboots to ensure files are written to disc. 2245 2246 """ 2247 if self.is_up_fast(): 2248 self.run("sync") 2249 return 2250 # If it is not up, attempt to sync in the rare event the DUT is up but 2251 # doesn't respond to a ping. Ignore any errors. 2252 try: 2253 self.run("sync", ignore_status=True, timeout=1) 2254 except Exception: 2255 pass 2256 2257 def power_off_via_servo(self): 2258 """Force the DUT to power off. 2259 2260 The DUT is guaranteed to be off at the end of this call, 2261 regardless of its previous state, provided that there is 2262 working EC and boot firmware. There is no requirement for 2263 working OS software. 2264 2265 """ 2266 self._check_supported() 2267 self._sync_if_up() 2268 self.servo.set_nocheck('power_state', 'off') 2269 2270 def power_on_via_servo(self, rec_mode='on'): 2271 """Force the DUT to power on. 2272 2273 Prior to calling this function, the DUT must be powered off, 2274 e.g. with a call to `power_off()`. 2275 2276 At power on, recovery mode is set as specified by the 2277 corresponding argument. When booting with recovery mode on, it 2278 is the caller's responsibility to unplug/plug in a bootable 2279 external storage device. 2280 2281 If the DUT requires a delay after powering on but before 2282 processing inputs such as USB stick insertion, the delay is 2283 handled by this method; the caller is not responsible for such 2284 delays. 2285 2286 @param rec_mode Setting of recovery mode to be applied at 2287 power on. default: REC_OFF aka 'off' 2288 2289 """ 2290 self._check_supported() 2291 self.servo.set_nocheck('power_state', rec_mode) 2292 2293 def reset_via_servo(self): 2294 """Force the DUT to reset. 2295 2296 The DUT is guaranteed to be on at the end of this call, 2297 regardless of its previous state, provided that there is 2298 working OS software. This also guarantees that the EC has 2299 been restarted. 2300 2301 """ 2302 self._check_supported() 2303 self._sync_if_up() 2304 self.servo.set_nocheck('power_state', 'reset') 2305 2306 2307 def power_on(self, power_method=None): 2308 """Turn on power to this host via RPM, CCD, Servo or manual. 2309 2310 @param power_method Specifies which method of power control to 2311 use. By default "RPM" or "CCD" will be used based 2312 on servo type. Valid values from 2313 POWER_CONTROL_VALID_ARGS, or None to use default. 2314 2315 """ 2316 self._set_power('ON', power_method) 2317 2318 2319 def power_cycle(self, power_method=None): 2320 """Cycle power to this host by turning it OFF, then ON. 2321 2322 @param power_method Specifies which method of power control to 2323 use. By default "RPM" or "CCD" will be used based 2324 on servo type. Valid values from 2325 POWER_CONTROL_VALID_ARGS, or None to use default. 2326 2327 """ 2328 if not power_method: 2329 power_method = self.get_default_power_method() 2330 2331 if power_method in (self.POWER_CONTROL_SERVO, 2332 self.POWER_CONTROL_MANUAL, 2333 self.POWER_CONTROL_CCD): 2334 self.power_off(power_method=power_method) 2335 time.sleep(self._POWER_CYCLE_TIMEOUT) 2336 self.power_on(power_method=power_method) 2337 else: 2338 self._add_rpm_changed_tag() 2339 rpm_client.set_power(self, 'CYCLE') 2340 2341 2342 def get_platform_from_fwid(self): 2343 """Determine the platform from the crossystem fwid. 2344 2345 @returns a string representing this host's platform. 2346 """ 2347 # Look at the firmware for non-unibuild cases or if cros_config fails. 2348 crossystem = utils.Crossystem(self) 2349 crossystem.init() 2350 # Extract fwid value and use the leading part as the platform id. 2351 # fwid generally follow the format of {platform}.{firmware version} 2352 # Example: Alex.X.YYY.Z or Google_Alex.X.YYY.Z 2353 platform = crossystem.fwid().split('.')[0].lower() 2354 # Newer platforms start with 'Google_' while the older ones do not. 2355 return platform.replace('google_', '') 2356 2357 2358 def get_platform(self): 2359 """Determine the correct platform label for this host. 2360 2361 @returns a string representing this host's platform. 2362 """ 2363 release_info = utils.parse_cmd_output('cat /etc/lsb-release', 2364 run_method=self.run) 2365 platform = '' 2366 if release_info.get('CHROMEOS_RELEASE_UNIBUILD') == '1': 2367 platform = self.get_model_from_cros_config() 2368 return platform if platform else self.get_platform_from_fwid() 2369 2370 2371 def get_model_from_cros_config(self): 2372 """Get the host model from cros_config command. 2373 2374 @returns a string representing this host's model. 2375 """ 2376 return cros_config.call_cros_config_get_output('/ name', 2377 self.run, ignore_status=True) 2378 2379 2380 def get_architecture(self): 2381 """Determine the correct architecture label for this host. 2382 2383 @returns a string representing this host's architecture. 2384 """ 2385 crossystem = utils.Crossystem(self) 2386 crossystem.init() 2387 return crossystem.arch() 2388 2389 2390 def get_chrome_version(self): 2391 """Gets the Chrome version number and milestone as strings. 2392 2393 Invokes "chrome --version" to get the version number and milestone. 2394 2395 @return A tuple (chrome_ver, milestone) where "chrome_ver" is the 2396 current Chrome version number as a string (in the form "W.X.Y.Z") 2397 and "milestone" is the first component of the version number 2398 (the "W" from "W.X.Y.Z"). If the version number cannot be parsed 2399 in the "W.X.Y.Z" format, the "chrome_ver" will be the full output 2400 of "chrome --version" and the milestone will be the empty string. 2401 2402 """ 2403 version_string = self.run(client_constants.CHROME_VERSION_COMMAND).stdout 2404 return utils.parse_chrome_version(version_string) 2405 2406 2407 def get_ec_version(self): 2408 """Get the ec version as strings. 2409 2410 @returns a string representing this host's ec version. 2411 """ 2412 command = 'mosys ec info -s fw_version' 2413 result = self.run(command, ignore_status=True) 2414 if result.exit_status != 0: 2415 return '' 2416 return result.stdout.strip() 2417 2418 2419 def get_firmware_version(self): 2420 """Get the firmware version as strings. 2421 2422 @returns a string representing this host's firmware version. 2423 """ 2424 crossystem = utils.Crossystem(self) 2425 crossystem.init() 2426 return crossystem.fwid() 2427 2428 2429 def get_hardware_revision(self): 2430 """Get the hardware revision as strings. 2431 2432 @returns a string representing this host's hardware revision. 2433 """ 2434 command = 'mosys platform version' 2435 result = self.run(command, ignore_status=True) 2436 if result.exit_status != 0: 2437 return '' 2438 return result.stdout.strip() 2439 2440 2441 def get_kernel_version(self): 2442 """Get the kernel version as strings. 2443 2444 @returns a string representing this host's kernel version. 2445 """ 2446 return self.run('uname -r').stdout.strip() 2447 2448 2449 def get_cpu_name(self): 2450 """Get the cpu name as strings. 2451 2452 @returns a string representing this host's cpu name. 2453 """ 2454 2455 # Try get cpu name from device tree first 2456 if self.path_exists('/proc/device-tree/compatible'): 2457 command = ' | '.join( 2458 ["sed -e 's/\\x0/\\n/g' /proc/device-tree/compatible", 2459 'tail -1']) 2460 return self.run(command).stdout.strip().replace(',', ' ') 2461 2462 # Get cpu name from uname -p 2463 command = 'uname -p' 2464 ret = self.run(command).stdout.strip() 2465 2466 # 'uname -p' return variant of unknown or amd64 or x86_64 or i686 2467 # Try get cpu name from /proc/cpuinfo instead 2468 if re.match("unknown|amd64|[ix][0-9]?86(_64)?", ret, re.IGNORECASE): 2469 command = "grep model.name /proc/cpuinfo | cut -f 2 -d: | head -1" 2470 self = self.run(command).stdout.strip() 2471 2472 # Remove bloat from CPU name, for example 2473 # Intel(R) Core(TM) i5-7Y57 CPU @ 1.20GHz -> Intel Core i5-7Y57 2474 # Intel(R) Xeon(R) CPU E5-2690 v4 @ 2.60GHz -> Intel Xeon E5-2690 v4 2475 # AMD A10-7850K APU with Radeon(TM) R7 Graphics -> AMD A10-7850K 2476 # AMD GX-212JC SOC with Radeon(TM) R2E Graphics -> AMD GX-212JC 2477 trim_re = r' (@|processor|apu|soc|radeon).*|\(.*?\)| cpu' 2478 return re.sub(trim_re, '', ret, flags=re.IGNORECASE) 2479 2480 2481 def get_screen_resolution(self): 2482 """Get the screen(s) resolution as strings. 2483 In case of more than 1 monitor, return resolution for each monitor 2484 separate with plus sign. 2485 2486 @returns a string representing this host's screen(s) resolution. 2487 """ 2488 command = 'for f in /sys/class/drm/*/*/modes; do head -1 $f; done' 2489 ret = self.run(command, ignore_status=True) 2490 # We might have Chromebox without a screen 2491 if ret.exit_status != 0: 2492 return '' 2493 return ret.stdout.strip().replace('\n', '+') 2494 2495 2496 def get_mem_total_gb(self): 2497 """Get total memory available in the system in GiB (2^20). 2498 2499 @returns an integer representing total memory 2500 """ 2501 mem_total_kb = self.read_from_meminfo('MemTotal') 2502 kb_in_gb = float(2 ** 20) 2503 return int(round(mem_total_kb / kb_in_gb)) 2504 2505 2506 def get_disk_size_gb(self): 2507 """Get size of disk in GB (10^9) 2508 2509 @returns an integer representing size of disk, 0 in Error Case 2510 """ 2511 command = 'grep $(rootdev -s -d | cut -f3 -d/)$ /proc/partitions' 2512 result = self.run(command, ignore_status=True) 2513 if result.exit_status != 0: 2514 return 0 2515 _, _, block, _ = re.split(r' +', result.stdout.strip()) 2516 byte_per_block = 1024.0 2517 disk_kb_in_gb = 1e9 2518 return int(int(block) * byte_per_block / disk_kb_in_gb + 0.5) 2519 2520 2521 def get_battery_size(self): 2522 """Get size of battery in Watt-hour via sysfs 2523 2524 This method assumes that battery support voltage_min_design and 2525 charge_full_design sysfs. 2526 2527 @returns a float representing Battery size, 0 if error. 2528 """ 2529 # sysfs report data in micro scale 2530 battery_scale = 1e6 2531 2532 command = 'cat /sys/class/power_supply/*/voltage_min_design' 2533 result = self.run(command, ignore_status=True) 2534 if result.exit_status != 0: 2535 return 0 2536 voltage = float(result.stdout.strip()) / battery_scale 2537 2538 command = 'cat /sys/class/power_supply/*/charge_full_design' 2539 result = self.run(command, ignore_status=True) 2540 if result.exit_status != 0: 2541 return 0 2542 amphereHour = float(result.stdout.strip()) / battery_scale 2543 2544 return voltage * amphereHour 2545 2546 2547 def get_low_battery_shutdown_percent(self): 2548 """Get the percent-based low-battery shutdown threshold. 2549 2550 @returns a float representing low-battery shutdown percent, 0 if error. 2551 """ 2552 ret = 0.0 2553 try: 2554 command = 'check_powerd_config --low_battery_shutdown_percent' 2555 ret = float(self.run(command).stdout) 2556 except error.CmdError: 2557 logging.debug("Can't run %s", command) 2558 except ValueError: 2559 logging.debug("Didn't get number from %s", command) 2560 2561 return ret 2562 2563 2564 def has_hammer(self): 2565 """Check whether DUT has hammer device or not. 2566 2567 @returns boolean whether device has hammer or not 2568 """ 2569 command = 'grep Hammer /sys/bus/usb/devices/*/product' 2570 return self.run(command, ignore_status=True).exit_status == 0 2571 2572 2573 def is_chrome_switch_present(self, switch): 2574 """Returns True if the specified switch was provided to Chrome. 2575 2576 @param switch The chrome switch to search for. 2577 """ 2578 2579 command = 'pgrep -x -f -c "/opt/google/chrome/chrome.*%s.*"' % switch 2580 return self.run(command, ignore_status=True).exit_status == 0 2581 2582 2583 def oobe_triggers_update(self): 2584 """Returns True if this host has an OOBE flow during which 2585 it will perform an update check and perhaps an update. 2586 One example of such a flow is Hands-Off Zero-Touch Enrollment. 2587 As more such flows are developed, code handling them needs 2588 to be added here. 2589 2590 @return Boolean indicating whether this host's OOBE triggers an update. 2591 """ 2592 return self.is_chrome_switch_present( 2593 '--enterprise-enable-zero-touch-enrollment=hands-off') 2594 2595 2596 # TODO(kevcheng): change this to just return the board without the 2597 # 'board:' prefix and fix up all the callers. Also look into removing the 2598 # need for this method. 2599 def get_board(self): 2600 """Determine the correct board label for this host. 2601 2602 @returns a string representing this host's board. 2603 """ 2604 release_info = utils.parse_cmd_output('cat /etc/lsb-release', 2605 run_method=self.run) 2606 return (ds_constants.BOARD_PREFIX + 2607 release_info['CHROMEOS_RELEASE_BOARD']) 2608 2609 def get_channel(self): 2610 """Determine the correct channel label for this host. 2611 2612 @returns: a string represeting this host's build channel. 2613 (stable, dev, beta). None on fail. 2614 """ 2615 return lsbrelease_utils.get_chromeos_channel( 2616 lsb_release_content=self._get_lsb_release_content()) 2617 2618 def get_power_supply(self): 2619 """ 2620 Determine what type of power supply the host has 2621 2622 @returns a string representing this host's power supply. 2623 'power:battery' when the device has a battery intended for 2624 extended use 2625 'power:AC_primary' when the device has a battery not intended 2626 for extended use (for moving the machine, etc) 2627 'power:AC_only' when the device has no battery at all. 2628 """ 2629 psu = self.run(command='mosys psu type', ignore_status=True) 2630 if psu.exit_status: 2631 # The psu command for mosys is not included for all platforms. The 2632 # assumption is that the device will have a battery if the command 2633 # is not found. 2634 return 'power:battery' 2635 2636 psu_str = psu.stdout.strip() 2637 if psu_str == 'unknown': 2638 return None 2639 2640 return 'power:%s' % psu_str 2641 2642 2643 def has_battery(self): 2644 """Determine if DUT has a battery. 2645 2646 Returns: 2647 Boolean, False if known not to have battery, True otherwise. 2648 """ 2649 rv = True 2650 power_supply = self.get_power_supply() 2651 if power_supply == 'power:battery': 2652 _NO_BATTERY_BOARD_TYPE = ['CHROMEBOX', 'CHROMEBIT', 'CHROMEBASE'] 2653 board_type = self.get_board_type() 2654 if board_type in _NO_BATTERY_BOARD_TYPE: 2655 logging.warn('Do NOT believe type %s has battery. ' 2656 'See debug for mosys details', board_type) 2657 psu = utils.system_output('mosys -vvvv psu type', 2658 ignore_status=True) 2659 logging.debug(psu) 2660 rv = False 2661 elif power_supply == 'power:AC_only': 2662 rv = False 2663 2664 return rv 2665 2666 2667 def get_servo(self): 2668 """Determine if the host has a servo attached. 2669 2670 If the host has a working servo attached, it should have a servo label. 2671 2672 @return: string 'servo' if the host has servo attached. Otherwise, 2673 returns None. 2674 """ 2675 return 'servo' if self._servo_host else None 2676 2677 2678 def has_internal_display(self): 2679 """Determine if the device under test is equipped with an internal 2680 display. 2681 2682 @return: 'internal_display' if one is present; None otherwise. 2683 """ 2684 from autotest_lib.client.cros.graphics import graphics_utils 2685 from autotest_lib.client.common_lib import utils as common_utils 2686 2687 def __system_output(cmd): 2688 return self.run(cmd).stdout 2689 2690 def __read_file(remote_path): 2691 return self.run('cat %s' % remote_path).stdout 2692 2693 # Hijack the necessary client functions so that we can take advantage 2694 # of the client lib here. 2695 # FIXME: find a less hacky way than this 2696 original_system_output = utils.system_output 2697 original_read_file = common_utils.read_file 2698 utils.system_output = __system_output 2699 common_utils.read_file = __read_file 2700 try: 2701 return ('internal_display' if graphics_utils.has_internal_display() 2702 else None) 2703 finally: 2704 utils.system_output = original_system_output 2705 common_utils.read_file = original_read_file 2706 2707 2708 def is_boot_from_usb(self): 2709 """Check if DUT is boot from USB. 2710 2711 @return: True if DUT is boot from usb. 2712 """ 2713 device = self.run('rootdev -s -d').stdout.strip() 2714 removable = int(self.run('cat /sys/block/%s/removable' % 2715 os.path.basename(device)).stdout.strip()) 2716 return removable == 1 2717 2718 def is_boot_from_external_device(self): 2719 """Check if DUT is boot from external storage. 2720 2721 @return: True if DUT is boot from external storage. 2722 """ 2723 boot_device = self.run('rootdev -s -d', ignore_status=True, 2724 timeout=60).stdout.strip() 2725 if not boot_device: 2726 logging.debug('Boot storage not detected on the host.') 2727 return False 2728 main_storage_cmd = ('. /usr/sbin/write_gpt.sh;' 2729 ' . /usr/share/misc/chromeos-common.sh;' 2730 ' load_base_vars; get_fixed_dst_drive') 2731 main_storage = self.run(main_storage_cmd, 2732 ignore_status=True, 2733 timeout=60).stdout.strip() 2734 if not main_storage or boot_device != main_storage: 2735 logging.debug('Device booted from external storage storage.') 2736 return True 2737 logging.debug('Device booted from main storage.') 2738 return False 2739 2740 def read_from_meminfo(self, key): 2741 """Return the memory info from /proc/meminfo 2742 2743 @param key: meminfo requested 2744 2745 @return the memory value as a string 2746 2747 """ 2748 meminfo = self.run('grep %s /proc/meminfo' % key).stdout.strip() 2749 logging.debug('%s', meminfo) 2750 return int(re.search(r'\d+', meminfo).group(0)) 2751 2752 2753 def get_cpu_arch(self): 2754 """Returns CPU arch of the device. 2755 2756 @return CPU architecture of the DUT. 2757 """ 2758 # Add CPUs by following logic in client/bin/utils.py. 2759 if self.run("grep '^flags.*:.* lm .*' /proc/cpuinfo", 2760 ignore_status=True).stdout: 2761 return 'x86_64' 2762 if self.run("grep -Ei 'ARM|CPU implementer' /proc/cpuinfo", 2763 ignore_status=True).stdout: 2764 return 'arm' 2765 return 'i386' 2766 2767 2768 def get_board_type(self): 2769 """ 2770 Get the DUT's device type from /etc/lsb-release. 2771 DEVICETYPE can be one of CHROMEBOX, CHROMEBASE, CHROMEBOOK or more. 2772 2773 @return value of DEVICETYPE param from lsb-release. 2774 """ 2775 device_type = self.run('grep DEVICETYPE /etc/lsb-release', 2776 ignore_status=True).stdout 2777 if device_type: 2778 return device_type.split('=')[-1].strip() 2779 return '' 2780 2781 2782 def get_arc_version(self): 2783 """Return ARC version installed on the DUT. 2784 2785 @returns ARC version as string if the CrOS build has ARC, else None. 2786 """ 2787 arc_version = self.run('grep CHROMEOS_ARC_VERSION /etc/lsb-release', 2788 ignore_status=True).stdout 2789 if arc_version: 2790 return arc_version.split('=')[-1].strip() 2791 return None 2792 2793 2794 def get_os_type(self): 2795 return 'cros' 2796 2797 2798 def get_labels(self): 2799 """Return the detected labels on the host.""" 2800 return self.labels.get_labels(self) 2801 2802 2803 def get_default_power_method(self): 2804 """ 2805 Get the default power method for power_on/off/cycle() methods. 2806 @return POWER_CONTROL_RPM or POWER_CONTROL_CCD 2807 """ 2808 if not self._default_power_method: 2809 self._default_power_method = self.POWER_CONTROL_RPM 2810 if self.servo and self.servo.supports_built_in_pd_control(): 2811 self._default_power_method = self.POWER_CONTROL_CCD 2812 else: 2813 logging.debug('Either servo is unitialized or the servo ' 2814 'setup does not support pd controls. Falling ' 2815 'back to default RPM method.') 2816 return self._default_power_method 2817 2818 2819 def find_usb_devices(self, idVendor, idProduct): 2820 """ 2821 Get usb device sysfs name for specific device. 2822 2823 @param idVendor Vendor ID to search in sysfs directory. 2824 @param idProduct Product ID to search in sysfs directory. 2825 2826 @return Usb node names in /sys/bus/usb/drivers/usb/ that match. 2827 """ 2828 # Look for matching file and cut at position 7 to get dir name. 2829 grep_cmd = 'grep {} /sys/bus/usb/drivers/usb/*/{} | cut -f 7 -d /' 2830 2831 vendor_cmd = grep_cmd.format(idVendor, 'idVendor') 2832 product_cmd = grep_cmd.format(idProduct, 'idProduct') 2833 2834 # Use uniq -d to print duplicate line from both command 2835 cmd = 'sort <({}) <({}) | uniq -d'.format(vendor_cmd, product_cmd) 2836 2837 return self.run(cmd, ignore_status=True).stdout.strip().split('\n') 2838 2839 2840 def bind_usb_device(self, usb_node): 2841 """ 2842 Bind usb device 2843 2844 @param usb_node Node name in /sys/bus/usb/drivers/usb/ 2845 """ 2846 cmd = 'echo {} > /sys/bus/usb/drivers/usb/bind'.format(usb_node) 2847 self.run(cmd, ignore_status=True) 2848 2849 2850 def unbind_usb_device(self, usb_node): 2851 """ 2852 Unbind usb device 2853 2854 @param usb_node Node name in /sys/bus/usb/drivers/usb/ 2855 """ 2856 cmd = 'echo {} > /sys/bus/usb/drivers/usb/unbind'.format(usb_node) 2857 self.run(cmd, ignore_status=True) 2858 2859 2860 def get_wlan_ip(self): 2861 """ 2862 Get ip address of wlan interface. 2863 2864 @return ip address of wlan or empty string if wlan is not connected. 2865 """ 2866 cmds = [ 2867 'iw dev', # List wlan physical device 2868 'grep Interface', # Grep only interface name 2869 'cut -f 2 -d" "', # Cut the name part 2870 'xargs ifconfig', # Feed it to ifconfig to get ip 2871 'grep -oE "inet [0-9.]+"', # Grep only ipv4 2872 'cut -f 2 -d " "' # Cut the ip part 2873 ] 2874 return self.run(' | '.join(cmds), ignore_status=True).stdout.strip() 2875 2876 def connect_to_wifi(self, ssid, passphrase=None, security=None): 2877 """ 2878 Connect to wifi network 2879 2880 @param ssid SSID of the wifi network. 2881 @param passphrase Passphrase of the wifi network. None if not existed. 2882 @param security Security of the wifi network. Default to "psk" if 2883 passphase is given without security. Possible values 2884 are "none", "psk", "802_1x". 2885 2886 @return True if succeed, False if not. 2887 """ 2888 cmd = '/usr/local/autotest/cros/scripts/wifi connect ' + ssid 2889 if passphrase: 2890 cmd += ' ' + passphrase 2891 if security: 2892 cmd += ' ' + security 2893 return self.run(cmd, ignore_status=True).exit_status == 0 2894 2895 def get_device_repair_state(self): 2896 """Get device repair state""" 2897 return self._device_repair_state 2898 2899 def set_device_repair_state(self, state, resultdir=None): 2900 """Set device repair state. 2901 2902 The special device state will be written to the 'dut_state.repair' 2903 file in result directory. The file will be read by Lucifer. The 2904 file will not be created if result directory not specified. 2905 2906 @params state: The new state for the device. 2907 @params resultdir: The path to result directory. If path not provided 2908 will be attempt to get retrieve it from job 2909 if present. 2910 """ 2911 resultdir = resultdir or getattr(self.job, 'resultdir', '') 2912 if resultdir: 2913 target = os.path.join(resultdir, 'dut_state.repair') 2914 common_utils.open_write_close(target, state) 2915 logging.info('Set device state as %s. ' 2916 'Created dut_state.repair file.', state) 2917 else: 2918 logging.debug('Cannot write the device state due missing info ' 2919 'about result dir.') 2920 self._device_repair_state = state 2921 2922 def set_device_needs_replacement(self, resultdir=None): 2923 """Set device as required replacement. 2924 2925 @params resultdir: The path to result directory. If path not provided 2926 will be attempt to get retrieve it from job 2927 if present. 2928 """ 2929 self.set_device_repair_state( 2930 cros_constants.DEVICE_STATE_NEEDS_REPLACEMENT, 2931 resultdir=resultdir) 2932 2933 def _dut_fail_ssh_verifier(self): 2934 """Check if DUT failed SSH verifier. 2935 2936 @returns: bool, True - verifier marked as fail. 2937 False - result not reachable, verifier did not fail. 2938 """ 2939 if not self._repair_strategy: 2940 return False 2941 dut_ssh_verifier = self._repair_strategy.verifier_is_good('ssh') 2942 return dut_ssh_verifier == hosts.VERIFY_FAILED 2943 2944 def _stat_if_pingable_but_not_sshable(self): 2945 """Check if DUT pingable but failed SSH verifier.""" 2946 if not self._repair_strategy: 2947 return 2948 dut_ssh = self._repair_strategy.verifier_is_good('ssh') 2949 dut_ping = self._repair_strategy.verifier_is_good('ping') 2950 if (dut_ping == hosts.VERIFY_FAILED 2951 and dut_ssh == hosts.VERIFY_FAILED): 2952 metrics.Counter('chromeos/autotest/dut_pingable_no_ssh').increment( 2953 fields={'host': self.hostname}) 2954 2955 def try_set_device_needs_manual_repair(self): 2956 """Check if device require manual attention to be fixed. 2957 2958 The state 'needs_manual_repair' can be set when auto repair cannot 2959 fix the device due hardware or cable issues. 2960 """ 2961 # ignore the logic if state present 2962 # state can be set by any cros repair actions 2963 if self.get_device_repair_state(): 2964 return 2965 if not self._dut_fail_ssh_verifier(): 2966 # DUT is sshable and we still have many options to repair it. 2967 return 2968 needs_manual_repair = False 2969 dhp = self.health_profile 2970 if dhp and dhp.get_repair_fail_count() > 49: 2971 # 42 = 6 times during 7 days. (every 4 hour repair) 2972 # round up to 50 in case somebody will run some attempt on it. 2973 logging.info( 2974 'DUT is not sshable and fail %s times.' 2975 ' Limit to try repair is 50 times', 2976 dhp.get_repair_fail_count()) 2977 needs_manual_repair = True 2978 2979 if not needs_manual_repair: 2980 # We cannot ssh to the DUT and we have hardware or set-up issues 2981 # with servo then we need request manual repair for the DUT. 2982 servo_state_required_manual_fix = [ 2983 servo_constants.SERVO_STATE_DUT_NOT_CONNECTED, 2984 servo_constants.SERVO_STATE_NEED_REPLACEMENT, 2985 ] 2986 if self.get_servo_state() in servo_state_required_manual_fix: 2987 logging.info( 2988 'DUT required manual repair because it is not sshable' 2989 ' and possible have setup issue with Servo. Please' 2990 ' verify all connections and present of devices.') 2991 needs_manual_repair = True 2992 2993 if needs_manual_repair: 2994 self.set_device_repair_state( 2995 cros_constants.DEVICE_STATE_NEEDS_MANUAL_REPAIR) 2996 2997 def _reboot_labstation_if_needed(self): 2998 """Place request to reboot the labstation if DUT is not sshable. 2999 3000 @returns: None 3001 """ 3002 message_prefix = "Don't need to request servo-host reboot " 3003 if not self._dut_fail_ssh_verifier(): 3004 return 3005 if not self._servo_host: 3006 logging.debug(message_prefix + 'as it not initialized') 3007 return 3008 if not self._servo_host.is_up_fast(): 3009 logging.debug(message_prefix + 'as servo-host is not sshable') 3010 return 3011 if not self._servo_host.is_labstation(): 3012 logging.debug('Servo_v3 is not requested to reboot for the DUT') 3013 return 3014 usb_path = self._servo_host.get_main_servo_usb_path() 3015 if usb_path: 3016 connected_port = os.path.basename(os.path.normpath(usb_path)) 3017 # Directly connected servo to the labstation looks like '1-5.3' 3018 # and when connected by hub - '1-5.2.3' or '1-5.2.1.3'. Where: 3019 # - '1-5' - port on labstation 3020 # - '2' or '2.1' - port on the hub or smart-hub 3021 # - '3' - port on servo hub 3022 if len(connected_port.split('.')) > 2: 3023 logging.debug(message_prefix + 'as servo connected by hub') 3024 return 3025 self._servo_host.request_reboot() 3026 logging.info('Requested labstation reboot because DUT is not sshable') 3027 3028 def is_file_system_writable(self, testdirs=None): 3029 """Check is the file systems are writable. 3030 3031 The standard linux response to certain unexpected file system errors 3032 (including hardware errors in block devices) is to change the file 3033 system status to read-only. This checks that that hasn't happened. 3034 3035 @param testdirs: List of directories to check. If no data provided 3036 then '/mnt/stateful_partition' and '/var/tmp' 3037 directories will be checked. 3038 3039 @returns boolean whether file-system writable. 3040 """ 3041 def _check_dir(testdir): 3042 # check if we can create a file 3043 filename = os.path.join(testdir, 'writable_my_test_file') 3044 command = 'touch %s && rm %s' % (filename, filename) 3045 rv = self.run(command=command, 3046 timeout=30, 3047 ignore_status=True) 3048 is_writable = rv.exit_status == 0 3049 if not is_writable: 3050 logging.info('Cannot create a file in "%s"!' 3051 ' Probably the FS is read-only', testdir) 3052 logging.info("FileSystem is not writable!") 3053 return False 3054 return True 3055 3056 if not testdirs or len(testdirs) == 0: 3057 # N.B. Order matters here: Encrypted stateful is loop-mounted 3058 # from a file in unencrypted stateful, so we don't test for 3059 # errors in encrypted stateful if unencrypted fails. 3060 testdirs = ['/mnt/stateful_partition', '/var/tmp'] 3061 3062 for dir in testdirs: 3063 # loop will be stopped if any directory fill fail the check 3064 try: 3065 if not _check_dir(dir): 3066 return False 3067 except Exception as e: 3068 # here expected only timeout error, all other will 3069 # be catch by 'ignore_status=True' 3070 logging.debug('Fail to check %s to write in it', dir) 3071 return False 3072 return True 3073 3074 def blocking_sync(self, freeze_for_reset=False): 3075 """Sync root device and internal device, via script. 3076 3077 The actual calls end up logged by the run() call, since they're printed 3078 to stdout/stderr in the script. 3079 3080 @param freeze_for_reset: if True, prepare for reset by blocking writes 3081 (only if enable_fs_sync_fsfreeze=True) 3082 """ 3083 3084 if freeze_for_reset and self.USE_FSFREEZE: 3085 logging.info('Blocking sync and freeze') 3086 elif freeze_for_reset: 3087 logging.info('Blocking sync for reset') 3088 else: 3089 logging.info('Blocking sync') 3090 3091 # client/bin is installed on the DUT as /usr/local/autotest/bin 3092 sync_cmd = '/usr/local/autotest/bin/fs_sync.py' 3093 if freeze_for_reset and self.USE_FSFREEZE: 3094 sync_cmd += ' --freeze' 3095 return self.run(sync_cmd) 3096 3097 def set_health_profile_dut_state(self, state): 3098 if not self.health_profile: 3099 logging.debug('Device health profile is not initialized, skip' 3100 ' set dut state.') 3101 return 3102 reset_counters = state in profile_constants.STATES_NEED_RESET_COUNTER 3103 self.health_profile.update_dut_state(state, reset_counters) 3104 3105 def require_snk_mode_in_recovery(self): 3106 """Check whether we need to switch servo_v4 role to snk when 3107 booting into recovery mode. (See crbug.com/1129165) 3108 """ 3109 has_battery = True 3110 # Determine if the host has battery based on host_info first. 3111 power_info = self.host_info_store.get().get_label_value('power') 3112 if power_info: 3113 has_battery = power_info == 'battery' 3114 elif self.is_up_fast(): 3115 # when running local tests host_info is not available, so we 3116 # need to determine whether the host has battery by checking 3117 # from host side. 3118 logging.debug('Label `power` is not found in host_info, checking' 3119 ' if the host has battery from host side.') 3120 has_battery = self.has_battery() 3121 3122 if not has_battery: 3123 logging.info( 3124 '%s does not has battery, snk mode is not needed' 3125 ' for recovery.', self.hostname) 3126 return False 3127 3128 if not self.servo.supports_built_in_pd_control(): 3129 logging.info('Power delivery is not supported on this servo, snk' 3130 ' mode is not needed for recovery.') 3131 return False 3132 try: 3133 battery_percent = self.servo.get('battery_charge_percent') 3134 if battery_percent < cros_constants.MIN_BATTERY_LEVEL: 3135 logging.info( 3136 'Current battery level %s%% below %s%% threshold, we' 3137 ' will attempt to boot host in recovery mode without' 3138 ' changing servo to snk mode. Please note the host may' 3139 ' not able to see usb drive in recovery mode later due' 3140 ' to servo not in snk mode.', battery_percent, 3141 cros_constants.MIN_BATTERY_LEVEL) 3142 return False 3143 except Exception as e: 3144 logging.info( 3145 'Unexpected error occurred when getting' 3146 ' battery_charge_percent from servo; %s', str(e)) 3147 return False 3148 return True 3149 3150 def _set_servo_topology(self): 3151 """Set servo-topology info to the host-info.""" 3152 logging.debug('Try to save servo topology to host-info.') 3153 if not self._servo_host: 3154 logging.info('Servo host is not initilized.') 3155 return 3156 if not self._servo_host.is_servo_topology_supported(): 3157 logging.info('Servo-topology is not supported.') 3158 return 3159 servo_topology = self._servo_host.get_topology() 3160 if not servo_topology or servo_topology.is_empty(): 3161 logging.info('Servo topology is empty') 3162 return 3163 servo_topology.save(self.host_info_store) 3164