blob: 174691f1f37f30735aac546472753bdd980b9406 [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
Alex9a4ad212020-10-01 18:04:25 -05006from cfg_checker.common.settings import ENV_TYPE_SALT
Alex41485522019-04-12 17:26:18 -05007from cfg_checker.helpers.console_utils import Progress
Alexd0391d42019-05-21 18:48:55 -05008from cfg_checker.modules.packages.repos import RepoManager
Alex9a4ad212020-10-01 18:04:25 -05009from cfg_checker.nodes import SaltNodes, KubeNodes
Alex41485522019-04-12 17:26:18 -050010from cfg_checker.reports import reporter
11
Alex3bc95f62020-03-05 17:00:04 -060012from .versions import DebianVersion, PkgVersions, VersionCmpResult
savex4448e132018-04-25 15:51:14 +020013
14
Alexe0c5b9e2019-04-23 18:51:23 -050015class CloudPackageChecker(object):
Alexe9908f72020-05-19 16:04:53 -050016 def __init__(
17 self,
Alex9a4ad212020-10-01 18:04:25 -050018 config,
Alexe9908f72020-05-19 16:04:53 -050019 force_tag=None,
20 exclude_keywords=[],
21 skip_list=None,
22 skip_list_file=None
23 ):
Alex9a4ad212020-10-01 18:04:25 -050024 # check that this env tag is present in Manager
25 self.env_config = config
26 self.rm = RepoManager()
27 self.force_tag = force_tag
28 self.exclude_keywords = exclude_keywords
29
Alexd0391d42019-05-21 18:48:55 -050030 # Init salt master info
Alex9a4ad212020-10-01 18:04:25 -050031 if not self.master.nodes:
32 self.master.nodes = self.master.get_nodes(
Alexe9908f72020-05-19 16:04:53 -050033 skip_list=skip_list,
34 skip_list_file=skip_list_file
35 )
Alexd0391d42019-05-21 18:48:55 -050036
Alex9a4ad212020-10-01 18:04:25 -050037 _tags = self.rm.get_available_tags(tag=self.master.mcp_release)
Alexd0391d42019-05-21 18:48:55 -050038 if not _tags:
39 logger_cli.warning(
Alex9a4ad212020-10-01 18:04:25 -050040 "\n# WARNING: '{0}' is not listed in repo index. "
Alexd0391d42019-05-21 18:48:55 -050041 "Consider running:\n\t{1}\nto add info on this tag's "
42 "release package versions".format(
Alex9a4ad212020-10-01 18:04:25 -050043 self.master.mcp_release,
44 "mcp-checker packages versions --tag <target_tag>"
Alexd0391d42019-05-21 18:48:55 -050045 )
46 )
47
Alex41485522019-04-12 17:26:18 -050048 @staticmethod
49 def presort_packages(all_packages, full=None):
50 logger_cli.info("-> Presorting packages")
51 # labels
52 _data = {}
53 _data = {
54 "cs": {
55 "ok": const.VERSION_OK,
56 "up": const.VERSION_UP,
57 "down": const.VERSION_DOWN,
Alex26b8a8c2019-10-09 17:09:07 -050058 "warn": const.VERSION_WARN,
Alex41485522019-04-12 17:26:18 -050059 "err": const.VERSION_ERR
60 },
61 "ca": {
62 "na": const.ACT_NA,
63 "up": const.ACT_UPGRADE,
64 "need_up": const.ACT_NEED_UP,
65 "need_down": const.ACT_NEED_DOWN,
66 "repo": const.ACT_REPO
67 }
68 }
69 _data['status_err'] = const.VERSION_ERR
Alex26b8a8c2019-10-09 17:09:07 -050070 _data['status_warn'] = const.VERSION_WARN
Alex41485522019-04-12 17:26:18 -050071 _data['status_down'] = const.VERSION_DOWN
Alexe9908f72020-05-19 16:04:53 -050072 _data['status_skip'] = const.VERSION_NA
Alex41485522019-04-12 17:26:18 -050073
74 # Presort packages
75 _data['critical'] = {}
76 _data['system'] = {}
77 _data['other'] = {}
78 _data['unlisted'] = {}
79
80 _l = len(all_packages)
81 _progress = Progress(_l)
82 _progress_index = 0
83 # counters
84 _ec = _es = _eo = _eu = 0
Alex26b8a8c2019-10-09 17:09:07 -050085 _wc = _ws = _wo = _wu = 0
Alex41485522019-04-12 17:26:18 -050086 _dc = _ds = _do = _du = 0
87 while _progress_index < _l:
88 # progress bar
89 _progress_index += 1
90 _progress.write_progress(_progress_index)
91 # sort packages
92 _pn, _val = all_packages.popitem()
Alexd0391d42019-05-21 18:48:55 -050093 _c = _val['desc']['section']
Alex3bc95f62020-03-05 17:00:04 -060094 _rkeys = _val['results'].keys()
Alexd0391d42019-05-21 18:48:55 -050095
Alexe0c5b9e2019-04-23 18:51:23 -050096 if not full:
Alex41485522019-04-12 17:26:18 -050097 # Check if this packet has errors
98 # if all is ok -> just skip it
99 _max_status = max(_val['results'].keys())
100 if _max_status <= const.VERSION_OK:
101 _max_action = max(_val['results'][_max_status].keys())
102 if _max_action == const.ACT_NA:
Alexd0391d42019-05-21 18:48:55 -0500103 # this package does not have any comments
Alex41485522019-04-12 17:26:18 -0500104 # ...just skip it from report
105 continue
106
Alex26b8a8c2019-10-09 17:09:07 -0500107 _differ = len(set(_val['results'].keys())) > 1
108 if _differ:
109 # in case package has different status across nodes
110 # Warning becomes Error.
111 if const.VERSION_WARN in _val['results']:
112 if const.VERSION_ERR in _val['results']:
113 # add warns to err
114 # should never happen, though
115 merge_dict(
116 _val['results'].pop(const.VERSION_WARN),
117 _val['results'][const.VERSION_ERR]
118 )
119 else:
120 _val['results'][const.VERSION_ERR] = \
121 _val['results'].pop(const.VERSION_WARN)
122 else:
123 # in case package has same status on all nodes
124 # Error becomes Warning
125 if const.VERSION_ERR in _val['results']:
126 if const.VERSION_WARN in _val['results']:
127 # add warns to err
128 # should never happen, though
129 merge_dict(
130 _val['results'].pop(const.VERSION_ERR),
131 _val['results'][const.VERSION_WARN]
132 )
133 else:
134 _val['results'][const.VERSION_WARN] = \
135 _val['results'].pop(const.VERSION_ERR)
136
Alexd0391d42019-05-21 18:48:55 -0500137 if len(_c) > 0 and _val['is_mirantis'] is None:
Alex41485522019-04-12 17:26:18 -0500138 # not listed package in version lib
139 _data['unlisted'].update({
140 _pn: _val
141 })
Alex3bc95f62020-03-05 17:00:04 -0600142 _eu += sum(x == const.VERSION_ERR for x in _rkeys)
143 _wu += sum(x == const.VERSION_WARN for x in _rkeys)
144 _du += sum(x == const.VERSION_DOWN for x in _rkeys)
Alex41485522019-04-12 17:26:18 -0500145 # mirantis/critical
Alexd0391d42019-05-21 18:48:55 -0500146 # elif len(_c) > 0 and _c != 'System':
147 elif _val['is_mirantis']:
Alex41485522019-04-12 17:26:18 -0500148 # not blank and not system
149 _data['critical'].update({
150 _pn: _val
151 })
Alex3bc95f62020-03-05 17:00:04 -0600152 _ec += sum(x == const.VERSION_ERR for x in _rkeys)
153 _wc += sum(x == const.VERSION_WARN for x in _rkeys)
154 _dc += sum(x == const.VERSION_DOWN for x in _rkeys)
Alex41485522019-04-12 17:26:18 -0500155 # system
156 elif _c == 'System':
157 _data['system'].update({
158 _pn: _val
159 })
Alex3bc95f62020-03-05 17:00:04 -0600160 _es += sum(x == const.VERSION_ERR for x in _rkeys)
161 _ws += sum(x == const.VERSION_WARN for x in _rkeys)
162 _ds += sum(x == const.VERSION_DOWN for x in _rkeys)
Alex41485522019-04-12 17:26:18 -0500163 # rest
164 else:
165 _data['other'].update({
166 _pn: _val
167 })
Alex3bc95f62020-03-05 17:00:04 -0600168 _eo += sum(x == const.VERSION_ERR for x in _rkeys)
169 _wo += sum(x == const.VERSION_WARN for x in _rkeys)
170 _do += sum(x == const.VERSION_DOWN for x in _rkeys)
Alex41485522019-04-12 17:26:18 -0500171
Alexd9fd85e2019-05-16 16:58:24 -0500172 _progress.end()
Alex41485522019-04-12 17:26:18 -0500173
174 _data['errors'] = {
175 'mirantis': _ec,
176 'system': _es,
177 'other': _eo,
178 'unlisted': _eu
179 }
Alex26b8a8c2019-10-09 17:09:07 -0500180 _data['warnings'] = {
181 'mirantis': _wc,
182 'system': _ws,
183 'other': _wo,
184 'unlisted': _wu
185 }
Alex41485522019-04-12 17:26:18 -0500186 _data['downgrades'] = {
187 'mirantis': _dc,
188 'system': _ds,
189 'other': _do,
190 'unlisted': _du
191 }
192
193 return _data
194
savex4448e132018-04-25 15:51:14 +0200195 def collect_packages(self):
196 """
197 Check package versions in repos vs installed
198
199 :return: no return values, all date put to dict in place
200 """
Alex41485522019-04-12 17:26:18 -0500201 # Preload OpenStack release versions
Alex9a4ad212020-10-01 18:04:25 -0500202 _desc = PkgVersions(self.env_config)
Alex3ebc5632019-04-18 16:47:18 -0500203 logger_cli.info(
204 "# Cross-comparing: Installed vs Candidates vs Release"
205 )
Alexd0391d42019-05-21 18:48:55 -0500206 # shortcuts for this cloud values
Alex9a4ad212020-10-01 18:04:25 -0500207 _os = self.master.openstack_release
208 _mcp = self.master.mcp_release
Alexe9547d82019-06-03 15:22:50 -0500209 _t = [self.force_tag] if self.force_tag else []
210 _t.append(_mcp)
211
212 logger_cli.info("# Tag search list: {}".format(", ".join(_t)))
213 logger_cli.info("# Openstack version: {}".format(_os))
214 logger_cli.info(
215 "# Release versions repos keyword exclude list: {}".format(
Alex9e4bfaf2019-06-11 15:21:59 -0500216 ", ".join(self.exclude_keywords)
Alexe9547d82019-06-03 15:22:50 -0500217 )
218 )
219
Alexd0391d42019-05-21 18:48:55 -0500220 # Progress class
Alex9a4ad212020-10-01 18:04:25 -0500221 _progress = Progress(len(self.master.nodes.keys()))
Alex41485522019-04-12 17:26:18 -0500222 _progress_index = 0
223 _total_processed = 0
Alex Savatieiev3db12a72019-03-22 16:32:31 -0500224 # Collect packages from all of the nodes in flat dict
Alex41485522019-04-12 17:26:18 -0500225 _all_packages = {}
Alex9a4ad212020-10-01 18:04:25 -0500226 for node_name, node_value in self.master.nodes.items():
Alex41485522019-04-12 17:26:18 -0500227 _uniq_len = len(_all_packages.keys())
228 _progress_index += 1
Alexd0391d42019-05-21 18:48:55 -0500229 # progress updates shown before next node only
230 # it is costly operation to do it for each of the 150k packages
Alex41485522019-04-12 17:26:18 -0500231 _progress.write_progress(
232 _progress_index,
233 note="/ {} uniq out of {} packages found".format(
234 _uniq_len,
235 _total_processed
236 )
237 )
Alex3bc95f62020-03-05 17:00:04 -0600238 for _name, _value in node_value['packages'].items():
Alex41485522019-04-12 17:26:18 -0500239 _total_processed += 1
Alexcf91b182019-05-31 11:57:07 -0500240 # Parse versions from nodes
Alex41485522019-04-12 17:26:18 -0500241 _ver_ins = DebianVersion(_value['installed'])
242 _ver_can = DebianVersion(_value['candidate'])
243
Alexcf91b182019-05-31 11:57:07 -0500244 # Process package description and release version
245 # at a first sight
Alex41485522019-04-12 17:26:18 -0500246 if _name not in _all_packages:
Alexcf91b182019-05-31 11:57:07 -0500247 # get node attributes
Alex9a4ad212020-10-01 18:04:25 -0500248 _linux = \
249 self.master.nodes[node_name]['linux_codename']
250 _arch = self.master.nodes[node_name]['linux_arch']
Alexe9547d82019-06-03 15:22:50 -0500251 # get versions for tag, Openstack release and repo headers
252 # excluding 'nightly' repos by default
253 _r = {}
254 # if there is a forced tag = use it
255 if self.force_tag:
256 _r = self.rm.get_filtered_versions(
257 _name,
258 tag=self.force_tag,
259 include=[_os, _linux, _arch],
Alex9e4bfaf2019-06-11 15:21:59 -0500260 exclude=self.exclude_keywords
Alexe9547d82019-06-03 15:22:50 -0500261 )
262 # if nothing found, look everywhere
Alex9e4bfaf2019-06-11 15:21:59 -0500263 # but with no word 'openstack'
Alexe9547d82019-06-03 15:22:50 -0500264 if not _r:
265 _r = self.rm.get_filtered_versions(
266 _name,
267 tag=self.force_tag,
268 include=[_linux, _arch],
Alex9e4bfaf2019-06-11 15:21:59 -0500269 exclude=self.exclude_keywords + ['openstack']
Alexe9547d82019-06-03 15:22:50 -0500270 )
271 # if nothing is found at this point,
272 # repeat search using normal tags
273 if not _r:
274 _r = self.rm.get_filtered_versions(
275 _name,
276 tag=_mcp,
277 include=[_os, _linux, _arch],
Alex9e4bfaf2019-06-11 15:21:59 -0500278 exclude=self.exclude_keywords
Alexe9547d82019-06-03 15:22:50 -0500279 )
280 # Once again, if nothing found, look everywhere
281 if not _r:
282 _r = self.rm.get_filtered_versions(
283 _name,
284 tag=_mcp,
285 include=[_linux, _arch],
Alex9e4bfaf2019-06-11 15:21:59 -0500286 exclude=self.exclude_keywords + ['openstack']
Alexe9547d82019-06-03 15:22:50 -0500287 )
Alexcf91b182019-05-31 11:57:07 -0500288 # repack versions in flat format
Alexd0391d42019-05-21 18:48:55 -0500289 _vs = {}
290 _sections = {}
291 _apps = {}
Alex3bc95f62020-03-05 17:00:04 -0600292 for s, apps in _r.items():
293 for a, versions in apps.items():
294 for v, repos in versions.items():
Alexd0391d42019-05-21 18:48:55 -0500295 for repo in repos:
Alexcf91b182019-05-31 11:57:07 -0500296 if v not in _vs:
297 _vs[v] = []
298 _vs[v].append(repo)
299 if v not in _sections:
300 _sections[v] = []
301 _sections[v].append(s)
302 if v not in _apps:
303 _apps[v] = []
304 _apps[v].append(a)
305 # search for the newest version among filtered
Alexd0391d42019-05-21 18:48:55 -0500306 _r_desc = []
Alex3bc95f62020-03-05 17:00:04 -0600307 _vs_keys = iter(_vs.keys())
308 # get next version, if any
309 try:
310 _newest = DebianVersion(next(_vs_keys))
311 except StopIteration:
Alexd0391d42019-05-21 18:48:55 -0500312 _newest = DebianVersion('')
Alex3bc95f62020-03-05 17:00:04 -0600313 # iterate others, if any
Alexd0391d42019-05-21 18:48:55 -0500314 for v in _vs_keys:
315 _this = DebianVersion(v)
316 if _this > _newest:
317 _newest = _this
Alexd0391d42019-05-21 18:48:55 -0500318 _release = _newest
Alexcf91b182019-05-31 11:57:07 -0500319 # Get best description for the package
Alexd0391d42019-05-21 18:48:55 -0500320 if _release.version != 'n/a':
321 _r_desc = _vs[_release.version]
322 # preload special description
Alex41485522019-04-12 17:26:18 -0500323 if _desc[_name]:
Alex41485522019-04-12 17:26:18 -0500324 _pkg_desc = _desc[_name]
325 else:
Alex41485522019-04-12 17:26:18 -0500326 _pkg_desc = _desc.dummy_desc
Alexcf91b182019-05-31 11:57:07 -0500327 # Save repos list and desc for this version
Alexd0391d42019-05-21 18:48:55 -0500328 # Check if we can provide better from the package
329 if _release.version != 'n/a':
330 if not _pkg_desc['section']:
331 _pkg_desc['section'] = \
332 "/".join(_sections[_release.version])
333 if not _pkg_desc['app']:
334 _pkg_desc['app'] = \
335 "/".join(_apps[_release.version])
Alex3ebc5632019-04-18 16:47:18 -0500336
Alexcf91b182019-05-31 11:57:07 -0500337 # Populate package info, once for package
Alexd0391d42019-05-21 18:48:55 -0500338 _m = _r_desc[0]["maintainer"] if _r_desc else 'n/a'
Alex41485522019-04-12 17:26:18 -0500339 _all_packages[_name] = {
340 "desc": _pkg_desc,
Alexd0391d42019-05-21 18:48:55 -0500341 "repos": _r_desc,
342 "maintainer": _m,
343 "is_mirantis": self.rm.is_mirantis(
344 _name,
Alexcf91b182019-05-31 11:57:07 -0500345 tag=_mcp
Alexd0391d42019-05-21 18:48:55 -0500346 ),
Alex41485522019-04-12 17:26:18 -0500347 "results": {},
348 "r": _release,
Alex Savatieiev3db12a72019-03-22 16:32:31 -0500349 }
Alexcf91b182019-05-31 11:57:07 -0500350 # Cross-compare versions
Alex41485522019-04-12 17:26:18 -0500351 _cmp = VersionCmpResult(
352 _ver_ins,
353 _ver_can,
354 _all_packages[_name]['r']
355 )
Alexcf91b182019-05-31 11:57:07 -0500356 # Update results structure
Alex41485522019-04-12 17:26:18 -0500357 # shortcut to results
358 _res = _all_packages[_name]['results']
359 # update status
360 if _cmp.status not in _res:
361 _res[_cmp.status] = {}
362 # update action
363 if _cmp.action not in _res[_cmp.status]:
364 _res[_cmp.status][_cmp.action] = {}
365 # update node
366 if node_name not in _res[_cmp.status][_cmp.action]:
367 _res[_cmp.status][_cmp.action][node_name] = {}
368 # put result
369 _res[_cmp.status][_cmp.action][node_name] = {
370 'i': _ver_ins,
371 'c': _ver_can,
372 'res': _cmp,
373 'raw': _value['raw']
374 }
savex4448e132018-04-25 15:51:14 +0200375
Alex41485522019-04-12 17:26:18 -0500376 self._packages = _all_packages
Alexd9fd85e2019-05-16 16:58:24 -0500377 _progress.end()
savex4448e132018-04-25 15:51:14 +0200378
Alex41485522019-04-12 17:26:18 -0500379 def create_report(self, filename, rtype, full=None):
savex4448e132018-04-25 15:51:14 +0200380 """
381 Create static html showing packages diff per node
382
383 :return: buff with html
384 """
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600385 logger_cli.info("# Generating report to '{}'".format(filename))
Alex41485522019-04-12 17:26:18 -0500386 if rtype == 'html':
Alex9a4ad212020-10-01 18:04:25 -0500387 _type = reporter.HTMLPackageCandidates(self.master)
Alex41485522019-04-12 17:26:18 -0500388 elif rtype == 'csv':
Alex9a4ad212020-10-01 18:04:25 -0500389 _type = reporter.CSVAllPackages(self.master)
Alex41485522019-04-12 17:26:18 -0500390 else:
391 raise ConfigException("Report type not set")
Alex Savatieievd48994d2018-12-13 12:13:00 +0100392 _report = reporter.ReportToFile(
Alex41485522019-04-12 17:26:18 -0500393 _type,
savex4448e132018-04-25 15:51:14 +0200394 filename
395 )
Alex41485522019-04-12 17:26:18 -0500396 payload = {
Alex9a4ad212020-10-01 18:04:25 -0500397 "nodes": self.master.nodes,
398 "mcp_release": self.master.mcp_release,
399 "openstack_release": self.master.openstack_release
Alex41485522019-04-12 17:26:18 -0500400 }
401 payload.update(self.presort_packages(self._packages, full))
402 _report(payload)
Alex Savatieiev799bee32019-02-20 17:19:26 -0600403 logger_cli.info("-> Done")
Alex9a4ad212020-10-01 18:04:25 -0500404
405 def collect_installed_packages(self):
406 """
407 Collect installed packages on each node
408 sets 'installed' dict property in the class
409
410 :return: none
411 """
412 logger_cli.info("# Collecting installed packages")
413 if self.master.env_type == ENV_TYPE_SALT:
414 self.master.prepare_script_on_active_nodes("pkg_versions.py")
415 _result = self.master.execute_script_on_active_nodes(
416 "pkg_versions.py"
417 )
418
419 for key in self.master.nodes.keys():
420 # due to much data to be passed from salt, it is happening in order
421 if key in _result and _result[key]:
422 _text = _result[key]
423 try:
424 _dict = json.loads(_text[_text.find('{'):])
425 except ValueError:
426 logger_cli.info("... no JSON for '{}'".format(
427 key
428 ))
429 logger_cli.debug(
430 "ERROR:\n{}\n".format(_text[:_text.find('{')])
431 )
432 _dict = {}
433
434 self.master.nodes[key]['packages'] = _dict
435 else:
436 self.master.nodes[key]['packages'] = {}
437 logger_cli.debug("... {} has {} packages installed".format(
438 key,
439 len(self.master.nodes[key]['packages'].keys())
440 ))
441 logger_cli.info("-> Done")
442
443
444class SaltCloudPackageChecker(CloudPackageChecker):
445 def __init__(
446 self,
447 config,
448 force_tag=None,
449 exclude_keywords=[],
450 skip_list=None,
451 skip_list_file=None
452 ):
453 self.master = SaltNodes(config)
454 super(SaltCloudPackageChecker, self).__init__(
455 config,
Alexccb72e02021-01-20 16:38:03 -0600456 force_tag=force_tag,
Alex9a4ad212020-10-01 18:04:25 -0500457 exclude_keywords=[],
Alex359e5752021-08-16 17:28:30 -0500458 skip_list=skip_list,
459 skip_list_file=skip_list_file
Alex9a4ad212020-10-01 18:04:25 -0500460 )
461
462
463class KubeCloudPackageChecker(CloudPackageChecker):
464 def __init__(
465 self,
466 config,
467 force_tag=None,
468 exclude_keywords=[],
469 skip_list=None,
470 skip_list_file=None
471 ):
472 self.master = KubeNodes(config)
473 super(KubeCloudPackageChecker, self).__init__(
474 config,
Alexccb72e02021-01-20 16:38:03 -0600475 force_tag=force_tag,
Alex9a4ad212020-10-01 18:04:25 -0500476 exclude_keywords=[],
Alex359e5752021-08-16 17:28:30 -0500477 skip_list=skip_list,
478 skip_list_file=skip_list_file
Alex9a4ad212020-10-01 18:04:25 -0500479 )