Logs from Beaker jobs are normally very brief, with the standard output/error containing detailed information. Make ipa-run-tests with BeakerLib plugin follow this convention. Only include INFO and higher level messages in the Beaker logs. Downgrade several message levels to DEBUG. Log to console using Python logging instead of showing the Beaker logs. Since ipa-run-tests sets up its own logging, Nose's own log handling just causes duplicate messages. Disable it with --nologcapture.
317 lines
11 KiB
Python
317 lines
11 KiB
Python
# Authors:
|
|
# Petr Viktorin <pviktori@redhat.com>
|
|
#
|
|
# Copyright (C) 2013 Red Hat
|
|
# see file 'COPYING' for use and warranty information
|
|
#
|
|
# 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 3 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, see <http://www.gnu.org/licenses/>.
|
|
|
|
"""Host class for integration testing"""
|
|
|
|
import os
|
|
import socket
|
|
import threading
|
|
import subprocess
|
|
import errno
|
|
|
|
import paramiko
|
|
|
|
from ipapython import ipautil
|
|
from ipapython.ipa_log_manager import log_mgr
|
|
|
|
|
|
class RemoteCommand(object):
|
|
"""A Popen-style object representing a remote command
|
|
|
|
Unlike subprocess.Popen, this does not run the given command; instead
|
|
it only starts a shell. The command must be written to stdin manually.
|
|
|
|
The standard error and output are handled by this class. They're not
|
|
available for file-like reading. They are logged by default.
|
|
To make sure reading doesn't stall after one buffer fills up, they are read
|
|
in parallel using threads.
|
|
|
|
After calling wait(), stdout_text and stderr_text attributes will be
|
|
strings containing the output, and returncode will contain the
|
|
exit code.
|
|
|
|
:param host: The Host on which the command is run
|
|
:param argv: The command that will be run (for logging only)
|
|
:param index: An identification number added to the logs
|
|
:param log_stdout: If false, stdout will not be logged
|
|
"""
|
|
def __init__(self, host, argv, index, log_stdout=True):
|
|
self.returncode = None
|
|
self.host = host
|
|
self.argv = argv
|
|
self._stdout_lines = []
|
|
self._stderr_lines = []
|
|
self.running_threads = set()
|
|
|
|
self.logger_name = '%s.cmd%s' % (self.host.logger_name, index)
|
|
self.log = log_mgr.get_logger(self.logger_name)
|
|
|
|
self.log.info('RUN %s', argv)
|
|
|
|
self._ssh = host.transport.open_channel('session')
|
|
|
|
self._ssh.invoke_shell()
|
|
stdin = self.stdin = self._ssh.makefile('wb')
|
|
stdout = self._ssh.makefile('rb')
|
|
stderr = self._ssh.makefile_stderr('rb')
|
|
|
|
self._start_pipe_thread(self._stdout_lines, stdout, 'out', log_stdout)
|
|
self._start_pipe_thread(self._stderr_lines, stderr, 'err', True)
|
|
|
|
self._done = False
|
|
|
|
def wait(self, raiseonerr=True):
|
|
"""Wait for the remote process to exit
|
|
|
|
Raises an excption if the exit code is not 0.
|
|
"""
|
|
if self._done:
|
|
return self.returncode
|
|
|
|
self._ssh.shutdown_write()
|
|
while self.running_threads:
|
|
self.running_threads.pop().join()
|
|
|
|
self.stdout_text = ''.join(self._stdout_lines)
|
|
self.stderr_text = ''.join(self._stderr_lines)
|
|
self.returncode = self._ssh.recv_exit_status()
|
|
self._ssh.close()
|
|
|
|
self._done = True
|
|
|
|
if raiseonerr and self.returncode:
|
|
self.log.error('Exit code: %s', self.returncode)
|
|
raise subprocess.CalledProcessError(self.returncode, self.argv)
|
|
else:
|
|
self.log.debug('Exit code: %s', self.returncode)
|
|
return self.returncode
|
|
|
|
def _start_pipe_thread(self, result_list, stream, name, do_log=True):
|
|
log = log_mgr.get_logger('%s.%s' % (self.logger_name, name))
|
|
|
|
def read_stream():
|
|
for line in stream:
|
|
if do_log:
|
|
log.debug(line.rstrip('\n'))
|
|
result_list.append(line)
|
|
|
|
thread = threading.Thread(target=read_stream)
|
|
self.running_threads.add(thread)
|
|
thread.start()
|
|
return thread
|
|
|
|
|
|
class Host(object):
|
|
"""Representation of a remote IPA host"""
|
|
def __init__(self, domain, hostname, role, index):
|
|
self.domain = domain
|
|
self.role = role
|
|
self.index = index
|
|
|
|
shortname, dot, ext_domain = hostname.partition('.')
|
|
self.shortname = shortname
|
|
self.hostname = shortname + '.' + self.domain.name
|
|
self.external_hostname = hostname
|
|
|
|
self.logger_name = '%s.%s.%s' % (
|
|
self.__module__, type(self).__name__, shortname)
|
|
self.log = log_mgr.get_logger(self.logger_name)
|
|
|
|
if self.config.ipv6:
|
|
# $(dig +short $M $rrtype|tail -1)
|
|
stdout, stderr, returncode = ipautil.run(
|
|
['dig', '+short', self.external_hostname, 'AAAA'])
|
|
self.ip = stdout.splitlines()[-1].strip()
|
|
else:
|
|
try:
|
|
self.ip = socket.gethostbyname(self.external_hostname)
|
|
except socket.gaierror:
|
|
self.ip = None
|
|
|
|
if not self.ip:
|
|
self.ip = ''
|
|
self.role = 'other'
|
|
|
|
self.root_password = self.config.root_password
|
|
self.root_ssh_key_filename = self.config.root_ssh_key_filename
|
|
self.host_key = None
|
|
self.ssh_port = 22
|
|
|
|
self.env_sh_path = os.path.join(domain.config.test_dir, 'env.sh')
|
|
|
|
self._command_index = 0
|
|
|
|
def __repr__(self):
|
|
template = ('<{s.__module__}.{s.__class__.__name__} '
|
|
'{s.hostname} ({s.role})>')
|
|
return template.format(s=self)
|
|
|
|
@classmethod
|
|
def from_env(cls, env, domain, hostname, role, index):
|
|
self = cls(domain, hostname, role, index)
|
|
return self
|
|
|
|
@property
|
|
def config(self):
|
|
return self.domain.config
|
|
|
|
def to_env(self, **kwargs):
|
|
"""Return environment variables specific to this host"""
|
|
env = self.domain.to_env(**kwargs)
|
|
|
|
role = self.role.upper()
|
|
if self.role != 'master':
|
|
role += str(self.index)
|
|
|
|
env['MYHOSTNAME'] = self.hostname
|
|
env['MYBEAKERHOSTNAME'] = self.external_hostname
|
|
env['MYIP'] = self.ip
|
|
|
|
env['MYROLE'] = '%s%s' % (role, self.domain._env)
|
|
env['MYENV'] = str(self.domain.index)
|
|
|
|
return env
|
|
|
|
def run_command(self, argv, set_env=True, stdin_text=None,
|
|
log_stdout=True, raiseonerr=True,
|
|
cwd=None):
|
|
"""Run the given command on this host
|
|
|
|
Returns a RemoteCommand instance. The command will have already run
|
|
when this method returns, so its stdout_text, stderr_text, and
|
|
returncode attributes will be available.
|
|
|
|
:param argv: Command to run, as either a Popen-style list, or a string
|
|
containing a shell script
|
|
:param set_env: If true, env.sh exporting configuration variables will
|
|
be sourced before running the command.
|
|
:param stdin_text: If given, will be written to the command's stdin
|
|
:param log_stdout: If false, standard output will not be logged
|
|
(but will still be available as cmd.stdout_text)
|
|
:param raiseonerr: If true, an exception will be raised if the command
|
|
does not exit with return code 0
|
|
"""
|
|
command = RemoteCommand(self, argv, index=self._command_index,
|
|
log_stdout=log_stdout)
|
|
self._command_index += 1
|
|
|
|
if cwd is None:
|
|
cwd = self.config.test_dir
|
|
command.stdin.write('cd %s\n' % ipautil.shell_quote(cwd))
|
|
|
|
if set_env:
|
|
command.stdin.write('. %s\n' %
|
|
ipautil.shell_quote(self.env_sh_path))
|
|
command.stdin.write('set -e\n')
|
|
|
|
if isinstance(argv, basestring):
|
|
command.stdin.write('(')
|
|
command.stdin.write(argv)
|
|
command.stdin.write(')')
|
|
else:
|
|
for arg in argv:
|
|
command.stdin.write(ipautil.shell_quote(arg))
|
|
command.stdin.write(' ')
|
|
command.stdin.write(';exit\n')
|
|
if stdin_text:
|
|
command.stdin.write(stdin_text)
|
|
command.stdin.flush()
|
|
|
|
command.wait(raiseonerr=raiseonerr)
|
|
return command
|
|
|
|
@property
|
|
def transport(self):
|
|
"""Paramiko Transport connected to this host"""
|
|
try:
|
|
return self._transport
|
|
except AttributeError:
|
|
sock = socket.create_connection((self.external_hostname,
|
|
self.ssh_port))
|
|
self._transport = transport = paramiko.Transport(sock)
|
|
transport.connect(hostkey=self.host_key)
|
|
if self.root_ssh_key_filename:
|
|
self.log.debug('Authenticating with private RSA key')
|
|
filename = os.path.expanduser(self.root_ssh_key_filename)
|
|
key = paramiko.RSAKey.from_private_key_file(filename)
|
|
transport.auth_publickey(username='root', key=key)
|
|
elif self.root_password:
|
|
self.log.debug('Authenticating with password')
|
|
transport.auth_password(username='root',
|
|
password=self.root_password)
|
|
else:
|
|
self.log.critical('No SSH credentials configured')
|
|
raise RuntimeError('No SSH credentials configured')
|
|
# Clean up the test directory
|
|
self.run_command(['rm', '-rvf', self.config.test_dir])
|
|
return transport
|
|
|
|
@property
|
|
def sftp(self):
|
|
"""Paramiko SFTPClient connected to this host"""
|
|
try:
|
|
return self._sftp
|
|
except AttributeError:
|
|
transport = self.transport
|
|
self._sftp = paramiko.SFTPClient.from_transport(transport)
|
|
return self._sftp
|
|
|
|
def mkdir_recursive(self, path):
|
|
"""`mkdir -p` on the remote host"""
|
|
try:
|
|
self.sftp.chdir(path or '/')
|
|
except IOError as e:
|
|
if not path or path == '/':
|
|
raise
|
|
self.mkdir_recursive(os.path.dirname(path))
|
|
self.sftp.mkdir(path)
|
|
self.sftp.chdir(path)
|
|
|
|
def get_file_contents(self, filename):
|
|
"""Read the named remote file and return the contents as a string"""
|
|
self.log.debug('READ %s', filename)
|
|
with self.sftp.open(filename) as f:
|
|
return f.read()
|
|
|
|
def put_file_contents(self, filename, contents):
|
|
"""Write the given string to the named remote file"""
|
|
self.log.info('WRITE %s', filename)
|
|
with self.sftp.open(filename, 'w') as f:
|
|
f.write(contents)
|
|
|
|
def file_exists(self, filename):
|
|
"""Return true if the named remote file exists"""
|
|
self.log.debug('STAT %s', filename)
|
|
try:
|
|
self.sftp.stat(filename)
|
|
except IOError, e:
|
|
if e.errno == errno.ENOENT:
|
|
return False
|
|
else:
|
|
raise
|
|
return True
|
|
|
|
def get_file(self, remotepath, localpath):
|
|
self.log.debug('GET %s', remotepath)
|
|
self.sftp.get(remotepath, localpath)
|
|
|
|
def put_file(self, localpath, remotepath):
|
|
self.log.info('PUT %s', remotepath)
|
|
self.sftp.put(localpath, remotepath)
|