Allow nested validation for template-validate

Adds an optional flag that causes template-validate to recursively
validate nested stacks and expose a schema of the parameters for
all stacks in the tree.  This uses a similar method to stack preview
in that it builds the tree in memory so we can then walk it and
extract the parameter data we need.

Change-Id: I7e3d752124997a0a7b8ff53e774ed8034dd2f81d
Co-Authored-By: Jay Dobies <jdobies@redhat.com>
Implements-blueprint: nested-validation
diff --git a/functional/test_template_validate.py b/functional/test_template_validate.py
index bd5aaca..e62c31b 100644
--- a/functional/test_template_validate.py
+++ b/functional/test_template_validate.py
@@ -35,6 +35,29 @@
       length: {get_param: aparam}
 '''
 
+    parent_template = '''
+heat_template_version: 2014-10-16
+description: the parent template
+parameters:
+  pparam:
+    type: number
+    default: 5
+    description: the param description
+resources:
+  nres:
+    type: mynested.yaml
+    properties:
+      aparam: {get_param: pparam}
+'''
+
+    parent_template_noprop = '''
+heat_template_version: 2014-10-16
+description: the parent template
+resources:
+  nres:
+    type: mynested.yaml
+'''
+
     random_template_groups = '''
 heat_template_version: 2014-10-16
 description: the stack description
@@ -147,3 +170,75 @@
                       'NoEcho': 'true',
                       'Type': 'String'}}}
         self.assertEqual(expected, ret)
+
+    def test_template_validate_nested_off(self):
+        files = {'mynested.yaml': self.random_template}
+        ret = self.client.stacks.validate(template=self.parent_template,
+                                          files=files)
+        expected = {'Description': 'the parent template',
+                    'Parameters': {
+                        'pparam': {'Default': 5,
+                                   'Description': 'the param description',
+                                   'Label': 'pparam',
+                                   'NoEcho': 'false',
+                                   'Type': 'Number'}}}
+        self.assertEqual(expected, ret)
+
+    def test_template_validate_nested_on(self):
+        files = {'mynested.yaml': self.random_template}
+        ret = self.client.stacks.validate(template=self.parent_template_noprop,
+                                          files=files,
+                                          show_nested=True)
+        expected = {'Description': 'the parent template',
+                    'Parameters': {},
+                    'NestedParameters': {
+                        'nres': {'Description': 'the stack description',
+                                 'Parameters': {'aparam': {'Default': 10,
+                                                           'Description':
+                                                           'the param '
+                                                           'description',
+                                                           'Label': 'aparam',
+                                                           'NoEcho': 'false',
+                                                           'Type': 'Number'}},
+                                 'Type': 'mynested.yaml'}}}
+        self.assertEqual(expected, ret)
+
+    def test_template_validate_nested_on_multiple(self):
+        # parent_template -> nested_template -> random_template
+        nested_template = self.random_template.replace(
+            'OS::Heat::RandomString', 'mynested2.yaml')
+        files = {'mynested.yaml': nested_template,
+                 'mynested2.yaml': self.random_template}
+        ret = self.client.stacks.validate(template=self.parent_template,
+                                          files=files,
+                                          show_nested=True)
+
+        n_param2 = {'myres': {'Description': 'the stack description',
+                              'Parameters': {'aparam': {'Default': 10,
+                                                        'Description':
+                                                        'the param '
+                                                        'description',
+                                                        'Label': 'aparam',
+                                                        'NoEcho': 'false',
+                                                        'Type': 'Number'}},
+                              'Type': 'mynested2.yaml'}}
+        expected = {'Description': 'the parent template',
+                    'Parameters': {
+                        'pparam': {'Default': 5,
+                                   'Description': 'the param description',
+                                   'Label': 'pparam',
+                                   'NoEcho': 'false',
+                                   'Type': 'Number'}},
+                    'NestedParameters': {
+                        'nres': {'Description': 'the stack description',
+                                 'Parameters': {'aparam': {'Default': 10,
+                                                           'Description':
+                                                           'the param '
+                                                           'description',
+                                                           'Label': 'aparam',
+                                                           'Value': 5,
+                                                           'NoEcho': 'false',
+                                                           'Type': 'Number'}},
+                                 'NestedParameters': n_param2,
+                                 'Type': 'mynested.yaml'}}}
+        self.assertEqual(expected, ret)