| # Copyright 2021 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """cros ap: firmware AP related commands.""" |
| |
| import argparse |
| import logging |
| import os |
| from pathlib import Path |
| |
| from chromite.cli import command |
| from chromite.lib import build_target_lib |
| from chromite.lib import commandline |
| from chromite.lib import cros_build_lib |
| from chromite.lib.firmware import dut |
| from chromite.lib.firmware import firmware_config |
| from chromite.lib.firmware import firmware_lib |
| |
| |
| # All known ap subcommands. |
| SUBCOMMANDS = {} |
| |
| |
| def subcommand_decorator(name): |
| """Decorator that validates and registers subcommands.""" |
| |
| def inner_decorator(original_class): |
| """Inner decorator that actually wraps the class.""" |
| assert hasattr(original_class, '__doc__'), f'{name}: missing docstring' |
| |
| assert issubclass(original_class, command.CliCommand), ( |
| f'{original_class}: subcommands must derive from CliCommand') |
| |
| SUBCOMMANDS[name] = original_class |
| |
| return original_class |
| |
| return inner_decorator |
| |
| |
| @command.command_decorator('ap') |
| class APCommand(command.CliCommand): |
| """Execute an AP-related command.""" |
| |
| EPILOG = 'Use `cros ${subcommand} --help` to see command-specific help.' |
| |
| @classmethod |
| def AddParser(cls, parser): |
| """Add AP specific subcommands and options.""" |
| super(APCommand, cls).AddParser(parser) |
| subparsers = parser.add_subparsers( |
| title='AP subcommands', dest='ap_command') |
| subparsers.required = True |
| |
| for name, subcommand_class in SUBCOMMANDS.items(): |
| sub_parser = subparsers.add_parser( |
| name, |
| description=subcommand_class.__doc__, |
| caching=parser.caching, |
| help=subcommand_class.__doc__, |
| formatter_class=parser.formatter_class) |
| subcommand_class.AddParser(sub_parser) |
| |
| @classmethod |
| def ProcessOptions(cls, parser, options): |
| """Post process options.""" |
| sub_class = SUBCOMMANDS[options.ap_command] |
| sub_class.ProcessOptions(parser, options) |
| |
| def Run(self): |
| """The main handler of this CLI.""" |
| cls = SUBCOMMANDS[self.options.ap_command] |
| subcmd = cls(self.options) |
| return subcmd.Run() |
| |
| |
| @subcommand_decorator('dump-config') |
| class DumpConfigSubcommand(command.CliCommand): |
| """Dump the AP Config to a file.""" |
| |
| @classmethod |
| def ProcessOptions(cls, parser, options): |
| """Post process options.""" |
| if options.output: |
| options.output = Path(options.output) |
| |
| @classmethod |
| def AddParser(cls, parser): |
| """Adds AP DumpConfig specific CLI arguments to parser.""" |
| parser.add_argument( |
| '-b', |
| '--boards', |
| default=None, |
| action='split_extend', |
| dest='boards', |
| help='Quoted, space-separated list of boards. ' |
| '(default: all boards in chromite/lib/firmware/ap_firmware_config)') |
| parser.add_argument( |
| '--serial', |
| default='%s', |
| help='Serial of the servos. (default: %(default)s)') |
| parser.add_argument('-o', '--output', type='path', help='Output file.') |
| parser.epilog = """ |
| Dump DUT controls and programmer arguments into a provided file. |
| |
| To dump AP config of all boards into /tmp/cros-read-ap-config.json |
| cros ap dump-config -o /tmp/cros-read-ap-config.json |
| |
| To dump AP config of drallion and dedede boards: |
| cros ap dump-config -o /tmp/cros-read-ap-config.json -b "drallion dedede" |
| """ |
| |
| def Run(self): |
| """Perform the cros ap dump-config command.""" |
| boards = None |
| if self.options.boards: |
| boards = self.options.boards |
| |
| firmware_config.export_config_as_json(boards, self.options.output, |
| self.options.serial) |
| |
| |
| @subcommand_decorator('build') |
| class BuildSubcommand(command.CliCommand): |
| """Build the AP Firmware for the requested build target.""" |
| |
| def __init__(self, options): |
| super().__init__(options) |
| self.build_target = build_target_lib.BuildTarget(self.options.build_target) |
| |
| @classmethod |
| def AddParser(cls, parser): |
| """Adds AP Build specific CLI arguments to parser.""" |
| parser.add_argument( |
| '-b', |
| '--build-target', |
| dest='build_target', |
| default=cros_build_lib.GetDefaultBoard(), |
| required=not bool(cros_build_lib.GetDefaultBoard()), |
| help='The build target whose AP firmware should be built.') |
| parser.add_argument( |
| '--fw-name', |
| '--variant', |
| dest='fw_name', |
| help='Sets the FW_NAME environment variable. Set to build only the ' |
| "specified variant's firmware.") |
| parser.add_argument( |
| '-n', |
| '--dry-run', |
| action='store_true', |
| default=False, |
| help='Perform a dry run, describing the steps without running them.') |
| parser.epilog = """ |
| To build the AP Firmware for foo: |
| cros ap build -b foo |
| |
| To build the AP Firmware only for foo-variant: |
| cros ap build -b foo --fw-name foo-variant |
| """ |
| |
| def Run(self): |
| commandline.RunInsideChroot(self) |
| |
| try: |
| firmware_lib.build( |
| self.build_target, |
| fw_name=self.options.fw_name, |
| dry_run=self.options.dry_run) |
| except firmware_lib.Error as e: |
| cros_build_lib.Die(e) |
| |
| |
| @subcommand_decorator('read') |
| class ReadSubcommand(command.CliCommand): |
| """Read the AP Firmware from a device.""" |
| |
| @classmethod |
| def ProcessOptions(cls, parser, options): |
| """Post process options.""" |
| if options.device is None: |
| parser.error('Specify device using --device argument.') |
| options.output_path = Path(options.output) |
| |
| @classmethod |
| def AddParser(cls, parser): |
| """Adds AP Read specific CLI arguments to parser.""" |
| cls.AddDeviceArgument( |
| parser, |
| schemes=[ |
| commandline.DEVICE_SCHEME_SSH, commandline.DEVICE_SCHEME_SERVO |
| ]) |
| parser.add_argument( |
| '-b', |
| '--build-target', |
| default=cros_build_lib.GetDefaultBoard(), |
| dest='build_target', |
| help='The name of the build target.') |
| parser.add_argument('-r' |
| '--region', |
| dest='region', |
| type=str, |
| help='Region to read.') |
| parser.add_argument( |
| '-o', '--output', type='path', required=True, help='Output file.') |
| parser.add_argument( |
| '-n', |
| '--dry-run', |
| action='store_true', |
| help='Execute a dry-run. Print the commands that would be run instead ' |
| 'of running them.') |
| parser.epilog = """Command to read the AP firmware from a DUT. |
| To read image of device.cros via SSH: |
| cros ap read -b volteer -o /tmp/volteer-image.bin -d ssh://device.cros |
| |
| If you don't have ssh access from within the chroot, you may set up ssh tunnel: |
| ssh -L 2222:localhost:22 device.cros |
| cros ap read -b volteer -o /tmp/volteer-image.bin -d ssh://localhost:2222 |
| |
| To read image from DUT via SERVO on port 1234: |
| cros ap read -b volteer -o /tmp/volteer-image.bin -d servo:port:1234 |
| |
| To read a specific region from DUT via SERVO on default port(9999): |
| cros ap read -b volteer -r region -o /tmp/volteer-image.bin -d servo:port |
| """ |
| |
| def Run(self): |
| if not cros_build_lib.IsInsideChroot(): |
| logging.notice('Command will run in chroot, ' |
| 'and the output file path will be inside.') |
| commandline.RunInsideChroot(self) |
| build_target = build_target_lib.BuildTarget(self.options.build_target) |
| |
| ip = None |
| if self.options.device: |
| port = self.options.device.port |
| if self.options.device.scheme == commandline.DEVICE_SCHEME_SSH: |
| ip = self.options.device.hostname |
| port = port or self.options.device.port |
| else: |
| ip = os.getenv('IP') |
| |
| region = None |
| if hasattr(self.options, 'region'): |
| region = self.options.region |
| |
| if ip: |
| firmware_lib.ssh_read(self.options.output, self.options.verbose, ip, port, |
| self.options.dry_run, region) |
| else: |
| dut_ctl = dut.DutControl(port) |
| servo = dut_ctl.get_servo() |
| |
| ap_config = firmware_config.get_config(build_target.name, servo) |
| |
| flashrom_cmd = [ |
| 'flashrom', '-p', ap_config.programmer, '-r', self.options.output |
| ] |
| if self.options.verbose: |
| flashrom_cmd += ['-V'] |
| if region: |
| flashrom_cmd += ['-i', self.options.region] |
| if not dut_ctl.servo_run(ap_config.dut_control_on, |
| ap_config.dut_control_off, flashrom_cmd, |
| self.options.verbose, self.options.dry_run): |
| logging.error('Unable to read, verify servo connection ' |
| 'is correct and servod is running in the background.') |
| |
| |
| @subcommand_decorator('flash') |
| class FlashSubcommand(command.CliCommand): |
| """Update the AP Firmware on a device.""" |
| |
| @classmethod |
| def ProcessOptions(cls, parser, options): |
| """Post process options.""" |
| if not os.path.exists(options.image): |
| parser.error( |
| f'{options.image} does not exist, verify the path of your build and ' |
| 'try again.') |
| if options.fast: |
| parser.error( |
| 'Flags such as --fast must be passed directly after --\n' |
| 'For futility use: cros ap flash ${OTHER_ARGS} -- --fast\n' |
| 'For flashrom use: cros ap flash --flashrom ${OTHER_ARGS} -- -n') |
| |
| @classmethod |
| def AddParser(cls, parser): |
| """Adds AP Flash specific CLI arguments to parser.""" |
| cls.AddDeviceArgument( |
| parser, |
| schemes=[ |
| commandline.DEVICE_SCHEME_SSH, commandline.DEVICE_SCHEME_SERVO |
| ]) |
| parser.add_argument( |
| '-i', |
| '--image', |
| required=True, |
| type='path', |
| help='/path/to/BIOS_image.bin') |
| parser.add_argument( |
| '-b', |
| '--build-target', |
| default=cros_build_lib.GetDefaultBoard(), |
| dest='build_target', |
| help='The name of the build target.') |
| parser.add_argument( |
| '--flashrom', |
| action='store_true', |
| help='Use flashrom to flash instead of futility.') |
| parser.add_argument( |
| '--flash-contents', |
| type=str, |
| help='Assume flash contents to be the specified file. ' |
| 'Only available when using --flashrom.') |
| parser.add_argument( |
| '--fast', |
| action='store_true', |
| help='Deprecated. Pass your arbitrary flags after --.') |
| parser.add_argument( |
| '-n', |
| '--dry-run', |
| action='store_true', |
| help='Execute a dry-run. Print the commands that would be run instead ' |
| 'of running them.') |
| parser.add_argument( |
| 'extra_options', |
| nargs=argparse.REMAINDER, |
| help='Pass additional options to flashrom/futility.') |
| parser.epilog = """ |
| Command to flash the AP firmware onto a DUT. |
| |
| To flash your zork DUT with an IP of 1.1.1.1 via SSH: |
| cros ap flash -b zork -i /path/to/image.bin -d ssh://1.1.1.1 |
| |
| To flash your volteer DUT via SERVO on the default port (9999): |
| cros ap flash -d servo:port -b volteer -i /path/to/image.bin |
| |
| To flash your volteer DUT via SERVO on port 1234: |
| cros ap flash -d servo:port:1234 -b volteer -i /path/to/image.bin |
| |
| To pass additional options to futility or flashrom, provide them after `--`, |
| e.g.: |
| cros ap flash -b zork -i /path/to/image.bin -d ssh://1.1.1.1 -- --force |
| """ |
| |
| def Run(self): |
| commandline.RunInsideChroot(self) |
| |
| passthrough_args = self.options.extra_options |
| if passthrough_args and passthrough_args[0] == '--': |
| del passthrough_args[0] |
| |
| build_target = build_target_lib.BuildTarget(self.options.build_target) |
| try: |
| firmware_lib.deploy( |
| build_target, |
| self.options.image, |
| self.options.device, |
| flashrom=self.options.flashrom, |
| verbose=self.options.verbose, |
| dryrun=self.options.dry_run, |
| flash_contents=self.options.flash_contents, |
| passthrough_args=passthrough_args) |
| except firmware_lib.Error as e: |
| cros_build_lib.Die(e) |
| |
| |
| @subcommand_decorator('clean') |
| class CleanSubcommand(command.CliCommand): |
| """Clean up dependencies and artifacts for the requested build target.""" |
| |
| def __init__(self, options): |
| super().__init__(options) |
| self.build_target = build_target_lib.BuildTarget(self.options.build_target) |
| |
| @classmethod |
| def AddParser(cls, parser): |
| """Adds AP Clean specific CLI arguments to parser.""" |
| parser.add_argument( |
| '-b', |
| '--build-target', |
| default=cros_build_lib.GetDefaultBoard(), |
| required=not bool(cros_build_lib.GetDefaultBoard()), |
| help='The build target whose artifacts should be cleaned.') |
| parser.add_argument( |
| '-n', |
| '--dry-run', |
| action='store_true', |
| help='Perform a dry run, describing the steps without running them.') |
| parser.epilog = """ |
| This command removes firmware-related packages, including everything in |
| `/build/${build_target}/firmware`. |
| """ |
| |
| def Run(self): |
| if not cros_build_lib.IsInsideChroot(): |
| logging.notice('Command will run in chroot, ' |
| 'and the output file path will be inside.') |
| commandline.RunInsideChroot(self) |
| |
| try: |
| firmware_lib.clean(self.build_target, self.options.dry_run) |
| except firmware_lib.Error as e: |
| cros_build_lib.Die(e) |