blob: 7f4e4b0963b3900224a50d8a52e69ab305a1758c [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
David Pursellf80b8c52015-04-03 12:46:45 -070022from chromite.lib import commandline
23from chromite.lib import cros_build_lib
David Pursellf1c27c12015-03-18 09:51:38 -070024from chromite.lib import cros_import
Ralph Nathan69931252015-04-14 16:49:21 -070025from chromite.lib import cros_logging as logging
David Pursellf1c27c12015-03-18 09:51:38 -070026
Brian Harring984988f2012-10-10 22:53:30 -070027
David Pursellc7ba7842015-07-08 10:48:41 -070028# Paths for finding and importing subcommand modules.
29_SUBCOMMAND_MODULE_DIRECTORY = os.path.join(os.path.dirname(__file__), 'cros')
30_SUBCOMMAND_MODULE_PREFIX = 'cros_'
31
32
Brian Harring984988f2012-10-10 22:53:30 -070033_commands = dict()
34
35
Ralph Nathan69931252015-04-14 16:49:21 -070036def UseProgressBar():
37 """Determine whether the progress bar is to be used or not.
38
39 We only want the progress bar to display for the brillo commands which operate
40 at logging level NOTICE. If the user wants to see the noisy output, then they
41 can execute the command at logging level INFO or DEBUG.
42 """
43 return logging.getLogger().getEffectiveLevel() == logging.NOTICE
44
45
David Pursellc7ba7842015-07-08 10:48:41 -070046def _FindModules(subdir_path):
47 """Returns a list of subcommand python modules in |subdir_path|.
David Pursellffb90042015-03-23 09:21:41 -070048
49 Args:
50 subdir_path: directory (string) to search for modules in.
David Pursellffb90042015-03-23 09:21:41 -070051
52 Returns:
53 List of filenames (strings).
54 """
David Pursellf1c27c12015-03-18 09:51:38 -070055 modules = []
David Pursellc7ba7842015-07-08 10:48:41 -070056 glob_path = os.path.join(subdir_path, '%s*.py' % _SUBCOMMAND_MODULE_PREFIX)
57 for file_path in glob.glob(glob_path):
David Pursellf1c27c12015-03-18 09:51:38 -070058 if not file_path.endswith('_unittest.py'):
59 modules.append(file_path)
David Pursellf1c27c12015-03-18 09:51:38 -070060 return modules
61
62
David Pursellc7ba7842015-07-08 10:48:41 -070063def _ImportCommands():
64 """Directly imports all subcommand python modules.
David Pursellf1c27c12015-03-18 09:51:38 -070065
David Pursellc7ba7842015-07-08 10:48:41 -070066 This method imports the modules which may contain subcommands. When
67 these modules are loaded, declared commands (those that use
David Pursellffb90042015-03-23 09:21:41 -070068 CommandDecorator) will automatically get added to |_commands|.
David Pursellf1c27c12015-03-18 09:51:38 -070069 """
David Pursellc7ba7842015-07-08 10:48:41 -070070 for file_path in _FindModules(_SUBCOMMAND_MODULE_DIRECTORY):
71 module_path = os.path.splitext(file_path)[0]
72 import_path = os.path.relpath(os.path.realpath(module_path),
73 os.path.dirname(constants.CHROMITE_DIR))
74 cros_import.ImportModule(import_path.split(os.path.sep))
David Pursellf1c27c12015-03-18 09:51:38 -070075
76
David Pursellc7ba7842015-07-08 10:48:41 -070077def ListCommands():
David Pursellffb90042015-03-23 09:21:41 -070078 """Return a dictionary mapping command names to classes.
79
David Pursellffb90042015-03-23 09:21:41 -070080 Returns:
81 A dictionary mapping names (strings) to commands (classes).
82 """
David Pursellc7ba7842015-07-08 10:48:41 -070083 _ImportCommands()
David Pursellf1c27c12015-03-18 09:51:38 -070084 return _commands.copy()
85
86
Brian Harring984988f2012-10-10 22:53:30 -070087class InvalidCommandError(Exception):
88 """Error that occurs when command class fails sanity checks."""
89 pass
90
91
92def CommandDecorator(command_name):
93 """Decorator that sanity checks and adds class to list of usable commands."""
94
95 def InnerCommandDecorator(original_class):
96 """"Inner Decorator that actually wraps the class."""
97 if not hasattr(original_class, '__doc__'):
98 raise InvalidCommandError('All handlers must have docstrings: %s' %
99 original_class)
100
David Pursellffb90042015-03-23 09:21:41 -0700101 if not issubclass(original_class, CliCommand):
102 raise InvalidCommandError('All Commands must derive from CliCommand: %s' %
103 original_class)
Brian Harring984988f2012-10-10 22:53:30 -0700104
105 _commands[command_name] = original_class
Ryan Cui47f80e42013-04-01 19:01:54 -0700106 original_class.command_name = command_name
107
Brian Harring984988f2012-10-10 22:53:30 -0700108 return original_class
109
110 return InnerCommandDecorator
111
112
David Pursellffb90042015-03-23 09:21:41 -0700113class CliCommand(object):
114 """All CLI commands must derive from this class.
Brian Harring984988f2012-10-10 22:53:30 -0700115
David Pursellffb90042015-03-23 09:21:41 -0700116 This class provides the abstract interface for all CLI commands. When
Brian Harring984988f2012-10-10 22:53:30 -0700117 designing a new command, you must sub-class from this class and use the
118 CommandDecorator decorator. You must specify a class docstring as that will be
119 used as the usage for the sub-command.
120
121 In addition your command should implement AddParser which is passed in a
122 parser that you can add your own custom arguments. See argparse for more
123 information.
124 """
Ryan Cui47f80e42013-04-01 19:01:54 -0700125 # Indicates whether command stats should be uploaded for this command.
126 # Override to enable command stats uploading.
127 upload_stats = False
128 # We set the default timeout to 1 second, to prevent overly long waits for
129 # commands to complete. From manual tests, stat uploads usually take
130 # between 0.35s-0.45s in MTV.
131 upload_stats_timeout = 1
132
Ryo Hashimoto8bc997b2014-01-22 18:46:17 +0900133 # Indicates whether command uses cache related commandline options.
134 use_caching_options = False
135
Brian Harring984988f2012-10-10 22:53:30 -0700136 def __init__(self, options):
137 self.options = options
138
139 @classmethod
140 def AddParser(cls, parser):
141 """Add arguments for this command to the parser."""
David Pursellffb90042015-03-23 09:21:41 -0700142 parser.set_defaults(command_class=cls)
Brian Harring984988f2012-10-10 22:53:30 -0700143
David Pursellf80b8c52015-04-03 12:46:45 -0700144 @classmethod
David Pursellc7ba7842015-07-08 10:48:41 -0700145 def AddDeviceArgument(cls, parser, schemes=commandline.DEVICE_SCHEME_SSH):
David Pursellf80b8c52015-04-03 12:46:45 -0700146 """Add a device argument to the parser.
147
David Pursellc7ba7842015-07-08 10:48:41 -0700148 This standardizes the help message across all subcommands.
David Pursellf80b8c52015-04-03 12:46:45 -0700149
150 Args:
151 parser: The parser to add the device argument to.
152 schemes: List of device schemes or single scheme to allow.
David Pursellf80b8c52015-04-03 12:46:45 -0700153 """
David Pursellf80b8c52015-04-03 12:46:45 -0700154 help_strings = []
155 schemes = list(cros_build_lib.iflatten_instance(schemes))
156 if commandline.DEVICE_SCHEME_SSH in schemes:
157 help_strings.append('Target a device with [user@]hostname[:port].')
158 if commandline.DEVICE_SCHEME_USB in schemes:
159 help_strings.append('Target removable media with usb://[path].')
160 if commandline.DEVICE_SCHEME_FILE in schemes:
161 help_strings.append('Target a local file with file://path.')
David Pursellc7ba7842015-07-08 10:48:41 -0700162 parser.add_argument('device',
David Pursellf80b8c52015-04-03 12:46:45 -0700163 type=commandline.DeviceParser(schemes),
164 help=' '.join(help_strings))
165
Brian Harring984988f2012-10-10 22:53:30 -0700166 def Run(self):
167 """The command to run."""
168 raise NotImplementedError()