blob: 360a6ec10b6108281e6b9c44009334124f85645f [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.
Brian Harring984988f2012-10-10 22:53:30 -07009 CommandDecorator: Decorator that must be used to ensure that the command shows
David Pursellffb90042015-03-23 09:21:41 -070010 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
Mike Frysinger383367e2014-09-16 15:06:17 -040016from __future__ import print_function
17
David Pursellf1c27c12015-03-18 09:51:38 -070018import glob
19import os
20
Ralph Nathan74e864d2015-05-11 12:13:53 -070021from chromite.cbuildbot import constants
Bertrand SIMONNET2b1ed052015-03-02 11:17:40 -080022from chromite.lib import brick_lib
David Pursellf80b8c52015-04-03 12:46:45 -070023from chromite.lib import commandline
24from chromite.lib import cros_build_lib
David Pursellf1c27c12015-03-18 09:51:38 -070025from chromite.lib import cros_import
Ralph Nathan69931252015-04-14 16:49:21 -070026from chromite.lib import cros_logging as logging
Ralph Nathan74e864d2015-05-11 12:13:53 -070027from chromite.lib import osutils
28from chromite.lib import workspace_lib
David Pursellf1c27c12015-03-18 09:51:38 -070029
Brian Harring984988f2012-10-10 22:53:30 -070030
David Pursellc7ba7842015-07-08 10:48:41 -070031# Paths for finding and importing subcommand modules.
32_SUBCOMMAND_MODULE_DIRECTORY = os.path.join(os.path.dirname(__file__), 'cros')
33_SUBCOMMAND_MODULE_PREFIX = 'cros_'
34
35
Brian Harring984988f2012-10-10 22:53:30 -070036_commands = dict()
37
38
Ralph Nathan74e864d2015-05-11 12:13:53 -070039def SetupFileLogger(filename='brillo.log', log_level=logging.DEBUG):
40 """Store log messages to a file.
41
42 In case of an error, this file can be made visible to the user.
43 """
44 workspace_path = workspace_lib.WorkspacePath()
45 if workspace_path is None:
46 return
Ralph Nathan7a0bae22015-05-13 10:49:54 -070047 path = os.path.join(workspace_path, workspace_lib.WORKSPACE_LOGS_DIR,
48 filename)
Ralph Nathan74e864d2015-05-11 12:13:53 -070049 osutils.Touch(path, makedirs=True)
50 logger = logging.getLogger()
51 fh = logging.FileHandler(path, mode='w')
52 fh.setLevel(log_level)
53 fh.setFormatter(
54 logging.Formatter(fmt=constants.LOGGER_FMT,
55 datefmt=constants.LOGGER_DATE_FMT))
56 logger.addHandler(fh)
57
58
Ralph Nathan69931252015-04-14 16:49:21 -070059def UseProgressBar():
60 """Determine whether the progress bar is to be used or not.
61
62 We only want the progress bar to display for the brillo commands which operate
63 at logging level NOTICE. If the user wants to see the noisy output, then they
64 can execute the command at logging level INFO or DEBUG.
65 """
66 return logging.getLogger().getEffectiveLevel() == logging.NOTICE
67
68
David Pursellc7ba7842015-07-08 10:48:41 -070069def _FindModules(subdir_path):
70 """Returns a list of subcommand python modules in |subdir_path|.
David Pursellffb90042015-03-23 09:21:41 -070071
72 Args:
73 subdir_path: directory (string) to search for modules in.
David Pursellffb90042015-03-23 09:21:41 -070074
75 Returns:
76 List of filenames (strings).
77 """
David Pursellf1c27c12015-03-18 09:51:38 -070078 modules = []
David Pursellc7ba7842015-07-08 10:48:41 -070079 glob_path = os.path.join(subdir_path, '%s*.py' % _SUBCOMMAND_MODULE_PREFIX)
80 for file_path in glob.glob(glob_path):
David Pursellf1c27c12015-03-18 09:51:38 -070081 if not file_path.endswith('_unittest.py'):
82 modules.append(file_path)
David Pursellf1c27c12015-03-18 09:51:38 -070083 return modules
84
85
David Pursellc7ba7842015-07-08 10:48:41 -070086def _ImportCommands():
87 """Directly imports all subcommand python modules.
David Pursellf1c27c12015-03-18 09:51:38 -070088
David Pursellc7ba7842015-07-08 10:48:41 -070089 This method imports the modules which may contain subcommands. When
90 these modules are loaded, declared commands (those that use
David Pursellffb90042015-03-23 09:21:41 -070091 CommandDecorator) will automatically get added to |_commands|.
David Pursellf1c27c12015-03-18 09:51:38 -070092 """
David Pursellc7ba7842015-07-08 10:48:41 -070093 for file_path in _FindModules(_SUBCOMMAND_MODULE_DIRECTORY):
94 module_path = os.path.splitext(file_path)[0]
95 import_path = os.path.relpath(os.path.realpath(module_path),
96 os.path.dirname(constants.CHROMITE_DIR))
97 cros_import.ImportModule(import_path.split(os.path.sep))
David Pursellf1c27c12015-03-18 09:51:38 -070098
99
David Pursellc7ba7842015-07-08 10:48:41 -0700100def ListCommands():
David Pursellffb90042015-03-23 09:21:41 -0700101 """Return a dictionary mapping command names to classes.
102
David Pursellffb90042015-03-23 09:21:41 -0700103 Returns:
104 A dictionary mapping names (strings) to commands (classes).
105 """
David Pursellc7ba7842015-07-08 10:48:41 -0700106 _ImportCommands()
David Pursellf1c27c12015-03-18 09:51:38 -0700107 return _commands.copy()
108
109
Brian Harring984988f2012-10-10 22:53:30 -0700110class InvalidCommandError(Exception):
111 """Error that occurs when command class fails sanity checks."""
112 pass
113
114
115def CommandDecorator(command_name):
116 """Decorator that sanity checks and adds class to list of usable commands."""
117
118 def InnerCommandDecorator(original_class):
119 """"Inner Decorator that actually wraps the class."""
120 if not hasattr(original_class, '__doc__'):
121 raise InvalidCommandError('All handlers must have docstrings: %s' %
122 original_class)
123
David Pursellffb90042015-03-23 09:21:41 -0700124 if not issubclass(original_class, CliCommand):
125 raise InvalidCommandError('All Commands must derive from CliCommand: %s' %
126 original_class)
Brian Harring984988f2012-10-10 22:53:30 -0700127
128 _commands[command_name] = original_class
Ryan Cui47f80e42013-04-01 19:01:54 -0700129 original_class.command_name = command_name
130
Brian Harring984988f2012-10-10 22:53:30 -0700131 return original_class
132
133 return InnerCommandDecorator
134
135
David Pursellffb90042015-03-23 09:21:41 -0700136class CliCommand(object):
137 """All CLI commands must derive from this class.
Brian Harring984988f2012-10-10 22:53:30 -0700138
David Pursellffb90042015-03-23 09:21:41 -0700139 This class provides the abstract interface for all CLI commands. When
Brian Harring984988f2012-10-10 22:53:30 -0700140 designing a new command, you must sub-class from this class and use the
141 CommandDecorator decorator. You must specify a class docstring as that will be
142 used as the usage for the sub-command.
143
144 In addition your command should implement AddParser which is passed in a
145 parser that you can add your own custom arguments. See argparse for more
146 information.
147 """
Ryan Cui47f80e42013-04-01 19:01:54 -0700148 # Indicates whether command stats should be uploaded for this command.
149 # Override to enable command stats uploading.
150 upload_stats = False
151 # We set the default timeout to 1 second, to prevent overly long waits for
152 # commands to complete. From manual tests, stat uploads usually take
153 # between 0.35s-0.45s in MTV.
154 upload_stats_timeout = 1
155
Ryo Hashimoto8bc997b2014-01-22 18:46:17 +0900156 # Indicates whether command uses cache related commandline options.
157 use_caching_options = False
158
Brian Harring984988f2012-10-10 22:53:30 -0700159 def __init__(self, options):
160 self.options = options
Bertrand SIMONNET2b1ed052015-03-02 11:17:40 -0800161 brick = brick_lib.FindBrickInPath()
Bertrand SIMONNET79e077d2015-03-12 18:31:12 -0700162 self.curr_brick_locator = brick.brick_locator if brick else None
Brian Harring984988f2012-10-10 22:53:30 -0700163
164 @classmethod
165 def AddParser(cls, parser):
166 """Add arguments for this command to the parser."""
David Pursellffb90042015-03-23 09:21:41 -0700167 parser.set_defaults(command_class=cls)
Brian Harring984988f2012-10-10 22:53:30 -0700168
David Pursellf80b8c52015-04-03 12:46:45 -0700169 @classmethod
David Pursellc7ba7842015-07-08 10:48:41 -0700170 def AddDeviceArgument(cls, parser, schemes=commandline.DEVICE_SCHEME_SSH):
David Pursellf80b8c52015-04-03 12:46:45 -0700171 """Add a device argument to the parser.
172
David Pursellc7ba7842015-07-08 10:48:41 -0700173 This standardizes the help message across all subcommands.
David Pursellf80b8c52015-04-03 12:46:45 -0700174
175 Args:
176 parser: The parser to add the device argument to.
177 schemes: List of device schemes or single scheme to allow.
David Pursellf80b8c52015-04-03 12:46:45 -0700178 """
David Pursellf80b8c52015-04-03 12:46:45 -0700179 help_strings = []
180 schemes = list(cros_build_lib.iflatten_instance(schemes))
181 if commandline.DEVICE_SCHEME_SSH in schemes:
182 help_strings.append('Target a device with [user@]hostname[:port].')
183 if commandline.DEVICE_SCHEME_USB in schemes:
184 help_strings.append('Target removable media with usb://[path].')
185 if commandline.DEVICE_SCHEME_FILE in schemes:
186 help_strings.append('Target a local file with file://path.')
David Pursellc7ba7842015-07-08 10:48:41 -0700187 parser.add_argument('device',
David Pursellf80b8c52015-04-03 12:46:45 -0700188 type=commandline.DeviceParser(schemes),
189 help=' '.join(help_strings))
190
Brian Harring984988f2012-10-10 22:53:30 -0700191 def Run(self):
192 """The command to run."""
193 raise NotImplementedError()