blob: 555906131a3143385ccfcb8b1413d4701849e503 [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
Alex Klein665aa072019-12-12 15:13:53 -070019import importlib
David Pursellf1c27c12015-03-18 09:51:38 -070020import 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
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))
Alex Klein665aa072019-12-12 15:13:53 -070063 module_parts = import_path.split(os.path.sep)
64 importlib.import_module('.'.join(module_parts))
Mike Frysinger1a470812019-11-07 01:19:17 -050065 return _commands[name]
David Pursellf1c27c12015-03-18 09:51:38 -070066
67
David Pursellc7ba7842015-07-08 10:48:41 -070068def ListCommands():
Mike Frysinger1a470812019-11-07 01:19:17 -050069 """Return the set of available subcommands.
David Pursellffb90042015-03-23 09:21:41 -070070
Mike Frysinger1a470812019-11-07 01:19:17 -050071 We assume that there is a direct one-to-one relationship between the module
72 name on disk and the command that module implements. We assume this as a
73 performance requirement (to avoid importing every subcommand every time even
74 though we'd only ever run a single one), and to avoid 3rd party module usage
75 in one subcommand breaking all other subcommands (not a great solution).
David Pursellffb90042015-03-23 09:21:41 -070076 """
Mike Frysinger1a470812019-11-07 01:19:17 -050077 # Filenames use underscores due to python naming limitations, but subcommands
78 # use dashes as they're easier for humans to type.
79 # Strip off the leading "cros_" and the trailing ".py".
80 return set(x[5:-3].replace('_', '-')
81 for x in os.listdir(_SUBCOMMAND_MODULE_DIRECTORY)
82 if (x.startswith(_SUBCOMMAND_MODULE_PREFIX) and x.endswith('.py')
83 and not x.endswith('_unittest.py')))
David Pursellf1c27c12015-03-18 09:51:38 -070084
85
Brian Harring984988f2012-10-10 22:53:30 -070086class InvalidCommandError(Exception):
87 """Error that occurs when command class fails sanity checks."""
Brian Harring984988f2012-10-10 22:53:30 -070088
89
90def CommandDecorator(command_name):
91 """Decorator that sanity checks and adds class to list of usable commands."""
92
93 def InnerCommandDecorator(original_class):
Mike Frysinger07475fa2019-08-01 14:44:55 -040094 """Inner Decorator that actually wraps the class."""
Brian Harring984988f2012-10-10 22:53:30 -070095 if not hasattr(original_class, '__doc__'):
96 raise InvalidCommandError('All handlers must have docstrings: %s' %
97 original_class)
98
David Pursellffb90042015-03-23 09:21:41 -070099 if not issubclass(original_class, CliCommand):
100 raise InvalidCommandError('All Commands must derive from CliCommand: %s' %
101 original_class)
Brian Harring984988f2012-10-10 22:53:30 -0700102
103 _commands[command_name] = original_class
Ryan Cui47f80e42013-04-01 19:01:54 -0700104 original_class.command_name = command_name
105
Brian Harring984988f2012-10-10 22:53:30 -0700106 return original_class
107
108 return InnerCommandDecorator
109
110
David Pursellffb90042015-03-23 09:21:41 -0700111class CliCommand(object):
112 """All CLI commands must derive from this class.
Brian Harring984988f2012-10-10 22:53:30 -0700113
David Pursellffb90042015-03-23 09:21:41 -0700114 This class provides the abstract interface for all CLI commands. When
Brian Harring984988f2012-10-10 22:53:30 -0700115 designing a new command, you must sub-class from this class and use the
116 CommandDecorator decorator. You must specify a class docstring as that will be
117 used as the usage for the sub-command.
118
119 In addition your command should implement AddParser which is passed in a
120 parser that you can add your own custom arguments. See argparse for more
121 information.
122 """
Ryo Hashimoto8bc997b2014-01-22 18:46:17 +0900123 # Indicates whether command uses cache related commandline options.
124 use_caching_options = False
125
Brian Harring984988f2012-10-10 22:53:30 -0700126 def __init__(self, options):
127 self.options = options
128
129 @classmethod
130 def AddParser(cls, parser):
131 """Add arguments for this command to the parser."""
David Pursellffb90042015-03-23 09:21:41 -0700132 parser.set_defaults(command_class=cls)
Brian Harring984988f2012-10-10 22:53:30 -0700133
David Pursellf80b8c52015-04-03 12:46:45 -0700134 @classmethod
David Pursellc7ba7842015-07-08 10:48:41 -0700135 def AddDeviceArgument(cls, parser, schemes=commandline.DEVICE_SCHEME_SSH):
David Pursellf80b8c52015-04-03 12:46:45 -0700136 """Add a device argument to the parser.
137
David Pursellc7ba7842015-07-08 10:48:41 -0700138 This standardizes the help message across all subcommands.
David Pursellf80b8c52015-04-03 12:46:45 -0700139
140 Args:
141 parser: The parser to add the device argument to.
142 schemes: List of device schemes or single scheme to allow.
David Pursellf80b8c52015-04-03 12:46:45 -0700143 """
David Pursellf80b8c52015-04-03 12:46:45 -0700144 help_strings = []
145 schemes = list(cros_build_lib.iflatten_instance(schemes))
146 if commandline.DEVICE_SCHEME_SSH in schemes:
Mike Frysinger7163c3a2018-02-08 16:45:10 -0500147 help_strings.append('Target a device with [user@]hostname[:port]. '
148 'IPv4/IPv6 addresses are allowed, but IPv6 must '
149 'use brackets (e.g. [::1]).')
David Pursellf80b8c52015-04-03 12:46:45 -0700150 if commandline.DEVICE_SCHEME_USB in schemes:
151 help_strings.append('Target removable media with usb://[path].')
152 if commandline.DEVICE_SCHEME_FILE in schemes:
153 help_strings.append('Target a local file with file://path.')
David Pursellc7ba7842015-07-08 10:48:41 -0700154 parser.add_argument('device',
David Pursellf80b8c52015-04-03 12:46:45 -0700155 type=commandline.DeviceParser(schemes),
156 help=' '.join(help_strings))
157
Brian Harring984988f2012-10-10 22:53:30 -0700158 def Run(self):
159 """The command to run."""
160 raise NotImplementedError()