e5f59417f84d4acbe34d810059671aff287912e8
[vvp/validation-scripts.git] / ice_validator / tests / utils / nested_files.py
1 # -*- coding: utf8 -*-
2 # ============LICENSE_START====================================================
3 # org.onap.vvp/validation-scripts
4 # ===================================================================
5 # Copyright © 2017 AT&T Intellectual Property. All rights reserved.
6 # ===================================================================
7 #
8 # Unless otherwise specified, all software contained herein is licensed
9 # under the Apache License, Version 2.0 (the "License");
10 # you may not use this software except in compliance with the License.
11 # You may obtain a copy of the License at
12 #
13 #             http://www.apache.org/licenses/LICENSE-2.0
14 #
15 # Unless required by applicable law or agreed to in writing, software
16 # distributed under the License is distributed on an "AS IS" BASIS,
17 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 # See the License for the specific language governing permissions and
19 # limitations under the License.
20 #
21 #
22 #
23 # Unless otherwise specified, all documentation contained herein is licensed
24 # under the Creative Commons License, Attribution 4.0 Intl. (the "License");
25 # you may not use this documentation except in compliance with the License.
26 # You may obtain a copy of the License at
27 #
28 #             https://creativecommons.org/licenses/by/4.0/
29 #
30 # Unless required by applicable law or agreed to in writing, documentation
31 # distributed under the License is distributed on an "AS IS" BASIS,
32 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
33 # See the License for the specific language governing permissions and
34 # limitations under the License.
35 #
36 # ============LICENSE_END============================================
37 #
38 #
39
40 """nested files
41 """
42 from functools import lru_cache
43 from os import path, listdir
44 import re
45 from tests import cached_yaml as yaml
46 from tests.structures import Heat
47
48 from tests.helpers import load_yaml
49
50 VERSION = "1.4.0"
51
52 """
53 test nesting depth
54 0 -> 1 -> 2 -> too deep.
55 """
56 MAX_DEPTH = 3
57
58
59 def check_for_invalid_nesting(  # pylint: disable=too-many-branches
60     yml, yaml_file, dirpath
61 ):
62     """
63     return a list of all nested files
64     """
65     if not hasattr(yml, "items"):
66         return []
67     invalid_nesting = []
68     p = re.compile("^[A-z]*::[A-z]*::[A-z]*$")
69
70     for v in yml.values():
71         if isinstance(v, dict) and "type" in v:
72             t = v["type"]
73             if t.endswith(".yml") or t.endswith(".yaml"):
74                 filepath = path.join(dirpath, t)
75             elif t == "OS::Heat::ResourceGroup":
76                 rd = v["properties"]["resource_def"]
77                 if not isinstance(rd, dict) or "type" not in rd:
78                     invalid_nesting.append(yaml_file)
79                     continue
80                 elif not p.match(rd["type"]):
81                     filepath = path.join(dirpath, rd["type"])
82                 else:
83                     continue
84             else:
85                 continue
86             try:
87                 with open(filepath) as fh:
88                     yml = yaml.load(fh)
89             except yaml.YAMLError as e:
90                 invalid_nesting.append(filepath)
91                 print(e)  # pylint: disable=superfluous-parens
92             invalid_nesting.extend(check_for_invalid_nesting(yml, filepath, dirpath))
93         if isinstance(v, dict):
94             invalid_nesting.extend(check_for_invalid_nesting(v, yaml_file, dirpath))
95         elif isinstance(v, list):
96             for d in v:
97                 invalid_nesting.extend(check_for_invalid_nesting(d, yaml_file, dirpath))
98     return invalid_nesting
99
100
101 def get_dict_of_nested_files(yml, dirpath):
102     """Return dict.
103     key: resource id in yml which references a nested file.
104     value: the nested file name.
105     Nested files are either referenced through "type", or
106     for OS::Heat::ResourceGroup, through "resource_def type".
107     """
108     nested_files = get_type_nested_files(yml, dirpath)
109     nested_files.update(get_resourcegroup_nested_files(yml, dirpath))
110     return nested_files
111
112
113 @lru_cache(maxsize=None)
114 def get_list_of_nested_files(yml_path, dirpath):
115     """
116     return a list of all nested files
117     """
118
119     yml = load_yaml(yml_path)
120     nested_files = []
121     resources = yml.get("resources") or {}
122
123     for v in resources.values():
124         if isinstance(v, dict) and "type" in v:
125             t = v["type"]
126             if t.endswith(".yml") or t.endswith(".yaml"):
127                 filepath = path.join(dirpath, t)
128                 if path.exists(filepath):
129                     nested_files.append(filepath)
130                     nested_files.extend(get_list_of_nested_files(filepath, dirpath))
131             elif t == "OS::Heat::ResourceGroup":
132                 rdt = v.get("properties", {}).get("resource_def", {}).get("type", None)
133                 if rdt and (rdt.endswith(".yml") or rdt.endswith(".yaml")):
134                     filepath = path.join(dirpath, rdt)
135                     if path.exists(filepath):
136                         nested_files.append(filepath)
137                         nested_files.extend(get_list_of_nested_files(filepath, dirpath))
138     return nested_files
139
140
141 def get_nesting(yaml_files):
142     """return bad, files, heat, depths
143     bad - list of error messages.
144     files - dict: key is filename, value is dict of nested files.
145             This is the tree.
146     heat - dict,: key is filename, value is Heat instance.
147     depths - dict: key is filename, value is a depth tuple
148
149     level: 0           1         2         3
150     file:  template -> nested -> nested -> nested
151     depth: 3           2         1         0
152     """
153     bad = []
154     files = {}
155     heat = {}
156     depths = {}
157     for yaml_file in yaml_files:
158         dirname, basename = path.split(yaml_file)
159         h = Heat(filepath=yaml_file)
160         heat[basename] = h
161         files[basename] = get_dict_of_nested_files(h.yml, dirname)
162     for filename in files:
163         depths[filename] = _get_nesting_depth_start(0, filename, files, [])
164         for depth in depths[filename]:
165             if depth[0] > MAX_DEPTH:
166                 bad.append("{} {}".format(filename, str(depth[1])))
167     return bad, files, heat, depths
168
169
170 def _get_nesting_depth_start(depth, filename, files, context):
171     depths = []
172     for rid, nf in files[filename].items():
173         depths.append(_get_nesting_depth(1, nf, files, context))
174     return depths
175
176
177 def _get_nesting_depth(depth, filename, files, context):
178     """Return a depth tuple (max_depth, current_context).
179     `context` is the list of filenames.
180     `depth` is the length of `context`.
181     Finds the max_depth of all the resources of `filename`.
182     current_context is the updated list of filenames
183     and max_depth is its length.
184     """
185     max_depth = depth + 1
186     current_context = context + [filename]
187     if depth <= MAX_DEPTH:
188         nested_filenames = files.get(filename, {})
189         if nested_filenames:
190             max_depth, current_context = max(
191                 _get_nesting_depth(depth + 1, nested_filename, files, current_context)
192                 for nested_filename in nested_filenames.values()
193             )
194     return max_depth, current_context
195
196
197 def get_resourcegroup_nested_files(yml, dirpath):
198     """
199     return a dict.
200     key: key in yml which references a nested ResourceGroup file.
201         (resource->type is ResourceGroup
202             and resource->properties->resource_def->type is a yaml file)
203     value: the nested file name.
204
205     The keys are assumed to be unique across files.
206     A separate test checks for that.
207     """
208
209     if not hasattr(yml, "get"):
210         return {}
211
212     nested_files = {}
213     for rid, r in yml.get("resources", {}).items():
214         if isinstance(r, dict) and "type" in r:
215             t = r["type"]
216             nested_file = None
217             if t == "OS::Heat::ResourceGroup":
218                 rdt = r.get("properties", {}).get("resource_def", {}).get("type", None)
219                 if rdt and (rdt.endswith(".yml") or rdt.endswith(".yaml")):
220                     nested_file = rdt
221             if nested_file:
222                 filepath = path.join(dirpath, nested_file)
223                 if path.exists(filepath):
224                     nested_files[rid] = nested_file
225     return nested_files
226
227
228 def get_type_nested_files(yml, dirpath):
229     """
230     return a dict.
231     key: key in yml which references a nested type file.
232         (the resource "type" is a yaml file.)
233     value: the nested file name.
234
235     The keys are assumed to be unique across files.
236     A separate test checks for that.
237     """
238
239     if not hasattr(yml, "get"):
240         return {}
241
242     nested_files = {}
243     for rid, r in yml.get("resources", {}).items():
244         if isinstance(r, dict) and "type" in r:
245             t = r["type"]
246             nested_file = None
247             if t.endswith(".yml") or t.endswith(".yaml"):
248                 nested_file = t
249             if nested_file:
250                 filepath = path.join(dirpath, nested_file)
251                 if path.exists(filepath):
252                     nested_files[rid] = nested_file
253     return nested_files
254
255
256 def get_nested_files(filenames):
257     """
258     returns all the nested files for a set of filenames
259     """
260     nested_files = []
261     for filename in filenames:
262         if file_is_a_nested_template(filename):
263             nested_files.append(filename)
264     return nested_files
265
266
267 @lru_cache(maxsize=None)
268 def file_is_a_nested_template(file):
269     directory = path.dirname(file)
270     nested_files = []
271     for filename in listdir(directory):
272         if filename.endswith(".yaml") or filename.endswith(".yml"):
273             filename = "{}/{}".format(directory, filename)
274             try:
275                 nested_files.extend(
276                     get_list_of_nested_files(filename, path.dirname(filename))
277                 )
278             except yaml.YAMLError as e:
279                 print(e)  # pylint: disable=superfluous-parens
280                 continue
281     return file in nested_files