blob: b55ab2e83dc5d0efe9d8ad6858e690e03307e2cf [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
15class ModelComparer(object):
16 """Collection of functions to compare model data.
17 """
18 models = {}
19
20 @staticmethod
21 def load_yaml_class(fname):
22 """Loads a yaml from the file and forms a tree item
23
24 Arguments:
25 fname {string} -- full path to the yaml file
26 """
27 _yaml = {}
28 try:
29 _size = 0
30 with open(fname, 'r') as f:
31 _yaml = yaml.load(f)
32 _size = f.tell()
33 # TODO: do smth with the data
34 if not _yaml:
35 logger_cli.warning("WARN: empty file '{}'".format(fname))
36 _yaml = {}
37 else:
38 logger.debug("...loaded YAML '{}' ({}b)".format(fname, _size))
39 return _yaml
40 except yaml.YAMLError as exc:
41 logger_cli.error(exc)
42 except IOError as e:
43 logger_cli.error(
44 "Error loading file '{}': {}".format(fname, e.message)
45 )
46 raise Exception("CRITICAL: Failed to load YAML data: {}".format(
Alex Savatieiev36b938d2019-01-21 11:01:18 +010047 e.message + e.strerror
Alex Savatieievd48994d2018-12-13 12:13:00 +010048 ))
49
50 def load_model_tree(self, name, root_path="/srv/salt/reclass"):
51 """Walks supplied path for the YAML filed and loads the tree
52
53 Arguments:
54 root_folder_path {string} -- Path to Model's root folder. Optional
55 """
56 logger_cli.info("Loading reclass tree from '{}'".format(root_path))
57 # prepare the file tree to walk
58 raw_tree = {}
59 # Credits to Andrew Clark@MIT. Original code is here:
60 # http://code.activestate.com/recipes/577879-create-a-nested-dictionary-from-oswalk/
61 root_path = root_path.rstrip(os.sep)
62 start = root_path.rfind(os.sep) + 1
63 root_key = root_path.rsplit(os.sep, 1)[1]
64 # Look Ma! I am walking the file tree with no recursion!
65 for path, dirs, files in os.walk(root_path):
66 # if this is a hidden folder, ignore it
67 _filders_list = path[start:].split(os.sep)
68 if any(item.startswith(".") for item in _filders_list):
69 continue
70 # cut absolute part of the path and split folder names
71 folders = path[start:].split(os.sep)
72 subdir = {}
73 # create generator of files that are not hidden
Alex Savatieiev36b938d2019-01-21 11:01:18 +010074 _exts = ('.yml', '.yaml')
75 _subfiles = (file for file in files
76 if file.endswith(_exts) and not file.startswith('.'))
Alex Savatieievd48994d2018-12-13 12:13:00 +010077 for _file in _subfiles:
78 # cut file extension. All reclass files are '.yml'
79 _subnode = _file
80 # load all YAML class data into the tree
81 subdir[_subnode] = self.load_yaml_class(
82 os.path.join(path, _file)
83 )
Alex Savatieiev36b938d2019-01-21 11:01:18 +010084 try:
85 # Save original filepath, just in case
86 subdir[_subnode]["_source"] = os.path.join(
87 path[start:],
88 _file
89 )
90 except Exception:
91 logger.warning(
92 "Non-yaml file detected: {}".format(_file)
93 )
Alex Savatieievd48994d2018-12-13 12:13:00 +010094 # creating dict structure out of folder list. Pure python magic
95 parent = reduce(dict.get, folders[:-1], raw_tree)
96 parent[folders[-1]] = subdir
97 # save it as a single data object
98 self.models[name] = raw_tree[root_key]
99 return True
100
101 def generate_model_report_tree(self):
102 """Use all loaded models to generate comparison table with
103 values are groupped by YAML files
104 """
105 def find_changes(dict1, dict2, path=""):
106 _report = {}
107 for k in dict1.keys():
108 _new_path = path + ":" + k
109 if k == "_source":
110 continue
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100111 if dict2 is None or k not in dict2:
Alex Savatieievd48994d2018-12-13 12:13:00 +0100112 # no key in dict2
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100113 _report[_new_path] = {
114 "type": "value",
115 "raw_values": [dict1[k], "N/A"],
116 "str_values": [
117 "{}".format(dict1[k]),
118 "n/a"
119 ],
120 "str_short": [
121 "{:25.25}...".format(str(dict1[k])),
122 "n/a"
123 ]
124 }
125 logger.info(
Alex Savatieievd48994d2018-12-13 12:13:00 +0100126 "{}: {}, {}".format(_new_path, dict1[k], "N/A")
127 )
128 else:
129 if type(dict1[k]) is dict:
130 if path == "":
131 _new_path = k
132 _child_report = find_changes(
133 dict1[k],
134 dict2[k],
135 _new_path
136 )
137 _report.update(_child_report)
138 elif type(dict1[k]) is list:
139 # use ifilterfalse to compare lists of dicts
140 _removed = list(
141 itertools.ifilterfalse(
142 lambda x: x in dict2[k],
143 dict1[k]
144 )
145 )
146 _added = list(
147 itertools.ifilterfalse(
148 lambda x: x in dict1[k],
149 dict2[k]
150 )
151 )
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100152 _original = ["= {}".format(item) for item in dict1[k]]
Alex Savatieievd48994d2018-12-13 12:13:00 +0100153 if _removed or _added:
154 _removed_str_lst = ["- {}".format(item)
155 for item in _removed]
156 _added_str_lst = ["+ {}".format(item)
157 for item in _added]
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100158 _report[_new_path] = {
159 "type": "list",
160 "raw_values": [
161 dict1[k],
162 _removed_str_lst + _added_str_lst
163 ],
164 "str_values": [
165 "{}".format('\n'.join(_original)),
166 "{}\n{}".format(
167 '\n'.join(_removed_str_lst),
168 '\n'.join(_added_str_lst)
169 )
170 ],
171 "str_short": [
172 "{} items".format(len(dict1[k])),
173 "{}\n{}".format(
174 '\n'.join(_removed_str_lst),
175 '\n'.join(_added_str_lst)
176 )
177 ]
178 }
179 logger.info(
Alex Savatieievd48994d2018-12-13 12:13:00 +0100180 "{}:\n"
181 "{} original items total".format(
182 _new_path,
183 len(dict1[k])
184 )
185 )
186 if _removed:
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100187 logger.info(
Alex Savatieievd48994d2018-12-13 12:13:00 +0100188 "{}".format('\n'.join(_removed_str_lst))
189 )
190 if _added:
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100191 logger.info(
Alex Savatieievd48994d2018-12-13 12:13:00 +0100192 "{}".format('\n'.join(_added_str_lst))
193 )
194 else:
195 if dict1[k] != dict2[k]:
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100196 _str1 = str(dict1[k])
197 _str1_cut = "{:20.20}...".format(str(dict1[k]))
198 _str2 = str(dict2[k])
199 _str2_cut = "{:20.20}...".format(str(dict2[k]))
200 _report[_new_path] = {
201 "type": "value",
202 "raw_values": [dict1[k], dict2[k]],
203 "str_values": [
204 "{}".format(dict1[k]),
205 "{}".format(dict2[k])
206 ],
207 "str_short": [
208 _str1 if len(_str1) < 20 else _str1_cut,
209 _str2 if len(_str1) < 20 else _str2_cut,
210 ]
211 }
212 logger.info("{}: {}, {}".format(
Alex Savatieievd48994d2018-12-13 12:13:00 +0100213 _new_path,
214 dict1[k],
215 dict2[k]
216 ))
217 return _report
218 # tmp report for keys
219 diff_report = find_changes(
220 self.models["inspur_Aug"],
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100221 self.models["inspur_Original"]
Alex Savatieievd48994d2018-12-13 12:13:00 +0100222 )
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100223 # prettify the report
224 for key in diff_report.keys():
225 # break the key in two parts
226 _ext = ".yml"
227 if ".yaml" in key:
228 _ext = ".yaml"
229 _split = key.split(_ext)
230 _file_path = _split[0]
231 _param_path = "none"
232 if len(_split) > 1:
233 _param_path = _split[1]
234 diff_report[key].update({
235 "class_file": _file_path + _ext,
236 "param": _param_path,
237 })
238
239 diff_report["diff_names"] = ["inspur_Aug", "inspur_Original"]
Alex Savatieievd48994d2018-12-13 12:13:00 +0100240 return diff_report
241
242
243# temporary executing the parser as a main prog
244if __name__ == '__main__':
245 mComparer = ModelComparer()
246 mComparer.load_model_tree(
247 'inspur_Aug',
248 '/Users/savex/proj/inspur_hc/reclass_cmp/reclass-20180810'
249 )
250 mComparer.load_model_tree(
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100251 'inspur_Original',
252 '/Users/savex/proj/inspur_hc/reclass_cmp/reclass_original'
Alex Savatieievd48994d2018-12-13 12:13:00 +0100253 )
254 diffs = mComparer.generate_model_report_tree()
255
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100256 report_file = "./mdl_diff_original.html"
Alex Savatieievd48994d2018-12-13 12:13:00 +0100257 report = reporter.ReportToFile(
258 reporter.HTMLModelCompare(),
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100259 report_file
Alex Savatieievd48994d2018-12-13 12:13:00 +0100260 )
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100261 logger_cli.info("...generating report to {}".format(report_file))
262 report({
263 "nodes": {},
264 "diffs": diffs
265 })
Alex Savatieievd48994d2018-12-13 12:13:00 +0100266 # with open("./gen_tree.json", "w+") as _out:
267 # _out.write(json.dumps(mComparer.generate_model_report_tree))