Create discovery option for stress tests

Adds a decorator @stresstest which automatically sets the type
attr to "stress". It uses the testtools discover functionality to
automatically discover stress tests out of all tempest test.

It is possible to filter stress test with a given attribute type.
For instance to filter out only smoke tests that have a stress test
attribute.

blueprint: stress-tests
Change-Id: I8acf0b608cb500c2679a36a4a00ca4fa14668fad
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index f1aaa07..d6de7a5 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -387,3 +387,11 @@
 heat = false
 # Whether or not horizon is expected to be available
 horizon = True
+
+[stress]
+# Maximum number of instances to create during test
+max_instances = 32
+# Time (in seconds) between log file error checks
+log_check_interval = 60
+# The default number of threads created while stress test
+default_thread_number_per_action=4
\ No newline at end of file
diff --git a/tempest/api/volume/test_volumes_actions.py b/tempest/api/volume/test_volumes_actions.py
index 5861497..9e3ff67 100644
--- a/tempest/api/volume/test_volumes_actions.py
+++ b/tempest/api/volume/test_volumes_actions.py
@@ -18,6 +18,7 @@
 from tempest.api.volume.base import BaseVolumeTest
 from tempest.common.utils.data_utils import rand_name
 from tempest.test import attr
+from tempest.test import stresstest
 
 
 class VolumesActionsTest(BaseVolumeTest):
@@ -52,6 +53,7 @@
 
         super(VolumesActionsTest, cls).tearDownClass()
 
+    @stresstest(class_setup_per='process')
     @attr(type='smoke')
     def test_attach_detach_volume_to_instance(self):
         # Volume is attached and detached successfully from an instance
@@ -70,6 +72,7 @@
             self.assertEqual(202, resp.status)
             self.client.wait_for_volume_status(self.volume['id'], 'available')
 
+    @stresstest(class_setup_per='process')
     @attr(type='gate')
     def test_get_volume_attachment(self):
         # Verify that a volume's attachment information is retrieved
diff --git a/tempest/config.py b/tempest/config.py
index 9b1a91e..c6a291e 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -515,7 +515,10 @@
                help='regexp for list of log files.'),
     cfg.StrOpt('log_check_interval',
                default=60,
-               help='time between log file error checks.')
+               help='time (in seconds) between log file error checks.'),
+    cfg.StrOpt('default_thread_number_per_action',
+               default=4,
+               help='The number of threads created while stress test.')
 ]
 
 
diff --git a/tempest/stress/driver.py b/tempest/stress/driver.py
index efc57a9..5171da2 100644
--- a/tempest/stress/driver.py
+++ b/tempest/stress/driver.py
@@ -102,6 +102,8 @@
     """
     logfiles = admin_manager.config.stress.target_logfiles
     log_check_interval = int(admin_manager.config.stress.log_check_interval)
+    default_thread_num = int(admin_manager.config.stress.
+                             default_thread_number_per_action)
     if logfiles:
         controller = admin_manager.config.stress.target_controller
         computes = _get_compute_nodes(controller)
@@ -112,7 +114,7 @@
             manager = admin_manager
         else:
             manager = clients.Manager()
-        for p_number in xrange(test.get('threads', 1)):
+        for p_number in xrange(test.get('threads', default_thread_num)):
             if test.get('use_isolated_tenants', False):
                 username = rand_name("stress_user")
                 tenant_name = rand_name("stress_tenant")
diff --git a/tempest/stress/run_stress.py b/tempest/stress/run_stress.py
index 32e3ae0..aab2afd 100755
--- a/tempest/stress/run_stress.py
+++ b/tempest/stress/run_stress.py
@@ -19,13 +19,52 @@
 import argparse
 import json
 import sys
+from testtools.testsuite import iterate_tests
+from unittest import loader
+
+
+def discover_stress_tests(path="./", filter_attr=None):
+    """Discovers all tempest tests and create action out of them
+    """
+
+    tests = []
+    testloader = loader.TestLoader()
+    list = testloader.discover(path)
+    for func in (iterate_tests(list)):
+        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 not filter_attr 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
+                                 }
+                      }
+            tests.append(action)
+    return tests
 
 
 def main(ns):
     # NOTE(mkoderer): moved import to make "-h" possible without OpenStack
     from tempest.stress import driver
     result = 0
-    tests = json.load(open(ns.tests, 'r'))
+    if not ns.all:
+        tests = json.load(open(ns.tests, 'r'))
+    else:
+        tests = discover_stress_tests(filter_attr=ns.type)
+
     if ns.serial:
         for test in tests:
             step_result = driver.stress_openstack([test],
@@ -49,7 +88,13 @@
                     default=False, help="Stop on first error.")
 parser.add_argument('-n', '--number', type=int,
                     help="How often an action is executed for each process.")
-parser.add_argument('tests', help="Name of the file with test description.")
+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)")
+group.add_argument('-t', "--tests", nargs='?',
+                   help="Name of the file with test description.")
 
 if __name__ == "__main__":
     sys.exit(main(parser.parse_args()))
diff --git a/tempest/stress/stressaction.py b/tempest/stress/stressaction.py
index 3719841..6284cef 100644
--- a/tempest/stress/stressaction.py
+++ b/tempest/stress/stressaction.py
@@ -60,6 +60,8 @@
 
         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:
diff --git a/tempest/test.py b/tempest/test.py
index 0cd0b08..ced9580 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -56,6 +56,27 @@
     return decorator
 
 
+def stresstest(*args, **kwargs):
+    """Add stress test decorator
+
+    For all functions with this decorator a attr stress will be
+    set automatically.
+
+    @param class_setup_per: allowed values are application, process, action
+           ``application``: once in the stress job lifetime
+           ``process``: once in the worker process lifetime
+           ``action``: on each action
+    """
+    def decorator(f):
+        if 'class_setup_per' in kwargs:
+            setattr(f, "st_class_setup_per", kwargs['class_setup_per'])
+        else:
+            setattr(f, "st_class_setup_per", 'process')
+        attr(type='stress')(f)
+        return f
+    return decorator
+
+
 # there is a mis-match between nose and testtools for older pythons.
 # testtools will set skipException to be either
 # unittest.case.SkipTest, unittest2.case.SkipTest or an internal skip
diff --git a/tox.ini b/tox.ini
index dc8980d..5445cd5 100644
--- a/tox.ini
+++ b/tox.ini
@@ -45,7 +45,7 @@
 sitepackages = True
 setenv = VIRTUAL_ENV={envdir}
 commands =
-    python -m tempest/stress/run_stress tempest/stress/etc/stress-tox-job.json -d 3600
+    python -m tempest/stress/run_stress -a -d 3600
 
 [testenv:venv]
 commands = {posargs}