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