Merge "Update processutils."

This commit is contained in:
Jenkins 2013-05-03 07:40:44 +00:00 committed by Gerrit Code Review
commit 3ebead64a7
2 changed files with 141 additions and 12 deletions

View File

@ -19,8 +19,10 @@
System-level utilities and helper functions. System-level utilities and helper functions.
""" """
import os
import random import random
import shlex import shlex
import signal
from eventlet.green import subprocess from eventlet.green import subprocess
from eventlet import greenthread from eventlet import greenthread
@ -40,6 +42,12 @@ class UnknownArgumentError(Exception):
class ProcessExecutionError(Exception): class ProcessExecutionError(Exception):
def __init__(self, stdout=None, stderr=None, exit_code=None, cmd=None, def __init__(self, stdout=None, stderr=None, exit_code=None, cmd=None,
description=None): description=None):
self.exit_code = exit_code
self.stderr = stderr
self.stdout = stdout
self.cmd = cmd
self.description = description
if description is None: if description is None:
description = "Unexpected error while running command." description = "Unexpected error while running command."
if exit_code is None: if exit_code is None:
@ -49,6 +57,17 @@ class ProcessExecutionError(Exception):
super(ProcessExecutionError, self).__init__(message) super(ProcessExecutionError, self).__init__(message)
class NoRootWrapSpecified(Exception):
def __init__(self, message=None):
super(NoRootWrapSpecified, self).__init__(message)
def _subprocess_setup():
# Python installs a SIGPIPE handler by default. This is usually not what
# non-Python subprocesses expect.
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
def execute(*cmd, **kwargs): def execute(*cmd, **kwargs):
""" """
Helper method to shell out and execute a command through subprocess with Helper method to shell out and execute a command through subprocess with
@ -58,11 +77,11 @@ def execute(*cmd, **kwargs):
:type cmd: string :type cmd: string
:param process_input: Send to opened process. :param process_input: Send to opened process.
:type proces_input: string :type proces_input: string
:param check_exit_code: Defaults to 0. Will raise :param check_exit_code: Single bool, int, or list of allowed exit
:class:`ProcessExecutionError` codes. Defaults to [0]. Raise
if the command exits without returning this value :class:`ProcessExecutionError` unless
as a returncode program exits with one of these code.
:type check_exit_code: int :type check_exit_code: boolean, int, or [int]
:param delay_on_retry: True | False. Defaults to True. If set to True, :param delay_on_retry: True | False. Defaults to True. If set to True,
wait a short amount of time before retrying. wait a short amount of time before retrying.
:type delay_on_retry: boolean :type delay_on_retry: boolean
@ -72,8 +91,12 @@ def execute(*cmd, **kwargs):
the command is prefixed by the command specified the command is prefixed by the command specified
in the root_helper kwarg. in the root_helper kwarg.
:type run_as_root: boolean :type run_as_root: boolean
:param root_helper: command to prefix all cmd's with :param root_helper: command to prefix to commands called with
run_as_root=True
:type root_helper: string :type root_helper: string
:param shell: whether or not there should be a shell used to
execute this command. Defaults to false.
:type shell: boolean
:returns: (stdout, stderr) from process execution :returns: (stdout, stderr) from process execution
:raises: :class:`UnknownArgumentError` on :raises: :class:`UnknownArgumentError` on
receiving unknown arguments receiving unknown arguments
@ -81,16 +104,31 @@ def execute(*cmd, **kwargs):
""" """
process_input = kwargs.pop('process_input', None) process_input = kwargs.pop('process_input', None)
check_exit_code = kwargs.pop('check_exit_code', 0) check_exit_code = kwargs.pop('check_exit_code', [0])
ignore_exit_code = False
delay_on_retry = kwargs.pop('delay_on_retry', True) delay_on_retry = kwargs.pop('delay_on_retry', True)
attempts = kwargs.pop('attempts', 1) attempts = kwargs.pop('attempts', 1)
run_as_root = kwargs.pop('run_as_root', False) run_as_root = kwargs.pop('run_as_root', False)
root_helper = kwargs.pop('root_helper', '') root_helper = kwargs.pop('root_helper', '')
shell = kwargs.pop('shell', False)
if isinstance(check_exit_code, bool):
ignore_exit_code = not check_exit_code
check_exit_code = [0]
elif isinstance(check_exit_code, int):
check_exit_code = [check_exit_code]
if len(kwargs): if len(kwargs):
raise UnknownArgumentError(_('Got unknown keyword args ' raise UnknownArgumentError(_('Got unknown keyword args '
'to utils.execute: %r') % kwargs) 'to utils.execute: %r') % kwargs)
if run_as_root:
if run_as_root and os.geteuid() != 0:
if not root_helper:
raise NoRootWrapSpecified(
message=('Command requested root, but did not specify a root '
'helper.'))
cmd = shlex.split(root_helper) + list(cmd) cmd = shlex.split(root_helper) + list(cmd)
cmd = map(str, cmd) cmd = map(str, cmd)
while attempts > 0: while attempts > 0:
@ -98,11 +136,21 @@ def execute(*cmd, **kwargs):
try: try:
LOG.debug(_('Running cmd (subprocess): %s'), ' '.join(cmd)) LOG.debug(_('Running cmd (subprocess): %s'), ' '.join(cmd))
_PIPE = subprocess.PIPE # pylint: disable=E1101 _PIPE = subprocess.PIPE # pylint: disable=E1101
if os.name == 'nt':
preexec_fn = None
close_fds = False
else:
preexec_fn = _subprocess_setup
close_fds = True
obj = subprocess.Popen(cmd, obj = subprocess.Popen(cmd,
stdin=_PIPE, stdin=_PIPE,
stdout=_PIPE, stdout=_PIPE,
stderr=_PIPE, stderr=_PIPE,
close_fds=True) close_fds=close_fds,
preexec_fn=preexec_fn,
shell=shell)
result = None result = None
if process_input is not None: if process_input is not None:
result = obj.communicate(process_input) result = obj.communicate(process_input)
@ -112,9 +160,7 @@ def execute(*cmd, **kwargs):
_returncode = obj.returncode # pylint: disable=E1101 _returncode = obj.returncode # pylint: disable=E1101
if _returncode: if _returncode:
LOG.debug(_('Result was %s') % _returncode) LOG.debug(_('Result was %s') % _returncode)
if (isinstance(check_exit_code, int) and if not ignore_exit_code and _returncode not in check_exit_code:
not isinstance(check_exit_code, bool) and
_returncode != check_exit_code):
(stdout, stderr) = result (stdout, stderr) = result
raise ProcessExecutionError(exit_code=_returncode, raise ProcessExecutionError(exit_code=_returncode,
stdout=stdout, stdout=stdout,

View File

@ -17,6 +17,9 @@
from __future__ import print_function from __future__ import print_function
import os
import tempfile
from openstack.common import processutils from openstack.common import processutils
from tests import utils from tests import utils
@ -81,3 +84,83 @@ class ProcessExecutionErrorTest(utils.BaseTestCase):
stderr = 'Cottonian library' stderr = 'Cottonian library'
err = processutils.ProcessExecutionError(stderr=stderr) err = processutils.ProcessExecutionError(stderr=stderr)
self.assertTrue(stderr in str(err.message)) self.assertTrue(stderr in str(err.message))
def test_retry_on_failure(self):
fd, tmpfilename = tempfile.mkstemp()
_, tmpfilename2 = tempfile.mkstemp()
try:
fp = os.fdopen(fd, 'w+')
fp.write('''#!/bin/sh
# If stdin fails to get passed during one of the runs, make a note.
if ! grep -q foo
then
echo 'failure' > "$1"
fi
# If stdin has failed to get passed during this or a previous run, exit early.
if grep failure "$1"
then
exit 1
fi
runs="$(cat $1)"
if [ -z "$runs" ]
then
runs=0
fi
runs=$(($runs + 1))
echo $runs > "$1"
exit 1
''')
fp.close()
os.chmod(tmpfilename, 0755)
self.assertRaises(processutils.ProcessExecutionError,
processutils.execute,
tmpfilename, tmpfilename2, attempts=10,
process_input='foo',
delay_on_retry=False)
fp = open(tmpfilename2, 'r')
runs = fp.read()
fp.close()
self.assertNotEquals(runs.strip(), 'failure', 'stdin did not '
'always get passed '
'correctly')
runs = int(runs.strip())
self.assertEquals(runs, 10,
'Ran %d times instead of 10.' % (runs,))
finally:
os.unlink(tmpfilename)
os.unlink(tmpfilename2)
def test_unknown_kwargs_raises_error(self):
self.assertRaises(processutils.UnknownArgumentError,
processutils.execute,
'/usr/bin/env', 'true',
this_is_not_a_valid_kwarg=True)
def test_check_exit_code_boolean(self):
processutils.execute('/usr/bin/env', 'false', check_exit_code=False)
self.assertRaises(processutils.ProcessExecutionError,
processutils.execute,
'/usr/bin/env', 'false', check_exit_code=True)
def test_no_retry_on_success(self):
fd, tmpfilename = tempfile.mkstemp()
_, tmpfilename2 = tempfile.mkstemp()
try:
fp = os.fdopen(fd, 'w+')
fp.write("""#!/bin/sh
# If we've already run, bail out.
grep -q foo "$1" && exit 1
# Mark that we've run before.
echo foo > "$1"
# Check that stdin gets passed correctly.
grep foo
""")
fp.close()
os.chmod(tmpfilename, 0755)
processutils.execute(tmpfilename,
tmpfilename2,
process_input='foo',
attempts=2)
finally:
os.unlink(tmpfilename)
os.unlink(tmpfilename2)