blob: ae5b55fe6e76aec3e3a383a0a2be1a657b385554 [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
Trent Apted1e2e4f32023-05-05 03:50:20 +000031_commands = {}
Brian Harring984988f2012-10-10 22:53:30 -070032
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(
Mike Frysingera69df982023-03-21 16:52:27 -040061 os.path.realpath(module_path), constants.CHROMITE_DIR.parent
Alex Klein1699fab2022-09-08 08:46:06 -060062 )
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
Li-Yu Yuafe7ea92022-12-21 13:17:35 +0800136 # Indicates whether command uses filter related commandline options.
137 use_filter_options = False
138
Alex Klein1699fab2022-09-08 08:46:06 -0600139 def __init__(self, options):
140 self.options = options
Alex Klein3a883272021-03-19 16:02:43 -0600141
Alex Klein1699fab2022-09-08 08:46:06 -0600142 @classmethod
143 def AddParser(cls, parser):
144 """Add arguments for this command to the parser."""
145 parser.set_defaults(command_class=cls)
146
147 @classmethod
148 def ProcessOptions(
149 cls,
150 parser: commandline.ArgumentParser,
151 options: commandline.ArgumentNamespace,
152 ) -> None:
153 """Validate & post-process options before freezing."""
154
155 @classmethod
156 def AddDeviceArgument(
157 cls, parser, schemes=commandline.DEVICE_SCHEME_SSH, positional=False
158 ):
159 """Add a device argument to the parser.
160
161 This standardizes the help message across all subcommands.
162
163 Args:
Alex Klein53cc3bf2022-10-13 08:50:01 -0600164 parser: The parser to add the device argument to.
165 schemes: List of device schemes or single scheme to allow.
166 positional: Whether it should be a positional or named argument.
Alex Klein1699fab2022-09-08 08:46:06 -0600167 """
168 help_strings = []
169 schemes = list(cros_build_lib.iflatten_instance(schemes))
170 if commandline.DEVICE_SCHEME_SSH in schemes:
171 help_strings.append(
172 "Target a device with [user@]hostname[:port]. "
173 "IPv4/IPv6 addresses are allowed, but IPv6 must "
174 "use brackets (e.g. [::1])."
175 )
176 if commandline.DEVICE_SCHEME_USB in schemes:
177 help_strings.append("Target removable media with usb://[path].")
178 if commandline.DEVICE_SCHEME_SERVO in schemes:
179 help_strings.append(
180 "Target a servo by port or serial number with "
181 "servo:port[:port] or servo:serial:serial-number. "
182 "e.g. servo:port:1234 or servo:serial:C1230024192."
183 )
184 if commandline.DEVICE_SCHEME_FILE in schemes:
185 help_strings.append("Target a local file with file://path.")
186 if positional:
187 parser.add_argument(
188 "device",
189 type=commandline.DeviceParser(schemes),
190 help=" ".join(help_strings),
191 )
192 else:
193 parser.add_argument(
194 "-d",
195 "--device",
196 type=commandline.DeviceParser(schemes),
197 help=" ".join(help_strings),
198 )
199
200 def Run(self):
201 """The command to run."""
202 raise NotImplementedError()
203
204 def TranslateToChrootArgv(self):
205 """Hook to get the argv for reexecution inside the chroot.
206
207 By default, return the same args used to execute it in the first place.
Alex Klein975e86c2023-01-23 16:49:10 -0700208 Hook allows commands to translate specific arguments, i.e. change paths
209 to chroot paths.
Alex Klein1699fab2022-09-08 08:46:06 -0600210 """
211 return sys.argv[:]