blob: 75e455bfc886278fe9b2d66b411121e7286dfbe1 [file] [log] [blame]
Brian Harring984988f2012-10-10 22:53:30 -07001# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
David Pursellffb90042015-03-23 09:21:41 -07005"""Module that contains meta-logic related to CLI commands.
6
David Pursellf1c27c12015-03-18 09:51:38 -07007This module contains two important definitions used by all commands:
David Pursellffb90042015-03-23 09:21:41 -07008 CliCommand: The parent class of all CLI commands.
Brian Harring984988f2012-10-10 22:53:30 -07009 CommandDecorator: Decorator that must be used to ensure that the command shows
David Pursellffb90042015-03-23 09:21:41 -070010 up in |_commands| and is discoverable.
David Pursellf1c27c12015-03-18 09:51:38 -070011
12Commands can be either imported directly or looked up using this module's
13ListCommands() function.
Brian Harring984988f2012-10-10 22:53:30 -070014"""
15
Alex Klein665aa072019-12-12 15:13:53 -070016import importlib
Chris McDonald14ac61d2021-07-21 11:49:56 -060017import logging
David Pursellf1c27c12015-03-18 09:51:38 -070018import os
19
David Pursellf80b8c52015-04-03 12:46:45 -070020from chromite.lib import commandline
Chris McDonald14ac61d2021-07-21 11:49:56 -060021from chromite.lib import constants
David Pursellf80b8c52015-04-03 12:46:45 -070022from chromite.lib import cros_build_lib
David Pursellf1c27c12015-03-18 09:51:38 -070023
Brian Harring984988f2012-10-10 22:53:30 -070024
David Pursellc7ba7842015-07-08 10:48:41 -070025# Paths for finding and importing subcommand modules.
26_SUBCOMMAND_MODULE_DIRECTORY = os.path.join(os.path.dirname(__file__), 'cros')
27_SUBCOMMAND_MODULE_PREFIX = 'cros_'
28
29
Brian Harring984988f2012-10-10 22:53:30 -070030_commands = dict()
31
32
Ralph Nathan69931252015-04-14 16:49:21 -070033def UseProgressBar():
34 """Determine whether the progress bar is to be used or not.
35
36 We only want the progress bar to display for the brillo commands which operate
37 at logging level NOTICE. If the user wants to see the noisy output, then they
38 can execute the command at logging level INFO or DEBUG.
39 """
40 return logging.getLogger().getEffectiveLevel() == logging.NOTICE
41
42
Mike Frysinger1a470812019-11-07 01:19:17 -050043def ImportCommand(name):
44 """Directly import the specified subcommand.
45
46 This method imports the module which must contain the single subcommand. When
47 the module is loaded, the declared command (those that use CommandDecorator)
48 will automatically get added to |_commands|.
David Pursellffb90042015-03-23 09:21:41 -070049
50 Args:
Mike Frysinger1a470812019-11-07 01:19:17 -050051 name: The subcommand to load.
David Pursellffb90042015-03-23 09:21:41 -070052
53 Returns:
Mike Frysinger1a470812019-11-07 01:19:17 -050054 A reference to the subcommand class.
David Pursellffb90042015-03-23 09:21:41 -070055 """
Mike Frysinger1a470812019-11-07 01:19:17 -050056 module_path = os.path.join(_SUBCOMMAND_MODULE_DIRECTORY,
57 'cros_%s' % (name.replace('-', '_'),))
58 import_path = os.path.relpath(os.path.realpath(module_path),
59 os.path.dirname(constants.CHROMITE_DIR))
Alex Klein665aa072019-12-12 15:13:53 -070060 module_parts = import_path.split(os.path.sep)
61 importlib.import_module('.'.join(module_parts))
Mike Frysinger1a470812019-11-07 01:19:17 -050062 return _commands[name]
David Pursellf1c27c12015-03-18 09:51:38 -070063
64
David Pursellc7ba7842015-07-08 10:48:41 -070065def ListCommands():
Mike Frysinger1a470812019-11-07 01:19:17 -050066 """Return the set of available subcommands.
David Pursellffb90042015-03-23 09:21:41 -070067
Mike Frysinger1a470812019-11-07 01:19:17 -050068 We assume that there is a direct one-to-one relationship between the module
69 name on disk and the command that module implements. We assume this as a
70 performance requirement (to avoid importing every subcommand every time even
71 though we'd only ever run a single one), and to avoid 3rd party module usage
72 in one subcommand breaking all other subcommands (not a great solution).
David Pursellffb90042015-03-23 09:21:41 -070073 """
Mike Frysinger1a470812019-11-07 01:19:17 -050074 # Filenames use underscores due to python naming limitations, but subcommands
75 # use dashes as they're easier for humans to type.
76 # Strip off the leading "cros_" and the trailing ".py".
77 return set(x[5:-3].replace('_', '-')
78 for x in os.listdir(_SUBCOMMAND_MODULE_DIRECTORY)
79 if (x.startswith(_SUBCOMMAND_MODULE_PREFIX) and x.endswith('.py')
80 and not x.endswith('_unittest.py')))
David Pursellf1c27c12015-03-18 09:51:38 -070081
82
Brian Harring984988f2012-10-10 22:53:30 -070083class InvalidCommandError(Exception):
84 """Error that occurs when command class fails sanity checks."""
Brian Harring984988f2012-10-10 22:53:30 -070085
86
87def CommandDecorator(command_name):
88 """Decorator that sanity checks and adds class to list of usable commands."""
89
90 def InnerCommandDecorator(original_class):
Mike Frysinger07475fa2019-08-01 14:44:55 -040091 """Inner Decorator that actually wraps the class."""
Brian Harring984988f2012-10-10 22:53:30 -070092 if not hasattr(original_class, '__doc__'):
93 raise InvalidCommandError('All handlers must have docstrings: %s' %
94 original_class)
95
David Pursellffb90042015-03-23 09:21:41 -070096 if not issubclass(original_class, CliCommand):
97 raise InvalidCommandError('All Commands must derive from CliCommand: %s' %
98 original_class)
Brian Harring984988f2012-10-10 22:53:30 -070099
100 _commands[command_name] = original_class
Ryan Cui47f80e42013-04-01 19:01:54 -0700101 original_class.command_name = command_name
102
Brian Harring984988f2012-10-10 22:53:30 -0700103 return original_class
104
105 return InnerCommandDecorator
106
107
David Pursellffb90042015-03-23 09:21:41 -0700108class CliCommand(object):
109 """All CLI commands must derive from this class.
Brian Harring984988f2012-10-10 22:53:30 -0700110
David Pursellffb90042015-03-23 09:21:41 -0700111 This class provides the abstract interface for all CLI commands. When
Brian Harring984988f2012-10-10 22:53:30 -0700112 designing a new command, you must sub-class from this class and use the
113 CommandDecorator decorator. You must specify a class docstring as that will be
114 used as the usage for the sub-command.
115
116 In addition your command should implement AddParser which is passed in a
117 parser that you can add your own custom arguments. See argparse for more
118 information.
119 """
Ryo Hashimoto8bc997b2014-01-22 18:46:17 +0900120 # Indicates whether command uses cache related commandline options.
121 use_caching_options = False
122
Brian Harring984988f2012-10-10 22:53:30 -0700123 def __init__(self, options):
124 self.options = options
125
126 @classmethod
127 def AddParser(cls, parser):
128 """Add arguments for this command to the parser."""
David Pursellffb90042015-03-23 09:21:41 -0700129 parser.set_defaults(command_class=cls)
Brian Harring984988f2012-10-10 22:53:30 -0700130
David Pursellf80b8c52015-04-03 12:46:45 -0700131 @classmethod
Alex Kleindf030342020-02-14 10:07:38 -0700132 def AddDeviceArgument(cls, parser, schemes=commandline.DEVICE_SCHEME_SSH,
133 positional=False):
David Pursellf80b8c52015-04-03 12:46:45 -0700134 """Add a device argument to the parser.
135
David Pursellc7ba7842015-07-08 10:48:41 -0700136 This standardizes the help message across all subcommands.
David Pursellf80b8c52015-04-03 12:46:45 -0700137
138 Args:
139 parser: The parser to add the device argument to.
140 schemes: List of device schemes or single scheme to allow.
Alex Kleindf030342020-02-14 10:07:38 -0700141 positional: Whether it should be a positional or named argument.
David Pursellf80b8c52015-04-03 12:46:45 -0700142 """
David Pursellf80b8c52015-04-03 12:46:45 -0700143 help_strings = []
144 schemes = list(cros_build_lib.iflatten_instance(schemes))
145 if commandline.DEVICE_SCHEME_SSH in schemes:
Mike Frysinger7163c3a2018-02-08 16:45:10 -0500146 help_strings.append('Target a device with [user@]hostname[:port]. '
147 'IPv4/IPv6 addresses are allowed, but IPv6 must '
148 'use brackets (e.g. [::1]).')
David Pursellf80b8c52015-04-03 12:46:45 -0700149 if commandline.DEVICE_SCHEME_USB in schemes:
150 help_strings.append('Target removable media with usb://[path].')
Alex Kleine83d86c2019-12-11 15:17:31 -0700151 if commandline.DEVICE_SCHEME_SERVO in schemes:
152 help_strings.append('Target a servo by port or serial number with '
153 'servo:port[:port] or servo:serial:serial-number. '
154 'e.g. servo:port:1234 or servo:serial:C1230024192.')
David Pursellf80b8c52015-04-03 12:46:45 -0700155 if commandline.DEVICE_SCHEME_FILE in schemes:
156 help_strings.append('Target a local file with file://path.')
Alex Kleindf030342020-02-14 10:07:38 -0700157 if positional:
158 parser.add_argument('device',
159 type=commandline.DeviceParser(schemes),
160 help=' '.join(help_strings))
161 else:
162 parser.add_argument('-d', '--device',
163 type=commandline.DeviceParser(schemes),
164 help=' '.join(help_strings))
David Pursellf80b8c52015-04-03 12:46:45 -0700165
Brian Harring984988f2012-10-10 22:53:30 -0700166 def Run(self):
167 """The command to run."""
168 raise NotImplementedError()