From 3a3f7d7d9cd1e85bf92cff609575c6f5a86ce098 Mon Sep 17 00:00:00 2001 From: ghanshyam Date: Fri, 7 Oct 2016 17:28:43 +0900 Subject: [PATCH] Initial commit for stress framework from tempest --- CONTRIBUTING.rst | 17 ++ HACKING.rst | 4 + LICENSE | 28 +- MANIFEST.in | 6 + README.md | 2 - README.rst | 64 +++++ babel.cfg | 1 + doc/source/conf.py | 75 +++++ doc/source/contributing.rst | 4 + doc/source/index.rst | 24 ++ doc/source/installation.rst | 12 + doc/source/readme.rst | 1 + doc/source/usage.rst | 7 + requirements.txt | 8 + setup.cfg | 54 ++++ setup.py | 29 ++ tempest_stress/__init__.py | 0 tempest_stress/actions/__init__.py | 0 .../actions/server_create_destroy.py | 42 +++ tempest_stress/actions/ssh_floating.py | 200 +++++++++++++ tempest_stress/actions/unit_test.py | 92 ++++++ .../actions/volume_attach_delete.py | 70 +++++ .../actions/volume_attach_verify.py | 233 ++++++++++++++++ .../actions/volume_create_delete.py | 34 +++ tempest_stress/cleanup.py | 118 ++++++++ tempest_stress/cmd/__init__.py | 0 tempest_stress/cmd/run_stress.py | 130 +++++++++ tempest_stress/config.py | 55 ++++ tempest_stress/driver.py | 264 ++++++++++++++++++ tempest_stress/etc/sample-unit-test.json | 8 + .../etc/server-create-destroy-test.json | 7 + tempest_stress/etc/ssh_floating.json | 16 ++ tempest_stress/etc/stress-tox-job.json | 28 ++ .../etc/volume-attach-delete-test.json | 7 + tempest_stress/etc/volume-attach-verify.json | 11 + .../etc/volume-create-delete-test.json | 7 + tempest_stress/plugin.py | 38 +++ tempest_stress/stress/__init__.py | 0 tempest_stress/stress/test_stress.py | 54 ++++ tempest_stress/stress/test_stressaction.py | 63 +++++ tempest_stress/stressaction.py | 96 +++++++ tempest_stress/tools/cleanup.py | 19 ++ test-requirements.txt | 14 + tox.ini | 43 +++ 44 files changed, 1956 insertions(+), 29 deletions(-) create mode 100644 CONTRIBUTING.rst create mode 100644 HACKING.rst create mode 100644 MANIFEST.in delete mode 100644 README.md create mode 100644 README.rst create mode 100644 babel.cfg create mode 100644 doc/source/conf.py create mode 100644 doc/source/contributing.rst create mode 100644 doc/source/index.rst create mode 100644 doc/source/installation.rst create mode 100644 doc/source/readme.rst create mode 100644 doc/source/usage.rst create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 tempest_stress/__init__.py create mode 100644 tempest_stress/actions/__init__.py create mode 100644 tempest_stress/actions/server_create_destroy.py create mode 100644 tempest_stress/actions/ssh_floating.py create mode 100644 tempest_stress/actions/unit_test.py create mode 100644 tempest_stress/actions/volume_attach_delete.py create mode 100644 tempest_stress/actions/volume_attach_verify.py create mode 100644 tempest_stress/actions/volume_create_delete.py create mode 100644 tempest_stress/cleanup.py create mode 100644 tempest_stress/cmd/__init__.py create mode 100755 tempest_stress/cmd/run_stress.py create mode 100644 tempest_stress/config.py create mode 100644 tempest_stress/driver.py create mode 100644 tempest_stress/etc/sample-unit-test.json create mode 100644 tempest_stress/etc/server-create-destroy-test.json create mode 100644 tempest_stress/etc/ssh_floating.json create mode 100644 tempest_stress/etc/stress-tox-job.json create mode 100644 tempest_stress/etc/volume-attach-delete-test.json create mode 100644 tempest_stress/etc/volume-attach-verify.json create mode 100644 tempest_stress/etc/volume-create-delete-test.json create mode 100644 tempest_stress/plugin.py create mode 100644 tempest_stress/stress/__init__.py create mode 100644 tempest_stress/stress/test_stress.py create mode 100644 tempest_stress/stress/test_stressaction.py create mode 100644 tempest_stress/stressaction.py create mode 100755 tempest_stress/tools/cleanup.py create mode 100644 test-requirements.txt create mode 100644 tox.ini diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..fbb0897 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,17 @@ +If you would like to contribute to the development of OpenStack, you must +follow the steps in this page: + + http://docs.openstack.org/infra/manual/developers.html + +If you already have a good understanding of how the system works and your +OpenStack accounts are set up, you can skip to the development workflow +section of this documentation to learn how changes to OpenStack should be +submitted for review via the Gerrit tool: + + http://docs.openstack.org/infra/manual/developers.html#development-workflow + +Pull requests submitted through GitHub will be ignored. + +Bugs should be filed on Launchpad, not GitHub: + + https://bugs.launchpad.net/tempest_stress \ No newline at end of file diff --git a/HACKING.rst b/HACKING.rst new file mode 100644 index 0000000..abe46a2 --- /dev/null +++ b/HACKING.rst @@ -0,0 +1,4 @@ +tempest_stress Style Commandments +=============================================== + +Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/ \ No newline at end of file diff --git a/LICENSE b/LICENSE index 8dada3e..67db858 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,4 @@ + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -172,30 +173,3 @@ defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - 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. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..90f8a7a --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include AUTHORS +include ChangeLog +exclude .gitignore +exclude .gitreview + +global-exclude *.pyc \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 2cd478d..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# tempest_stress -Tempest Stress Tests diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..d446e42 --- /dev/null +++ b/README.rst @@ -0,0 +1,64 @@ +.. _stress_field_guide: + +Tempest Field Guide to Stress Tests +=================================== + +OpenStack is a distributed, asynchronous system that is prone to race condition +bugs. These bugs will not be easily found during +functional testing but will be encountered by users in large deployments in a +way that is hard to debug. The stress test tries to cause these bugs to happen +in a more controlled environment. + +Stress tests are designed to stress an OpenStack environment by running a high +workload against it and seeing what breaks. The stress test framework runs +several test jobs in parallel and can run any existing test in Tempest as a +stress job. + +Environment +----------- +This particular framework assumes your working Nova cluster understands Nova +API 2.0. The stress tests can read the logs from the cluster. To enable this +you have to provide the hostname to call 'nova-manage' and +the private key and user name for ssh to the cluster in the +[stress] section of tempest.conf. You also need to provide the +location of the log files: + + target_logfiles = "regexp to all log files to be checked for errors" + target_private_key_path = "private ssh key for controller and log file nodes" + target_ssh_user = "username for controller and log file nodes" + target_controller = "hostname or ip of controller node (for nova-manage) + log_check_interval = "time between checking logs for errors (default 60s)" + +To activate logging on your console please make sure that you activate `use_stderr` +in tempest.conf or use the default `logging.conf.sample` file. + +Running default stress test set +------------------------------- + +The stress test framework can automatically discover test inside the tempest +test suite. All test flag with the `@stresstest` decorator will be executed. +In order to use this discovery you have to install tempest CLI, be in the +tempest root directory and execute the following: + + tempest run-stress -a -d 30 + +Running the sample test +----------------------- + +To test installation, do the following: + + tempest run-stress -t tempest/stress/etc/server-create-destroy-test.json -d 30 + +This sample test tries to create a few VMs and kill a few VMs. + + +Additional Tools +---------------- + +Sometimes the tests don't finish, or there are failures. In these +cases, you may want to clean out the nova cluster. We have provided +some scripts to do this in the ``tools`` subdirectory. +You can use the following script to destroy any keypairs, +floating ips, and servers: + +tempest/stress/tools/cleanup.py diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 0000000..efceab8 --- /dev/null +++ b/babel.cfg @@ -0,0 +1 @@ +[python: **.py] diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 0000000..77280ea --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# 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 os +import sys + +sys.path.insert(0, os.path.abspath('../..')) +# -- General configuration ---------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [ + 'sphinx.ext.autodoc', + #'sphinx.ext.intersphinx', + 'oslosphinx' +] + +# autodoc generation is a bit aggressive and a nuisance when doing heavy +# text edit cycles. +# execute "export SPHINX_DEBUG=1" in your terminal to disable + +# The suffix of source filenames. +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'tempest_stress' +copyright = u'2016, OpenStack Foundation' + +# If true, '()' will be appended to :func: etc. cross-reference text. +add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +add_module_names = True + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# -- Options for HTML output -------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +# html_theme_path = ["."] +# html_theme = '_theme' +# html_static_path = ['static'] + +# Output file base name for HTML help builder. +htmlhelp_basename = '%sdoc' % project + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass +# [howto/manual]). +latex_documents = [ + ('index', + '%s.tex' % project, + u'%s Documentation' % project, + u'OpenStack Foundation', 'manual'), +] + +# Example configuration for intersphinx: refer to the Python standard library. +#intersphinx_mapping = {'http://docs.python.org/': None} \ No newline at end of file diff --git a/doc/source/contributing.rst b/doc/source/contributing.rst new file mode 100644 index 0000000..ed77c12 --- /dev/null +++ b/doc/source/contributing.rst @@ -0,0 +1,4 @@ +============ +Contributing +============ +.. include:: ../../CONTRIBUTING.rst \ No newline at end of file diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..b8024e0 --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,24 @@ +.. tempest_stress documentation master file, created by + sphinx-quickstart on Tue Jul 9 22:26:36 2013. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to tempest_stress's documentation! +======================================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + readme + installation + usage + contributing + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/doc/source/installation.rst b/doc/source/installation.rst new file mode 100644 index 0000000..d4770a6 --- /dev/null +++ b/doc/source/installation.rst @@ -0,0 +1,12 @@ +============ +Installation +============ + +At the command line:: + + $ pip install tempest_stress + +Or, if you have virtualenvwrapper installed:: + + $ mkvirtualenv tempest_stress + $ pip install tempest_stress \ No newline at end of file diff --git a/doc/source/readme.rst b/doc/source/readme.rst new file mode 100644 index 0000000..38ba804 --- /dev/null +++ b/doc/source/readme.rst @@ -0,0 +1 @@ +.. include:: ../../README.rst \ No newline at end of file diff --git a/doc/source/usage.rst b/doc/source/usage.rst new file mode 100644 index 0000000..7b199a6 --- /dev/null +++ b/doc/source/usage.rst @@ -0,0 +1,7 @@ +======== +Usage +======== + +To use tempest_stress in a project:: + + import tempest_stress \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d639e14 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +# The order of packages is significant, because pip processes them in the order +# of appearance. Changing the order has an impact on the overall integration +# process, which may cause wedges in the gate later. + +pbr>=1.6 # Apache-2.0 +Babel>=1.3 +oslo.log>=1.14.0 # Apache-2.0 +tempest>=1333.0.0 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..2f2d123 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,54 @@ +[metadata] +name = tempest_stress +summary = OpenStack is a distributed, asynchronous system that is prone to race condition +description-file = + README.rst +author = OpenStack +author-email = openstack-dev@lists.openstack.org +home-page = http://www.openstack.org/ +classifier = + Environment :: OpenStack + Intended Audience :: Information Technology + Intended Audience :: System Administrators + License :: OSI Approved :: Apache Software License + Operating System :: POSIX :: Linux + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.3 + Programming Language :: Python :: 3.4 + +[files] +packages = + tempest_stress + +[entry_points] +console_scripts = + run-tempest-stress = tempest_stress.cmd.run_stress:main + +tempest.test_plugins = + tempest_stress = tempest_stress.plugin:TempestStressPlugin + + +[build_sphinx] +source-dir = doc/source +build-dir = doc/build +all_files = 1 + +[upload_sphinx] +upload-dir = doc/build/html + +[compile_catalog] +directory = tempest_stress/locale +domain = tempest_stress + +[update_catalog] +domain = tempest_stress +output_dir = tempest_stress/locale +input_file = tempest_stress/locale/tempest_stress.pot + +[extract_messages] +keywords = _ gettext ngettext l_ lazy_gettext +mapping_file = babel.cfg +output_file = tempest_stress/locale/tempest_stress.pot diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..4b34a0a --- /dev/null +++ b/setup.py @@ -0,0 +1,29 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# 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. + +# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT +import setuptools + +# In python < 2.7.4, a lazy loading of package `pbr` will break +# setuptools if some other modules registered functions in `atexit`. +# solution from: http://bugs.python.org/issue15881#msg170215 +try: + import multiprocessing # noqa +except ImportError: + pass + +setuptools.setup( + setup_requires=['pbr'], + pbr=True) \ No newline at end of file diff --git a/tempest_stress/__init__.py b/tempest_stress/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tempest_stress/actions/__init__.py b/tempest_stress/actions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tempest_stress/actions/server_create_destroy.py b/tempest_stress/actions/server_create_destroy.py new file mode 100644 index 0000000..183bc6c --- /dev/null +++ b/tempest_stress/actions/server_create_destroy.py @@ -0,0 +1,42 @@ +# Copyright 2013 Quanta Research Cambridge, Inc. +# +# 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. + +from tempest.common.utils import data_utils +from tempest.common import waiters +from tempest import config +import tempest.stress.stressaction as stressaction + +CONF = config.CONF + + +class ServerCreateDestroyTest(stressaction.StressAction): + + def setUp(self, **kwargs): + self.image = CONF.compute.image_ref + self.flavor = CONF.compute.flavor_ref + + def run(self): + name = data_utils.rand_name(self.__class__.__name__ + "-instance") + self.logger.info("creating %s" % name) + server = self.manager.servers_client.create_server( + name=name, imageRef=self.image, flavorRef=self.flavor)['server'] + server_id = server['id'] + waiters.wait_for_server_status(self.manager.servers_client, server_id, + 'ACTIVE') + self.logger.info("created %s" % server_id) + self.logger.info("deleting %s" % name) + self.manager.servers_client.delete_server(server_id) + waiters.wait_for_server_termination(self.manager.servers_client, + server_id) + self.logger.info("deleted %s" % server_id) diff --git a/tempest_stress/actions/ssh_floating.py b/tempest_stress/actions/ssh_floating.py new file mode 100644 index 0000000..845b4a7 --- /dev/null +++ b/tempest_stress/actions/ssh_floating.py @@ -0,0 +1,200 @@ +# 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 socket +import subprocess + +from tempest.common.utils import data_utils +from tempest.common import waiters +from tempest import config +from tempest.lib.common.utils import test_utils +import tempest.stress.stressaction as stressaction + +CONF = config.CONF + + +class FloatingStress(stressaction.StressAction): + + # from the scenario manager + def ping_ip_address(self, ip_address): + cmd = ['ping', '-c1', '-w1', ip_address] + + proc = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + proc.communicate() + success = proc.returncode == 0 + return success + + def tcp_connect_scan(self, addr, port): + # like tcp + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + s.connect((addr, port)) + except socket.error as exc: + self.logger.info("%s(%s): %s", self.server_id, self.floating['ip'], + str(exc)) + return False + self.logger.info("%s(%s): Connected :)", self.server_id, + self.floating['ip']) + s.close() + return True + + def check_port_ssh(self): + def func(): + return self.tcp_connect_scan(self.floating['ip'], 22) + if not test_utils.call_until_true(func, self.check_timeout, + self.check_interval): + raise RuntimeError("Cannot connect to the ssh port.") + + def check_icmp_echo(self): + self.logger.info("%s(%s): Pinging..", + self.server_id, self.floating['ip']) + + def func(): + return self.ping_ip_address(self.floating['ip']) + if not test_utils.call_until_true(func, self.check_timeout, + self.check_interval): + raise RuntimeError("%s(%s): Cannot ping the machine.", + self.server_id, self.floating['ip']) + self.logger.info("%s(%s): pong :)", + self.server_id, self.floating['ip']) + + def _create_vm(self): + self.name = name = data_utils.rand_name( + self.__class__.__name__ + "-instance") + servers_client = self.manager.servers_client + self.logger.info("creating %s" % name) + vm_args = self.vm_extra_args.copy() + vm_args['security_groups'] = [self.sec_grp] + server = servers_client.create_server(name=name, imageRef=self.image, + flavorRef=self.flavor, + **vm_args)['server'] + self.server_id = server['id'] + if self.wait_after_vm_create: + waiters.wait_for_server_status(self.manager.servers_client, + self.server_id, 'ACTIVE') + + def _destroy_vm(self): + self.logger.info("deleting %s" % self.server_id) + self.manager.servers_client.delete_server(self.server_id) + waiters.wait_for_server_termination(self.manager.servers_client, + self.server_id) + self.logger.info("deleted %s" % self.server_id) + + def _create_sec_group(self): + sec_grp_cli = self.manager.compute_security_groups_client + s_name = data_utils.rand_name(self.__class__.__name__ + '-sec_grp') + s_description = data_utils.rand_name('desc') + self.sec_grp = sec_grp_cli.create_security_group( + name=s_name, description=s_description)['security_group'] + create_rule = sec_grp_cli.create_security_group_rule + create_rule(parent_group_id=self.sec_grp['id'], ip_protocol='tcp', + from_port=22, to_port=22) + create_rule(parent_group_id=self.sec_grp['id'], ip_protocol='icmp', + from_port=-1, to_port=-1) + + def _destroy_sec_grp(self): + sec_grp_cli = self.manager.compute_security_groups_client + sec_grp_cli.delete_security_group(self.sec_grp['id']) + + def _create_floating_ip(self): + floating_cli = self.manager.compute_floating_ips_client + self.floating = (floating_cli.create_floating_ip(self.floating_pool) + ['floating_ip']) + + def _destroy_floating_ip(self): + cli = self.manager.compute_floating_ips_client + cli.delete_floating_ip(self.floating['id']) + cli.wait_for_resource_deletion(self.floating['id']) + self.logger.info("Deleted Floating IP %s", str(self.floating['ip'])) + + def setUp(self, **kwargs): + self.image = CONF.compute.image_ref + self.flavor = CONF.compute.flavor_ref + self.vm_extra_args = kwargs.get('vm_extra_args', {}) + self.wait_after_vm_create = kwargs.get('wait_after_vm_create', + True) + self.new_vm = kwargs.get('new_vm', False) + self.new_sec_grp = kwargs.get('new_sec_group', False) + self.new_floating = kwargs.get('new_floating', False) + self.reboot = kwargs.get('reboot', False) + self.floating_pool = kwargs.get('floating_pool', None) + self.verify = kwargs.get('verify', ('check_port_ssh', + 'check_icmp_echo')) + self.check_timeout = kwargs.get('check_timeout', 120) + self.check_interval = kwargs.get('check_interval', 1) + self.wait_for_disassociate = kwargs.get('wait_for_disassociate', + True) + + # allocate floating + if not self.new_floating: + self._create_floating_ip() + # add security group + if not self.new_sec_grp: + self._create_sec_group() + # create vm + if not self.new_vm: + self._create_vm() + + def wait_disassociate(self): + cli = self.manager.compute_floating_ips_client + + def func(): + floating = (cli.show_floating_ip(self.floating['id']) + ['floating_ip']) + return floating['instance_id'] is None + + if not test_utils.call_until_true(func, self.check_timeout, + self.check_interval): + raise RuntimeError("IP disassociate timeout!") + + def run_core(self): + cli = self.manager.compute_floating_ips_client + cli.associate_floating_ip_to_server(self.floating['ip'], + self.server_id) + for method in self.verify: + m = getattr(self, method) + m() + cli.disassociate_floating_ip_from_server(self.floating['ip'], + self.server_id) + if self.wait_for_disassociate: + self.wait_disassociate() + + def run(self): + if self.new_sec_grp: + self._create_sec_group() + if self.new_floating: + self._create_floating_ip() + if self.new_vm: + self._create_vm() + if self.reboot: + self.manager.servers_client.reboot(self.server_id, 'HARD') + waiters.wait_for_server_status(self.manager.servers_client, + self.server_id, 'ACTIVE') + + self.run_core() + + if self.new_vm: + self._destroy_vm() + if self.new_floating: + self._destroy_floating_ip() + if self.new_sec_grp: + self._destroy_sec_grp() + + def tearDown(self): + if not self.new_vm: + self._destroy_vm() + if not self.new_floating: + self._destroy_floating_ip() + if not self.new_sec_grp: + self._destroy_sec_grp() diff --git a/tempest_stress/actions/unit_test.py b/tempest_stress/actions/unit_test.py new file mode 100644 index 0000000..e016c61 --- /dev/null +++ b/tempest_stress/actions/unit_test.py @@ -0,0 +1,92 @@ +# 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. + +from oslo_log import log as logging +from oslo_utils import importutils + +from tempest import config +import tempest.stress.stressaction as stressaction + +CONF = config.CONF + + +class SetUpClassRunTime(object): + + process = 'process' + action = 'action' + application = 'application' + + allowed = set((process, action, application)) + + @classmethod + def validate(cls, name): + if name not in cls.allowed: + raise KeyError("\'%s\' not a valid option" % name) + + +class UnitTest(stressaction.StressAction): + """This is a special action for running existing unittests as stress test. + + You need to pass ``test_method`` and ``class_setup_per`` + using ``kwargs`` in the JSON descriptor; + ``test_method`` should be the fully qualified name of a unittest, + ``class_setup_per`` should be one from: + ``application``: once in the stress job lifetime + ``process``: once in the worker process lifetime + ``action``: on each action + Not all combination working in every case. + """ + + def setUp(self, **kwargs): + method = kwargs['test_method'].split('.') + self.test_method = method.pop() + self.klass = importutils.import_class('.'.join(method)) + self.logger = logging.getLogger('.'.join(method)) + # valid options are 'process', 'application' , 'action' + self.class_setup_per = kwargs.get('class_setup_per', + SetUpClassRunTime.process) + SetUpClassRunTime.validate(self.class_setup_per) + + if self.class_setup_per == SetUpClassRunTime.application: + self.klass.setUpClass() + self.setupclass_called = False + + @property + def action(self): + if self.test_method: + return self.test_method + return super(UnitTest, self).action + + def run_core(self): + res = self.klass(self.test_method).run() + if res.errors: + raise RuntimeError(res.errors) + + def run(self): + if self.class_setup_per != SetUpClassRunTime.application: + if (self.class_setup_per == SetUpClassRunTime.action + or self.setupclass_called is False): + self.klass.setUpClass() + self.setupclass_called = True + + try: + self.run_core() + finally: + if (CONF.stress.leave_dirty_stack is False + and self.class_setup_per == SetUpClassRunTime.action): + self.klass.tearDownClass() + else: + self.run_core() + + def tearDown(self): + if self.class_setup_per != SetUpClassRunTime.action: + self.klass.tearDownClass() diff --git a/tempest_stress/actions/volume_attach_delete.py b/tempest_stress/actions/volume_attach_delete.py new file mode 100644 index 0000000..5fc006e --- /dev/null +++ b/tempest_stress/actions/volume_attach_delete.py @@ -0,0 +1,70 @@ +# (c) 2013 Deutsche Telekom AG +# 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. + +from tempest.common.utils import data_utils +from tempest.common import waiters +from tempest import config +import tempest.stress.stressaction as stressaction + +CONF = config.CONF + + +class VolumeAttachDeleteTest(stressaction.StressAction): + + def setUp(self, **kwargs): + self.image = CONF.compute.image_ref + self.flavor = CONF.compute.flavor_ref + + def run(self): + # Step 1: create volume + name = data_utils.rand_name(self.__class__.__name__ + "-volume") + self.logger.info("creating volume: %s" % name) + volume = self.manager.volumes_client.create_volume( + display_name=name, size=CONF.volume.volume_size)['volume'] + self.manager.volumes_client.wait_for_volume_status(volume['id'], + 'available') + self.logger.info("created volume: %s" % volume['id']) + + # Step 2: create vm instance + vm_name = data_utils.rand_name(self.__class__.__name__ + "-instance") + self.logger.info("creating vm: %s" % vm_name) + server = self.manager.servers_client.create_server( + name=vm_name, imageRef=self.image, flavorRef=self.flavor)['server'] + server_id = server['id'] + waiters.wait_for_server_status(self.manager.servers_client, server_id, + 'ACTIVE') + self.logger.info("created vm %s" % server_id) + + # Step 3: attach volume to vm + self.logger.info("attach volume (%s) to vm %s" % + (volume['id'], server_id)) + self.manager.servers_client.attach_volume(server_id, + volumeId=volume['id'], + device='/dev/vdc') + self.manager.volumes_client.wait_for_volume_status(volume['id'], + 'in-use') + self.logger.info("volume (%s) attached to vm %s" % + (volume['id'], server_id)) + + # Step 4: delete vm + self.logger.info("deleting vm: %s" % vm_name) + self.manager.servers_client.delete_server(server_id) + waiters.wait_for_server_termination(self.manager.servers_client, + server_id) + self.logger.info("deleted vm: %s" % server_id) + + # Step 5: delete volume + self.logger.info("deleting volume: %s" % volume['id']) + self.manager.volumes_client.delete_volume(volume['id']) + self.manager.volumes_client.wait_for_resource_deletion(volume['id']) + self.logger.info("deleted volume: %s" % volume['id']) diff --git a/tempest_stress/actions/volume_attach_verify.py b/tempest_stress/actions/volume_attach_verify.py new file mode 100644 index 0000000..4fbb851 --- /dev/null +++ b/tempest_stress/actions/volume_attach_verify.py @@ -0,0 +1,233 @@ +# 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 re + +from tempest.common.utils import data_utils +from tempest.common.utils.linux import remote_client +from tempest.common import waiters +from tempest import config +from tempest.lib.common.utils import test_utils +import tempest.stress.stressaction as stressaction + +CONF = config.CONF + + +class VolumeVerifyStress(stressaction.StressAction): + + def _create_keypair(self): + keyname = data_utils.rand_name("key") + self.key = (self.manager.keypairs_client.create_keypair(name=keyname) + ['keypair']) + + def _delete_keypair(self): + self.manager.keypairs_client.delete_keypair(self.key['name']) + + def _create_vm(self): + self.name = name = data_utils.rand_name( + self.__class__.__name__ + "-instance") + servers_client = self.manager.servers_client + self.logger.info("creating %s" % name) + vm_args = self.vm_extra_args.copy() + vm_args['security_groups'] = [self.sec_grp] + vm_args['key_name'] = self.key['name'] + server = servers_client.create_server(name=name, imageRef=self.image, + flavorRef=self.flavor, + **vm_args)['server'] + self.server_id = server['id'] + waiters.wait_for_server_status(self.manager.servers_client, + self.server_id, 'ACTIVE') + + def _destroy_vm(self): + self.logger.info("deleting server: %s" % self.server_id) + self.manager.servers_client.delete_server(self.server_id) + waiters.wait_for_server_termination(self.manager.servers_client, + self.server_id) + self.logger.info("deleted server: %s" % self.server_id) + + def _create_sec_group(self): + sec_grp_cli = self.manager.compute_security_groups_client + s_name = data_utils.rand_name(self.__class__.__name__ + '-sec_grp') + s_description = data_utils.rand_name('desc') + self.sec_grp = sec_grp_cli.create_security_group( + name=s_name, description=s_description)['security_group'] + create_rule = sec_grp_cli.create_security_group_rule + create_rule(parent_group_id=self.sec_grp['id'], ip_protocol='tcp', + from_port=22, to_port=22) + create_rule(parent_group_id=self.sec_grp['id'], ip_protocol='icmp', + from_port=-1, to_port=-1) + + def _destroy_sec_grp(self): + sec_grp_cli = self.manager.compute_security_groups_client + sec_grp_cli.delete_security_group(self.sec_grp['id']) + + def _create_floating_ip(self): + floating_cli = self.manager.compute_floating_ips_client + self.floating = (floating_cli.create_floating_ip(self.floating_pool) + ['floating_ip']) + + def _destroy_floating_ip(self): + cli = self.manager.compute_floating_ips_client + cli.delete_floating_ip(self.floating['id']) + cli.wait_for_resource_deletion(self.floating['id']) + self.logger.info("Deleted Floating IP %s", str(self.floating['ip'])) + + def _create_volume(self): + name = data_utils.rand_name(self.__class__.__name__ + "-volume") + self.logger.info("creating volume: %s" % name) + volumes_client = self.manager.volumes_client + self.volume = volumes_client.create_volume( + display_name=name, size=CONF.volume.volume_size)['volume'] + volumes_client.wait_for_volume_status(self.volume['id'], + 'available') + self.logger.info("created volume: %s" % self.volume['id']) + + def _delete_volume(self): + self.logger.info("deleting volume: %s" % self.volume['id']) + volumes_client = self.manager.volumes_client + volumes_client.delete_volume(self.volume['id']) + volumes_client.wait_for_resource_deletion(self.volume['id']) + self.logger.info("deleted volume: %s" % self.volume['id']) + + def _wait_disassociate(self): + cli = self.manager.compute_floating_ips_client + + def func(): + floating = (cli.show_floating_ip(self.floating['id']) + ['floating_ip']) + return floating['instance_id'] is None + + if not test_utils.call_until_true(func, CONF.compute.build_timeout, + CONF.compute.build_interval): + raise RuntimeError("IP disassociate timeout!") + + def new_server_ops(self): + self._create_vm() + cli = self.manager.compute_floating_ips_client + cli.associate_floating_ip_to_server(self.floating['ip'], + self.server_id) + if self.ssh_test_before_attach and self.enable_ssh_verify: + self.logger.info("Scanning for block devices via ssh on %s" + % self.server_id) + self.part_wait(self.detach_match_count) + + def setUp(self, **kwargs): + """Note able configuration combinations: + + Closest options to the test_stamp_pattern: + new_server = True + new_volume = True + enable_ssh_verify = True + ssh_test_before_attach = False + Just attaching: + new_server = False + new_volume = False + enable_ssh_verify = True + ssh_test_before_attach = True + Mostly API load by repeated attachment: + new_server = False + new_volume = False + enable_ssh_verify = False + ssh_test_before_attach = False + Minimal Nova load, but cinder load not decreased: + new_server = False + new_volume = True + enable_ssh_verify = True + ssh_test_before_attach = True + """ + self.image = CONF.compute.image_ref + self.flavor = CONF.compute.flavor_ref + self.vm_extra_args = kwargs.get('vm_extra_args', {}) + self.floating_pool = kwargs.get('floating_pool', None) + self.new_volume = kwargs.get('new_volume', True) + self.new_server = kwargs.get('new_server', False) + self.enable_ssh_verify = kwargs.get('enable_ssh_verify', True) + self.ssh_test_before_attach = kwargs.get('ssh_test_before_attach', + False) + self.part_line_re = re.compile(kwargs.get('part_line_re', '.*vd.*')) + self.detach_match_count = kwargs.get('detach_match_count', 1) + self.attach_match_count = kwargs.get('attach_match_count', 2) + self.part_name = kwargs.get('part_name', '/dev/vdc') + + self._create_floating_ip() + self._create_sec_group() + self._create_keypair() + private_key = self.key['private_key'] + username = CONF.validation.image_ssh_user + self.remote_client = remote_client.RemoteClient(self.floating['ip'], + username, + pkey=private_key) + if not self.new_volume: + self._create_volume() + if not self.new_server: + self.new_server_ops() + + # now we just test that the number of partitions has increased or decreased + def part_wait(self, num_match): + def _part_state(): + self.partitions = self.remote_client.get_partitions().split('\n') + matching = 0 + for part_line in self.partitions[1:]: + if self.part_line_re.match(part_line): + matching += 1 + return matching == num_match + if test_utils.call_until_true(_part_state, + CONF.compute.build_timeout, + CONF.compute.build_interval): + return + else: + raise RuntimeError("Unexpected partitions: %s", + str(self.partitions)) + + def run(self): + if self.new_server: + self.new_server_ops() + if self.new_volume: + self._create_volume() + servers_client = self.manager.servers_client + self.logger.info("attach volume (%s) to vm %s" % + (self.volume['id'], self.server_id)) + servers_client.attach_volume(self.server_id, + volumeId=self.volume['id'], + device=self.part_name) + self.manager.volumes_client.wait_for_volume_status(self.volume['id'], + 'in-use') + if self.enable_ssh_verify: + self.logger.info("Scanning for new block device on %s" + % self.server_id) + self.part_wait(self.attach_match_count) + + servers_client.detach_volume(self.server_id, + self.volume['id']) + self.manager.volumes_client.wait_for_volume_status(self.volume['id'], + 'available') + if self.enable_ssh_verify: + self.logger.info("Scanning for block device disappearance on %s" + % self.server_id) + self.part_wait(self.detach_match_count) + if self.new_volume: + self._delete_volume() + if self.new_server: + self._destroy_vm() + + def tearDown(self): + cli = self.manager.compute_floating_ips_client + cli.disassociate_floating_ip_from_server(self.floating['ip'], + self.server_id) + self._wait_disassociate() + if not self.new_server: + self._destroy_vm() + self._delete_keypair() + self._destroy_floating_ip() + self._destroy_sec_grp() + if not self.new_volume: + self._delete_volume() diff --git a/tempest_stress/actions/volume_create_delete.py b/tempest_stress/actions/volume_create_delete.py new file mode 100644 index 0000000..66971ea --- /dev/null +++ b/tempest_stress/actions/volume_create_delete.py @@ -0,0 +1,34 @@ +# 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. + +from tempest.common.utils import data_utils +from tempest import config +import tempest.stress.stressaction as stressaction + +CONF = config.CONF + + +class VolumeCreateDeleteTest(stressaction.StressAction): + + def run(self): + name = data_utils.rand_name("volume") + self.logger.info("creating %s" % name) + volumes_client = self.manager.volumes_client + volume = volumes_client.create_volume( + display_name=name, size=CONF.volume.volume_size)['volume'] + vol_id = volume['id'] + volumes_client.wait_for_volume_status(vol_id, 'available') + self.logger.info("created %s" % volume['id']) + self.logger.info("deleting %s" % name) + volumes_client.delete_volume(vol_id) + volumes_client.wait_for_resource_deletion(vol_id) + self.logger.info("deleted %s" % vol_id) diff --git a/tempest_stress/cleanup.py b/tempest_stress/cleanup.py new file mode 100644 index 0000000..3b0a937 --- /dev/null +++ b/tempest_stress/cleanup.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python + +# Copyright 2013 Quanta Research Cambridge, Inc. +# +# 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. + +from oslo_log import log as logging + +from tempest.common import credentials_factory as credentials +from tempest.common import waiters + +LOG = logging.getLogger(__name__) + + +def cleanup(): + admin_manager = credentials.AdminManager() + + body = admin_manager.servers_client.list_servers(all_tenants=True) + LOG.info("Cleanup::remove %s servers" % len(body['servers'])) + for s in body['servers']: + try: + admin_manager.servers_client.delete_server(s['id']) + except Exception: + pass + + for s in body['servers']: + try: + waiters.wait_for_server_termination(admin_manager.servers_client, + s['id']) + except Exception: + pass + + keypairs = admin_manager.keypairs_client.list_keypairs()['keypairs'] + LOG.info("Cleanup::remove %s keypairs" % len(keypairs)) + for k in keypairs: + try: + admin_manager.keypairs_client.delete_keypair(k['name']) + except Exception: + pass + + secgrp_client = admin_manager.compute_security_groups_client + secgrp = (secgrp_client.list_security_groups(all_tenants=True) + ['security_groups']) + secgrp_del = [grp for grp in secgrp if grp['name'] != 'default'] + LOG.info("Cleanup::remove %s Security Group" % len(secgrp_del)) + for g in secgrp_del: + try: + secgrp_client.delete_security_group(g['id']) + except Exception: + pass + + admin_floating_ips_client = admin_manager.compute_floating_ips_client + floating_ips = (admin_floating_ips_client.list_floating_ips() + ['floating_ips']) + LOG.info("Cleanup::remove %s floating ips" % len(floating_ips)) + for f in floating_ips: + try: + admin_floating_ips_client.delete_floating_ip(f['id']) + except Exception: + pass + + users = admin_manager.users_client.list_users()['users'] + LOG.info("Cleanup::remove %s users" % len(users)) + for user in users: + if user['name'].startswith("stress_user"): + admin_manager.users_client.delete_user(user['id']) + tenants = admin_manager.tenants_client.list_tenants()['tenants'] + LOG.info("Cleanup::remove %s tenants" % len(tenants)) + for tenant in tenants: + if tenant['name'].startswith("stress_tenant"): + admin_manager.tenants_client.delete_tenant(tenant['id']) + + # We have to delete snapshots first or + # volume deletion may block + + _, snaps = admin_manager.snapshots_client.list_snapshots( + all_tenants=True)['snapshots'] + LOG.info("Cleanup::remove %s snapshots" % len(snaps)) + for v in snaps: + try: + waiters.wait_for_snapshot_status( + admin_manager.snapshots_client, v['id'], 'available') + admin_manager.snapshots_client.delete_snapshot(v['id']) + except Exception: + pass + + for v in snaps: + try: + admin_manager.snapshots_client.wait_for_resource_deletion(v['id']) + except Exception: + pass + + vols = admin_manager.volumes_client.list_volumes( + params={"all_tenants": True}) + LOG.info("Cleanup::remove %s volumes" % len(vols)) + for v in vols: + try: + waiters.wait_for_volume_status( + admin_manager.volumes_client, v['id'], 'available') + admin_manager.volumes_client.delete_volume(v['id']) + except Exception: + pass + + for v in vols: + try: + admin_manager.volumes_client.wait_for_resource_deletion(v['id']) + except Exception: + pass diff --git a/tempest_stress/cmd/__init__.py b/tempest_stress/cmd/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tempest_stress/cmd/run_stress.py b/tempest_stress/cmd/run_stress.py new file mode 100755 index 0000000..06b338d --- /dev/null +++ b/tempest_stress/cmd/run_stress.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python + +# Copyright 2013 Quanta Research Cambridge, Inc. +# +# 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 argparse +import inspect +import json +import sys +try: + from unittest import loader +except ImportError: + # unittest in python 2.6 does not contain loader, so uses unittest2 + from unittest2 import loader + +from oslo_log import log as logging +from testtools import testsuite + +from tempest.stress import driver + +LOG = logging.getLogger(__name__) + + +def discover_stress_tests(path="./", filter_attr=None, call_inherited=False): + """Discovers all tempest tests and create action out of them + """ + LOG.info("Start test discovery") + tests = [] + testloader = loader.TestLoader() + list = testloader.discover(path) + for func in (testsuite.iterate_tests(list)): + attrs = [] + try: + method_name = getattr(func, '_testMethodName') + full_name = "%s.%s.%s" % (func.__module__, + func.__class__.__name__, + method_name) + test_func = getattr(func, method_name) + # NOTE(mkoderer): this contains a list of all type attributes + attrs = getattr(test_func, "__testtools_attrs") + except Exception: + next + if 'stress' in attrs: + if filter_attr is not None and filter_attr not in attrs: + continue + class_setup_per = getattr(test_func, "st_class_setup_per") + + action = {'action': + "tempest.stress.actions.unit_test.UnitTest", + 'kwargs': {"test_method": full_name, + "class_setup_per": class_setup_per + } + } + if (not call_inherited and + getattr(test_func, "st_allow_inheritance") is not True): + class_structure = inspect.getmro(test_func.im_class) + if test_func.__name__ not in class_structure[0].__dict__: + continue + tests.append(action) + return tests + + +parser = argparse.ArgumentParser(description='Run stress tests') +parser.add_argument('-d', '--duration', default=300, type=int, + help="Duration of test in secs") +parser.add_argument('-s', '--serial', action='store_true', + help="Trigger running tests serially") +parser.add_argument('-S', '--stop', action='store_true', + default=False, help="Stop on first error") +parser.add_argument('-n', '--number', type=int, + help="How often an action is executed for each process") +group = parser.add_mutually_exclusive_group(required=True) +group.add_argument('-a', '--all', action='store_true', + help="Execute all stress tests") +parser.add_argument('-T', '--type', + help="Filters tests of a certain type (e.g. gate)") +parser.add_argument('-i', '--call-inherited', action='store_true', + default=False, + help="Call also inherited function with stress attribute") +group.add_argument('-t', "--tests", nargs='?', + help="Name of the file with test description") + + +def main(): + ns = parser.parse_args() + result = 0 + if not ns.all: + tests = json.load(open(ns.tests, 'r')) + else: + tests = discover_stress_tests(filter_attr=ns.type, + call_inherited=ns.call_inherited) + + if ns.serial: + # Duration is total time + duration = ns.duration / len(tests) + for test in tests: + step_result = driver.stress_openstack([test], + duration, + ns.number, + ns.stop) + # NOTE(mkoderer): we just save the last result code + if (step_result != 0): + result = step_result + if ns.stop: + return result + else: + result = driver.stress_openstack(tests, + ns.duration, + ns.number, + ns.stop) + return result + + +if __name__ == "__main__": + try: + sys.exit(main()) + except Exception: + LOG.exception("Failure in the stress test framework") + sys.exit(1) diff --git a/tempest_stress/config.py b/tempest_stress/config.py new file mode 100644 index 0000000..588ec7f --- /dev/null +++ b/tempest_stress/config.py @@ -0,0 +1,55 @@ +# Copyright 2015 Intel Corp +# 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. + +from oslo_config import cfg + +from tempest import config # noqa + +stress_group = cfg.OptGroup(name='stress', title='Stress Test Options') + +StressGroup = [ + cfg.StrOpt('nova_logdir', + help='Directory containing log files on the compute nodes'), + cfg.IntOpt('max_instances', + default=16, + help='Maximum number of instances to create during test.'), + cfg.StrOpt('controller', + help='Controller host.'), + # new stress options + cfg.StrOpt('target_controller', + help='Controller host.'), + cfg.StrOpt('target_ssh_user', + help='ssh user.'), + cfg.StrOpt('target_private_key_path', + help='Path to private key.'), + cfg.StrOpt('target_logfiles', + help='regexp for list of log files.'), + cfg.IntOpt('log_check_interval', + default=60, + help='time (in seconds) between log file error checks.'), + cfg.IntOpt('default_thread_number_per_action', + default=4, + help='The number of threads created while stress test.'), + cfg.BoolOpt('leave_dirty_stack', + default=False, + help='Prevent the cleaning (tearDownClass()) between' + ' each stress test run if an exception occurs' + ' during this run.'), + cfg.BoolOpt('full_clean_stack', + default=False, + help='Allows a full cleaning process after a stress test.' + ' Caution : this cleanup will remove every objects of' + ' every project.') +] diff --git a/tempest_stress/driver.py b/tempest_stress/driver.py new file mode 100644 index 0000000..1e33e88 --- /dev/null +++ b/tempest_stress/driver.py @@ -0,0 +1,264 @@ +# Copyright 2013 Quanta Research Cambridge, Inc. +# +# 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 multiprocessing +import os +import signal +import time + +from oslo_log import log as logging +from oslo_utils import importutils +import six + +from tempest import clients +from tempest.common import cred_client +from tempest.common import credentials_factory as credentials +from tempest.common.utils import data_utils +from tempest import config +from tempest import exceptions +from tempest.lib.common import ssh +from tempest.stress import cleanup + +CONF = config.CONF + +LOG = logging.getLogger(__name__) +processes = [] + + +def do_ssh(command, host, ssh_user, ssh_key=None): + ssh_client = ssh.Client(host, ssh_user, key_filename=ssh_key) + try: + return ssh_client.exec_command(command) + except exceptions.SSHExecCommandFailed: + LOG.error('do_ssh raise exception. command:%s, host:%s.' + % (command, host)) + return None + + +def _get_compute_nodes(controller, ssh_user, ssh_key=None): + """Returns a list of active compute nodes. + + List is generated by running nova-manage on the controller. + """ + nodes = [] + cmd = "nova-manage service list | grep ^nova-compute" + output = do_ssh(cmd, controller, ssh_user, ssh_key) + if not output: + return nodes + # For example: nova-compute xg11eth0 nova enabled :-) 2011-10-31 18:57:46 + # This is fragile but there is, at present, no other way to get this info. + for line in output.split('\n'): + words = line.split() + if len(words) > 0 and words[4] == ":-)": + nodes.append(words[1]) + return nodes + + +def _has_error_in_logs(logfiles, nodes, ssh_user, ssh_key=None, + stop_on_error=False): + """Detect errors in nova log files on the controller and compute nodes.""" + grep = 'egrep "ERROR|TRACE" %s' % logfiles + ret = False + for node in nodes: + errors = do_ssh(grep, node, ssh_user, ssh_key) + if len(errors) > 0: + LOG.error('%s: %s' % (node, errors)) + ret = True + if stop_on_error: + break + return ret + + +def sigchld_handler(signalnum, frame): + """Signal handler (only active if stop_on_error is True).""" + for process in processes: + if (not process['process'].is_alive() and + process['process'].exitcode != 0): + signal.signal(signalnum, signal.SIG_DFL) + terminate_all_processes() + break + + +def terminate_all_processes(check_interval=20): + """Goes through the process list and terminates all child processes.""" + LOG.info("Stopping all processes.") + for process in processes: + if process['process'].is_alive(): + try: + process['process'].terminate() + except Exception: + pass + time.sleep(check_interval) + for process in processes: + if process['process'].is_alive(): + try: + pid = process['process'].pid + LOG.warning("Process %d hangs. Send SIGKILL." % pid) + os.kill(pid, signal.SIGKILL) + except Exception: + pass + process['process'].join() + + +def stress_openstack(tests, duration, max_runs=None, stop_on_error=False): + """Workload driver. Executes an action function against a nova-cluster.""" + admin_manager = credentials.AdminManager() + + ssh_user = CONF.stress.target_ssh_user + ssh_key = CONF.stress.target_private_key_path + logfiles = CONF.stress.target_logfiles + log_check_interval = int(CONF.stress.log_check_interval) + default_thread_num = int(CONF.stress.default_thread_number_per_action) + if logfiles: + controller = CONF.stress.target_controller + computes = _get_compute_nodes(controller, ssh_user, ssh_key) + for node in computes: + do_ssh("rm -f %s" % logfiles, node, ssh_user, ssh_key) + skip = False + for test in tests: + for service in test.get('required_services', []): + if not CONF.service_available.get(service): + skip = True + break + if skip: + break + # TODO(andreaf) This has to be reworked to use the credential + # provider interface. For now only tests marked as 'use_admin' will + # work. + if test.get('use_admin', False): + manager = admin_manager + else: + raise NotImplemented('Non admin tests are not supported') + for p_number in range(test.get('threads', default_thread_num)): + if test.get('use_isolated_tenants', False): + username = data_utils.rand_name("stress_user") + tenant_name = data_utils.rand_name("stress_tenant") + password = "pass" + if CONF.identity.auth_version == 'v2': + identity_client = admin_manager.identity_client + projects_client = admin_manager.tenants_client + roles_client = admin_manager.roles_client + users_client = admin_manager.users_client + domains_client = None + else: + identity_client = admin_manager.identity_v3_client + projects_client = admin_manager.projects_client + roles_client = admin_manager.roles_v3_client + users_client = admin_manager.users_v3_client + domains_client = admin_manager.domains_client + domain = (identity_client.auth_provider.credentials. + get('project_domain_name', 'Default')) + credentials_client = cred_client.get_creds_client( + identity_client, projects_client, users_client, + roles_client, domains_client, project_domain_name=domain) + project = credentials_client.create_project( + name=tenant_name, description=tenant_name) + user = credentials_client.create_user(username, password, + project, "email") + # Add roles specified in config file + for conf_role in CONF.auth.tempest_roles: + credentials_client.assign_user_role(user, project, + conf_role) + creds = credentials_client.get_credentials(user, project, + password) + manager = clients.Manager(credentials=creds) + + test_obj = importutils.import_class(test['action']) + test_run = test_obj(manager, max_runs, stop_on_error) + + kwargs = test.get('kwargs', {}) + test_run.setUp(**dict(six.iteritems(kwargs))) + + LOG.debug("calling Target Object %s" % + test_run.__class__.__name__) + + mp_manager = multiprocessing.Manager() + shared_statistic = mp_manager.dict() + shared_statistic['runs'] = 0 + shared_statistic['fails'] = 0 + + p = multiprocessing.Process(target=test_run.execute, + args=(shared_statistic,)) + + process = {'process': p, + 'p_number': p_number, + 'action': test_run.action, + 'statistic': shared_statistic} + + processes.append(process) + p.start() + if stop_on_error: + # NOTE(mkoderer): only the parent should register the handler + signal.signal(signal.SIGCHLD, sigchld_handler) + end_time = time.time() + duration + had_errors = False + try: + while True: + if max_runs is None: + remaining = end_time - time.time() + if remaining <= 0: + break + else: + remaining = log_check_interval + all_proc_term = True + for process in processes: + if process['process'].is_alive(): + all_proc_term = False + break + if all_proc_term: + break + + time.sleep(min(remaining, log_check_interval)) + if stop_on_error: + if any([True for proc in processes + if proc['statistic']['fails'] > 0]): + break + + if not logfiles: + continue + if _has_error_in_logs(logfiles, computes, ssh_user, ssh_key, + stop_on_error): + had_errors = True + break + except KeyboardInterrupt: + LOG.warning("Interrupted, going to print statistics and exit ...") + + if stop_on_error: + signal.signal(signal.SIGCHLD, signal.SIG_DFL) + terminate_all_processes() + + sum_fails = 0 + sum_runs = 0 + + LOG.info("Statistics (per process):") + for process in processes: + if process['statistic']['fails'] > 0: + had_errors = True + sum_runs += process['statistic']['runs'] + sum_fails += process['statistic']['fails'] + print("Process %d (%s): Run %d actions (%d failed)" % ( + process['p_number'], + process['action'], + process['statistic']['runs'], + process['statistic']['fails'])) + print("Summary:") + print("Run %d actions (%d failed)" % (sum_runs, sum_fails)) + + if not had_errors and CONF.stress.full_clean_stack: + LOG.info("cleaning up") + cleanup.cleanup() + if had_errors: + return 1 + else: + return 0 diff --git a/tempest_stress/etc/sample-unit-test.json b/tempest_stress/etc/sample-unit-test.json new file mode 100644 index 0000000..54433d5 --- /dev/null +++ b/tempest_stress/etc/sample-unit-test.json @@ -0,0 +1,8 @@ +[{"action": "tempest.stress.actions.unit_test.UnitTest", + "threads": 8, + "use_admin": true, + "use_isolated_tenants": true, + "kwargs": {"test_method": "tempest.cli.simple_read_only.test_glance.SimpleReadOnlyGlanceClientTest.test_glance_fake_action", + "class_setup_per": "process"} + } +] diff --git a/tempest_stress/etc/server-create-destroy-test.json b/tempest_stress/etc/server-create-destroy-test.json new file mode 100644 index 0000000..bbb5352 --- /dev/null +++ b/tempest_stress/etc/server-create-destroy-test.json @@ -0,0 +1,7 @@ +[{"action": "tempest.stress.actions.server_create_destroy.ServerCreateDestroyTest", + "threads": 8, + "use_admin": true, + "use_isolated_tenants": true, + "kwargs": {} + } +] diff --git a/tempest_stress/etc/ssh_floating.json b/tempest_stress/etc/ssh_floating.json new file mode 100644 index 0000000..c502e96 --- /dev/null +++ b/tempest_stress/etc/ssh_floating.json @@ -0,0 +1,16 @@ +[{"action": "tempest.stress.actions.ssh_floating.FloatingStress", + "threads": 8, + "use_admin": true, + "use_isolated_tenants": true, + "kwargs": {"vm_extra_args": {}, + "new_vm": true, + "new_sec_group": true, + "new_floating": true, + "verify": ["check_icmp_echo", "check_port_ssh"], + "check_timeout": 120, + "check_interval": 1, + "wait_after_vm_create": true, + "wait_for_disassociate": true, + "reboot": false} +} +] diff --git a/tempest_stress/etc/stress-tox-job.json b/tempest_stress/etc/stress-tox-job.json new file mode 100644 index 0000000..bfa448d --- /dev/null +++ b/tempest_stress/etc/stress-tox-job.json @@ -0,0 +1,28 @@ +[{"action": "tempest.stress.actions.server_create_destroy.ServerCreateDestroyTest", + "threads": 8, + "use_admin": true, + "use_isolated_tenants": true, + "kwargs": {} + }, + {"action": "tempest.stress.actions.volume_create_delete.VolumeCreateDeleteTest", + "threads": 4, + "use_admin": true, + "use_isolated_tenants": true, + "kwargs": {} + }, + {"action": "tempest.stress.actions.volume_attach_delete.VolumeAttachDeleteTest", + "threads": 2, + "use_admin": true, + "use_isolated_tenants": true, + "kwargs": {} + }, + {"action": "tempest.stress.actions.unit_test.UnitTest", + "threads": 4, + "use_admin": true, + "use_isolated_tenants": true, + "required_services": ["neutron"], + "kwargs": {"test_method": "tempest.scenario.test_network_advanced_server_ops.TestNetworkAdvancedServerOps.test_server_connectivity_stop_start", + "class_setup_per": "process"} + } +] + diff --git a/tempest_stress/etc/volume-attach-delete-test.json b/tempest_stress/etc/volume-attach-delete-test.json new file mode 100644 index 0000000..d468967 --- /dev/null +++ b/tempest_stress/etc/volume-attach-delete-test.json @@ -0,0 +1,7 @@ +[{"action": "tempest.stress.actions.volume_attach_delete.VolumeAttachDeleteTest", + "threads": 4, + "use_admin": true, + "use_isolated_tenants": true, + "kwargs": {} + } +] diff --git a/tempest_stress/etc/volume-attach-verify.json b/tempest_stress/etc/volume-attach-verify.json new file mode 100644 index 0000000..d8c96fd --- /dev/null +++ b/tempest_stress/etc/volume-attach-verify.json @@ -0,0 +1,11 @@ +[{"action": "tempest.stress.actions.volume_attach_verify.VolumeVerifyStress", + "threads": 1, + "use_admin": true, + "use_isolated_tenants": true, + "kwargs": {"vm_extra_args": {}, + "new_volume": true, + "new_server": false, + "ssh_test_before_attach": false, + "enable_ssh_verify": true} +} +] diff --git a/tempest_stress/etc/volume-create-delete-test.json b/tempest_stress/etc/volume-create-delete-test.json new file mode 100644 index 0000000..a60cde6 --- /dev/null +++ b/tempest_stress/etc/volume-create-delete-test.json @@ -0,0 +1,7 @@ +[{"action": "tempest.stress.actions.volume_create_delete.VolumeCreateDeleteTest", + "threads": 4, + "use_admin": true, + "use_isolated_tenants": true, + "kwargs": {} + } +] diff --git a/tempest_stress/plugin.py b/tempest_stress/plugin.py new file mode 100644 index 0000000..4574e73 --- /dev/null +++ b/tempest_stress/plugin.py @@ -0,0 +1,38 @@ +# Copyright 2015 Intel +# 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 os + +from tempest import config +from tempest.test_discover import plugins + +from tempest_stress import config as config_opts + + +class TempestStressPlugin(plugins.TempestPlugin): + def load_tests(self): + return {} + + def register_opts(self, conf): + config.register_opt_group(conf, + config_opts.stress_group, + config_opts.StressGroup) + + def get_opt_lists(self): + return [ + (config_opts.stress_group.name, + config_opts.StressGroup) + ] diff --git a/tempest_stress/stress/__init__.py b/tempest_stress/stress/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tempest_stress/stress/test_stress.py b/tempest_stress/stress/test_stress.py new file mode 100644 index 0000000..dfe0291 --- /dev/null +++ b/tempest_stress/stress/test_stress.py @@ -0,0 +1,54 @@ +# Copyright 2013 Deutsche Telekom AG +# 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 shlex +import subprocess + +from oslo_log import log as logging +from tempest.lib import exceptions +from tempest.tests import base + +LOG = logging.getLogger(__name__) + + +class StressFrameworkTest(base.TestCase): + """Basic test for the stress test framework.""" + + def _cmd(self, cmd, param): + """Executes specified command.""" + cmd = ' '.join([cmd, param]) + LOG.info("running: '%s'" % cmd) + cmd_str = cmd + cmd = shlex.split(cmd) + result = '' + result_err = '' + try: + stdout = subprocess.PIPE + stderr = subprocess.PIPE + proc = subprocess.Popen( + cmd, stdout=stdout, stderr=stderr) + result, result_err = proc.communicate() + if proc.returncode != 0: + LOG.debug('error of %s:\n%s' % (cmd_str, result_err)) + raise exceptions.CommandFailed(proc.returncode, + cmd, + result) + finally: + LOG.debug('output of %s:\n%s' % (cmd_str, result)) + return proc.returncode + + def test_help_function(self): + result = self._cmd("python", "-m tempest.cmd.run_stress -h") + self.assertEqual(0, result) diff --git a/tempest_stress/stress/test_stressaction.py b/tempest_stress/stress/test_stressaction.py new file mode 100644 index 0000000..1a1bb67 --- /dev/null +++ b/tempest_stress/stress/test_stressaction.py @@ -0,0 +1,63 @@ +# Copyright 2013 Deutsche Telekom AG +# 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 tempest.stress.stressaction as stressaction +import tempest.test + + +class FakeStressAction(stressaction.StressAction): + def __init__(self, manager, max_runs=None, stop_on_error=False): + super(self.__class__, self).__init__(manager, max_runs, stop_on_error) + self._run_called = False + + def run(self): + self._run_called = True + + @property + def run_called(self): + return self._run_called + + +class FakeStressActionFailing(stressaction.StressAction): + def run(self): + raise Exception('FakeStressActionFailing raise exception') + + +class TestStressAction(tempest.test.BaseTestCase): + def _bulid_stats_dict(self, runs=0, fails=0): + return {'runs': runs, 'fails': fails} + + def testStressTestRun(self): + stressAction = FakeStressAction(manager=None, max_runs=1) + stats = self._bulid_stats_dict() + stressAction.execute(stats) + self.assertTrue(stressAction.run_called) + self.assertEqual(stats['runs'], 1) + self.assertEqual(stats['fails'], 0) + + def testStressMaxTestRuns(self): + stressAction = FakeStressAction(manager=None, max_runs=500) + stats = self._bulid_stats_dict(runs=499) + stressAction.execute(stats) + self.assertTrue(stressAction.run_called) + self.assertEqual(stats['runs'], 500) + self.assertEqual(stats['fails'], 0) + + def testStressTestRunWithException(self): + stressAction = FakeStressActionFailing(manager=None, max_runs=1) + stats = self._bulid_stats_dict() + stressAction.execute(stats) + self.assertEqual(stats['runs'], 1) + self.assertEqual(stats['fails'], 1) diff --git a/tempest_stress/stressaction.py b/tempest_stress/stressaction.py new file mode 100644 index 0000000..cf0a08a --- /dev/null +++ b/tempest_stress/stressaction.py @@ -0,0 +1,96 @@ +# (c) Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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 abc +import signal +import sys + +import six + +from oslo_log import log as logging + + +@six.add_metaclass(abc.ABCMeta) +class StressAction(object): + + def __init__(self, manager, max_runs=None, stop_on_error=False): + full_cname = self.__module__ + "." + self.__class__.__name__ + self.logger = logging.getLogger(full_cname) + self.manager = manager + self.max_runs = max_runs + self.stop_on_error = stop_on_error + + def _shutdown_handler(self, signal, frame): + try: + self.tearDown() + except Exception: + self.logger.exception("Error while tearDown") + sys.exit(0) + + @property + def action(self): + """This methods returns the action. + + Overload this if you create a stress test wrapper. + """ + return self.__class__.__name__ + + def setUp(self, **kwargs): + """Initialize test structures/resources + + This method is called before "run" method to help the test + initialize any structures. kwargs contains arguments passed + in from the configuration json file. + + setUp doesn't count against the time duration. + """ + self.logger.debug("setUp") + + def tearDown(self): + """Cleanup test structures/resources + + This method is called to do any cleanup after the test is complete. + """ + self.logger.debug("tearDown") + + def execute(self, shared_statistic): + """This is the main execution entry point called by the driver. + + We register a signal handler to allow us to tearDown gracefully, + and then exit. We also keep track of how many runs we do. + """ + signal.signal(signal.SIGHUP, self._shutdown_handler) + signal.signal(signal.SIGTERM, self._shutdown_handler) + + while self.max_runs is None or (shared_statistic['runs'] < + self.max_runs): + self.logger.debug("Trigger new run (run %d)" % + shared_statistic['runs']) + try: + self.run() + except Exception: + shared_statistic['fails'] += 1 + self.logger.exception("Failure in run") + finally: + shared_statistic['runs'] += 1 + if self.stop_on_error and (shared_statistic['fails'] > 1): + self.logger.warning("Stop process due to" + "\"stop-on-error\" argument") + self.tearDown() + sys.exit(1) + + @abc.abstractmethod + def run(self): + """This method is where the stress test code runs.""" + return diff --git a/tempest_stress/tools/cleanup.py b/tempest_stress/tools/cleanup.py new file mode 100755 index 0000000..3885ba0 --- /dev/null +++ b/tempest_stress/tools/cleanup.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python + +# Copyright 2013 Quanta Research Cambridge, Inc. +# +# 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. + +from tempest.stress import cleanup + +cleanup.cleanup() diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..757b1b9 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,14 @@ +# The order of packages is significant, because pip processes them in the order +# of appearance. Changing the order has an impact on the overall integration +# process, which may cause wedges in the gate later. + +hacking<0.12,>=0.11.0 # Apache-2.0 + +coverage>=3.6 # Apache-2.0 +python-subunit>=0.0.18 # Apache-2.0/BSD +sphinx!=1.3b1,<1.3,>=1.2.1 # BSD +oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 +oslotest>=1.10.0 # Apache-2.0 +testrepository>=0.0.18 # Apache-2.0/BSD +testscenarios>=0.4 # Apache-2.0/BSD +testtools>=1.4.0 # MIT diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..e70934b --- /dev/null +++ b/tox.ini @@ -0,0 +1,43 @@ +[tox] +minversion = 2.0 +envlist = py34,py27,pypy,pep8 +skipsdist = True + +[testenv] +usedevelop = True +setenv = + VIRTUAL_ENV={envdir} + PYTHONWARNINGS=default::DeprecationWarning +deps = -r{toxinidir}/test-requirements.txt +commands = python setup.py test --slowest --testr-args='{posargs}' + +[testenv:stress] +envdir = .tox/tempest_stress +sitepackages = {[tempestenv]sitepackages} +setenv = {[tempestenv]setenv} +deps = {[tempestenv]deps} +commands = + run-tempest-stress {posargs} + +[testenv:pep8] +commands = flake8 {posargs} + +[testenv:venv] +commands = {posargs} + +[testenv:cover] +commands = python setup.py test --coverage --testr-args='{posargs}' + +[testenv:docs] +commands = python setup.py build_sphinx + +[testenv:debug] +commands = oslo_debug_helper {posargs} + +[flake8] +# E123, E125 skipped as they are invalid PEP-8. + +show-source = True +ignore = E123,E125 +builtins = _ +exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build