# # Copyright (c) 2018 Red Hat, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301, USA. # """ Command - runs commands and manages their results $Id: //eng/vdo-releases/magnesium/src/python/vdo/utils/Command.py#3 $ """ from .Logger import Logger from subprocess import Popen, PIPE import copy import logging import os import pipes import time class CommandError(Exception): """Exception raised to indicate an error running a command. Arguments: exitCode (int): The exit code of the command stdout (string): The output from the command stderr (string): The error output from the command message (string): The (localized) error text; will be formatted with the remaining arguments args: Arguments for formatting the message """ ###################################################################### # Public methods ###################################################################### def getExitCode(self): return self._exitCode ###################################################################### def getStandardError(self): return self._stderr ###################################################################### def logOutputs(self, logMethod): """Log the outputs of the failed command which generated this exception. Arguments: logMethod (callable): The method to log with """ logMethod(self._stdout) logMethod(self._stderr) ###################################################################### def setMessage(self, message, *args): """Set the error message in this exception. Arguments: message (string): The (localized) message text; will be formatted with *args args: Values to pass to the format of message """ self._message = message.format(*args) ###################################################################### # Overridden methods ###################################################################### def __init__(self, exitCode, stdout, stderr, message, *args): super(CommandError, self).__init__() self._exitCode = exitCode self._stdout = stdout self._stderr = stderr self.setMessage(message, args) ###################################################################### def __str__(self): return self._message ######################################################################## class Command(object): """Command encapsulates shell commands, runs them, and manages the result. Attributes: noRun (bool): if True, don't run the command, and always succeed shell (bool): if True, run this command using shell -c verbose (int): if > 0, print commands to stdout before executing them _commandList (list): command and its arguments """ defaultNoRun = False defaultVerbose = 0 log = logging.getLogger(Logger.myname + '.Command') ###################################################################### # Public methods ###################################################################### @classmethod def noRunMode(cls): """Returns True iff Commands default to noRun.""" return cls.defaultNoRun ###################################################################### @classmethod def setDefaults(cls, verbose = False, noRun = False): """Sets the verbose and noRun default values. Arguments: verbose: (boolean) If True, operate verbosely. noRun: (boolean) If True, do not actually execute. """ if noRun: cls.defaultNoRun = noRun cls.defaultVerbose = True if verbose: cls.defaultVerbose = verbose ###################################################################### def commandName(self): """Returns an identifier (argv[0]) for error messages.""" return self._commandList[0] ###################################################################### def run(self, **kwargs): """Run a command. Returns the output of running the command. Arguments: noThrow: If True, will return an empty string instead of throwing on error. retries: The number of times to try the command before giving up. Defaults to 1. shell: Indicate that this is a shell command stdin: If not None, the stream from which the command should take its input, defaults to None. strip: If True, strip leading and trailing whitespace from the command output before returning it. Exceptions: CommandError: if the command failed and noThrow is False """ retries = kwargs.get('retries', 1) stdin = kwargs.get('stdin', None) if not self.shell: self.shell = kwargs.get('shell', False) commandLine = self._getCommandLine() if retries > 1: self.log.debug("Waiting for '{0}'".format(commandLine)) try: for count in range(retries): if retries > 1: self.log.debug(" ... {0}/{1}".format(count, retries)) if self.verbose > 0: print(' ' + commandLine) self.log.info(commandLine) if self.noRun: return try: output = self._execute(stdin) return output.strip() if kwargs.get('strip', False) else output except CommandError as e: if count == (retries - 1): if retries > 1: e.setMessage(_("{0}: timed out after {1} seconds"), self.commandName(), retries) raise e time.sleep(1) except CommandError as e: if kwargs.get('noThrow', False): return '' raise e ###################################################################### # Overridden methods ###################################################################### def __init__(self, commandList, environment=None): super(Command, self).__init__() self.noRun = Command.defaultNoRun self.shell = False self.verbose = Command.defaultVerbose self._commandList = commandList if environment: self.env = copy.deepcopy(os.environ) for var, value in environment.items(): self.env[var] = value else: self.env = None ###################################################################### def __str__(self): ' '.join(self._commandList) ###################################################################### # Protected methods ###################################################################### def _checkResults(self, exitCode=0, stdout='', stderr=''): """Sets the result values of this object. Raises an exception if there was an error, or returns the output of the command if there was not. Arguments: exitCode (int): the process exit code stdout (str): the standard output stderr (str): the standard error logResults (bool): if True, the results will be logged Exceptions: CommandError: if exitCode is non-zero """ try: if (exitCode == 0): self.log.debug(_("{0}: command succeeded").format(self.commandName())) return stdout failureType = _('exit status') if exitCode > 0 else _('signal') status = _("{0}: command failed, {1} {2}").format(self.commandName(), failureType, abs(exitCode)) self.log.debug(status) raise CommandError(exitCode, stdout, stderr, status) finally: self.log.debug('stdout: ' + stdout.rstrip()) self.log.debug('stderr: ' + stderr.rstrip()) ###################################################################### def _execute(self, stdin): """Execute the command once. Returns the output of the command. Arguments: stdin: If not None, the stream from which the command should take its input. Exceptions: CommandError: if the command failed """ command = self._getCommandLine() if self.shell else self._commandList try: p = Popen(command, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True, env=self.env, shell=self.shell) stdoutdata, stderrdata = p.communicate(stdin) return self._checkResults(p.returncode, "".join(stdoutdata), "".join(stderrdata)) except OSError as e: self._checkResults(e.errno, '', ': '.join([self.commandName(), e.strerror])) except CommandError as e: error = e._stderr.split(os.linesep)[0] if error: e.setMessage(error) raise e ###################################################################### def _getCommandLine(self): """Returns the appropriately quoted command line.""" return ' '.join(self._commandList if self.shell else map(pipes.quote, self._commandList)) ######################################################################## def runCommand(commandList, **kwargs): """Run a command. Returns the output of the command (but see Keyword Arguments). Arguments: commandList: The command as a list of strings. Keyword Arguments: environment: A dict of environment variables and their values to use for the command. noThrow: If True, will return an empty string instead of throwing on error. retries: The number of times to try the command before giving up. Defaults to 1. shell: Indicate that this is a shell command. stdin: If not None, the stream from which the command should take its input, defaults to None. strip: If True, strip leading and trailing whitespace from the command output before returning it. Exceptions: CommandError: if the command failed and noThrow is False """ return Command(commandList, kwargs.pop('environment', None)).run(**kwargs) ######################################################################## def tryCommandsUntilSuccess(commands, **kwargs): """Try each of a series of commands in turn until one succeeds. If all the commands fail, give up and raise an exception. Arguments: commands: A list of command lists Keyword Arguments: Supports all of the arguments which may be passed to runCommand(). Returns: the output of the first successful command Exceptions: CommandError: if none of the commands succeeds and the noThrow keyword option is False (or omitted); the error will be the one raised by the last command in the list """ error = None noThrow = kwargs.pop('noThrow', False) for command in commands: try: return runCommand(command, **kwargs) except CommandError as e: error = e if noThrow: error = None # Pylint thinks we can raise None here. if error is not None: #pylint: disable=E0702 raise error