blob: 53a47b6de003f3845e3cc5b3cbd3370ad635d6d5 [file] [log] [blame]
Alex Savatieievd48994d2018-12-13 12:13:00 +01001"""Model Comparer:
2- yaml parser
3- class tree comparison
4"""
5import itertools
6# import json
7import os
8import yaml
9
10import reporter
11from ci_checker.common import utils
12from ci_checker.common import base_config, logger, logger_cli, PKG_DIR
13
14
Alex Savatieievf00743b2019-01-25 11:14:08 +010015global prefix_name
16global model_name_1, model_path_1
17global model_name_2, model_path_2
18
19prefix_name = "emk"
20model_name_1 = "dev"
21model_path_1 = "/Users/savex/proj/mediakind/reclass-dev"
22model_name_2 = "stg"
23model_path_2 = "/Users/savex/proj/mediakind/reclass-stg"
24
25
Alex Savatieievd48994d2018-12-13 12:13:00 +010026class ModelComparer(object):
27 """Collection of functions to compare model data.
28 """
29 models = {}
30
31 @staticmethod
32 def load_yaml_class(fname):
33 """Loads a yaml from the file and forms a tree item
34
35 Arguments:
36 fname {string} -- full path to the yaml file
37 """
38 _yaml = {}
39 try:
40 _size = 0
41 with open(fname, 'r') as f:
42 _yaml = yaml.load(f)
43 _size = f.tell()
44 # TODO: do smth with the data
45 if not _yaml:
46 logger_cli.warning("WARN: empty file '{}'".format(fname))
47 _yaml = {}
48 else:
49 logger.debug("...loaded YAML '{}' ({}b)".format(fname, _size))
50 return _yaml
51 except yaml.YAMLError as exc:
52 logger_cli.error(exc)
53 except IOError as e:
54 logger_cli.error(
55 "Error loading file '{}': {}".format(fname, e.message)
56 )
57 raise Exception("CRITICAL: Failed to load YAML data: {}".format(
Alex Savatieiev36b938d2019-01-21 11:01:18 +010058 e.message + e.strerror
Alex Savatieievd48994d2018-12-13 12:13:00 +010059 ))
60
61 def load_model_tree(self, name, root_path="/srv/salt/reclass"):
62 """Walks supplied path for the YAML filed and loads the tree
63
64 Arguments:
65 root_folder_path {string} -- Path to Model's root folder. Optional
66 """
67 logger_cli.info("Loading reclass tree from '{}'".format(root_path))
68 # prepare the file tree to walk
69 raw_tree = {}
70 # Credits to Andrew Clark@MIT. Original code is here:
71 # http://code.activestate.com/recipes/577879-create-a-nested-dictionary-from-oswalk/
72 root_path = root_path.rstrip(os.sep)
73 start = root_path.rfind(os.sep) + 1
74 root_key = root_path.rsplit(os.sep, 1)[1]
75 # Look Ma! I am walking the file tree with no recursion!
76 for path, dirs, files in os.walk(root_path):
77 # if this is a hidden folder, ignore it
78 _filders_list = path[start:].split(os.sep)
79 if any(item.startswith(".") for item in _filders_list):
80 continue
81 # cut absolute part of the path and split folder names
82 folders = path[start:].split(os.sep)
83 subdir = {}
84 # create generator of files that are not hidden
Alex Savatieiev36b938d2019-01-21 11:01:18 +010085 _exts = ('.yml', '.yaml')
86 _subfiles = (file for file in files
87 if file.endswith(_exts) and not file.startswith('.'))
Alex Savatieievd48994d2018-12-13 12:13:00 +010088 for _file in _subfiles:
89 # cut file extension. All reclass files are '.yml'
90 _subnode = _file
91 # load all YAML class data into the tree
92 subdir[_subnode] = self.load_yaml_class(
93 os.path.join(path, _file)
94 )
Alex Savatieiev36b938d2019-01-21 11:01:18 +010095 try:
96 # Save original filepath, just in case
97 subdir[_subnode]["_source"] = os.path.join(
98 path[start:],
99 _file
100 )
101 except Exception:
102 logger.warning(
103 "Non-yaml file detected: {}".format(_file)
104 )
Alex Savatieievd48994d2018-12-13 12:13:00 +0100105 # creating dict structure out of folder list. Pure python magic
106 parent = reduce(dict.get, folders[:-1], raw_tree)
107 parent[folders[-1]] = subdir
108 # save it as a single data object
109 self.models[name] = raw_tree[root_key]
110 return True
111
112 def generate_model_report_tree(self):
113 """Use all loaded models to generate comparison table with
114 values are groupped by YAML files
115 """
116 def find_changes(dict1, dict2, path=""):
117 _report = {}
118 for k in dict1.keys():
Alex Savatieievf00743b2019-01-25 11:14:08 +0100119 if not isinstance(k, str):
120 _new_path = path + ":" + str(k)
121 else:
122 _new_path = path + ":" + k
Alex Savatieievd48994d2018-12-13 12:13:00 +0100123 if k == "_source":
124 continue
Alex Savatieievf00743b2019-01-25 11:14:08 +0100125 # check if this is an env name cluster entry
126 if dict2 is not None and \
127 k == model_name_1 and \
128 model_name_2 in dict2.keys():
129 k1 = model_name_1
130 k2 = model_name_2
131 if type(dict1[k1]) is dict:
132 if path == "":
133 _new_path = k1
134 _child_report = find_changes(
135 dict1[k1],
136 dict2[k2],
137 _new_path
138 )
139 _report.update(_child_report)
140 elif dict2 is None or k not in dict2:
Alex Savatieievd48994d2018-12-13 12:13:00 +0100141 # no key in dict2
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100142 _report[_new_path] = {
143 "type": "value",
144 "raw_values": [dict1[k], "N/A"],
145 "str_values": [
146 "{}".format(dict1[k]),
147 "n/a"
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100148 ]
149 }
150 logger.info(
Alex Savatieievd48994d2018-12-13 12:13:00 +0100151 "{}: {}, {}".format(_new_path, dict1[k], "N/A")
152 )
153 else:
154 if type(dict1[k]) is dict:
155 if path == "":
156 _new_path = k
157 _child_report = find_changes(
158 dict1[k],
159 dict2[k],
160 _new_path
161 )
162 _report.update(_child_report)
Alex Savatieievf00743b2019-01-25 11:14:08 +0100163 elif type(dict1[k]) is list and type(dict2[k]) is list:
Alex Savatieievd48994d2018-12-13 12:13:00 +0100164 # use ifilterfalse to compare lists of dicts
Alex Savatieievf00743b2019-01-25 11:14:08 +0100165 try:
166 _removed = list(
167 itertools.ifilterfalse(
168 lambda x: x in dict2[k],
169 dict1[k]
170 )
Alex Savatieievd48994d2018-12-13 12:13:00 +0100171 )
Alex Savatieievf00743b2019-01-25 11:14:08 +0100172 _added = list(
173 itertools.ifilterfalse(
174 lambda x: x in dict1[k],
175 dict2[k]
176 )
Alex Savatieievd48994d2018-12-13 12:13:00 +0100177 )
Alex Savatieievf00743b2019-01-25 11:14:08 +0100178 except TypeError as e:
179 # debug routine,
180 # should not happen, due to list check above
181 logger.error(
182 "Caught lambda type mismatch: {}".format(
183 e.message
184 )
185 )
186 logger_cli.warning(
187 "Types mismatch for correct compare: "
188 "{}, {}".format(
189 type(dict1[k]),
190 type(dict2[k])
191 )
192 )
193 _removed = None
194 _added = None
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100195 _original = ["= {}".format(item) for item in dict1[k]]
Alex Savatieievd48994d2018-12-13 12:13:00 +0100196 if _removed or _added:
197 _removed_str_lst = ["- {}".format(item)
198 for item in _removed]
199 _added_str_lst = ["+ {}".format(item)
200 for item in _added]
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100201 _report[_new_path] = {
202 "type": "list",
203 "raw_values": [
204 dict1[k],
205 _removed_str_lst + _added_str_lst
206 ],
207 "str_values": [
208 "{}".format('\n'.join(_original)),
209 "{}\n{}".format(
210 '\n'.join(_removed_str_lst),
211 '\n'.join(_added_str_lst)
212 )
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100213 ]
214 }
215 logger.info(
Alex Savatieievd48994d2018-12-13 12:13:00 +0100216 "{}:\n"
217 "{} original items total".format(
218 _new_path,
219 len(dict1[k])
220 )
221 )
222 if _removed:
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100223 logger.info(
Alex Savatieievd48994d2018-12-13 12:13:00 +0100224 "{}".format('\n'.join(_removed_str_lst))
225 )
226 if _added:
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100227 logger.info(
Alex Savatieievd48994d2018-12-13 12:13:00 +0100228 "{}".format('\n'.join(_added_str_lst))
229 )
230 else:
Alex Savatieievf00743b2019-01-25 11:14:08 +0100231 # in case of type mismatch
232 # considering it as not equal
233 d1 = dict1
234 d2 = dict2
235 val1 = d1[k] if isinstance(d1, dict) else d1
236 val2 = d2[k] if isinstance(d2, dict) else d2
237 try:
238 match = val1 == val2
239 except TypeError as e:
240 logger.warning(
241 "One of the values is not a dict: "
242 "{}, {}".format(
243 str(dict1),
244 str(dict2)
245 ))
246 match = False
247 if not match:
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100248 _report[_new_path] = {
249 "type": "value",
Alex Savatieievf00743b2019-01-25 11:14:08 +0100250 "raw_values": [val1, val2],
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100251 "str_values": [
Alex Savatieievf00743b2019-01-25 11:14:08 +0100252 "{}".format(val1),
253 "{}".format(val2)
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100254 ]
255 }
256 logger.info("{}: {}, {}".format(
Alex Savatieievd48994d2018-12-13 12:13:00 +0100257 _new_path,
Alex Savatieievf00743b2019-01-25 11:14:08 +0100258 val1,
259 val2
Alex Savatieievd48994d2018-12-13 12:13:00 +0100260 ))
261 return _report
262 # tmp report for keys
263 diff_report = find_changes(
Alex Savatieievf00743b2019-01-25 11:14:08 +0100264 self.models[model_name_1],
265 self.models[model_name_2]
Alex Savatieievd48994d2018-12-13 12:13:00 +0100266 )
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100267 # prettify the report
268 for key in diff_report.keys():
269 # break the key in two parts
270 _ext = ".yml"
271 if ".yaml" in key:
272 _ext = ".yaml"
273 _split = key.split(_ext)
274 _file_path = _split[0]
275 _param_path = "none"
276 if len(_split) > 1:
277 _param_path = _split[1]
278 diff_report[key].update({
279 "class_file": _file_path + _ext,
280 "param": _param_path,
281 })
282
Alex Savatieievf00743b2019-01-25 11:14:08 +0100283 diff_report["diff_names"] = [model_name_1, model_name_2]
Alex Savatieievd48994d2018-12-13 12:13:00 +0100284 return diff_report
285
286
287# temporary executing the parser as a main prog
288if __name__ == '__main__':
289 mComparer = ModelComparer()
290 mComparer.load_model_tree(
Alex Savatieievf00743b2019-01-25 11:14:08 +0100291 model_name_1,
292 model_path_1
Alex Savatieievd48994d2018-12-13 12:13:00 +0100293 )
294 mComparer.load_model_tree(
Alex Savatieievf00743b2019-01-25 11:14:08 +0100295 model_name_2,
296 model_path_2
Alex Savatieievd48994d2018-12-13 12:13:00 +0100297 )
298 diffs = mComparer.generate_model_report_tree()
299
Alex Savatieievf00743b2019-01-25 11:14:08 +0100300 report_file = \
301 prefix_name + "-" + model_name_1 + "-vs-" + model_name_2 + ".html"
Alex Savatieievd48994d2018-12-13 12:13:00 +0100302 report = reporter.ReportToFile(
303 reporter.HTMLModelCompare(),
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100304 report_file
Alex Savatieievd48994d2018-12-13 12:13:00 +0100305 )
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100306 logger_cli.info("...generating report to {}".format(report_file))
307 report({
308 "nodes": {},
309 "diffs": diffs
310 })
Alex Savatieievd48994d2018-12-13 12:13:00 +0100311 # with open("./gen_tree.json", "w+") as _out:
312 # _out.write(json.dumps(mComparer.generate_model_report_tree))