#
# 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