Added a script to compare 2 chromiumos images.

This is done by comparing each ELF files of those images.

TEST=manually
BUG=None

Change-Id: I7b19665c074645c810d974fc13aa3070d4cba6ee
Reviewed-on: https://chrome-internal-review.googlesource.com/151367
Reviewed-by: Han Shen <shenhan@google.com>
Tested-by: Han Shen <shenhan@google.com>
Commit-Queue: Han Shen <shenhan@google.com>
diff --git a/chromiumos_image_diff.py b/chromiumos_image_diff.py
new file mode 100755
index 0000000..6b45582
--- /dev/null
+++ b/chromiumos_image_diff.py
@@ -0,0 +1,332 @@
+#!/usr/bin/python
+"""Diff 2 chromiumos images by comparing each elf file.
+
+   The script diffs every *ELF* files by dissembling every *executable* section,
+   which means it is not a FULL elf differ.
+
+   A simple usage example -
+     chromiumos_image_diff.py --image1 image-path-1 --image2 image-path-2
+
+   Note that image path should be inside the chroot, if not (ie, image is
+   downloaded from web), please specify a chromiumos checkout via
+   "--chromeos_root".
+
+   And this script should be executed outside chroot.
+"""
+
+__author__ = 'shenhan@google.com (Han Shen)'
+
+import argparse
+import os
+import re
+import sys
+import tempfile
+
+import image_chromeos
+from utils import command_executer
+from utils import logger
+from utils import misc
+
+
+class CrosImage(object):
+  """A cros image object."""
+
+  def __init__(self, image, chromeos_root, no_unmount):
+    self.image = image
+    self.chromeos_root = chromeos_root
+    self.mounted = False
+    self._ce = command_executer.GetCommandExecuter()
+    self.logger = logger.GetLogger()
+    self.elf_files = []
+    self.no_unmount = no_unmount
+
+  def MountImage(self, mount_basename):
+    """Mount/unpack the image."""
+
+    if mount_basename:
+      self.rootfs = '/tmp/{0}.rootfs'.format(mount_basename)
+      self.stateful = '/tmp/{0}.stateful'.format(mount_basename)
+      self.unmount_script = '/tmp/{0}.unmount.sh'.format(mount_basename)
+    else:
+      self.rootfs = tempfile.mkdtemp(suffix='.rootfs',
+                                     prefix='chromiumos_image_diff')
+      ## rootfs is like /tmp/tmpxyz012.rootfs.
+      match = re.match(r'^(.*)\.rootfs$', self.rootfs)
+      basename = match.group(1)
+      self.stateful = basename + '.stateful'
+      os.mkdir(self.stateful)
+      self.unmount_script = '{0}.unmount.sh'.format(basename)
+
+    self.logger.LogOutput('Mounting "{0}" onto "{1}" and "{2}"'.format(
+        self.image, self.rootfs, self.stateful))
+    ## First of all creating an unmount image
+    self.CreateUnmountScript()
+    command = image_chromeos.GetImageMountCommand(
+        self.chromeos_root, self.image, self.rootfs, self.stateful)
+    rv = self._ce.RunCommand(
+        command, return_output=False, print_to_console=True)
+    self.mounted = (rv == 0)
+    if not self.mounted:
+      self.logger.LogError('Failed to mount "{0}" onto "{1}" and "{2}".'.format(
+          self.image, self.rootfs, self.stateful))
+    return self.mounted
+
+  def CreateUnmountScript(self):
+    command = ('sudo umount {r}/usr/local {r}/usr/share/oem '
+               '{r}/var {r}/mnt/stateful_partition {r}; sudo umount {s} ; '
+               'rmdir {r} ; rmdir {s}\n').format(r=self.rootfs, s=self.stateful)
+    f = open(self.unmount_script, 'w')
+    f.write(command)
+    f.close()
+    self._ce.RunCommand('chmod +x {}'.format(self.unmount_script),
+                        print_to_console=False, return_output=False)
+    self.logger.LogOutput('Created an unmount script - "{0}"'.format(
+        self.unmount_script))
+
+  def UnmountImage(self):
+    """Unmount the image and delete mount point."""
+
+    self.logger.LogOutput('Unmounting image "{0}" from "{1}" and "{2}"'.format(
+        self.image, self.rootfs, self.stateful))
+    if self.mounted:
+      command = 'bash "{0}"'.format(self.unmount_script)
+      if self.no_unmount:
+        self.logger.LogOutput(('Please unmount manually - \n'
+                               '\t bash "{0}"'.format(self.unmount_script)))
+      else:
+        if self._ce.RunCommand(
+            command, return_output=False, print_to_console=True) == 0:
+          self._ce.RunCommand('rm {0}'.format(self.unmount_script))
+          self.mounted = False
+          self.rootfs = None
+          self.stateful = None
+          self.unmount_script = None
+
+   return not self.mounted
+
+  def FindElfFiles(self):
+    """Find all elf files for the image.
+
+    Returns:
+      Always true
+    """
+
+    self.logger.LogOutput('Finding all elf files in "{0}" ...'.format(
+        self.rootfs))
+    # Note '\;' must be prefixed by 'r'.
+    command = ('find "{0}" -type f -exec '
+               'bash -c \'file -b "{{}}" | grep -q "ELF"\'' r' \; '
+               r'-exec echo "{{}}" \;').format(self.rootfs)
+    self.logger.LogCmd(command)
+    _, out, _ = self._ce.RunCommand(
+        command, return_output=True, print_to_console=False)
+    self.elf_files = out.splitlines()
+    self.logger.LogOutput(
+        'Total {0} elf files found.'.format(len(self.elf_files)))
+    return True
+
+
+class ImageComparator(object):
+  """A class that wraps comparsion actions."""
+
+  def __init__(self, images, diff_file):
+    self.images = images
+    self.logger = logger.GetLogger()
+    self.diff_file = diff_file
+    self.tempf1 = None
+    self.tempf2 = None
+
+  def Cleanup(self):
+    if self.tempf1 and self.tempf2:
+      command_executer.GetCommandExecuter().RunCommand(
+          'rm {0} {1}'.format(self.tempf1, self.tempf2))
+      logger.GetLogger('Removed "{0}" and "{1}".'.format(
+          self.tempf1, self.tempf2))
+
+  def CheckElfFileSetEquality(self):
+    """Checking whether images have exactly number of elf files."""
+
+    self.logger.LogOutput('Checking elf file equality ...')
+    i1 = self.images[0]
+    i2 = self.images[1]
+    t1 = i1.rootfs + '/'
+    elfset1 = set([e.replace(t1, '') for e in i1.elf_files])
+    t2 = i2.rootfs + '/'
+    elfset2 = set([e.replace(t2, '') for e in i2.elf_files])
+    dif1 = elfset1.difference(elfset2)
+    msg = None
+    if dif1:
+      msg = 'The following files are not in "{image}" - "{rootfs}":\n'.format(
+          image=i2.image, rootfs=i2.rootfs)
+      for d in dif1:
+        msg += '\t' + d + '\n'
+    dif2 = elfset2.difference(elfset1)
+    if dif2:
+      msg = 'The following files are not in "{image}" - "{rootfs}":\n'.format(
+          image=i1.image, rootfs=i1.rootfs)
+      for d in dif2:
+        msg += '\t' + d + '\n'
+    if msg:
+      self.logger.LogError(msg)
+      return False
+    return True
+
+  def CompareImages(self):
+    """Do the comparsion work."""
+
+    if not self.CheckElfFileSetEquality():
+      return False
+
+    mismatch_list = []
+    match_count = 0
+    i1 = self.images[0]
+    i2 = self.images[1]
+    self.logger.LogOutput('Start comparing {0} elf file by file ...'.format(
+        len(i1.elf_files)))
+    ## Note - i1.elf_files and i2.elf_files have exactly the same entries here.
+
+    ## Create 2 temp files to be used for all disassembed files.
+    handle, self.tempf1 = tempfile.mkstemp()
+    os.close(handle)  # We do not need the handle
+    handle, self.tempf2 = tempfile.mkstemp()
+    os.close(handle)
+
+    cmde = command_executer.GetCommandExecuter()
+    for elf1 in i1.elf_files:
+      tmp_rootfs = i1.rootfs + '/'
+      f1 = elf1.replace(tmp_rootfs, '')
+      full_path1 = elf1
+      full_path2 = elf1.replace(i1.rootfs, i2.rootfs)
+
+      if full_path1 == full_path2:
+        self.logger.LogError(
+            'Error:  We\'re comparing the SAME file - {0}'.format(f1))
+        continue
+
+      command = ('objdump -d "{f1}" > {tempf1} ; '
+                 'objdump -d "{f2}" > {tempf2} ; '
+                 # Remove path string inside the dissemble
+                 'sed -i \'s!{rootfs1}!!g\' {tempf1} ; '
+                 'sed -i \'s!{rootfs2}!!g\' {tempf2} ; '
+                 'diff {tempf1} {tempf2} 1>/dev/null 2>&1').format(
+                     f1=full_path1, f2=full_path2,
+                     rootfs1=i1.rootfs, rootfs2=i2.rootfs,
+                     tempf1=self.tempf1, tempf2=self.tempf2)
+      ret = cmde.RunCommand(command, return_output=False,
+                            print_to_console=False)
+      if ret != 0:
+        self.logger.LogOutput('*** Not match - "{0}" "{1}"'.format(
+            full_path1, full_path2))
+        mismatch_list.append(f1)
+        if self.diff_file:
+          command = (
+              'echo "Diffs of disassemble of \"{f1}\" and \"{f2}\"" '
+              '>> {diff_file} ; diff {tempf1} {tempf2} '
+              '>> {diff_file}').format(
+                  f1=full_path1, f2=full_path2, diff_file=self.diff_file,
+                  tempf1=self.tempf1, tempf2=self.tempf2)
+          cmde.RunCommand(command, return_output=False, print_to_console=False)
+      else:
+        match_count += 1
+    ## End of comparing every elf files.
+
+    if not mismatch_list:
+      self.logger.LogOutput('** COOL, ALL {0} BINARIES MATCHED!! **'.format(
+          match_count))
+      return True
+
+    mismatch_str = 'Found {0} mismatch:\n'.format(len(mismatch_list))
+    for b in mismatch_list:
+      mismatch_str += '\t' + b + '\n'
+
+    self.logger.LogOutput(mismatch_str)
+    return False
+
+
+def Main(argv):
+  """The main function."""
+
+  command_executer.InitCommandExecuter()
+  images = []
+
+  parser = argparse.ArgumentParser()
+  parser.add_argument(
+      '--no_unmount', action='store_true', dest='no_unmount', default=False,
+      help='Do not unmount after finish, this is useful for debugging.')
+  parser.add_argument(
+      '--chromeos_root', dest='chromeos_root', default=None, action='store',
+      help=('[Optional] Specify a chromeos tree instead of '
+            'deducing it from image path so that we can compare '
+            '2 images that are downloaded.'))
+  parser.add_argument(
+      '--mount_basename', dest='mount_basename', default=None, action='store',
+      help=('Specify a meaningful name for the mount point. With this being '
+            'set, the mount points would be "/tmp/mount_basename.x.rootfs" '
+            ' and "/tmp/mount_basename.x.stateful". (x is 1 or 2).'))
+  parser.add_argument('--diff_file', dest='diff_file', default=None,
+                      help='Dumping all the diffs (if any) to the diff file')
+  parser.add_argument('--image1', dest='image1', default=None,
+                      required=True, help=('Image 1 file name.'))
+  parser.add_argument('--image2', dest='image2', default=None,
+                      required=True, help=('Image 2 file name.'))
+  options = parser.parse_args(argv[1:])
+
+  if options.mount_basename and options.mount_basename.find('/') >= 0:
+    logger.GetLogger().LogError(
+        '"--mount_basename" must be a name, not a path.')
+    parser.print_help()
+    return 1
+
+  result = False
+  image_comparator = None
+  try:
+    for image_path in [options.image1, options.image2]:
+      image_path = os.path.realpath(image_path)
+      if not os.path.isfile(image_path):
+        logger.getLogger().LogError('"{0}" is not a file.'.format(image_path))
+        return 1
+
+      chromeos_root = None
+      if options.chromeos_root:
+        chromeos_root = options.chromeos_root
+      else:
+        ## Deduce chromeos root from image
+        t = image_path
+        while t != '/':
+          if misc.IsChromeOsTree(t):
+            break
+          t = os.path.dirname(t)
+        if misc.IsChromeOsTree(t):
+          chromeos_root = t
+
+      if not chromeos_root:
+        logger.GetLogger().LogError(
+            'Please provide a valid chromeos root via --chromeos_root')
+        return 1
+
+      image = CrosImage(image_path, chromeos_root, options.no_unmount)
+
+      if options.mount_basename:
+        mount_basename = '{basename}.{index}'.format(
+            basename=options.mount_basename, index=i)
+      else:
+        mount_basename = None
+
+      if image.MountImage(mount_basename):
+        images.append(image)
+        image.FindElfFiles()
+
+    if len(images) == 2:
+      image_comparator = ImageComparator(images, options.diff_file)
+      result = image_comparator.CompareImages()
+  finally:
+    for image in images:
+      image.UnmountImage()
+    if image_comparator:
+      image_comparator.Cleanup()
+
+  return 0 if result else 1
+
+
+if __name__ == '__main__':
+  Main(sys.argv)