blob: 1780b6013bf9fa44afac35aef1dbb1a728b97c02 [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
Alex9a4ad212020-10-01 18:04:25 -05004from multiprocessing.dummy import Pool
Alex Savatieiev9b2f6512019-02-20 18:05:00 -06005
Alex9a4ad212020-10-01 18:04:25 -05006from cfg_checker.clients import get_salt_remote, get_kube_remote
7from cfg_checker.common.const import all_salt_roles_map, all_kube_roles_map
Alexe9908f72020-05-19 16:04:53 -05008from cfg_checker.common.const import NODE_UP, NODE_DOWN, NODE_SKIP
Alex9a4ad212020-10-01 18:04:25 -05009from cfg_checker.common.const import ubuntu_versions, nova_openstack_versions
Alex7c9494e2019-04-22 10:40:59 -050010from cfg_checker.common import logger, logger_cli
Alexe0c5b9e2019-04-23 18:51:23 -050011from cfg_checker.common import utils
Alex9a4ad212020-10-01 18:04:25 -050012from cfg_checker.common.file_utils import create_temp_file_with_content
13from cfg_checker.common.exception import SaltException, KubeException
14from cfg_checker.common.ssh_utils import PortForward, SshShell
15from cfg_checker.common.settings import pkg_dir, ENV_TYPE_KUBE, ENV_TYPE_SALT
16from cfg_checker.helpers.console_utils import Progress
17
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060018
19node_tmpl = {
20 'role': '',
21 'node_group': '',
Alexe9908f72020-05-19 16:04:53 -050022 'status': NODE_DOWN,
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060023 'pillars': {},
Alex9a4ad212020-10-01 18:04:25 -050024 'grains': {},
25 'raw': {}
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060026}
27
28
Alex9a4ad212020-10-01 18:04:25 -050029def _prepare_skipped_nodes(_names, skip_list, skip_list_file):
30 _skipped_minions = []
31 # skip list file
32 if skip_list_file:
33 _valid, _invalid = utils.get_nodes_list(skip_list_file)
34 logger_cli.info(
35 "\n# WARNING: Detected invalid entries "
36 "in nodes skip list: {}\n".format(
37 "\n".join(_invalid)
38 )
39 )
40 _skipped_minions.extend(_valid)
41 # process wildcard, create node list out of mask
42 if skip_list:
43 _list = []
44 _invalid = []
45 for _item in skip_list:
46 if '*' in _item:
47 _str = _item[:_item.index('*')]
48 _nodes = [_m for _m in _names if _m.startswith(_str)]
49 if not _nodes:
50 logger_cli.warn(
51 "# WARNING: No nodes found for {}".format(_item)
52 )
53 _list.extend(_nodes)
54 else:
55 if _item in _names:
56 _list += _item
57 else:
58 logger_cli.warn(
59 "# WARNING: No node found for {}".format(_item)
60 )
61 # removing duplicates
62 _list = list(set(_list))
63 _skipped_minions.extend(_list)
64
65 return _skipped_minions
66
67
68class Nodes(object):
69 def __init__(self, config):
70 self.nodes = None
71 self.env_config = config
72
73 def skip_node(self, node):
74 # Add node to skip list
75 # Fro example if it is fails to comply with the rules
76
77 # check if we know such node
78 if node in self.nodes.keys() and node not in self.skip_list:
79 # yes, add it
80 self.skip_list.append(node)
81 return True
82 else:
83 return False
84
85 def get_nodes(self, skip_list=None, skip_list_file=None):
86 if not self.nodes:
87 if not skip_list and self.env_config.skip_nodes:
88 self.gather_node_info(
89 self.env_config.skip_nodes,
90 skip_list_file
91 )
92 else:
93 self.gather_node_info(skip_list, skip_list_file)
94 return self.nodes
95
96 def get_info(self):
97 _info = {
98 'mcp_release': self.mcp_release,
99 'openstack_release': self.openstack_release
100 }
101 return _info
102
103 def is_node_available(self, node, log=True):
104 if node in self.skip_list:
105 if log:
106 logger_cli.info("-> node '{}' not active".format(node))
107 return False
108 elif node in self.not_responded:
109 if log:
110 logger_cli.info("-> node '{}' not responded".format(node))
111 return False
112 else:
113 return True
114
115
116class SaltNodes(Nodes):
117 def __init__(self, config):
118 super(SaltNodes, self).__init__(config)
Alexe0c5b9e2019-04-23 18:51:23 -0500119 logger_cli.info("# Gathering environment information")
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600120 # simple salt rest client
Alex9a4ad212020-10-01 18:04:25 -0500121 self.salt = None
122 self.env_type = ENV_TYPE_SALT
Alex3ebc5632019-04-18 16:47:18 -0500123
Alexe9908f72020-05-19 16:04:53 -0500124 def gather_node_info(self, skip_list, skip_list_file):
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600125 # Keys for all nodes
126 # this is not working in scope of 2016.8.3, will overide with list
Alexb151fbe2019-04-22 16:53:30 -0500127 logger_cli.debug("... collecting node names existing in the cloud")
Alexe0c5b9e2019-04-23 18:51:23 -0500128 if not self.salt:
Alex9a4ad212020-10-01 18:04:25 -0500129 self.salt = get_salt_remote(self.env_config)
Alexe0c5b9e2019-04-23 18:51:23 -0500130
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600131 try:
132 _keys = self.salt.list_keys()
133 _str = []
Alex3bc95f62020-03-05 17:00:04 -0600134 for _k, _v in _keys.items():
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600135 _str.append("{}: {}".format(_k, len(_v)))
136 logger_cli.info("-> keys collected: {}".format(", ".join(_str)))
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600137
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600138 self.node_keys = {
139 'minions': _keys['minions']
140 }
Alex3ebc5632019-04-18 16:47:18 -0500141 except Exception:
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600142 _keys = None
143 self.node_keys = None
Alex3ebc5632019-04-18 16:47:18 -0500144
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600145 # List of minions with grains
146 _minions = self.salt.list_minions()
147 if _minions:
Alex3ebc5632019-04-18 16:47:18 -0500148 logger_cli.info(
149 "-> api reported {} active minions".format(len(_minions))
150 )
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600151 elif not self.node_keys:
152 # this is the last resort
Alex9a4ad212020-10-01 18:04:25 -0500153 _minions = self.env_config.load_nodes_list()
Alex3ebc5632019-04-18 16:47:18 -0500154 logger_cli.info(
155 "-> {} nodes loaded from list file".format(len(_minions))
156 )
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600157 else:
158 _minions = self.node_keys['minions']
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600159
Alexe9908f72020-05-19 16:04:53 -0500160 # Skip nodes if needed
Alex9a4ad212020-10-01 18:04:25 -0500161 _skipped_minions = \
162 _prepare_skipped_nodes(_minions, skip_list, skip_list_file)
Alexe9908f72020-05-19 16:04:53 -0500163
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600164 # in case API not listed minions, we need all that answer ping
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600165 _active = self.salt.get_active_nodes()
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600166 logger_cli.info("-> nodes responded: {}".format(len(_active)))
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600167 # iterate through all accepted nodes and create a dict for it
168 self.nodes = {}
Alex Savatieievefa79c42019-03-14 19:14:04 -0500169 self.skip_list = []
Alexe9908f72020-05-19 16:04:53 -0500170 _domains = set()
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600171 for _name in _minions:
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600172 _nc = utils.get_node_code(_name)
Alex9a4ad212020-10-01 18:04:25 -0500173 _rmap = all_salt_roles_map
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600174 _role = _rmap[_nc] if _nc in _rmap else 'unknown'
Alexe9908f72020-05-19 16:04:53 -0500175 if _name in _skipped_minions:
176 _status = NODE_SKIP
Alex Savatieievefa79c42019-03-14 19:14:04 -0500177 self.skip_list.append(_name)
Alexe9908f72020-05-19 16:04:53 -0500178 else:
179 _status = NODE_UP if _name in _active else NODE_DOWN
180 if _status == NODE_DOWN:
181 self.skip_list.append(_name)
182 logger_cli.info(
183 "-> '{}' is down, "
184 "added to skip list".format(
185 _name
186 )
187 )
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600188 self.nodes[_name] = deepcopy(node_tmpl)
Alexe9908f72020-05-19 16:04:53 -0500189 self.nodes[_name]['shortname'] = _name.split(".", 1)[0]
190 _domains.add(_name.split(".", 1)[1])
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600191 self.nodes[_name]['node_group'] = _nc
192 self.nodes[_name]['role'] = _role
193 self.nodes[_name]['status'] = _status
Alexe9908f72020-05-19 16:04:53 -0500194 _domains = list(_domains)
195 if len(_domains) > 1:
196 logger_cli.warning(
197 "Multiple domains detected: {}".format(",".join(_domains))
198 )
Alex205546c2020-12-30 19:22:30 -0600199 # TODO: Use domain with biggest node count by default
200 # or force it via config option
Alexe9908f72020-05-19 16:04:53 -0500201 else:
202 self.domain = _domains[0]
Alex Savatieievefa79c42019-03-14 19:14:04 -0500203 logger_cli.info("-> {} nodes inactive".format(len(self.skip_list)))
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600204 logger_cli.info("-> {} nodes collected".format(len(self.nodes)))
205
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600206 # form an all nodes compound string to use in salt
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600207 self.active_nodes_compound = self.salt.compound_string_from_list(
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600208 filter(
Alexe9908f72020-05-19 16:04:53 -0500209 lambda nd: self.nodes[nd]['status'] == NODE_UP,
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600210 self.nodes
211 )
212 )
Alex41485522019-04-12 17:26:18 -0500213 # get master node fqdn
Alex3bc95f62020-03-05 17:00:04 -0600214 # _filtered = filter(
215 # lambda nd: self.nodes[nd]['role'] == const.all_roles_map['cfg'],
216 # self.nodes
217 # )
Alex9a4ad212020-10-01 18:04:25 -0500218 _role = all_salt_roles_map['cfg']
Alex3bc95f62020-03-05 17:00:04 -0600219 _filtered = [n for n, v in self.nodes.items() if v['role'] == _role]
Alexe0c5b9e2019-04-23 18:51:23 -0500220 if len(_filtered) < 1:
221 raise SaltException(
222 "No master node detected! Check/Update node role map."
223 )
224 else:
225 self.salt.master_node = _filtered[0]
Alex3ebc5632019-04-18 16:47:18 -0500226
Alex41485522019-04-12 17:26:18 -0500227 # OpenStack versions
228 self.mcp_release = self.salt.pillar_get(
Alexe0c5b9e2019-04-23 18:51:23 -0500229 self.salt.master_node,
Alex41485522019-04-12 17:26:18 -0500230 "_param:apt_mk_version"
Alexe0c5b9e2019-04-23 18:51:23 -0500231 )[self.salt.master_node]
Alex41485522019-04-12 17:26:18 -0500232 self.openstack_release = self.salt.pillar_get(
Alexe0c5b9e2019-04-23 18:51:23 -0500233 self.salt.master_node,
Alex41485522019-04-12 17:26:18 -0500234 "_param:openstack_version"
Alexe0c5b9e2019-04-23 18:51:23 -0500235 )[self.salt.master_node]
Alexd0391d42019-05-21 18:48:55 -0500236 # Preload codenames
237 # do additional queries to get linux codename and arch for each node
238 self.get_specific_pillar_for_nodes("_param:linux_system_codename")
239 self.get_specific_pillar_for_nodes("_param:linux_system_architecture")
240 for _name in self.nodes.keys():
Alexe9547d82019-06-03 15:22:50 -0500241 _n = self.nodes[_name]
242 if _name not in self.skip_list:
243 _p = _n['pillars']['_param']
244 _n['linux_codename'] = _p['linux_system_codename']
245 _n['linux_arch'] = _p['linux_system_architecture']
Alex41485522019-04-12 17:26:18 -0500246
Alex1839bbf2019-08-22 17:17:21 -0500247 def get_cmd_for_nodes(self, cmd, target_key, target_dict=None, nodes=None):
Alex836fac82019-08-22 13:36:16 -0500248 """Function runs. cmd.run and parses result into place
249 or into dict structure provided
250
251 :return: no return value, data pulished internally
252 """
253 logger_cli.debug(
254 "... collecting results for '{}'".format(cmd)
255 )
256 if target_dict:
257 _nodes = target_dict
258 else:
259 _nodes = self.nodes
Alex1839bbf2019-08-22 17:17:21 -0500260 _result = self.execute_cmd_on_active_nodes(cmd, nodes=nodes)
Alex3bc95f62020-03-05 17:00:04 -0600261 for node, data in _nodes.items():
Alexf3dbe862019-10-07 15:17:04 -0500262
Alex836fac82019-08-22 13:36:16 -0500263 if node in self.skip_list:
264 logger_cli.debug(
265 "... '{}' skipped while collecting '{}'".format(
266 node,
267 cmd
268 )
269 )
270 continue
271 # Prepare target key
272 if target_key not in data:
273 data[target_key] = None
274 # Save data
Alexe9908f72020-05-19 16:04:53 -0500275 if data['status'] in [NODE_DOWN, NODE_SKIP]:
Alex836fac82019-08-22 13:36:16 -0500276 data[target_key] = None
Alex1839bbf2019-08-22 17:17:21 -0500277 elif node not in _result:
278 continue
Alex836fac82019-08-22 13:36:16 -0500279 elif not _result[node]:
280 logger_cli.debug(
281 "... '{}' not responded after '{}'".format(
282 node,
Alex9a4ad212020-10-01 18:04:25 -0500283 self.env_config.salt_timeout
Alex836fac82019-08-22 13:36:16 -0500284 )
285 )
286 data[target_key] = None
287 else:
288 data[target_key] = _result[node]
289
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600290 def get_specific_pillar_for_nodes(self, pillar_path):
291 """Function gets pillars on given path for all nodes
292
293 :return: no return value, data pulished internally
294 """
Alex3ebc5632019-04-18 16:47:18 -0500295 logger_cli.debug(
Alexb151fbe2019-04-22 16:53:30 -0500296 "... collecting node pillars for '{}'".format(pillar_path)
Alex3ebc5632019-04-18 16:47:18 -0500297 )
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600298 _result = self.salt.pillar_get(self.active_nodes_compound, pillar_path)
Alex Savatieievefa79c42019-03-14 19:14:04 -0500299 self.not_responded = []
Alex3bc95f62020-03-05 17:00:04 -0600300 for node, data in self.nodes.items():
Alex Savatieievefa79c42019-03-14 19:14:04 -0500301 if node in self.skip_list:
302 logger_cli.debug(
303 "... '{}' skipped while collecting '{}'".format(
304 node,
305 pillar_path
306 )
307 )
308 continue
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600309 _pillar_keys = pillar_path.split(':')
310 _data = data['pillars']
311 # pre-create nested dict
312 for idx in range(0, len(_pillar_keys)-1):
313 _key = _pillar_keys[idx]
314 if _key not in _data:
315 _data[_key] = {}
316 _data = _data[_key]
Alexe9908f72020-05-19 16:04:53 -0500317 if data['status'] in [NODE_DOWN, NODE_SKIP]:
Alex Savatieievefa79c42019-03-14 19:14:04 -0500318 _data[_pillar_keys[-1]] = None
319 elif not _result[node]:
320 logger_cli.debug(
321 "... '{}' not responded after '{}'".format(
322 node,
Alex9a4ad212020-10-01 18:04:25 -0500323 self.env_config.salt_timeout
Alex Savatieievefa79c42019-03-14 19:14:04 -0500324 )
325 )
326 _data[_pillar_keys[-1]] = None
327 self.not_responded.append(node)
328 else:
329 _data[_pillar_keys[-1]] = _result[node]
Alex3ebc5632019-04-18 16:47:18 -0500330
Alexe0c5b9e2019-04-23 18:51:23 -0500331 def prepare_json_on_node(self, node, _dict, filename):
332 # this function assumes that all folders are created
333 _dumps = json.dumps(_dict, indent=2).splitlines()
334 _storage_path = os.path.join(
Alex9a4ad212020-10-01 18:04:25 -0500335 self.env_config.salt_file_root, self.env_config.salt_scripts_folder
Alexe0c5b9e2019-04-23 18:51:23 -0500336 )
337 logger_cli.debug(
338 "... uploading data as '{}' "
339 "to master's file cache folder: '{}'".format(
340 filename,
341 _storage_path
342 )
343 )
344 _cache_path = os.path.join(_storage_path, filename)
345 _source_path = os.path.join(
346 'salt://',
Alex9a4ad212020-10-01 18:04:25 -0500347 self.env_config.salt_scripts_folder,
Alexe0c5b9e2019-04-23 18:51:23 -0500348 filename
349 )
350 _target_path = os.path.join(
351 '/root',
Alex9a4ad212020-10-01 18:04:25 -0500352 self.env_config.salt_scripts_folder,
Alexe0c5b9e2019-04-23 18:51:23 -0500353 filename
354 )
355
356 logger_cli.debug("... creating file in cache '{}'".format(_cache_path))
357 self.salt.f_touch_master(_cache_path)
358 self.salt.f_append_master(_cache_path, _dumps)
359 logger.debug("... syncing file to '{}'".format(node))
360 self.salt.get_file(
361 node,
362 _source_path,
363 _target_path,
364 tgt_type="compound"
365 )
366 return _target_path
367
368 def prepare_script_on_active_nodes(self, script_filename):
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600369 # Prepare script
370 _p = os.path.join(pkg_dir, 'scripts', script_filename)
371 with open(_p, 'rt') as fd:
372 _script = fd.read().splitlines()
373 _storage_path = os.path.join(
Alex9a4ad212020-10-01 18:04:25 -0500374 self.env_config.salt_file_root, self.env_config.salt_scripts_folder
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600375 )
376 logger_cli.debug(
Alexb151fbe2019-04-22 16:53:30 -0500377 "... uploading script {} "
Alex3ebc5632019-04-18 16:47:18 -0500378 "to master's file cache folder: '{}'".format(
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600379 script_filename,
380 _storage_path
381 )
382 )
Alexe0c5b9e2019-04-23 18:51:23 -0500383 self.salt.mkdir(self.salt.master_node, _storage_path)
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600384 # Form cache, source and target path
385 _cache_path = os.path.join(_storage_path, script_filename)
386 _source_path = os.path.join(
387 'salt://',
Alex9a4ad212020-10-01 18:04:25 -0500388 self.env_config.salt_scripts_folder,
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600389 script_filename
390 )
391 _target_path = os.path.join(
392 '/root',
Alex9a4ad212020-10-01 18:04:25 -0500393 self.env_config.salt_scripts_folder,
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600394 script_filename
395 )
396
Alexb151fbe2019-04-22 16:53:30 -0500397 logger_cli.debug("... creating file in cache '{}'".format(_cache_path))
Alex3ebc5632019-04-18 16:47:18 -0500398 self.salt.f_touch_master(_cache_path)
399 self.salt.f_append_master(_cache_path, _script)
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600400 # command salt to copy file to minions
Alex3ebc5632019-04-18 16:47:18 -0500401 logger_cli.debug(
Alexb151fbe2019-04-22 16:53:30 -0500402 "... creating script target folder '{}'".format(
Alex3ebc5632019-04-18 16:47:18 -0500403 _cache_path
404 )
405 )
406 self.salt.mkdir(
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600407 self.active_nodes_compound,
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600408 os.path.join(
409 '/root',
Alex9a4ad212020-10-01 18:04:25 -0500410 self.env_config.salt_scripts_folder
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600411 ),
412 tgt_type="compound"
413 )
Alex3ebc5632019-04-18 16:47:18 -0500414 logger.debug("... syncing file to nodes")
415 self.salt.get_file(
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600416 self.active_nodes_compound,
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600417 _source_path,
418 _target_path,
419 tgt_type="compound"
420 )
Alexe0c5b9e2019-04-23 18:51:23 -0500421 # return path on nodes, just in case
422 return _target_path
423
424 def execute_script_on_node(self, node, script_filename, args=[]):
425 # Prepare path
426 _target_path = os.path.join(
427 '/root',
Alex9a4ad212020-10-01 18:04:25 -0500428 self.env_config.salt_scripts_folder,
Alexe0c5b9e2019-04-23 18:51:23 -0500429 script_filename
430 )
431
432 # execute script
433 logger.debug("... running script on '{}'".format(node))
434 # handle results for each node
435 _script_arguments = " ".join(args) if args else ""
436 self.not_responded = []
437 _r = self.salt.cmd(
438 node,
439 'cmd.run',
440 param='python {} {}'.format(_target_path, _script_arguments),
441 expr_form="compound"
442 )
443
444 # all false returns means that there is no response
445 self.not_responded = [_n for _n in _r.keys() if not _r[_n]]
446 return _r
447
448 def execute_script_on_active_nodes(self, script_filename, args=[]):
449 # Prepare path
450 _target_path = os.path.join(
451 '/root',
Alex9a4ad212020-10-01 18:04:25 -0500452 self.env_config.salt_scripts_folder,
Alexe0c5b9e2019-04-23 18:51:23 -0500453 script_filename
454 )
455
456 # execute script
Alexd0391d42019-05-21 18:48:55 -0500457 logger_cli.debug("... running script")
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600458 # handle results for each node
459 _script_arguments = " ".join(args) if args else ""
Alex Savatieievefa79c42019-03-14 19:14:04 -0500460 self.not_responded = []
461 _r = self.salt.cmd(
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600462 self.active_nodes_compound,
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600463 'cmd.run',
464 param='python {} {}'.format(_target_path, _script_arguments),
465 expr_form="compound"
466 )
467
Alex Savatieievefa79c42019-03-14 19:14:04 -0500468 # all false returns means that there is no response
Alex3ebc5632019-04-18 16:47:18 -0500469 self.not_responded = [_n for _n in _r.keys() if not _r[_n]]
Alex Savatieievefa79c42019-03-14 19:14:04 -0500470 return _r
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600471
Alex1839bbf2019-08-22 17:17:21 -0500472 def execute_cmd_on_active_nodes(self, cmd, nodes=None):
Alex836fac82019-08-22 13:36:16 -0500473 # execute cmd
474 self.not_responded = []
475 _r = self.salt.cmd(
Alex1839bbf2019-08-22 17:17:21 -0500476 nodes if nodes else self.active_nodes_compound,
Alex836fac82019-08-22 13:36:16 -0500477 'cmd.run',
478 param=cmd,
479 expr_form="compound"
480 )
481
482 # all false returns means that there is no response
483 self.not_responded = [_n for _n in _r.keys() if not _r[_n]]
484 return _r
485
Alex9a4ad212020-10-01 18:04:25 -0500486
487class KubeNodes(Nodes):
488 def __init__(self, config):
489 super(KubeNodes, self).__init__(config)
490 logger_cli.info("# Gathering environment information")
491 # simple salt rest client
492 self.kube = get_kube_remote(self.env_config)
493 self.env_type = ENV_TYPE_KUBE
494
495 def gather_node_info(self, skip_list, skip_list_file):
496 # Gather nodes info and query pod lists for each node
497 logger_cli.debug("... collecting node names existing in the cloud")
498
499 # Gather node names and info
500 _nodes = self.kube.get_node_info()
501 _node_names = list(_nodes.keys())
502 # Skip nodes if needed
503 _skipped_nodes = \
504 _prepare_skipped_nodes(_node_names, skip_list, skip_list_file)
505
506 # Count how many nodes active
507 self._active = [n for n, v in _nodes.items()
508 if v['conditions']['ready']['status']]
509
510 # iterate through all accepted nodes and create a dict for it
511 self.nodes = {}
512 self.skip_list = []
513 # _domains = set()
514 for _name in _node_names:
515 if _name in _skipped_nodes:
516 _status = NODE_SKIP
517 self.skip_list.append(_name)
518 else:
519 _status = NODE_UP if _name in self._active else NODE_DOWN
520 if _status == NODE_DOWN:
521 self.skip_list.append(_name)
522 logger_cli.info(
523 "-> '{}' shows 'Ready' as 'False', "
524 "added to skip list".format(
525 _name
526 )
527 )
528 _roles = {}
529 _labels = {}
530 for _label, _value in _nodes[_name]['labels'].items():
531 if _label in all_kube_roles_map:
532 _roles[all_kube_roles_map[_label]] = _value
533 else:
534 _labels[_label] = _value
535
536 self.nodes[_name] = deepcopy(node_tmpl)
537 self.nodes[_name].pop("grains")
538 self.nodes[_name].pop("pillars")
539
540 # hostname
541 self.nodes[_name]['shortname'] = \
542 _nodes[_name]['addresses']['hostname']['address']
543 self.nodes[_name]['internalip'] = \
544 _nodes[_name]['addresses']['internalip']['address']
545 # _domains.add(_name.split(".", 1)[1])
546 self.nodes[_name]['node_group'] = None
547 self.nodes[_name]['labels'] = _labels
548 self.nodes[_name]['roles'] = _roles
549 self.nodes[_name]['status'] = _status
550 # Backward compatibility
551 _info = _nodes[_name]['status']['node_info']
552 self.nodes[_name]['linux_image'] = _info['os_image']
553 self.nodes[_name]['linux_arch'] = _info['architecture']
554
555 _codename = "unknown"
556 _n, _v, _c = _info['os_image'].split()
557 if _n.lower() == 'ubuntu':
558 _v, _, _ = _v.rpartition('.') if '.' in _v else (_v, "", "")
559 if _v in ubuntu_versions:
560 _codename = ubuntu_versions[_v].split()[0].lower()
561 self.nodes[_name]['linux_codename'] = _codename
562
563 # Consider per-data type transfer
564 self.nodes[_name]["raw"] = _nodes[_name]
565 # TODO: Investigate how to handle domains in Kube, probably - skip
566 # _domains = list(_domains)
567 # if len(_domains) > 1:
568 # logger_cli.warning(
569 # "Multiple domains detected: {}".format(",".join(_domains))
570 # )
571 # else:
572 # self.domain = _domains[0]
573 logger_cli.info(
574 "-> {} nodes collected: {} - active, {} - not active".format(
575 len(self.nodes),
576 len(self._active),
577 len(self.skip_list)
578 )
579 )
580
581 _role = "k8s-master"
582 _filtered = [n for n, v in self.nodes.items() if _role in v['roles']]
583 if len(_filtered) < 1:
584 raise KubeException(
585 "No k8s-master nodes detected! Check/Update node role map."
586 )
Alex Savatieievefa79c42019-03-14 19:14:04 -0500587 else:
Alex9a4ad212020-10-01 18:04:25 -0500588 _r = [n for n, v in self.nodes.items()
589 if v['status'] != NODE_UP and _role in v['roles']]
590 if len(_r) > 0:
591 logger_cli.warn(
592 "Master nodes are reporting 'NotReady:\n{}".format(
593 "\n".join(_r)
594 )
595 )
596 self.kube.master_node = _filtered[0]
Alexe0c5b9e2019-04-23 18:51:23 -0500597
Alex9a4ad212020-10-01 18:04:25 -0500598 # get specific data upfront
599 # OpenStack versions
600 self.mcp_release = ""
601 # Quick and Dirty way to detect OS release
Alexccb72e02021-01-20 16:38:03 -0600602 try:
603 _nova_version = self.kube.exec_on_target_pod(
604 "nova-manage --version",
605 "nova-api-osapi",
606 "openstack"
607 )
608 _nmajor = _nova_version.partition('.')[0]
609 self.openstack_release = nova_openstack_versions[_nmajor]
610 except KubeException as e:
611 logger_cli.warn("Openstack not detected: {}".format(e.message))
612 self.openstack_release = nova_openstack_versions["00"]
Alexe0c5b9e2019-04-23 18:51:23 -0500613
Alex9a4ad212020-10-01 18:04:25 -0500614 return
615
616 @staticmethod
617 def _get_ssh_shell(_h, _u, _k, _p, _q, _pipe):
618 _ssh = SshShell(
619 _h,
620 user=_u,
621 keypath=_k,
622 port=_p,
623 silent=_q,
624 piped=_pipe
625 )
626 return _ssh.connect()
627
628 @staticmethod
629 def _do_ssh_cmd(_cmd, _h, _u, _k, _p, _q, _pipe):
630 with SshShell(
631 _h,
632 user=_u,
633 keypath=_k,
634 port=_p,
635 silent=_q,
636 piped=_pipe
637 ) as ssh:
638 _r = ssh.do(_cmd)
639 logger_cli.debug("'{}'".format(_r))
640 return _r
641
642 def node_shell(
643 self,
644 node,
645 silent=True,
646 piped=True,
647 use_sudo=True,
648 fport=None
649 ):
650 _u = self.env_config.kube_node_user
651 _k = self.env_config.kube_node_keypath
652 _h = self.nodes[node]['internalip']
653 _p = 22
Alexeffa0682021-06-04 12:18:33 -0500654 if self.kube.is_local or self.kube.config.ssh_direct:
Alex9a4ad212020-10-01 18:04:25 -0500655 return None, self._get_ssh_shell(_h, _u, _k, _p, silent, piped)
656 else:
657 _fh = "localhost"
658 _p = 10022 if not fport else fport
659 _pfwd = PortForward(
660 self.env_config.ssh_host,
661 _h,
662 user=_u,
663 keypath=self.env_config.ssh_key,
664 loc_port=_p
665 )
666 _pfwd.connect()
667 _ssh = self._get_ssh_shell(_fh, _u, _k, _p, silent, piped)
668 return _pfwd, _ssh
669
670 def execute_script_on_node(self, node, script_filename, args=[]):
671 # Prepare path
672 _target_path = os.path.join(
Alexccb72e02021-01-20 16:38:03 -0600673 self.env_config.kube_node_homepath,
Alex9a4ad212020-10-01 18:04:25 -0500674 self.env_config.kube_scripts_folder,
675 script_filename
676 )
677
678 # execute script
679 logger_cli.debug("... running script on '{}'".format(node))
680 # handle results for each node
681 _script_arguments = " ".join(args) if args else ""
682 self.not_responded = []
683 # get result
684 _nr = self.node_shell(
685 node,
686 "python {} {}".format(
687 _target_path,
688 _script_arguments
689 )
690 )
691
692 if not _nr:
693 self.not_responded.append(node)
694 return {}
695 else:
696 return {node: _nr}
697
698 def execute_cmd_on_active_nodes(self, cmd, nodes=None):
699 # execute script
700 logger_cli.debug("...running '{}' on active nodes".format(cmd))
701 # handle results for each node
702 self.not_responded = []
703 _r = {}
704 # TODO: Use threading and pool
705 for node in self._active:
706 _nr = self.node_shell(
707 node,
708 cmd
709 )
710
711 if not _nr:
712 self.not_responded.append(node)
713 else:
714 _r[node] = _nr
715
716 return _r
717
718 def _exec_script(self, params):
719 """
720 Threadsafe method to get shell to node,
721 check/copy script and get results
722 [
723 node_name,
724 src_path,
725 tgt_path,
726 conf,
727 args
728 ]
729 """
730 _name = params[0]
731 _src = params[1]
732 _tgt = params[2]
733 _conf = params[3]
734 _args = params[4]
735 _port = params[5]
736 _log_name = "["+_name+"]:"
737 _check = "echo $(if [[ -s '{}' ]]; then echo True; " \
738 "else echo False; fi)"
739 _fwd_sh, _sh = self.node_shell(
740 _name,
741 use_sudo=False,
742 fport=_port
743 )
744 # check python3
745 _python = _sh.do("which python3")
746 _python = utils.to_bool(
747 _sh.do(_check.format(_python))
748 )
749 if not _python:
750 _sh.do("apt install python3", sudo=True)
751 # check if script already there
752 _folder = os.path.join(
Alexccb72e02021-01-20 16:38:03 -0600753 self.env_config.kube_node_homepath,
Alex9a4ad212020-10-01 18:04:25 -0500754 _conf.kube_scripts_folder
755 )
756 # check if folder exists
757 _folder_exists = utils.to_bool(
758 _sh.do(_check.format(_folder))
759 )
760 if not _folder_exists:
761 _sh.do("mkdir " + _folder)
762 logger.info("{} Syncing file".format(_log_name))
763 _code, _r, _e = _sh.scp(
764 _src,
765 _sh.get_host_path(_tgt),
766 )
767 # handle error code
768 if _code:
769 logger_cli.warn(
770 "{} Error in scp:\n"
771 "\tstdout:'{}'\n"
772 "\tstderr:'{}'".format(_log_name, _r, _e)
773 )
774
775 # execute script
776 logger.debug("{} Running script".format(_log_name))
777 _out = _sh.do(
778 "python3 {}{}".format(
779 _tgt,
780 _args
781 ),
782 sudo=True
783 )
784
785 if _fwd_sh:
786 _fwd_sh.kill()
787 _sh.kill()
788
789 return [_name, _out]
790
791 def execute_script_on_active_nodes(self, script_filename, args=[]):
792 # Prepare script
793 _source_path = os.path.join(pkg_dir, 'scripts', script_filename)
794 _target_path = os.path.join(
Alexccb72e02021-01-20 16:38:03 -0600795 self.env_config.kube_node_homepath,
Alex9a4ad212020-10-01 18:04:25 -0500796 self.env_config.kube_scripts_folder,
797 script_filename
798 )
799 # handle results for each node
800 _script_arguments = " ".join(args) if args else ""
801 if _script_arguments:
802 _script_arguments = " " + _script_arguments
803 self.not_responded = []
804 _results = {}
805 logger_cli.debug(
806 "...running '{}' on active nodes, {} worker threads".format(
807 script_filename,
808 self.env_config.threads
809 )
810 )
811 # Workers pool
812 pool = Pool(self.env_config.threads)
813
814 # init the parameters
815 # node_name,
816 # src_path,
817 # tgt_path,
818 # conf,
819 # args
820 _params = []
821 _port = 10022
822 for node in self._active:
823 # build parameter blocks
824 _p_list = [
825 node,
826 _source_path,
827 _target_path,
828 self.env_config,
829 _script_arguments,
830 _port
831 ]
832 _params.append(_p_list)
833 _port += 1
834
835 _progress = Progress(len(_params))
836 results = pool.imap_unordered(self._exec_script, _params)
837
838 for ii in enumerate(results, start=1):
839 if not ii[1][1]:
840 self.not_responded.append(ii[1][0])
841 else:
842 _results[ii[1][0]] = ii[1][1]
843 _progress.write_progress(ii[0])
844
845 _progress.end()
846 pool.close()
847 pool.join()
848
849 # return path on nodes, just in case
850 return _results
851
852 def prepare_json_on_node(self, node, _dict, filename):
853 # this function assumes that all folders are created
854 _dumps = json.dumps(_dict, indent=2).splitlines()
855
856 _source_path = create_temp_file_with_content(_dumps)
857 _target_path = os.path.join(
Alexccb72e02021-01-20 16:38:03 -0600858 self.env_config.kube_node_homepath,
Alex9a4ad212020-10-01 18:04:25 -0500859 self.env_config.kube_scripts_folder,
860 filename
861 )
862 _folder = os.path.join(
Alexccb72e02021-01-20 16:38:03 -0600863 self.env_config.kube_node_homepath,
Alex9a4ad212020-10-01 18:04:25 -0500864 self.env_config.kube_scripts_folder
865 )
866 _check = "echo $(if [[ -s '{}' ]]; then echo True; " \
867 "else echo False; fi)"
868 _fwd_sh, _sh = self.node_shell(
869 node,
870 use_sudo=False
871 )
872
873 # check if folder exists
874 _folder_exists = utils.to_bool(
875 _sh.do(_check.format(_folder))
876 )
877 if not _folder_exists:
878 _sh.do("mkdir " + _folder)
879 logger_cli.debug(
880 "...create data on node '{}':'{}'".format(node, _target_path)
881 )
882 _code, _r, _e = _sh.scp(
883 _source_path,
884 _sh.get_host_path(_target_path),
885 )
886 # handle error code
887 if _code:
888 logger_cli.warn(
889 "Error in scp:\n"
890 "\tstdout:'{}'\n"
891 "\tstderr:'{}'".format(_r, _e)
892 )
893
894 _fwd_sh.kill()
895 _sh.kill()
896 return _target_path