blob: 3c02c5b06b43032ac2567f52d525a7216f63657c [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
Ralph Nathan74e864d2015-05-11 12:13:53 -070025from chromite.cbuildbot import constants
Bertrand SIMONNET2b1ed052015-03-02 11:17:40 -080026from chromite.lib import brick_lib
David Pursellf80b8c52015-04-03 12:46:45 -070027from chromite.lib import commandline
28from chromite.lib import cros_build_lib
David Pursellf1c27c12015-03-18 09:51:38 -070029from chromite.lib import cros_import
Ralph Nathan69931252015-04-14 16:49:21 -070030from chromite.lib import cros_logging as logging
Ralph Nathan74e864d2015-05-11 12:13:53 -070031from chromite.lib import osutils
32from chromite.lib import workspace_lib
David Pursellf1c27c12015-03-18 09:51:38 -070033
Brian Harring984988f2012-10-10 22:53:30 -070034
35_commands = dict()
36
37
Ralph Nathan74e864d2015-05-11 12:13:53 -070038def SetupFileLogger(filename='brillo.log', log_level=logging.DEBUG):
39 """Store log messages to a file.
40
41 In case of an error, this file can be made visible to the user.
42 """
43 workspace_path = workspace_lib.WorkspacePath()
44 if workspace_path is None:
45 return
46 path = os.path.join(workspace_path, 'build/logs', filename)
47 osutils.Touch(path, makedirs=True)
48 logger = logging.getLogger()
49 fh = logging.FileHandler(path, mode='w')
50 fh.setLevel(log_level)
51 fh.setFormatter(
52 logging.Formatter(fmt=constants.LOGGER_FMT,
53 datefmt=constants.LOGGER_DATE_FMT))
54 logger.addHandler(fh)
55
56
Ralph Nathan69931252015-04-14 16:49:21 -070057def UseProgressBar():
58 """Determine whether the progress bar is to be used or not.
59
60 We only want the progress bar to display for the brillo commands which operate
61 at logging level NOTICE. If the user wants to see the noisy output, then they
62 can execute the command at logging level INFO or DEBUG.
63 """
64 return logging.getLogger().getEffectiveLevel() == logging.NOTICE
65
66
Ralph Nathan1fc77f22015-04-21 15:05:48 -070067def GetToolset():
David Pursellffb90042015-03-23 09:21:41 -070068 """Return the CLI toolset invoked by the user.
69
70 For example, if the user is executing `cros flash`, this will return 'cros'.
71
72 This won't work for unittests so if a certain toolset must be loaded for
73 a unittest this should be mocked out to return the desired string.
74 """
75 return os.path.basename(sys.argv[0])
76
77
78def _FindModules(subdir_path, toolset):
79 """Returns a list of all the relevant python modules in |subdir_path|.
80
81 The modules returned are based on |toolset|, so if |toolset| is 'cros'
82 then cros_xxx.py modules will be found.
83
84 Args:
85 subdir_path: directory (string) to search for modules in.
86 toolset: toolset (string) to find.
87
88 Returns:
89 List of filenames (strings).
90 """
David Pursellf1c27c12015-03-18 09:51:38 -070091 modules = []
David Pursellffb90042015-03-23 09:21:41 -070092 for file_path in glob.glob(os.path.join(subdir_path, toolset + '_*.py')):
David Pursellf1c27c12015-03-18 09:51:38 -070093 if not file_path.endswith('_unittest.py'):
94 modules.append(file_path)
David Pursellf1c27c12015-03-18 09:51:38 -070095 return modules
96
97
David Pursellffb90042015-03-23 09:21:41 -070098def _ImportCommands(toolset):
99 """Directly imports all |toolset| python modules.
David Pursellf1c27c12015-03-18 09:51:38 -0700100
David Pursellffb90042015-03-23 09:21:41 -0700101 This method imports the |toolset|_[!unittest] modules which may contain
David Pursellf1c27c12015-03-18 09:51:38 -0700102 commands. When these modules are loaded, declared commands (those that use
David Pursellffb90042015-03-23 09:21:41 -0700103 CommandDecorator) will automatically get added to |_commands|.
104
105 Args:
106 toolset: toolset (string) to import.
David Pursellf1c27c12015-03-18 09:51:38 -0700107 """
David Pursellffb90042015-03-23 09:21:41 -0700108 subdir_path = os.path.join(os.path.dirname(__file__), toolset)
109 for file_path in _FindModules(subdir_path, toolset):
David Pursellf1c27c12015-03-18 09:51:38 -0700110 file_name = os.path.basename(file_path)
111 mod_name = os.path.splitext(file_name)[0]
David Pursellffb90042015-03-23 09:21:41 -0700112 cros_import.ImportModule(('chromite', 'cli', toolset, mod_name))
David Pursellf1c27c12015-03-18 09:51:38 -0700113
114
David Pursellffb90042015-03-23 09:21:41 -0700115def ListCommands(toolset=None):
116 """Return a dictionary mapping command names to classes.
117
118 Args:
119 toolset: toolset (string) to list, None to determine from the commandline.
120
121 Returns:
122 A dictionary mapping names (strings) to commands (classes).
123 """
Ralph Nathan1fc77f22015-04-21 15:05:48 -0700124 _ImportCommands(toolset or GetToolset())
David Pursellf1c27c12015-03-18 09:51:38 -0700125 return _commands.copy()
126
127
Brian Harring984988f2012-10-10 22:53:30 -0700128class InvalidCommandError(Exception):
129 """Error that occurs when command class fails sanity checks."""
130 pass
131
132
133def CommandDecorator(command_name):
134 """Decorator that sanity checks and adds class to list of usable commands."""
135
136 def InnerCommandDecorator(original_class):
137 """"Inner Decorator that actually wraps the class."""
138 if not hasattr(original_class, '__doc__'):
139 raise InvalidCommandError('All handlers must have docstrings: %s' %
140 original_class)
141
David Pursellffb90042015-03-23 09:21:41 -0700142 if not issubclass(original_class, CliCommand):
143 raise InvalidCommandError('All Commands must derive from CliCommand: %s' %
144 original_class)
Brian Harring984988f2012-10-10 22:53:30 -0700145
146 _commands[command_name] = original_class
Ryan Cui47f80e42013-04-01 19:01:54 -0700147 original_class.command_name = command_name
148
Brian Harring984988f2012-10-10 22:53:30 -0700149 return original_class
150
151 return InnerCommandDecorator
152
153
David Pursellffb90042015-03-23 09:21:41 -0700154class CliCommand(object):
155 """All CLI commands must derive from this class.
Brian Harring984988f2012-10-10 22:53:30 -0700156
David Pursellffb90042015-03-23 09:21:41 -0700157 This class provides the abstract interface for all CLI commands. When
Brian Harring984988f2012-10-10 22:53:30 -0700158 designing a new command, you must sub-class from this class and use the
159 CommandDecorator decorator. You must specify a class docstring as that will be
160 used as the usage for the sub-command.
161
162 In addition your command should implement AddParser which is passed in a
163 parser that you can add your own custom arguments. See argparse for more
164 information.
165 """
Ryan Cui47f80e42013-04-01 19:01:54 -0700166 # Indicates whether command stats should be uploaded for this command.
167 # Override to enable command stats uploading.
168 upload_stats = False
169 # We set the default timeout to 1 second, to prevent overly long waits for
170 # commands to complete. From manual tests, stat uploads usually take
171 # between 0.35s-0.45s in MTV.
172 upload_stats_timeout = 1
173
Ryo Hashimoto8bc997b2014-01-22 18:46:17 +0900174 # Indicates whether command uses cache related commandline options.
175 use_caching_options = False
176
Brian Harring984988f2012-10-10 22:53:30 -0700177 def __init__(self, options):
178 self.options = options
Bertrand SIMONNET2b1ed052015-03-02 11:17:40 -0800179 brick = brick_lib.FindBrickInPath()
Bertrand SIMONNET79e077d2015-03-12 18:31:12 -0700180 self.curr_brick_locator = brick.brick_locator if brick else None
Brian Harring984988f2012-10-10 22:53:30 -0700181
182 @classmethod
183 def AddParser(cls, parser):
184 """Add arguments for this command to the parser."""
David Pursellffb90042015-03-23 09:21:41 -0700185 parser.set_defaults(command_class=cls)
Brian Harring984988f2012-10-10 22:53:30 -0700186
David Pursellf80b8c52015-04-03 12:46:45 -0700187 @classmethod
188 def AddDeviceArgument(cls, parser, schemes=commandline.DEVICE_SCHEME_SSH,
189 optional=None):
190 """Add a device argument to the parser.
191
192 This has a few advantages over adding a device argument directly:
193 - Standardizes the device --help message for all tools.
194 - May allow `brillo` and `cros` to use the same source.
195
196 The device argument is normally positional in cros but optional in
197 brillo. If that is the only difference between a cros and brillo
198 tool, this function allows the same source be shared for both.
199
200 Args:
201 parser: The parser to add the device argument to.
202 schemes: List of device schemes or single scheme to allow.
203 optional: Whether the device is an optional or positional
204 argument; None to auto-determine based on toolset.
205 """
206 if optional is None:
Ralph Nathan1fc77f22015-04-21 15:05:48 -0700207 optional = (GetToolset() == 'brillo')
David Pursellf80b8c52015-04-03 12:46:45 -0700208 help_strings = []
209 schemes = list(cros_build_lib.iflatten_instance(schemes))
210 if commandline.DEVICE_SCHEME_SSH in schemes:
211 help_strings.append('Target a device with [user@]hostname[:port].')
212 if commandline.DEVICE_SCHEME_USB in schemes:
213 help_strings.append('Target removable media with usb://[path].')
214 if commandline.DEVICE_SCHEME_FILE in schemes:
215 help_strings.append('Target a local file with file://path.')
216 parser.add_argument('--device' if optional else 'device',
217 type=commandline.DeviceParser(schemes),
218 help=' '.join(help_strings))
219
Brian Harring984988f2012-10-10 22:53:30 -0700220 def Run(self):
221 """The command to run."""
222 raise NotImplementedError()