172 lines
6.4 KiB
Python
172 lines
6.4 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
||
|
|
# 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.
|
||
|
|
CommandDecorator: 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.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import print_function
|
||
|
|
|
||
|
|
import importlib
|
||
|
|
import os
|
||
|
|
|
||
|
|
from autotest_lib.utils.frozen_chromite.lib import constants
|
||
|
|
from autotest_lib.utils.frozen_chromite.lib import commandline
|
||
|
|
from autotest_lib.utils.frozen_chromite.lib import cros_build_lib
|
||
|
|
from autotest_lib.utils.frozen_chromite.lib import cros_logging as logging
|
||
|
|
|
||
|
|
|
||
|
|
# Paths for finding and importing subcommand modules.
|
||
|
|
_SUBCOMMAND_MODULE_DIRECTORY = os.path.join(os.path.dirname(__file__), 'cros')
|
||
|
|
_SUBCOMMAND_MODULE_PREFIX = '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 CommandDecorator)
|
||
|
|
will automatically get added to |_commands|.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
name: The subcommand to load.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
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),
|
||
|
|
os.path.dirname(constants.CHROMITE_DIR))
|
||
|
|
module_parts = import_path.split(os.path.sep)
|
||
|
|
importlib.import_module('.'.join(module_parts))
|
||
|
|
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('_', '-')
|
||
|
|
for x in os.listdir(_SUBCOMMAND_MODULE_DIRECTORY)
|
||
|
|
if (x.startswith(_SUBCOMMAND_MODULE_PREFIX) and x.endswith('.py')
|
||
|
|
and not x.endswith('_unittest.py')))
|
||
|
|
|
||
|
|
|
||
|
|
class InvalidCommandError(Exception):
|
||
|
|
"""Error that occurs when command class fails sanity checks."""
|
||
|
|
|
||
|
|
|
||
|
|
def CommandDecorator(command_name):
|
||
|
|
"""Decorator that sanity checks and adds class to list of usable commands."""
|
||
|
|
|
||
|
|
def InnerCommandDecorator(original_class):
|
||
|
|
"""Inner Decorator that actually wraps the class."""
|
||
|
|
if not hasattr(original_class, '__doc__'):
|
||
|
|
raise InvalidCommandError('All handlers must have docstrings: %s' %
|
||
|
|
original_class)
|
||
|
|
|
||
|
|
if not issubclass(original_class, CliCommand):
|
||
|
|
raise InvalidCommandError('All Commands must derive from CliCommand: %s' %
|
||
|
|
original_class)
|
||
|
|
|
||
|
|
_commands[command_name] = original_class
|
||
|
|
original_class.command_name = command_name
|
||
|
|
|
||
|
|
return original_class
|
||
|
|
|
||
|
|
return InnerCommandDecorator
|
||
|
|
|
||
|
|
|
||
|
|
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
|
||
|
|
CommandDecorator 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
|
||
|
|
information.
|
||
|
|
"""
|
||
|
|
# Indicates whether command uses cache related commandline options.
|
||
|
|
use_caching_options = False
|
||
|
|
|
||
|
|
def __init__(self, options):
|
||
|
|
self.options = options
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def AddParser(cls, parser):
|
||
|
|
"""Add arguments for this command to the parser."""
|
||
|
|
parser.set_defaults(command_class=cls)
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def AddDeviceArgument(cls, parser, schemes=commandline.DEVICE_SCHEME_SSH,
|
||
|
|
positional=False):
|
||
|
|
"""Add a device argument to the parser.
|
||
|
|
|
||
|
|
This standardizes the help message across all subcommands.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
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:
|
||
|
|
parser.add_argument('device',
|
||
|
|
type=commandline.DeviceParser(schemes),
|
||
|
|
help=' '.join(help_strings))
|
||
|
|
else:
|
||
|
|
parser.add_argument('-d', '--device',
|
||
|
|
type=commandline.DeviceParser(schemes),
|
||
|
|
help=' '.join(help_strings))
|
||
|
|
|
||
|
|
def Run(self):
|
||
|
|
"""The command to run."""
|
||
|
|
raise NotImplementedError()
|