blob: fb02db27208b9cbc7b45e1abe5eea2912f742d98 [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):
Alexe9908f72020-05-19 16:04:53 -050015 def __init__(
16 self,
17 force_tag=None,
18 exclude_keywords=[],
19 skip_list=None,
20 skip_list_file=None
21 ):
Alexd0391d42019-05-21 18:48:55 -050022 # Init salt master info
23 if not salt_master.nodes:
Alexe9908f72020-05-19 16:04:53 -050024 salt_master.nodes = salt_master.get_nodes(
25 skip_list=skip_list,
26 skip_list_file=skip_list_file
27 )
Alexd0391d42019-05-21 18:48:55 -050028
29 # check that this env tag is present in Manager
Alex571bf152019-05-31 16:28:30 -050030 self.rm = RepoManager()
Alexd0391d42019-05-21 18:48:55 -050031 _tags = self.rm.get_available_tags(tag=salt_master.mcp_release)
32 if not _tags:
33 logger_cli.warning(
34 "\n# hWARNING: '{0}' is not listed in repo index. "
35 "Consider running:\n\t{1}\nto add info on this tag's "
36 "release package versions".format(
37 salt_master.mcp_release,
38 "mcp-checker packages versions --tag {0}"
39 )
40 )
41
Alexe9547d82019-06-03 15:22:50 -050042 self.force_tag = force_tag
Alex9e4bfaf2019-06-11 15:21:59 -050043 self.exclude_keywords = exclude_keywords
Alexe9547d82019-06-03 15:22:50 -050044
Alex41485522019-04-12 17:26:18 -050045 @staticmethod
46 def presort_packages(all_packages, full=None):
47 logger_cli.info("-> Presorting packages")
48 # labels
49 _data = {}
50 _data = {
51 "cs": {
52 "ok": const.VERSION_OK,
53 "up": const.VERSION_UP,
54 "down": const.VERSION_DOWN,
Alex26b8a8c2019-10-09 17:09:07 -050055 "warn": const.VERSION_WARN,
Alex41485522019-04-12 17:26:18 -050056 "err": const.VERSION_ERR
57 },
58 "ca": {
59 "na": const.ACT_NA,
60 "up": const.ACT_UPGRADE,
61 "need_up": const.ACT_NEED_UP,
62 "need_down": const.ACT_NEED_DOWN,
63 "repo": const.ACT_REPO
64 }
65 }
66 _data['status_err'] = const.VERSION_ERR
Alex26b8a8c2019-10-09 17:09:07 -050067 _data['status_warn'] = const.VERSION_WARN
Alex41485522019-04-12 17:26:18 -050068 _data['status_down'] = const.VERSION_DOWN
Alexe9908f72020-05-19 16:04:53 -050069 _data['status_skip'] = const.VERSION_NA
Alex41485522019-04-12 17:26:18 -050070
71 # Presort packages
72 _data['critical'] = {}
73 _data['system'] = {}
74 _data['other'] = {}
75 _data['unlisted'] = {}
76
77 _l = len(all_packages)
78 _progress = Progress(_l)
79 _progress_index = 0
80 # counters
81 _ec = _es = _eo = _eu = 0
Alex26b8a8c2019-10-09 17:09:07 -050082 _wc = _ws = _wo = _wu = 0
Alex41485522019-04-12 17:26:18 -050083 _dc = _ds = _do = _du = 0
84 while _progress_index < _l:
85 # progress bar
86 _progress_index += 1
87 _progress.write_progress(_progress_index)
88 # sort packages
89 _pn, _val = all_packages.popitem()
Alexd0391d42019-05-21 18:48:55 -050090 _c = _val['desc']['section']
Alex3bc95f62020-03-05 17:00:04 -060091 _rkeys = _val['results'].keys()
Alexd0391d42019-05-21 18:48:55 -050092
Alexe0c5b9e2019-04-23 18:51:23 -050093 if not full:
Alex41485522019-04-12 17:26:18 -050094 # Check if this packet has errors
95 # if all is ok -> just skip it
96 _max_status = max(_val['results'].keys())
97 if _max_status <= const.VERSION_OK:
98 _max_action = max(_val['results'][_max_status].keys())
99 if _max_action == const.ACT_NA:
Alexd0391d42019-05-21 18:48:55 -0500100 # this package does not have any comments
Alex41485522019-04-12 17:26:18 -0500101 # ...just skip it from report
102 continue
103
Alex26b8a8c2019-10-09 17:09:07 -0500104 _differ = len(set(_val['results'].keys())) > 1
105 if _differ:
106 # in case package has different status across nodes
107 # Warning becomes Error.
108 if const.VERSION_WARN in _val['results']:
109 if const.VERSION_ERR in _val['results']:
110 # add warns to err
111 # should never happen, though
112 merge_dict(
113 _val['results'].pop(const.VERSION_WARN),
114 _val['results'][const.VERSION_ERR]
115 )
116 else:
117 _val['results'][const.VERSION_ERR] = \
118 _val['results'].pop(const.VERSION_WARN)
119 else:
120 # in case package has same status on all nodes
121 # Error becomes Warning
122 if const.VERSION_ERR in _val['results']:
123 if const.VERSION_WARN in _val['results']:
124 # add warns to err
125 # should never happen, though
126 merge_dict(
127 _val['results'].pop(const.VERSION_ERR),
128 _val['results'][const.VERSION_WARN]
129 )
130 else:
131 _val['results'][const.VERSION_WARN] = \
132 _val['results'].pop(const.VERSION_ERR)
133
Alexd0391d42019-05-21 18:48:55 -0500134 if len(_c) > 0 and _val['is_mirantis'] is None:
Alex41485522019-04-12 17:26:18 -0500135 # not listed package in version lib
136 _data['unlisted'].update({
137 _pn: _val
138 })
Alex3bc95f62020-03-05 17:00:04 -0600139 _eu += sum(x == const.VERSION_ERR for x in _rkeys)
140 _wu += sum(x == const.VERSION_WARN for x in _rkeys)
141 _du += sum(x == const.VERSION_DOWN for x in _rkeys)
Alex41485522019-04-12 17:26:18 -0500142 # mirantis/critical
Alexd0391d42019-05-21 18:48:55 -0500143 # elif len(_c) > 0 and _c != 'System':
144 elif _val['is_mirantis']:
Alex41485522019-04-12 17:26:18 -0500145 # not blank and not system
146 _data['critical'].update({
147 _pn: _val
148 })
Alex3bc95f62020-03-05 17:00:04 -0600149 _ec += sum(x == const.VERSION_ERR for x in _rkeys)
150 _wc += sum(x == const.VERSION_WARN for x in _rkeys)
151 _dc += sum(x == const.VERSION_DOWN for x in _rkeys)
Alex41485522019-04-12 17:26:18 -0500152 # system
153 elif _c == 'System':
154 _data['system'].update({
155 _pn: _val
156 })
Alex3bc95f62020-03-05 17:00:04 -0600157 _es += sum(x == const.VERSION_ERR for x in _rkeys)
158 _ws += sum(x == const.VERSION_WARN for x in _rkeys)
159 _ds += sum(x == const.VERSION_DOWN for x in _rkeys)
Alex41485522019-04-12 17:26:18 -0500160 # rest
161 else:
162 _data['other'].update({
163 _pn: _val
164 })
Alex3bc95f62020-03-05 17:00:04 -0600165 _eo += sum(x == const.VERSION_ERR for x in _rkeys)
166 _wo += sum(x == const.VERSION_WARN for x in _rkeys)
167 _do += sum(x == const.VERSION_DOWN for x in _rkeys)
Alex41485522019-04-12 17:26:18 -0500168
Alexd9fd85e2019-05-16 16:58:24 -0500169 _progress.end()
Alex41485522019-04-12 17:26:18 -0500170
171 _data['errors'] = {
172 'mirantis': _ec,
173 'system': _es,
174 'other': _eo,
175 'unlisted': _eu
176 }
Alex26b8a8c2019-10-09 17:09:07 -0500177 _data['warnings'] = {
178 'mirantis': _wc,
179 'system': _ws,
180 'other': _wo,
181 'unlisted': _wu
182 }
Alex41485522019-04-12 17:26:18 -0500183 _data['downgrades'] = {
184 'mirantis': _dc,
185 'system': _ds,
186 'other': _do,
187 'unlisted': _du
188 }
189
190 return _data
191
savex4448e132018-04-25 15:51:14 +0200192 def collect_installed_packages(self):
193 """
194 Collect installed packages on each node
195 sets 'installed' dict property in the class
196
197 :return: none
198 """
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600199 logger_cli.info("# Collecting installed packages")
Alexe0c5b9e2019-04-23 18:51:23 -0500200 salt_master.prepare_script_on_active_nodes("pkg_versions.py")
201 _result = salt_master.execute_script_on_active_nodes("pkg_versions.py")
savex4448e132018-04-25 15:51:14 +0200202
Alexe0c5b9e2019-04-23 18:51:23 -0500203 for key in salt_master.nodes.keys():
savex4448e132018-04-25 15:51:14 +0200204 # due to much data to be passed from salt, it is happening in order
Alexe9547d82019-06-03 15:22:50 -0500205 if key in _result and _result[key]:
savex4448e132018-04-25 15:51:14 +0200206 _text = _result[key]
Alex Savatieievfa5910a2019-03-18 18:12:24 -0500207 try:
208 _dict = json.loads(_text[_text.find('{'):])
Alex3ebc5632019-04-18 16:47:18 -0500209 except ValueError:
Alex Savatieievfa5910a2019-03-18 18:12:24 -0500210 logger_cli.info("... no JSON for '{}'".format(
211 key
212 ))
Alex3ebc5632019-04-18 16:47:18 -0500213 logger_cli.debug(
214 "ERROR:\n{}\n".format(_text[:_text.find('{')])
215 )
Alex Savatieievfa5910a2019-03-18 18:12:24 -0500216 _dict = {}
Alex3ebc5632019-04-18 16:47:18 -0500217
Alexe0c5b9e2019-04-23 18:51:23 -0500218 salt_master.nodes[key]['packages'] = _dict
savex4448e132018-04-25 15:51:14 +0200219 else:
Alexe0c5b9e2019-04-23 18:51:23 -0500220 salt_master.nodes[key]['packages'] = {}
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600221 logger_cli.debug("... {} has {} packages installed".format(
savex4448e132018-04-25 15:51:14 +0200222 key,
Alexe0c5b9e2019-04-23 18:51:23 -0500223 len(salt_master.nodes[key]['packages'].keys())
savex4448e132018-04-25 15:51:14 +0200224 ))
Alex Savatieiev799bee32019-02-20 17:19:26 -0600225 logger_cli.info("-> Done")
savex4448e132018-04-25 15:51:14 +0200226
227 def collect_packages(self):
228 """
229 Check package versions in repos vs installed
230
231 :return: no return values, all date put to dict in place
232 """
Alex41485522019-04-12 17:26:18 -0500233 # Preload OpenStack release versions
234 _desc = PkgVersions()
Alex3ebc5632019-04-18 16:47:18 -0500235 logger_cli.info(
236 "# Cross-comparing: Installed vs Candidates vs Release"
237 )
Alexd0391d42019-05-21 18:48:55 -0500238 # shortcuts for this cloud values
239 _os = salt_master.openstack_release
240 _mcp = salt_master.mcp_release
Alexe9547d82019-06-03 15:22:50 -0500241 _t = [self.force_tag] if self.force_tag else []
242 _t.append(_mcp)
243
244 logger_cli.info("# Tag search list: {}".format(", ".join(_t)))
245 logger_cli.info("# Openstack version: {}".format(_os))
246 logger_cli.info(
247 "# Release versions repos keyword exclude list: {}".format(
Alex9e4bfaf2019-06-11 15:21:59 -0500248 ", ".join(self.exclude_keywords)
Alexe9547d82019-06-03 15:22:50 -0500249 )
250 )
251
Alexd0391d42019-05-21 18:48:55 -0500252 # Progress class
Alexe0c5b9e2019-04-23 18:51:23 -0500253 _progress = Progress(len(salt_master.nodes.keys()))
Alex41485522019-04-12 17:26:18 -0500254 _progress_index = 0
255 _total_processed = 0
Alex Savatieiev3db12a72019-03-22 16:32:31 -0500256 # Collect packages from all of the nodes in flat dict
Alex41485522019-04-12 17:26:18 -0500257 _all_packages = {}
Alex3bc95f62020-03-05 17:00:04 -0600258 for node_name, node_value in salt_master.nodes.items():
Alex41485522019-04-12 17:26:18 -0500259 _uniq_len = len(_all_packages.keys())
260 _progress_index += 1
Alexd0391d42019-05-21 18:48:55 -0500261 # progress updates shown before next node only
262 # it is costly operation to do it for each of the 150k packages
Alex41485522019-04-12 17:26:18 -0500263 _progress.write_progress(
264 _progress_index,
265 note="/ {} uniq out of {} packages found".format(
266 _uniq_len,
267 _total_processed
268 )
269 )
Alex3bc95f62020-03-05 17:00:04 -0600270 for _name, _value in node_value['packages'].items():
Alex41485522019-04-12 17:26:18 -0500271 _total_processed += 1
Alexcf91b182019-05-31 11:57:07 -0500272 # Parse versions from nodes
Alex41485522019-04-12 17:26:18 -0500273 _ver_ins = DebianVersion(_value['installed'])
274 _ver_can = DebianVersion(_value['candidate'])
275
Alexcf91b182019-05-31 11:57:07 -0500276 # Process package description and release version
277 # at a first sight
Alex41485522019-04-12 17:26:18 -0500278 if _name not in _all_packages:
Alexcf91b182019-05-31 11:57:07 -0500279 # get node attributes
Alexd0391d42019-05-21 18:48:55 -0500280 _linux = salt_master.nodes[node_name]['linux_codename']
281 _arch = salt_master.nodes[node_name]['linux_arch']
Alexe9547d82019-06-03 15:22:50 -0500282 # get versions for tag, Openstack release and repo headers
283 # excluding 'nightly' repos by default
284 _r = {}
285 # if there is a forced tag = use it
286 if self.force_tag:
287 _r = self.rm.get_filtered_versions(
288 _name,
289 tag=self.force_tag,
290 include=[_os, _linux, _arch],
Alex9e4bfaf2019-06-11 15:21:59 -0500291 exclude=self.exclude_keywords
Alexe9547d82019-06-03 15:22:50 -0500292 )
293 # if nothing found, look everywhere
Alex9e4bfaf2019-06-11 15:21:59 -0500294 # but with no word 'openstack'
Alexe9547d82019-06-03 15:22:50 -0500295 if not _r:
296 _r = self.rm.get_filtered_versions(
297 _name,
298 tag=self.force_tag,
299 include=[_linux, _arch],
Alex9e4bfaf2019-06-11 15:21:59 -0500300 exclude=self.exclude_keywords + ['openstack']
Alexe9547d82019-06-03 15:22:50 -0500301 )
302 # if nothing is found at this point,
303 # repeat search using normal tags
304 if not _r:
305 _r = self.rm.get_filtered_versions(
306 _name,
307 tag=_mcp,
308 include=[_os, _linux, _arch],
Alex9e4bfaf2019-06-11 15:21:59 -0500309 exclude=self.exclude_keywords
Alexe9547d82019-06-03 15:22:50 -0500310 )
311 # Once again, if nothing found, look everywhere
312 if not _r:
313 _r = self.rm.get_filtered_versions(
314 _name,
315 tag=_mcp,
316 include=[_linux, _arch],
Alex9e4bfaf2019-06-11 15:21:59 -0500317 exclude=self.exclude_keywords + ['openstack']
Alexe9547d82019-06-03 15:22:50 -0500318 )
Alexcf91b182019-05-31 11:57:07 -0500319 # repack versions in flat format
Alexd0391d42019-05-21 18:48:55 -0500320 _vs = {}
321 _sections = {}
322 _apps = {}
Alex3bc95f62020-03-05 17:00:04 -0600323 for s, apps in _r.items():
324 for a, versions in apps.items():
325 for v, repos in versions.items():
Alexd0391d42019-05-21 18:48:55 -0500326 for repo in repos:
Alexcf91b182019-05-31 11:57:07 -0500327 if v not in _vs:
328 _vs[v] = []
329 _vs[v].append(repo)
330 if v not in _sections:
331 _sections[v] = []
332 _sections[v].append(s)
333 if v not in _apps:
334 _apps[v] = []
335 _apps[v].append(a)
336 # search for the newest version among filtered
Alexd0391d42019-05-21 18:48:55 -0500337 _r_desc = []
Alex3bc95f62020-03-05 17:00:04 -0600338 _vs_keys = iter(_vs.keys())
339 # get next version, if any
340 try:
341 _newest = DebianVersion(next(_vs_keys))
342 except StopIteration:
Alexd0391d42019-05-21 18:48:55 -0500343 _newest = DebianVersion('')
Alex3bc95f62020-03-05 17:00:04 -0600344 # iterate others, if any
Alexd0391d42019-05-21 18:48:55 -0500345 for v in _vs_keys:
346 _this = DebianVersion(v)
347 if _this > _newest:
348 _newest = _this
Alexd0391d42019-05-21 18:48:55 -0500349 _release = _newest
Alexcf91b182019-05-31 11:57:07 -0500350 # Get best description for the package
Alexd0391d42019-05-21 18:48:55 -0500351 if _release.version != 'n/a':
352 _r_desc = _vs[_release.version]
353 # preload special description
Alex41485522019-04-12 17:26:18 -0500354 if _desc[_name]:
Alex41485522019-04-12 17:26:18 -0500355 _pkg_desc = _desc[_name]
356 else:
Alex41485522019-04-12 17:26:18 -0500357 _pkg_desc = _desc.dummy_desc
Alexcf91b182019-05-31 11:57:07 -0500358 # Save repos list and desc for this version
Alexd0391d42019-05-21 18:48:55 -0500359 # Check if we can provide better from the package
360 if _release.version != 'n/a':
361 if not _pkg_desc['section']:
362 _pkg_desc['section'] = \
363 "/".join(_sections[_release.version])
364 if not _pkg_desc['app']:
365 _pkg_desc['app'] = \
366 "/".join(_apps[_release.version])
Alex3ebc5632019-04-18 16:47:18 -0500367
Alexcf91b182019-05-31 11:57:07 -0500368 # Populate package info, once for package
Alexd0391d42019-05-21 18:48:55 -0500369 _m = _r_desc[0]["maintainer"] if _r_desc else 'n/a'
Alex41485522019-04-12 17:26:18 -0500370 _all_packages[_name] = {
371 "desc": _pkg_desc,
Alexd0391d42019-05-21 18:48:55 -0500372 "repos": _r_desc,
373 "maintainer": _m,
374 "is_mirantis": self.rm.is_mirantis(
375 _name,
Alexcf91b182019-05-31 11:57:07 -0500376 tag=_mcp
Alexd0391d42019-05-21 18:48:55 -0500377 ),
Alex41485522019-04-12 17:26:18 -0500378 "results": {},
379 "r": _release,
Alex Savatieiev3db12a72019-03-22 16:32:31 -0500380 }
Alexcf91b182019-05-31 11:57:07 -0500381 # Cross-compare versions
Alex41485522019-04-12 17:26:18 -0500382 _cmp = VersionCmpResult(
383 _ver_ins,
384 _ver_can,
385 _all_packages[_name]['r']
386 )
Alexcf91b182019-05-31 11:57:07 -0500387 # Update results structure
Alex41485522019-04-12 17:26:18 -0500388 # shortcut to results
389 _res = _all_packages[_name]['results']
390 # update status
391 if _cmp.status not in _res:
392 _res[_cmp.status] = {}
393 # update action
394 if _cmp.action not in _res[_cmp.status]:
395 _res[_cmp.status][_cmp.action] = {}
396 # update node
397 if node_name not in _res[_cmp.status][_cmp.action]:
398 _res[_cmp.status][_cmp.action][node_name] = {}
399 # put result
400 _res[_cmp.status][_cmp.action][node_name] = {
401 'i': _ver_ins,
402 'c': _ver_can,
403 'res': _cmp,
404 'raw': _value['raw']
405 }
savex4448e132018-04-25 15:51:14 +0200406
Alex41485522019-04-12 17:26:18 -0500407 self._packages = _all_packages
Alexd9fd85e2019-05-16 16:58:24 -0500408 _progress.end()
savex4448e132018-04-25 15:51:14 +0200409
Alex41485522019-04-12 17:26:18 -0500410 def create_report(self, filename, rtype, full=None):
savex4448e132018-04-25 15:51:14 +0200411 """
412 Create static html showing packages diff per node
413
414 :return: buff with html
415 """
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600416 logger_cli.info("# Generating report to '{}'".format(filename))
Alex41485522019-04-12 17:26:18 -0500417 if rtype == 'html':
418 _type = reporter.HTMLPackageCandidates()
419 elif rtype == 'csv':
420 _type = reporter.CSVAllPackages()
421 else:
422 raise ConfigException("Report type not set")
Alex Savatieievd48994d2018-12-13 12:13:00 +0100423 _report = reporter.ReportToFile(
Alex41485522019-04-12 17:26:18 -0500424 _type,
savex4448e132018-04-25 15:51:14 +0200425 filename
426 )
Alex41485522019-04-12 17:26:18 -0500427 payload = {
Alexe0c5b9e2019-04-23 18:51:23 -0500428 "nodes": salt_master.nodes,
429 "mcp_release": salt_master.mcp_release,
430 "openstack_release": salt_master.openstack_release
Alex41485522019-04-12 17:26:18 -0500431 }
432 payload.update(self.presort_packages(self._packages, full))
433 _report(payload)
Alex Savatieiev799bee32019-02-20 17:19:26 -0600434 logger_cli.info("-> Done")