blob: 09a7cf785195152ae907a39f38e42a209a56f044 [file] [log] [blame]
Sen Jiange6e0f042018-05-24 15:40:41 -07001#!/usr/bin/env python2
2# -*- coding: utf-8 -*-
3#
4# Copyright (C) 2015 The Android Open Source Project
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17#
18
19"""payload_info: Show information about an update payload."""
20
21from __future__ import print_function
22
23import argparse
24import itertools
25import sys
26import textwrap
27
28import update_payload
29
30MAJOR_PAYLOAD_VERSION_CHROMEOS = 1
31MAJOR_PAYLOAD_VERSION_BRILLO = 2
32
33def DisplayValue(key, value):
34 """Print out a key, value pair with values left-aligned."""
35 if value != None:
36 print('%-*s %s' % (28, key + ':', value))
37 else:
38 raise ValueError('Cannot display an empty value.')
39
40
41def DisplayHexData(data, indent=0):
42 """Print out binary data as a hex values."""
43 for off in range(0, len(data), 16):
44 chunk = data[off:off + 16]
45 print(' ' * indent +
46 ' '.join('%.2x' % ord(c) for c in chunk) +
47 ' ' * (16 - len(chunk)) +
48 ' | ' +
49 ''.join(c if 32 <= ord(c) < 127 else '.' for c in chunk))
50
51
52class PayloadCommand(object):
53 """Show basic information about an update payload.
54
55 This command parses an update payload and displays information from
56 its header and manifest.
57 """
58
59 def __init__(self, options):
60 self.options = options
61 self.payload = None
62
63 def _DisplayHeader(self):
64 """Show information from the payload header."""
65 header = self.payload.header
66 DisplayValue('Payload version', header.version)
67 DisplayValue('Manifest length', header.manifest_len)
68
69 def _DisplayManifest(self):
70 """Show information from the payload manifest."""
71 manifest = self.payload.manifest
72 if self.payload.header.version == MAJOR_PAYLOAD_VERSION_BRILLO:
73 DisplayValue('Number of partitions', len(manifest.partitions))
74 for partition in manifest.partitions:
75 DisplayValue(' Number of "%s" ops' % partition.partition_name,
76 len(partition.operations))
77 else:
78 DisplayValue('Number of operations', len(manifest.install_operations))
79 DisplayValue('Number of kernel ops',
80 len(manifest.kernel_install_operations))
81 DisplayValue('Block size', manifest.block_size)
82 DisplayValue('Minor version', manifest.minor_version)
83
84 def _DisplaySignatures(self):
85 """Show information about the signatures from the manifest."""
86 header = self.payload.header
87 if header.metadata_signature_len:
88 offset = header.size + header.manifest_len
89 DisplayValue('Metadata signatures blob',
90 'file_offset=%d (%d bytes)' %
91 (offset, header.metadata_signature_len))
92 # pylint: disable=invalid-unary-operand-type
93 signatures_blob = self.payload.ReadDataBlob(
94 -header.metadata_signature_len,
95 header.metadata_signature_len)
96 self._DisplaySignaturesBlob('Metadata', signatures_blob)
97 else:
98 print('No metadata signatures stored in the payload')
99
100 manifest = self.payload.manifest
101 if manifest.HasField('signatures_offset'):
102 signature_msg = 'blob_offset=%d' % manifest.signatures_offset
103 if manifest.signatures_size:
104 signature_msg += ' (%d bytes)' % manifest.signatures_size
105 DisplayValue('Payload signatures blob', signature_msg)
106 signatures_blob = self.payload.ReadDataBlob(manifest.signatures_offset,
107 manifest.signatures_size)
108 self._DisplaySignaturesBlob('Payload', signatures_blob)
109 else:
110 print('No payload signatures stored in the payload')
111
112 @staticmethod
113 def _DisplaySignaturesBlob(signature_name, signatures_blob):
114 """Show information about the signatures blob."""
115 signatures = update_payload.update_metadata_pb2.Signatures()
116 signatures.ParseFromString(signatures_blob)
117 print('%s signatures: (%d entries)' %
118 (signature_name, len(signatures.signatures)))
119 for signature in signatures.signatures:
120 print(' version=%s, hex_data: (%d bytes)' %
121 (signature.version if signature.HasField('version') else None,
122 len(signature.data)))
123 DisplayHexData(signature.data, indent=4)
124
125
126 def _DisplayOps(self, name, operations):
127 """Show information about the install operations from the manifest.
128
129 The list shown includes operation type, data offset, data length, source
130 extents, source length, destination extents, and destinations length.
131
132 Args:
133 name: The name you want displayed above the operation table.
134 operations: The install_operations object that you want to display
135 information about.
136 """
137 def _DisplayExtents(extents, name):
138 """Show information about extents."""
139 num_blocks = sum([ext.num_blocks for ext in extents])
140 ext_str = ' '.join(
141 '(%s,%s)' % (ext.start_block, ext.num_blocks) for ext in extents)
142 # Make extent list wrap around at 80 chars.
143 ext_str = '\n '.join(textwrap.wrap(ext_str, 74))
144 extent_plural = 's' if len(extents) > 1 else ''
145 block_plural = 's' if num_blocks > 1 else ''
146 print(' %s: %d extent%s (%d block%s)' %
147 (name, len(extents), extent_plural, num_blocks, block_plural))
148 print(' %s' % ext_str)
149
150 op_dict = update_payload.common.OpType.NAMES
151 print('%s:' % name)
152 for op, op_count in itertools.izip(operations, itertools.count()):
153 print(' %d: %s' % (op_count, op_dict[op.type]))
154 if op.HasField('data_offset'):
155 print(' Data offset: %s' % op.data_offset)
156 if op.HasField('data_length'):
157 print(' Data length: %s' % op.data_length)
158 if op.src_extents:
159 _DisplayExtents(op.src_extents, 'Source')
160 if op.dst_extents:
161 _DisplayExtents(op.dst_extents, 'Destination')
162
163 def _GetStats(self, manifest):
164 """Returns various statistics about a payload file.
165
166 Returns a dictionary containing the number of blocks read during payload
167 application, the number of blocks written, and the number of seeks done
168 when writing during operation application.
169 """
170 read_blocks = 0
171 written_blocks = 0
172 num_write_seeks = 0
173 if self.payload.header.version == MAJOR_PAYLOAD_VERSION_BRILLO:
174 partitions_operations = [part.operations for part in manifest.partitions]
175 else:
176 partitions_operations = [manifest.install_operations,
177 manifest.kernel_install_operations]
178 for operations in partitions_operations:
179 last_ext = None
180 for curr_op in operations:
181 read_blocks += sum([ext.num_blocks for ext in curr_op.src_extents])
182 written_blocks += sum([ext.num_blocks for ext in curr_op.dst_extents])
183 for curr_ext in curr_op.dst_extents:
184 # See if the extent is contiguous with the last extent seen.
185 if last_ext and (curr_ext.start_block !=
186 last_ext.start_block + last_ext.num_blocks):
187 num_write_seeks += 1
188 last_ext = curr_ext
189
190 if manifest.minor_version == 1:
191 # Rootfs and kernel are written during the filesystem copy in version 1.
192 written_blocks += manifest.old_rootfs_info.size / manifest.block_size
193 written_blocks += manifest.old_kernel_info.size / manifest.block_size
194 # Old and new rootfs and kernel are read once during verification
195 read_blocks += manifest.old_rootfs_info.size / manifest.block_size
196 read_blocks += manifest.old_kernel_info.size / manifest.block_size
197 read_blocks += manifest.new_rootfs_info.size / manifest.block_size
198 read_blocks += manifest.new_kernel_info.size / manifest.block_size
199 stats = {'read_blocks': read_blocks,
200 'written_blocks': written_blocks,
201 'num_write_seeks': num_write_seeks}
202 return stats
203
204 def _DisplayStats(self, manifest):
205 stats = self._GetStats(manifest)
206 DisplayValue('Blocks read', stats['read_blocks'])
207 DisplayValue('Blocks written', stats['written_blocks'])
208 DisplayValue('Seeks when writing', stats['num_write_seeks'])
209
210 def Run(self):
211 """Parse the update payload and display information from it."""
212 self.payload = update_payload.Payload(self.options.payload_file)
213 self.payload.Init()
214 self._DisplayHeader()
215 self._DisplayManifest()
216 if self.options.signatures:
217 self._DisplaySignatures()
218 if self.options.stats:
219 self._DisplayStats(self.payload.manifest)
220 if self.options.list_ops:
221 print()
222 if self.payload.header.version == MAJOR_PAYLOAD_VERSION_BRILLO:
223 for partition in self.payload.manifest.partitions:
224 self._DisplayOps('%s install operations' % partition.partition_name,
225 partition.operations)
226 else:
227 self._DisplayOps('Install operations',
228 self.payload.manifest.install_operations)
229 self._DisplayOps('Kernel install operations',
230 self.payload.manifest.kernel_install_operations)
231
232
233def main():
234 parser = argparse.ArgumentParser(
235 description='Show information about an update payload.')
236 parser.add_argument('payload_file', type=file,
237 help='The update payload file.')
238 parser.add_argument('--list_ops', default=False, action='store_true',
239 help='List the install operations and their extents.')
240 parser.add_argument('--stats', default=False, action='store_true',
241 help='Show information about overall input/output.')
242 parser.add_argument('--signatures', default=False, action='store_true',
243 help='Show signatures stored in the payload.')
244 args = parser.parse_args()
245
246 PayloadCommand(args).Run()
247
248if __name__ == '__main__':
249 sys.exit(main())