blob: 13ab3e78c4131312e4458b10023796c6c34ae150 [file] [log] [blame]
Alexe0c5b9e2019-04-23 18:51:23 -05001import json
Alex Savatieiev9b2f6512019-02-20 18:05:00 -06002import os
Alex3ebc5632019-04-18 16:47:18 -05003from copy import deepcopy
Alex Savatieiev9b2f6512019-02-20 18:05:00 -06004
Alexe0c5b9e2019-04-23 18:51:23 -05005from cfg_checker.clients import get_salt_remote, salt
Alexe9908f72020-05-19 16:04:53 -05006from cfg_checker.common import config
7from cfg_checker.common.const import all_roles_map
8from cfg_checker.common.const import NODE_UP, NODE_DOWN, NODE_SKIP
Alex7c9494e2019-04-22 10:40:59 -05009from cfg_checker.common import logger, logger_cli
Alexe0c5b9e2019-04-23 18:51:23 -050010from cfg_checker.common import utils
11from cfg_checker.common.exception import SaltException
Alex7c9494e2019-04-22 10:40:59 -050012from cfg_checker.common.settings import pkg_dir
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060013
14node_tmpl = {
15 'role': '',
16 'node_group': '',
Alexe9908f72020-05-19 16:04:53 -050017 'status': NODE_DOWN,
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060018 'pillars': {},
19 'grains': {}
20}
21
22
23class SaltNodes(object):
24 def __init__(self):
Alexe0c5b9e2019-04-23 18:51:23 -050025 logger_cli.info("# Gathering environment information")
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060026 # simple salt rest client
Alexe0c5b9e2019-04-23 18:51:23 -050027 self.salt = salt
28 self.nodes = None
Alex3ebc5632019-04-18 16:47:18 -050029
Alexe9908f72020-05-19 16:04:53 -050030 def gather_node_info(self, skip_list, skip_list_file):
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060031 # Keys for all nodes
32 # this is not working in scope of 2016.8.3, will overide with list
Alexb151fbe2019-04-22 16:53:30 -050033 logger_cli.debug("... collecting node names existing in the cloud")
Alexe0c5b9e2019-04-23 18:51:23 -050034 if not self.salt:
35 self.salt = get_salt_remote(config)
36
Alex Savatieiev9df93a92019-02-27 17:40:16 -060037 try:
38 _keys = self.salt.list_keys()
39 _str = []
Alex3bc95f62020-03-05 17:00:04 -060040 for _k, _v in _keys.items():
Alex Savatieiev9df93a92019-02-27 17:40:16 -060041 _str.append("{}: {}".format(_k, len(_v)))
42 logger_cli.info("-> keys collected: {}".format(", ".join(_str)))
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060043
Alex Savatieiev9df93a92019-02-27 17:40:16 -060044 self.node_keys = {
45 'minions': _keys['minions']
46 }
Alex3ebc5632019-04-18 16:47:18 -050047 except Exception:
Alex Savatieiev9df93a92019-02-27 17:40:16 -060048 _keys = None
49 self.node_keys = None
Alex3ebc5632019-04-18 16:47:18 -050050
Alex Savatieiev9df93a92019-02-27 17:40:16 -060051 # List of minions with grains
52 _minions = self.salt.list_minions()
53 if _minions:
Alex3ebc5632019-04-18 16:47:18 -050054 logger_cli.info(
55 "-> api reported {} active minions".format(len(_minions))
56 )
Alex Savatieiev9df93a92019-02-27 17:40:16 -060057 elif not self.node_keys:
58 # this is the last resort
59 _minions = config.load_nodes_list()
Alex3ebc5632019-04-18 16:47:18 -050060 logger_cli.info(
61 "-> {} nodes loaded from list file".format(len(_minions))
62 )
Alex Savatieiev9df93a92019-02-27 17:40:16 -060063 else:
64 _minions = self.node_keys['minions']
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060065
Alexe9908f72020-05-19 16:04:53 -050066 # Skip nodes if needed
67 _skipped_minions = []
68 # skip list file
69 if skip_list_file:
70 _valid, _invalid = utils.get_nodes_list(skip_list_file)
71 logger_cli.info(
72 "\n# WARNING: Detected invalid entries "
73 "in nodes skip list:\n".format(
74 "\n".join(_invalid)
75 )
76 )
77 _skipped_minions.extend(_valid)
78 # process wildcard, create node list out of mask
79 if skip_list:
80 _list = []
81 _invalid = []
82 for _item in skip_list:
83 if '*' in _item:
84 _str = _item[:_item.index('*')]
85 _nodes = [_m for _m in _minions if _m.startswith(_str)]
86 if not _nodes:
87 logger_cli.warn(
88 "# WARNING: No nodes found for {}".format(_item)
89 )
90 _list.extend(_nodes)
91 else:
92 if _item in _minions:
93 _list += _item
94 else:
95 logger_cli.warn(
96 "# WARNING: No node found for {}".format(_item)
97 )
98 # removing duplicates
99 _list = list(set(_list))
100 _skipped_minions.extend(_list)
101
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600102 # in case API not listed minions, we need all that answer ping
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600103 _active = self.salt.get_active_nodes()
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600104 logger_cli.info("-> nodes responded: {}".format(len(_active)))
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600105 # iterate through all accepted nodes and create a dict for it
106 self.nodes = {}
Alex Savatieievefa79c42019-03-14 19:14:04 -0500107 self.skip_list = []
Alexe9908f72020-05-19 16:04:53 -0500108 _domains = set()
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600109 for _name in _minions:
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600110 _nc = utils.get_node_code(_name)
Alexe9908f72020-05-19 16:04:53 -0500111 _rmap = all_roles_map
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600112 _role = _rmap[_nc] if _nc in _rmap else 'unknown'
Alexe9908f72020-05-19 16:04:53 -0500113 if _name in _skipped_minions:
114 _status = NODE_SKIP
Alex Savatieievefa79c42019-03-14 19:14:04 -0500115 self.skip_list.append(_name)
Alexe9908f72020-05-19 16:04:53 -0500116 else:
117 _status = NODE_UP if _name in _active else NODE_DOWN
118 if _status == NODE_DOWN:
119 self.skip_list.append(_name)
120 logger_cli.info(
121 "-> '{}' is down, "
122 "added to skip list".format(
123 _name
124 )
125 )
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600126 self.nodes[_name] = deepcopy(node_tmpl)
Alexe9908f72020-05-19 16:04:53 -0500127 self.nodes[_name]['shortname'] = _name.split(".", 1)[0]
128 _domains.add(_name.split(".", 1)[1])
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600129 self.nodes[_name]['node_group'] = _nc
130 self.nodes[_name]['role'] = _role
131 self.nodes[_name]['status'] = _status
Alexe9908f72020-05-19 16:04:53 -0500132 _domains = list(_domains)
133 if len(_domains) > 1:
134 logger_cli.warning(
135 "Multiple domains detected: {}".format(",".join(_domains))
136 )
137 else:
138 self.domain = _domains[0]
Alex Savatieievefa79c42019-03-14 19:14:04 -0500139 logger_cli.info("-> {} nodes inactive".format(len(self.skip_list)))
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600140 logger_cli.info("-> {} nodes collected".format(len(self.nodes)))
141
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600142 # form an all nodes compound string to use in salt
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600143 self.active_nodes_compound = self.salt.compound_string_from_list(
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600144 filter(
Alexe9908f72020-05-19 16:04:53 -0500145 lambda nd: self.nodes[nd]['status'] == NODE_UP,
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600146 self.nodes
147 )
148 )
Alex41485522019-04-12 17:26:18 -0500149 # get master node fqdn
Alex3bc95f62020-03-05 17:00:04 -0600150 # _filtered = filter(
151 # lambda nd: self.nodes[nd]['role'] == const.all_roles_map['cfg'],
152 # self.nodes
153 # )
Alexe9908f72020-05-19 16:04:53 -0500154 _role = all_roles_map['cfg']
Alex3bc95f62020-03-05 17:00:04 -0600155 _filtered = [n for n, v in self.nodes.items() if v['role'] == _role]
Alexe0c5b9e2019-04-23 18:51:23 -0500156 if len(_filtered) < 1:
157 raise SaltException(
158 "No master node detected! Check/Update node role map."
159 )
160 else:
161 self.salt.master_node = _filtered[0]
Alex3ebc5632019-04-18 16:47:18 -0500162
Alex41485522019-04-12 17:26:18 -0500163 # OpenStack versions
164 self.mcp_release = self.salt.pillar_get(
Alexe0c5b9e2019-04-23 18:51:23 -0500165 self.salt.master_node,
Alex41485522019-04-12 17:26:18 -0500166 "_param:apt_mk_version"
Alexe0c5b9e2019-04-23 18:51:23 -0500167 )[self.salt.master_node]
Alex41485522019-04-12 17:26:18 -0500168 self.openstack_release = self.salt.pillar_get(
Alexe0c5b9e2019-04-23 18:51:23 -0500169 self.salt.master_node,
Alex41485522019-04-12 17:26:18 -0500170 "_param:openstack_version"
Alexe0c5b9e2019-04-23 18:51:23 -0500171 )[self.salt.master_node]
Alexd0391d42019-05-21 18:48:55 -0500172 # Preload codenames
173 # do additional queries to get linux codename and arch for each node
174 self.get_specific_pillar_for_nodes("_param:linux_system_codename")
175 self.get_specific_pillar_for_nodes("_param:linux_system_architecture")
176 for _name in self.nodes.keys():
Alexe9547d82019-06-03 15:22:50 -0500177 _n = self.nodes[_name]
178 if _name not in self.skip_list:
179 _p = _n['pillars']['_param']
180 _n['linux_codename'] = _p['linux_system_codename']
181 _n['linux_arch'] = _p['linux_system_architecture']
Alex41485522019-04-12 17:26:18 -0500182
Alex Savatieieva1f6f8c2019-03-18 17:13:55 -0500183 def skip_node(self, node):
184 # Add node to skip list
185 # Fro example if it is fails to comply with the rules
186
187 # check if we know such node
188 if node in self.nodes.keys() and node not in self.skip_list:
189 # yes, add it
190 self.skip_list.append(node)
191 return True
192 else:
193 return False
194
Alexe9908f72020-05-19 16:04:53 -0500195 def get_nodes(self, skip_list=None, skip_list_file=None):
Alexe0c5b9e2019-04-23 18:51:23 -0500196 if not self.nodes:
Alexe9908f72020-05-19 16:04:53 -0500197 if not skip_list and config.skip_nodes:
198 self.gather_node_info(config.skip_nodes, skip_list_file)
199 else:
200 self.gather_node_info(skip_list, skip_list_file)
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600201 return self.nodes
202
Alex836fac82019-08-22 13:36:16 -0500203 def get_info(self):
204 _info = {
205 'mcp_release': self.mcp_release,
206 'openstack_release': self.openstack_release
207 }
208 return _info
209
Alex1839bbf2019-08-22 17:17:21 -0500210 def get_cmd_for_nodes(self, cmd, target_key, target_dict=None, nodes=None):
Alex836fac82019-08-22 13:36:16 -0500211 """Function runs. cmd.run and parses result into place
212 or into dict structure provided
213
214 :return: no return value, data pulished internally
215 """
216 logger_cli.debug(
217 "... collecting results for '{}'".format(cmd)
218 )
219 if target_dict:
220 _nodes = target_dict
221 else:
222 _nodes = self.nodes
Alex1839bbf2019-08-22 17:17:21 -0500223 _result = self.execute_cmd_on_active_nodes(cmd, nodes=nodes)
Alex3bc95f62020-03-05 17:00:04 -0600224 for node, data in _nodes.items():
Alexf3dbe862019-10-07 15:17:04 -0500225
Alex836fac82019-08-22 13:36:16 -0500226 if node in self.skip_list:
227 logger_cli.debug(
228 "... '{}' skipped while collecting '{}'".format(
229 node,
230 cmd
231 )
232 )
233 continue
234 # Prepare target key
235 if target_key not in data:
236 data[target_key] = None
237 # Save data
Alexe9908f72020-05-19 16:04:53 -0500238 if data['status'] in [NODE_DOWN, NODE_SKIP]:
Alex836fac82019-08-22 13:36:16 -0500239 data[target_key] = None
Alex1839bbf2019-08-22 17:17:21 -0500240 elif node not in _result:
241 continue
Alex836fac82019-08-22 13:36:16 -0500242 elif not _result[node]:
243 logger_cli.debug(
244 "... '{}' not responded after '{}'".format(
245 node,
246 config.salt_timeout
247 )
248 )
249 data[target_key] = None
250 else:
251 data[target_key] = _result[node]
252
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600253 def get_specific_pillar_for_nodes(self, pillar_path):
254 """Function gets pillars on given path for all nodes
255
256 :return: no return value, data pulished internally
257 """
Alex3ebc5632019-04-18 16:47:18 -0500258 logger_cli.debug(
Alexb151fbe2019-04-22 16:53:30 -0500259 "... collecting node pillars for '{}'".format(pillar_path)
Alex3ebc5632019-04-18 16:47:18 -0500260 )
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600261 _result = self.salt.pillar_get(self.active_nodes_compound, pillar_path)
Alex Savatieievefa79c42019-03-14 19:14:04 -0500262 self.not_responded = []
Alex3bc95f62020-03-05 17:00:04 -0600263 for node, data in self.nodes.items():
Alex Savatieievefa79c42019-03-14 19:14:04 -0500264 if node in self.skip_list:
265 logger_cli.debug(
266 "... '{}' skipped while collecting '{}'".format(
267 node,
268 pillar_path
269 )
270 )
271 continue
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600272 _pillar_keys = pillar_path.split(':')
273 _data = data['pillars']
274 # pre-create nested dict
275 for idx in range(0, len(_pillar_keys)-1):
276 _key = _pillar_keys[idx]
277 if _key not in _data:
278 _data[_key] = {}
279 _data = _data[_key]
Alexe9908f72020-05-19 16:04:53 -0500280 if data['status'] in [NODE_DOWN, NODE_SKIP]:
Alex Savatieievefa79c42019-03-14 19:14:04 -0500281 _data[_pillar_keys[-1]] = None
282 elif not _result[node]:
283 logger_cli.debug(
284 "... '{}' not responded after '{}'".format(
285 node,
286 config.salt_timeout
287 )
288 )
289 _data[_pillar_keys[-1]] = None
290 self.not_responded.append(node)
291 else:
292 _data[_pillar_keys[-1]] = _result[node]
Alex3ebc5632019-04-18 16:47:18 -0500293
Alexe0c5b9e2019-04-23 18:51:23 -0500294 def prepare_json_on_node(self, node, _dict, filename):
295 # this function assumes that all folders are created
296 _dumps = json.dumps(_dict, indent=2).splitlines()
297 _storage_path = os.path.join(
298 config.salt_file_root, config.salt_scripts_folder
299 )
300 logger_cli.debug(
301 "... uploading data as '{}' "
302 "to master's file cache folder: '{}'".format(
303 filename,
304 _storage_path
305 )
306 )
307 _cache_path = os.path.join(_storage_path, filename)
308 _source_path = os.path.join(
309 'salt://',
310 config.salt_scripts_folder,
311 filename
312 )
313 _target_path = os.path.join(
314 '/root',
315 config.salt_scripts_folder,
316 filename
317 )
318
319 logger_cli.debug("... creating file in cache '{}'".format(_cache_path))
320 self.salt.f_touch_master(_cache_path)
321 self.salt.f_append_master(_cache_path, _dumps)
322 logger.debug("... syncing file to '{}'".format(node))
323 self.salt.get_file(
324 node,
325 _source_path,
326 _target_path,
327 tgt_type="compound"
328 )
329 return _target_path
330
331 def prepare_script_on_active_nodes(self, script_filename):
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600332 # Prepare script
333 _p = os.path.join(pkg_dir, 'scripts', script_filename)
334 with open(_p, 'rt') as fd:
335 _script = fd.read().splitlines()
336 _storage_path = os.path.join(
337 config.salt_file_root, config.salt_scripts_folder
338 )
339 logger_cli.debug(
Alexb151fbe2019-04-22 16:53:30 -0500340 "... uploading script {} "
Alex3ebc5632019-04-18 16:47:18 -0500341 "to master's file cache folder: '{}'".format(
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600342 script_filename,
343 _storage_path
344 )
345 )
Alexe0c5b9e2019-04-23 18:51:23 -0500346 self.salt.mkdir(self.salt.master_node, _storage_path)
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600347 # Form cache, source and target path
348 _cache_path = os.path.join(_storage_path, script_filename)
349 _source_path = os.path.join(
350 'salt://',
351 config.salt_scripts_folder,
352 script_filename
353 )
354 _target_path = os.path.join(
355 '/root',
356 config.salt_scripts_folder,
357 script_filename
358 )
359
Alexb151fbe2019-04-22 16:53:30 -0500360 logger_cli.debug("... creating file in cache '{}'".format(_cache_path))
Alex3ebc5632019-04-18 16:47:18 -0500361 self.salt.f_touch_master(_cache_path)
362 self.salt.f_append_master(_cache_path, _script)
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600363 # command salt to copy file to minions
Alex3ebc5632019-04-18 16:47:18 -0500364 logger_cli.debug(
Alexb151fbe2019-04-22 16:53:30 -0500365 "... creating script target folder '{}'".format(
Alex3ebc5632019-04-18 16:47:18 -0500366 _cache_path
367 )
368 )
369 self.salt.mkdir(
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600370 self.active_nodes_compound,
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600371 os.path.join(
372 '/root',
373 config.salt_scripts_folder
374 ),
375 tgt_type="compound"
376 )
Alex3ebc5632019-04-18 16:47:18 -0500377 logger.debug("... syncing file to nodes")
378 self.salt.get_file(
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600379 self.active_nodes_compound,
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600380 _source_path,
381 _target_path,
382 tgt_type="compound"
383 )
Alexe0c5b9e2019-04-23 18:51:23 -0500384 # return path on nodes, just in case
385 return _target_path
386
387 def execute_script_on_node(self, node, script_filename, args=[]):
388 # Prepare path
389 _target_path = os.path.join(
390 '/root',
391 config.salt_scripts_folder,
392 script_filename
393 )
394
395 # execute script
396 logger.debug("... running script on '{}'".format(node))
397 # handle results for each node
398 _script_arguments = " ".join(args) if args else ""
399 self.not_responded = []
400 _r = self.salt.cmd(
401 node,
402 'cmd.run',
403 param='python {} {}'.format(_target_path, _script_arguments),
404 expr_form="compound"
405 )
406
407 # all false returns means that there is no response
408 self.not_responded = [_n for _n in _r.keys() if not _r[_n]]
409 return _r
410
411 def execute_script_on_active_nodes(self, script_filename, args=[]):
412 # Prepare path
413 _target_path = os.path.join(
414 '/root',
415 config.salt_scripts_folder,
416 script_filename
417 )
418
419 # execute script
Alexd0391d42019-05-21 18:48:55 -0500420 logger_cli.debug("... running script")
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600421 # handle results for each node
422 _script_arguments = " ".join(args) if args else ""
Alex Savatieievefa79c42019-03-14 19:14:04 -0500423 self.not_responded = []
424 _r = self.salt.cmd(
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600425 self.active_nodes_compound,
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600426 'cmd.run',
427 param='python {} {}'.format(_target_path, _script_arguments),
428 expr_form="compound"
429 )
430
Alex Savatieievefa79c42019-03-14 19:14:04 -0500431 # all false returns means that there is no response
Alex3ebc5632019-04-18 16:47:18 -0500432 self.not_responded = [_n for _n in _r.keys() if not _r[_n]]
Alex Savatieievefa79c42019-03-14 19:14:04 -0500433 return _r
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600434
Alex1839bbf2019-08-22 17:17:21 -0500435 def execute_cmd_on_active_nodes(self, cmd, nodes=None):
Alex836fac82019-08-22 13:36:16 -0500436 # execute cmd
437 self.not_responded = []
438 _r = self.salt.cmd(
Alex1839bbf2019-08-22 17:17:21 -0500439 nodes if nodes else self.active_nodes_compound,
Alex836fac82019-08-22 13:36:16 -0500440 'cmd.run',
441 param=cmd,
442 expr_form="compound"
443 )
444
445 # all false returns means that there is no response
446 self.not_responded = [_n for _n in _r.keys() if not _r[_n]]
447 return _r
448
Alex Savatieievefa79c42019-03-14 19:14:04 -0500449 def is_node_available(self, node, log=True):
450 if node in self.skip_list:
451 if log:
452 logger_cli.info("-> node '{}' not active".format(node))
453 return False
454 elif node in self.not_responded:
455 if log:
456 logger_cli.info("-> node '{}' not responded".format(node))
457 return False
458 else:
459 return True
Alexe0c5b9e2019-04-23 18:51:23 -0500460
461
462salt_master = SaltNodes()