blob: 9795343b7ea5c331479495a7812ea4f720a61163 [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
Ralph Nathan7a0bae22015-05-13 10:49:54 -070046 path = os.path.join(workspace_path, workspace_lib.WORKSPACE_LOGS_DIR,
47 filename)
Ralph Nathan74e864d2015-05-11 12:13:53 -070048 osutils.Touch(path, makedirs=True)
49 logger = logging.getLogger()
50 fh = logging.FileHandler(path, mode='w')
51 fh.setLevel(log_level)
52 fh.setFormatter(
53 logging.Formatter(fmt=constants.LOGGER_FMT,
54 datefmt=constants.LOGGER_DATE_FMT))
55 logger.addHandler(fh)
56
57
Ralph Nathan69931252015-04-14 16:49:21 -070058def UseProgressBar():
59 """Determine whether the progress bar is to be used or not.
60
61 We only want the progress bar to display for the brillo commands which operate
62 at logging level NOTICE. If the user wants to see the noisy output, then they
63 can execute the command at logging level INFO or DEBUG.
64 """
65 return logging.getLogger().getEffectiveLevel() == logging.NOTICE
66
67
Ralph Nathan1fc77f22015-04-21 15:05:48 -070068def GetToolset():
David Pursellffb90042015-03-23 09:21:41 -070069 """Return the CLI toolset invoked by the user.
70
71 For example, if the user is executing `cros flash`, this will return 'cros'.
72
73 This won't work for unittests so if a certain toolset must be loaded for
74 a unittest this should be mocked out to return the desired string.
75 """
76 return os.path.basename(sys.argv[0])
77
78
79def _FindModules(subdir_path, toolset):
80 """Returns a list of all the relevant python modules in |subdir_path|.
81
82 The modules returned are based on |toolset|, so if |toolset| is 'cros'
83 then cros_xxx.py modules will be found.
84
85 Args:
86 subdir_path: directory (string) to search for modules in.
87 toolset: toolset (string) to find.
88
89 Returns:
90 List of filenames (strings).
91 """
David Pursellf1c27c12015-03-18 09:51:38 -070092 modules = []
David Pursellffb90042015-03-23 09:21:41 -070093 for file_path in glob.glob(os.path.join(subdir_path, toolset + '_*.py')):
David Pursellf1c27c12015-03-18 09:51:38 -070094 if not file_path.endswith('_unittest.py'):
95 modules.append(file_path)
David Pursellf1c27c12015-03-18 09:51:38 -070096 return modules
97
98
David Pursellffb90042015-03-23 09:21:41 -070099def _ImportCommands(toolset):
100 """Directly imports all |toolset| python modules.
David Pursellf1c27c12015-03-18 09:51:38 -0700101
David Pursellffb90042015-03-23 09:21:41 -0700102 This method imports the |toolset|_[!unittest] modules which may contain
David Pursellf1c27c12015-03-18 09:51:38 -0700103 commands. When these modules are loaded, declared commands (those that use
David Pursellffb90042015-03-23 09:21:41 -0700104 CommandDecorator) will automatically get added to |_commands|.
105
106 Args:
107 toolset: toolset (string) to import.
David Pursellf1c27c12015-03-18 09:51:38 -0700108 """
David Pursellffb90042015-03-23 09:21:41 -0700109 subdir_path = os.path.join(os.path.dirname(__file__), toolset)
110 for file_path in _FindModules(subdir_path, toolset):
David Pursellf1c27c12015-03-18 09:51:38 -0700111 file_name = os.path.basename(file_path)
112 mod_name = os.path.splitext(file_name)[0]
David Pursellffb90042015-03-23 09:21:41 -0700113 cros_import.ImportModule(('chromite', 'cli', toolset, mod_name))
David Pursellf1c27c12015-03-18 09:51:38 -0700114
115
David Pursellffb90042015-03-23 09:21:41 -0700116def ListCommands(toolset=None):
117 """Return a dictionary mapping command names to classes.
118
119 Args:
120 toolset: toolset (string) to list, None to determine from the commandline.
121
122 Returns:
123 A dictionary mapping names (strings) to commands (classes).
124 """
Ralph Nathan1fc77f22015-04-21 15:05:48 -0700125 _ImportCommands(toolset or GetToolset())
David Pursellf1c27c12015-03-18 09:51:38 -0700126 return _commands.copy()
127
128
Brian Harring984988f2012-10-10 22:53:30 -0700129class InvalidCommandError(Exception):
130 """Error that occurs when command class fails sanity checks."""
131 pass
132
133
134def CommandDecorator(command_name):
135 """Decorator that sanity checks and adds class to list of usable commands."""
136
137 def InnerCommandDecorator(original_class):
138 """"Inner Decorator that actually wraps the class."""
139 if not hasattr(original_class, '__doc__'):
140 raise InvalidCommandError('All handlers must have docstrings: %s' %
141 original_class)
142
David Pursellffb90042015-03-23 09:21:41 -0700143 if not issubclass(original_class, CliCommand):
144 raise InvalidCommandError('All Commands must derive from CliCommand: %s' %
145 original_class)
Brian Harring984988f2012-10-10 22:53:30 -0700146
147 _commands[command_name] = original_class
Ryan Cui47f80e42013-04-01 19:01:54 -0700148 original_class.command_name = command_name
149
Brian Harring984988f2012-10-10 22:53:30 -0700150 return original_class
151
152 return InnerCommandDecorator
153
154
David Pursellffb90042015-03-23 09:21:41 -0700155class CliCommand(object):
156 """All CLI commands must derive from this class.
Brian Harring984988f2012-10-10 22:53:30 -0700157
David Pursellffb90042015-03-23 09:21:41 -0700158 This class provides the abstract interface for all CLI commands. When
Brian Harring984988f2012-10-10 22:53:30 -0700159 designing a new command, you must sub-class from this class and use the
160 CommandDecorator decorator. You must specify a class docstring as that will be
161 used as the usage for the sub-command.
162
163 In addition your command should implement AddParser which is passed in a
164 parser that you can add your own custom arguments. See argparse for more
165 information.
166 """
Ryan Cui47f80e42013-04-01 19:01:54 -0700167 # Indicates whether command stats should be uploaded for this command.
168 # Override to enable command stats uploading.
169 upload_stats = False
170 # We set the default timeout to 1 second, to prevent overly long waits for
171 # commands to complete. From manual tests, stat uploads usually take
172 # between 0.35s-0.45s in MTV.
173 upload_stats_timeout = 1
174
Ryo Hashimoto8bc997b2014-01-22 18:46:17 +0900175 # Indicates whether command uses cache related commandline options.
176 use_caching_options = False
177
Brian Harring984988f2012-10-10 22:53:30 -0700178 def __init__(self, options):
179 self.options = options
Bertrand SIMONNET2b1ed052015-03-02 11:17:40 -0800180 brick = brick_lib.FindBrickInPath()
Bertrand SIMONNET79e077d2015-03-12 18:31:12 -0700181 self.curr_brick_locator = brick.brick_locator if brick else None
Brian Harring984988f2012-10-10 22:53:30 -0700182
183 @classmethod
184 def AddParser(cls, parser):
185 """Add arguments for this command to the parser."""
David Pursellffb90042015-03-23 09:21:41 -0700186 parser.set_defaults(command_class=cls)
Brian Harring984988f2012-10-10 22:53:30 -0700187
David Pursellf80b8c52015-04-03 12:46:45 -0700188 @classmethod
189 def AddDeviceArgument(cls, parser, schemes=commandline.DEVICE_SCHEME_SSH,
190 optional=None):
191 """Add a device argument to the parser.
192
193 This has a few advantages over adding a device argument directly:
194 - Standardizes the device --help message for all tools.
195 - May allow `brillo` and `cros` to use the same source.
196
197 The device argument is normally positional in cros but optional in
198 brillo. If that is the only difference between a cros and brillo
199 tool, this function allows the same source be shared for both.
200
201 Args:
202 parser: The parser to add the device argument to.
203 schemes: List of device schemes or single scheme to allow.
204 optional: Whether the device is an optional or positional
205 argument; None to auto-determine based on toolset.
206 """
207 if optional is None:
Ralph Nathan1fc77f22015-04-21 15:05:48 -0700208 optional = (GetToolset() == 'brillo')
David Pursellf80b8c52015-04-03 12:46:45 -0700209 help_strings = []
210 schemes = list(cros_build_lib.iflatten_instance(schemes))
211 if commandline.DEVICE_SCHEME_SSH in schemes:
212 help_strings.append('Target a device with [user@]hostname[:port].')
213 if commandline.DEVICE_SCHEME_USB in schemes:
214 help_strings.append('Target removable media with usb://[path].')
215 if commandline.DEVICE_SCHEME_FILE in schemes:
216 help_strings.append('Target a local file with file://path.')
217 parser.add_argument('--device' if optional else 'device',
218 type=commandline.DeviceParser(schemes),
219 help=' '.join(help_strings))
220
Brian Harring984988f2012-10-10 22:53:30 -0700221 def Run(self):
222 """The command to run."""
223 raise NotImplementedError()