blob: cd425eff23b17f6e5515113938b1fe5330d7b592 [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:
Alex Klein975e86c2023-01-23 16:49:10 -07008 CliCommand: The parent class of all CLI commands.
9 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 Klein975e86c2023-01-23 16:49:10 -070037 We only want the progress bar to display for the brillo commands which
38 operate at logging level NOTICE. If the user wants to see the noisy output,
39 then they can execute the command at logging level INFO or DEBUG.
Alex Klein1699fab2022-09-08 08:46:06 -060040 """
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 Klein975e86c2023-01-23 16:49:10 -070047 This method imports the module which must contain the single subcommand.
48 When the module is loaded, the declared command (those that use
49 command_decorator) will automatically get added to |_commands|.
David Pursellffb90042015-03-23 09:21:41 -070050
Alex Klein1699fab2022-09-08 08:46:06 -060051 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -060052 name: The subcommand to load.
David Pursellffb90042015-03-23 09:21:41 -070053
Alex Klein1699fab2022-09-08 08:46:06 -060054 Returns:
Alex Klein53cc3bf2022-10-13 08:50:01 -060055 A reference to the subcommand class.
Alex Klein1699fab2022-09-08 08:46:06 -060056 """
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 """
Alex Klein975e86c2023-01-23 16:49:10 -070077 # Filenames use underscores due to python naming limitations, but
78 # subcommands use dashes as they're easier for humans to type.
Alex Klein1699fab2022-09-08 08:46:06 -060079 # 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
Mike Frysinger57a8feb2023-02-02 08:48:38 -0500133 # Whether command uses dry-run options.
134 use_dryrun_options = False
Alex Klein3a883272021-03-19 16:02:43 -0600135
Alex Klein1699fab2022-09-08 08:46:06 -0600136 def __init__(self, options):
137 self.options = options
Alex Klein3a883272021-03-19 16:02:43 -0600138
Alex Klein1699fab2022-09-08 08:46:06 -0600139 @classmethod
140 def AddParser(cls, parser):
141 """Add arguments for this command to the parser."""
142 parser.set_defaults(command_class=cls)
143
144 @classmethod
145 def ProcessOptions(
146 cls,
147 parser: commandline.ArgumentParser,
148 options: commandline.ArgumentNamespace,
149 ) -> None:
150 """Validate & post-process options before freezing."""
151
152 @classmethod
153 def AddDeviceArgument(
154 cls, parser, schemes=commandline.DEVICE_SCHEME_SSH, positional=False
155 ):
156 """Add a device argument to the parser.
157
158 This standardizes the help message across all subcommands.
159
160 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600161 parser: The parser to add the device argument to.
162 schemes: List of device schemes or single scheme to allow.
163 positional: Whether it should be a positional or named argument.
Alex Klein1699fab2022-09-08 08:46:06 -0600164 """
165 help_strings = []
166 schemes = list(cros_build_lib.iflatten_instance(schemes))
167 if commandline.DEVICE_SCHEME_SSH in schemes:
168 help_strings.append(
169 "Target a device with [user@]hostname[:port]. "
170 "IPv4/IPv6 addresses are allowed, but IPv6 must "
171 "use brackets (e.g. [::1])."
172 )
173 if commandline.DEVICE_SCHEME_USB in schemes:
174 help_strings.append("Target removable media with usb://[path].")
175 if commandline.DEVICE_SCHEME_SERVO in schemes:
176 help_strings.append(
177 "Target a servo by port or serial number with "
178 "servo:port[:port] or servo:serial:serial-number. "
179 "e.g. servo:port:1234 or servo:serial:C1230024192."
180 )
181 if commandline.DEVICE_SCHEME_FILE in schemes:
182 help_strings.append("Target a local file with file://path.")
183 if positional:
184 parser.add_argument(
185 "device",
186 type=commandline.DeviceParser(schemes),
187 help=" ".join(help_strings),
188 )
189 else:
190 parser.add_argument(
191 "-d",
192 "--device",
193 type=commandline.DeviceParser(schemes),
194 help=" ".join(help_strings),
195 )
196
197 def Run(self):
198 """The command to run."""
199 raise NotImplementedError()
200
201 def TranslateToChrootArgv(self):
202 """Hook to get the argv for reexecution inside the chroot.
203
204 By default, return the same args used to execute it in the first place.
Alex Klein975e86c2023-01-23 16:49:10 -0700205 Hook allows commands to translate specific arguments, i.e. change paths
206 to chroot paths.
Alex Klein1699fab2022-09-08 08:46:06 -0600207 """
208 return sys.argv[:]