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