Home | History | Annotate | Download | only in toolchain-utils
      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