provision_devices.py (22301B)
1 #!/usr/bin/env vpython3 2 # 3 # Copyright 2013 The Chromium Authors 4 # Use of this source code is governed by a BSD-style license that can be 5 # found in the LICENSE file. 6 7 """Provisions Android devices with settings required for bots. 8 9 Usage: 10 ./provision_devices.py [-d <device serial number>] 11 """ 12 13 import argparse 14 import datetime 15 import json 16 import logging 17 import os 18 import posixpath 19 import re 20 import subprocess 21 import sys 22 import time 23 24 # Import _strptime before threaded code. datetime.datetime.strptime is 25 # threadsafe except for the initial import of the _strptime module. 26 # See crbug.com/584730 and https://bugs.python.org/issue7980. 27 import _strptime # pylint: disable=unused-import 28 29 import devil_chromium 30 from devil.android import battery_utils 31 from devil.android import device_denylist 32 from devil.android import device_errors 33 from devil.android import device_temp_file 34 from devil.android import device_utils 35 from devil.android.sdk import keyevent 36 from devil.android.sdk import version_codes 37 from devil.constants import exit_codes 38 from devil.utils import run_tests_helper 39 from devil.utils import timeout_retry 40 from pylib import constants 41 from pylib import device_settings 42 from pylib.constants import host_paths 43 44 _SYSTEM_WEBVIEW_PATHS = ['/system/app/webview', '/system/app/WebViewGoogle'] 45 _CHROME_PACKAGE_REGEX = re.compile('.*chrom.*') 46 _TOMBSTONE_REGEX = re.compile('tombstone.*') 47 48 49 class _DEFAULT_TIMEOUTS: 50 # L can take a while to reboot after a wipe. 51 LOLLIPOP = 600 52 PRE_LOLLIPOP = 180 53 54 HELP_TEXT = '{}s on L, {}s on pre-L'.format(LOLLIPOP, PRE_LOLLIPOP) 55 56 57 class _PHASES: 58 WIPE = 'wipe' 59 PROPERTIES = 'properties' 60 FINISH = 'finish' 61 62 ALL = [WIPE, PROPERTIES, FINISH] 63 64 65 def ProvisionDevices(args): 66 denylist = (device_denylist.Denylist(args.denylist_file) 67 if args.denylist_file else None) 68 devices = [ 69 d for d in device_utils.DeviceUtils.HealthyDevices(denylist) 70 if not args.emulators or d.is_emulator 71 ] 72 if args.device: 73 devices = [d for d in devices if d == args.device] 74 if not devices: 75 raise device_errors.DeviceUnreachableError(args.device) 76 parallel_devices = device_utils.DeviceUtils.parallel(devices) 77 if args.emulators: 78 parallel_devices.pMap(SetProperties, args) 79 else: 80 parallel_devices.pMap(ProvisionDevice, denylist, args) 81 if args.auto_reconnect: 82 _LaunchHostHeartbeat() 83 denylisted_devices = denylist.Read() if denylist else [] 84 if args.output_device_denylist: 85 with open(args.output_device_denylist, 'w') as f: 86 json.dump(denylisted_devices, f) 87 if all(d in denylisted_devices for d in devices): 88 raise device_errors.NoDevicesError 89 return 0 90 91 92 def ProvisionDevice(device, denylist, options): 93 def should_run_phase(phase_name): 94 return not options.phases or phase_name in options.phases 95 96 def run_phase(phase_func, reboot_timeout, reboot=True): 97 try: 98 device.WaitUntilFullyBooted(timeout=reboot_timeout, retries=0) 99 except device_errors.CommandTimeoutError: 100 logging.error('Device did not finish booting. Will try to reboot.') 101 device.Reboot(timeout=reboot_timeout) 102 phase_func(device, options) 103 if reboot: 104 device.Reboot(False, retries=0) 105 device.adb.WaitForDevice() 106 107 try: 108 if options.reboot_timeout: 109 reboot_timeout = options.reboot_timeout 110 elif device.build_version_sdk >= version_codes.LOLLIPOP: 111 reboot_timeout = _DEFAULT_TIMEOUTS.LOLLIPOP 112 else: 113 reboot_timeout = _DEFAULT_TIMEOUTS.PRE_LOLLIPOP 114 115 if should_run_phase(_PHASES.WIPE): 116 if (options.chrome_specific_wipe or device.IsUserBuild() or 117 device.build_version_sdk >= version_codes.MARSHMALLOW): 118 run_phase(WipeChromeData, reboot_timeout) 119 else: 120 run_phase(WipeDevice, reboot_timeout) 121 122 if should_run_phase(_PHASES.PROPERTIES): 123 run_phase(SetProperties, reboot_timeout) 124 125 if should_run_phase(_PHASES.FINISH): 126 run_phase(FinishProvisioning, reboot_timeout, reboot=False) 127 128 if options.chrome_specific_wipe: 129 package = "com.google.android.gms" 130 version_name = device.GetApplicationVersion(package) 131 logging.info("Version name for %s is %s", package, version_name) 132 133 CheckExternalStorage(device) 134 135 except device_errors.CommandTimeoutError: 136 logging.exception('Timed out waiting for device %s. Adding to denylist.', 137 str(device)) 138 if denylist: 139 denylist.Extend([str(device)], reason='provision_timeout') 140 141 except (device_errors.CommandFailedError, 142 device_errors.DeviceUnreachableError): 143 logging.exception('Failed to provision device %s. Adding to denylist.', 144 str(device)) 145 if denylist: 146 denylist.Extend([str(device)], reason='provision_failure') 147 148 149 def CheckExternalStorage(device): 150 """Checks that storage is writable and if not makes it writable. 151 152 Arguments: 153 device: The device to check. 154 """ 155 try: 156 with device_temp_file.DeviceTempFile( 157 device.adb, suffix='.sh', dir=device.GetExternalStoragePath()) as f: 158 device.WriteFile(f.name, 'test') 159 except device_errors.CommandFailedError: 160 logging.info('External storage not writable. Remounting / as RW') 161 device.RunShellCommand(['mount', '-o', 'remount,rw', '/'], 162 check_return=True, as_root=True) 163 device.EnableRoot() 164 with device_temp_file.DeviceTempFile( 165 device.adb, suffix='.sh', dir=device.GetExternalStoragePath()) as f: 166 device.WriteFile(f.name, 'test') 167 168 def WipeChromeData(device, options): 169 """Wipes chrome specific data from device 170 171 (1) uninstall any app whose name matches *chrom*, except 172 com.android.chrome, which is the chrome stable package. Doing so also 173 removes the corresponding dirs under /data/data/ and /data/app/ 174 (2) remove any dir under /data/app-lib/ whose name matches *chrom* 175 (3) remove any files under /data/tombstones/ whose name matches "tombstone*" 176 (4) remove /data/local.prop if there is any 177 (5) remove /data/local/chrome-command-line if there is any 178 (6) remove anything under /data/local/.config/ if the dir exists 179 (this is telemetry related) 180 (7) remove anything under /data/local/tmp/ 181 182 Arguments: 183 device: the device to wipe 184 """ 185 if options.skip_wipe: 186 return 187 188 try: 189 if device.IsUserBuild(): 190 _UninstallIfMatch(device, _CHROME_PACKAGE_REGEX, 191 constants.PACKAGE_INFO['chrome_stable'].package) 192 device.RunShellCommand('rm -rf %s/*' % device.GetExternalStoragePath(), 193 check_return=True) 194 device.RunShellCommand('rm -rf /data/local/tmp/*', check_return=True) 195 else: 196 device.EnableRoot() 197 _UninstallIfMatch(device, _CHROME_PACKAGE_REGEX, 198 constants.PACKAGE_INFO['chrome_stable'].package) 199 _WipeUnderDirIfMatch(device, '/data/app-lib/', _CHROME_PACKAGE_REGEX) 200 _WipeUnderDirIfMatch(device, '/data/tombstones/', _TOMBSTONE_REGEX) 201 202 _WipeFileOrDir(device, '/data/local.prop') 203 _WipeFileOrDir(device, '/data/local/chrome-command-line') 204 _WipeFileOrDir(device, '/data/local/.config/') 205 _WipeFileOrDir(device, '/data/local/tmp/') 206 device.RunShellCommand('rm -rf %s/*' % device.GetExternalStoragePath(), 207 check_return=True) 208 except device_errors.CommandFailedError: 209 logging.exception('Possible failure while wiping the device. ' 210 'Attempting to continue.') 211 212 213 def WipeDevice(device, options): 214 """Wipes data from device, keeping only the adb_keys for authorization. 215 216 After wiping data on a device that has been authorized, adb can still 217 communicate with the device, but after reboot the device will need to be 218 re-authorized because the adb keys file is stored in /data/misc/adb/. 219 Thus, adb_keys file is rewritten so the device does not need to be 220 re-authorized. 221 222 Arguments: 223 device: the device to wipe 224 """ 225 if options.skip_wipe: 226 return 227 228 try: 229 device.EnableRoot() 230 device_authorized = device.FileExists(constants.ADB_KEYS_FILE) 231 if device_authorized: 232 adb_keys = device.ReadFile(constants.ADB_KEYS_FILE, 233 as_root=True).splitlines() 234 device.RunShellCommand(['wipe', 'data'], 235 as_root=True, check_return=True) 236 device.adb.WaitForDevice() 237 238 if device_authorized: 239 adb_keys_set = set(adb_keys) 240 for adb_key_file in options.adb_key_files or []: 241 try: 242 with open(adb_key_file, 'r') as f: 243 adb_public_keys = f.readlines() 244 adb_keys_set.update(adb_public_keys) 245 except IOError: 246 logging.warning('Unable to find adb keys file %s.', adb_key_file) 247 _WriteAdbKeysFile(device, '\n'.join(adb_keys_set)) 248 except device_errors.CommandFailedError: 249 logging.exception('Possible failure while wiping the device. ' 250 'Attempting to continue.') 251 252 253 def _WriteAdbKeysFile(device, adb_keys_string): 254 dir_path = posixpath.dirname(constants.ADB_KEYS_FILE) 255 device.RunShellCommand(['mkdir', '-p', dir_path], 256 as_root=True, check_return=True) 257 device.RunShellCommand(['restorecon', dir_path], 258 as_root=True, check_return=True) 259 device.WriteFile(constants.ADB_KEYS_FILE, adb_keys_string, as_root=True) 260 device.RunShellCommand(['restorecon', constants.ADB_KEYS_FILE], 261 as_root=True, check_return=True) 262 263 264 def SetProperties(device, options): 265 try: 266 device.EnableRoot() 267 except device_errors.CommandFailedError as e: 268 logging.warning(str(e)) 269 270 if not device.IsUserBuild(): 271 _ConfigureLocalProperties(device, options.enable_java_debug) 272 else: 273 logging.warning('Cannot configure properties in user builds.') 274 device_settings.ConfigureContentSettings( 275 device, device_settings.DETERMINISTIC_DEVICE_SETTINGS) 276 if options.disable_location: 277 device_settings.ConfigureContentSettings( 278 device, device_settings.DISABLE_LOCATION_SETTINGS) 279 else: 280 device_settings.ConfigureContentSettings( 281 device, device_settings.ENABLE_LOCATION_SETTINGS) 282 283 if options.disable_mock_location: 284 device_settings.ConfigureContentSettings( 285 device, device_settings.DISABLE_MOCK_LOCATION_SETTINGS) 286 else: 287 device_settings.ConfigureContentSettings( 288 device, device_settings.ENABLE_MOCK_LOCATION_SETTINGS) 289 290 device_settings.SetLockScreenSettings(device) 291 if options.disable_network: 292 device_settings.ConfigureContentSettings( 293 device, device_settings.NETWORK_DISABLED_SETTINGS) 294 if device.build_version_sdk >= version_codes.MARSHMALLOW: 295 # Ensure that NFC is also switched off. 296 device.RunShellCommand(['svc', 'nfc', 'disable'], 297 as_root=True, check_return=True) 298 299 if options.disable_system_chrome: 300 # The system chrome version on the device interferes with some tests. 301 device.RunShellCommand(['pm', 'disable', 'com.android.chrome'], 302 check_return=True) 303 304 if options.remove_system_webview: 305 if any(device.PathExists(p) for p in _SYSTEM_WEBVIEW_PATHS): 306 logging.info('System WebView exists and needs to be removed') 307 if device.HasRoot(): 308 # Disabled Marshmallow's Verity security feature 309 if device.build_version_sdk >= version_codes.MARSHMALLOW: 310 device.adb.DisableVerity() 311 device.Reboot() 312 device.WaitUntilFullyBooted() 313 device.EnableRoot() 314 315 # This is required, e.g., to replace the system webview on a device. 316 device.adb.Remount() 317 device.RunShellCommand(['stop'], check_return=True) 318 device.RunShellCommand(['rm', '-rf'] + _SYSTEM_WEBVIEW_PATHS, 319 check_return=True) 320 device.RunShellCommand(['start'], check_return=True) 321 else: 322 logging.warning('Cannot remove system webview from a non-rooted device') 323 else: 324 logging.info('System WebView already removed') 325 326 # Some device types can momentarily disappear after setting properties. 327 device.adb.WaitForDevice() 328 329 330 def _ConfigureLocalProperties(device, java_debug=True): 331 """Set standard readonly testing device properties prior to reboot.""" 332 local_props = [ 333 'persist.sys.usb.config=adb', 334 'ro.monkey=1', 335 'ro.test_harness=1', 336 'ro.audio.silent=1', 337 'ro.setupwizard.mode=DISABLED', 338 ] 339 if java_debug: 340 local_props.append( 341 '%s=all' % device_utils.DeviceUtils.JAVA_ASSERT_PROPERTY) 342 local_props.append('debug.checkjni=1') 343 try: 344 device.WriteFile( 345 device.LOCAL_PROPERTIES_PATH, 346 '\n'.join(local_props), as_root=True) 347 # Android will not respect the local props file if it is world writable. 348 device.RunShellCommand( 349 ['chmod', '644', device.LOCAL_PROPERTIES_PATH], 350 as_root=True, check_return=True) 351 except device_errors.CommandFailedError: 352 logging.exception('Failed to configure local properties.') 353 354 355 def FinishProvisioning(device, options): 356 # The lockscreen can't be disabled on user builds, so send a keyevent 357 # to unlock it. 358 if device.IsUserBuild(): 359 device.SendKeyEvent(keyevent.KEYCODE_MENU) 360 361 if options.min_battery_level is not None: 362 battery = battery_utils.BatteryUtils(device) 363 try: 364 battery.ChargeDeviceToLevel(options.min_battery_level) 365 except device_errors.DeviceChargingError: 366 device.Reboot() 367 battery.ChargeDeviceToLevel(options.min_battery_level) 368 369 if options.max_battery_temp is not None: 370 try: 371 battery = battery_utils.BatteryUtils(device) 372 battery.LetBatteryCoolToTemperature(options.max_battery_temp) 373 except device_errors.CommandFailedError: 374 logging.exception('Unable to let battery cool to specified temperature.') 375 376 def _set_and_verify_date(): 377 if device.build_version_sdk >= version_codes.MARSHMALLOW: 378 date_format = '%m%d%H%M%Y.%S' 379 set_date_command = ['date', '-u'] 380 get_date_command = ['date', '-u'] 381 else: 382 date_format = '%Y%m%d.%H%M%S' 383 set_date_command = ['date', '-s'] 384 get_date_command = ['date'] 385 386 # TODO(jbudorick): This is wrong on pre-M devices -- get/set are 387 # dealing in local time, but we're setting based on GMT. 388 strgmtime = time.strftime(date_format, time.gmtime()) 389 set_date_command.append(strgmtime) 390 device.RunShellCommand(set_date_command, as_root=True, check_return=True) 391 392 get_date_command.append('+"%Y%m%d.%H%M%S"') 393 device_time = device.RunShellCommand( 394 get_date_command, as_root=True, single_line=True).replace('"', '') 395 device_time = datetime.datetime.strptime(device_time, "%Y%m%d.%H%M%S") 396 correct_time = datetime.datetime.strptime(strgmtime, date_format) 397 tdelta = abs(correct_time - device_time).seconds 398 if tdelta <= 1: 399 logging.info('Date/time successfully set on %s', device) 400 return True 401 logging.error('Date mismatch. Device: %s Correct: %s', 402 device_time.isoformat(), correct_time.isoformat()) 403 return False 404 405 # Sometimes the date is not set correctly on the devices. Retry on failure. 406 if device.IsUserBuild(): 407 # TODO(bpastene): Figure out how to set the date & time on user builds. 408 pass 409 else: 410 if not timeout_retry.WaitFor( 411 _set_and_verify_date, wait_period=1, max_tries=2): 412 raise device_errors.CommandFailedError( 413 'Failed to set date & time.', device_serial=str(device)) 414 415 props = device.RunShellCommand('getprop', check_return=True) 416 for prop in props: 417 logging.info(' %s', prop) 418 if options.auto_reconnect: 419 _PushAndLaunchAdbReboot(device, options.target) 420 421 422 def _UninstallIfMatch(device, pattern, app_to_keep): 423 installed_packages = device.RunShellCommand(['pm', 'list', 'packages']) 424 installed_system_packages = [ 425 pkg.split(':')[1] for pkg in device.RunShellCommand(['pm', 'list', 426 'packages', '-s'])] 427 for package_output in installed_packages: 428 package = package_output.split(":")[1] 429 if pattern.match(package) and not package == app_to_keep: 430 if not device.IsUserBuild() or package not in installed_system_packages: 431 device.Uninstall(package) 432 433 434 def _WipeUnderDirIfMatch(device, path, pattern): 435 for filename in device.ListDirectory(path): 436 if pattern.match(filename): 437 _WipeFileOrDir(device, posixpath.join(path, filename)) 438 439 440 def _WipeFileOrDir(device, path): 441 if device.PathExists(path): 442 device.RunShellCommand(['rm', '-rf', path], check_return=True) 443 444 445 def _PushAndLaunchAdbReboot(device, target): 446 """Pushes and launches the adb_reboot binary on the device. 447 448 Arguments: 449 device: The DeviceUtils instance for the device to which the adb_reboot 450 binary should be pushed. 451 target: The build target (example, Debug or Release) which helps in 452 locating the adb_reboot binary. 453 """ 454 logging.info('Will push and launch adb_reboot on %s', str(device)) 455 # Kill if adb_reboot is already running. 456 device.KillAll('adb_reboot', blocking=True, timeout=2, quiet=True) 457 # Push adb_reboot 458 logging.info(' Pushing adb_reboot ...') 459 adb_reboot = os.path.join(host_paths.DIR_SOURCE_ROOT, 460 'out/%s/adb_reboot' % target) 461 device.PushChangedFiles([(adb_reboot, '/data/local/tmp/')]) 462 # Launch adb_reboot 463 logging.info(' Launching adb_reboot ...') 464 device.RunShellCommand( 465 ['/data/local/tmp/adb_reboot'], 466 check_return=True) 467 468 469 def _LaunchHostHeartbeat(): 470 # Kill if existing host_heartbeat 471 KillHostHeartbeat() 472 # Launch a new host_heartbeat 473 logging.info('Spawning host heartbeat...') 474 subprocess.Popen([os.path.join(host_paths.DIR_SOURCE_ROOT, 475 'build/android/host_heartbeat.py')]) 476 477 def KillHostHeartbeat(): 478 ps = subprocess.Popen(['ps', 'aux'], stdout=subprocess.PIPE) 479 stdout, _ = ps.communicate() 480 matches = re.findall('\\n.*host_heartbeat.*', stdout) 481 for match in matches: 482 logging.info('An instance of host heart beart running... will kill') 483 pid = re.findall(r'(\S+)', match)[1] 484 subprocess.call(['kill', str(pid)]) 485 486 def main(): 487 # Recommended options on perf bots: 488 # --disable-network 489 # TODO(tonyg): We eventually want network on. However, currently radios 490 # can cause perfbots to drain faster than they charge. 491 # --min-battery-level 95 492 # Some perf bots run benchmarks with USB charging disabled which leads 493 # to gradual draining of the battery. We must wait for a full charge 494 # before starting a run in order to keep the devices online. 495 496 parser = argparse.ArgumentParser( 497 description='Provision Android devices with settings required for bots.') 498 parser.add_argument('-d', '--device', metavar='SERIAL', 499 help='the serial number of the device to be provisioned' 500 ' (the default is to provision all devices attached)') 501 parser.add_argument('--adb-path', 502 help='Absolute path to the adb binary to use.') 503 parser.add_argument('--denylist-file', help='Device denylist JSON file.') 504 parser.add_argument('--phase', action='append', choices=_PHASES.ALL, 505 dest='phases', 506 help='Phases of provisioning to run. ' 507 '(If omitted, all phases will be run.)') 508 parser.add_argument('--skip-wipe', action='store_true', default=False, 509 help="don't wipe device data during provisioning") 510 parser.add_argument('--reboot-timeout', metavar='SECS', type=int, 511 help='when wiping the device, max number of seconds to' 512 ' wait after each reboot ' 513 '(default: %s)' % _DEFAULT_TIMEOUTS.HELP_TEXT) 514 parser.add_argument('--min-battery-level', type=int, metavar='NUM', 515 help='wait for the device to reach this minimum battery' 516 ' level before trying to continue') 517 parser.add_argument('--disable-location', action='store_true', 518 help='disable Google location services on devices') 519 parser.add_argument('--disable-mock-location', action='store_true', 520 default=False, help='Set ALLOW_MOCK_LOCATION to false') 521 parser.add_argument('--disable-network', action='store_true', 522 help='disable network access on devices') 523 parser.add_argument('--disable-java-debug', action='store_false', 524 dest='enable_java_debug', default=True, 525 help='disable Java property asserts and JNI checking') 526 parser.add_argument('--disable-system-chrome', action='store_true', 527 help='Disable the system chrome from devices.') 528 parser.add_argument('--remove-system-webview', action='store_true', 529 help='Remove the system webview from devices.') 530 parser.add_argument('-t', '--target', default='Debug', 531 help='the build target (default: %(default)s)') 532 parser.add_argument('-r', '--auto-reconnect', action='store_true', 533 help='push binary which will reboot the device on adb' 534 ' disconnections') 535 parser.add_argument('--adb-key-files', type=str, nargs='+', 536 help='list of adb keys to push to device') 537 parser.add_argument('-v', '--verbose', action='count', default=1, 538 help='Log more information.') 539 parser.add_argument('--max-battery-temp', type=int, metavar='NUM', 540 help='Wait for the battery to have this temp or lower.') 541 parser.add_argument('--output-device-denylist', 542 help='Json file to output the device denylist.') 543 parser.add_argument('--chrome-specific-wipe', action='store_true', 544 help='only wipe chrome specific data during provisioning') 545 parser.add_argument('--emulators', action='store_true', 546 help='provision only emulators and ignore usb devices') 547 args = parser.parse_args() 548 constants.SetBuildType(args.target) 549 550 run_tests_helper.SetLogLevel(args.verbose) 551 552 devil_chromium.Initialize(adb_path=args.adb_path) 553 554 try: 555 return ProvisionDevices(args) 556 except (device_errors.DeviceUnreachableError, device_errors.NoDevicesError): 557 logging.exception('Unable to provision local devices.') 558 return exit_codes.INFRA 559 560 561 if __name__ == '__main__': 562 sys.exit(main())