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