blob: d644679250f69b54ce2cb01d40dd59a5059da355 [file] [log] [blame]
Alex Savatieievd48994d2018-12-13 12:13:00 +01001"""Model Comparer:
2- yaml parser
3- class tree comparison
4"""
5import itertools
Alex Savatieievd48994d2018-12-13 12:13:00 +01006import os
7import yaml
8
9import reporter
Alex Savatieiev5118de02019-02-20 15:50:42 -060010from cfg_checker.common import logger, logger_cli
Alex Savatieievd48994d2018-12-13 12:13:00 +010011
12
Alex Savatieievf00743b2019-01-25 11:14:08 +010013global prefix_name
14global model_name_1, model_path_1
15global model_name_2, model_path_2
16
17prefix_name = "emk"
18model_name_1 = "dev"
19model_path_1 = "/Users/savex/proj/mediakind/reclass-dev"
20model_name_2 = "stg"
21model_path_2 = "/Users/savex/proj/mediakind/reclass-stg"
22
23
Alex Savatieievd48994d2018-12-13 12:13:00 +010024class ModelComparer(object):
25 """Collection of functions to compare model data.
26 """
27 models = {}
28
29 @staticmethod
30 def load_yaml_class(fname):
31 """Loads a yaml from the file and forms a tree item
32
33 Arguments:
34 fname {string} -- full path to the yaml file
35 """
36 _yaml = {}
37 try:
38 _size = 0
39 with open(fname, 'r') as f:
40 _yaml = yaml.load(f)
41 _size = f.tell()
42 # TODO: do smth with the data
43 if not _yaml:
44 logger_cli.warning("WARN: empty file '{}'".format(fname))
45 _yaml = {}
46 else:
47 logger.debug("...loaded YAML '{}' ({}b)".format(fname, _size))
48 return _yaml
49 except yaml.YAMLError as exc:
50 logger_cli.error(exc)
51 except IOError as e:
52 logger_cli.error(
53 "Error loading file '{}': {}".format(fname, e.message)
54 )
55 raise Exception("CRITICAL: Failed to load YAML data: {}".format(
Alex Savatieiev36b938d2019-01-21 11:01:18 +010056 e.message + e.strerror
Alex Savatieievd48994d2018-12-13 12:13:00 +010057 ))
58
59 def load_model_tree(self, name, root_path="/srv/salt/reclass"):
60 """Walks supplied path for the YAML filed and loads the tree
61
62 Arguments:
63 root_folder_path {string} -- Path to Model's root folder. Optional
64 """
65 logger_cli.info("Loading reclass tree from '{}'".format(root_path))
66 # prepare the file tree to walk
67 raw_tree = {}
68 # Credits to Andrew Clark@MIT. Original code is here:
69 # http://code.activestate.com/recipes/577879-create-a-nested-dictionary-from-oswalk/
70 root_path = root_path.rstrip(os.sep)
71 start = root_path.rfind(os.sep) + 1
72 root_key = root_path.rsplit(os.sep, 1)[1]
73 # Look Ma! I am walking the file tree with no recursion!
74 for path, dirs, files in os.walk(root_path):
75 # if this is a hidden folder, ignore it
76 _filders_list = path[start:].split(os.sep)
77 if any(item.startswith(".") for item in _filders_list):
78 continue
79 # cut absolute part of the path and split folder names
80 folders = path[start:].split(os.sep)
81 subdir = {}
82 # create generator of files that are not hidden
Alex Savatieiev36b938d2019-01-21 11:01:18 +010083 _exts = ('.yml', '.yaml')
84 _subfiles = (file for file in files
85 if file.endswith(_exts) and not file.startswith('.'))
Alex Savatieievd48994d2018-12-13 12:13:00 +010086 for _file in _subfiles:
87 # cut file extension. All reclass files are '.yml'
88 _subnode = _file
89 # load all YAML class data into the tree
90 subdir[_subnode] = self.load_yaml_class(
91 os.path.join(path, _file)
92 )
Alex Savatieiev36b938d2019-01-21 11:01:18 +010093 try:
94 # Save original filepath, just in case
95 subdir[_subnode]["_source"] = os.path.join(
96 path[start:],
97 _file
98 )
99 except Exception:
100 logger.warning(
101 "Non-yaml file detected: {}".format(_file)
102 )
Alex Savatieievd48994d2018-12-13 12:13:00 +0100103 # creating dict structure out of folder list. Pure python magic
104 parent = reduce(dict.get, folders[:-1], raw_tree)
105 parent[folders[-1]] = subdir
106 # save it as a single data object
107 self.models[name] = raw_tree[root_key]
108 return True
109
110 def generate_model_report_tree(self):
Alex Savatieiev0137dad2019-01-25 16:18:42 +0100111 """Use two loaded models to generate comparison table with
Alex Savatieievd48994d2018-12-13 12:13:00 +0100112 values are groupped by YAML files
113 """
114 def find_changes(dict1, dict2, path=""):
115 _report = {}
116 for k in dict1.keys():
Alex Savatieiev0137dad2019-01-25 16:18:42 +0100117 # yamls might load values as non-str types
Alex Savatieievf00743b2019-01-25 11:14:08 +0100118 if not isinstance(k, str):
119 _new_path = path + ":" + str(k)
120 else:
121 _new_path = path + ":" + k
Alex Savatieiev0137dad2019-01-25 16:18:42 +0100122 # ignore _source key
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))