blob: e8339cb185b3bc833aeca0fd4001b93790667c31 [file] [log] [blame]
Alex0989ecf2022-03-29 13:43:21 -05001# Author: Alex Savatieiev (osavatieiev@mirantis.com; a.savex@gmail.com)
2# Copyright 2019-2022 Mirantis, Inc.
savex4448e132018-04-25 15:51:14 +02003import json
savex4448e132018-04-25 15:51:14 +02004
Alex3ebc5632019-04-18 16:47:18 -05005from cfg_checker.common import const, logger_cli
Alex41485522019-04-12 17:26:18 -05006from cfg_checker.common.exception import ConfigException
Alex26b8a8c2019-10-09 17:09:07 -05007from cfg_checker.common.other import merge_dict
Alex9a4ad212020-10-01 18:04:25 -05008from cfg_checker.common.settings import ENV_TYPE_SALT
Alex41485522019-04-12 17:26:18 -05009from cfg_checker.helpers.console_utils import Progress
Alexd0391d42019-05-21 18:48:55 -050010from cfg_checker.modules.packages.repos import RepoManager
Alex9a4ad212020-10-01 18:04:25 -050011from cfg_checker.nodes import SaltNodes, KubeNodes
Alex41485522019-04-12 17:26:18 -050012from cfg_checker.reports import reporter
13
Alex3bc95f62020-03-05 17:00:04 -060014from .versions import DebianVersion, PkgVersions, VersionCmpResult
savex4448e132018-04-25 15:51:14 +020015
16
Alexe0c5b9e2019-04-23 18:51:23 -050017class CloudPackageChecker(object):
Alexe9908f72020-05-19 16:04:53 -050018 def __init__(
19 self,
Alex9a4ad212020-10-01 18:04:25 -050020 config,
Alexe9908f72020-05-19 16:04:53 -050021 force_tag=None,
22 exclude_keywords=[],
23 skip_list=None,
24 skip_list_file=None
25 ):
Alex9a4ad212020-10-01 18:04:25 -050026 # check that this env tag is present in Manager
27 self.env_config = config
28 self.rm = RepoManager()
29 self.force_tag = force_tag
30 self.exclude_keywords = exclude_keywords
31
Alexd0391d42019-05-21 18:48:55 -050032 # Init salt master info
Alex9a4ad212020-10-01 18:04:25 -050033 if not self.master.nodes:
34 self.master.nodes = self.master.get_nodes(
Alexe9908f72020-05-19 16:04:53 -050035 skip_list=skip_list,
36 skip_list_file=skip_list_file
37 )
Alexd0391d42019-05-21 18:48:55 -050038
Alex9a4ad212020-10-01 18:04:25 -050039 _tags = self.rm.get_available_tags(tag=self.master.mcp_release)
Alexd0391d42019-05-21 18:48:55 -050040 if not _tags:
41 logger_cli.warning(
Alex9a4ad212020-10-01 18:04:25 -050042 "\n# WARNING: '{0}' is not listed in repo index. "
Alexd0391d42019-05-21 18:48:55 -050043 "Consider running:\n\t{1}\nto add info on this tag's "
44 "release package versions".format(
Alex9a4ad212020-10-01 18:04:25 -050045 self.master.mcp_release,
46 "mcp-checker packages versions --tag <target_tag>"
Alexd0391d42019-05-21 18:48:55 -050047 )
48 )
49
Alex41485522019-04-12 17:26:18 -050050 @staticmethod
51 def presort_packages(all_packages, full=None):
52 logger_cli.info("-> Presorting packages")
53 # labels
54 _data = {}
55 _data = {
56 "cs": {
57 "ok": const.VERSION_OK,
58 "up": const.VERSION_UP,
59 "down": const.VERSION_DOWN,
Alex26b8a8c2019-10-09 17:09:07 -050060 "warn": const.VERSION_WARN,
Alex41485522019-04-12 17:26:18 -050061 "err": const.VERSION_ERR
62 },
63 "ca": {
64 "na": const.ACT_NA,
65 "up": const.ACT_UPGRADE,
66 "need_up": const.ACT_NEED_UP,
67 "need_down": const.ACT_NEED_DOWN,
68 "repo": const.ACT_REPO
69 }
70 }
71 _data['status_err'] = const.VERSION_ERR
Alex26b8a8c2019-10-09 17:09:07 -050072 _data['status_warn'] = const.VERSION_WARN
Alex41485522019-04-12 17:26:18 -050073 _data['status_down'] = const.VERSION_DOWN
Alexe9908f72020-05-19 16:04:53 -050074 _data['status_skip'] = const.VERSION_NA
Alex41485522019-04-12 17:26:18 -050075
76 # Presort packages
77 _data['critical'] = {}
78 _data['system'] = {}
79 _data['other'] = {}
80 _data['unlisted'] = {}
81
82 _l = len(all_packages)
83 _progress = Progress(_l)
84 _progress_index = 0
85 # counters
86 _ec = _es = _eo = _eu = 0
Alex26b8a8c2019-10-09 17:09:07 -050087 _wc = _ws = _wo = _wu = 0
Alex41485522019-04-12 17:26:18 -050088 _dc = _ds = _do = _du = 0
89 while _progress_index < _l:
90 # progress bar
91 _progress_index += 1
92 _progress.write_progress(_progress_index)
93 # sort packages
94 _pn, _val = all_packages.popitem()
Alexd0391d42019-05-21 18:48:55 -050095 _c = _val['desc']['section']
Alex3bc95f62020-03-05 17:00:04 -060096 _rkeys = _val['results'].keys()
Alexd0391d42019-05-21 18:48:55 -050097
Alexe0c5b9e2019-04-23 18:51:23 -050098 if not full:
Alex41485522019-04-12 17:26:18 -050099 # Check if this packet has errors
100 # if all is ok -> just skip it
101 _max_status = max(_val['results'].keys())
102 if _max_status <= const.VERSION_OK:
103 _max_action = max(_val['results'][_max_status].keys())
104 if _max_action == const.ACT_NA:
Alexd0391d42019-05-21 18:48:55 -0500105 # this package does not have any comments
Alex41485522019-04-12 17:26:18 -0500106 # ...just skip it from report
107 continue
108
Alex26b8a8c2019-10-09 17:09:07 -0500109 _differ = len(set(_val['results'].keys())) > 1
110 if _differ:
111 # in case package has different status across nodes
112 # Warning becomes Error.
113 if const.VERSION_WARN in _val['results']:
114 if const.VERSION_ERR in _val['results']:
115 # add warns to err
116 # should never happen, though
117 merge_dict(
118 _val['results'].pop(const.VERSION_WARN),
119 _val['results'][const.VERSION_ERR]
120 )
121 else:
122 _val['results'][const.VERSION_ERR] = \
123 _val['results'].pop(const.VERSION_WARN)
124 else:
125 # in case package has same status on all nodes
126 # Error becomes Warning
127 if const.VERSION_ERR in _val['results']:
128 if const.VERSION_WARN in _val['results']:
129 # add warns to err
130 # should never happen, though
131 merge_dict(
132 _val['results'].pop(const.VERSION_ERR),
133 _val['results'][const.VERSION_WARN]
134 )
135 else:
136 _val['results'][const.VERSION_WARN] = \
137 _val['results'].pop(const.VERSION_ERR)
138
Alexd0391d42019-05-21 18:48:55 -0500139 if len(_c) > 0 and _val['is_mirantis'] is None:
Alex41485522019-04-12 17:26:18 -0500140 # not listed package in version lib
141 _data['unlisted'].update({
142 _pn: _val
143 })
Alex3bc95f62020-03-05 17:00:04 -0600144 _eu += sum(x == const.VERSION_ERR for x in _rkeys)
145 _wu += sum(x == const.VERSION_WARN for x in _rkeys)
146 _du += sum(x == const.VERSION_DOWN for x in _rkeys)
Alex41485522019-04-12 17:26:18 -0500147 # mirantis/critical
Alexd0391d42019-05-21 18:48:55 -0500148 # elif len(_c) > 0 and _c != 'System':
149 elif _val['is_mirantis']:
Alex41485522019-04-12 17:26:18 -0500150 # not blank and not system
151 _data['critical'].update({
152 _pn: _val
153 })
Alex3bc95f62020-03-05 17:00:04 -0600154 _ec += sum(x == const.VERSION_ERR for x in _rkeys)
155 _wc += sum(x == const.VERSION_WARN for x in _rkeys)
156 _dc += sum(x == const.VERSION_DOWN for x in _rkeys)
Alex41485522019-04-12 17:26:18 -0500157 # system
158 elif _c == 'System':
159 _data['system'].update({
160 _pn: _val
161 })
Alex3bc95f62020-03-05 17:00:04 -0600162 _es += sum(x == const.VERSION_ERR for x in _rkeys)
163 _ws += sum(x == const.VERSION_WARN for x in _rkeys)
164 _ds += sum(x == const.VERSION_DOWN for x in _rkeys)
Alex41485522019-04-12 17:26:18 -0500165 # rest
166 else:
167 _data['other'].update({
168 _pn: _val
169 })
Alex3bc95f62020-03-05 17:00:04 -0600170 _eo += sum(x == const.VERSION_ERR for x in _rkeys)
171 _wo += sum(x == const.VERSION_WARN for x in _rkeys)
172 _do += sum(x == const.VERSION_DOWN for x in _rkeys)
Alex41485522019-04-12 17:26:18 -0500173
Alexd9fd85e2019-05-16 16:58:24 -0500174 _progress.end()
Alex41485522019-04-12 17:26:18 -0500175
176 _data['errors'] = {
177 'mirantis': _ec,
178 'system': _es,
179 'other': _eo,
180 'unlisted': _eu
181 }
Alex26b8a8c2019-10-09 17:09:07 -0500182 _data['warnings'] = {
183 'mirantis': _wc,
184 'system': _ws,
185 'other': _wo,
186 'unlisted': _wu
187 }
Alex41485522019-04-12 17:26:18 -0500188 _data['downgrades'] = {
189 'mirantis': _dc,
190 'system': _ds,
191 'other': _do,
192 'unlisted': _du
193 }
194
195 return _data
196
savex4448e132018-04-25 15:51:14 +0200197 def collect_packages(self):
198 """
199 Check package versions in repos vs installed
200
201 :return: no return values, all date put to dict in place
202 """
Alex41485522019-04-12 17:26:18 -0500203 # Preload OpenStack release versions
Alex9a4ad212020-10-01 18:04:25 -0500204 _desc = PkgVersions(self.env_config)
Alex3ebc5632019-04-18 16:47:18 -0500205 logger_cli.info(
206 "# Cross-comparing: Installed vs Candidates vs Release"
207 )
Alexd0391d42019-05-21 18:48:55 -0500208 # shortcuts for this cloud values
Alex9a4ad212020-10-01 18:04:25 -0500209 _os = self.master.openstack_release
210 _mcp = self.master.mcp_release
Alexe9547d82019-06-03 15:22:50 -0500211 _t = [self.force_tag] if self.force_tag else []
212 _t.append(_mcp)
213
214 logger_cli.info("# Tag search list: {}".format(", ".join(_t)))
215 logger_cli.info("# Openstack version: {}".format(_os))
216 logger_cli.info(
217 "# Release versions repos keyword exclude list: {}".format(
Alex9e4bfaf2019-06-11 15:21:59 -0500218 ", ".join(self.exclude_keywords)
Alexe9547d82019-06-03 15:22:50 -0500219 )
220 )
221
Alexd0391d42019-05-21 18:48:55 -0500222 # Progress class
Alex9a4ad212020-10-01 18:04:25 -0500223 _progress = Progress(len(self.master.nodes.keys()))
Alex41485522019-04-12 17:26:18 -0500224 _progress_index = 0
225 _total_processed = 0
Alex Savatieiev3db12a72019-03-22 16:32:31 -0500226 # Collect packages from all of the nodes in flat dict
Alex41485522019-04-12 17:26:18 -0500227 _all_packages = {}
Alex9a4ad212020-10-01 18:04:25 -0500228 for node_name, node_value in self.master.nodes.items():
Alex41485522019-04-12 17:26:18 -0500229 _uniq_len = len(_all_packages.keys())
230 _progress_index += 1
Alexd0391d42019-05-21 18:48:55 -0500231 # progress updates shown before next node only
232 # it is costly operation to do it for each of the 150k packages
Alex41485522019-04-12 17:26:18 -0500233 _progress.write_progress(
234 _progress_index,
235 note="/ {} uniq out of {} packages found".format(
236 _uniq_len,
237 _total_processed
238 )
239 )
Alex3bc95f62020-03-05 17:00:04 -0600240 for _name, _value in node_value['packages'].items():
Alex41485522019-04-12 17:26:18 -0500241 _total_processed += 1
Alexcf91b182019-05-31 11:57:07 -0500242 # Parse versions from nodes
Alex41485522019-04-12 17:26:18 -0500243 _ver_ins = DebianVersion(_value['installed'])
244 _ver_can = DebianVersion(_value['candidate'])
245
Alexcf91b182019-05-31 11:57:07 -0500246 # Process package description and release version
247 # at a first sight
Alex41485522019-04-12 17:26:18 -0500248 if _name not in _all_packages:
Alexcf91b182019-05-31 11:57:07 -0500249 # get node attributes
Alex9a4ad212020-10-01 18:04:25 -0500250 _linux = \
251 self.master.nodes[node_name]['linux_codename']
252 _arch = self.master.nodes[node_name]['linux_arch']
Alexe9547d82019-06-03 15:22:50 -0500253 # get versions for tag, Openstack release and repo headers
254 # excluding 'nightly' repos by default
255 _r = {}
256 # if there is a forced tag = use it
257 if self.force_tag:
258 _r = self.rm.get_filtered_versions(
259 _name,
260 tag=self.force_tag,
261 include=[_os, _linux, _arch],
Alex9e4bfaf2019-06-11 15:21:59 -0500262 exclude=self.exclude_keywords
Alexe9547d82019-06-03 15:22:50 -0500263 )
264 # if nothing found, look everywhere
Alex9e4bfaf2019-06-11 15:21:59 -0500265 # but with no word 'openstack'
Alexe9547d82019-06-03 15:22:50 -0500266 if not _r:
267 _r = self.rm.get_filtered_versions(
268 _name,
269 tag=self.force_tag,
270 include=[_linux, _arch],
Alex9e4bfaf2019-06-11 15:21:59 -0500271 exclude=self.exclude_keywords + ['openstack']
Alexe9547d82019-06-03 15:22:50 -0500272 )
273 # if nothing is found at this point,
274 # repeat search using normal tags
275 if not _r:
276 _r = self.rm.get_filtered_versions(
277 _name,
278 tag=_mcp,
279 include=[_os, _linux, _arch],
Alex9e4bfaf2019-06-11 15:21:59 -0500280 exclude=self.exclude_keywords
Alexe9547d82019-06-03 15:22:50 -0500281 )
282 # Once again, if nothing found, look everywhere
283 if not _r:
284 _r = self.rm.get_filtered_versions(
285 _name,
286 tag=_mcp,
287 include=[_linux, _arch],
Alex9e4bfaf2019-06-11 15:21:59 -0500288 exclude=self.exclude_keywords + ['openstack']
Alexe9547d82019-06-03 15:22:50 -0500289 )
Alexcf91b182019-05-31 11:57:07 -0500290 # repack versions in flat format
Alexd0391d42019-05-21 18:48:55 -0500291 _vs = {}
292 _sections = {}
293 _apps = {}
Alex3bc95f62020-03-05 17:00:04 -0600294 for s, apps in _r.items():
295 for a, versions in apps.items():
296 for v, repos in versions.items():
Alexd0391d42019-05-21 18:48:55 -0500297 for repo in repos:
Alexcf91b182019-05-31 11:57:07 -0500298 if v not in _vs:
299 _vs[v] = []
300 _vs[v].append(repo)
301 if v not in _sections:
302 _sections[v] = []
303 _sections[v].append(s)
304 if v not in _apps:
305 _apps[v] = []
306 _apps[v].append(a)
307 # search for the newest version among filtered
Alexd0391d42019-05-21 18:48:55 -0500308 _r_desc = []
Alex3bc95f62020-03-05 17:00:04 -0600309 _vs_keys = iter(_vs.keys())
310 # get next version, if any
311 try:
312 _newest = DebianVersion(next(_vs_keys))
313 except StopIteration:
Alexd0391d42019-05-21 18:48:55 -0500314 _newest = DebianVersion('')
Alex3bc95f62020-03-05 17:00:04 -0600315 # iterate others, if any
Alexd0391d42019-05-21 18:48:55 -0500316 for v in _vs_keys:
317 _this = DebianVersion(v)
318 if _this > _newest:
319 _newest = _this
Alexd0391d42019-05-21 18:48:55 -0500320 _release = _newest
Alexcf91b182019-05-31 11:57:07 -0500321 # Get best description for the package
Alexd0391d42019-05-21 18:48:55 -0500322 if _release.version != 'n/a':
323 _r_desc = _vs[_release.version]
324 # preload special description
Alex41485522019-04-12 17:26:18 -0500325 if _desc[_name]:
Alex41485522019-04-12 17:26:18 -0500326 _pkg_desc = _desc[_name]
327 else:
Alex41485522019-04-12 17:26:18 -0500328 _pkg_desc = _desc.dummy_desc
Alexcf91b182019-05-31 11:57:07 -0500329 # Save repos list and desc for this version
Alexd0391d42019-05-21 18:48:55 -0500330 # Check if we can provide better from the package
331 if _release.version != 'n/a':
332 if not _pkg_desc['section']:
333 _pkg_desc['section'] = \
334 "/".join(_sections[_release.version])
335 if not _pkg_desc['app']:
336 _pkg_desc['app'] = \
337 "/".join(_apps[_release.version])
Alex3ebc5632019-04-18 16:47:18 -0500338
Alexcf91b182019-05-31 11:57:07 -0500339 # Populate package info, once for package
Alexd0391d42019-05-21 18:48:55 -0500340 _m = _r_desc[0]["maintainer"] if _r_desc else 'n/a'
Alex41485522019-04-12 17:26:18 -0500341 _all_packages[_name] = {
342 "desc": _pkg_desc,
Alexd0391d42019-05-21 18:48:55 -0500343 "repos": _r_desc,
344 "maintainer": _m,
345 "is_mirantis": self.rm.is_mirantis(
346 _name,
Alexcf91b182019-05-31 11:57:07 -0500347 tag=_mcp
Alexd0391d42019-05-21 18:48:55 -0500348 ),
Alex41485522019-04-12 17:26:18 -0500349 "results": {},
350 "r": _release,
Alex Savatieiev3db12a72019-03-22 16:32:31 -0500351 }
Alexcf91b182019-05-31 11:57:07 -0500352 # Cross-compare versions
Alex41485522019-04-12 17:26:18 -0500353 _cmp = VersionCmpResult(
354 _ver_ins,
355 _ver_can,
356 _all_packages[_name]['r']
357 )
Alexcf91b182019-05-31 11:57:07 -0500358 # Update results structure
Alex41485522019-04-12 17:26:18 -0500359 # shortcut to results
360 _res = _all_packages[_name]['results']
361 # update status
362 if _cmp.status not in _res:
363 _res[_cmp.status] = {}
364 # update action
365 if _cmp.action not in _res[_cmp.status]:
366 _res[_cmp.status][_cmp.action] = {}
367 # update node
368 if node_name not in _res[_cmp.status][_cmp.action]:
369 _res[_cmp.status][_cmp.action][node_name] = {}
370 # put result
371 _res[_cmp.status][_cmp.action][node_name] = {
372 'i': _ver_ins,
373 'c': _ver_can,
374 'res': _cmp,
375 'raw': _value['raw']
376 }
savex4448e132018-04-25 15:51:14 +0200377
Alex41485522019-04-12 17:26:18 -0500378 self._packages = _all_packages
Alexd9fd85e2019-05-16 16:58:24 -0500379 _progress.end()
savex4448e132018-04-25 15:51:14 +0200380
Alex41485522019-04-12 17:26:18 -0500381 def create_report(self, filename, rtype, full=None):
savex4448e132018-04-25 15:51:14 +0200382 """
383 Create static html showing packages diff per node
384
385 :return: buff with html
386 """
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600387 logger_cli.info("# Generating report to '{}'".format(filename))
Alex41485522019-04-12 17:26:18 -0500388 if rtype == 'html':
Alex9a4ad212020-10-01 18:04:25 -0500389 _type = reporter.HTMLPackageCandidates(self.master)
Alex41485522019-04-12 17:26:18 -0500390 elif rtype == 'csv':
Alex9a4ad212020-10-01 18:04:25 -0500391 _type = reporter.CSVAllPackages(self.master)
Alex41485522019-04-12 17:26:18 -0500392 else:
393 raise ConfigException("Report type not set")
Alex Savatieievd48994d2018-12-13 12:13:00 +0100394 _report = reporter.ReportToFile(
Alex41485522019-04-12 17:26:18 -0500395 _type,
savex4448e132018-04-25 15:51:14 +0200396 filename
397 )
Alex41485522019-04-12 17:26:18 -0500398 payload = {
Alex9a4ad212020-10-01 18:04:25 -0500399 "nodes": self.master.nodes,
400 "mcp_release": self.master.mcp_release,
401 "openstack_release": self.master.openstack_release
Alex41485522019-04-12 17:26:18 -0500402 }
403 payload.update(self.presort_packages(self._packages, full))
404 _report(payload)
Alex Savatieiev799bee32019-02-20 17:19:26 -0600405 logger_cli.info("-> Done")
Alex9a4ad212020-10-01 18:04:25 -0500406
407 def collect_installed_packages(self):
408 """
409 Collect installed packages on each node
410 sets 'installed' dict property in the class
411
412 :return: none
413 """
414 logger_cli.info("# Collecting installed packages")
415 if self.master.env_type == ENV_TYPE_SALT:
416 self.master.prepare_script_on_active_nodes("pkg_versions.py")
417 _result = self.master.execute_script_on_active_nodes(
418 "pkg_versions.py"
419 )
420
421 for key in self.master.nodes.keys():
422 # due to much data to be passed from salt, it is happening in order
423 if key in _result and _result[key]:
424 _text = _result[key]
425 try:
426 _dict = json.loads(_text[_text.find('{'):])
427 except ValueError:
428 logger_cli.info("... no JSON for '{}'".format(
429 key
430 ))
431 logger_cli.debug(
432 "ERROR:\n{}\n".format(_text[:_text.find('{')])
433 )
434 _dict = {}
435
436 self.master.nodes[key]['packages'] = _dict
437 else:
438 self.master.nodes[key]['packages'] = {}
439 logger_cli.debug("... {} has {} packages installed".format(
440 key,
441 len(self.master.nodes[key]['packages'].keys())
442 ))
443 logger_cli.info("-> Done")
444
445
446class SaltCloudPackageChecker(CloudPackageChecker):
447 def __init__(
448 self,
449 config,
450 force_tag=None,
451 exclude_keywords=[],
452 skip_list=None,
453 skip_list_file=None
454 ):
455 self.master = SaltNodes(config)
456 super(SaltCloudPackageChecker, self).__init__(
457 config,
Alexccb72e02021-01-20 16:38:03 -0600458 force_tag=force_tag,
Alex9a4ad212020-10-01 18:04:25 -0500459 exclude_keywords=[],
Alex359e5752021-08-16 17:28:30 -0500460 skip_list=skip_list,
461 skip_list_file=skip_list_file
Alex9a4ad212020-10-01 18:04:25 -0500462 )
463
464
465class KubeCloudPackageChecker(CloudPackageChecker):
466 def __init__(
467 self,
468 config,
469 force_tag=None,
470 exclude_keywords=[],
471 skip_list=None,
472 skip_list_file=None
473 ):
474 self.master = KubeNodes(config)
475 super(KubeCloudPackageChecker, self).__init__(
476 config,
Alexccb72e02021-01-20 16:38:03 -0600477 force_tag=force_tag,
Alex9a4ad212020-10-01 18:04:25 -0500478 exclude_keywords=[],
Alex359e5752021-08-16 17:28:30 -0500479 skip_list=skip_list,
480 skip_list_file=skip_list_file
Alex9a4ad212020-10-01 18:04:25 -0500481 )