blob: ee658bd87c3a2474812b27c039cba713a37e1540 [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
13class ModelComparer(object):
14 """Collection of functions to compare model data.
15 """
16 models = {}
Alex Savatieiev06ab17d2019-02-26 18:40:48 -060017 models_path = "/srv/salt/reclass"
18 model_name_1 = "source"
19 model_path_1 = os.path.join(models_path, model_name_1)
20 model_name_2 = "target"
21 model_path_2 = os.path.join(models_path, model_name_1)
Alex Savatieievd48994d2018-12-13 12:13:00 +010022
23 @staticmethod
24 def load_yaml_class(fname):
25 """Loads a yaml from the file and forms a tree item
26
27 Arguments:
28 fname {string} -- full path to the yaml file
29 """
30 _yaml = {}
31 try:
32 _size = 0
33 with open(fname, 'r') as f:
34 _yaml = yaml.load(f)
35 _size = f.tell()
36 # TODO: do smth with the data
37 if not _yaml:
38 logger_cli.warning("WARN: empty file '{}'".format(fname))
39 _yaml = {}
40 else:
41 logger.debug("...loaded YAML '{}' ({}b)".format(fname, _size))
42 return _yaml
43 except yaml.YAMLError as exc:
44 logger_cli.error(exc)
45 except IOError as e:
46 logger_cli.error(
47 "Error loading file '{}': {}".format(fname, e.message)
48 )
49 raise Exception("CRITICAL: Failed to load YAML data: {}".format(
Alex Savatieiev36b938d2019-01-21 11:01:18 +010050 e.message + e.strerror
Alex Savatieievd48994d2018-12-13 12:13:00 +010051 ))
52
53 def load_model_tree(self, name, root_path="/srv/salt/reclass"):
54 """Walks supplied path for the YAML filed and loads the tree
55
56 Arguments:
57 root_folder_path {string} -- Path to Model's root folder. Optional
58 """
59 logger_cli.info("Loading reclass tree from '{}'".format(root_path))
60 # prepare the file tree to walk
61 raw_tree = {}
62 # Credits to Andrew Clark@MIT. Original code is here:
63 # http://code.activestate.com/recipes/577879-create-a-nested-dictionary-from-oswalk/
64 root_path = root_path.rstrip(os.sep)
65 start = root_path.rfind(os.sep) + 1
66 root_key = root_path.rsplit(os.sep, 1)[1]
67 # Look Ma! I am walking the file tree with no recursion!
68 for path, dirs, files in os.walk(root_path):
69 # if this is a hidden folder, ignore it
Alex Savatieiev06ab17d2019-02-26 18:40:48 -060070 _folders_list = path[start:].split(os.sep)
71 if any(item.startswith(".") for item in _folders_list):
Alex Savatieievd48994d2018-12-13 12:13:00 +010072 continue
73 # cut absolute part of the path and split folder names
74 folders = path[start:].split(os.sep)
75 subdir = {}
76 # create generator of files that are not hidden
Alex Savatieiev36b938d2019-01-21 11:01:18 +010077 _exts = ('.yml', '.yaml')
Alex Savatieiev06ab17d2019-02-26 18:40:48 -060078 _subfiles = (_fl for _fl in files
79 if _fl.endswith(_exts) and not _fl.startswith('.'))
Alex Savatieievd48994d2018-12-13 12:13:00 +010080 for _file in _subfiles:
81 # cut file extension. All reclass files are '.yml'
82 _subnode = _file
83 # load all YAML class data into the tree
84 subdir[_subnode] = self.load_yaml_class(
85 os.path.join(path, _file)
86 )
Alex Savatieiev36b938d2019-01-21 11:01:18 +010087 try:
88 # Save original filepath, just in case
89 subdir[_subnode]["_source"] = os.path.join(
90 path[start:],
91 _file
92 )
93 except Exception:
94 logger.warning(
95 "Non-yaml file detected: {}".format(_file)
96 )
Alex Savatieievd48994d2018-12-13 12:13:00 +010097 # creating dict structure out of folder list. Pure python magic
98 parent = reduce(dict.get, folders[:-1], raw_tree)
99 parent[folders[-1]] = subdir
100 # save it as a single data object
101 self.models[name] = raw_tree[root_key]
102 return True
103
104 def generate_model_report_tree(self):
Alex Savatieiev0137dad2019-01-25 16:18:42 +0100105 """Use two loaded models to generate comparison table with
Alex Savatieievd48994d2018-12-13 12:13:00 +0100106 values are groupped by YAML files
107 """
108 def find_changes(dict1, dict2, path=""):
109 _report = {}
110 for k in dict1.keys():
Alex Savatieiev0137dad2019-01-25 16:18:42 +0100111 # yamls might load values as non-str types
Alex Savatieievf00743b2019-01-25 11:14:08 +0100112 if not isinstance(k, str):
113 _new_path = path + ":" + str(k)
114 else:
115 _new_path = path + ":" + k
Alex Savatieiev0137dad2019-01-25 16:18:42 +0100116 # ignore _source key
Alex Savatieievd48994d2018-12-13 12:13:00 +0100117 if k == "_source":
118 continue
Alex Savatieievf00743b2019-01-25 11:14:08 +0100119 # check if this is an env name cluster entry
120 if dict2 is not None and \
Alex Savatieiev06ab17d2019-02-26 18:40:48 -0600121 k == self.model_name_1 and \
122 self.model_name_2 in dict2.keys():
123 k1 = self.model_name_1
124 k2 = self.model_name_2
Alex Savatieievf00743b2019-01-25 11:14:08 +0100125 if type(dict1[k1]) is dict:
126 if path == "":
127 _new_path = k1
128 _child_report = find_changes(
129 dict1[k1],
130 dict2[k2],
131 _new_path
132 )
133 _report.update(_child_report)
134 elif dict2 is None or k not in dict2:
Alex Savatieievd48994d2018-12-13 12:13:00 +0100135 # no key in dict2
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100136 _report[_new_path] = {
137 "type": "value",
138 "raw_values": [dict1[k], "N/A"],
139 "str_values": [
140 "{}".format(dict1[k]),
141 "n/a"
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100142 ]
143 }
144 logger.info(
Alex Savatieievd48994d2018-12-13 12:13:00 +0100145 "{}: {}, {}".format(_new_path, dict1[k], "N/A")
146 )
147 else:
148 if type(dict1[k]) is dict:
149 if path == "":
150 _new_path = k
151 _child_report = find_changes(
152 dict1[k],
153 dict2[k],
154 _new_path
155 )
156 _report.update(_child_report)
Alex Savatieievf00743b2019-01-25 11:14:08 +0100157 elif type(dict1[k]) is list and type(dict2[k]) is list:
Alex Savatieievd48994d2018-12-13 12:13:00 +0100158 # use ifilterfalse to compare lists of dicts
Alex Savatieievf00743b2019-01-25 11:14:08 +0100159 try:
160 _removed = list(
161 itertools.ifilterfalse(
162 lambda x: x in dict2[k],
163 dict1[k]
164 )
Alex Savatieievd48994d2018-12-13 12:13:00 +0100165 )
Alex Savatieievf00743b2019-01-25 11:14:08 +0100166 _added = list(
167 itertools.ifilterfalse(
168 lambda x: x in dict1[k],
169 dict2[k]
170 )
Alex Savatieievd48994d2018-12-13 12:13:00 +0100171 )
Alex Savatieievf00743b2019-01-25 11:14:08 +0100172 except TypeError as e:
173 # debug routine,
174 # should not happen, due to list check above
175 logger.error(
176 "Caught lambda type mismatch: {}".format(
177 e.message
178 )
179 )
180 logger_cli.warning(
181 "Types mismatch for correct compare: "
182 "{}, {}".format(
183 type(dict1[k]),
184 type(dict2[k])
185 )
186 )
187 _removed = None
188 _added = None
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100189 _original = ["= {}".format(item) for item in dict1[k]]
Alex Savatieievd48994d2018-12-13 12:13:00 +0100190 if _removed or _added:
191 _removed_str_lst = ["- {}".format(item)
192 for item in _removed]
193 _added_str_lst = ["+ {}".format(item)
194 for item in _added]
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100195 _report[_new_path] = {
196 "type": "list",
197 "raw_values": [
198 dict1[k],
199 _removed_str_lst + _added_str_lst
200 ],
201 "str_values": [
202 "{}".format('\n'.join(_original)),
203 "{}\n{}".format(
204 '\n'.join(_removed_str_lst),
205 '\n'.join(_added_str_lst)
206 )
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100207 ]
208 }
209 logger.info(
Alex Savatieievd48994d2018-12-13 12:13:00 +0100210 "{}:\n"
211 "{} original items total".format(
212 _new_path,
213 len(dict1[k])
214 )
215 )
216 if _removed:
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100217 logger.info(
Alex Savatieievd48994d2018-12-13 12:13:00 +0100218 "{}".format('\n'.join(_removed_str_lst))
219 )
220 if _added:
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100221 logger.info(
Alex Savatieievd48994d2018-12-13 12:13:00 +0100222 "{}".format('\n'.join(_added_str_lst))
223 )
224 else:
Alex Savatieievf00743b2019-01-25 11:14:08 +0100225 # in case of type mismatch
226 # considering it as not equal
227 d1 = dict1
228 d2 = dict2
229 val1 = d1[k] if isinstance(d1, dict) else d1
230 val2 = d2[k] if isinstance(d2, dict) else d2
231 try:
232 match = val1 == val2
233 except TypeError as e:
234 logger.warning(
235 "One of the values is not a dict: "
236 "{}, {}".format(
237 str(dict1),
238 str(dict2)
239 ))
240 match = False
241 if not match:
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100242 _report[_new_path] = {
243 "type": "value",
Alex Savatieievf00743b2019-01-25 11:14:08 +0100244 "raw_values": [val1, val2],
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100245 "str_values": [
Alex Savatieievf00743b2019-01-25 11:14:08 +0100246 "{}".format(val1),
247 "{}".format(val2)
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100248 ]
249 }
250 logger.info("{}: {}, {}".format(
Alex Savatieievd48994d2018-12-13 12:13:00 +0100251 _new_path,
Alex Savatieievf00743b2019-01-25 11:14:08 +0100252 val1,
253 val2
Alex Savatieievd48994d2018-12-13 12:13:00 +0100254 ))
255 return _report
256 # tmp report for keys
257 diff_report = find_changes(
Alex Savatieiev06ab17d2019-02-26 18:40:48 -0600258 self.models[self.model_name_1],
259 self.models[self.model_name_2]
Alex Savatieievd48994d2018-12-13 12:13:00 +0100260 )
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100261 # prettify the report
262 for key in diff_report.keys():
263 # break the key in two parts
264 _ext = ".yml"
265 if ".yaml" in key:
266 _ext = ".yaml"
267 _split = key.split(_ext)
268 _file_path = _split[0]
269 _param_path = "none"
270 if len(_split) > 1:
271 _param_path = _split[1]
272 diff_report[key].update({
273 "class_file": _file_path + _ext,
274 "param": _param_path,
275 })
276
Alex Savatieiev06ab17d2019-02-26 18:40:48 -0600277 diff_report["diff_names"] = [self.model_name_1, self.model_name_2]
Alex Savatieievd48994d2018-12-13 12:13:00 +0100278 return diff_report
279
280
Alex Savatieiev06ab17d2019-02-26 18:40:48 -0600281def compare_models():
282 # Do actual compare using hardcoded model names
Alex Savatieievd48994d2018-12-13 12:13:00 +0100283 mComparer = ModelComparer()
284 mComparer.load_model_tree(
Alex Savatieiev06ab17d2019-02-26 18:40:48 -0600285 mComparer.model_name_1,
286 mComparer.model_path_1
Alex Savatieievd48994d2018-12-13 12:13:00 +0100287 )
288 mComparer.load_model_tree(
Alex Savatieiev06ab17d2019-02-26 18:40:48 -0600289 mComparer.model_name_2,
290 mComparer.model_path_2
Alex Savatieievd48994d2018-12-13 12:13:00 +0100291 )
292 diffs = mComparer.generate_model_report_tree()
293
Alex Savatieievf00743b2019-01-25 11:14:08 +0100294 report_file = \
Alex Savatieiev06ab17d2019-02-26 18:40:48 -0600295 mComparer.model_name_1 + "-vs-" + mComparer.model_name_2 + ".html"
Alex Savatieievd48994d2018-12-13 12:13:00 +0100296 report = reporter.ReportToFile(
297 reporter.HTMLModelCompare(),
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100298 report_file
Alex Savatieievd48994d2018-12-13 12:13:00 +0100299 )
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100300 logger_cli.info("...generating report to {}".format(report_file))
301 report({
302 "nodes": {},
303 "diffs": diffs
304 })
Alex Savatieievd48994d2018-12-13 12:13:00 +0100305 # with open("./gen_tree.json", "w+") as _out:
306 # _out.write(json.dumps(mComparer.generate_model_report_tree))
Alex Savatieiev06ab17d2019-02-26 18:40:48 -0600307
308 return
309
310
311# temporary executing the parser as a main prog
312if __name__ == '__main__':
313 compare_models()