Providing plug-in support:

Typically, a test program is written to perform certain basic tests on a test
machine.  For example, one might write an "obmc_boot" program that performs
various boot tests on the Open BMC machine.

Experience has shown that over time, additional testing needs often arise.
Examples of such additional testing needs might include:

- Data base logging of results
- Performance measurements
- Memory leak analysis
- Hardware verification
- Error log (sels) analysis
- SOL_console

The developer could add additional parms to obmc_boot and likewise add
supporting code in obmc_boot each time a need arises.  Users would employ
these new functions as follows:

obmc_boot --perf=1 --mem_leak=1 --db_logging=1 --db_userid=xxxx

However, another option would be to add general-purpose plug-in support to
obmc_boot.  This would allow the user to indicate to obmc_boot which plug-in
packages it ought to run.  Such plug-in packages could be written in any
langauge whatsoever: Robot, python, bash, perl, C++.

An example call to obmc_boot would then look something like this:

obmc_boot --plug_in_dir_paths="Perf:Mem_leak:DB_logging"

Now all the obmc_boot developer needs to do is call the plug-in processing
module (process_plug_in_packages.py) at various call points which are agreed
upon by the obmc_boot developer and the plug-in developers.  Example call
points which can be implemented are:

setup - Called at the start of obmc_boot
pre_boot - Called before each boot test initiated by obmc_boot
post_boot - Called after each boot test initiated by obmc_boot
cleanup - Called at the end of obmc_boot

This allows the choice of options to be passed as data to obmc_boot.  The
advantages of this approach are:
- Much less maintenance of the original test program (obmc_boot).
- Since plug-ins are separate from the main test program, users are free to
have plug-ins that suit their environments.  One user may wish to log results
to a database that is of no interest to the rest of the world.  Such a plug-in
can be written and need never be pushed to gerrit/github.
- One can even write temporary plug-ins designed just to collect data or stop
when a particular defect occurs.

In our current environment, the concept has proven exceedingly useful.  We
have over 40 permanent plug-ins and in our temp plug-in directory, we still
have over 80 plug-ins.

Change-Id: Iee0ea950cffaef202d56da4dae7c044b6366a59c
Signed-off-by: Michael Walsh <micwalsh@us.ibm.com>
diff --git a/lib/gen_plug_in.py b/lib/gen_plug_in.py
new file mode 100755
index 0000000..4c90473
--- /dev/null
+++ b/lib/gen_plug_in.py
@@ -0,0 +1,217 @@
+#!/usr/bin/env python
+
+r"""
+This module provides functions which are useful for running plug-ins.
+"""
+
+import sys
+import os
+import commands
+import glob
+
+import gen_print as gp
+import gen_misc as gm
+
+# Some help text that is common to more than one program.
+plug_in_dir_paths_help_text = \
+    'This is a colon-separated list of plug-in directory paths.  If one' +\
+    ' of the entries in the list is a plain directory name (i.e. no' +\
+    ' path info), it will be taken to be a native plug-in.  In that case,' +\
+    ' %(prog)s will search for the native plug-in in the "plug-ins"' +\
+    ' subdirectory of each path in the PATH environment variable until it' +\
+    ' is found.  Also, integrated plug-ins will automatically be appended' +\
+    ' to your plug_in_dir_paths list.  An integrated plug-in is any plug-in' +\
+    ' found using the PATH variable that contains a file named "integrated".'
+
+mch_class_help_text = \
+    'The class of machine that we are testing (e.g. "op" = "open power",' +\
+    ' "obmc" = "open bmc", etc).'
+
+PATH_LIST = gm.return_path_list()
+
+
+###############################################################################
+def get_plug_in_base_paths():
+
+    r"""
+    Get plug-in base paths and return them as a list.
+
+    This function searches the PATH_LIST (created from PATH environment
+    variable) for any paths that have a "plug_ins" subdirectory.  All such
+    paths are considered plug_in_base paths.
+    """
+
+    global PATH_LIST
+
+    plug_in_base_path_list = []
+
+    for path in PATH_LIST:
+        candidate_plug_in_base_path = path + "plug_ins/"
+        if os.path.isdir(candidate_plug_in_base_path):
+            plug_in_base_path_list.append(candidate_plug_in_base_path)
+
+    return plug_in_base_path_list
+
+###############################################################################
+# Define global plug_in_base_path_list and call get_plug_in_base_paths to set
+# its value.
+plug_in_base_path_list = get_plug_in_base_paths()
+
+
+###############################################################################
+def find_plug_in_package(plug_in_name):
+
+    r"""
+    Find and return the normalized directory path of the specified plug in.
+    This is done by searching the global plug_in_base_path_list.
+
+    Description of arguments:
+    plug_in_name                    The unqualified name of the plug-in
+                                    package.
+    """
+
+    global plug_in_base_path_list
+    for plug_in_base_dir_path in plug_in_base_path_list:
+        candidate_plug_in_dir_path = os.path.normpath(plug_in_base_dir_path +
+                                                      plug_in_name) + \
+                                                      os.sep
+        if os.path.isdir(candidate_plug_in_dir_path):
+            return candidate_plug_in_dir_path
+
+    return ""
+
+###############################################################################
+
+
+###############################################################################
+def validate_plug_in_package(plug_in_dir_path,
+                             mch_class="obmc"):
+
+    r"""
+    Validate the plug in package and return the normalized plug-in directory
+    path.
+
+    Description of arguments:
+    plug_in_dir_path                The "relative" or absolute path to a plug
+                                    in package directory.
+    mch_class                       The class of machine that we are testing
+                                    (e.g. "op" = "open power", "obmc" = "open
+                                    bmc", etc).
+    """
+
+    gp.dprint_executing()
+
+    if os.path.isabs(plug_in_dir_path):
+        # plug_in_dir_path begins with a slash so it is an absolute path.
+        candidate_plug_in_dir_path = os.path.normpath(plug_in_dir_path) +\
+                                     os.sep
+        if not os.path.isdir(candidate_plug_in_dir_path):
+            gp.print_error_report("Plug-in directory path \"" +
+                                  plug_in_dir_path + "\" does not exist.\n")
+            exit(1)
+    else:
+        # The plug_in_dir_path is actually a simple name (e.g.
+        # "OBMC_Sample")...
+        candidate_plug_in_dir_path = find_plug_in_package(plug_in_dir_path)
+        if candidate_plug_in_dir_path == "":
+            global PATH_LIST
+            gp.print_error_report("Plug-in directory path \"" +
+                                  plug_in_dir_path + "\" could not be found" +
+                                  " in any of the following directories:\n" +
+                                  gp.sprint_var(PATH_LIST))
+            exit(1)
+    # Make sure that this plug-in supports us...
+    supports_file_path = candidate_plug_in_dir_path + "supports_" + mch_class
+    if not os.path.exists(supports_file_path):
+        gp.print_error_report("The following file path could not be" +
+                              " found:\n" +
+                              gp.sprint_varx("supports_file_path",
+                                             supports_file_path) +
+                              "\nThis file is necessary to indicate that" +
+                              " the given plug-in supports the class of" +
+                              " machine we are testing, namely \"" +
+                              mch_class + "\".\n")
+        exit(1)
+
+    return candidate_plug_in_dir_path
+
+###############################################################################
+
+
+###############################################################################
+def return_integrated_plug_ins(mch_class="obmc"):
+
+    r"""
+    Return a list of integrated plug-ins.  Integrated plug-ins are plug-ins
+    which are selected without regard for whether the user has specified them.
+    In other words, they are "integrated" into the program suite.  The
+    programmer designates a plug-in as integrated by putting a file named
+    "integrated" into the plug-in package directory.
+
+    Description of arguments:
+    mch_class                       The class of machine that we are testing
+                                    (e.g. "op" = "open power", "obmc" = "open
+                                    bmc", etc).
+    """
+
+    global plug_in_base_path_list
+
+    integrated_plug_ins_list = []
+
+    for plug_in_base_path in plug_in_base_path_list:
+        # Get a list of all plug-in paths that support our mch_class.
+        mch_class_candidate_list = glob.glob(plug_in_base_path +
+                                             "*/supports_" + mch_class)
+        for candidate_path in mch_class_candidate_list:
+            integrated_plug_in_dir_path = os.path.dirname(candidate_path) +\
+                                          os.sep
+            integrated_file_path = integrated_plug_in_dir_path + "integrated"
+            if os.path.exists(integrated_file_path):
+                plug_in_name = \
+                    os.path.basename(os.path.dirname(candidate_path))
+                if plug_in_name not in integrated_plug_ins_list:
+                    # If this plug-in has not already been added to the list...
+                    integrated_plug_ins_list.append(plug_in_name)
+
+    return integrated_plug_ins_list
+
+###############################################################################
+
+
+###############################################################################
+def return_plug_in_packages_list(plug_in_dir_paths,
+                                 mch_class="obmc"):
+
+    r"""
+    Return a list of plug-in packages given the plug_in_dir_paths string.
+    This function calls validate_plug_in_package so it will fail if
+    plug_in_dir_paths contains any invalid plug-ins.
+
+    Description of arguments:
+    plug_in_dir_path                The "relative" or absolute path to a plug
+                                    in package directory.
+    mch_class                       The class of machine that we are testing
+                                    (e.g. "op" = "open power", "obmc" = "open
+                                    bmc", etc).
+    """
+
+    if plug_in_dir_paths != "":
+        plug_in_packages_list = plug_in_dir_paths.split(":")
+    else:
+        plug_in_packages_list = []
+
+    # Get a list of integrated plug-ins (w/o full path names).
+    integrated_plug_ins_list = return_integrated_plug_ins(mch_class)
+    # Put both lists together in plug_in_packages_list with no duplicates.
+    # NOTE: This won't catch duplicates if the caller specifies the full path
+    # name of a native plug-in but that should be rare enough.
+
+    plug_in_packages_list = plug_in_packages_list + integrated_plug_ins_list
+
+    plug_in_packages_list = \
+        list(set([validate_plug_in_package(path, mch_class)
+                  for path in plug_in_packages_list]))
+
+    return plug_in_packages_list
+
+###############################################################################