gsi_util: adding pull subcommand

'pull' command can pull a file from an image file, folder or adb.

The patch includes a "mounter" framework to implement different
source of the system/vendor image. And also includes several
"mounter" implementations.

CompositeMounter integrates all possible mounter implementations.
Usually just using CompositeMounter is enough. With
CompositeMounter, you could access files in different target
with an unique interface, such files in an image file, a folder or
a device with an unique interface. pull.py is an basic example to
use CompositeMounter.

Here are some example to use 'pull' command:

$ ./gsi_util.py pull --system adb:AB0123456789 /system/manifest.xml
$ ./gsi_util.py pull --vendor adb /vendor/compatibility_matrix.xml
$ ./gsi_util.py pull --system system.img /system/build.prop
$ ./gsi_util.py pull --system my/out/folder/system /system/build.prop

As current implementation, accessing files in a the image file requires
root permission. gsi_util will need user to input the password for sudo.

For the detail usage, reference:

$ ./gsi_util.py pull --help

Bug: 71029338
Test: pull /system/build.prop from different targets
Change-Id: Iaeb6352c14ebc24860ed79fc30edd314e225aef9
diff --git a/gsi/gsi_util/Android.bp b/gsi/gsi_util/Android.bp
index 2eb80fd..2ab04c8 100644
--- a/gsi/gsi_util/Android.bp
+++ b/gsi/gsi_util/Android.bp
@@ -18,10 +18,13 @@
     "gsi_util.py",
     "gsi_util/*.py",
     "gsi_util/commands/*.py",
+    "gsi_util/mounters/*.py",
     "gsi_util/utils/*.py",
   ],
   required: [
+    "adb",
     "avbtool",
+    "simg2img",
   ],
   version: {
     py2: {
diff --git a/gsi/gsi_util/gsi_util.py b/gsi/gsi_util/gsi_util.py
index 2b535a8..a226628 100755
--- a/gsi/gsi_util/gsi_util.py
+++ b/gsi/gsi_util/gsi_util.py
@@ -28,7 +28,7 @@
 
   # Adds gsi_util COMMAND here.
   # TODO(bowgotsai): auto collect from gsi_util/commands/*.py
-  _COMMANDS = ['flash_gsi', 'hello']
+  _COMMANDS = ['flash_gsi', 'pull', 'hello']
 
   _LOGGING_FORMAT = '%(message)s'
   _LOGGING_LEVEL = logging.WARNING
diff --git a/gsi/gsi_util/gsi_util/commands/pull.py b/gsi/gsi_util/gsi_util/commands/pull.py
new file mode 100644
index 0000000..0b03011
--- /dev/null
+++ b/gsi/gsi_util/gsi_util/commands/pull.py
@@ -0,0 +1,98 @@
+# Copyright 2017 - The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Implementation of gsi_util command 'pull'."""
+
+import argparse
+import logging
+import shutil
+import sys
+
+from gsi_util.mounters.composite_mounter import CompositeMounter
+
+
+def do_pull(args):
+  logging.info('==== PULL ====')
+  logging.info('  system=%s vendor=%s', args.system, args.vendor)
+
+  if not args.system and not args.vendor:
+    sys.exit('Without system nor vendor.')
+
+  source, dest = args.SOURCE, args.DEST
+
+  mounter = CompositeMounter()
+  if args.system:
+    mounter.add_by_mount_target('system', args.system)
+  if args.vendor:
+    mounter.add_by_mount_target('vendor', args.vendor)
+
+  with mounter as file_accessor:
+    with file_accessor.prepare_file(source) as filename:
+      if not filename:
+        print >> sys.stderr, 'Can not dump file: {}'.format(source)
+      else:
+        logging.debug('Copy %s -> %s', filename, dest)
+        shutil.copy(filename, dest)
+
+  logging.info('==== DONE ====')
+
+
+DUMP_DESCRIPTION = """'pull' command pulls a file from the give image.
+
+You must assign at least one image source by SYSTEM and/or VENDOR.
+Image source could be:
+
+ adb[:SERIAL_NUM]: pull the file form the device which be connected with adb
+  image file name: pull the file from the given image file, e.g. the file name
+                   of a GSI.
+                   If a image file is assigned to be the source of system
+                   image, gsu_util will detect system-as-root automatically.
+      folder name: pull the file from the given folder, e.g. the system/vendor
+                   folder in a Android build out folder.
+
+SOURCE is the full path file name to pull, which must start with '/' and
+includes the mount point. ex.
+
+    /system/build.prop
+    /vendor/compatibility_matrix.xml
+
+Some usage examples:
+
+    $ ./gsi_util.py pull --system adb:AB0123456789 /system/manifest.xml
+    $ ./gsi_util.py pull --vendor adb /vendor/compatibility_matrix.xml
+    $ ./gsi_util.py pull --system system.img /system/build.prop
+    $ ./gsi_util.py pull --system my/out/folder/system /system/build.prop"""
+
+
+def setup_command_args(parser):
+  # command 'pull'
+  dump_parser = parser.add_parser(
+      'pull',
+      help='pull a file from the given image',
+      description=DUMP_DESCRIPTION,
+      formatter_class=argparse.RawTextHelpFormatter)
+  dump_parser.add_argument(
+      '--system', type=str, help='system image file name, folder name or "adb"')
+  dump_parser.add_argument(
+      '--vendor', type=str, help='vendor image file name, folder name or "adb"')
+  dump_parser.add_argument(
+      'SOURCE',
+      type=str,
+      help='the full path file name in given image to be pull')
+  dump_parser.add_argument(
+      'DEST',
+      nargs='?',
+      default='.',
+      type=str,
+      help='the file name or directory to save the pulled file (default: .)')
+  dump_parser.set_defaults(func=do_pull)
diff --git a/gsi/gsi_util/gsi_util/mounters/__init__.py b/gsi/gsi_util/gsi_util/mounters/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gsi/gsi_util/gsi_util/mounters/__init__.py
diff --git a/gsi/gsi_util/gsi_util/mounters/adb_mounter.py b/gsi/gsi_util/gsi_util/mounters/adb_mounter.py
new file mode 100644
index 0000000..17d16ee
--- /dev/null
+++ b/gsi/gsi_util/gsi_util/mounters/adb_mounter.py
@@ -0,0 +1,82 @@
+# Copyright 2017 - The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Provides class AdbMounter.
+
+The AdbMounter implements the abstract class BaseMounter. It can get files from
+a device which is connected by adb.
+"""
+
+import errno
+import logging
+import os
+import shutil
+import tempfile
+
+import gsi_util.mounters.base_mounter as base_mounter
+import gsi_util.utils.adb_utils as adb_utils
+
+
+class _AdbFileAccessor(base_mounter.BaseFileAccessor):
+
+  def __init__(self, temp_dir, serial_num):
+    super(_AdbFileAccessor, self).__init__()
+    self._temp_dir = temp_dir
+    self._serial_num = serial_num
+
+  @staticmethod
+  def _make_parent_dirs(filename):
+    """Make parent directories as needed, no error if it exists."""
+    dir_path = os.path.dirname(filename)
+    try:
+      os.makedirs(dir_path)
+    except OSError as exc:
+      if exc.errno != errno.EEXIST:
+        raise
+
+  # override
+  def _handle_prepare_file(self, filename_in_storage):
+    filename = os.path.join(self._temp_dir, filename_in_storage)
+    logging.info('Prepare file %s -> %s', filename_in_storage, filename)
+
+    self._make_parent_dirs(filename)
+    if not adb_utils.pull(filename, filename_in_storage, self._serial_num):
+      logging.error('Fail to prepare file: %s', filename_in_storage)
+      return None
+
+    return base_mounter.MounterFile(filename)
+
+
+class AdbMounter(base_mounter.BaseMounter):
+  """Provides a file accessor which can access files by adb."""
+
+  def __init__(self, serial_num=None):
+    super(AdbMounter, self).__init__()
+    self._serial_num = serial_num
+
+  # override
+  def _handle_mount(self):
+    adb_utils.root(self._serial_num)
+
+    self._temp_dir = tempfile.mkdtemp()
+    logging.debug('Created temp dir: %s', self._temp_dir)
+
+    return _AdbFileAccessor(self._temp_dir, self._serial_num)
+
+  # override
+  def _handle_unmount(self):
+    if hasattr(self, '_temp_dir'):
+      logging.debug('Remove temp dir: %s', self._temp_dir)
+      shutil.rmtree(self._temp_dir)
+      del self._temp_dir
diff --git a/gsi/gsi_util/gsi_util/mounters/base_mounter.py b/gsi/gsi_util/gsi_util/mounters/base_mounter.py
new file mode 100644
index 0000000..c0402c7
--- /dev/null
+++ b/gsi/gsi_util/gsi_util/mounters/base_mounter.py
@@ -0,0 +1,180 @@
+# Copyright 2017 - The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Base classes to implement Mounter classes."""
+
+import abc
+import logging
+
+
+class MounterFile(object):
+
+  def __init__(self, filename, cleanup_func=None):
+    self._filename = filename
+    self._clean_up_func = cleanup_func
+
+  def _handle_get_filename(self):
+    return self._filename
+
+  def _handle_clean_up(self):
+    if self._clean_up_func:
+      self._clean_up_func()
+
+  def __enter__(self):
+    return self._handle_get_filename()
+
+  def __exit__(self, exc_type, exc_val, exc_tb):
+    self._handle_clean_up()
+
+  def get_filename(self):
+    return self._handle_get_filename()
+
+  def clean_up(self):
+    self._handle_clean_up()
+
+
+class MounterFileList(object):
+
+  def __init__(self, file_list):
+    self._file_list = file_list
+
+  def _handle_get_filenames(self):
+    return [x.get_filename() for x in self._file_list]
+
+  def _handle_clean_up(self):
+    for x in reversed(self._file_list):
+      x.clean_up()
+
+  def __enter__(self):
+    return self._handle_get_filenames()
+
+  def __exit__(self, exc_type, exc_val, exc_tb):
+    self._handle_clean_up()
+
+  def get_filenames(self):
+    return self._handle_get_filenames()
+
+  def clean_up(self):
+    self._handle_clean_up()
+
+
+class BaseFileAccessor(object):
+  """An abstract class to implement the file accessors.
+
+  A mounter returns a file accessor when it is mounted. A file accessor must
+  override the method  _handle_prepare_file() to return the file name of
+  the requested file in the storage. However, files in some mounter storages
+  couldn't be access directly, e.g. the file accessor of AdbMounter, which
+  accesses the file in a device by adb. In this case, file accessor could
+  return a temp file which contains the content. A file accessor could give the
+  cleanup_func when creating MounterFile to cleanup the temp file.
+  """
+
+  __metaclass__ = abc.ABCMeta
+
+  def __init__(self, path_prefix='/'):
+    logging.debug('BaseFileAccessor(path_prefix=%s)', path_prefix)
+    self._path_prefix = path_prefix
+
+  def _get_pathfile_to_access(self, file_to_map):
+    path_prefix = self._path_prefix
+
+    if not file_to_map.startswith(path_prefix):
+      raise RuntimeError('"%s" does not start with "%s"', file_to_map,
+                         path_prefix)
+
+    return file_to_map[len(path_prefix):]
+
+  @abc.abstractmethod
+  def _handle_prepare_file(self, filename_in_storage):
+    """Override this method to prepare the given file in the storage.
+
+    Args:
+      filename_in_storage: the file in the storage to be prepared
+
+    Returns:
+      Return an MounterFile instance. Return None if the request file is not
+      in the mount.
+    """
+
+  def prepare_file(self, filename_in_mount):
+    """Return the accessable file name in the storage.
+
+    The function prepares a accessable file which contains the content of the
+    filename_in_mount.
+
+    See BaseFileAccessor for the detail.
+
+    Args:
+      filename_in_mount: the file to map.
+        filename_in_mount should be a full path file as the path in a real
+        device, and must start with a '/'. For example: '/system/build.prop',
+        '/vendor/default.prop', '/init.rc', etc.
+
+    Returns:
+      A MounterFile instance. Return None if the file is not exit in the
+      storage.
+    """
+    filename_in_storage = self._get_pathfile_to_access(filename_in_mount)
+    ret = self._handle_prepare_file(filename_in_storage)
+    return ret if ret else MounterFile(None)
+
+  def prepare_multi_files(self, filenames_in_mount):
+    file_list = [self.prepare_file(x) for x in filenames_in_mount]
+    return MounterFileList(file_list)
+
+
+class BaseMounter(object):
+
+  __metaclass__ = abc.ABCMeta
+
+  @abc.abstractmethod
+  def _handle_mount(self):
+    """Override this method to handle mounting and return a file accessor.
+
+    File accessor must inherit from  BaseFileAccessor.
+    """
+
+  def _handle_unmount(self):
+    """Override this method to handle cleanup this mounting."""
+    # default is do nothing
+    return
+
+  def _process_mount(self):
+    if self._mounted:
+      raise RuntimeError('The mounter had been mounted.')
+
+    file_accessor = self._handle_mount()
+    self._mounted = True
+
+    return file_accessor
+
+  def _process_unmount(self):
+    if self._mounted:
+      self._handle_unmount()
+      self._mounted = False
+
+  def __init__(self):
+    self._mounted = False
+
+  def __enter__(self):
+    return self._process_mount()
+
+  def __exit__(self, exc_type, exc_val, exc_tb):
+    self._process_unmount()
+
+  def mount(self):
+    return self._process_mount()
+
+  def unmount(self):
+    self._process_unmount()
diff --git a/gsi/gsi_util/gsi_util/mounters/composite_mounter.py b/gsi/gsi_util/gsi_util/mounters/composite_mounter.py
new file mode 100644
index 0000000..0e67420
--- /dev/null
+++ b/gsi/gsi_util/gsi_util/mounters/composite_mounter.py
@@ -0,0 +1,125 @@
+# Copyright 2017 - The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Provides class CompositeMounter.
+
+CompositeMounter implements the abstract class BaseMounter. It can add multiple
+mounters inside as sub-mounters, and operate these sub-mounters with the
+BaseMounter interface. Uses CompositeMounter.add_sub_mounter() to add
+sub-mounter.
+
+Usually, using CompositeMounter.add_by_mount_target() to add mounters is easier,
+the method uses class _MounterFactory to create a mounter and then adds it.
+
+class _MounterFactory provides a method to create a mounter by 'mounter_target'.
+'mounter_target' is a name which identify what is the file source to be
+mounted. See _MounterFactory.create_by_mount_target() for the detail.
+"""
+
+import logging
+import os
+
+from adb_mounter import AdbMounter
+import base_mounter
+from folder_mounter import FolderMounter
+from image_mounter import ImageMounter
+
+
+class _MounterFactory(object):
+
+  _SUPPORTED_PARTITIONS = ['system', 'vendor']
+
+  @classmethod
+  def create_by_mount_target(cls, mount_target, partition):
+    """Create a proper Mounter instance by a string of mount target.
+
+    Args:
+      partition: the partition to be mounted as
+      mount_target: 'adb', a folder name or an image file name to mount.
+        see Returns for the detail.
+
+    Returns:
+      Returns an AdbMounter if mount_target is 'adb[:SERIAL_NUM]'
+      Returns a FolderMounter if mount_target is a folder name
+      Returns an ImageMounter if mount_target is an image file name
+
+    Raises:
+      ValueError: partiton is not support or mount_target is not exist.
+    """
+    if partition not in cls._SUPPORTED_PARTITIONS:
+      raise ValueError('Wrong partition name "{}"'.format(partition))
+
+    if mount_target == 'adb' or mount_target.startswith('adb:'):
+      (_, _, serial_num) = mount_target.partition(':')
+      return AdbMounter(serial_num)
+
+    path_prefix = '/{}/'.format(partition)
+
+    if os.path.isdir(mount_target):
+      return FolderMounter(mount_target, path_prefix)
+
+    if os.path.isfile(mount_target):
+      if partition == 'system':
+        path_prefix = ImageMounter.DETECT_SYSTEM_AS_ROOT
+      return ImageMounter(mount_target, path_prefix)
+
+    raise ValueError('Unknown target "{}"'.format(mount_target))
+
+
+class _CompositeFileAccessor(base_mounter.BaseFileAccessor):
+
+  def __init__(self, file_accessors):
+    super(_CompositeFileAccessor, self).__init__()
+    self._file_accessors = file_accessors
+
+  # override
+  def _handle_prepare_file(self, filename_in_storage):
+    logging.debug('_CompositeFileAccessor._handle_prepare_file(%s)',
+                  filename_in_storage)
+
+    pathfile_to_prepare = '/' + filename_in_storage
+    for (prefix_path, file_accessor) in self._file_accessors:
+      if pathfile_to_prepare.startswith(prefix_path):
+        return file_accessor.prepare_file(pathfile_to_prepare)
+
+    logging.debug('  Not found')
+    return None
+
+
+class CompositeMounter(base_mounter.BaseMounter):
+  """Implements a BaseMounter which can add multiple sub-mounters."""
+
+  def __init__(self):
+    super(CompositeMounter, self).__init__()
+    self._mounters = []
+
+  # override
+  def _handle_mount(self):
+    file_accessors = [(path_prefix, mounter.mount())
+                      for (path_prefix, mounter) in self._mounters]
+    return _CompositeFileAccessor(file_accessors)
+
+  # override
+  def _handle_unmount(self):
+    for (_, mounter) in reversed(self._mounters):
+      mounter.unmount()
+
+  def add_sub_mounter(self, mount_point, mounter):
+    self._mounters.append((mount_point, mounter))
+
+  def add_by_mount_target(self, partition, mount_target):
+    logging.debug('CompositeMounter.add_by_mount_target(%s, %s)',
+                  partition, mount_target)
+    mount_point = '/{}/'.format(partition)
+    mounter = _MounterFactory.create_by_mount_target(mount_target, partition)
+    self.add_sub_mounter(mount_point, mounter)
diff --git a/gsi/gsi_util/gsi_util/mounters/folder_mounter.py b/gsi/gsi_util/gsi_util/mounters/folder_mounter.py
new file mode 100644
index 0000000..83ba641
--- /dev/null
+++ b/gsi/gsi_util/gsi_util/mounters/folder_mounter.py
@@ -0,0 +1,54 @@
+# Copyright 2017 - The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Provides class FolderMounter.
+
+The FolderMounter implements the abstract class BaseMounter. It can
+get files from a given folder. The  folder is usually the system/vendor folder
+of $OUT folder in an Android build environment.
+"""
+
+import logging
+import os
+
+import base_mounter
+
+
+class _FolderFileAccessor(base_mounter.BaseFileAccessor):
+
+  def __init__(self, folder_dir, path_prefix):
+    super(_FolderFileAccessor, self).__init__(path_prefix)
+    self._folder_dir = folder_dir
+
+  # override
+  def _handle_prepare_file(self, filename_in_storage):
+    filename = os.path.join(self._folder_dir, filename_in_storage)
+    logging.debug('Prepare file %s -> %s', filename_in_storage, filename)
+    if not os.path.isfile(filename):
+      logging.error('File is not exist: %s', filename_in_storage)
+      return None
+    return base_mounter.MounterFile(filename)
+
+
+class FolderMounter(base_mounter.BaseMounter):
+  """Provides a file accessor which can access files in the given folder."""
+
+  def __init__(self, folder_dir, path_prefix):
+    super(FolderMounter, self).__init__()
+    self._folder_dir = folder_dir
+    self._path_prefix = path_prefix
+
+  # override
+  def _handle_mount(self):
+    return _FolderFileAccessor(self._folder_dir, self._path_prefix)
diff --git a/gsi/gsi_util/gsi_util/mounters/image_mounter.py b/gsi/gsi_util/gsi_util/mounters/image_mounter.py
new file mode 100644
index 0000000..22a32e5
--- /dev/null
+++ b/gsi/gsi_util/gsi_util/mounters/image_mounter.py
@@ -0,0 +1,138 @@
+# Copyright 2017 - The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Provides class ImageMounter.
+
+The ImageMounter implements the abstract calss BaseMounter,
+It can get files from an image file. e.g., system.img or vendor.img.
+"""
+
+import logging
+import os
+import shutil
+import tempfile
+
+import gsi_util.mounters.base_mounter as base_mounter
+import gsi_util.utils.image_utils as image_utils
+
+
+class _ImageFileAccessor(base_mounter.BaseFileAccessor):
+
+  @staticmethod
+  def _make_parent_dirs(filename):
+    """Make parent directories as needed, no error if it exists."""
+    dir_path = os.path.dirname(filename)
+    try:
+      os.makedirs(dir_path)
+    except OSError as exc:
+      if exc.errno != errno.EEXIST:
+        raise
+
+  def __init__(self, path_prefix, mount_point, temp_dir):
+    super(_ImageFileAccessor, self).__init__(path_prefix)
+    self._mount_point = mount_point
+    self._temp_dir = temp_dir
+
+  # override
+  def _handle_prepare_file(self, filename_in_storage):
+    mount_filename = os.path.join(self._mount_point, filename_in_storage)
+    filename = os.path.join(self._temp_dir, filename_in_storage)
+    logging.info('Prepare file %s -> %s', filename_in_storage, filename)
+    if not os.path.isfile(mount_filename):
+      logging.error('File does not exist: %s', filename_in_storage)
+      return None
+
+    self._make_parent_dirs(filename)
+    image_utils.copy_file(filename, mount_filename)
+
+    return base_mounter.MounterFile(filename)
+
+
+class ImageMounter(base_mounter.BaseMounter):
+  """Provides a file accessor which can access files in the given image file."""
+
+  DETECT_SYSTEM_AS_ROOT = 'detect-system-as-root'
+  _SYSTEM_FILES = ['compatibility_matrix.xml', 'build.prop', 'manifest.xml']
+
+  def __init__(self, image_filename, path_prefix):
+    super(ImageMounter, self).__init__()
+    self._image_filename = image_filename
+    self._path_prefix = path_prefix
+
+  @classmethod
+  def _detect_system_as_root(cls, mount_point):
+    """Returns True if the image layout on mount_point is system-as-root."""
+    logging.debug('Checking system-as-root on mount point %s...', mount_point)
+
+    system_without_root = True
+    for filename in cls._SYSTEM_FILES:
+      if not os.path.isfile(os.path.join(mount_point, filename)):
+        system_without_root = False
+        break
+
+    system_as_root = True
+    for filename in cls._SYSTEM_FILES:
+      if not os.path.isfile(os.path.join(mount_point, 'system', filename)):
+        system_as_root = False
+        break
+
+    ret = system_as_root and not system_without_root
+    logging.debug('  Result=%s', ret)
+    return ret
+
+  # override
+  def _handle_mount(self):
+    # Unsparse the image to a temp file
+    unsparsed_suffix = '_system.img.raw'
+    unsparsed_file = tempfile.NamedTemporaryFile(suffix=unsparsed_suffix)
+    unsparsed_filename = unsparsed_file.name
+    image_utils.unsparse(unsparsed_filename, self._image_filename)
+
+    # Mount it
+    mount_point = tempfile.mkdtemp()
+    logging.debug('Create a temp mount point %s', mount_point)
+    image_utils.mount(mount_point, unsparsed_filename)
+
+    # detect system-as-root if need
+    path_prefix = self._path_prefix
+    if path_prefix == self.DETECT_SYSTEM_AS_ROOT:
+      path_prefix = '/' if self._detect_system_as_root(
+          mount_point) else '/system/'
+
+    # Create a temp dir for the target of copying file from image
+    temp_dir = tempfile.mkdtemp()
+    logging.debug('Created temp dir: %s', temp_dir)
+
+    # Keep data to be removed on __exit__
+    self._unsparsed_file = unsparsed_file
+    self._mount_point = mount_point
+    self._temp_dir = tempfile.mkdtemp()
+
+    return _ImageFileAccessor(path_prefix, mount_point, temp_dir)
+
+  # override
+  def _handle_unmount(self):
+    if hasattr(self, '_temp_dir'):
+      logging.debug('Removing temp dir: %s', self._temp_dir)
+      shutil.rmtree(self._temp_dir)
+      del self._temp_dir
+
+    if hasattr(self, '_mount_point'):
+      image_utils.unmount(self._mount_point)
+      shutil.rmtree(self._mount_point)
+      del self._mount_point
+
+    if hasattr(self, '_unsparsed_file'):
+      # will also delete the temp file implicitly
+      del self._unsparsed_file
diff --git a/gsi/gsi_util/gsi_util/utils/adb_utils.py b/gsi/gsi_util/gsi_util/utils/adb_utils.py
new file mode 100644
index 0000000..3a7bfb3
--- /dev/null
+++ b/gsi/gsi_util/gsi_util/utils/adb_utils.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+#
+# Copyright 2017 - The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""ADB-related utilities."""
+
+import logging
+import subprocess
+
+from gsi_util.utils.cmd_utils import run_command
+
+
+def root(serial_num=None):
+  command = ['adb']
+  if serial_num:
+    command += ['-s', serial_num]
+  command += ['root']
+
+  # 'read_stdout=True' to disable output
+  run_command(command, raise_on_error=False, read_stdout=True, log_stderr=True)
+
+
+def pull(local_filename, remote_filename, serial_num=None):
+  command = ['adb']
+  if serial_num:
+    command += ['-s', serial_num]
+  command += ['pull', remote_filename, local_filename]
+
+  # 'read_stdout=True' to disable output
+  (returncode, _, _) = run_command(
+      command, raise_on_error=False, read_stdout=True, log_stdout=True)
+
+  return returncode == 0
diff --git a/gsi/gsi_util/gsi_util/utils/image_utils.py b/gsi/gsi_util/gsi_util/utils/image_utils.py
new file mode 100644
index 0000000..513c60e
--- /dev/null
+++ b/gsi/gsi_util/gsi_util/utils/image_utils.py
@@ -0,0 +1,41 @@
+# Copyright 2017 - The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Image-related utilities."""
+
+import logging
+
+from gsi_util.utils.cmd_utils import run_command
+
+
+def unsparse(output_filename, input_filename):
+  logging.debug('Unsparsing %s...', input_filename)
+  run_command(['simg2img', input_filename, output_filename])
+
+
+def mount(mount_point, image_filename):
+  logging.debug('Mounting...')
+  run_command(
+      ['mount', '-t', 'ext4', '-o', 'loop', image_filename, mount_point],
+      sudo=True)
+
+
+def unmount(mount_point):
+  logging.debug('Unmounting...')
+  run_command(['umount', '-l', mount_point], sudo=True, raise_on_error=False)
+
+
+def copy_file(dest, src):
+  run_command(['cp', src, dest], sudo=True)
+  # This is a hack to give access permission without root
+  run_command(['chmod', '+444', dest], sudo=True)