未验证 提交 8c4f9cc9 编写于 作者: L Lukáš Doktor

Merging pull request 1331

* https://github.com/avocado-framework/avocado:
  avocado.core.runner: Remove the unused setup/tear_down methods
  avocado.core.remote: Style fixes in runner
  avocado.core.remote: Improve the `tear_down` execution
  avocado.core.remote: Improve VMTestRunner tear_down method
  avocado.core.remote: Handle urls without assigned files
  avocado.plugins: Add plugin to run job in docker container
......@@ -45,6 +45,11 @@ class RemoteTestRunner(TestRunner):
remote_version_re = re.compile(r'^Avocado (\d+)\.(\d+)$',
re.MULTILINE)
def __init__(self, job, test_result):
super(RemoteTestRunner, self).__init__(job, test_result)
#: remoter connection to the remote machine
self.remote = None
def _copy_files(self):
"""
Gather test directories and copy them recursively to
......@@ -61,6 +66,9 @@ class RemoteTestRunner(TestRunner):
url = self.job.urls[i]
if not os.path.exists(url): # use test_dir path + py
url = os.path.join(data_dir.get_test_dir(), url)
if not os.path.exists(url):
raise exceptions.JobError("Unable to map test id '%s' to file"
% self.job.urls[i])
url = os.path.abspath(url) # always use abspath; avoid clashes
# modify url to remote_path + abspath
paths.add(url)
......@@ -84,13 +92,14 @@ class RemoteTestRunner(TestRunner):
self.job.args.remote_hostname,
self.job.args.remote_port,
self.job.args.remote_timeout)
self.remote = remoter.Remote(hostname=self.job.args.remote_hostname,
username=self.job.args.remote_username,
password=self.job.args.remote_password,
key_filename=self.job.args.remote_key_file,
port=self.job.args.remote_port,
timeout=self.job.args.remote_timeout,
env_keep=self.job.args.env_keep)
self.remote = remoter.Remote(
hostname=self.job.args.remote_hostname,
username=self.job.args.remote_username,
password=self.job.args.remote_password,
key_filename=self.job.args.remote_key_file,
port=self.job.args.remote_port,
timeout=self.job.args.remote_timeout,
env_keep=self.job.args.env_keep)
def check_remote_avocado(self):
"""
......@@ -119,7 +128,7 @@ class RemoteTestRunner(TestRunner):
return (False, None)
try:
return (True, tuple(map(int, match[0])))
return (True, tuple(int(_) for _ in match[0]))
except IndexError:
return (False, None)
......@@ -182,7 +191,8 @@ class RemoteTestRunner(TestRunner):
return json_result
def run_suite(self, test_suite, mux, timeout, replay_map=None, test_result_total=0):
def run_suite(self, test_suite, mux, timeout=0, replay_map=None,
test_result_total=0):
"""
Run one or more tests and report with test result.
......@@ -203,6 +213,7 @@ class RemoteTestRunner(TestRunner):
fabric_debugfile = os.path.join(self.job.logdir, 'remote.log')
paramiko_logger = logging.getLogger('paramiko')
fabric_logger = logging.getLogger('avocado.fabric')
remote_logger = logging.getLogger('avocado.remote')
app_logger = logging.getLogger('avocado.debug')
fmt = ('%(asctime)s %(module)-10.10s L%(lineno)-.4d %('
'levelname)-5.5s| %(message)s')
......@@ -211,6 +222,7 @@ class RemoteTestRunner(TestRunner):
file_handler.setFormatter(formatter)
fabric_logger.addHandler(file_handler)
paramiko_logger.addHandler(file_handler)
remote_logger.addHandler(file_handler)
logger_list = [fabric_logger]
if self.job.args.show_job_log:
logger_list.append(app_logger)
......@@ -222,8 +234,8 @@ class RemoteTestRunner(TestRunner):
self.setup()
avocado_installed, _ = self.check_remote_avocado()
if not avocado_installed:
raise exceptions.JobError('Remote machine does not seem to have '
'avocado installed')
raise exceptions.JobError('Remote machine does not seem to'
' have avocado installed')
self._copy_files()
except Exception as details:
stacktrace.log_exc_info(sys.exc_info(), logger='avocado.test')
......@@ -258,20 +270,42 @@ class RemoteTestRunner(TestRunner):
archive.uncompress(zip_path_filename, local_log_dir)
os.remove(zip_path_filename)
self.result.end_tests()
finally:
try:
self.tear_down()
except Exception as details:
stacktrace.log_exc_info(sys.exc_info(), logger='avocado.test')
raise exceptions.JobError(details)
finally:
sys.stdout = stdout_backup
sys.stderr = stderr_backup
return summary
def tear_down(self):
"""
This method is only called when `run_suite` gets to the point of to be
executing `setup` method and is called at the end of the execution.
:warning: It might be called on `setup` exceptions, so things
initialized during `setup` might not yet be initialized.
"""
pass
class VMTestRunner(RemoteTestRunner):
"""
Test runner to run tests using libvirt domain
"""
def __init__(self, job, test_result):
super(VMTestRunner, self).__init__(job, test_result)
#: VM used during testing
self.vm = None
def setup(self):
"""
Initialize VM and establish connection
"""
# Super called after VM is found and initialized
self.job.log.info("DOMAIN : %s", self.job.args.vm_domain)
self.vm = virt.vm_connect(self.job.args.vm_domain,
......@@ -297,23 +331,24 @@ class VMTestRunner(RemoteTestRunner):
e_msg = ("Could not create snapshot on VM '%s'" %
self.job.args.vm_domain)
raise exceptions.JobError(e_msg)
try:
# Finish remote setup and copy the tests
self.job.args.remote_hostname = self.job.args.vm_hostname
self.job.args.remote_port = self.job.args.vm_port
self.job.args.remote_username = self.job.args.vm_username
self.job.args.remote_password = self.job.args.vm_password
self.job.args.remote_key_file = self.job.args.vm_key_file
self.job.args.remote_no_copy = self.job.args.vm_no_copy
self.job.args.remote_timeout = self.job.args.vm_timeout
super(VMTestRunner, self).setup()
except Exception:
self.tear_down()
raise
# Finish remote setup and copy the tests
self.job.args.remote_hostname = self.job.args.vm_hostname
self.job.args.remote_port = self.job.args.vm_port
self.job.args.remote_username = self.job.args.vm_username
self.job.args.remote_password = self.job.args.vm_password
self.job.args.remote_key_file = self.job.args.vm_key_file
self.job.args.remote_no_copy = self.job.args.vm_no_copy
self.job.args.remote_timeout = self.job.args.vm_timeout
super(VMTestRunner, self).setup()
def tear_down(self):
"""
Stop VM and restore snapshot (if asked for it)
"""
super(VMTestRunner, self).tear_down()
if self.job.args.vm_cleanup is True:
if (self.job.args.vm_cleanup is True and
isinstance(getattr(self, 'vm', None), virt.VM)):
self.vm.stop()
if self.vm.snapshot is not None:
self.vm.restore_snapshot()
self.vm = None
......@@ -314,18 +314,6 @@ class TestRunner(object):
except Exception:
instance.error(stacktrace.str_unpickable_object(state))
def setup(self):
"""
(Optional) initialization method for the test runner
"""
pass
def tear_down(self):
"""
(Optional) cleanup method for the test runner
"""
pass
def run_test(self, test_factory, queue, summary, job_deadline=0):
"""
Run a test instance inside a subprocess.
......
# 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 LICENSE for more details.
#
# Copyright: 2016 Red Hat, Inc.
# Author: Lukas Doktor <ldoktor@redhat.com>
"""Run the job inside a docker container."""
import logging
import time
import aexpect
from avocado.core.plugin_interfaces import CLI
from avocado.core.remote.runner import RemoteTestRunner
from avocado.utils import process
from avocado.utils.wait import wait_for
LOG = logging.getLogger('avocado.remote')
class DockerRemoter(object):
"""
Remoter object similar to `avocado.core.remoter` which implements subset
of the commands on docker container.
"""
def __init__(self, dkrcmd, image):
"""
Executes docker container and attaches it.
:param dkrcmd: The base docker binary (or command)
:param image: docker image to be used in this instance
"""
self._dkrcmd = dkrcmd
run_cmd = "%s run -t -i -d '%s' bash" % (self._dkrcmd, image)
self._docker_id = process.system_output(run_cmd, 10).strip()
self._docker = aexpect.ShellSession("%s attach %s"
% (self._dkrcmd, self._docker_id))
# Disable echo to avoid duplicate output
self._docker.cmd("stty -echo")
def get_cid(self):
""" Return this remoter's container ID """
return self._docker_id
def makedir(self, remote_path):
"""
Create a directory on the container
:warning: No other process must be running on foreground
:param remote_path: the remote path to create.
"""
self._docker.cmd("mkdir -p %s" % remote_path)
def send_files(self, local_path, remote_path):
"""
Send files to the container
"""
process.run("%s cp %s %s:%s" % (self._dkrcmd, local_path,
self._docker_id, remote_path))
def receive_files(self, local_path, remote_path):
"""
Receive files from the container
"""
process.run("%s cp %s:%s %s" % (self._dkrcmd, self._docker_id,
remote_path, local_path))
def run(self, command, ignore_status=False, quiet=None, timeout=60):
"""
Run command inside the container
"""
def print_func(*args, **kwargs): # pylint: disable=W0613
""" Accept anything and does nothing """
pass
if timeout is None:
timeout = 31536000 # aexpect does not support None, use one year
start = time.time()
if quiet is not False:
print_func = LOG.debug
status, output = self._docker.cmd_status_output(command,
timeout=timeout,
print_func=print_func)
result = process.CmdResult(command, output, '', status,
time.time() - start)
if status and not ignore_status:
raise process.CmdError(command, result, "in container %s"
% self._docker_id)
return result
def cleanup(self):
"""
Stop the container and remove it
"""
process.system("%s stop -t 1 %s" % (self._dkrcmd, self._docker_id))
process.system("%s rm %s" % (self._dkrcmd, self._docker_id))
def close(self):
"""
Safely postprocess the container
:note: It won't remove the container, you need to do it manually
"""
self._docker.sendline("exit")
# Leave the process up to 10s to finish, then nuke it
wait_for(lambda: not self._docker.is_alive(), 10)
self._docker.close()
class DockerTestRunner(RemoteTestRunner):
"""
Test runner which runs the job inside a docker container
"""
remote_test_dir = "/avocado_remote_test_dir" # Absolute path only
def __init__(self, job, test_result):
super(DockerTestRunner, self).__init__(job, test_result)
self.remote = None # Will be set in `setup`
def setup(self):
dkrcmd = self.job.args.docker_cmd
self.remote = DockerRemoter(dkrcmd, self.job.args.docker)
# We need to create the base dir, otherwise docker creates it as root
self.remote.makedir(self.remote_test_dir)
self.job.log.info("DOCKER : Container id '%s'"
% self.remote.get_cid())
self.job.args.remote_no_copy = self.job.args.docker_no_copy
def tear_down(self):
self.remote.close()
if not self.job.args.docker_no_cleanup:
self.remote.cleanup()
class Docker(CLI):
"""
Run the job inside a docker container
"""
name = 'docker'
description = "Run tests inside docker container"
def configure(self, parser):
run_subcommand_parser = parser.subcommands.choices.get('run', None)
if run_subcommand_parser is None:
return
msg = 'test execution inside docker container'
cmd_parser = run_subcommand_parser.add_argument_group(msg)
cmd_parser.add_argument("--docker", help="Name of the docker image to"
"run tests on.", metavar="IMAGE")
cmd_parser.add_argument("--docker-cmd", default="docker",
help="Override the docker command, eg. 'sudo "
"docker' or other base docker options like "
"hypervisor. Default: '%(default)s'",
metavar="CMD")
cmd_parser.add_argument("--docker-no-copy", action="store_true",
help="Assume tests are already in the "
"container")
cmd_parser.add_argument("--docker-no-cleanup", action="store_true",
help="Preserve container after test")
def run(self, args):
if getattr(args, "docker", None):
args.test_runner = DockerTestRunner
......@@ -147,6 +147,53 @@ execute them. A bit of extra logging information is added to your job summary,
mainly to distinguish the regular execution from the remote one. Note here that
we did not need `--vm-password` because the SSH key is already setup.
Running Tests on a Docker container
===================================
Avocado also lets you run tests on a Docker container, starting and
cleaning it up automatically with every execution.
You can check if this feature (a plugin) is enabled by running::
$ avocado plugins
...
docker Run tests inside docker container
...
Docker container images
-----------------------
Avocado needs to be present inside the container image in order for
the test execution to be properly performed. There's one ready to use
image (``ldoktor/fedora-avocado``) in the default image repository
(``docker.io``)::
$ docker pull ldoktor/fedora-avocado
Using default tag: latest
Trying to pull repository docker.io/ldoktor/fedora-avocado ...
latest: Pulling from docker.io/ldoktor/fedora-avocado
...
Status: Downloaded newer image for docker.io/ldoktor/fedora-avocado:latest
Running your test
-----------------
Assuming your system is properly setup to run Docker, including having
an image with Avocado, you can run a test inside the container with a
command similar to::
$ avocado run passtest.py warntest.py failtest.py --docker ldoktor/fedora-avocado
DOCKER : Container id '4bcbcd69801211501a0dde5926c0282a9630adbe29ecb17a21ef04f024366943'
JOB ID : db309f5daba562235834f97cad5f4458e3fe6e32
JOB LOG : $HOME/avocado/job-results/job-2016-07-25T08.01-db309f5/job.log
TESTS : 3
(1/3) /avocado_remote_test_dir/$HOME/passtest.py:PassTest.test: PASS (0.00 s)
(2/3) /avocado_remote_test_dir/$HOME/warntest.py:WarnTest.test: WARN (0.00 s)
(3/3) /avocado_remote_test_dir/$HOME/failtest.py:FailTest.test: FAIL (0.00 s)
RESULTS : PASS 1 | ERROR 0 | FAIL 1 | SKIP 0 | WARN 1 | INTERRUPT 0
JOB HTML : $HOME/avocado/job-results/job-2016-07-25T08.01-db309f5/html/results.html
TESTS TIME : 0.00 s
Environment Variables
=====================
......
......@@ -16,13 +16,23 @@ JSON_RESULTS = ('Something other than json\n'
'1}\nAdditional stuff other than json')
class _FakeVM(virt.VM):
"""
Fake VM-inherited object (it's better to inherit it, than to flexmock the
isinstance)
"""
def __init__(self): # don't call virt.VM.__init__ pylint: disable=W0231
self.snapshot = True
self.domain = flexmock(isActive=lambda: True)
class VMTestRunnerSetup(unittest.TestCase):
""" Tests the VMTestRunner setup() method """
def setUp(self):
mock_vm = flexmock(snapshot=True,
domain=flexmock(isActive=lambda: True))
mock_vm = flexmock(_FakeVM())
flexmock(virt).should_receive('vm_connect').and_return(mock_vm).once().ordered()
mock_vm.should_receive('start').and_return(True).once().ordered()
mock_vm.should_receive('create_snapshot').once().ordered()
......
......@@ -139,6 +139,7 @@ if __name__ == '__main__':
'replay = avocado.plugins.replay:Replay',
'tap = avocado.plugins.tap:TAP',
'vm = avocado.plugins.vm:VM',
'docker = avocado.plugins.docker:Docker',
],
'avocado.plugins.cli.cmd': [
'config = avocado.plugins.config:Config',
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册