blob: cdb3536caf0067c50f8cb0e4382f0a6df53d8b85 [file] [log] [blame]
Mike Frysingere58c0e22017-10-04 15:43:30 -04001# -*- coding: utf-8 -*-
Brian Harring984988f2012-10-10 22:53:30 -07002# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
David Pursellffb90042015-03-23 09:21:41 -07006"""Module that contains meta-logic related to CLI commands.
7
David Pursellf1c27c12015-03-18 09:51:38 -07008This module contains two important definitions used by all commands:
David Pursellffb90042015-03-23 09:21:41 -07009 CliCommand: The parent class of all CLI commands.
Brian Harring984988f2012-10-10 22:53:30 -070010 CommandDecorator: Decorator that must be used to ensure that the command shows
David Pursellffb90042015-03-23 09:21:41 -070011 up in |_commands| and is discoverable.
David Pursellf1c27c12015-03-18 09:51:38 -070012
13Commands can be either imported directly or looked up using this module's
14ListCommands() function.
Brian Harring984988f2012-10-10 22:53:30 -070015"""
16
Mike Frysinger383367e2014-09-16 15:06:17 -040017from __future__ import print_function
18
David Pursellf1c27c12015-03-18 09:51:38 -070019import glob
20import os
21
Aviv Keshetb7519e12016-10-04 00:50:00 -070022from chromite.lib import constants
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
David Pursellf1c27c12015-03-18 09:51:38 -070027
Brian Harring984988f2012-10-10 22:53:30 -070028
David Pursellc7ba7842015-07-08 10:48:41 -070029# Paths for finding and importing subcommand modules.
30_SUBCOMMAND_MODULE_DIRECTORY = os.path.join(os.path.dirname(__file__), 'cros')
31_SUBCOMMAND_MODULE_PREFIX = 'cros_'
32
33
Brian Harring984988f2012-10-10 22:53:30 -070034_commands = dict()
35
36
Ralph Nathan69931252015-04-14 16:49:21 -070037def UseProgressBar():
38 """Determine whether the progress bar is to be used or not.
39
40 We only want the progress bar to display for the brillo commands which operate
41 at logging level NOTICE. If the user wants to see the noisy output, then they
42 can execute the command at logging level INFO or DEBUG.
43 """
44 return logging.getLogger().getEffectiveLevel() == logging.NOTICE
45
46
David Pursellc7ba7842015-07-08 10:48:41 -070047def _FindModules(subdir_path):
48 """Returns a list of subcommand python modules in |subdir_path|.
David Pursellffb90042015-03-23 09:21:41 -070049
50 Args:
51 subdir_path: directory (string) to search for modules in.
David Pursellffb90042015-03-23 09:21:41 -070052
53 Returns:
54 List of filenames (strings).
55 """
David Pursellf1c27c12015-03-18 09:51:38 -070056 modules = []
David Pursellc7ba7842015-07-08 10:48:41 -070057 glob_path = os.path.join(subdir_path, '%s*.py' % _SUBCOMMAND_MODULE_PREFIX)
58 for file_path in glob.glob(glob_path):
David Pursellf1c27c12015-03-18 09:51:38 -070059 if not file_path.endswith('_unittest.py'):
60 modules.append(file_path)
David Pursellf1c27c12015-03-18 09:51:38 -070061 return modules
62
63
David Pursellc7ba7842015-07-08 10:48:41 -070064def _ImportCommands():
65 """Directly imports all subcommand python modules.
David Pursellf1c27c12015-03-18 09:51:38 -070066
David Pursellc7ba7842015-07-08 10:48:41 -070067 This method imports the modules which may contain subcommands. When
68 these modules are loaded, declared commands (those that use
David Pursellffb90042015-03-23 09:21:41 -070069 CommandDecorator) will automatically get added to |_commands|.
David Pursellf1c27c12015-03-18 09:51:38 -070070 """
David Pursellc7ba7842015-07-08 10:48:41 -070071 for file_path in _FindModules(_SUBCOMMAND_MODULE_DIRECTORY):
72 module_path = os.path.splitext(file_path)[0]
73 import_path = os.path.relpath(os.path.realpath(module_path),
74 os.path.dirname(constants.CHROMITE_DIR))
75 cros_import.ImportModule(import_path.split(os.path.sep))
David Pursellf1c27c12015-03-18 09:51:38 -070076
77
David Pursellc7ba7842015-07-08 10:48:41 -070078def ListCommands():
David Pursellffb90042015-03-23 09:21:41 -070079 """Return a dictionary mapping command names to classes.
80
David Pursellffb90042015-03-23 09:21:41 -070081 Returns:
82 A dictionary mapping names (strings) to commands (classes).
83 """
David Pursellc7ba7842015-07-08 10:48:41 -070084 _ImportCommands()
David Pursellf1c27c12015-03-18 09:51:38 -070085 return _commands.copy()
86
87
Brian Harring984988f2012-10-10 22:53:30 -070088class InvalidCommandError(Exception):
89 """Error that occurs when command class fails sanity checks."""
90 pass
91
92
93def CommandDecorator(command_name):
94 """Decorator that sanity checks and adds class to list of usable commands."""
95
96 def InnerCommandDecorator(original_class):
Mike Frysinger07475fa2019-08-01 14:44:55 -040097 """Inner Decorator that actually wraps the class."""
Brian Harring984988f2012-10-10 22:53:30 -070098 if not hasattr(original_class, '__doc__'):
99 raise InvalidCommandError('All handlers must have docstrings: %s' %
100 original_class)
101
David Pursellffb90042015-03-23 09:21:41 -0700102 if not issubclass(original_class, CliCommand):
103 raise InvalidCommandError('All Commands must derive from CliCommand: %s' %
104 original_class)
Brian Harring984988f2012-10-10 22:53:30 -0700105
106 _commands[command_name] = original_class
Ryan Cui47f80e42013-04-01 19:01:54 -0700107 original_class.command_name = command_name
108
Brian Harring984988f2012-10-10 22:53:30 -0700109 return original_class
110
111 return InnerCommandDecorator
112
113
David Pursellffb90042015-03-23 09:21:41 -0700114class CliCommand(object):
115 """All CLI commands must derive from this class.
Brian Harring984988f2012-10-10 22:53:30 -0700116
David Pursellffb90042015-03-23 09:21:41 -0700117 This class provides the abstract interface for all CLI commands. When
Brian Harring984988f2012-10-10 22:53:30 -0700118 designing a new command, you must sub-class from this class and use the
119 CommandDecorator decorator. You must specify a class docstring as that will be
120 used as the usage for the sub-command.
121
122 In addition your command should implement AddParser which is passed in a
123 parser that you can add your own custom arguments. See argparse for more
124 information.
125 """
Ryo Hashimoto8bc997b2014-01-22 18:46:17 +0900126 # Indicates whether command uses cache related commandline options.
127 use_caching_options = False
128
Brian Harring984988f2012-10-10 22:53:30 -0700129 def __init__(self, options):
130 self.options = options
131
132 @classmethod
133 def AddParser(cls, parser):
134 """Add arguments for this command to the parser."""
David Pursellffb90042015-03-23 09:21:41 -0700135 parser.set_defaults(command_class=cls)
Brian Harring984988f2012-10-10 22:53:30 -0700136
David Pursellf80b8c52015-04-03 12:46:45 -0700137 @classmethod
David Pursellc7ba7842015-07-08 10:48:41 -0700138 def AddDeviceArgument(cls, parser, schemes=commandline.DEVICE_SCHEME_SSH):
David Pursellf80b8c52015-04-03 12:46:45 -0700139 """Add a device argument to the parser.
140
David Pursellc7ba7842015-07-08 10:48:41 -0700141 This standardizes the help message across all subcommands.
David Pursellf80b8c52015-04-03 12:46:45 -0700142
143 Args:
144 parser: The parser to add the device argument to.
145 schemes: List of device schemes or single scheme to allow.
David Pursellf80b8c52015-04-03 12:46:45 -0700146 """
David Pursellf80b8c52015-04-03 12:46:45 -0700147 help_strings = []
148 schemes = list(cros_build_lib.iflatten_instance(schemes))
149 if commandline.DEVICE_SCHEME_SSH in schemes:
Mike Frysinger7163c3a2018-02-08 16:45:10 -0500150 help_strings.append('Target a device with [user@]hostname[:port]. '
151 'IPv4/IPv6 addresses are allowed, but IPv6 must '
152 'use brackets (e.g. [::1]).')
David Pursellf80b8c52015-04-03 12:46:45 -0700153 if commandline.DEVICE_SCHEME_USB in schemes:
154 help_strings.append('Target removable media with usb://[path].')
155 if commandline.DEVICE_SCHEME_FILE in schemes:
156 help_strings.append('Target a local file with file://path.')
David Pursellc7ba7842015-07-08 10:48:41 -0700157 parser.add_argument('device',
David Pursellf80b8c52015-04-03 12:46:45 -0700158 type=commandline.DeviceParser(schemes),
159 help=' '.join(help_strings))
160
Brian Harring984988f2012-10-10 22:53:30 -0700161 def Run(self):
162 """The command to run."""
163 raise NotImplementedError()