diff --git a/avocado.spec b/avocado.spec index 5b52dce99e04daaa2be246c962d0ac16ff0f9375..738fcf079490e675f7c6d4317fe1a3804f19ab94 100644 --- a/avocado.spec +++ b/avocado.spec @@ -7,7 +7,7 @@ Summary: Avocado Test Framework Name: avocado Version: 0.34.0 -Release: 0%{?dist} +Release: 1%{?dist} License: GPLv2 Group: Development/Tools URL: http://avocado-framework.github.io/ @@ -61,12 +61,16 @@ selftests/run %dir /etc/avocado %dir /etc/avocado/conf.d %dir /etc/avocado/sysinfo +%dir /etc/avocado/scripts/job/pre.d +%dir /etc/avocado/scripts/job/post.d %config(noreplace)/etc/avocado/avocado.conf %config(noreplace)/etc/avocado/conf.d/README %config(noreplace)/etc/avocado/conf.d/gdb.conf %config(noreplace)/etc/avocado/sysinfo/commands %config(noreplace)/etc/avocado/sysinfo/files %config(noreplace)/etc/avocado/sysinfo/profilers +%config(noreplace)/etc/avocado/scripts/job/pre.d/README +%config(noreplace)/etc/avocado/scripts/job/post.d/README %{python_sitelib}/avocado* %{_bindir}/avocado %{_bindir}/avocado-rest-client @@ -110,6 +114,9 @@ examples of how to write tests on your own. %{_datadir}/avocado/wrappers %changelog +* Thu Apr 14 2016 Cleber Rosa - 0.34.0-1 +- Added job pre/post scripts directories + * Mon Mar 21 2016 Cleber Rosa - 0.34.0-0 - New upstream release 0.34.0 diff --git a/avocado/plugins/jobscripts.py b/avocado/plugins/jobscripts.py new file mode 100644 index 0000000000000000000000000000000000000000..06225b4473f113f462898b4c662825ea23b3b96c --- /dev/null +++ b/avocado/plugins/jobscripts.py @@ -0,0 +1,69 @@ +import os +import logging + +from avocado.utils import process +from avocado.core.settings import settings +from avocado.plugins.base import JobPre, JobPost + + +CONFIG_SECTION = 'plugins.jobscripts' + + +class JobScripts(JobPre, JobPost): + + name = 'jobscripts' + description = 'Runs scripts before/after the job is run' + + def __init__(self): + self.log = logging.getLogger("avocado.app") + self.warn_non_existing_dir = settings.get_value(section=CONFIG_SECTION, + key="warn_non_existing_dir", + key_type=bool, + default=False) + self.warn_non_zero_status = settings.get_value(section=CONFIG_SECTION, + key="warn_non_zero_status", + key_type=bool, + default=True) + + def _run_scripts(self, kind, scripts_dir, job): + if not os.path.isdir(scripts_dir): + if self.warn_non_existing_dir: + self.log.error("Directory configured to hold %s-job scripts " + "has not been found: %s", kind, scripts_dir) + return + + dir_list = os.listdir(scripts_dir) + scripts = [os.path.join(scripts_dir, f) for f in dir_list] + scripts = [f for f in scripts + if os.access(f, os.R_OK | os.X_OK)] + scripts.sort() + if not scripts: + return + + env = self._job_to_environment_variables(job) + for script in scripts: + result = process.run(script, ignore_status=True, env=env) + if (result.exit_status != 0) and self.warn_non_zero_status: + self.log.error('%s job script "%s" exited with status "%i"', + kind.capitalize(), script, result.exit_status) + + @staticmethod + def _job_to_environment_variables(job): + env = {} + env['AVOCADO_JOB_UNIQUE_ID'] = job.unique_id + env['AVOCADO_JOB_STATUS'] = job.status + if job.logdir is not None: + env['AVOCADO_JOB_LOGDIR'] = job.logdir + return env + + def pre(self, job): + path = settings.get_value(section=CONFIG_SECTION, + key="pre", key_type=str, + default="/etc/avocado/scripts/job/pre.d/") + self._run_scripts('pre', path, job) + + def post(self, job): + path = settings.get_value(section=CONFIG_SECTION, + key="post", key_type=str, + default="/etc/avocado/scripts/job/post.d/") + self._run_scripts('post', path, job) diff --git a/docs/source/ReferenceGuide.rst b/docs/source/ReferenceGuide.rst index d40b1e808945d05c03a00b0a2be7904a4747dd6f..0114e67ab3af34492854a7577f8db7c23bea0609 100644 --- a/docs/source/ReferenceGuide.rst +++ b/docs/source/ReferenceGuide.rst @@ -258,6 +258,72 @@ The instances should have: .. [#f1] Avocado plugins can introduce additional test types. +Job Pre and Post Scripts +======================== + +Avocado ships with a plugin (installed by default) that allows running +scripts before and after the actual execution of Jobs. A user can be +sure that, when a given "pre" script is run, no test in that job has +been run, and when the "post" scripts are run, all the tests in a +given job have already finished running. + +Configuration +------------- + +By default, the script directory location is:: + + /etc/avocado/scripts/job + +Inside that directory, that is a directory for pre-job scripts:: + + /etc/avocado/scripts/job/pre.d + +And for post-job scripts:: + + /etc/avocado/scripts/job/post.d + +All the configuration about the Pre/Post Job Scripts are placed under +the ``avocado.plugins.jobscripts`` config section. To change the +location for the pre-job scripts, your configuration should look +something like this:: + + [plugins.jobscripts] + pre = /my/custom/directory/for/pre/job/scripts/ + +Accordingly, to change the location for the post-job scripts, your +configuration should look something like this:: + + [plugins.jobscripts] + post = /my/custom/directory/for/post/scripts/ + +A couple of other configuration options are available under the same +section: + +* ``warn_non_existing_dir``: gives warnings if the configured (or + default) directory set for either pre or post scripts do not exist +* ``warn_non_zero_status``: gives warnings if a given script (either + pre or post) exits with non-zero status + +Script Execution Environment +---------------------------- + +All scripts are run in separate process with some environment +variables set. These can be used in your scripts in any way you wish: + +* ``AVOCADO_JOB_UNIQUE_ID``: the unique `job-id`_. +* ``AVOCADO_JOB_STATUS``: the current status of the job. +* ``AVOCADO_JOB_LOGDIR``: the filesystem location that holds the logs + and various other files for a given job run. + +Note: Even though these variables should all be set, it's a good +practice for scripts to check if they're set before using their +values. This may prevent unintended actions such as writing to the +current working directory instead of to the ``AVOCADO_JOB_LOGDIR`` if +this is not set. + +Finally, any failures in the Pre/Post scripts will not alter the +status of the corresponding jobs. + Job Cleanup =========== diff --git a/etc/avocado/conf.d/jobscripts.conf b/etc/avocado/conf.d/jobscripts.conf new file mode 100644 index 0000000000000000000000000000000000000000..40ad1c0c15a1dab2c3b2a27a58eb58511e3275ec --- /dev/null +++ b/etc/avocado/conf.d/jobscripts.conf @@ -0,0 +1,9 @@ +[plugins.jobscripts] +# Directory with scripts to be executed before a job is run +pre = /etc/avocado/scripts/job/pre.d/ +# Directory with scripts to be executed after a job is run +post = /etc/avocado/scripts/job/post.d/ +# Warn if configured (or default) directory does not exist +warn_non_existing_dir = False +# Warn if any script run return non-zero status +warn_non_zero_status = True diff --git a/etc/avocado/scripts/job/post.d/README b/etc/avocado/scripts/job/post.d/README new file mode 100644 index 0000000000000000000000000000000000000000..1648156b101bf5175f600cd18f5860d89e245574 --- /dev/null +++ b/etc/avocado/scripts/job/post.d/README @@ -0,0 +1,4 @@ +Put your post-job scripts here. They need to be readable and executable by +the Avocado user running the jobs. The order of execution is based on their +file names. If order is important, use a prefix, such as 001-myscript, +002-otherscript, etc. diff --git a/etc/avocado/scripts/job/pre.d/README b/etc/avocado/scripts/job/pre.d/README new file mode 100644 index 0000000000000000000000000000000000000000..12d0206043464ca47701b6dd8509f945bf95053d --- /dev/null +++ b/etc/avocado/scripts/job/pre.d/README @@ -0,0 +1,4 @@ +Put your pre-job scripts here. They need to be readable and executable by +the Avocado user running the jobs. The order of execution is based on their +file names. If order is important, use a prefix, such as 001-myscript, +002-otherscript, etc. diff --git a/selftests/functional/test_plugin_jobscripts.py b/selftests/functional/test_plugin_jobscripts.py new file mode 100644 index 0000000000000000000000000000000000000000..948e65d3bc99915893c9771e696fc4e652b732f2 --- /dev/null +++ b/selftests/functional/test_plugin_jobscripts.py @@ -0,0 +1,131 @@ +import os +import shutil +import sys +import tempfile + +if sys.version_info[:2] == (2, 6): + import unittest2 as unittest +else: + import unittest + +from avocado.core import exit_codes +from avocado.utils import process +from avocado.utils import script + + +SCRIPT_PRE_TOUCH = """#!/bin/sh -e +touch %s""" + +TEST_CHECK_TOUCH = """#!/bin/sh -e +test -f %s""" + +SCRIPT_POST_RM = """#!/bin/sh -e +rm %s""" + +SCRIPT_PRE_POST_CFG = """[plugins.jobscripts] +pre = %s +post = %s +warn_non_existing_dir = True +warn_non_zero_status = True""" + +SCRIPT_NON_EXISTING_DIR_CFG = """[plugins.jobscripts] +pre = %s +warn_non_existing_dir = True +warn_non_zero_status = False""" + +SCRIPT_NON_ZERO_STATUS = """#!/bin/sh +exit 1""" + +SCRIPT_NON_ZERO_CFG = """[plugins.jobscripts] +pre = %s +warn_non_existing_dir = False +warn_non_zero_status = True""" + + +class JobScriptsTest(unittest.TestCase): + + def setUp(self): + self.tmpdir = tempfile.mkdtemp(prefix='avocado_' + __name__) + self.pre_dir = os.path.join(self.tmpdir, 'pre.d') + os.mkdir(self.pre_dir) + self.post_dir = os.path.join(self.tmpdir, 'post.d') + os.mkdir(self.post_dir) + + def test_pre_post(self): + """ + Runs both pre and post scripts and makes sure both execute properly + """ + touch_script = script.Script(os.path.join(self.pre_dir, + 'touch.sh'), + SCRIPT_PRE_TOUCH) + touch_script.save() + test_check_touch = script.Script(os.path.join(self.tmpdir, + 'check_touch.sh'), + TEST_CHECK_TOUCH) + test_check_touch.save() + rm_script = script.Script(os.path.join(self.post_dir, + 'rm.sh'), + SCRIPT_POST_RM) + rm_script.save() + config = script.TemporaryScript("pre_post.conf", + SCRIPT_PRE_POST_CFG % (self.pre_dir, + self.post_dir)) + with config: + cmd = './scripts/avocado --config %s run %s' % (config, + test_check_touch) + result = process.run(cmd) + + # Pre/Post scripts failures do not (currently?) alter the exit status + self.assertEqual(result.exit_status, exit_codes.AVOCADO_ALL_OK) + self.assertNotIn('Pre job script "%s" exited with status "1"' % touch_script, + result.stderr) + self.assertNotIn('Post job script "%s" exited with status "1"' % rm_script, + result.stderr) + + def test_status_non_zero(self): + """ + Checks warning when script returns non-zero status + """ + non_zero_script = script.Script(os.path.join(self.pre_dir, + 'non_zero.sh'), + SCRIPT_NON_ZERO_STATUS) + non_zero_script.save() + config = script.TemporaryScript("non_zero.conf", + SCRIPT_NON_ZERO_CFG % self.pre_dir) + with config: + cmd = './scripts/avocado --config %s run passtest' % config + result = process.run(cmd) + + # Pre/Post scripts failures do not (currently?) alter the exit status + self.assertEqual(result.exit_status, exit_codes.AVOCADO_ALL_OK) + self.assertEqual('Pre job script "%s" exited with status "1"\n' % non_zero_script, + result.stderr) + + def test_non_existing_dir(self): + """ + Checks warning with non existing pre dir + """ + non_zero_script = script.Script(os.path.join(self.pre_dir, + 'non_zero.sh'), + SCRIPT_NON_ZERO_STATUS) + non_zero_script.save() + + self.pre_dir = '/non/existing/dir' + config = script.TemporaryScript("non_existing_dir.conf", + SCRIPT_NON_EXISTING_DIR_CFG % self.pre_dir) + with config: + cmd = './scripts/avocado --config %s run passtest' % config + result = process.run(cmd) + + # Pre/Post scripts failures do not (currently?) alter the exit status + self.assertEqual(result.exit_status, exit_codes.AVOCADO_ALL_OK) + self.assertIn('-job scripts has not been found', result.stderr) + self.assertNotIn('Pre job script "%s" exited with status "1"' % non_zero_script, + result.stderr) + + def tearDown(self): + shutil.rmtree(self.tmpdir) + + +if __name__ == '__main__': + unittest.main() diff --git a/setup.py b/setup.py index 48c8824df3771642457c5b7f8cf52b6de2238c01..28369507623d11fa6961b04c4e54b5bd618a5b1a 100755 --- a/setup.py +++ b/setup.py @@ -64,6 +64,10 @@ def get_data_files(): data_files += [(get_dir(['etc', 'avocado', 'sysinfo']), ['etc/avocado/sysinfo/commands', 'etc/avocado/sysinfo/files', 'etc/avocado/sysinfo/profilers'])] + data_files += [(get_dir(['etc', 'avocado', 'scripts', 'job', 'pre.d']), + ['etc/avocado/scripts/job/pre.d/README'])] + data_files += [(get_dir(['etc', 'avocado', 'scripts', 'job', 'post.d']), + ['etc/avocado/scripts/job/post.d/README'])] data_files += [(get_tests_dir(), glob.glob('examples/tests/*.py'))] for data_dir in glob.glob('examples/tests/*.data'): fmt_str = '%s/*' % data_dir @@ -146,7 +150,10 @@ if __name__ == '__main__': 'run = avocado.plugins.run:Run', 'sysinfo = avocado.plugins.sysinfo:SysInfo', 'plugins = avocado.plugins.plugins:Plugins', - ] + ], + 'avocado.plugins.job.prepost': [ + 'jobscripts = avocado.plugins.jobscripts:JobScripts', + ], }, zip_safe=False, test_suite='selftests')