blob: 0ca1e85c6506a1a401a3015ea8e41d783a7f46da [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
Alex3ebc5632019-04-18 16:47:18 -05006from cfg_checker.common import config, const
Alex7c9494e2019-04-22 10:40:59 -05007from cfg_checker.common import logger, logger_cli
Alexe0c5b9e2019-04-23 18:51:23 -05008from cfg_checker.common import utils
9from cfg_checker.common.exception import SaltException
Alex7c9494e2019-04-22 10:40:59 -050010from cfg_checker.common.settings import pkg_dir
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060011
12node_tmpl = {
13 'role': '',
14 'node_group': '',
15 'status': const.NODE_DOWN,
16 'pillars': {},
17 'grains': {}
18}
19
20
21class SaltNodes(object):
22 def __init__(self):
Alexe0c5b9e2019-04-23 18:51:23 -050023 logger_cli.info("# Gathering environment information")
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060024 # simple salt rest client
Alexe0c5b9e2019-04-23 18:51:23 -050025 self.salt = salt
26 self.nodes = None
Alex3ebc5632019-04-18 16:47:18 -050027
Alexe0c5b9e2019-04-23 18:51:23 -050028 def gather_node_info(self):
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060029 # Keys for all nodes
30 # this is not working in scope of 2016.8.3, will overide with list
Alexb151fbe2019-04-22 16:53:30 -050031 logger_cli.debug("... collecting node names existing in the cloud")
Alexe0c5b9e2019-04-23 18:51:23 -050032 if not self.salt:
33 self.salt = get_salt_remote(config)
34
Alex Savatieiev9df93a92019-02-27 17:40:16 -060035 try:
36 _keys = self.salt.list_keys()
37 _str = []
38 for _k, _v in _keys.iteritems():
39 _str.append("{}: {}".format(_k, len(_v)))
40 logger_cli.info("-> keys collected: {}".format(", ".join(_str)))
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060041
Alex Savatieiev9df93a92019-02-27 17:40:16 -060042 self.node_keys = {
43 'minions': _keys['minions']
44 }
Alex3ebc5632019-04-18 16:47:18 -050045 except Exception:
Alex Savatieiev9df93a92019-02-27 17:40:16 -060046 _keys = None
47 self.node_keys = None
Alex3ebc5632019-04-18 16:47:18 -050048
Alex Savatieiev9df93a92019-02-27 17:40:16 -060049 # List of minions with grains
50 _minions = self.salt.list_minions()
51 if _minions:
Alex3ebc5632019-04-18 16:47:18 -050052 logger_cli.info(
53 "-> api reported {} active minions".format(len(_minions))
54 )
Alex Savatieiev9df93a92019-02-27 17:40:16 -060055 elif not self.node_keys:
56 # this is the last resort
57 _minions = config.load_nodes_list()
Alex3ebc5632019-04-18 16:47:18 -050058 logger_cli.info(
59 "-> {} nodes loaded from list file".format(len(_minions))
60 )
Alex Savatieiev9df93a92019-02-27 17:40:16 -060061 else:
62 _minions = self.node_keys['minions']
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060063
Alex Savatieiev9df93a92019-02-27 17:40:16 -060064 # in case API not listed minions, we need all that answer ping
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060065 _active = self.salt.get_active_nodes()
Alex Savatieiev9df93a92019-02-27 17:40:16 -060066 logger_cli.info("-> nodes responded: {}".format(len(_active)))
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060067 # iterate through all accepted nodes and create a dict for it
68 self.nodes = {}
Alex Savatieievefa79c42019-03-14 19:14:04 -050069 self.skip_list = []
Alex Savatieiev9df93a92019-02-27 17:40:16 -060070 for _name in _minions:
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060071 _nc = utils.get_node_code(_name)
72 _rmap = const.all_roles_map
73 _role = _rmap[_nc] if _nc in _rmap else 'unknown'
74 _status = const.NODE_UP if _name in _active else const.NODE_DOWN
Alex Savatieievefa79c42019-03-14 19:14:04 -050075 if _status == const.NODE_DOWN:
76 self.skip_list.append(_name)
77 logger_cli.info("-> '{}' is down, marked to skip".format(
78 _name
79 ))
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060080 self.nodes[_name] = deepcopy(node_tmpl)
81 self.nodes[_name]['node_group'] = _nc
82 self.nodes[_name]['role'] = _role
83 self.nodes[_name]['status'] = _status
Alex Savatieievefa79c42019-03-14 19:14:04 -050084 logger_cli.info("-> {} nodes inactive".format(len(self.skip_list)))
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060085 logger_cli.info("-> {} nodes collected".format(len(self.nodes)))
86
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060087 # form an all nodes compound string to use in salt
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -060088 self.active_nodes_compound = self.salt.compound_string_from_list(
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060089 filter(
90 lambda nd: self.nodes[nd]['status'] == const.NODE_UP,
91 self.nodes
92 )
93 )
Alex41485522019-04-12 17:26:18 -050094 # get master node fqdn
Alexe0c5b9e2019-04-23 18:51:23 -050095 _filtered = filter(
Alex41485522019-04-12 17:26:18 -050096 lambda nd: self.nodes[nd]['role'] == const.all_roles_map['cfg'],
97 self.nodes
Alexe0c5b9e2019-04-23 18:51:23 -050098 )
99 if len(_filtered) < 1:
100 raise SaltException(
101 "No master node detected! Check/Update node role map."
102 )
103 else:
104 self.salt.master_node = _filtered[0]
Alex3ebc5632019-04-18 16:47:18 -0500105
Alex41485522019-04-12 17:26:18 -0500106 # OpenStack versions
107 self.mcp_release = self.salt.pillar_get(
Alexe0c5b9e2019-04-23 18:51:23 -0500108 self.salt.master_node,
Alex41485522019-04-12 17:26:18 -0500109 "_param:apt_mk_version"
Alexe0c5b9e2019-04-23 18:51:23 -0500110 )[self.salt.master_node]
Alex41485522019-04-12 17:26:18 -0500111 self.openstack_release = self.salt.pillar_get(
Alexe0c5b9e2019-04-23 18:51:23 -0500112 self.salt.master_node,
Alex41485522019-04-12 17:26:18 -0500113 "_param:openstack_version"
Alexe0c5b9e2019-04-23 18:51:23 -0500114 )[self.salt.master_node]
Alexd0391d42019-05-21 18:48:55 -0500115 # Preload codenames
116 # do additional queries to get linux codename and arch for each node
117 self.get_specific_pillar_for_nodes("_param:linux_system_codename")
118 self.get_specific_pillar_for_nodes("_param:linux_system_architecture")
119 for _name in self.nodes.keys():
Alexe9547d82019-06-03 15:22:50 -0500120 _n = self.nodes[_name]
121 if _name not in self.skip_list:
122 _p = _n['pillars']['_param']
123 _n['linux_codename'] = _p['linux_system_codename']
124 _n['linux_arch'] = _p['linux_system_architecture']
Alex41485522019-04-12 17:26:18 -0500125
Alex Savatieieva1f6f8c2019-03-18 17:13:55 -0500126 def skip_node(self, node):
127 # Add node to skip list
128 # Fro example if it is fails to comply with the rules
129
130 # check if we know such node
131 if node in self.nodes.keys() and node not in self.skip_list:
132 # yes, add it
133 self.skip_list.append(node)
134 return True
135 else:
136 return False
137
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600138 def get_nodes(self):
Alexe0c5b9e2019-04-23 18:51:23 -0500139 if not self.nodes:
140 self.gather_node_info()
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600141 return self.nodes
142
Alex836fac82019-08-22 13:36:16 -0500143 def get_info(self):
144 _info = {
145 'mcp_release': self.mcp_release,
146 'openstack_release': self.openstack_release
147 }
148 return _info
149
Alex1839bbf2019-08-22 17:17:21 -0500150 def get_cmd_for_nodes(self, cmd, target_key, target_dict=None, nodes=None):
Alex836fac82019-08-22 13:36:16 -0500151 """Function runs. cmd.run and parses result into place
152 or into dict structure provided
153
154 :return: no return value, data pulished internally
155 """
156 logger_cli.debug(
157 "... collecting results for '{}'".format(cmd)
158 )
159 if target_dict:
160 _nodes = target_dict
161 else:
162 _nodes = self.nodes
Alex1839bbf2019-08-22 17:17:21 -0500163 _result = self.execute_cmd_on_active_nodes(cmd, nodes=nodes)
Alex836fac82019-08-22 13:36:16 -0500164 for node, data in _nodes.iteritems():
Alex1839bbf2019-08-22 17:17:21 -0500165
Alex836fac82019-08-22 13:36:16 -0500166 if node in self.skip_list:
167 logger_cli.debug(
168 "... '{}' skipped while collecting '{}'".format(
169 node,
170 cmd
171 )
172 )
173 continue
174 # Prepare target key
175 if target_key not in data:
176 data[target_key] = None
177 # Save data
178 if data['status'] == const.NODE_DOWN:
179 data[target_key] = None
Alex1839bbf2019-08-22 17:17:21 -0500180 elif node not in _result:
181 continue
Alex836fac82019-08-22 13:36:16 -0500182 elif not _result[node]:
183 logger_cli.debug(
184 "... '{}' not responded after '{}'".format(
185 node,
186 config.salt_timeout
187 )
188 )
189 data[target_key] = None
190 else:
191 data[target_key] = _result[node]
192
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600193 def get_specific_pillar_for_nodes(self, pillar_path):
194 """Function gets pillars on given path for all nodes
195
196 :return: no return value, data pulished internally
197 """
Alex3ebc5632019-04-18 16:47:18 -0500198 logger_cli.debug(
Alexb151fbe2019-04-22 16:53:30 -0500199 "... collecting node pillars for '{}'".format(pillar_path)
Alex3ebc5632019-04-18 16:47:18 -0500200 )
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600201 _result = self.salt.pillar_get(self.active_nodes_compound, pillar_path)
Alex Savatieievefa79c42019-03-14 19:14:04 -0500202 self.not_responded = []
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600203 for node, data in self.nodes.iteritems():
Alex Savatieievefa79c42019-03-14 19:14:04 -0500204 if node in self.skip_list:
205 logger_cli.debug(
206 "... '{}' skipped while collecting '{}'".format(
207 node,
208 pillar_path
209 )
210 )
211 continue
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600212 _pillar_keys = pillar_path.split(':')
213 _data = data['pillars']
214 # pre-create nested dict
215 for idx in range(0, len(_pillar_keys)-1):
216 _key = _pillar_keys[idx]
217 if _key not in _data:
218 _data[_key] = {}
219 _data = _data[_key]
Alex Savatieievefa79c42019-03-14 19:14:04 -0500220 if data['status'] == const.NODE_DOWN:
221 _data[_pillar_keys[-1]] = None
222 elif not _result[node]:
223 logger_cli.debug(
224 "... '{}' not responded after '{}'".format(
225 node,
226 config.salt_timeout
227 )
228 )
229 _data[_pillar_keys[-1]] = None
230 self.not_responded.append(node)
231 else:
232 _data[_pillar_keys[-1]] = _result[node]
Alex3ebc5632019-04-18 16:47:18 -0500233
Alexe0c5b9e2019-04-23 18:51:23 -0500234 def prepare_json_on_node(self, node, _dict, filename):
235 # this function assumes that all folders are created
236 _dumps = json.dumps(_dict, indent=2).splitlines()
237 _storage_path = os.path.join(
238 config.salt_file_root, config.salt_scripts_folder
239 )
240 logger_cli.debug(
241 "... uploading data as '{}' "
242 "to master's file cache folder: '{}'".format(
243 filename,
244 _storage_path
245 )
246 )
247 _cache_path = os.path.join(_storage_path, filename)
248 _source_path = os.path.join(
249 'salt://',
250 config.salt_scripts_folder,
251 filename
252 )
253 _target_path = os.path.join(
254 '/root',
255 config.salt_scripts_folder,
256 filename
257 )
258
259 logger_cli.debug("... creating file in cache '{}'".format(_cache_path))
260 self.salt.f_touch_master(_cache_path)
261 self.salt.f_append_master(_cache_path, _dumps)
262 logger.debug("... syncing file to '{}'".format(node))
263 self.salt.get_file(
264 node,
265 _source_path,
266 _target_path,
267 tgt_type="compound"
268 )
269 return _target_path
270
271 def prepare_script_on_active_nodes(self, script_filename):
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600272 # Prepare script
273 _p = os.path.join(pkg_dir, 'scripts', script_filename)
274 with open(_p, 'rt') as fd:
275 _script = fd.read().splitlines()
276 _storage_path = os.path.join(
277 config.salt_file_root, config.salt_scripts_folder
278 )
279 logger_cli.debug(
Alexb151fbe2019-04-22 16:53:30 -0500280 "... uploading script {} "
Alex3ebc5632019-04-18 16:47:18 -0500281 "to master's file cache folder: '{}'".format(
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600282 script_filename,
283 _storage_path
284 )
285 )
Alexe0c5b9e2019-04-23 18:51:23 -0500286 self.salt.mkdir(self.salt.master_node, _storage_path)
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600287 # Form cache, source and target path
288 _cache_path = os.path.join(_storage_path, script_filename)
289 _source_path = os.path.join(
290 'salt://',
291 config.salt_scripts_folder,
292 script_filename
293 )
294 _target_path = os.path.join(
295 '/root',
296 config.salt_scripts_folder,
297 script_filename
298 )
299
Alexb151fbe2019-04-22 16:53:30 -0500300 logger_cli.debug("... creating file in cache '{}'".format(_cache_path))
Alex3ebc5632019-04-18 16:47:18 -0500301 self.salt.f_touch_master(_cache_path)
302 self.salt.f_append_master(_cache_path, _script)
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600303 # command salt to copy file to minions
Alex3ebc5632019-04-18 16:47:18 -0500304 logger_cli.debug(
Alexb151fbe2019-04-22 16:53:30 -0500305 "... creating script target folder '{}'".format(
Alex3ebc5632019-04-18 16:47:18 -0500306 _cache_path
307 )
308 )
309 self.salt.mkdir(
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600310 self.active_nodes_compound,
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600311 os.path.join(
312 '/root',
313 config.salt_scripts_folder
314 ),
315 tgt_type="compound"
316 )
Alex3ebc5632019-04-18 16:47:18 -0500317 logger.debug("... syncing file to nodes")
318 self.salt.get_file(
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600319 self.active_nodes_compound,
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600320 _source_path,
321 _target_path,
322 tgt_type="compound"
323 )
Alexe0c5b9e2019-04-23 18:51:23 -0500324 # return path on nodes, just in case
325 return _target_path
326
327 def execute_script_on_node(self, node, script_filename, args=[]):
328 # Prepare path
329 _target_path = os.path.join(
330 '/root',
331 config.salt_scripts_folder,
332 script_filename
333 )
334
335 # execute script
336 logger.debug("... running script on '{}'".format(node))
337 # handle results for each node
338 _script_arguments = " ".join(args) if args else ""
339 self.not_responded = []
340 _r = self.salt.cmd(
341 node,
342 'cmd.run',
343 param='python {} {}'.format(_target_path, _script_arguments),
344 expr_form="compound"
345 )
346
347 # all false returns means that there is no response
348 self.not_responded = [_n for _n in _r.keys() if not _r[_n]]
349 return _r
350
351 def execute_script_on_active_nodes(self, script_filename, args=[]):
352 # Prepare path
353 _target_path = os.path.join(
354 '/root',
355 config.salt_scripts_folder,
356 script_filename
357 )
358
359 # execute script
Alexd0391d42019-05-21 18:48:55 -0500360 logger_cli.debug("... running script")
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600361 # handle results for each node
362 _script_arguments = " ".join(args) if args else ""
Alex Savatieievefa79c42019-03-14 19:14:04 -0500363 self.not_responded = []
364 _r = self.salt.cmd(
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600365 self.active_nodes_compound,
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600366 'cmd.run',
367 param='python {} {}'.format(_target_path, _script_arguments),
368 expr_form="compound"
369 )
370
Alex Savatieievefa79c42019-03-14 19:14:04 -0500371 # all false returns means that there is no response
Alex3ebc5632019-04-18 16:47:18 -0500372 self.not_responded = [_n for _n in _r.keys() if not _r[_n]]
Alex Savatieievefa79c42019-03-14 19:14:04 -0500373 return _r
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600374
Alex1839bbf2019-08-22 17:17:21 -0500375 def execute_cmd_on_active_nodes(self, cmd, nodes=None):
Alex836fac82019-08-22 13:36:16 -0500376 # execute cmd
377 self.not_responded = []
378 _r = self.salt.cmd(
Alex1839bbf2019-08-22 17:17:21 -0500379 nodes if nodes else self.active_nodes_compound,
Alex836fac82019-08-22 13:36:16 -0500380 'cmd.run',
381 param=cmd,
382 expr_form="compound"
383 )
384
385 # all false returns means that there is no response
386 self.not_responded = [_n for _n in _r.keys() if not _r[_n]]
387 return _r
388
Alex Savatieievefa79c42019-03-14 19:14:04 -0500389 def is_node_available(self, node, log=True):
390 if node in self.skip_list:
391 if log:
392 logger_cli.info("-> node '{}' not active".format(node))
393 return False
394 elif node in self.not_responded:
395 if log:
396 logger_cli.info("-> node '{}' not responded".format(node))
397 return False
398 else:
399 return True
Alexe0c5b9e2019-04-23 18:51:23 -0500400
401
402salt_master = SaltNodes()