Add basic external test plugin support to tempest

This commit starts the basic framework for using external plugins in
tempest. It adds a new singleton class to load the plugins once from
stevedore and also provides an interface for different steps in the
tempest execution to use plugins as well as in-tree code.

As part of this an ABC abstract class is created to simplify the
plugin side creation. Eventually the expectation is that this
abstract class will live in tempest-lib. But, for right now while
this feature is still experimental and under development this will
likely change frequently so it'll live in tempest for the time being.

Partially Implements bp external-plugin-interface

Change-Id: I8ebabdb4ce9f4d3b3aca375158835f907d5ca315
diff --git a/requirements.txt b/requirements.txt
index d15a5a1..66e5b16 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -24,3 +24,4 @@
 testscenarios>=0.4
 tempest-lib>=0.6.1
 PyYAML>=3.1.0
+stevedore>=1.5.0 # Apache-2.0
diff --git a/tempest/test_discover/plugins.py b/tempest/test_discover/plugins.py
new file mode 100644
index 0000000..197bd0c
--- /dev/null
+++ b/tempest/test_discover/plugins.py
@@ -0,0 +1,56 @@
+# Copyright (c) 2015 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 six
+import stevedore
+from tempest_lib.common.utils import misc
+
+
+@six.add_metaclass(abc.ABCMeta)
+class TempestPlugin(object):
+    """A TempestPlugin class provides the basic hooks for an external
+    plugin to provide tempest the necessary information to run the plugin.
+    """
+
+    @abc.abstractmethod
+    def load_tests(self):
+        """Method to return the information necessary to load the tests in the
+        plugin.
+
+        :return: a tuple with the first value being the test_dir and the second
+                 being the top_level
+        :rtype: tuple
+        """
+        return
+
+
+@misc.singleton
+class TempestTestPluginManager(object):
+    """Tempest test plugin manager class
+
+    This class is used to manage the lifecycle of external tempest test
+    plugins. It provides functions for getting set
+    """
+    def __init__(self):
+        self.ext_plugins = stevedore.ExtensionManager(
+            'tempest.test.plugins', invoke_on_load=True,
+            propagate_map_exceptions=True)
+
+    def get_plugin_load_tests_tuple(self):
+        load_tests_dict = {}
+        for plug in self.ext_plugins:
+            load_tests_dict[plug.name] = plug.obj.load_tests()
+        return load_tests_dict
diff --git a/tempest/test_discover/test_discover.py b/tempest/test_discover/test_discover.py
index 4a4b43a..a871d10 100644
--- a/tempest/test_discover/test_discover.py
+++ b/tempest/test_discover/test_discover.py
@@ -15,6 +15,8 @@
 import os
 import sys
 
+from tempest.test_discover import plugins
+
 if sys.version_info >= (2, 7):
     import unittest
 else:
@@ -22,9 +24,12 @@
 
 
 def load_tests(loader, tests, pattern):
+    ext_plugins = plugins.TempestTestPluginManager()
+
     suite = unittest.TestSuite()
     base_path = os.path.split(os.path.dirname(os.path.abspath(__file__)))[0]
     base_path = os.path.split(base_path)[0]
+    # Load local tempest tests
     for test_dir in ['./tempest/api', './tempest/scenario',
                      './tempest/thirdparty']:
         if not pattern:
@@ -32,4 +37,17 @@
         else:
             suite.addTests(loader.discover(test_dir, pattern=pattern,
                            top_level_dir=base_path))
+
+    plugin_load_tests = ext_plugins.get_plugin_load_tests_tuple()
+    if not plugin_load_tests:
+        return suite
+
+    # Load any installed plugin tests
+    for plugin in plugin_load_tests:
+        test_dir, top_path = plugin_load_tests[plugin]
+        if not pattern:
+            suite.addTests(loader.discover(test_dir, top_level=top_path))
+        else:
+            suite.addTests(loader.discover(test_dir, pattern=pattern,
+                                           top_level=top_path))
     return suite
diff --git a/test-requirements.txt b/test-requirements.txt
index 9bd3f46..2b3607d 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -10,4 +10,3 @@
 mock>=1.0
 coverage>=3.6
 oslotest>=1.5.1 # Apache-2.0
-stevedore>=1.5.0 # Apache-2.0