blob: 8059fab232abd669fc861e4831c784d768c0cc52 [file] [log] [blame]
savex4448e132018-04-25 15:51:14 +02001import abc
2import os
Alex41485522019-04-12 17:26:18 -05003import time
savex4448e132018-04-25 15:51:14 +02004
Alex Savatieiev5118de02019-02-20 15:50:42 -06005from cfg_checker.common import const
Alex3ebc5632019-04-18 16:47:18 -05006from cfg_checker.common import logger_cli
Alex836fac82019-08-22 13:36:16 -05007from cfg_checker.nodes import salt_master
Alex3ebc5632019-04-18 16:47:18 -05008
9import jinja2
10
11import six
savex4448e132018-04-25 15:51:14 +020012
13pkg_dir = os.path.dirname(__file__)
Alex Savatieiev6d010be2019-03-11 10:36:59 -050014pkg_dir = os.path.join(pkg_dir, os.pardir, os.pardir)
savex4448e132018-04-25 15:51:14 +020015pkg_dir = os.path.normpath(pkg_dir)
16
Alex836fac82019-08-22 13:36:16 -050017# % threshhold values
18_disk_warn = 80
19_disk_critical = 90
20_ram_warn = 5
21_ram_critical = 3
22
savex4448e132018-04-25 15:51:14 +020023
savex4448e132018-04-25 15:51:14 +020024def line_breaks(text):
25 # replace python linebreaks with html breaks
26 return text.replace("\n", "<br />")
27
28
Alex41485522019-04-12 17:26:18 -050029def get_sorted_keys(td):
30 # detect if we can sort by desc
31 # Yes, this is slow, but bullet-proof from empty desc
32 _desc = all([bool(td[k]['desc']) for k in td.keys()])
33 # Get sorted list
34 if not _desc:
35 return sorted(td.keys())
36 else:
37 return sorted(
38 td.keys(),
39 key=lambda k: (
Alexd0391d42019-05-21 18:48:55 -050040 td[k]['desc']['section'],
Alex41485522019-04-12 17:26:18 -050041 td[k]['desc']['app'],
42 k
43 )
44 )
45
46
47def get_max(_list):
48 return sorted(_list)[-1]
49
50
Alex836fac82019-08-22 13:36:16 -050051def make_pkg_action_label(act):
Alex41485522019-04-12 17:26:18 -050052 _act_labels = {
53 const.ACT_UPGRADE: "Upgrade possible",
54 const.ACT_NEED_UP: "Needs upgrade",
55 const.ACT_NEED_DOWN: "Needs downgrade",
Alex9e4bfaf2019-06-11 15:21:59 -050056 const.ACT_REPO: "Repo update",
Alex41485522019-04-12 17:26:18 -050057 const.ACT_NA: ""
58 }
59 return _act_labels[act]
60
61
Alex836fac82019-08-22 13:36:16 -050062def make_pkg_action_class(act):
Alex41485522019-04-12 17:26:18 -050063 _act_classes = {
64 const.ACT_UPGRADE: "possible",
65 const.ACT_NEED_UP: "needs_up",
66 const.ACT_NEED_DOWN: "needs_down",
67 const.ACT_REPO: "needs_repo",
68 const.ACT_NA: ""
69 }
70 return _act_classes[act]
71
72
Alex836fac82019-08-22 13:36:16 -050073def make_pkg_status_label(sts):
Alex41485522019-04-12 17:26:18 -050074 _status_labels = {
75 const.VERSION_OK: "OK",
76 const.VERSION_UP: "Upgraded",
77 const.VERSION_DOWN: "Downgraded",
78 const.VERSION_ERR: "ERROR",
79 const.VERSION_NA: "N/A"
80 }
81 return _status_labels[sts]
82
83
Alex836fac82019-08-22 13:36:16 -050084def make_pkg_status_class(sts):
85 return const.all_pkg_statuses[sts]
86
87
88def make_node_status(sts):
89 return const.node_status[sts]
Alex41485522019-04-12 17:26:18 -050090
91
Alexd0391d42019-05-21 18:48:55 -050092def make_repo_info(repos):
93 _text = ""
94 for r in repos:
95 # tag
96 _text += r['tag'] + ": "
97 # repo header
98 _text += " ".join([
99 r['subset'],
100 r['release'],
101 r['ubuntu-release'],
102 r['type'],
103 r['arch']
104 ]) + ", "
105 # maintainer w/o email
106 _m = r['maintainer'][:r['maintainer'].find('<')-1]
107 _m_ascii = _m.encode('ascii', errors="xmlcharrefreplace")
108 _text += _m_ascii
109 # newline
110 _text += "<br />"
111 return _text
112
113
savex4448e132018-04-25 15:51:14 +0200114@six.add_metaclass(abc.ABCMeta)
115class _Base(object):
116 def __init__(self):
117 self.jinja2_env = self.init_jinja2_env()
118
119 @abc.abstractmethod
120 def __call__(self, payload):
121 pass
122
123 @staticmethod
124 def init_jinja2_env():
125 return jinja2.Environment(
126 loader=jinja2.FileSystemLoader(os.path.join(pkg_dir, 'templates')),
127 trim_blocks=True,
128 lstrip_blocks=True)
129
130
131class _TMPLBase(_Base):
132 @abc.abstractproperty
133 def tmpl(self):
134 pass
135
136 @staticmethod
137 def _count_totals(data):
138 data['counters']['total_nodes'] = len(data['nodes'])
139
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100140 def __call__(self, payload):
savex4448e132018-04-25 15:51:14 +0200141 # init data structures
142 data = self.common_data()
Alex41485522019-04-12 17:26:18 -0500143 # payload should have pre-sorted structure according to report called
144 # nodes, openstack_release, mcp_release, etc...
145 data.update(payload)
savex4448e132018-04-25 15:51:14 +0200146
147 # add template specific data
148 self._extend_data(data)
149
150 # do counts global
151 self._count_totals(data)
152
153 # specific filters
savex4448e132018-04-25 15:51:14 +0200154 self.jinja2_env.filters['linebreaks'] = line_breaks
Alex41485522019-04-12 17:26:18 -0500155 self.jinja2_env.filters['get_max'] = get_max
156
157 self.jinja2_env.filters['get_sorted_keys'] = get_sorted_keys
Alex836fac82019-08-22 13:36:16 -0500158 self.jinja2_env.filters['pkg_status_label'] = make_pkg_status_label
159 self.jinja2_env.filters['pkg_status_class'] = make_pkg_status_class
160 self.jinja2_env.filters['pkg_action_label'] = make_pkg_action_label
161 self.jinja2_env.filters['pkg_action_class'] = make_pkg_action_class
162 self.jinja2_env.filters['node_status_class'] = make_node_status
Alexd0391d42019-05-21 18:48:55 -0500163 self.jinja2_env.filters['make_repo_info'] = make_repo_info
savex4448e132018-04-25 15:51:14 +0200164
165 # render!
Alex41485522019-04-12 17:26:18 -0500166 logger_cli.info("-> Using template: {}".format(self.tmpl))
savex4448e132018-04-25 15:51:14 +0200167 tmpl = self.jinja2_env.get_template(self.tmpl)
Alex41485522019-04-12 17:26:18 -0500168 logger_cli.info("-> Rendering")
savex4448e132018-04-25 15:51:14 +0200169 return tmpl.render(data)
170
171 def common_data(self):
172 return {
173 'counters': {},
Alex41485522019-04-12 17:26:18 -0500174 'salt_info': {},
175 'gen_date': time.strftime("%m/%d/%Y %H:%M:%S")
savex4448e132018-04-25 15:51:14 +0200176 }
177
178 def _extend_data(self, data):
179 pass
180
181
Alex41485522019-04-12 17:26:18 -0500182# HTML Package versions report
183class CSVAllPackages(_TMPLBase):
184 tmpl = "pkg_versions_csv.j2"
185
186
187# HTML Package versions report
savexce010ba2018-04-27 09:49:23 +0200188class HTMLPackageCandidates(_TMPLBase):
Alex41485522019-04-12 17:26:18 -0500189 tmpl = "pkg_versions_html.j2"
savex4448e132018-04-25 15:51:14 +0200190
191
Alex Savatieievd48994d2018-12-13 12:13:00 +0100192# Package versions report
193class HTMLModelCompare(_TMPLBase):
194 tmpl = "model_tree_cmp_tmpl.j2"
195
196 def _extend_data(self, data):
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100197 # move names into separate place
Alexb8af13a2019-04-16 18:38:12 -0500198 data["names"] = data["diffs"].pop("diff_names")
199 data["tabs"] = data.pop("diffs")
Alex3ebc5632019-04-18 16:47:18 -0500200
Alex Savatieievd48994d2018-12-13 12:13:00 +0100201 # counters - mdl_diff
Alex Savatieiev4f149d02019-02-28 17:15:29 -0600202 for _tab in data["tabs"].keys():
203 data['counters'][_tab] = len(data["tabs"][_tab]["diffs"].keys())
Alex Savatieievd48994d2018-12-13 12:13:00 +0100204
205
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600206class HTMLNetworkReport(_TMPLBase):
207 tmpl = "network_check_tmpl.j2"
208
Alex836fac82019-08-22 13:36:16 -0500209 def _extend_data(self, data):
210 def get_bytes(value):
211 if value[-1] == 'G':
212 return int(float(value[:-1]) * 1024 * 1024 * 1024)
213 elif value[-1] == 'M':
214 return int(float(value[:-1]) * 1024 * 1024)
215 elif value[-1] == 'K':
216 return int(float(value[:-1]) * 1024)
217 else:
218 return int(value)
219
220 def _lscpu(field, key, _dict):
221 _f_cmd = salt_master.get_cmd_for_nodes
222 _cmd = "lscpu | grep -e \"^{}:\" | cut -d\":\" -f2 " \
223 "| sed -e 's/^[[:space:]]*//'"
224 _f_cmd(_cmd.format(field), key, target_dict=_dict)
225
226 def _free(field, key, _dict):
227 _f_cmd = salt_master.get_cmd_for_nodes
228 _cmd = "free -h | sed -n '/Mem/s/ \\+/ /gp' | cut -d\" \" -f {}"
229 _f_cmd(_cmd.format(field), key, target_dict=_dict)
230
231 def _services(_dict):
232 _key = "services"
233 _key_r = "services_raw"
234 _f_cmd = salt_master.get_cmd_for_nodes
235 _cmd = "service --status-all"
236 _f_cmd(_cmd, _key_r, target_dict=_dict)
237 for node, dt in _dict.iteritems():
238 dt[_key] = {}
239 lines = dt[_key_r].splitlines()
240 for line in lines:
241 li = line.split()
242 _status = li[1]
243 _name = li[3]
244 if _status == '-':
245 dt[_key][_name] = False
246 elif _status == '+':
247 dt[_key][_name] = True
248 else:
249 dt[_key][_name] = None
250 dt.pop(_key_r)
251
252 data["const"] = {
253 "ram_warn": _ram_warn,
254 "ram_critical": _ram_critical,
255 "disk_warn": _disk_warn,
256 "disk_critical": _disk_critical
257 }
258
259 # get kernel version
260 salt_master.get_cmd_for_nodes(
261 "uname -r",
262 "kernel",
263 target_dict=data["nodes"]
264 )
265 # cpu info
266 # Sample: VT-x, KVM, full
267 _lscpu("Virtualization", "virt_mode", data["nodes"])
268 _lscpu("Hypervisor vendor", "virt_vendor", data["nodes"])
269 _lscpu("Virtualization type", "virt_type", data["nodes"])
270 # sample: 4
271 _lscpu("CPU(s)", "cpus", data["nodes"])
272
273 # free ram
274 # sample: 16425392 14883144 220196
275 _free("2", "ram_total", data["nodes"])
276 _free("3", "ram_used", data["nodes"])
277 _free("4", "ram_free", data["nodes"])
278 _free("7", "ram_available", data["nodes"])
279 for _data in data["nodes"].itervalues():
280 _total = get_bytes(_data["ram_total"])
281 _avail = get_bytes(_data["ram_available"])
282 _m = _avail * 100.0 / _total
283 if _m < _ram_critical:
284 _data["ram_status"] = "fail"
285 elif _m < _ram_warn:
286 _data["ram_status"] = "warn"
287 else:
288 _data["ram_status"] = ""
289
290 # disk space
291 # sample: /dev/vda1 78G 33G 45G 43%
292 salt_master.get_cmd_for_nodes(
293 "df -h | sed -n '/^\\/dev/s/ \\+/ /gp' | cut -d\" \" -f 1-5",
294 "disk_raw",
295 target_dict=data["nodes"]
296 )
297 for _data in data["nodes"].itervalues():
298 _data["disk"] = {}
299 # show first device row by default
300 _data["disk_max_dev"] = None
301 _d = _data["disk"]
302 _r = _data["disk_raw"]
303 _r = _r.splitlines()
304 _max = -1
305 for idx in range(0, len(_r)):
306 _t = _r[idx].split()
307 _d[_t[0]] = {}
308 _d[_t[0]]['v'] = _t[1:]
309 _chk = int(_t[-1].split('%')[0])
310 if _chk > _max:
311 _data["disk_max_dev"] = _t[0]
312 _max = _chk
313 if _chk > _disk_critical:
314 _d[_t[0]]['f'] = "fail"
315 elif _chk > _disk_warn:
316 _d[_t[0]]['f'] = "warn"
317 else:
318 _d[_t[0]]['f'] = ""
319
320 # prepare networks data for report
321 for net, net_v in data['map'].iteritems():
322 for node, ifs in net_v.iteritems():
323 for d in ifs:
324 _err = "fail"
325 d['interface_error'] = _err if d['interface_error'] else ""
326 d['mtu_error'] = _err if d['mtu_error'] else ""
327 d['status_error'] = _err if d['status_error'] else ""
328 d['subnet_gateway_error'] = \
329 _err if d['subnet_gateway_error'] else ""
330
331 _services(data["nodes"])
332
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600333
savex4448e132018-04-25 15:51:14 +0200334class ReportToFile(object):
335 def __init__(self, report, target):
336 self.report = report
337 self.target = target
338
339 def __call__(self, payload):
340 payload = self.report(payload)
341
342 if isinstance(self.target, six.string_types):
343 self._wrapped_dump(payload)
344 else:
345 self._dump(payload, self.target)
346
347 def _wrapped_dump(self, payload):
348 with open(self.target, 'wt') as target:
349 self._dump(payload, target)
350
351 @staticmethod
352 def _dump(payload, target):
353 target.write(payload)