diff --git a/gsi/gsi_util/Android.bp b/gsi/gsi_util/Android.bp index 2ab04c80b..cf72dd9bb 100644 --- a/gsi/gsi_util/Android.bp +++ b/gsi/gsi_util/Android.bp @@ -18,6 +18,7 @@ python_binary_host { "gsi_util.py", "gsi_util/*.py", "gsi_util/commands/*.py", + "gsi_util/dumpers/*.py", "gsi_util/mounters/*.py", "gsi_util/utils/*.py", ], diff --git a/gsi/gsi_util/gsi_util.py b/gsi/gsi_util/gsi_util.py index a226628e6..6dc09311e 100755 --- a/gsi/gsi_util/gsi_util.py +++ b/gsi/gsi_util/gsi_util.py @@ -28,7 +28,7 @@ class GsiUtil(object): # Adds gsi_util COMMAND here. # TODO(bowgotsai): auto collect from gsi_util/commands/*.py - _COMMANDS = ['flash_gsi', 'pull', 'hello'] + _COMMANDS = ['flash_gsi', 'pull', 'dump', 'hello'] _LOGGING_FORMAT = '%(message)s' _LOGGING_LEVEL = logging.WARNING diff --git a/gsi/gsi_util/gsi_util/commands/dump.py b/gsi/gsi_util/gsi_util/commands/dump.py new file mode 100644 index 000000000..f89db3ec2 --- /dev/null +++ b/gsi/gsi_util/gsi_util/commands/dump.py @@ -0,0 +1,168 @@ +# 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 'dump'.""" + +import argparse +import logging +import sys + +from gsi_util.dumpers.dumper import Dumper +from gsi_util.mounters.composite_mounter import CompositeMounter + + +class DumpReporter(object): + """Format and output dump info result to a output stream. + + When constructing DumpReporter, you need to give os and name_list. + os is the stream to output the formatted, which should be inherited from + io.IOBase. name_list is a string list describe the info names to be output. + + After collected all dump result, calls output() to output the + dump_result_dict. dump_result_dict is a dictionary that maps info names to + theirs result values. + """ + + _UNKNOWN_VALUE = '' + + def __init__(self, os, name_list): + """Inits DumpReporter with an output stream and an info name list. + + Args: + os: the output stream of outputing the report + name_list: the info name list will be output + """ + self._os = os + self._name_list = name_list + self._show_unknown = False + + def set_show_unknown(self): + """Enable force output dump info without dump result. + + By default, it doesn't output the dump info in the info name list which + is not in dump result, i.e. the dump_result_dict of output(). + """ + self._show_unknown = True + + def _output_dump_info(self, info_name, value): + print >> self._os, '{:30}: {}'.format(info_name, value) + + def output(self, dump_result_dict): + """Output the given dump result. + + Args: + dump_result_dict: the dump result dictionary to be output + """ + for info_name in self._name_list: + value = dump_result_dict.get(info_name) + if not value: + if not self._show_unknown: + continue + value = self._UNKNOWN_VALUE + + self._output_dump_info(info_name, value) + + +def do_list_dump(_): + for info in Dumper.get_all_dump_list(): + print info.info_name + + +def do_dump(args): + logging.info('==== DUMP ====') + logging.info(' system=%s vendor=%s', args.system, args.vendor) + if not args.system and not args.vendor: + sys.exit('Without system nor vendor.') + + mounter = CompositeMounter() + if args.system: + mounter.add_by_mount_target('system', args.system) + if args.vendor: + mounter.add_by_mount_target('vendor', args.vendor) + + logging.debug('Info name list: %s', args.INFO_NAME) + dump_list = Dumper.make_dump_list_by_name_list(args.INFO_NAME) if len( + args.INFO_NAME) else Dumper.get_all_dump_list() + + with mounter as file_accessor: + dumper = Dumper(file_accessor) + dump_result_dict = dumper.dump(dump_list) + + # reserved for output to a file + os = sys.stdout + reporter = DumpReporter(os, (x.info_name for x in dump_list)) + if args.show_unknown: + reporter.set_show_unknown() + reporter.output(dump_result_dict) + + logging.info('==== DONE ====') + + +DUMP_DESCRIPTION = """'dump' command dumps information from given image + +You must assign at least one image source by SYSTEM and/or VENDOR. +Image source could be: + + adb[:SERIAL_NUM]: form the device which be connected with adb + image file name: 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: from the given folder, e.g. the system/vendor folder in an + Android build out folder. + +You could use command 'list_dump' to query all info names: + + $ ./gsi_util.py list_dump + +For example you could use following command to query the security patch level +in an system image file: + + $ ./gsi_util.py dump --system system.img system_security_patch_level + +You there is no given INFO_NAME, all information will be dumped. + +Here are some other usage examples: + + $ ./gsi_util.py dump --system adb --vendor adb + $ ./gsi_util.py dump --system system.img --show-unknown + $ ./gsi_util.py dump --system my/out/folder/system""" + + +def setup_command_args(parser): + # command 'list_dump' + list_dump_parser = parser.add_parser( + 'list_dump', help='list all possible info names') + list_dump_parser.set_defaults(func=do_list_dump) + + # command 'dump' + dump_parser = parser.add_parser( + 'dump', + help='dump information from 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( + '-u', + '--show-unknown', + action='store_true', + help='force display the dump info items in list which does not exist') + dump_parser.add_argument( + 'INFO_NAME', + type=str, + nargs='*', + help='the info name to be dumped. Dump all if not given') + dump_parser.set_defaults(func=do_dump) diff --git a/gsi/gsi_util/gsi_util/dumpers/__init__.py b/gsi/gsi_util/gsi_util/dumpers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/gsi/gsi_util/gsi_util/dumpers/dump_info_list.py b/gsi/gsi_util/gsi_util/dumpers/dump_info_list.py new file mode 100644 index 000000000..defc2c253 --- /dev/null +++ b/gsi/gsi_util/gsi_util/dumpers/dump_info_list.py @@ -0,0 +1,44 @@ +# 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. + +"""Provide the information list for command 'dump'.""" + +from collections import namedtuple + +from gsi_util.dumpers.prop_dumper import PropDumper +from gsi_util.dumpers.xml_dumper import XmlDumper + +SYSTEM_MATRIX_DUMPER = (XmlDumper, '/system/compatibility_matrix.xml') +SYSTEM_BUILD_PROP_DUMPER = (PropDumper, '/system/build.prop') +SYSTEM_MANIFEST_DUMPER = (PropDumper, '/system/manifest.xml') + +VENDOR_DEFAULT_PROP_DUMPER = (PropDumper, '/vendor/default.prop') +VENDOR_BUILD_PROP_DUMPER = (PropDumper, '/vendor/build.prop') + +DumpInfoListItem = namedtuple('DumpInfoListItem', + 'info_name dumper_create_args lookup_key') + +# The total list of all possible dump info. +# It will be output by the order of the list. +DUMP_LIST = [ + DumpInfoListItem('system_build_id', SYSTEM_BUILD_PROP_DUMPER, 'ro.build.display.id'), + DumpInfoListItem('system_sdk_ver', SYSTEM_BUILD_PROP_DUMPER, 'ro.build.version.sdk'), + DumpInfoListItem('system_security_patch_level', SYSTEM_BUILD_PROP_DUMPER, 'ro.build.version.security_patch'), + DumpInfoListItem('system_kernel_sepolicy_ver', SYSTEM_MATRIX_DUMPER, './sepolicy/kernel-sepolicy-version'), + DumpInfoListItem('system_support_sepolicy_ver', SYSTEM_MATRIX_DUMPER, './sepolicy/sepolicy-version'), + DumpInfoListItem('system_avb_ver', SYSTEM_MATRIX_DUMPER, './avb/vbmeta-version'), + DumpInfoListItem('vendor_fingerprint', VENDOR_BUILD_PROP_DUMPER, 'ro.vendor.build.fingerprint'), + DumpInfoListItem('vendor_low_ram', VENDOR_BUILD_PROP_DUMPER, 'ro.config.low_ram'), + DumpInfoListItem('vendor_zygote', VENDOR_DEFAULT_PROP_DUMPER, 'ro.zygote'), +] # pyformat: disable diff --git a/gsi/gsi_util/gsi_util/dumpers/dumper.py b/gsi/gsi_util/gsi_util/dumpers/dumper.py new file mode 100644 index 000000000..8980d1d3f --- /dev/null +++ b/gsi/gsi_util/gsi_util/dumpers/dumper.py @@ -0,0 +1,86 @@ +# 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. + +"""Implement dump methods and utils to dump info from a mounter.""" + +from gsi_util.dumpers.dump_info_list import DUMP_LIST + + +class Dumper(object): + + def __init__(self, file_accessor): + self._file_accessor = file_accessor + + @staticmethod + def _dump_by_dumper(dumper_instance, dump_list): + """Dump info by the given dumper instance according the given dump_list. + + Used for dump(), see the comment of dump() for type details. + + Args: + dumper_instance: a dumper instance to process dump. + dump_list: a list of dump info to be dump. The items in the list must + relative to dumper_instance. + + Returns: + The dump result by dictionary maps info_name to the value of dump result. + """ + dump_result = {} + + for dump_info in dump_list: + value = dumper_instance.dump(dump_info.lookup_key) + dump_result[dump_info.info_name] = value + + return dump_result + + def dump(self, dump_list): + """Dump info according the given dump_list. + + Args: + dump_list: a list of dump info to be dump. See dump_info_list.py for + the detail types. + Returns: + The dump result by dictionary maps info_name to the value of dump result. + """ + dump_result = {} + + # query how many different dumpers to dump + dumper_set = set([x.dumper_create_args for x in dump_list]) + for dumper_create_args in dumper_set: + # The type of a dumper_create_args is (Class, instantiation args...) + dumper_class = dumper_create_args[0] + dumper_args = dumper_create_args[1:] + # Create the dumper + with dumper_class(self._file_accessor, dumper_args) as dumper_instance: + dump_list_for_the_dumper = ( + x for x in dump_list if x.dumper_create_args == dumper_create_args) + dumper_result = self._dump_by_dumper(dumper_instance, + dump_list_for_the_dumper) + dump_result.update(dumper_result) + + return dump_result + + @staticmethod + def make_dump_list_by_name_list(name_list): + info_list = [] + for info_name in name_list: + info = next((x for x in DUMP_LIST if x.info_name == info_name), None) + if not info: + raise RuntimeError('Unknown info name: "{}"'.format(info_name)) + info_list.append(info) + return info_list + + @staticmethod + def get_all_dump_list(): + return DUMP_LIST diff --git a/gsi/gsi_util/gsi_util/dumpers/prop_dumper.py b/gsi/gsi_util/gsi_util/dumpers/prop_dumper.py new file mode 100644 index 000000000..e87992c60 --- /dev/null +++ b/gsi/gsi_util/gsi_util/dumpers/prop_dumper.py @@ -0,0 +1,44 @@ +# 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 PropDumper.""" + +import logging +import re + + +class PropDumper(object): + + def __init__(self, file_accessor, args): + filename_in_mount = args[0] + logging.debug('Parse %s...', filename_in_mount) + with file_accessor.prepare_file(filename_in_mount) as filename: + if filename: + with open(filename) as fp: + self._content = fp.read() + + def __enter__(self): + # do nothing + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if hasattr(self, '_content'): + del self._content + + def dump(self, lookup_key): + if not hasattr(self, '_content'): + return None + + match = re.search('%s=(.*)' % (lookup_key), self._content) + return match.group(1) if match else None diff --git a/gsi/gsi_util/gsi_util/dumpers/xml_dumper.py b/gsi/gsi_util/gsi_util/dumpers/xml_dumper.py new file mode 100644 index 000000000..9ea6f6a5c --- /dev/null +++ b/gsi/gsi_util/gsi_util/dumpers/xml_dumper.py @@ -0,0 +1,44 @@ +# 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 XmlDumper.""" + +import logging +import xml.etree.ElementTree as ET + + +class XmlDumper(object): + + def __init__(self, file_accessor, args): + filename_in_mount = args[0] + logging.debug('Parse %s...', filename_in_mount) + with file_accessor.prepare_file(filename_in_mount) as filename: + if filename: + self._tree = ET.parse(filename) + + def __enter__(self): + # do nothing + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if hasattr(self, '_tree'): + del self._tree + + def dump(self, lookup_key): + if not hasattr(self, '_tree'): + return None + + xpath = lookup_key + results = self._tree.findall(xpath) + return ', '.join([e.text for e in results]) if results else None