blob: 79eefda12d2b434d0451504279eaf9100ed5de8a [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2012 The ChromiumOS Authors
Brian Harring984988f2012-10-10 22:53:30 -07002# 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.
Mike Frysinger61cf22d2021-12-15 00:37:54 -05009 command_decorator: Decorator that must be used to ensure that the command
10 shows 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
Alex Klein3a883272021-03-19 16:02:43 -060019import sys
David Pursellf1c27c12015-03-18 09:51:38 -070020
David Pursellf80b8c52015-04-03 12:46:45 -070021from chromite.lib import commandline
Chris McDonald14ac61d2021-07-21 11:49:56 -060022from chromite.lib import constants
David Pursellf80b8c52015-04-03 12:46:45 -070023from chromite.lib import cros_build_lib
David Pursellf1c27c12015-03-18 09:51:38 -070024
Brian Harring984988f2012-10-10 22:53:30 -070025
David Pursellc7ba7842015-07-08 10:48:41 -070026# Paths for finding and importing subcommand modules.
Alex Klein1699fab2022-09-08 08:46:06 -060027_SUBCOMMAND_MODULE_DIRECTORY = os.path.join(os.path.dirname(__file__), "cros")
28_SUBCOMMAND_MODULE_PREFIX = "cros_"
David Pursellc7ba7842015-07-08 10:48:41 -070029
30
Brian Harring984988f2012-10-10 22:53:30 -070031_commands = dict()
32
33
Ralph Nathan69931252015-04-14 16:49:21 -070034def UseProgressBar():
Alex Klein1699fab2022-09-08 08:46:06 -060035 """Determine whether the progress bar is to be used or not.
Ralph Nathan69931252015-04-14 16:49:21 -070036
Alex Klein1699fab2022-09-08 08:46:06 -060037 We only want the progress bar to display for the brillo commands which operate
38 at logging level NOTICE. If the user wants to see the noisy output, then they
39 can execute the command at logging level INFO or DEBUG.
40 """
41 return logging.getLogger().getEffectiveLevel() == logging.NOTICE
Ralph Nathan69931252015-04-14 16:49:21 -070042
43
Mike Frysinger1a470812019-11-07 01:19:17 -050044def ImportCommand(name):
Alex Klein1699fab2022-09-08 08:46:06 -060045 """Directly import the specified subcommand.
Mike Frysinger1a470812019-11-07 01:19:17 -050046
Alex Klein1699fab2022-09-08 08:46:06 -060047 This method imports the module which must contain the single subcommand. When
48 the module is loaded, the declared command (those that use command_decorator)
49 will automatically get added to |_commands|.
David Pursellffb90042015-03-23 09:21:41 -070050
Alex Klein1699fab2022-09-08 08:46:06 -060051 Args:
52 name: The subcommand to load.
David Pursellffb90042015-03-23 09:21:41 -070053
Alex Klein1699fab2022-09-08 08:46:06 -060054 Returns:
55 A reference to the subcommand class.
56 """
57 module_path = os.path.join(
58 _SUBCOMMAND_MODULE_DIRECTORY, "cros_%s" % (name.replace("-", "_"),)
59 )
60 import_path = os.path.relpath(
61 os.path.realpath(module_path), os.path.dirname(constants.CHROMITE_DIR)
62 )
63 module_parts = import_path.split(os.path.sep)
64 importlib.import_module(".".join(module_parts))
65 return _commands[name]
David Pursellf1c27c12015-03-18 09:51:38 -070066
67
David Pursellc7ba7842015-07-08 10:48:41 -070068def ListCommands():
Alex Klein1699fab2022-09-08 08:46:06 -060069 """Return the set of available subcommands.
David Pursellffb90042015-03-23 09:21:41 -070070
Alex Klein1699fab2022-09-08 08:46:06 -060071 We assume that there is a direct one-to-one relationship between the module
72 name on disk and the command that module implements. We assume this as a
73 performance requirement (to avoid importing every subcommand every time even
74 though we'd only ever run a single one), and to avoid 3rd party module usage
75 in one subcommand breaking all other subcommands (not a great solution).
76 """
77 # Filenames use underscores due to python naming limitations, but subcommands
78 # use dashes as they're easier for humans to type.
79 # Strip off the leading "cros_" and the trailing ".py".
80 return set(
81 x[5:-3].replace("_", "-")
82 for x in os.listdir(_SUBCOMMAND_MODULE_DIRECTORY)
83 if (
84 x.startswith(_SUBCOMMAND_MODULE_PREFIX)
85 and x.endswith(".py")
86 and not x.endswith("_unittest.py")
87 )
88 )
David Pursellf1c27c12015-03-18 09:51:38 -070089
90
Brian Harring984988f2012-10-10 22:53:30 -070091class InvalidCommandError(Exception):
Alex Klein1699fab2022-09-08 08:46:06 -060092 """Error that occurs when command class fails validity checks."""
Brian Harring984988f2012-10-10 22:53:30 -070093
94
Mike Frysinger61cf22d2021-12-15 00:37:54 -050095def command_decorator(name):
Alex Klein1699fab2022-09-08 08:46:06 -060096 """Decorator to check validity and add class to list of usable commands."""
Brian Harring984988f2012-10-10 22:53:30 -070097
Alex Klein1699fab2022-09-08 08:46:06 -060098 def inner_decorator(original_class):
99 """Inner Decorator that actually wraps the class."""
100 if not hasattr(original_class, "__doc__"):
101 raise InvalidCommandError(
102 "All handlers must have docstrings: %s" % original_class
103 )
Brian Harring984988f2012-10-10 22:53:30 -0700104
Alex Klein1699fab2022-09-08 08:46:06 -0600105 if not issubclass(original_class, CliCommand):
106 raise InvalidCommandError(
107 "All Commands must derive from CliCommand: %s" % original_class
108 )
Brian Harring984988f2012-10-10 22:53:30 -0700109
Alex Klein1699fab2022-09-08 08:46:06 -0600110 _commands[name] = original_class
111 original_class.name = name
Ryan Cui47f80e42013-04-01 19:01:54 -0700112
Alex Klein1699fab2022-09-08 08:46:06 -0600113 return original_class
Brian Harring984988f2012-10-10 22:53:30 -0700114
Alex Klein1699fab2022-09-08 08:46:06 -0600115 return inner_decorator
Brian Harring984988f2012-10-10 22:53:30 -0700116
117
David Pursellffb90042015-03-23 09:21:41 -0700118class CliCommand(object):
Alex Klein1699fab2022-09-08 08:46:06 -0600119 """All CLI commands must derive from this class.
Brian Harring984988f2012-10-10 22:53:30 -0700120
Alex Klein1699fab2022-09-08 08:46:06 -0600121 This class provides the abstract interface for all CLI commands. When
122 designing a new command, you must sub-class from this class and use the
123 command_decorator decorator. You must specify a class docstring as that will
124 be used as the usage for the sub-command.
Brian Harring984988f2012-10-10 22:53:30 -0700125
Alex Klein1699fab2022-09-08 08:46:06 -0600126 In addition your command should implement AddParser which is passed in a
127 parser that you can add your own custom arguments. See argparse for more
128 information.
David Pursellf80b8c52015-04-03 12:46:45 -0700129 """
David Pursellf80b8c52015-04-03 12:46:45 -0700130
Alex Klein1699fab2022-09-08 08:46:06 -0600131 # Indicates whether command uses cache related commandline options.
132 use_caching_options = False
Alex Klein3a883272021-03-19 16:02:43 -0600133
Alex Klein1699fab2022-09-08 08:46:06 -0600134 def __init__(self, options):
135 self.options = options
Alex Klein3a883272021-03-19 16:02:43 -0600136
Alex Klein1699fab2022-09-08 08:46:06 -0600137 @classmethod
138 def AddParser(cls, parser):
139 """Add arguments for this command to the parser."""
140 parser.set_defaults(command_class=cls)
141
142 @classmethod
143 def ProcessOptions(
144 cls,
145 parser: commandline.ArgumentParser,
146 options: commandline.ArgumentNamespace,
147 ) -> None:
148 """Validate & post-process options before freezing."""
149
150 @classmethod
151 def AddDeviceArgument(
152 cls, parser, schemes=commandline.DEVICE_SCHEME_SSH, positional=False
153 ):
154 """Add a device argument to the parser.
155
156 This standardizes the help message across all subcommands.
157
158 Args:
159 parser: The parser to add the device argument to.
160 schemes: List of device schemes or single scheme to allow.
161 positional: Whether it should be a positional or named argument.
162 """
163 help_strings = []
164 schemes = list(cros_build_lib.iflatten_instance(schemes))
165 if commandline.DEVICE_SCHEME_SSH in schemes:
166 help_strings.append(
167 "Target a device with [user@]hostname[:port]. "
168 "IPv4/IPv6 addresses are allowed, but IPv6 must "
169 "use brackets (e.g. [::1])."
170 )
171 if commandline.DEVICE_SCHEME_USB in schemes:
172 help_strings.append("Target removable media with usb://[path].")
173 if commandline.DEVICE_SCHEME_SERVO in schemes:
174 help_strings.append(
175 "Target a servo by port or serial number with "
176 "servo:port[:port] or servo:serial:serial-number. "
177 "e.g. servo:port:1234 or servo:serial:C1230024192."
178 )
179 if commandline.DEVICE_SCHEME_FILE in schemes:
180 help_strings.append("Target a local file with file://path.")
181 if positional:
182 parser.add_argument(
183 "device",
184 type=commandline.DeviceParser(schemes),
185 help=" ".join(help_strings),
186 )
187 else:
188 parser.add_argument(
189 "-d",
190 "--device",
191 type=commandline.DeviceParser(schemes),
192 help=" ".join(help_strings),
193 )
194
195 def Run(self):
196 """The command to run."""
197 raise NotImplementedError()
198
199 def TranslateToChrootArgv(self):
200 """Hook to get the argv for reexecution inside the chroot.
201
202 By default, return the same args used to execute it in the first place.
203 Hook allows commands to translate specific arguments, i.e. change paths to
204 chroot paths.
205 """
206 return sys.argv[:]