blob: 96b1032feca6e2c10e16e1cbd888756b0e34ddd9 [file] [log] [blame]
#!/usr/bin/python2
#
# Copyright (C) 2013 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.
#
"""Command-line tool for checking and applying Chrome OS update payloads."""
from __future__ import print_function
# pylint: disable=import-error
import argparse
import filecmp
import os
import sys
import tempfile
from update_payload import error
lib_dir = os.path.join(os.path.dirname(__file__), 'lib')
if os.path.exists(lib_dir) and os.path.isdir(lib_dir):
sys.path.insert(1, lib_dir)
import update_payload
_TYPE_FULL = 'full'
_TYPE_DELTA = 'delta'
def ParseArguments(argv):
"""Parse and validate command-line arguments.
Args:
argv: command-line arguments to parse (excluding the program name)
Returns:
Returns the arguments returned by the argument parser.
"""
parser = argparse.ArgumentParser(
description=('Applies a Chrome OS update PAYLOAD to src_kern and '
'src_root emitting dst_kern and dst_root, respectively. '
'src_kern and src_root are only needed for delta payloads. '
'When no partitions are provided, verifies the payload '
'integrity.'),
epilog=('Note: a payload may verify correctly but fail to apply, and '
'vice versa; this is by design and can be thought of as static '
'vs dynamic correctness. A payload that both verifies and '
'applies correctly should be safe for use by the Chrome OS '
'Update Engine. Use --check to verify a payload prior to '
'applying it.'),
formatter_class=argparse.RawDescriptionHelpFormatter
)
check_args = parser.add_argument_group('Checking payload integrity')
check_args.add_argument('-c', '--check', action='store_true', default=False,
help=('force payload integrity check (e.g. before '
'applying)'))
check_args.add_argument('-D', '--describe', action='store_true',
default=False,
help='Print a friendly description of the payload.')
check_args.add_argument('-r', '--report', metavar='FILE',
help="dump payload report (`-' for stdout)")
check_args.add_argument('-t', '--type', dest='assert_type',
help='assert the payload type',
choices=[_TYPE_FULL, _TYPE_DELTA])
check_args.add_argument('-z', '--block-size', metavar='NUM', default=0,
type=int,
help='assert a non-default (4096) payload block size')
check_args.add_argument('-u', '--allow-unhashed', action='store_true',
default=False, help='allow unhashed operations')
check_args.add_argument('-d', '--disabled_tests', default=(), metavar='',
help=('space separated list of tests to disable. '
'allowed options include: ' +
', '.join(update_payload.CHECKS_TO_DISABLE)),
choices=update_payload.CHECKS_TO_DISABLE)
check_args.add_argument('-k', '--key', metavar='FILE',
help=('override standard key used for signature '
'validation'))
check_args.add_argument('-m', '--meta-sig', metavar='FILE',
help='verify metadata against its signature')
check_args.add_argument('-p', '--root-part-size', metavar='NUM',
default=0, type=int,
help='override rootfs partition size auto-inference')
check_args.add_argument('-P', '--kern-part-size', metavar='NUM',
default=0, type=int,
help='override kernel partition size auto-inference')
apply_args = parser.add_argument_group('Applying payload')
# TODO(ahassani): Extent extract-bsdiff to puffdiff too.
apply_args.add_argument('-x', '--extract-bsdiff', action='store_true',
default=False,
help=('use temp input/output files with BSDIFF '
'operations (not in-place)'))
apply_args.add_argument('--bspatch-path', metavar='FILE',
help='use the specified bspatch binary')
apply_args.add_argument('--puffpatch-path', metavar='FILE',
help='use the specified puffpatch binary')
apply_args.add_argument('--dst_kern', metavar='FILE',
help='destination kernel partition file')
apply_args.add_argument('--dst_root', metavar='FILE',
help='destination root partition file')
apply_args.add_argument('--src_kern', metavar='FILE',
help='source kernel partition file')
apply_args.add_argument('--src_root', metavar='FILE',
help='source root partition file')
apply_args.add_argument('--out_dst_kern', metavar='FILE',
help='created destination kernel partition file')
apply_args.add_argument('--out_dst_root', metavar='FILE',
help='created destination root partition file')
parser.add_argument('payload', metavar='PAYLOAD', help='the payload file')
# Parse command-line arguments.
args = parser.parse_args(argv)
# There are several options that imply --check.
args.check = (args.check or args.report or args.assert_type or
args.block_size or args.allow_unhashed or
args.disabled_tests or args.meta_sig or args.key or
args.root_part_size or args.kern_part_size)
# Check the arguments, enforce payload type accordingly.
if (args.src_kern is None) != (args.src_root is None):
parser.error('--src_kern and --src_root should be given together')
if (args.dst_kern is None) != (args.dst_root is None):
parser.error('--dst_kern and --dst_root should be given together')
if (args.out_dst_kern is None) != (args.out_dst_root is None):
parser.error('--out_dst_kern and --out_dst_root should be given together')
if (args.dst_kern and args.dst_root) or \
(args.out_dst_kern and args.out_dst_root):
if args.src_kern and args.src_root:
if args.assert_type == _TYPE_FULL:
parser.error('%s payload does not accept source partition arguments'
% _TYPE_FULL)
else:
args.assert_type = _TYPE_DELTA
else:
if args.assert_type == _TYPE_DELTA:
parser.error('%s payload requires source partitions arguments'
% _TYPE_DELTA)
else:
args.assert_type = _TYPE_FULL
else:
# Not applying payload.
if args.extract_bsdiff:
parser.error('--extract-bsdiff can only be used when applying payloads')
if args.bspatch_path:
parser.error('--bspatch-path can only be used when applying payloads')
if args.puffpatch_path:
parser.error('--puffpatch-path can only be used when applying payloads')
# By default, look for a metadata-signature file with a name based on the name
# of the payload we are checking. We only do it if check was triggered.
if args.check and not args.meta_sig:
default_meta_sig = args.payload + '.metadata-signature'
if os.path.isfile(default_meta_sig):
args.meta_sig = default_meta_sig
print('Using default metadata signature', args.meta_sig, file=sys.stderr)
return args
def main(argv):
# Parse and validate arguments.
args = ParseArguments(argv[1:])
with open(args.payload) as payload_file:
payload = update_payload.Payload(payload_file)
try:
# Initialize payload.
payload.Init()
if args.describe:
payload.Describe()
# Perform payload integrity checks.
if args.check:
report_file = None
do_close_report_file = False
metadata_sig_file = None
try:
if args.report:
if args.report == '-':
report_file = sys.stdout
else:
report_file = open(args.report, 'w')
do_close_report_file = True
metadata_sig_file = args.meta_sig and open(args.meta_sig)
payload.Check(
pubkey_file_name=args.key,
metadata_sig_file=metadata_sig_file,
report_out_file=report_file,
assert_type=args.assert_type,
block_size=int(args.block_size),
rootfs_part_size=args.root_part_size,
kernel_part_size=args.kern_part_size,
allow_unhashed=args.allow_unhashed,
disabled_tests=args.disabled_tests)
finally:
if metadata_sig_file:
metadata_sig_file.close()
if do_close_report_file:
report_file.close()
# Apply payload.
if (args.dst_root and args.dst_kern) or \
(args.out_dst_root and args.out_dst_kern):
dargs = {'bsdiff_in_place': not args.extract_bsdiff}
if args.bspatch_path:
dargs['bspatch_path'] = args.bspatch_path
if args.puffpatch_path:
dargs['puffpatch_path'] = args.puffpatch_path
if args.assert_type == _TYPE_DELTA:
dargs['old_kernel_part'] = args.src_kern
dargs['old_rootfs_part'] = args.src_root
if args.out_dst_kern and args.out_dst_root:
out_dst_kern = open(args.out_dst_kern, 'w+')
out_dst_root = open(args.out_dst_root, 'w+')
else:
out_dst_kern = tempfile.NamedTemporaryFile()
out_dst_root = tempfile.NamedTemporaryFile()
payload.Apply(out_dst_kern.name, out_dst_root.name, **dargs)
# If destination kernel and rootfs partitions are not given, then this
# just becomes an apply operation with no check.
if args.dst_kern and args.dst_root:
# Prior to comparing, add the unused space past the filesystem
# boundary in the new target partitions to become the same size as
# the given partitions. This will truncate to larger size.
out_dst_kern.truncate(os.path.getsize(args.dst_kern))
out_dst_root.truncate(os.path.getsize(args.dst_root))
# Compare resulting partitions with the ones from the target image.
if not filecmp.cmp(out_dst_kern.name, args.dst_kern):
raise error.PayloadError('Resulting kernel partition corrupted.')
if not filecmp.cmp(out_dst_root.name, args.dst_root):
raise error.PayloadError('Resulting rootfs partition corrupted.')
# Close the output files. If args.out_dst_* was not given, then these
# files are created as temp files and will be deleted upon close().
out_dst_kern.close()
out_dst_root.close()
except error.PayloadError, e:
sys.stderr.write('Error: %s\n' % e)
return 1
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv))