# Copyright 2015 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.

"""Command-line parsing for the DUT deployment tool.

This contains parsing for the legacy `repair_test` and `deployment_test`
commands, and for the new `deploy` command.

The syntax for the two legacy commands is identical; the difference in
the two commands is merely slightly different default options.

The full deployment flow performs all of the following actions:
  * Stage the USB image:  Install the DUT's assigned repair image onto
    the Servo USB stick.
  * Install firmware:  Boot the DUT from the USB stick, and run
    `chromeos-firmwareupdate` to install dev-signed RO and RW firmware.
    The DUT must begin in dev-mode, with hardware write-protect
    disabled.  At successful completion, the DUT is in verified boot
    mode.
  * Install test image:  Boot the DUT in recovery mode from the USB
    stick, and run `chromeos-install` to install the OS.

The new `deploy` command chooses particular combinations of the steps
above based on a subcommand and options:
    `deploy servo`:  Only stage the USB image.
    `deploy firmware`:  Install both the firmware and the test image,
        in that order.  Optionally, first stage the USB image.
    `deploy test-image`: Install the test image.  Optionally, first
        stage the USB image.
    `deploy repair`:  Equivalent to `deploy test-image`, except that
        by default it doesn't upload its logs to storage.

This module exports two functions, `parse_deprecated_command()` (for the
two legacy commands) and `parse_command()` (for the new `deploy`
command).  Although the functions parse slightly different syntaxes,
they return `argparse.Namespace` objects with identical fields, described
below.

The following fields represent parameter inputs to the underlying
deployment:
    `web`:  Server name (or URL) for the AFE RPC service.
    `logdir`:  The directory where logs are to be stored.
    `board`:  Specifies the board to be used when creating DUTs.
    `build`:  When provided, the repair image assigned to the board for
        the target DUT will be updated to this value prior to staging
        USB image.  The build is in a form like 'R66-10447.0.0'.
    `hostname_file`:  Name of a file in CSV format with information
        about the hosts and servos to be deployed/repaired.
    `hostnames`:  List of DUT host names.

The following fields specify options that are used to enable or disable
specific deployment steps:
    `upload`:  When true, logs will be uploaded to googlestorage after
        the command completes.
    `dry_run`:  When true, disables operations with any kind of
        side-effect.  This option implicitly overrides and disables all
        of the deployment steps below.
    `stageusb`:  When true, enable staging the USB image.  Disabling
        this will speed up operations when the stick is known to already
        have the proper image.
    `install_firmware`:  When true, enable firmware installation.
    `install_test_image`:  When true, enable installing the test image.

The `dry_run` option is off by default.  The `upload` option is on by
default, except for `deploy repair` and `repair_test`.  The values for
all other options are determined by the subcommand.
"""

import argparse
import os


class _ArgumentParser(argparse.ArgumentParser):
    """`argparse.ArgumentParser` extended with boolean option pairs."""

    def add_boolean_argument(self, name, default, **kwargs):
        """Add a pair of argument flags for a boolean option.

        This add a pair of options, named `--<name>` and `--no<name>`.
        The actions of the two options are 'store_true' and
        'store_false', respectively, with the destination `<name>`.

        If neither option is present on the command line, the default
        value for destination `<name>` is given by `default`.

        The given `kwargs` may be any arguments accepted by
        `ArgumentParser.add_argument()`, except for `action` and `dest`.

        @param name     The name of the boolean argument, used to
                        construct the option names and destination field
                        name.
        @param default  Default setting for the option when not present
                        on the command line.
        """
        exclusion_group = self.add_mutually_exclusive_group()
        exclusion_group.add_argument('--%s' % name, action='store_true',
                                     dest=name, **kwargs)
        exclusion_group.add_argument('--no%s' % name, action='store_false',
                                     dest=name, **kwargs)
        self.set_defaults(**{name: bool(default)})


def _add_common_options(parser):
    # frontend.AFE(server=None) will use the default web server,
    # so default for --web is `None`.
    parser.add_argument('-w', '--web', metavar='SERVER', default=None,
                        help='specify web server')
    parser.add_argument('-d', '--dir', dest='logdir',
                        help='directory for logs')
    parser.add_argument('-n', '--dry-run', action='store_true',
                        help='apply no changes, install nothing')
    parser.add_argument('-i', '--build',
                        help='select stable test build version')
    parser.add_argument('-f', '--hostname_file',
                        help='CSV file that contains a list of hostnames and '
                             'their details to install with.')


def _add_upload_option(parser, default):
    """Add a boolean option for whether to upload logs.

    @param parser   _ArgumentParser instance.
    @param default  Default option value.
    """
    parser.add_boolean_argument('upload', default,
                                help='whether to upload logs to GS bucket')


def _add_subcommand(subcommands, name, upload_default, description):
    """Add a subcommand plus standard arguments to the `deploy` command.

    This creates a new argument parser for a subcommand (as for
    `subcommands.add_parser()`).  The parser is populated with the
    standard arguments required by all `deploy` subcommands.

    @param subcommands      Subcommand object as returned by
                            `ArgumentParser.add_subcommands`
    @param name             Name of the new subcommand.
    @param upload_default   Default setting for the `--upload` option.
    @param description      Description for the subcommand, for help text.
    @returns The argument parser for the new subcommand.
    """
    subparser = subcommands.add_parser(name, description=description)
    _add_common_options(subparser)
    _add_upload_option(subparser, upload_default)
    subparser.add_argument('-b', '--board', metavar='BOARD',
                           help='board for DUTs to be installed')
    subparser.add_argument('-m', '--model', metavar='MODEL',
                           help='model for DUTs to be installed.')
    subparser.add_argument('hostnames', nargs='*', metavar='HOSTNAME',
                           help='host names of DUTs to be installed')
    return subparser


def _add_servo_subcommand(subcommands):
    """Add the `servo` subcommand to `subcommands`.

    @param subcommands  Subcommand object as returned by
                        `ArgumentParser.add_subcommands`
    """
    subparser = _add_subcommand(
        subcommands, 'servo', True,
        'Test servo and install the image on the USB stick')
    subparser.set_defaults(stageusb=True,
                           install_firmware=False,
                           install_test_image=False)


def _add_stageusb_option(parser):
    """Add a boolean option for whether to stage an image to USB.

    @param parser   _ArgumentParser instance.
    """
    parser.add_boolean_argument('stageusb', False,
                                help='Include USB stick setup')


def _add_firmware_subcommand(subcommands):
    """Add the `firmware` subcommand to `subcommands`.

    @param subcommands  Subcommand object as returned by
                        `ArgumentParser.add_subcommands`
    """
    subparser = _add_subcommand(
        subcommands, 'firmware', True,
        'Install firmware and initial test image on DUT')
    _add_stageusb_option(subparser)
    subparser.add_argument(
            '--using-servo', action='store_true',
            help='Flash DUT firmware directly using servo')
    subparser.add_argument(
            '--force-firmware', action='store_true', default=False,
            help='Force firmware installation using chromeos-installfirmware.')
    subparser.set_defaults(install_firmware=True,
                           install_test_image=True)


def _add_test_image_subcommand(subcommands):
    """Add the `test-image` subcommand to `subcommands`.

    @param subcommands  Subcommand object as returned by
                        `ArgumentParser.add_subcommands`
    """
    subparser = _add_subcommand(
        subcommands, 'test-image', True,
        'Install initial test image on DUT from servo')
    _add_stageusb_option(subparser)
    subparser.set_defaults(install_firmware=False,
                           install_test_image=True)


def _add_repair_subcommand(subcommands):
    """Add the `repair` subcommand to `subcommands`.

    @param subcommands  Subcommand object as returned by
                        `ArgumentParser.add_subcommands`
    """
    subparser = _add_subcommand(
        subcommands, 'repair', False,
        'Re-install test image on DUT from servo')
    _add_stageusb_option(subparser)
    subparser.set_defaults(install_firmware=False,
                           install_test_image=True)


def parse_command(argv):
    """Parse arguments for the `deploy` command.

    Create an argument parser for the `deploy` command and its
    subcommands.  Then parse the command line arguments, and return an
    `argparse.Namespace` object with the results.

    @param argv         Standard command line argument vector;
                        argv[0] is assumed to be the command name.
    @return `Namespace` object with standard fields as described in the
            module docstring.
    """
    parser = _ArgumentParser(
            prog=os.path.basename(argv[0]),
            description='DUT deployment and repair operations')
    subcommands = parser.add_subparsers()
    _add_servo_subcommand(subcommands)
    _add_firmware_subcommand(subcommands)
    _add_test_image_subcommand(subcommands)
    _add_repair_subcommand(subcommands)
    return parser.parse_args(argv[1:])