f2e985e8fd
Enable sphinx to generate documentation from docstrings by running 'tox -e docs'. Change-Id: I5996e5f07493f69f14172b4bb0535852e89d5456
181 lines
6.5 KiB
Python
181 lines
6.5 KiB
Python
# Copyright (c) 2018 Red Hat, Inc.
|
|
#
|
|
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import collections
|
|
import subprocess
|
|
import sys
|
|
|
|
from oslo_log import log
|
|
from tempest.lib import exceptions as lib_exc
|
|
|
|
from neutron_tempest_plugin.common import ssh
|
|
from neutron_tempest_plugin import config
|
|
from neutron_tempest_plugin import exceptions
|
|
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
CONF = config.CONF
|
|
|
|
if ssh.Client.proxy_jump_host:
|
|
# Perform all SSH connections passing through configured SSH server
|
|
SSH_PROXY_CLIENT = ssh.Client.create_proxy_client()
|
|
else:
|
|
SSH_PROXY_CLIENT = None
|
|
|
|
|
|
def execute(command, ssh_client=None, timeout=None, check=True):
|
|
"""Execute command inside a remote or local shell
|
|
|
|
:param command: command string to be executed
|
|
|
|
:param ssh_client: SSH client instance used for remote shell execution
|
|
|
|
:param timeout: command execution timeout in seconds
|
|
|
|
:param check: when False it doesn't raises ShellCommandFailed when
|
|
exit status is not zero. True by default
|
|
|
|
:returns: STDOUT text when command execution terminates with zero exit
|
|
status.
|
|
|
|
:raises ShellTimeoutExpired: when timeout expires before command execution
|
|
terminates. In such case it kills the process, then it eventually would
|
|
try to read STDOUT and STDERR buffers (not fully implemented) before
|
|
raising the exception.
|
|
|
|
:raises ShellCommandFailed: when command execution terminates with non-zero
|
|
exit status.
|
|
"""
|
|
ssh_client = ssh_client or SSH_PROXY_CLIENT
|
|
if timeout:
|
|
timeout = float(timeout)
|
|
|
|
if ssh_client:
|
|
result = execute_remote_command(command=command, timeout=timeout,
|
|
ssh_client=ssh_client)
|
|
else:
|
|
result = execute_local_command(command=command, timeout=timeout)
|
|
|
|
if result.exit_status == 0:
|
|
LOG.debug("Command %r succeeded:\n"
|
|
"stderr:\n%s\n"
|
|
"stdout:\n%s\n",
|
|
command, result.stderr, result.stdout)
|
|
elif result.exit_status is None:
|
|
LOG.debug("Command %r timeout expired (timeout=%s):\n"
|
|
"stderr:\n%s\n"
|
|
"stdout:\n%s\n",
|
|
command, timeout, result.stderr, result.stdout)
|
|
else:
|
|
LOG.debug("Command %r failed (exit_status=%s):\n"
|
|
"stderr:\n%s\n"
|
|
"stdout:\n%s\n",
|
|
command, result.exit_status, result.stderr, result.stdout)
|
|
if check:
|
|
result.check()
|
|
|
|
return result
|
|
|
|
|
|
def execute_remote_command(command, ssh_client, timeout=None):
|
|
"""Execute command on a remote host using SSH client"""
|
|
LOG.debug("Executing command %r on remote host %r (timeout=%r)...",
|
|
command, ssh_client.host, timeout)
|
|
|
|
stdout = stderr = exit_status = None
|
|
|
|
try:
|
|
# TODO(fressi): re-implement to capture stderr
|
|
stdout = ssh_client.exec_command(command, timeout=timeout)
|
|
exit_status = 0
|
|
|
|
except lib_exc.TimeoutException:
|
|
# TODO(fressi): re-implement to capture STDOUT and STDERR and make
|
|
# sure process is killed
|
|
pass
|
|
|
|
except lib_exc.SSHExecCommandFailed as ex:
|
|
# Please note class SSHExecCommandFailed has been re-based on
|
|
# top of ShellCommandFailed
|
|
stdout = ex.stdout
|
|
stderr = ex.stderr
|
|
exit_status = ex.exit_status
|
|
|
|
return ShellExecuteResult(command=command, timeout=timeout,
|
|
exit_status=exit_status,
|
|
stdout=stdout, stderr=stderr)
|
|
|
|
|
|
def execute_local_command(command, timeout=None):
|
|
"""Execute command on local host using local shell"""
|
|
|
|
LOG.debug("Executing command %r on local host (timeout=%r)...",
|
|
command, timeout)
|
|
|
|
process = subprocess.Popen(command, shell=True,
|
|
universal_newlines=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
|
|
if timeout and sys.version_info < (3, 3):
|
|
# TODO(fressi): re-implement to timeout support on older Pythons
|
|
LOG.warning("Popen.communicate method doens't support for timeout "
|
|
"on Python %r", sys.version)
|
|
timeout = None
|
|
|
|
# Wait for process execution while reading STDERR and STDOUT streams
|
|
if timeout:
|
|
try:
|
|
stdout, stderr = process.communicate(timeout=timeout)
|
|
except subprocess.TimeoutExpired:
|
|
# At this state I expect the process to be still running
|
|
# therefore it has to be kill later after calling poll()
|
|
LOG.exception("Command %r timeout expired.", command)
|
|
stdout = stderr = None
|
|
else:
|
|
stdout, stderr = process.communicate()
|
|
|
|
# Check process termination status
|
|
exit_status = process.poll()
|
|
if exit_status is None:
|
|
# The process is still running after calling communicate():
|
|
# let kill it and then read buffers again
|
|
process.kill()
|
|
stdout, stderr = process.communicate()
|
|
|
|
return ShellExecuteResult(command=command, timeout=timeout,
|
|
stdout=stdout, stderr=stderr,
|
|
exit_status=exit_status)
|
|
|
|
|
|
class ShellExecuteResult(collections.namedtuple(
|
|
'ShellExecuteResult', ['command', 'timeout', 'exit_status', 'stdout',
|
|
'stderr'])):
|
|
|
|
def check(self):
|
|
if self.exit_status is None:
|
|
raise exceptions.ShellTimeoutExpired(command=self.command,
|
|
timeout=self.timeout,
|
|
stderr=self.stderr,
|
|
stdout=self.stdout)
|
|
|
|
elif self.exit_status != 0:
|
|
raise exceptions.ShellCommandFailed(command=self.command,
|
|
exit_status=self.exit_status,
|
|
stderr=self.stderr,
|
|
stdout=self.stdout)
|