1#!/usr/bin/env python2 2# 3# Copyright 2011 Google Inc. All Rights Reserved. 4"""Script to image a ChromeOS device. 5 6This script images a remote ChromeOS device with a specific image." 7""" 8 9from __future__ import print_function 10 11__author__ = 'asharif@google.com (Ahmad Sharif)' 12 13import argparse 14import filecmp 15import getpass 16import glob 17import os 18import re 19import shutil 20import sys 21import tempfile 22import time 23 24from cros_utils import command_executer 25from cros_utils import locks 26from cros_utils import logger 27from cros_utils import misc 28from cros_utils.file_utils import FileUtils 29 30checksum_file = '/usr/local/osimage_checksum_file' 31lock_file = '/tmp/image_chromeos_lock/image_chromeos_lock' 32 33 34def Usage(parser, message): 35 print('ERROR: %s' % message) 36 parser.print_help() 37 sys.exit(0) 38 39 40def CheckForCrosFlash(chromeos_root, remote, log_level): 41 cmd_executer = command_executer.GetCommandExecuter(log_level=log_level) 42 43 # Check to see if remote machine has cherrypy, ctypes 44 command = "python -c 'import cherrypy, ctypes'" 45 ret = cmd_executer.CrosRunCommand( 46 command, chromeos_root=chromeos_root, machine=remote) 47 logger.GetLogger().LogFatalIf( 48 ret == 255, 'Failed ssh to %s (for checking cherrypy)' % remote) 49 logger.GetLogger().LogFatalIf( 50 ret != 0, "Failed to find cherrypy or ctypes on remote '{}', " 51 'cros flash cannot work.'.format(remote)) 52 53 54def DisableCrosBeeps(chromeos_root, remote, log_level): 55 """Disable annoying chromebooks beeps after reboots.""" 56 cmd_executer = command_executer.GetCommandExecuter(log_level=log_level) 57 58 command = '/usr/share/vboot/bin/set_gbb_flags.sh 0x1' 59 logger.GetLogger().LogOutput('Trying to disable beeping.') 60 61 ret, o, _ = cmd_executer.CrosRunCommandWOutput( 62 command, chromeos_root=chromeos_root, machine=remote) 63 if ret != 0: 64 logger.GetLogger().LogOutput(o) 65 logger.GetLogger().LogOutput('Failed to disable beeps.') 66 67 68def FindChromeOSImage(image_file, chromeos_root): 69 """Find path for ChromeOS image inside chroot. 70 71 This function could be called with image paths that are either inside 72 or outside the chroot. In either case the path needs to be translated 73 to an real/absolute path inside the chroot. 74 Example input paths: 75 /usr/local/google/home/uname/chromeos/chroot/tmp/my-test-images/image 76 ~/trunk/src/build/images/board/latest/image 77 /tmp/peppy-release/R67-1235.0.0/image 78 79 Corresponding example output paths: 80 /tmp/my-test-images/image 81 /home/uname/trunk/src/build/images/board/latest/image 82 /tmp/peppy-release/R67-1235.0,0/image 83 """ 84 85 # Get the name of the user, for "/home/<user>" part of the path. 86 whoami = getpass.getuser() 87 # Get the full path for the chroot dir, including 'chroot' 88 real_chroot_dir = os.path.join(os.path.realpath(chromeos_root), 'chroot') 89 # Get the full path for the chromeos root, excluding 'chroot' 90 real_chromeos_root = os.path.realpath(chromeos_root) 91 92 # If path name starts with real_chroot_dir, remove that piece, but assume 93 # the rest of the path is correct. 94 if image_file.find(real_chroot_dir) != -1: 95 chroot_image = image_file[len(real_chroot_dir):] 96 # If path name starts with chromeos_root, excluding 'chroot', replace the 97 # chromeos_root with the prefix: '/home/<username>/trunk'. 98 elif image_file.find(real_chromeos_root) != -1: 99 chroot_image = image_file[len(real_chromeos_root):] 100 chroot_image = '/home/%s/trunk%s' % (whoami, chroot_image) 101 # Else assume the path is already internal, so leave it alone. 102 else: 103 chroot_image = image_file 104 105 return chroot_image 106 107 108def DoImage(argv): 109 """Image ChromeOS.""" 110 111 parser = argparse.ArgumentParser() 112 parser.add_argument( 113 '-c', 114 '--chromeos_root', 115 dest='chromeos_root', 116 help='Target directory for ChromeOS installation.') 117 parser.add_argument('-r', '--remote', dest='remote', help='Target device.') 118 parser.add_argument('-i', '--image', dest='image', help='Image binary file.') 119 parser.add_argument( 120 '-b', '--board', dest='board', help='Target board override.') 121 parser.add_argument( 122 '-f', 123 '--force', 124 dest='force', 125 action='store_true', 126 default=False, 127 help='Force an image even if it is non-test.') 128 parser.add_argument( 129 '-n', 130 '--no_lock', 131 dest='no_lock', 132 default=False, 133 action='store_true', 134 help='Do not attempt to lock remote before imaging. ' 135 'This option should only be used in cases where the ' 136 'exclusive lock has already been acquired (e.g. in ' 137 'a script that calls this one).') 138 parser.add_argument( 139 '-l', 140 '--logging_level', 141 dest='log_level', 142 default='verbose', 143 help='Amount of logging to be used. Valid levels are ' 144 "'quiet', 'average', and 'verbose'.") 145 parser.add_argument('-a', '--image_args', dest='image_args') 146 147 options = parser.parse_args(argv[1:]) 148 149 if not options.log_level in command_executer.LOG_LEVEL: 150 Usage(parser, "--logging_level must be 'quiet', 'average' or 'verbose'") 151 else: 152 log_level = options.log_level 153 154 # Common initializations 155 cmd_executer = command_executer.GetCommandExecuter(log_level=log_level) 156 l = logger.GetLogger() 157 158 if options.chromeos_root is None: 159 Usage(parser, '--chromeos_root must be set') 160 161 if options.remote is None: 162 Usage(parser, '--remote must be set') 163 164 options.chromeos_root = os.path.expanduser(options.chromeos_root) 165 166 if options.board is None: 167 board = cmd_executer.CrosLearnBoard(options.chromeos_root, options.remote) 168 else: 169 board = options.board 170 171 if options.image is None: 172 images_dir = misc.GetImageDir(options.chromeos_root, board) 173 image = os.path.join(images_dir, 'latest', 'chromiumos_test_image.bin') 174 if not os.path.exists(image): 175 image = os.path.join(images_dir, 'latest', 'chromiumos_image.bin') 176 is_xbuddy_image = False 177 else: 178 image = options.image 179 is_xbuddy_image = image.startswith('xbuddy://') 180 if not is_xbuddy_image: 181 image = os.path.expanduser(image) 182 183 if not is_xbuddy_image: 184 image = os.path.realpath(image) 185 186 if not os.path.exists(image) and not is_xbuddy_image: 187 Usage(parser, 'Image file: ' + image + ' does not exist!') 188 189 try: 190 should_unlock = False 191 if not options.no_lock: 192 try: 193 _ = locks.AcquireLock( 194 list(options.remote.split()), options.chromeos_root) 195 should_unlock = True 196 except Exception as e: 197 raise RuntimeError('Error acquiring machine: %s' % str(e)) 198 199 reimage = False 200 local_image = False 201 if not is_xbuddy_image: 202 local_image = True 203 image_checksum = FileUtils().Md5File(image, log_level=log_level) 204 205 command = 'cat ' + checksum_file 206 ret, device_checksum, _ = cmd_executer.CrosRunCommandWOutput( 207 command, chromeos_root=options.chromeos_root, machine=options.remote) 208 209 device_checksum = device_checksum.strip() 210 image_checksum = str(image_checksum) 211 212 l.LogOutput('Image checksum: ' + image_checksum) 213 l.LogOutput('Device checksum: ' + device_checksum) 214 215 if image_checksum != device_checksum: 216 [found, located_image] = LocateOrCopyImage( 217 options.chromeos_root, image, board=board) 218 219 reimage = True 220 l.LogOutput('Checksums do not match. Re-imaging...') 221 222 chroot_image = FindChromeOSImage(located_image, options.chromeos_root) 223 224 is_test_image = IsImageModdedForTest(options.chromeos_root, 225 chroot_image, log_level) 226 227 if not is_test_image and not options.force: 228 logger.GetLogger().LogFatal('Have to pass --force to image a ' 229 'non-test image!') 230 else: 231 reimage = True 232 found = True 233 l.LogOutput('Using non-local image; Re-imaging...') 234 235 if reimage: 236 # If the device has /tmp mounted as noexec, image_to_live.sh can fail. 237 command = 'mount -o remount,rw,exec /tmp' 238 cmd_executer.CrosRunCommand( 239 command, chromeos_root=options.chromeos_root, machine=options.remote) 240 241 real_src_dir = os.path.join( 242 os.path.realpath(options.chromeos_root), 'src') 243 real_chroot_dir = os.path.join( 244 os.path.realpath(options.chromeos_root), 'chroot') 245 246 # Check to see if cros flash will work for the remote machine. 247 CheckForCrosFlash(options.chromeos_root, options.remote, log_level) 248 249 # Disable the annoying chromebook beeps after reboot. 250 DisableCrosBeeps(options.chromeos_root, options.remote, log_level) 251 252 cros_flash_args = [ 253 'cros', 'flash', 254 '--board=%s' % board, '--clobber-stateful', options.remote 255 ] 256 if local_image: 257 cros_flash_args.append(chroot_image) 258 else: 259 cros_flash_args.append(image) 260 261 command = ' '.join(cros_flash_args) 262 263 # Workaround for crosbug.com/35684. 264 os.chmod(misc.GetChromeOSKeyFile(options.chromeos_root), 0600) 265 266 if log_level == 'average': 267 cmd_executer.SetLogLevel('verbose') 268 retries = 0 269 while True: 270 if log_level == 'quiet': 271 l.LogOutput('CMD : %s' % command) 272 ret = cmd_executer.ChrootRunCommand( 273 options.chromeos_root, command, command_timeout=1800) 274 if ret == 0 or retries >= 2: 275 break 276 retries += 1 277 if log_level == 'quiet': 278 l.LogOutput('Imaging failed. Retry # %d.' % retries) 279 280 if log_level == 'average': 281 cmd_executer.SetLogLevel(log_level) 282 283 logger.GetLogger().LogFatalIf(ret, 'Image command failed') 284 285 # Unfortunately cros_image_to_target.py sometimes returns early when the 286 # machine isn't fully up yet. 287 ret = EnsureMachineUp(options.chromeos_root, options.remote, log_level) 288 289 # If this is a non-local image, then the ret returned from 290 # EnsureMachineUp is the one that will be returned by this function; 291 # in that case, make sure the value in 'ret' is appropriate. 292 if not local_image and ret == True: 293 ret = 0 294 else: 295 ret = 1 296 297 if local_image: 298 if log_level == 'average': 299 l.LogOutput('Verifying image.') 300 command = 'echo %s > %s && chmod -w %s' % (image_checksum, 301 checksum_file, checksum_file) 302 ret = cmd_executer.CrosRunCommand( 303 command, 304 chromeos_root=options.chromeos_root, 305 machine=options.remote) 306 logger.GetLogger().LogFatalIf(ret, 'Writing checksum failed.') 307 308 successfully_imaged = VerifyChromeChecksum( 309 options.chromeos_root, chroot_image, options.remote, log_level) 310 logger.GetLogger().LogFatalIf(not successfully_imaged, 311 'Image verification failed!') 312 TryRemountPartitionAsRW(options.chromeos_root, options.remote, 313 log_level) 314 315 if found == False: 316 temp_dir = os.path.dirname(located_image) 317 l.LogOutput('Deleting temp image dir: %s' % temp_dir) 318 shutil.rmtree(temp_dir) 319 else: 320 l.LogOutput('Checksums match. Skipping reimage') 321 return ret 322 finally: 323 if should_unlock: 324 locks.ReleaseLock(list(options.remote.split()), options.chromeos_root) 325 326 327def LocateOrCopyImage(chromeos_root, image, board=None): 328 l = logger.GetLogger() 329 if board is None: 330 board_glob = '*' 331 else: 332 board_glob = board 333 334 chromeos_root_realpath = os.path.realpath(chromeos_root) 335 image = os.path.realpath(image) 336 337 if image.startswith('%s/' % chromeos_root_realpath): 338 return [True, image] 339 340 # First search within the existing build dirs for any matching files. 341 images_glob = ('%s/src/build/images/%s/*/*.bin' % (chromeos_root_realpath, 342 board_glob)) 343 images_list = glob.glob(images_glob) 344 for potential_image in images_list: 345 if filecmp.cmp(potential_image, image): 346 l.LogOutput('Found matching image %s in chromeos_root.' % potential_image) 347 return [True, potential_image] 348 # We did not find an image. Copy it in the src dir and return the copied 349 # file. 350 if board is None: 351 board = '' 352 base_dir = ('%s/src/build/images/%s' % (chromeos_root_realpath, board)) 353 if not os.path.isdir(base_dir): 354 os.makedirs(base_dir) 355 temp_dir = tempfile.mkdtemp(prefix='%s/tmp' % base_dir) 356 new_image = '%s/%s' % (temp_dir, os.path.basename(image)) 357 l.LogOutput('No matching image found. Copying %s to %s' % (image, new_image)) 358 shutil.copyfile(image, new_image) 359 return [False, new_image] 360 361 362def GetImageMountCommand(chromeos_root, image, rootfs_mp, stateful_mp): 363 image_dir = os.path.dirname(image) 364 image_file = os.path.basename(image) 365 mount_command = ('cd ~/trunk/src/scripts &&' 366 './mount_gpt_image.sh --from=%s --image=%s' 367 ' --safe --read_only' 368 ' --rootfs_mountpt=%s' 369 ' --stateful_mountpt=%s' % (image_dir, image_file, rootfs_mp, 370 stateful_mp)) 371 return mount_command 372 373 374def MountImage(chromeos_root, 375 image, 376 rootfs_mp, 377 stateful_mp, 378 log_level, 379 unmount=False, 380 extra_commands=''): 381 cmd_executer = command_executer.GetCommandExecuter(log_level=log_level) 382 command = GetImageMountCommand(chromeos_root, image, rootfs_mp, stateful_mp) 383 if unmount: 384 command = '%s --unmount' % command 385 if extra_commands: 386 command = '%s ; %s' % (command, extra_commands) 387 ret, out, _ = cmd_executer.ChrootRunCommandWOutput(chromeos_root, command) 388 logger.GetLogger().LogFatalIf(ret, 'Mount/unmount command failed!') 389 return out 390 391 392def IsImageModdedForTest(chromeos_root, image, log_level): 393 if log_level != 'verbose': 394 log_level = 'quiet' 395 command = 'mktemp -d' 396 cmd_executer = command_executer.GetCommandExecuter(log_level=log_level) 397 _, rootfs_mp, _ = cmd_executer.ChrootRunCommandWOutput(chromeos_root, command) 398 _, stateful_mp, _ = cmd_executer.ChrootRunCommandWOutput( 399 chromeos_root, command) 400 rootfs_mp = rootfs_mp.strip() 401 stateful_mp = stateful_mp.strip() 402 lsb_release_file = os.path.join(rootfs_mp, 'etc/lsb-release') 403 extra = ( 404 'grep CHROMEOS_RELEASE_DESCRIPTION %s | grep -i test' % lsb_release_file) 405 output = MountImage( 406 chromeos_root, 407 image, 408 rootfs_mp, 409 stateful_mp, 410 log_level, 411 extra_commands=extra) 412 is_test_image = re.search('test', output, re.IGNORECASE) 413 MountImage( 414 chromeos_root, image, rootfs_mp, stateful_mp, log_level, unmount=True) 415 return is_test_image 416 417 418def VerifyChromeChecksum(chromeos_root, image, remote, log_level): 419 command = 'mktemp -d' 420 cmd_executer = command_executer.GetCommandExecuter(log_level=log_level) 421 _, rootfs_mp, _ = cmd_executer.ChrootRunCommandWOutput(chromeos_root, command) 422 _, stateful_mp, _ = cmd_executer.ChrootRunCommandWOutput( 423 chromeos_root, command) 424 rootfs_mp = rootfs_mp.strip() 425 stateful_mp = stateful_mp.strip() 426 chrome_file = '%s/opt/google/chrome/chrome' % rootfs_mp 427 extra = 'md5sum %s' % chrome_file 428 out = MountImage( 429 chromeos_root, 430 image, 431 rootfs_mp, 432 stateful_mp, 433 log_level, 434 extra_commands=extra) 435 image_chrome_checksum = out.strip().split()[0] 436 MountImage( 437 chromeos_root, image, rootfs_mp, stateful_mp, log_level, unmount=True) 438 439 command = 'md5sum /opt/google/chrome/chrome' 440 [_, o, _] = cmd_executer.CrosRunCommandWOutput( 441 command, chromeos_root=chromeos_root, machine=remote) 442 device_chrome_checksum = o.split()[0] 443 if image_chrome_checksum.strip() == device_chrome_checksum.strip(): 444 return True 445 else: 446 return False 447 448 449# Remount partition as writable. 450# TODO: auto-detect if an image is built using --noenable_rootfs_verification. 451def TryRemountPartitionAsRW(chromeos_root, remote, log_level): 452 l = logger.GetLogger() 453 cmd_executer = command_executer.GetCommandExecuter(log_level=log_level) 454 command = 'sudo mount -o remount,rw /' 455 ret = cmd_executer.CrosRunCommand(\ 456 command, chromeos_root=chromeos_root, machine=remote, 457 terminated_timeout=10) 458 if ret: 459 ## Safely ignore. 460 l.LogWarning('Failed to remount partition as rw, ' 461 'probably the image was not built with ' 462 "\"--noenable_rootfs_verification\", " 463 'you can safely ignore this.') 464 else: 465 l.LogOutput('Re-mounted partition as writable.') 466 467 468def EnsureMachineUp(chromeos_root, remote, log_level): 469 l = logger.GetLogger() 470 cmd_executer = command_executer.GetCommandExecuter(log_level=log_level) 471 timeout = 600 472 magic = 'abcdefghijklmnopqrstuvwxyz' 473 command = 'echo %s' % magic 474 start_time = time.time() 475 while True: 476 current_time = time.time() 477 if current_time - start_time > timeout: 478 l.LogError( 479 'Timeout of %ss reached. Machine still not up. Aborting.' % timeout) 480 return False 481 ret = cmd_executer.CrosRunCommand( 482 command, chromeos_root=chromeos_root, machine=remote) 483 if not ret: 484 return True 485 486 487if __name__ == '__main__': 488 retval = DoImage(sys.argv) 489 sys.exit(retval) 490