aboutsummaryrefslogtreecommitdiffstats
path: root/check-blueprint-vs-input/bin/check-blueprint-vs-input
blob: c6b271b3853dd0fb4906a8063167c10ee4ac0fb1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
#!/usr/bin/env python3
# -*- indent-tabs-mode: nil -*-
# Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. 
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this code 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.

from __future__ import print_function

"""

 NAME
    check-blueprint-vs-input - given a blueprint and inputs file pair, validate them against each other

 USAGE
    check-blueprint-vs-input [-v] [-t] -b BLUEPRINT [-B exclusion-list] -i INPUTS [-B exclusion-list]

 DESCRIPTION
"""
description = """
    Validate a blueprint and inputs file against each other. This looks for the inputs: node of the blueprint
    file, the inputs used by {get_input} within the blueprint, and the values found in the inputs file. The
    files may be in either YAML or JSON formats. The names default to blueprint.yaml and inputs.yaml. If
    a blueprint inputs name has a default value, it is not considered an error if it is not in the inputs file.

    If using a template inputs file, add the -t/--template option. This will look for the inputs under
    an "inputs:" node instead of at the top level.

    If there are blueprint nodes or inputs nodes that should not be considered an error, specify them
    using the -B/--blueprint-exclusion-list and -I/inputs-exclusion-list parameters.

    "check-blueprint-vs-input --help" will list all of the available options.
"""

import sys, argparse, json, yaml
from yaml.composer import Composer
from yaml.constructor import Constructor
from yaml.constructor import SafeConstructor
from yaml.constructor import ScalarNode

def main():
    DEF_BLUEPRINT_NAME = "blueprint.yaml"
    DEF_INPUTS_NAME = "inputs.yaml"
    parser = argparse.ArgumentParser(description=description)
    parser.add_argument("-b", "--blueprint", type=str, help="Path to blueprint file, defaults to '%s'" % DEF_BLUEPRINT_NAME,
                        default=DEF_BLUEPRINT_NAME)
    parser.add_argument("-i", "--inputs", type=str, help="Port to listen on, defaults to '%s'" % DEF_INPUTS_NAME,
                        default=DEF_INPUTS_NAME)
    parser.add_argument("-B", "--blueprint-exclusion-list", type=str, help="Comma-separated list of names not to warn about not being in the blueprint file", default="")
    parser.add_argument("-I", "--inputs-exclusion-list", type=str, help="Comma-separated list of names not to warn about not being in the inputs file", default="")
    parser.add_argument("-t", "--inputs-template", help="Treat inputs file as coming from template area", action="store_true")
    parser.add_argument("-v", "--verbose", help="Verbose, may be specified multiple times", action="count", default=0)
    args = parser.parse_args()

    blueprintExclusionList = args.blueprint_exclusion_list.split(",")
    if args.verbose: print("blueprintExclusionList=%s" % blueprintExclusionList)

    inputsExclusionList = args.inputs_exclusion_list.split(",")
    if args.verbose: print("inputsExclusionList=%s" % inputsExclusionList)

    def loadYaml(filename, where):
        """
        Load a YAML file

        Line number manipulation is inspired by:
        https://stackoverflow.com/questions/13319067/parsing-yaml-return-with-line-number

        The YAML loader parses the file first into a set of nodes. Capture the
        line numbers and column numbers during that parsing pass.
        The YAML object is then created from those objects.
        """

        def compose_node(parent, index):
            lineno = loader.line    # the line number where the previous token has ended (plus empty lines)
            # column = loader.column
            node = Composer.compose_node(loader, parent, index)
            node.__lineno__ = lineno + 1
            # node.__column__ = column + 1
            return node

        def construct_scalar(node):
            where[node.value] = str(node.__lineno__) # + ":" + str(node.__column__)
            return SafeConstructor.construct_scalar(loader, node)

        def construct_mapping(node, deep=False):
            mapping = SafeConstructor.construct_mapping(loader, node, deep=deep)
            mapping['__lineno__'] = str(node.__lineno__) # + ":" + str(node.__column__)
            return mapping

        yread = None
        try:
            with open(filename, "r") as fd:
                yread = fd.read()
        except:
            type, value, traceback = sys.exc_info()
            sys.exit(value)

        loader = yaml.SafeLoader(yread)
        loader.compose_node = compose_node
        loader.construct_mapping = construct_mapping
        loader.construct_scalar = construct_scalar
        data = loader.get_single_data()
        if args.verbose > 2:
            print("================ %s ================" % filename)
            yaml.dump(data, sys.stdout)
            print("================================")
        return data

    blueprint = loadYaml(args.blueprint, {})
    inputsWhere = { }
    inputs = loadYaml(args.inputs, inputsWhere)

    # if inputs file is empty, provide an empty dictionary
    if inputs is None: inputs = { }

    # blueprint file has inputs under the inputs: node
    blueprintInputs = blueprint['inputs']

    # inputs file normally has inputs at the top level,
    # but templated inputs files have themunder the inputs: node
    if args.inputs_template: inputs = inputs['inputs']


    exitval = 0

    def check_blueprint_inputs(blueprintInputs, inputs, inputsExclusionList):
        """
        check the blueprint inputs against the inputs file
        """
        foundone = False
        for input in blueprintInputs:
            if input == '__lineno__': continue
            if args.verbose: print("blueprint input=%s\n%s" % (input, blueprintInputs[input]))
            if input in inputs:
                if args.verbose: print("\tIS in inputs file")
            else:
                # print("blueprintInputs.get(input)=%s and blueprintInputs[input].get('default')=%s" % (blueprintInputs.get(input), blueprintInputs[input].get('default')))
                if blueprintInputs.get(input) and blueprintInputs[input].get('default'):
                    if args.verbose: print("\tHAS a default value")
                elif input not in inputsExclusionList:
                    print("<<<<<<<<<<<<<<<< %s (blueprint line %s) not in inputs file" % (input, blueprintInputs[input].get('__lineno__')))
                    foundone = True
                else:
                    if args.verbose: print("<<<<<<<<<<<<<<<< %s not in inputs file, but being ignored" % input)
        return foundone

    # check the blueprint inputs: against the inputs file
    if args.verbose: print("================ check the blueprint inputs: against the inputs file")
    foundone = check_blueprint_inputs(blueprintInputs, inputs, inputsExclusionList)
    if foundone: print("")
    if foundone: exitval = 1

    def prettyprint(msg,j):
        print(msg)
        json.dump(j, sys.stdout, indent=4, sort_keys=True)
        print("")

    def check_get_inputs(blueprint, blueprintInputs, inputs, inputsExclusionList):
        """
        check the blueprint get_input values against the inputs file
        """

        def findInputs(d, where):
            if args.verbose > 2: print("check_get_inputs(): d=%s" % d)
            ret = [ ]
            if isinstance(d, dict):
                if args.verbose: print("type(d) is dict")
                for key,val in d.items():
                    linecol = d.get('__lineno__')
                    if args.verbose: print("looking at d[key=%s], line=%s" % (key, linecol))
                    if key == '__lineno__': continue
                    if key == "get_input":
                        if args.verbose: print("found get_input, adding '%s'" % val)
                        ret += [ val ]
                        if not where.get(val): where[val] = str(linecol)
                        else: where[val] += "," + str(linecol)
                        return ret
                    else:
                        if args.verbose: print("going recursive on '%s'" % val)
                        ret += findInputs(val, where)
            elif isinstance(d, list):
                if args.verbose: print("type(d) is list")
                for val in d:
                    if args.verbose: print("going recursive on '%s'" % val)
                    ret += findInputs(val, where)
            else:
                if args.verbose: print("type(d) is scalar: %s" % d)
            return ret

        foundone = False
        where = {}
        inputList = findInputs(blueprint, where)
        if args.verbose:
            print("done looking for get_input, found:\n%s" % inputList)
            prettyprint("where=",where)
        alreadySeen = { }
        for input in inputList:
            if input not in alreadySeen:
                alreadySeen[input] = True
                if args.verbose: print("checking input %s" % input)
                if input in inputs:
                    if args.verbose: print("\tIS in input file")
                else:
                    if blueprintInputs.get(input) and blueprintInputs[input].get('default'):
                        if args.verbose: print("\tHAS a default value")
                    elif input not in inputsExclusionList:
                        line = where[input]
                        s = "s" if line.find(",") >= 0 else ""
                        print(":::::::::::::::: get_input: {0} is NOT in input file (blueprint line{1} {2})".format(input, s, line))
                        foundone = True
                    else:
                        if args.verbose:
                            line = where[input]
                            s = "s" if line.find(",") >= 0 else ""
                            print(":::::::::::::::: get_input: %s is NOT in input file (blueprint line{1} {2}), but being ignored" % (input, s, line))

        return foundone



    # check the blueprint's get_input calls against the inputs file
    if args.verbose: print("================ check the blueprint's get_input calls against the inputs file ================")
    foundone = check_get_inputs(blueprint, blueprintInputs, inputs, inputsExclusionList)
    if foundone: print("")
    if foundone: exitval = 1

    def check_inputs(blueprintInputs, inputs, blueprintExclusionList):
        """
        check the inputs file against the blueprints inputs list
        """
        foundone = False
        # prettyprint("inputs=", inputs)
        for key,val in inputs.items():
            if key == '__lineno__': continue
            if args.verbose: print("inputs key=%s" % key)
            # print("inputs key=%s, line=%s, val=%s" % (key,inputsWhere[key],val)) # DELETE
            if key in blueprintInputs:
                if args.verbose: print("\tIS in blueprint")
            else:
                if key not in blueprintExclusionList:
                    print(">>>>>>>>>>>>>>>> %s is in inputs file (around line %s) but not in blueprint file" % (key, inputsWhere[key]))
                    foundone = True
                else:
                    if args.verbose: print(">>>>>>>>>>>>>>>> %s is in inputs file (around line %s), but not in blueprint file and being ignored" % (key, inputsWhere[key]))
        return foundone

    # check the inputs file against the blueprints input: section
    if args.verbose: print("================ check the inputs file against the blueprints input: section ================")
    foundone = check_inputs(blueprintInputs, inputs, blueprintExclusionList)
    if foundone: exitval = 1
    sys.exit(exitval)

if __name__ == "__main__":
    main()