diff --git a/lib/ansible/modules/cloud/openstack/os_tempest_run.py b/lib/ansible/modules/cloud/openstack/os_tempest_run.py
new file mode 100644
index 00000000000000..43b5f4f1d8dd56
--- /dev/null
+++ b/lib/ansible/modules/cloud/openstack/os_tempest_run.py
@@ -0,0 +1,293 @@
+#!/usr/bin/python
+
+# (c) 2017, Tal Shafir
+
+# This file is part of Ansible
+#
+# Ansible 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.
+#
+# Ansible 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 Ansible. If not, see .
+
+ANSIBLE_METADATA = {'metadata_version': '1.0',
+ 'status': ['preview'],
+ 'supported_by': 'community'}
+
+DOCUMENTATION = '''
+---
+module: os_tempest_run
+short_description: Run OpenStack Tempest
+description:
+ - Runs Tempest according to the configuration file in the given workspace
+
+version_added: "2.4"
+
+author: "Tal Shafir (@TalShafir)"
+requirements: ["Tempest"]
+options:
+ workspace:
+ description:
+ The workspace as was configured in 'Tempest init '
+ required: True
+ dest:
+ description:
+ Path for the output from Tempest, if not given the result will be printed in the exit json under 'out' and 'err'
+ required: True
+ regex:
+ description:
+ Selection regex for tests, Tempest will run any tests that match on the I(regex)
+ must use format for python's re.match()
+ required: False
+ default: ""
+ whitelist_file:
+ description:
+ - Path for a file with a line separated list of regex, Tempest will run only tests that match at least one regex.
+ - Cannot work with the I(blacklist_file) argument
+ required: False
+ default: ""
+ blacklist_file:
+ description:
+ - Path for a file with a line separated list of regex, Tempest won't run tests that match at least one regex.
+ - Cannot work with the I(whitelist_file) argument
+ required: False
+ default: ""
+ concurrency:
+ description:
+ Number of workers to use(the default is one worker for each CPU core), set to C(1) in case you want to run serially
+ required: False
+ default: None
+ force:
+ description:
+ When C(True) override output file if already exists
+ required: False
+ default: 'False'
+ subunit:
+ description:
+ When C(True) show the output as subunit v2
+ required: False
+ default: 'False'
+notes:
+ - You can find out more about Tempest at U(http://docs.openstack.org/developer/tempest/)
+ - The module requires to tempest init before usage
+'''
+
+EXAMPLES = '''
+- name: Run all tests with default number of workers
+- os_tempest_run:
+ dest: DEST
+ workspace: cloud
+
+- name: Run all tests matching to a regex with default number of workers
+- os_tempest_run:
+ dest: DEST
+ workspace: cloud
+ regex: REGEX
+
+- name: Run all tests serially
+- os_tempest_run:
+ dest: DEST
+ workspace: cloud
+ concurrency: 1
+
+- name: Run all tests with 4 workers
+- os_tempest_run:
+ dest: DEST
+ workspace: cloud
+ concurrency: 4
+
+- name: Run all tests that their REGEX is in the whitelist file
+- os_tempest_run:
+ dest: DEST
+ workspace: cloud
+ whitelist_file: /path/to/whitelist
+
+- name: Run all tests that their REGEX is not in the blacklist file
+- os_tempest_run:
+ dest: DEST
+ workspace: cloud
+ blacklist_file: /path/to/blacklist
+'''
+RETURN = '''
+stdout:
+ description: Tempest's stdout
+ returned: fail
+ type: string
+ sample: ""
+stderr:
+ description: Tempest's stderr
+ returned: fail
+ type: string
+ sample: ""
+command:
+ description: the command executed to run Tempest
+ returned: fail
+ type: string
+ sample: "tempest run --workspace cloud"
+output:
+ description: path to the file containing the output from Tempest
+ returned: success
+ type: string
+ sample: "/path/to/file.subunit"
+'''
+from ansible.module_utils.basic import AnsibleModule
+import os
+import sys
+
+# imports for the code copied from ansible.utils.path
+from errno import EEXIST
+from ansible.module_utils._text import to_bytes
+from ansible.module_utils._text import to_native
+from ansible.module_utils._text import to_text
+
+
+def main():
+ ansible_module = AnsibleModule(argument_spec=dict(
+ workspace=dict(type="str", required=True),
+ dest=dict(type="path", required=True),
+ regex=dict(type="str", required=False, default=""),
+ whitelist_file=dict(type="path", required=False, default=""),
+ blacklist_file=dict(type="path", required=False, default=""),
+ concurrency=dict(type="int", required=False, default=None),
+ force=dict(type="bool", required=False, default=False),
+ subunit=dict(type="bool", required=False, default=True),
+ ), mutually_exclusive=(('blacklist_file', 'whitelist_file'),), )
+
+ # check if the arguments are valid
+ if ansible_module.params['whitelist_file']:
+ whitelist_file_path = unfrackpath(ansible_module.params['whitelist_file'])
+ if ansible_module.params['blacklist_file']:
+ blacklist_file_path = unfrackpath(ansible_module.params['blacklist_file'])
+
+ if ansible_module.params["whitelist_file"] and not os.path.isfile(whitelist_file_path):
+ ansible_module.fail_json(msg="'whitelist_file' is not a path to a file: '%s'" % whitelist_file_path)
+ if ansible_module.params["blacklist_file"] and not os.path.isfile(blacklist_file_path):
+ ansible_module.fail_json(msg="'blacklist_file' is not a path to a file: '%s'" % blacklist_file_path)
+
+ if ansible_module.params['dest']:
+ output_path = unfrackpath(ansible_module.params['dest'])
+ if os.path.isdir(output_path):
+ output_path = os.path.join(output_path, 'tempest-results.subunit')
+ else:
+ ansible_module.fail_json(msg="the dest parameter cannot be empty")
+
+ if output_path and os.path.isfile(output_path) and not ansible_module.params['force']:
+ ansible_module.exit_json(changed=False, msg="The output file already exists")
+
+ # initial must-have args
+ tempest_args = ['run']
+
+ if ansible_module.params["subunit"]:
+ tempest_args.extend(['--subunit'])
+
+ # check if the user wants to choose tests using regex
+ if ansible_module.params["regex"]:
+ tempest_args.extend(['--regex', ansible_module.params["regex"]])
+
+ # check if the user wants either blacklist or whitelist
+ if ansible_module.params["whitelist_file"]:
+ tempest_args.extend(['--whitelist-file', whitelist_file_path])
+
+ elif ansible_module.params["blacklist_file"]:
+ tempest_args.extend(['--blacklist-file', blacklist_file_path])
+
+ # add the workspace to the execution
+ if ansible_module.params["workspace"]:
+ tempest_args.extend(['--workspace', ansible_module.params['workspace']])
+ else:
+ ansible_module.fail_json(msg="The workspace name cannot be empty")
+
+ if ansible_module.params["concurrency"] and ansible_module.params["concurrency"] > 0:
+ tempest_args.extend(['--concurrency', ansible_module.params["concurrency"]])
+
+ # add virtualenv's bin directory to PATH
+ env_update = dict()
+ if os.path.dirname(sys.executable) not in os.environ.get('PATH', ''):
+ env_update['PATH'] = os.path.dirname(sys.executable) + os.pathsep + os.environ.get('PATH', '')
+
+ rc, tempest_stdout, tempest_stderr = ansible_module.run_command(['tempest'] + tempest_args,
+ environ_update=env_update)
+
+ if rc != 0:
+ ansible_module.fail_json(msg="Tempest running has failed", stdout=tempest_stdout, stderr=tempest_stderr,
+ changed=False, command=' '.join(['tempest'] + tempest_args), rc=rc)
+
+ # create path if doesn't exists
+ prepare_path(output_path)
+
+ with open(output_path, 'w') as output_file:
+ output_file.write(tempest_stdout)
+
+ ansible_module.exit_json(msg="Tempest has ran successfully", output=output_path, changed=True)
+
+
+def prepare_path(file_path):
+ """
+ Creates the path to the dir that contains the file if it doesn't exists
+
+ :arg file_path: A path to a file, will create the dir containing the file if it doesn't already exists.
+ :type file_path: str
+ """
+
+ dir_name = os.path.dirname(file_path)
+ if not os.path.isdir(dir_name):
+ makedirs_safe(dir_name)
+
+
+# copied from ansible.utils.path
+def unfrackpath(path, follow=True):
+ """
+ Returns a path that is free of symlinks (if follow=True), environment variables, relative path traversals and symbols (~)
+
+ :arg path: A byte or text string representing a path to be canonicalized
+ :arg follow: A boolean to indicate of symlinks should be resolved or not
+ :raises UnicodeDecodeError: If the canonicalized version of the path
+ contains non-utf8 byte sequences.
+ :rtype: A text string (unicode on python2, str on python3).
+ :returns: An absolute path with symlinks, environment variables, and tilde
+ expanded. Note that this does not check whether a path exists.
+
+ example::
+ '$HOME/../../var/mail' becomes '/var/spool/mail'
+ """
+
+ if follow:
+ final_path = os.path.normpath(
+ os.path.realpath(os.path.expanduser(os.path.expandvars(to_bytes(path, errors='surrogate_or_strict')))))
+ else:
+ final_path = os.path.normpath(
+ os.path.abspath(os.path.expanduser(os.path.expandvars(to_bytes(path, errors='surrogate_or_strict')))))
+
+ return to_text(final_path, errors='surrogate_or_strict')
+
+
+def makedirs_safe(path, mode=None):
+ """Safe way to create dirs in muliprocess/thread environments.
+
+ :arg path: A byte or text string representing a directory to be created
+ :kwarg mode: If given, the mode to set the directory to
+ :raises Exception: If the directory cannot be created and does not already exists.
+ :raises UnicodeDecodeError: if the path is not decodable in the utf-8 encoding.
+ """
+
+ rpath = unfrackpath(path)
+ b_rpath = to_bytes(rpath)
+ if not os.path.exists(b_rpath):
+ try:
+ if mode:
+ os.makedirs(b_rpath, mode)
+ else:
+ os.makedirs(b_rpath)
+ except OSError as e:
+ if e.errno != EEXIST:
+ raise Exception("Unable to create local directories(%s): %s" % (to_native(rpath), to_native(e)))
+
+
+if __name__ == '__main__':
+ main()