blob: 8f30f3c644842b548810ef4ffb7db88eacbf57ce [file] [log] [blame]
savex4448e132018-04-25 15:51:14 +02001import json
savex4448e132018-04-25 15:51:14 +02002
Alex3ebc5632019-04-18 16:47:18 -05003from cfg_checker.common import const, logger_cli
Alex41485522019-04-12 17:26:18 -05004from cfg_checker.common.exception import ConfigException
Alex26b8a8c2019-10-09 17:09:07 -05005from cfg_checker.common.other import merge_dict
Alex41485522019-04-12 17:26:18 -05006from cfg_checker.helpers.console_utils import Progress
Alexd0391d42019-05-21 18:48:55 -05007from cfg_checker.modules.packages.repos import RepoManager
Alexe0c5b9e2019-04-23 18:51:23 -05008from cfg_checker.nodes import salt_master
Alex41485522019-04-12 17:26:18 -05009from cfg_checker.reports import reporter
10
Alex3bc95f62020-03-05 17:00:04 -060011from .versions import DebianVersion, PkgVersions, VersionCmpResult
savex4448e132018-04-25 15:51:14 +020012
13
Alexe0c5b9e2019-04-23 18:51:23 -050014class CloudPackageChecker(object):
Alexe9547d82019-06-03 15:22:50 -050015 def __init__(self, force_tag=None, exclude_keywords=[]):
Alexd0391d42019-05-21 18:48:55 -050016 # Init salt master info
17 if not salt_master.nodes:
18 salt_master.nodes = salt_master.get_nodes()
19
20 # check that this env tag is present in Manager
Alex571bf152019-05-31 16:28:30 -050021 self.rm = RepoManager()
Alexd0391d42019-05-21 18:48:55 -050022 _tags = self.rm.get_available_tags(tag=salt_master.mcp_release)
23 if not _tags:
24 logger_cli.warning(
25 "\n# hWARNING: '{0}' is not listed in repo index. "
26 "Consider running:\n\t{1}\nto add info on this tag's "
27 "release package versions".format(
28 salt_master.mcp_release,
29 "mcp-checker packages versions --tag {0}"
30 )
31 )
32
Alexe9547d82019-06-03 15:22:50 -050033 self.force_tag = force_tag
Alex9e4bfaf2019-06-11 15:21:59 -050034 self.exclude_keywords = exclude_keywords
Alexe9547d82019-06-03 15:22:50 -050035
Alex41485522019-04-12 17:26:18 -050036 @staticmethod
37 def presort_packages(all_packages, full=None):
38 logger_cli.info("-> Presorting packages")
39 # labels
40 _data = {}
41 _data = {
42 "cs": {
43 "ok": const.VERSION_OK,
44 "up": const.VERSION_UP,
45 "down": const.VERSION_DOWN,
Alex26b8a8c2019-10-09 17:09:07 -050046 "warn": const.VERSION_WARN,
Alex41485522019-04-12 17:26:18 -050047 "err": const.VERSION_ERR
48 },
49 "ca": {
50 "na": const.ACT_NA,
51 "up": const.ACT_UPGRADE,
52 "need_up": const.ACT_NEED_UP,
53 "need_down": const.ACT_NEED_DOWN,
54 "repo": const.ACT_REPO
55 }
56 }
57 _data['status_err'] = const.VERSION_ERR
Alex26b8a8c2019-10-09 17:09:07 -050058 _data['status_warn'] = const.VERSION_WARN
Alex41485522019-04-12 17:26:18 -050059 _data['status_down'] = const.VERSION_DOWN
60
61 # Presort packages
62 _data['critical'] = {}
63 _data['system'] = {}
64 _data['other'] = {}
65 _data['unlisted'] = {}
66
67 _l = len(all_packages)
68 _progress = Progress(_l)
69 _progress_index = 0
70 # counters
71 _ec = _es = _eo = _eu = 0
Alex26b8a8c2019-10-09 17:09:07 -050072 _wc = _ws = _wo = _wu = 0
Alex41485522019-04-12 17:26:18 -050073 _dc = _ds = _do = _du = 0
74 while _progress_index < _l:
75 # progress bar
76 _progress_index += 1
77 _progress.write_progress(_progress_index)
78 # sort packages
79 _pn, _val = all_packages.popitem()
Alexd0391d42019-05-21 18:48:55 -050080 _c = _val['desc']['section']
Alex3bc95f62020-03-05 17:00:04 -060081 _rkeys = _val['results'].keys()
Alexd0391d42019-05-21 18:48:55 -050082
Alexe0c5b9e2019-04-23 18:51:23 -050083 if not full:
Alex41485522019-04-12 17:26:18 -050084 # Check if this packet has errors
85 # if all is ok -> just skip it
86 _max_status = max(_val['results'].keys())
87 if _max_status <= const.VERSION_OK:
88 _max_action = max(_val['results'][_max_status].keys())
89 if _max_action == const.ACT_NA:
Alexd0391d42019-05-21 18:48:55 -050090 # this package does not have any comments
Alex41485522019-04-12 17:26:18 -050091 # ...just skip it from report
92 continue
93
Alex26b8a8c2019-10-09 17:09:07 -050094 _differ = len(set(_val['results'].keys())) > 1
95 if _differ:
96 # in case package has different status across nodes
97 # Warning becomes Error.
98 if const.VERSION_WARN in _val['results']:
99 if const.VERSION_ERR in _val['results']:
100 # add warns to err
101 # should never happen, though
102 merge_dict(
103 _val['results'].pop(const.VERSION_WARN),
104 _val['results'][const.VERSION_ERR]
105 )
106 else:
107 _val['results'][const.VERSION_ERR] = \
108 _val['results'].pop(const.VERSION_WARN)
109 else:
110 # in case package has same status on all nodes
111 # Error becomes Warning
112 if const.VERSION_ERR in _val['results']:
113 if const.VERSION_WARN in _val['results']:
114 # add warns to err
115 # should never happen, though
116 merge_dict(
117 _val['results'].pop(const.VERSION_ERR),
118 _val['results'][const.VERSION_WARN]
119 )
120 else:
121 _val['results'][const.VERSION_WARN] = \
122 _val['results'].pop(const.VERSION_ERR)
123
Alexd0391d42019-05-21 18:48:55 -0500124 if len(_c) > 0 and _val['is_mirantis'] is None:
Alex41485522019-04-12 17:26:18 -0500125 # not listed package in version lib
126 _data['unlisted'].update({
127 _pn: _val
128 })
Alex3bc95f62020-03-05 17:00:04 -0600129 _eu += sum(x == const.VERSION_ERR for x in _rkeys)
130 _wu += sum(x == const.VERSION_WARN for x in _rkeys)
131 _du += sum(x == const.VERSION_DOWN for x in _rkeys)
Alex41485522019-04-12 17:26:18 -0500132 # mirantis/critical
Alexd0391d42019-05-21 18:48:55 -0500133 # elif len(_c) > 0 and _c != 'System':
134 elif _val['is_mirantis']:
Alex41485522019-04-12 17:26:18 -0500135 # not blank and not system
136 _data['critical'].update({
137 _pn: _val
138 })
Alex3bc95f62020-03-05 17:00:04 -0600139 _ec += sum(x == const.VERSION_ERR for x in _rkeys)
140 _wc += sum(x == const.VERSION_WARN for x in _rkeys)
141 _dc += sum(x == const.VERSION_DOWN for x in _rkeys)
Alex41485522019-04-12 17:26:18 -0500142 # system
143 elif _c == 'System':
144 _data['system'].update({
145 _pn: _val
146 })
Alex3bc95f62020-03-05 17:00:04 -0600147 _es += sum(x == const.VERSION_ERR for x in _rkeys)
148 _ws += sum(x == const.VERSION_WARN for x in _rkeys)
149 _ds += sum(x == const.VERSION_DOWN for x in _rkeys)
Alex41485522019-04-12 17:26:18 -0500150 # rest
151 else:
152 _data['other'].update({
153 _pn: _val
154 })
Alex3bc95f62020-03-05 17:00:04 -0600155 _eo += sum(x == const.VERSION_ERR for x in _rkeys)
156 _wo += sum(x == const.VERSION_WARN for x in _rkeys)
157 _do += sum(x == const.VERSION_DOWN for x in _rkeys)
Alex41485522019-04-12 17:26:18 -0500158
Alexd9fd85e2019-05-16 16:58:24 -0500159 _progress.end()
Alex41485522019-04-12 17:26:18 -0500160
161 _data['errors'] = {
162 'mirantis': _ec,
163 'system': _es,
164 'other': _eo,
165 'unlisted': _eu
166 }
Alex26b8a8c2019-10-09 17:09:07 -0500167 _data['warnings'] = {
168 'mirantis': _wc,
169 'system': _ws,
170 'other': _wo,
171 'unlisted': _wu
172 }
Alex41485522019-04-12 17:26:18 -0500173 _data['downgrades'] = {
174 'mirantis': _dc,
175 'system': _ds,
176 'other': _do,
177 'unlisted': _du
178 }
179
180 return _data
181
savex4448e132018-04-25 15:51:14 +0200182 def collect_installed_packages(self):
183 """
184 Collect installed packages on each node
185 sets 'installed' dict property in the class
186
187 :return: none
188 """
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600189 logger_cli.info("# Collecting installed packages")
Alexe0c5b9e2019-04-23 18:51:23 -0500190 salt_master.prepare_script_on_active_nodes("pkg_versions.py")
191 _result = salt_master.execute_script_on_active_nodes("pkg_versions.py")
savex4448e132018-04-25 15:51:14 +0200192
Alexe0c5b9e2019-04-23 18:51:23 -0500193 for key in salt_master.nodes.keys():
savex4448e132018-04-25 15:51:14 +0200194 # due to much data to be passed from salt, it is happening in order
Alexe9547d82019-06-03 15:22:50 -0500195 if key in _result and _result[key]:
savex4448e132018-04-25 15:51:14 +0200196 _text = _result[key]
Alex Savatieievfa5910a2019-03-18 18:12:24 -0500197 try:
198 _dict = json.loads(_text[_text.find('{'):])
Alex3ebc5632019-04-18 16:47:18 -0500199 except ValueError:
Alex Savatieievfa5910a2019-03-18 18:12:24 -0500200 logger_cli.info("... no JSON for '{}'".format(
201 key
202 ))
Alex3ebc5632019-04-18 16:47:18 -0500203 logger_cli.debug(
204 "ERROR:\n{}\n".format(_text[:_text.find('{')])
205 )
Alex Savatieievfa5910a2019-03-18 18:12:24 -0500206 _dict = {}
Alex3ebc5632019-04-18 16:47:18 -0500207
Alexe0c5b9e2019-04-23 18:51:23 -0500208 salt_master.nodes[key]['packages'] = _dict
savex4448e132018-04-25 15:51:14 +0200209 else:
Alexe0c5b9e2019-04-23 18:51:23 -0500210 salt_master.nodes[key]['packages'] = {}
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600211 logger_cli.debug("... {} has {} packages installed".format(
savex4448e132018-04-25 15:51:14 +0200212 key,
Alexe0c5b9e2019-04-23 18:51:23 -0500213 len(salt_master.nodes[key]['packages'].keys())
savex4448e132018-04-25 15:51:14 +0200214 ))
Alex Savatieiev799bee32019-02-20 17:19:26 -0600215 logger_cli.info("-> Done")
savex4448e132018-04-25 15:51:14 +0200216
217 def collect_packages(self):
218 """
219 Check package versions in repos vs installed
220
221 :return: no return values, all date put to dict in place
222 """
Alex41485522019-04-12 17:26:18 -0500223 # Preload OpenStack release versions
224 _desc = PkgVersions()
Alex3ebc5632019-04-18 16:47:18 -0500225 logger_cli.info(
226 "# Cross-comparing: Installed vs Candidates vs Release"
227 )
Alexd0391d42019-05-21 18:48:55 -0500228 # shortcuts for this cloud values
229 _os = salt_master.openstack_release
230 _mcp = salt_master.mcp_release
Alexe9547d82019-06-03 15:22:50 -0500231 _t = [self.force_tag] if self.force_tag else []
232 _t.append(_mcp)
233
234 logger_cli.info("# Tag search list: {}".format(", ".join(_t)))
235 logger_cli.info("# Openstack version: {}".format(_os))
236 logger_cli.info(
237 "# Release versions repos keyword exclude list: {}".format(
Alex9e4bfaf2019-06-11 15:21:59 -0500238 ", ".join(self.exclude_keywords)
Alexe9547d82019-06-03 15:22:50 -0500239 )
240 )
241
Alexd0391d42019-05-21 18:48:55 -0500242 # Progress class
Alexe0c5b9e2019-04-23 18:51:23 -0500243 _progress = Progress(len(salt_master.nodes.keys()))
Alex41485522019-04-12 17:26:18 -0500244 _progress_index = 0
245 _total_processed = 0
Alex Savatieiev3db12a72019-03-22 16:32:31 -0500246 # Collect packages from all of the nodes in flat dict
Alex41485522019-04-12 17:26:18 -0500247 _all_packages = {}
Alex3bc95f62020-03-05 17:00:04 -0600248 for node_name, node_value in salt_master.nodes.items():
Alex41485522019-04-12 17:26:18 -0500249 _uniq_len = len(_all_packages.keys())
250 _progress_index += 1
Alexd0391d42019-05-21 18:48:55 -0500251 # progress updates shown before next node only
252 # it is costly operation to do it for each of the 150k packages
Alex41485522019-04-12 17:26:18 -0500253 _progress.write_progress(
254 _progress_index,
255 note="/ {} uniq out of {} packages found".format(
256 _uniq_len,
257 _total_processed
258 )
259 )
Alex3bc95f62020-03-05 17:00:04 -0600260 for _name, _value in node_value['packages'].items():
Alex41485522019-04-12 17:26:18 -0500261 _total_processed += 1
Alexcf91b182019-05-31 11:57:07 -0500262 # Parse versions from nodes
Alex41485522019-04-12 17:26:18 -0500263 _ver_ins = DebianVersion(_value['installed'])
264 _ver_can = DebianVersion(_value['candidate'])
265
Alexcf91b182019-05-31 11:57:07 -0500266 # Process package description and release version
267 # at a first sight
Alex41485522019-04-12 17:26:18 -0500268 if _name not in _all_packages:
Alexcf91b182019-05-31 11:57:07 -0500269 # get node attributes
Alexd0391d42019-05-21 18:48:55 -0500270 _linux = salt_master.nodes[node_name]['linux_codename']
271 _arch = salt_master.nodes[node_name]['linux_arch']
Alexe9547d82019-06-03 15:22:50 -0500272 # get versions for tag, Openstack release and repo headers
273 # excluding 'nightly' repos by default
274 _r = {}
275 # if there is a forced tag = use it
276 if self.force_tag:
277 _r = self.rm.get_filtered_versions(
278 _name,
279 tag=self.force_tag,
280 include=[_os, _linux, _arch],
Alex9e4bfaf2019-06-11 15:21:59 -0500281 exclude=self.exclude_keywords
Alexe9547d82019-06-03 15:22:50 -0500282 )
283 # if nothing found, look everywhere
Alex9e4bfaf2019-06-11 15:21:59 -0500284 # but with no word 'openstack'
Alexe9547d82019-06-03 15:22:50 -0500285 if not _r:
286 _r = self.rm.get_filtered_versions(
287 _name,
288 tag=self.force_tag,
289 include=[_linux, _arch],
Alex9e4bfaf2019-06-11 15:21:59 -0500290 exclude=self.exclude_keywords + ['openstack']
Alexe9547d82019-06-03 15:22:50 -0500291 )
292 # if nothing is found at this point,
293 # repeat search using normal tags
294 if not _r:
295 _r = self.rm.get_filtered_versions(
296 _name,
297 tag=_mcp,
298 include=[_os, _linux, _arch],
Alex9e4bfaf2019-06-11 15:21:59 -0500299 exclude=self.exclude_keywords
Alexe9547d82019-06-03 15:22:50 -0500300 )
301 # Once again, if nothing found, look everywhere
302 if not _r:
303 _r = self.rm.get_filtered_versions(
304 _name,
305 tag=_mcp,
306 include=[_linux, _arch],
Alex9e4bfaf2019-06-11 15:21:59 -0500307 exclude=self.exclude_keywords + ['openstack']
Alexe9547d82019-06-03 15:22:50 -0500308 )
Alexcf91b182019-05-31 11:57:07 -0500309 # repack versions in flat format
Alexd0391d42019-05-21 18:48:55 -0500310 _vs = {}
311 _sections = {}
312 _apps = {}
Alex3bc95f62020-03-05 17:00:04 -0600313 for s, apps in _r.items():
314 for a, versions in apps.items():
315 for v, repos in versions.items():
Alexd0391d42019-05-21 18:48:55 -0500316 for repo in repos:
Alexcf91b182019-05-31 11:57:07 -0500317 if v not in _vs:
318 _vs[v] = []
319 _vs[v].append(repo)
320 if v not in _sections:
321 _sections[v] = []
322 _sections[v].append(s)
323 if v not in _apps:
324 _apps[v] = []
325 _apps[v].append(a)
326 # search for the newest version among filtered
Alexd0391d42019-05-21 18:48:55 -0500327 _r_desc = []
Alex3bc95f62020-03-05 17:00:04 -0600328 _vs_keys = iter(_vs.keys())
329 # get next version, if any
330 try:
331 _newest = DebianVersion(next(_vs_keys))
332 except StopIteration:
Alexd0391d42019-05-21 18:48:55 -0500333 _newest = DebianVersion('')
Alex3bc95f62020-03-05 17:00:04 -0600334 # iterate others, if any
Alexd0391d42019-05-21 18:48:55 -0500335 for v in _vs_keys:
336 _this = DebianVersion(v)
337 if _this > _newest:
338 _newest = _this
Alexd0391d42019-05-21 18:48:55 -0500339 _release = _newest
Alexcf91b182019-05-31 11:57:07 -0500340 # Get best description for the package
Alexd0391d42019-05-21 18:48:55 -0500341 if _release.version != 'n/a':
342 _r_desc = _vs[_release.version]
343 # preload special description
Alex41485522019-04-12 17:26:18 -0500344 if _desc[_name]:
Alex41485522019-04-12 17:26:18 -0500345 _pkg_desc = _desc[_name]
346 else:
Alex41485522019-04-12 17:26:18 -0500347 _pkg_desc = _desc.dummy_desc
Alexcf91b182019-05-31 11:57:07 -0500348 # Save repos list and desc for this version
Alexd0391d42019-05-21 18:48:55 -0500349 # Check if we can provide better from the package
350 if _release.version != 'n/a':
351 if not _pkg_desc['section']:
352 _pkg_desc['section'] = \
353 "/".join(_sections[_release.version])
354 if not _pkg_desc['app']:
355 _pkg_desc['app'] = \
356 "/".join(_apps[_release.version])
Alex3ebc5632019-04-18 16:47:18 -0500357
Alexcf91b182019-05-31 11:57:07 -0500358 # Populate package info, once for package
Alexd0391d42019-05-21 18:48:55 -0500359 _m = _r_desc[0]["maintainer"] if _r_desc else 'n/a'
Alex41485522019-04-12 17:26:18 -0500360 _all_packages[_name] = {
361 "desc": _pkg_desc,
Alexd0391d42019-05-21 18:48:55 -0500362 "repos": _r_desc,
363 "maintainer": _m,
364 "is_mirantis": self.rm.is_mirantis(
365 _name,
Alexcf91b182019-05-31 11:57:07 -0500366 tag=_mcp
Alexd0391d42019-05-21 18:48:55 -0500367 ),
Alex41485522019-04-12 17:26:18 -0500368 "results": {},
369 "r": _release,
Alex Savatieiev3db12a72019-03-22 16:32:31 -0500370 }
Alexcf91b182019-05-31 11:57:07 -0500371 # Cross-compare versions
Alex41485522019-04-12 17:26:18 -0500372 _cmp = VersionCmpResult(
373 _ver_ins,
374 _ver_can,
375 _all_packages[_name]['r']
376 )
Alexcf91b182019-05-31 11:57:07 -0500377 # Update results structure
Alex41485522019-04-12 17:26:18 -0500378 # shortcut to results
379 _res = _all_packages[_name]['results']
380 # update status
381 if _cmp.status not in _res:
382 _res[_cmp.status] = {}
383 # update action
384 if _cmp.action not in _res[_cmp.status]:
385 _res[_cmp.status][_cmp.action] = {}
386 # update node
387 if node_name not in _res[_cmp.status][_cmp.action]:
388 _res[_cmp.status][_cmp.action][node_name] = {}
389 # put result
390 _res[_cmp.status][_cmp.action][node_name] = {
391 'i': _ver_ins,
392 'c': _ver_can,
393 'res': _cmp,
394 'raw': _value['raw']
395 }
savex4448e132018-04-25 15:51:14 +0200396
Alex41485522019-04-12 17:26:18 -0500397 self._packages = _all_packages
Alexd9fd85e2019-05-16 16:58:24 -0500398 _progress.end()
savex4448e132018-04-25 15:51:14 +0200399
Alex41485522019-04-12 17:26:18 -0500400 def create_report(self, filename, rtype, full=None):
savex4448e132018-04-25 15:51:14 +0200401 """
402 Create static html showing packages diff per node
403
404 :return: buff with html
405 """
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600406 logger_cli.info("# Generating report to '{}'".format(filename))
Alex41485522019-04-12 17:26:18 -0500407 if rtype == 'html':
408 _type = reporter.HTMLPackageCandidates()
409 elif rtype == 'csv':
410 _type = reporter.CSVAllPackages()
411 else:
412 raise ConfigException("Report type not set")
Alex Savatieievd48994d2018-12-13 12:13:00 +0100413 _report = reporter.ReportToFile(
Alex41485522019-04-12 17:26:18 -0500414 _type,
savex4448e132018-04-25 15:51:14 +0200415 filename
416 )
Alex41485522019-04-12 17:26:18 -0500417 payload = {
Alexe0c5b9e2019-04-23 18:51:23 -0500418 "nodes": salt_master.nodes,
419 "mcp_release": salt_master.mcp_release,
420 "openstack_release": salt_master.openstack_release
Alex41485522019-04-12 17:26:18 -0500421 }
422 payload.update(self.presort_packages(self._packages, full))
423 _report(payload)
Alex Savatieiev799bee32019-02-20 17:19:26 -0600424 logger_cli.info("-> Done")