blob: 5b924893135fdec1a2dd0c08f74ee4351bc875ac [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 os
20
Aviv Keshetb7519e12016-10-04 00:50:00 -070021from chromite.lib 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
Mike Frysinger1a470812019-11-07 01:19:17 -050046def ImportCommand(name):
47 """Directly import the specified subcommand.
48
49 This method imports the module which must contain the single subcommand. When
50 the module is loaded, the declared command (those that use CommandDecorator)
51 will automatically get added to |_commands|.
David Pursellffb90042015-03-23 09:21:41 -070052
53 Args:
Mike Frysinger1a470812019-11-07 01:19:17 -050054 name: The subcommand to load.
David Pursellffb90042015-03-23 09:21:41 -070055
56 Returns:
Mike Frysinger1a470812019-11-07 01:19:17 -050057 A reference to the subcommand class.
David Pursellffb90042015-03-23 09:21:41 -070058 """
Mike Frysinger1a470812019-11-07 01:19:17 -050059 module_path = os.path.join(_SUBCOMMAND_MODULE_DIRECTORY,
60 'cros_%s' % (name.replace('-', '_'),))
61 import_path = os.path.relpath(os.path.realpath(module_path),
62 os.path.dirname(constants.CHROMITE_DIR))
63 cros_import.ImportModule(import_path.split(os.path.sep))
64 return _commands[name]
David Pursellf1c27c12015-03-18 09:51:38 -070065
66
David Pursellc7ba7842015-07-08 10:48:41 -070067def ListCommands():
Mike Frysinger1a470812019-11-07 01:19:17 -050068 """Return the set of available subcommands.
David Pursellffb90042015-03-23 09:21:41 -070069
Mike Frysinger1a470812019-11-07 01:19:17 -050070 We assume that there is a direct one-to-one relationship between the module
71 name on disk and the command that module implements. We assume this as a
72 performance requirement (to avoid importing every subcommand every time even
73 though we'd only ever run a single one), and to avoid 3rd party module usage
74 in one subcommand breaking all other subcommands (not a great solution).
David Pursellffb90042015-03-23 09:21:41 -070075 """
Mike Frysinger1a470812019-11-07 01:19:17 -050076 # Filenames use underscores due to python naming limitations, but subcommands
77 # use dashes as they're easier for humans to type.
78 # Strip off the leading "cros_" and the trailing ".py".
79 return set(x[5:-3].replace('_', '-')
80 for x in os.listdir(_SUBCOMMAND_MODULE_DIRECTORY)
81 if (x.startswith(_SUBCOMMAND_MODULE_PREFIX) and x.endswith('.py')
82 and not x.endswith('_unittest.py')))
David Pursellf1c27c12015-03-18 09:51:38 -070083
84
Brian Harring984988f2012-10-10 22:53:30 -070085class InvalidCommandError(Exception):
86 """Error that occurs when command class fails sanity checks."""
Brian Harring984988f2012-10-10 22:53:30 -070087
88
89def CommandDecorator(command_name):
90 """Decorator that sanity checks and adds class to list of usable commands."""
91
92 def InnerCommandDecorator(original_class):
Mike Frysinger07475fa2019-08-01 14:44:55 -040093 """Inner Decorator that actually wraps the class."""
Brian Harring984988f2012-10-10 22:53:30 -070094 if not hasattr(original_class, '__doc__'):
95 raise InvalidCommandError('All handlers must have docstrings: %s' %
96 original_class)
97
David Pursellffb90042015-03-23 09:21:41 -070098 if not issubclass(original_class, CliCommand):
99 raise InvalidCommandError('All Commands must derive from CliCommand: %s' %
100 original_class)
Brian Harring984988f2012-10-10 22:53:30 -0700101
102 _commands[command_name] = original_class
Ryan Cui47f80e42013-04-01 19:01:54 -0700103 original_class.command_name = command_name
104
Brian Harring984988f2012-10-10 22:53:30 -0700105 return original_class
106
107 return InnerCommandDecorator
108
109
David Pursellffb90042015-03-23 09:21:41 -0700110class CliCommand(object):
111 """All CLI commands must derive from this class.
Brian Harring984988f2012-10-10 22:53:30 -0700112
David Pursellffb90042015-03-23 09:21:41 -0700113 This class provides the abstract interface for all CLI commands. When
Brian Harring984988f2012-10-10 22:53:30 -0700114 designing a new command, you must sub-class from this class and use the
115 CommandDecorator decorator. You must specify a class docstring as that will be
116 used as the usage for the sub-command.
117
118 In addition your command should implement AddParser which is passed in a
119 parser that you can add your own custom arguments. See argparse for more
120 information.
121 """
Ryo Hashimoto8bc997b2014-01-22 18:46:17 +0900122 # Indicates whether command uses cache related commandline options.
123 use_caching_options = False
124
Brian Harring984988f2012-10-10 22:53:30 -0700125 def __init__(self, options):
126 self.options = options
127
128 @classmethod
129 def AddParser(cls, parser):
130 """Add arguments for this command to the parser."""
David Pursellffb90042015-03-23 09:21:41 -0700131 parser.set_defaults(command_class=cls)
Brian Harring984988f2012-10-10 22:53:30 -0700132
David Pursellf80b8c52015-04-03 12:46:45 -0700133 @classmethod
David Pursellc7ba7842015-07-08 10:48:41 -0700134 def AddDeviceArgument(cls, parser, schemes=commandline.DEVICE_SCHEME_SSH):
David Pursellf80b8c52015-04-03 12:46:45 -0700135 """Add a device argument to the parser.
136
David Pursellc7ba7842015-07-08 10:48:41 -0700137 This standardizes the help message across all subcommands.
David Pursellf80b8c52015-04-03 12:46:45 -0700138
139 Args:
140 parser: The parser to add the device argument to.
141 schemes: List of device schemes or single scheme to allow.
David Pursellf80b8c52015-04-03 12:46:45 -0700142 """
David Pursellf80b8c52015-04-03 12:46:45 -0700143 help_strings = []
144 schemes = list(cros_build_lib.iflatten_instance(schemes))
145 if commandline.DEVICE_SCHEME_SSH in schemes:
Mike Frysinger7163c3a2018-02-08 16:45:10 -0500146 help_strings.append('Target a device with [user@]hostname[:port]. '
147 'IPv4/IPv6 addresses are allowed, but IPv6 must '
148 'use brackets (e.g. [::1]).')
David Pursellf80b8c52015-04-03 12:46:45 -0700149 if commandline.DEVICE_SCHEME_USB in schemes:
150 help_strings.append('Target removable media with usb://[path].')
151 if commandline.DEVICE_SCHEME_FILE in schemes:
152 help_strings.append('Target a local file with file://path.')
David Pursellc7ba7842015-07-08 10:48:41 -0700153 parser.add_argument('device',
David Pursellf80b8c52015-04-03 12:46:45 -0700154 type=commandline.DeviceParser(schemes),
155 help=' '.join(help_strings))
156
Brian Harring984988f2012-10-10 22:53:30 -0700157 def Run(self):
158 """The command to run."""
159 raise NotImplementedError()