blob: ad7fd6a503a16bc76246788b5e1a6309095f5779 [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.
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.
27_SUBCOMMAND_MODULE_DIRECTORY = os.path.join(os.path.dirname(__file__), 'cros')
28_SUBCOMMAND_MODULE_PREFIX = 'cros_'
29
30
Brian Harring984988f2012-10-10 22:53:30 -070031_commands = dict()
32
33
Ralph Nathan69931252015-04-14 16:49:21 -070034def UseProgressBar():
35 """Determine whether the progress bar is to be used or not.
36
37 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
42
43
Mike Frysinger1a470812019-11-07 01:19:17 -050044def ImportCommand(name):
45 """Directly import the specified subcommand.
46
47 This method imports the module which must contain the single subcommand. When
Mike Frysinger61cf22d2021-12-15 00:37:54 -050048 the module is loaded, the declared command (those that use command_decorator)
Mike Frysinger1a470812019-11-07 01:19:17 -050049 will automatically get added to |_commands|.
David Pursellffb90042015-03-23 09:21:41 -070050
51 Args:
Mike Frysinger1a470812019-11-07 01:19:17 -050052 name: The subcommand to load.
David Pursellffb90042015-03-23 09:21:41 -070053
54 Returns:
Mike Frysinger1a470812019-11-07 01:19:17 -050055 A reference to the subcommand class.
David Pursellffb90042015-03-23 09:21:41 -070056 """
Mike Frysinger1a470812019-11-07 01:19:17 -050057 module_path = os.path.join(_SUBCOMMAND_MODULE_DIRECTORY,
58 'cros_%s' % (name.replace('-', '_'),))
59 import_path = os.path.relpath(os.path.realpath(module_path),
60 os.path.dirname(constants.CHROMITE_DIR))
Alex Klein665aa072019-12-12 15:13:53 -070061 module_parts = import_path.split(os.path.sep)
62 importlib.import_module('.'.join(module_parts))
Mike Frysinger1a470812019-11-07 01:19:17 -050063 return _commands[name]
David Pursellf1c27c12015-03-18 09:51:38 -070064
65
David Pursellc7ba7842015-07-08 10:48:41 -070066def ListCommands():
Mike Frysinger1a470812019-11-07 01:19:17 -050067 """Return the set of available subcommands.
David Pursellffb90042015-03-23 09:21:41 -070068
Mike Frysinger1a470812019-11-07 01:19:17 -050069 We assume that there is a direct one-to-one relationship between the module
70 name on disk and the command that module implements. We assume this as a
71 performance requirement (to avoid importing every subcommand every time even
72 though we'd only ever run a single one), and to avoid 3rd party module usage
73 in one subcommand breaking all other subcommands (not a great solution).
David Pursellffb90042015-03-23 09:21:41 -070074 """
Mike Frysinger1a470812019-11-07 01:19:17 -050075 # Filenames use underscores due to python naming limitations, but subcommands
76 # use dashes as they're easier for humans to type.
77 # Strip off the leading "cros_" and the trailing ".py".
78 return set(x[5:-3].replace('_', '-')
79 for x in os.listdir(_SUBCOMMAND_MODULE_DIRECTORY)
80 if (x.startswith(_SUBCOMMAND_MODULE_PREFIX) and x.endswith('.py')
81 and not x.endswith('_unittest.py')))
David Pursellf1c27c12015-03-18 09:51:38 -070082
83
Brian Harring984988f2012-10-10 22:53:30 -070084class InvalidCommandError(Exception):
85 """Error that occurs when command class fails sanity checks."""
Brian Harring984988f2012-10-10 22:53:30 -070086
87
Mike Frysinger61cf22d2021-12-15 00:37:54 -050088def command_decorator(name):
Brian Harring984988f2012-10-10 22:53:30 -070089 """Decorator that sanity checks and adds class to list of usable commands."""
90
Mike Frysinger61cf22d2021-12-15 00:37:54 -050091 def inner_decorator(original_class):
Mike Frysinger07475fa2019-08-01 14:44:55 -040092 """Inner Decorator that actually wraps the class."""
Brian Harring984988f2012-10-10 22:53:30 -070093 if not hasattr(original_class, '__doc__'):
94 raise InvalidCommandError('All handlers must have docstrings: %s' %
95 original_class)
96
David Pursellffb90042015-03-23 09:21:41 -070097 if not issubclass(original_class, CliCommand):
98 raise InvalidCommandError('All Commands must derive from CliCommand: %s' %
99 original_class)
Brian Harring984988f2012-10-10 22:53:30 -0700100
Mike Frysinger61cf22d2021-12-15 00:37:54 -0500101 _commands[name] = original_class
102 original_class.name = name
Ryan Cui47f80e42013-04-01 19:01:54 -0700103
Brian Harring984988f2012-10-10 22:53:30 -0700104 return original_class
105
Mike Frysinger61cf22d2021-12-15 00:37:54 -0500106 return inner_decorator
Brian Harring984988f2012-10-10 22:53:30 -0700107
108
David Pursellffb90042015-03-23 09:21:41 -0700109class CliCommand(object):
110 """All CLI commands must derive from this class.
Brian Harring984988f2012-10-10 22:53:30 -0700111
David Pursellffb90042015-03-23 09:21:41 -0700112 This class provides the abstract interface for all CLI commands. When
Brian Harring984988f2012-10-10 22:53:30 -0700113 designing a new command, you must sub-class from this class and use the
Mike Frysinger61cf22d2021-12-15 00:37:54 -0500114 command_decorator decorator. You must specify a class docstring as that will
115 be used as the usage for the sub-command.
Brian Harring984988f2012-10-10 22:53:30 -0700116
117 In addition your command should implement AddParser which is passed in a
118 parser that you can add your own custom arguments. See argparse for more
119 information.
120 """
Ryo Hashimoto8bc997b2014-01-22 18:46:17 +0900121 # Indicates whether command uses cache related commandline options.
122 use_caching_options = False
123
Brian Harring984988f2012-10-10 22:53:30 -0700124 def __init__(self, options):
125 self.options = options
126
127 @classmethod
128 def AddParser(cls, parser):
129 """Add arguments for this command to the parser."""
David Pursellffb90042015-03-23 09:21:41 -0700130 parser.set_defaults(command_class=cls)
Brian Harring984988f2012-10-10 22:53:30 -0700131
David Pursellf80b8c52015-04-03 12:46:45 -0700132 @classmethod
Alex Kleindf030342020-02-14 10:07:38 -0700133 def AddDeviceArgument(cls, parser, schemes=commandline.DEVICE_SCHEME_SSH,
134 positional=False):
David Pursellf80b8c52015-04-03 12:46:45 -0700135 """Add a device argument to the parser.
136
David Pursellc7ba7842015-07-08 10:48:41 -0700137 This standardizes the help message across all subcommands.
David Pursellf80b8c52015-04-03 12:46:45 -0700138
139 Args:
140 parser: The parser to add the device argument to.
141 schemes: List of device schemes or single scheme to allow.
Alex Kleindf030342020-02-14 10:07:38 -0700142 positional: Whether it should be a positional or named argument.
David Pursellf80b8c52015-04-03 12:46:45 -0700143 """
David Pursellf80b8c52015-04-03 12:46:45 -0700144 help_strings = []
145 schemes = list(cros_build_lib.iflatten_instance(schemes))
146 if commandline.DEVICE_SCHEME_SSH in schemes:
Mike Frysinger7163c3a2018-02-08 16:45:10 -0500147 help_strings.append('Target a device with [user@]hostname[:port]. '
148 'IPv4/IPv6 addresses are allowed, but IPv6 must '
149 'use brackets (e.g. [::1]).')
David Pursellf80b8c52015-04-03 12:46:45 -0700150 if commandline.DEVICE_SCHEME_USB in schemes:
151 help_strings.append('Target removable media with usb://[path].')
Alex Kleine83d86c2019-12-11 15:17:31 -0700152 if commandline.DEVICE_SCHEME_SERVO in schemes:
153 help_strings.append('Target a servo by port or serial number with '
154 'servo:port[:port] or servo:serial:serial-number. '
155 'e.g. servo:port:1234 or servo:serial:C1230024192.')
David Pursellf80b8c52015-04-03 12:46:45 -0700156 if commandline.DEVICE_SCHEME_FILE in schemes:
157 help_strings.append('Target a local file with file://path.')
Alex Kleindf030342020-02-14 10:07:38 -0700158 if positional:
159 parser.add_argument('device',
160 type=commandline.DeviceParser(schemes),
161 help=' '.join(help_strings))
162 else:
163 parser.add_argument('-d', '--device',
164 type=commandline.DeviceParser(schemes),
165 help=' '.join(help_strings))
David Pursellf80b8c52015-04-03 12:46:45 -0700166
Brian Harring984988f2012-10-10 22:53:30 -0700167 def Run(self):
168 """The command to run."""
169 raise NotImplementedError()
Alex Klein3a883272021-03-19 16:02:43 -0600170
171 def TranslateToChrootArgv(self):
172 """Hook to get the argv for reexecution inside the chroot.
173
174 By default, return the same args used to execute it in the first place.
175 Hook allows commands to translate specific arguments, i.e. change paths to
176 chroot paths.
177 """
178 return sys.argv[:]