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
This commit is contained in:
SzuWei Lin
2017-12-25 17:59:18 +08:00
parent ba6d87d125
commit e3658bd0c1
11 changed files with 766 additions and 1 deletions

View File

@@ -18,10 +18,13 @@ python_binary_host {
"gsi_util.py",
"gsi_util/*.py",
"gsi_util/commands/*.py",
"gsi_util/mounters/*.py",
"gsi_util/utils/*.py",
],
required: [
"adb",
"avbtool",
"simg2img",
],
version: {
py2: {

View File

@@ -28,7 +28,7 @@ class GsiUtil(object):
# 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

View File

@@ -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)

View File

@@ -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

View File

@@ -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()

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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)