CORD-1138 create corebuilder unit test framework; add corebuilder exceptions

Change-Id: I405e1cbc7652f9f70f2a7105ea43526e8604a30d
diff --git a/xos/tools/corebuilder/README.md b/xos/tools/corebuilder/README.md
new file mode 100644
index 0000000..0813fcc
--- /dev/null
+++ b/xos/tools/corebuilder/README.md
@@ -0,0 +1,28 @@
+# Corebuilder #
+
+Corebuilder is a build tool that is used to aid in generating the `xosproject/xos-ui` container image from the `xosproject/xos` container image. It does this by creating a `BUILD` tree that can be used to layer models and other files on top of the code base that is present in `xosproject/xos`. It's intended that corebuilder is run once to generate the BUILD tree, and then docker is run to generate the `xosproject/xos-ui` container image.  
+
+TODO: Say something about how Corebuilder integrates xproto here.
+
+## Running Corebuilder ##
+
+Corebuilder is usually containerized and run from inside its container. See `Dockerfile.corebuilder` in `containers/xos` for reference on how the Corebuilder container is built. Running the corebuilder container usually requires a few docker volume mounts be used. For example, to run from the CORD dev node:
+
+    docker run -it --rm -v /cord:/opt/cord -v /cord//orchestration/xos/containers/xos/BUILD:/opt/xos_corebuilder/BUILD /bin/bash docker-registry:5000/xosproject/xos-corebuilder:candidate <list of onboarding recipe pathnames>
+
+In a typical CORD build, Corebuilder will be run automatically by the `xos-core-build` role of `platform-install`. 
+
+## Running Unit Tests ##
+
+The corebuilder container image includes the dependencies necessary to run the tool. If this container image is constructed, then the unit tests may be run as follows:
+
+    docker run -it --rm --entrypoint python docker-registry:5000/xosproject/xos-corebuilder:candidate ./corebuilder_test.py
+
+If the container image is not available, then the local environment can be setup to run the unit test directly.
+
+1. Install the necessary python pip dependencies. See `containers/xos/pip_requirements.txt` for reference. At minimum, `tosca-parser==0.7.0` should be installed.
+2. Create a directory, `custom_types` in the directory where `corebuilder_test.py` is to be run from.
+3. Copy the contents of `xos/tosca/custom_types` to the `custom_types` directory created in step 2.
+4. Execute `corebuilder_test.py`
+
+
diff --git a/xos/tools/corebuilder/corebuilder.py b/xos/tools/corebuilder/corebuilder.py
index c2a584a..6e00962 100644
--- a/xos/tools/corebuilder/corebuilder.py
+++ b/xos/tools/corebuilder/corebuilder.py
@@ -39,6 +39,7 @@
             /opt/cord/orchestration/xos_libraries/ng-xos-lib/ng-xos-lib-onboard.yaml
 """
 
+import argparse
 import os
 import pdb

 import shutil

@@ -55,6 +56,24 @@
     if not os.path.exists(pathname):
         os.makedirs(pathname)
 
+class CoreBuilderException(Exception):
+    pass
+
+class CoreBuilderMissingRecipeException(CoreBuilderException):
+    pass
+
+class CoreBuilderMalformedValueException(CoreBuilderException):
+    pass
+
+class CoreBuilderMalformedUrlException(CoreBuilderException):
+    pass
+
+class CoreBuilderMissingResourceException(CoreBuilderException):
+    pass
+
+class CoreBuilderUnknownResourceException(CoreBuilderException):
+    pass
+
 class XOSCoreBuilder(object):
     def __init__(self, recipe_list, parent_dir=None):
         # TOSCA will look for imports using a relative path from where the
@@ -74,6 +93,8 @@
         self.app_names = []
 
         for recipe in recipe_list:
+            if not os.path.exists(recipe):
+                raise CoreBuilderMissingRecipeException("Onboarding Recipe %s does not exist" % recipe)
             tosca_yaml = open(recipe).read()
             self.execute_recipe(tosca_yaml)
 
@@ -149,7 +170,7 @@
             # Library works just like ServiceController

             self.execute_servicecontroller(nodetemplate)

         else:

-            raise Exception("Nodetemplate %s's type %s is not a known resource" % (nodetemplate.name, nodetemplate.type))

+            raise CoreBuilderUnknownResourceException("Nodetemplate %s's type %s is not a known resource" % (nodetemplate.name, nodetemplate.type))

 

     def execute_servicecontroller(self, nodetemplate):

         service_name = nodetemplate.name

@@ -193,9 +214,9 @@
                            if lhs=="subdirectory":
                                subdirectory=rhs
                            else:
-                               raise Exception("Malformed value %s" % value)
+                               raise CoreBuilderMalformedValueException("Malformed value %s in resource %s of recipe %s" % (v, k, nodetemplate.name))
                        else:
-                           raise Exception("Malformed value %s" % value)
+                           raise CoreBuilderMalformedValueException("Malformed value %s in resource %s of recipe %s" % (v, k, nodetemplate.name))
                     src_fn = parts[-1]

 

                 # apply base_url to src_fn

@@ -204,13 +225,13 @@
 

                 # ensure that it's a file:// url

                 if not src_fn.startswith("file://"):

-                    raise Exception("%s does not start with file://" % src_fn)

+                    raise CoreBuilderMalformedUrlException("Resource `%s: %s` of recipe %s does not start with file://" % (k, src_fn, nodetemplate.name))

                 src_fn = src_fn[7:]

 

                 src_fn = self.fixup_path(src_fn)

 

                 if not os.path.exists(src_fn):

-                    raise Exception("%s does not exist" % src_fn)

+                    raise CoreBuilderMissingResourceException("Resource '%s: %s' of recipe %s does not exist" % (k, src_fn, nodetemplate.name))

 

                 dest_dir = self.get_dest_dir(k, service_name)

                 dest_fn = os.path.join(dest_dir, subdirectory, os.path.basename(src_fn))

@@ -266,12 +287,40 @@
         makedirs_if_noexist(os.path.dirname(app_list_fn))

         file(app_list_fn, "w").write("\n".join(["services.%s" % x for x in self.app_names])+"\n")

 

-def main():

-   if len(sys.argv)<=1:

-       print >> sys.stderr, "Syntax: corebuilder.py [recipe1, recipe2, ...]"

+def parse_args():

+    parser = argparse.ArgumentParser()
+
+    _help = 'enable verbose logging'
+    parser.add_argument('-v', '--verbose',
+                        dest='verbose',
+                        action='store_true',
+                        default=False,
+                        help=_help)
+
+    _help = 'list of onboarding recipe filenames'
+    parser.add_argument('recipe_names',
+                        metavar="RECIPE",
+                        nargs='+',
+                        help=_help)
+
+    args = parser.parse_args()
+
+    return args
 

-   builder = XOSCoreBuilder(sys.argv[1:])

-   builder.build()

+def main():

+   global options

+

+   options = parse_args()

+

+   try:

+       builder = XOSCoreBuilder(options.recipe_names)

+       builder.build()

+   except CoreBuilderException, e:

+       if options.verbose:

+           traceback.print_exc()

+       else:

+           print >> sys.stderr, "Error:", str(e)

+       sys.exit(-1)

 

 if __name__ == "__main__":

     main()

diff --git a/xos/tools/corebuilder/corebuilder_test.py b/xos/tools/corebuilder/corebuilder_test.py
new file mode 100644
index 0000000..6bbf03b
--- /dev/null
+++ b/xos/tools/corebuilder/corebuilder_test.py
@@ -0,0 +1,157 @@
+import shutil
+import unittest
+from corebuilder import *
+
+class TestCoreBuilder(unittest.TestCase):
+    def setUp(self):
+        if not os.path.exists("/tmp/fake_library"):
+            os.mkdir("/tmp/fake_library")
+        file("/tmp/fake_library/fake.js","w").write("stuff")
+
+    def tearDown(self):
+        if os.path.exists("/tmp/fake_library"):
+           shutil.rmtree("/tmp/fake_library")
+        if os.path.exists("/opt/xos_corebuilder/BUILD/opt"):
+           shutil.rmtree("/opt/xos_corebuilder/BUILD/opt")
+        if os.path.exists("/tmp/recipe"):
+           os.remove("/tmp/recipe")
+
+    #--------------------------------------------------------------------------
+    # test_XOSCoreBuilder_init
+    #--------------------------------------------------------------------------
+
+    def test_XOSCoreBuilder_init(self):
+        recipe = \
+"""tosca_definitions_version: tosca_simple_yaml_1_0
+

+description: Onboard fake library

+

+imports:

+   - custom_types/xos.yaml

+

+topology_template:

+  node_templates:

+    library#fake:

+      type: tosca.nodes.Library

+      properties:

+          base_url: file:///tmp/fake_library/

+          vendor_js: fake.js

+"""
+        file("/tmp/recipe", "w").write(recipe)
+
+        CoreBuilderMissingResourceException(XOSCoreBuilder, ["/tmp/recipe"])
+
+    #--------------------------------------------------------------------------
+    # test_bad_recipe_name
+    #--------------------------------------------------------------------------
+
+    def test_bad_recipe_name(self):
+        self.assertRaises(CoreBuilderMissingRecipeException,
+                          XOSCoreBuilder,
+                          ["/tmp/does_not_exit"])
+
+    #--------------------------------------------------------------------------
+    # test_missing_resource
+    #--------------------------------------------------------------------------
+
+    def test_missing_resource(self):
+        recipe = \
+"""tosca_definitions_version: tosca_simple_yaml_1_0
+

+description: Onboard fake library

+

+imports:

+   - custom_types/xos.yaml

+

+topology_template:

+  node_templates:

+    library#fake:

+      type: tosca.nodes.Library

+      properties:

+          base_url: file:///tmp/fake_library/

+          vendor_js: missing_fake.js

+"""
+        file("/tmp/recipe", "w").write(recipe)
+        self.assertRaises(CoreBuilderMissingResourceException,
+                          XOSCoreBuilder,
+                          ["/tmp/recipe"])
+
+    #--------------------------------------------------------------------------
+    # test_malformed_url
+    #--------------------------------------------------------------------------
+
+    def test_malformed_url(self):
+        recipe = \
+"""tosca_definitions_version: tosca_simple_yaml_1_0
+
+description: Onboard fake library

+

+imports:

+   - custom_types/xos.yaml

+

+topology_template:

+  node_templates:

+    library#fake:

+      type: tosca.nodes.Library

+      properties:

+          base_url: http:///tmp/fake_library/

+          vendor_js: fake.js

+"""
+        file("/tmp/recipe", "w").write(recipe)
+
        self.assertRaises(CoreBuilderMalformedUrlException,
+                          XOSCoreBuilder,
+                          ["/tmp/recipe"])
+
+    #--------------------------------------------------------------------------
+    # test_malformed_value
+    #--------------------------------------------------------------------------
+
+    def test_malformed_value(self):
+        recipe = \
+"""tosca_definitions_version: tosca_simple_yaml_1_0
+
+description: Onboard fake library

+

+imports:

+   - custom_types/xos.yaml

+

+topology_template:

+  node_templates:

+    library#fake:

+      type: tosca.nodes.Library

+      properties:

+          base_url: file:///tmp/fake_library/

+          vendor_js: badvalue=bad fake.js

+"""
+        file("/tmp/recipe", "w").write(recipe)
+
        self.assertRaises(CoreBuilderMalformedValueException,
+                          XOSCoreBuilder,
+                          ["/tmp/recipe"])
+
+    #--------------------------------------------------------------------------
+    # test_unknown_resource
+    #--------------------------------------------------------------------------
+
+    def test_unknown_resource(self):
+        recipe = \
+"""tosca_definitions_version: tosca_simple_yaml_1_0
+
+description: Onboard fake library

+

+imports:

+   - custom_types/xos.yaml

+

+topology_template:

+  node_templates:

+    library#fake:

+      type: tosca.nodes.Slice

+"""

+        file("/tmp/recipe", "w").write(recipe)
+
        self.assertRaises(CoreBuilderUnknownResourceException,
+                          XOSCoreBuilder,
+                          ["/tmp/recipe"])
+
+if __name__ == '__main__':
+    unittest.main()
+
+