blob: 473377b18c588ff0caa2faa106c6da8016808ad9 [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
7CLI commands are the subcommands available to the user, such as "deploy" in
8`cros deploy` or "shell" in `brillo shell`.
Brian Harring984988f2012-10-10 22:53:30 -07009
David Pursellf1c27c12015-03-18 09:51:38 -070010This module contains two important definitions used by all commands:
David Pursellffb90042015-03-23 09:21:41 -070011 CliCommand: The parent class of all CLI commands.
Brian Harring984988f2012-10-10 22:53:30 -070012 CommandDecorator: Decorator that must be used to ensure that the command shows
David Pursellffb90042015-03-23 09:21:41 -070013 up in |_commands| and is discoverable.
David Pursellf1c27c12015-03-18 09:51:38 -070014
15Commands can be either imported directly or looked up using this module's
16ListCommands() function.
Brian Harring984988f2012-10-10 22:53:30 -070017"""
18
Mike Frysinger383367e2014-09-16 15:06:17 -040019from __future__ import print_function
20
David Pursellf1c27c12015-03-18 09:51:38 -070021import glob
22import os
David Pursellffb90042015-03-23 09:21:41 -070023import sys
David Pursellf1c27c12015-03-18 09:51:38 -070024
Bertrand SIMONNET2b1ed052015-03-02 11:17:40 -080025from chromite.lib import brick_lib
David Pursellf80b8c52015-04-03 12:46:45 -070026from chromite.lib import commandline
27from chromite.lib import cros_build_lib
David Pursellf1c27c12015-03-18 09:51:38 -070028from chromite.lib import cros_import
Ralph Nathan69931252015-04-14 16:49:21 -070029from chromite.lib import cros_logging as logging
David Pursellf1c27c12015-03-18 09:51:38 -070030
Brian Harring984988f2012-10-10 22:53:30 -070031
32_commands = dict()
33
34
Ralph Nathan69931252015-04-14 16:49:21 -070035def UseProgressBar():
36 """Determine whether the progress bar is to be used or not.
37
38 We only want the progress bar to display for the brillo commands which operate
39 at logging level NOTICE. If the user wants to see the noisy output, then they
40 can execute the command at logging level INFO or DEBUG.
41 """
42 return logging.getLogger().getEffectiveLevel() == logging.NOTICE
43
44
David Pursellffb90042015-03-23 09:21:41 -070045def _GetToolset():
46 """Return the CLI toolset invoked by the user.
47
48 For example, if the user is executing `cros flash`, this will return 'cros'.
49
50 This won't work for unittests so if a certain toolset must be loaded for
51 a unittest this should be mocked out to return the desired string.
52 """
53 return os.path.basename(sys.argv[0])
54
55
56def _FindModules(subdir_path, toolset):
57 """Returns a list of all the relevant python modules in |subdir_path|.
58
59 The modules returned are based on |toolset|, so if |toolset| is 'cros'
60 then cros_xxx.py modules will be found.
61
62 Args:
63 subdir_path: directory (string) to search for modules in.
64 toolset: toolset (string) to find.
65
66 Returns:
67 List of filenames (strings).
68 """
David Pursellf1c27c12015-03-18 09:51:38 -070069 modules = []
David Pursellffb90042015-03-23 09:21:41 -070070 for file_path in glob.glob(os.path.join(subdir_path, toolset + '_*.py')):
David Pursellf1c27c12015-03-18 09:51:38 -070071 if not file_path.endswith('_unittest.py'):
72 modules.append(file_path)
David Pursellf1c27c12015-03-18 09:51:38 -070073 return modules
74
75
David Pursellffb90042015-03-23 09:21:41 -070076def _ImportCommands(toolset):
77 """Directly imports all |toolset| python modules.
David Pursellf1c27c12015-03-18 09:51:38 -070078
David Pursellffb90042015-03-23 09:21:41 -070079 This method imports the |toolset|_[!unittest] modules which may contain
David Pursellf1c27c12015-03-18 09:51:38 -070080 commands. When these modules are loaded, declared commands (those that use
David Pursellffb90042015-03-23 09:21:41 -070081 CommandDecorator) will automatically get added to |_commands|.
82
83 Args:
84 toolset: toolset (string) to import.
David Pursellf1c27c12015-03-18 09:51:38 -070085 """
David Pursellffb90042015-03-23 09:21:41 -070086 subdir_path = os.path.join(os.path.dirname(__file__), toolset)
87 for file_path in _FindModules(subdir_path, toolset):
David Pursellf1c27c12015-03-18 09:51:38 -070088 file_name = os.path.basename(file_path)
89 mod_name = os.path.splitext(file_name)[0]
David Pursellffb90042015-03-23 09:21:41 -070090 cros_import.ImportModule(('chromite', 'cli', toolset, mod_name))
David Pursellf1c27c12015-03-18 09:51:38 -070091
92
David Pursellffb90042015-03-23 09:21:41 -070093def ListCommands(toolset=None):
94 """Return a dictionary mapping command names to classes.
95
96 Args:
97 toolset: toolset (string) to list, None to determine from the commandline.
98
99 Returns:
100 A dictionary mapping names (strings) to commands (classes).
101 """
102 _ImportCommands(toolset or _GetToolset())
David Pursellf1c27c12015-03-18 09:51:38 -0700103 return _commands.copy()
104
105
Brian Harring984988f2012-10-10 22:53:30 -0700106class InvalidCommandError(Exception):
107 """Error that occurs when command class fails sanity checks."""
108 pass
109
110
111def CommandDecorator(command_name):
112 """Decorator that sanity checks and adds class to list of usable commands."""
113
114 def InnerCommandDecorator(original_class):
115 """"Inner Decorator that actually wraps the class."""
116 if not hasattr(original_class, '__doc__'):
117 raise InvalidCommandError('All handlers must have docstrings: %s' %
118 original_class)
119
David Pursellffb90042015-03-23 09:21:41 -0700120 if not issubclass(original_class, CliCommand):
121 raise InvalidCommandError('All Commands must derive from CliCommand: %s' %
122 original_class)
Brian Harring984988f2012-10-10 22:53:30 -0700123
124 _commands[command_name] = original_class
Ryan Cui47f80e42013-04-01 19:01:54 -0700125 original_class.command_name = command_name
126
Brian Harring984988f2012-10-10 22:53:30 -0700127 return original_class
128
129 return InnerCommandDecorator
130
131
David Pursellffb90042015-03-23 09:21:41 -0700132class CliCommand(object):
133 """All CLI commands must derive from this class.
Brian Harring984988f2012-10-10 22:53:30 -0700134
David Pursellffb90042015-03-23 09:21:41 -0700135 This class provides the abstract interface for all CLI commands. When
Brian Harring984988f2012-10-10 22:53:30 -0700136 designing a new command, you must sub-class from this class and use the
137 CommandDecorator decorator. You must specify a class docstring as that will be
138 used as the usage for the sub-command.
139
140 In addition your command should implement AddParser which is passed in a
141 parser that you can add your own custom arguments. See argparse for more
142 information.
143 """
Ryan Cui47f80e42013-04-01 19:01:54 -0700144 # Indicates whether command stats should be uploaded for this command.
145 # Override to enable command stats uploading.
146 upload_stats = False
147 # We set the default timeout to 1 second, to prevent overly long waits for
148 # commands to complete. From manual tests, stat uploads usually take
149 # between 0.35s-0.45s in MTV.
150 upload_stats_timeout = 1
151
Ryo Hashimoto8bc997b2014-01-22 18:46:17 +0900152 # Indicates whether command uses cache related commandline options.
153 use_caching_options = False
154
Brian Harring984988f2012-10-10 22:53:30 -0700155 def __init__(self, options):
156 self.options = options
Bertrand SIMONNET2b1ed052015-03-02 11:17:40 -0800157 brick = brick_lib.FindBrickInPath()
Bertrand SIMONNET79e077d2015-03-12 18:31:12 -0700158 self.curr_brick_locator = brick.brick_locator if brick else None
Brian Harring984988f2012-10-10 22:53:30 -0700159
160 @classmethod
161 def AddParser(cls, parser):
162 """Add arguments for this command to the parser."""
David Pursellffb90042015-03-23 09:21:41 -0700163 parser.set_defaults(command_class=cls)
Brian Harring984988f2012-10-10 22:53:30 -0700164
David Pursellf80b8c52015-04-03 12:46:45 -0700165 @classmethod
166 def AddDeviceArgument(cls, parser, schemes=commandline.DEVICE_SCHEME_SSH,
167 optional=None):
168 """Add a device argument to the parser.
169
170 This has a few advantages over adding a device argument directly:
171 - Standardizes the device --help message for all tools.
172 - May allow `brillo` and `cros` to use the same source.
173
174 The device argument is normally positional in cros but optional in
175 brillo. If that is the only difference between a cros and brillo
176 tool, this function allows the same source be shared for both.
177
178 Args:
179 parser: The parser to add the device argument to.
180 schemes: List of device schemes or single scheme to allow.
181 optional: Whether the device is an optional or positional
182 argument; None to auto-determine based on toolset.
183 """
184 if optional is None:
185 optional = (_GetToolset() == 'brillo')
186 help_strings = []
187 schemes = list(cros_build_lib.iflatten_instance(schemes))
188 if commandline.DEVICE_SCHEME_SSH in schemes:
189 help_strings.append('Target a device with [user@]hostname[:port].')
190 if commandline.DEVICE_SCHEME_USB in schemes:
191 help_strings.append('Target removable media with usb://[path].')
192 if commandline.DEVICE_SCHEME_FILE in schemes:
193 help_strings.append('Target a local file with file://path.')
194 parser.add_argument('--device' if optional else 'device',
195 type=commandline.DeviceParser(schemes),
196 help=' '.join(help_strings))
197
Brian Harring984988f2012-10-10 22:53:30 -0700198 def Run(self):
199 """The command to run."""
200 raise NotImplementedError()