blob: 6484faa301d4b7f33c2726002504bb28982b325b [file] [log] [blame]
# Copyright (c) 2012 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.
"""Module that contains meta-logic related to CLI commands.
This module contains two important definitions used by all commands:
CliCommand: The parent class of all CLI commands.
command_decorator: Decorator that must be used to ensure that the command
shows up in |_commands| and is discoverable.
Commands can be either imported directly or looked up using this module's
ListCommands() function.
import importlib
import logging
import os
import sys
from chromite.lib import commandline
from chromite.lib import constants
from chromite.lib import cros_build_lib
# Paths for finding and importing subcommand modules.
_SUBCOMMAND_MODULE_DIRECTORY = os.path.join(os.path.dirname(__file__), 'cros')
_commands = dict()
def UseProgressBar():
"""Determine whether the progress bar is to be used or not.
We only want the progress bar to display for the brillo commands which operate
at logging level NOTICE. If the user wants to see the noisy output, then they
can execute the command at logging level INFO or DEBUG.
return logging.getLogger().getEffectiveLevel() == logging.NOTICE
def ImportCommand(name):
"""Directly import the specified subcommand.
This method imports the module which must contain the single subcommand. When
the module is loaded, the declared command (those that use command_decorator)
will automatically get added to |_commands|.
name: The subcommand to load.
A reference to the subcommand class.
module_path = os.path.join(_SUBCOMMAND_MODULE_DIRECTORY,
'cros_%s' % (name.replace('-', '_'),))
import_path = os.path.relpath(os.path.realpath(module_path),
module_parts = import_path.split(os.path.sep)
return _commands[name]
def ListCommands():
"""Return the set of available subcommands.
We assume that there is a direct one-to-one relationship between the module
name on disk and the command that module implements. We assume this as a
performance requirement (to avoid importing every subcommand every time even
though we'd only ever run a single one), and to avoid 3rd party module usage
in one subcommand breaking all other subcommands (not a great solution).
# Filenames use underscores due to python naming limitations, but subcommands
# use dashes as they're easier for humans to type.
# Strip off the leading "cros_" and the trailing ".py".
return set(x[5:-3].replace('_', '-')
if (x.startswith(_SUBCOMMAND_MODULE_PREFIX) and x.endswith('.py')
and not x.endswith('')))
class InvalidCommandError(Exception):
"""Error that occurs when command class fails validity checks."""
def command_decorator(name):
"""Decorator to check validity and add class to list of usable commands."""
def inner_decorator(original_class):
"""Inner Decorator that actually wraps the class."""
if not hasattr(original_class, '__doc__'):
raise InvalidCommandError('All handlers must have docstrings: %s' %
if not issubclass(original_class, CliCommand):
raise InvalidCommandError('All Commands must derive from CliCommand: %s' %
_commands[name] = original_class = name
return original_class
return inner_decorator
class CliCommand(object):
"""All CLI commands must derive from this class.
This class provides the abstract interface for all CLI commands. When
designing a new command, you must sub-class from this class and use the
command_decorator decorator. You must specify a class docstring as that will
be used as the usage for the sub-command.
In addition your command should implement AddParser which is passed in a
parser that you can add your own custom arguments. See argparse for more
# Indicates whether command uses cache related commandline options.
use_caching_options = False
def __init__(self, options):
self.options = options
def AddParser(cls, parser):
"""Add arguments for this command to the parser."""
def ProcessOptions(cls,
parser: commandline.ArgumentParser,
options: commandline.ArgumentNamespace) -> None:
"""Validate & post-process options before freezing."""
def AddDeviceArgument(cls, parser, schemes=commandline.DEVICE_SCHEME_SSH,
"""Add a device argument to the parser.
This standardizes the help message across all subcommands.
parser: The parser to add the device argument to.
schemes: List of device schemes or single scheme to allow.
positional: Whether it should be a positional or named argument.
help_strings = []
schemes = list(cros_build_lib.iflatten_instance(schemes))
if commandline.DEVICE_SCHEME_SSH in schemes:
help_strings.append('Target a device with [user@]hostname[:port]. '
'IPv4/IPv6 addresses are allowed, but IPv6 must '
'use brackets (e.g. [::1]).')
if commandline.DEVICE_SCHEME_USB in schemes:
help_strings.append('Target removable media with usb://[path].')
if commandline.DEVICE_SCHEME_SERVO in schemes:
help_strings.append('Target a servo by port or serial number with '
'servo:port[:port] or servo:serial:serial-number. '
'e.g. servo:port:1234 or servo:serial:C1230024192.')
if commandline.DEVICE_SCHEME_FILE in schemes:
help_strings.append('Target a local file with file://path.')
if positional:
help=' '.join(help_strings))
parser.add_argument('-d', '--device',
help=' '.join(help_strings))
def Run(self):
"""The command to run."""
raise NotImplementedError()
def TranslateToChrootArgv(self):
"""Hook to get the argv for reexecution inside the chroot.
By default, return the same args used to execute it in the first place.
Hook allows commands to translate specific arguments, i.e. change paths to
chroot paths.
return sys.argv[:]