blob: e568cecf013c0adace8a57a2b8d7571cdb6341ca [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 )
199 else:
200 self.domain = _domains[0]
Alex Savatieievefa79c42019-03-14 19:14:04 -0500201 logger_cli.info("-> {} nodes inactive".format(len(self.skip_list)))
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600202 logger_cli.info("-> {} nodes collected".format(len(self.nodes)))
203
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600204 # form an all nodes compound string to use in salt
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600205 self.active_nodes_compound = self.salt.compound_string_from_list(
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600206 filter(
Alexe9908f72020-05-19 16:04:53 -0500207 lambda nd: self.nodes[nd]['status'] == NODE_UP,
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600208 self.nodes
209 )
210 )
Alex41485522019-04-12 17:26:18 -0500211 # get master node fqdn
Alex3bc95f62020-03-05 17:00:04 -0600212 # _filtered = filter(
213 # lambda nd: self.nodes[nd]['role'] == const.all_roles_map['cfg'],
214 # self.nodes
215 # )
Alex9a4ad212020-10-01 18:04:25 -0500216 _role = all_salt_roles_map['cfg']
Alex3bc95f62020-03-05 17:00:04 -0600217 _filtered = [n for n, v in self.nodes.items() if v['role'] == _role]
Alexe0c5b9e2019-04-23 18:51:23 -0500218 if len(_filtered) < 1:
219 raise SaltException(
220 "No master node detected! Check/Update node role map."
221 )
222 else:
223 self.salt.master_node = _filtered[0]
Alex3ebc5632019-04-18 16:47:18 -0500224
Alex41485522019-04-12 17:26:18 -0500225 # OpenStack versions
226 self.mcp_release = self.salt.pillar_get(
Alexe0c5b9e2019-04-23 18:51:23 -0500227 self.salt.master_node,
Alex41485522019-04-12 17:26:18 -0500228 "_param:apt_mk_version"
Alexe0c5b9e2019-04-23 18:51:23 -0500229 )[self.salt.master_node]
Alex41485522019-04-12 17:26:18 -0500230 self.openstack_release = self.salt.pillar_get(
Alexe0c5b9e2019-04-23 18:51:23 -0500231 self.salt.master_node,
Alex41485522019-04-12 17:26:18 -0500232 "_param:openstack_version"
Alexe0c5b9e2019-04-23 18:51:23 -0500233 )[self.salt.master_node]
Alexd0391d42019-05-21 18:48:55 -0500234 # Preload codenames
235 # do additional queries to get linux codename and arch for each node
236 self.get_specific_pillar_for_nodes("_param:linux_system_codename")
237 self.get_specific_pillar_for_nodes("_param:linux_system_architecture")
238 for _name in self.nodes.keys():
Alexe9547d82019-06-03 15:22:50 -0500239 _n = self.nodes[_name]
240 if _name not in self.skip_list:
241 _p = _n['pillars']['_param']
242 _n['linux_codename'] = _p['linux_system_codename']
243 _n['linux_arch'] = _p['linux_system_architecture']
Alex41485522019-04-12 17:26:18 -0500244
Alex1839bbf2019-08-22 17:17:21 -0500245 def get_cmd_for_nodes(self, cmd, target_key, target_dict=None, nodes=None):
Alex836fac82019-08-22 13:36:16 -0500246 """Function runs. cmd.run and parses result into place
247 or into dict structure provided
248
249 :return: no return value, data pulished internally
250 """
251 logger_cli.debug(
252 "... collecting results for '{}'".format(cmd)
253 )
254 if target_dict:
255 _nodes = target_dict
256 else:
257 _nodes = self.nodes
Alex1839bbf2019-08-22 17:17:21 -0500258 _result = self.execute_cmd_on_active_nodes(cmd, nodes=nodes)
Alex3bc95f62020-03-05 17:00:04 -0600259 for node, data in _nodes.items():
Alexf3dbe862019-10-07 15:17:04 -0500260
Alex836fac82019-08-22 13:36:16 -0500261 if node in self.skip_list:
262 logger_cli.debug(
263 "... '{}' skipped while collecting '{}'".format(
264 node,
265 cmd
266 )
267 )
268 continue
269 # Prepare target key
270 if target_key not in data:
271 data[target_key] = None
272 # Save data
Alexe9908f72020-05-19 16:04:53 -0500273 if data['status'] in [NODE_DOWN, NODE_SKIP]:
Alex836fac82019-08-22 13:36:16 -0500274 data[target_key] = None
Alex1839bbf2019-08-22 17:17:21 -0500275 elif node not in _result:
276 continue
Alex836fac82019-08-22 13:36:16 -0500277 elif not _result[node]:
278 logger_cli.debug(
279 "... '{}' not responded after '{}'".format(
280 node,
Alex9a4ad212020-10-01 18:04:25 -0500281 self.env_config.salt_timeout
Alex836fac82019-08-22 13:36:16 -0500282 )
283 )
284 data[target_key] = None
285 else:
286 data[target_key] = _result[node]
287
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600288 def get_specific_pillar_for_nodes(self, pillar_path):
289 """Function gets pillars on given path for all nodes
290
291 :return: no return value, data pulished internally
292 """
Alex3ebc5632019-04-18 16:47:18 -0500293 logger_cli.debug(
Alexb151fbe2019-04-22 16:53:30 -0500294 "... collecting node pillars for '{}'".format(pillar_path)
Alex3ebc5632019-04-18 16:47:18 -0500295 )
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600296 _result = self.salt.pillar_get(self.active_nodes_compound, pillar_path)
Alex Savatieievefa79c42019-03-14 19:14:04 -0500297 self.not_responded = []
Alex3bc95f62020-03-05 17:00:04 -0600298 for node, data in self.nodes.items():
Alex Savatieievefa79c42019-03-14 19:14:04 -0500299 if node in self.skip_list:
300 logger_cli.debug(
301 "... '{}' skipped while collecting '{}'".format(
302 node,
303 pillar_path
304 )
305 )
306 continue
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600307 _pillar_keys = pillar_path.split(':')
308 _data = data['pillars']
309 # pre-create nested dict
310 for idx in range(0, len(_pillar_keys)-1):
311 _key = _pillar_keys[idx]
312 if _key not in _data:
313 _data[_key] = {}
314 _data = _data[_key]
Alexe9908f72020-05-19 16:04:53 -0500315 if data['status'] in [NODE_DOWN, NODE_SKIP]:
Alex Savatieievefa79c42019-03-14 19:14:04 -0500316 _data[_pillar_keys[-1]] = None
317 elif not _result[node]:
318 logger_cli.debug(
319 "... '{}' not responded after '{}'".format(
320 node,
Alex9a4ad212020-10-01 18:04:25 -0500321 self.env_config.salt_timeout
Alex Savatieievefa79c42019-03-14 19:14:04 -0500322 )
323 )
324 _data[_pillar_keys[-1]] = None
325 self.not_responded.append(node)
326 else:
327 _data[_pillar_keys[-1]] = _result[node]
Alex3ebc5632019-04-18 16:47:18 -0500328
Alexe0c5b9e2019-04-23 18:51:23 -0500329 def prepare_json_on_node(self, node, _dict, filename):
330 # this function assumes that all folders are created
331 _dumps = json.dumps(_dict, indent=2).splitlines()
332 _storage_path = os.path.join(
Alex9a4ad212020-10-01 18:04:25 -0500333 self.env_config.salt_file_root, self.env_config.salt_scripts_folder
Alexe0c5b9e2019-04-23 18:51:23 -0500334 )
335 logger_cli.debug(
336 "... uploading data as '{}' "
337 "to master's file cache folder: '{}'".format(
338 filename,
339 _storage_path
340 )
341 )
342 _cache_path = os.path.join(_storage_path, filename)
343 _source_path = os.path.join(
344 'salt://',
Alex9a4ad212020-10-01 18:04:25 -0500345 self.env_config.salt_scripts_folder,
Alexe0c5b9e2019-04-23 18:51:23 -0500346 filename
347 )
348 _target_path = os.path.join(
349 '/root',
Alex9a4ad212020-10-01 18:04:25 -0500350 self.env_config.salt_scripts_folder,
Alexe0c5b9e2019-04-23 18:51:23 -0500351 filename
352 )
353
354 logger_cli.debug("... creating file in cache '{}'".format(_cache_path))
355 self.salt.f_touch_master(_cache_path)
356 self.salt.f_append_master(_cache_path, _dumps)
357 logger.debug("... syncing file to '{}'".format(node))
358 self.salt.get_file(
359 node,
360 _source_path,
361 _target_path,
362 tgt_type="compound"
363 )
364 return _target_path
365
366 def prepare_script_on_active_nodes(self, script_filename):
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600367 # Prepare script
368 _p = os.path.join(pkg_dir, 'scripts', script_filename)
369 with open(_p, 'rt') as fd:
370 _script = fd.read().splitlines()
371 _storage_path = os.path.join(
Alex9a4ad212020-10-01 18:04:25 -0500372 self.env_config.salt_file_root, self.env_config.salt_scripts_folder
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600373 )
374 logger_cli.debug(
Alexb151fbe2019-04-22 16:53:30 -0500375 "... uploading script {} "
Alex3ebc5632019-04-18 16:47:18 -0500376 "to master's file cache folder: '{}'".format(
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600377 script_filename,
378 _storage_path
379 )
380 )
Alexe0c5b9e2019-04-23 18:51:23 -0500381 self.salt.mkdir(self.salt.master_node, _storage_path)
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600382 # Form cache, source and target path
383 _cache_path = os.path.join(_storage_path, script_filename)
384 _source_path = os.path.join(
385 'salt://',
Alex9a4ad212020-10-01 18:04:25 -0500386 self.env_config.salt_scripts_folder,
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600387 script_filename
388 )
389 _target_path = os.path.join(
390 '/root',
Alex9a4ad212020-10-01 18:04:25 -0500391 self.env_config.salt_scripts_folder,
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600392 script_filename
393 )
394
Alexb151fbe2019-04-22 16:53:30 -0500395 logger_cli.debug("... creating file in cache '{}'".format(_cache_path))
Alex3ebc5632019-04-18 16:47:18 -0500396 self.salt.f_touch_master(_cache_path)
397 self.salt.f_append_master(_cache_path, _script)
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600398 # command salt to copy file to minions
Alex3ebc5632019-04-18 16:47:18 -0500399 logger_cli.debug(
Alexb151fbe2019-04-22 16:53:30 -0500400 "... creating script target folder '{}'".format(
Alex3ebc5632019-04-18 16:47:18 -0500401 _cache_path
402 )
403 )
404 self.salt.mkdir(
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600405 self.active_nodes_compound,
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600406 os.path.join(
407 '/root',
Alex9a4ad212020-10-01 18:04:25 -0500408 self.env_config.salt_scripts_folder
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600409 ),
410 tgt_type="compound"
411 )
Alex3ebc5632019-04-18 16:47:18 -0500412 logger.debug("... syncing file to nodes")
413 self.salt.get_file(
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600414 self.active_nodes_compound,
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600415 _source_path,
416 _target_path,
417 tgt_type="compound"
418 )
Alexe0c5b9e2019-04-23 18:51:23 -0500419 # return path on nodes, just in case
420 return _target_path
421
422 def execute_script_on_node(self, node, script_filename, args=[]):
423 # Prepare path
424 _target_path = os.path.join(
425 '/root',
Alex9a4ad212020-10-01 18:04:25 -0500426 self.env_config.salt_scripts_folder,
Alexe0c5b9e2019-04-23 18:51:23 -0500427 script_filename
428 )
429
430 # execute script
431 logger.debug("... running script on '{}'".format(node))
432 # handle results for each node
433 _script_arguments = " ".join(args) if args else ""
434 self.not_responded = []
435 _r = self.salt.cmd(
436 node,
437 'cmd.run',
438 param='python {} {}'.format(_target_path, _script_arguments),
439 expr_form="compound"
440 )
441
442 # all false returns means that there is no response
443 self.not_responded = [_n for _n in _r.keys() if not _r[_n]]
444 return _r
445
446 def execute_script_on_active_nodes(self, script_filename, args=[]):
447 # Prepare path
448 _target_path = os.path.join(
449 '/root',
Alex9a4ad212020-10-01 18:04:25 -0500450 self.env_config.salt_scripts_folder,
Alexe0c5b9e2019-04-23 18:51:23 -0500451 script_filename
452 )
453
454 # execute script
Alexd0391d42019-05-21 18:48:55 -0500455 logger_cli.debug("... running script")
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600456 # handle results for each node
457 _script_arguments = " ".join(args) if args else ""
Alex Savatieievefa79c42019-03-14 19:14:04 -0500458 self.not_responded = []
459 _r = self.salt.cmd(
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600460 self.active_nodes_compound,
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600461 'cmd.run',
462 param='python {} {}'.format(_target_path, _script_arguments),
463 expr_form="compound"
464 )
465
Alex Savatieievefa79c42019-03-14 19:14:04 -0500466 # all false returns means that there is no response
Alex3ebc5632019-04-18 16:47:18 -0500467 self.not_responded = [_n for _n in _r.keys() if not _r[_n]]
Alex Savatieievefa79c42019-03-14 19:14:04 -0500468 return _r
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600469
Alex1839bbf2019-08-22 17:17:21 -0500470 def execute_cmd_on_active_nodes(self, cmd, nodes=None):
Alex836fac82019-08-22 13:36:16 -0500471 # execute cmd
472 self.not_responded = []
473 _r = self.salt.cmd(
Alex1839bbf2019-08-22 17:17:21 -0500474 nodes if nodes else self.active_nodes_compound,
Alex836fac82019-08-22 13:36:16 -0500475 'cmd.run',
476 param=cmd,
477 expr_form="compound"
478 )
479
480 # all false returns means that there is no response
481 self.not_responded = [_n for _n in _r.keys() if not _r[_n]]
482 return _r
483
Alex9a4ad212020-10-01 18:04:25 -0500484
485class KubeNodes(Nodes):
486 def __init__(self, config):
487 super(KubeNodes, self).__init__(config)
488 logger_cli.info("# Gathering environment information")
489 # simple salt rest client
490 self.kube = get_kube_remote(self.env_config)
491 self.env_type = ENV_TYPE_KUBE
492
493 def gather_node_info(self, skip_list, skip_list_file):
494 # Gather nodes info and query pod lists for each node
495 logger_cli.debug("... collecting node names existing in the cloud")
496
497 # Gather node names and info
498 _nodes = self.kube.get_node_info()
499 _node_names = list(_nodes.keys())
500 # Skip nodes if needed
501 _skipped_nodes = \
502 _prepare_skipped_nodes(_node_names, skip_list, skip_list_file)
503
504 # Count how many nodes active
505 self._active = [n for n, v in _nodes.items()
506 if v['conditions']['ready']['status']]
507
508 # iterate through all accepted nodes and create a dict for it
509 self.nodes = {}
510 self.skip_list = []
511 # _domains = set()
512 for _name in _node_names:
513 if _name in _skipped_nodes:
514 _status = NODE_SKIP
515 self.skip_list.append(_name)
516 else:
517 _status = NODE_UP if _name in self._active else NODE_DOWN
518 if _status == NODE_DOWN:
519 self.skip_list.append(_name)
520 logger_cli.info(
521 "-> '{}' shows 'Ready' as 'False', "
522 "added to skip list".format(
523 _name
524 )
525 )
526 _roles = {}
527 _labels = {}
528 for _label, _value in _nodes[_name]['labels'].items():
529 if _label in all_kube_roles_map:
530 _roles[all_kube_roles_map[_label]] = _value
531 else:
532 _labels[_label] = _value
533
534 self.nodes[_name] = deepcopy(node_tmpl)
535 self.nodes[_name].pop("grains")
536 self.nodes[_name].pop("pillars")
537
538 # hostname
539 self.nodes[_name]['shortname'] = \
540 _nodes[_name]['addresses']['hostname']['address']
541 self.nodes[_name]['internalip'] = \
542 _nodes[_name]['addresses']['internalip']['address']
543 # _domains.add(_name.split(".", 1)[1])
544 self.nodes[_name]['node_group'] = None
545 self.nodes[_name]['labels'] = _labels
546 self.nodes[_name]['roles'] = _roles
547 self.nodes[_name]['status'] = _status
548 # Backward compatibility
549 _info = _nodes[_name]['status']['node_info']
550 self.nodes[_name]['linux_image'] = _info['os_image']
551 self.nodes[_name]['linux_arch'] = _info['architecture']
552
553 _codename = "unknown"
554 _n, _v, _c = _info['os_image'].split()
555 if _n.lower() == 'ubuntu':
556 _v, _, _ = _v.rpartition('.') if '.' in _v else (_v, "", "")
557 if _v in ubuntu_versions:
558 _codename = ubuntu_versions[_v].split()[0].lower()
559 self.nodes[_name]['linux_codename'] = _codename
560
561 # Consider per-data type transfer
562 self.nodes[_name]["raw"] = _nodes[_name]
563 # TODO: Investigate how to handle domains in Kube, probably - skip
564 # _domains = list(_domains)
565 # if len(_domains) > 1:
566 # logger_cli.warning(
567 # "Multiple domains detected: {}".format(",".join(_domains))
568 # )
569 # else:
570 # self.domain = _domains[0]
571 logger_cli.info(
572 "-> {} nodes collected: {} - active, {} - not active".format(
573 len(self.nodes),
574 len(self._active),
575 len(self.skip_list)
576 )
577 )
578
579 _role = "k8s-master"
580 _filtered = [n for n, v in self.nodes.items() if _role in v['roles']]
581 if len(_filtered) < 1:
582 raise KubeException(
583 "No k8s-master nodes detected! Check/Update node role map."
584 )
Alex Savatieievefa79c42019-03-14 19:14:04 -0500585 else:
Alex9a4ad212020-10-01 18:04:25 -0500586 _r = [n for n, v in self.nodes.items()
587 if v['status'] != NODE_UP and _role in v['roles']]
588 if len(_r) > 0:
589 logger_cli.warn(
590 "Master nodes are reporting 'NotReady:\n{}".format(
591 "\n".join(_r)
592 )
593 )
594 self.kube.master_node = _filtered[0]
Alexe0c5b9e2019-04-23 18:51:23 -0500595
Alex9a4ad212020-10-01 18:04:25 -0500596 # get specific data upfront
597 # OpenStack versions
598 self.mcp_release = ""
599 # Quick and Dirty way to detect OS release
600 _nova_version = self.kube.exec_on_target_pod(
601 "nova-manage --version",
602 "nova-api-osapi",
603 "openstack"
604 )
605 _nmajor = _nova_version.partition('.')[0]
606 self.openstack_release = nova_openstack_versions[_nmajor]
Alexe0c5b9e2019-04-23 18:51:23 -0500607
Alex9a4ad212020-10-01 18:04:25 -0500608 return
609
610 @staticmethod
611 def _get_ssh_shell(_h, _u, _k, _p, _q, _pipe):
612 _ssh = SshShell(
613 _h,
614 user=_u,
615 keypath=_k,
616 port=_p,
617 silent=_q,
618 piped=_pipe
619 )
620 return _ssh.connect()
621
622 @staticmethod
623 def _do_ssh_cmd(_cmd, _h, _u, _k, _p, _q, _pipe):
624 with SshShell(
625 _h,
626 user=_u,
627 keypath=_k,
628 port=_p,
629 silent=_q,
630 piped=_pipe
631 ) as ssh:
632 _r = ssh.do(_cmd)
633 logger_cli.debug("'{}'".format(_r))
634 return _r
635
636 def node_shell(
637 self,
638 node,
639 silent=True,
640 piped=True,
641 use_sudo=True,
642 fport=None
643 ):
644 _u = self.env_config.kube_node_user
645 _k = self.env_config.kube_node_keypath
646 _h = self.nodes[node]['internalip']
647 _p = 22
648 if self.kube.is_local:
649 return None, self._get_ssh_shell(_h, _u, _k, _p, silent, piped)
650 else:
651 _fh = "localhost"
652 _p = 10022 if not fport else fport
653 _pfwd = PortForward(
654 self.env_config.ssh_host,
655 _h,
656 user=_u,
657 keypath=self.env_config.ssh_key,
658 loc_port=_p
659 )
660 _pfwd.connect()
661 _ssh = self._get_ssh_shell(_fh, _u, _k, _p, silent, piped)
662 return _pfwd, _ssh
663
664 def execute_script_on_node(self, node, script_filename, args=[]):
665 # Prepare path
666 _target_path = os.path.join(
667 self.env_config.node_homepath,
668 self.env_config.kube_scripts_folder,
669 script_filename
670 )
671
672 # execute script
673 logger_cli.debug("... running script on '{}'".format(node))
674 # handle results for each node
675 _script_arguments = " ".join(args) if args else ""
676 self.not_responded = []
677 # get result
678 _nr = self.node_shell(
679 node,
680 "python {} {}".format(
681 _target_path,
682 _script_arguments
683 )
684 )
685
686 if not _nr:
687 self.not_responded.append(node)
688 return {}
689 else:
690 return {node: _nr}
691
692 def execute_cmd_on_active_nodes(self, cmd, nodes=None):
693 # execute script
694 logger_cli.debug("...running '{}' on active nodes".format(cmd))
695 # handle results for each node
696 self.not_responded = []
697 _r = {}
698 # TODO: Use threading and pool
699 for node in self._active:
700 _nr = self.node_shell(
701 node,
702 cmd
703 )
704
705 if not _nr:
706 self.not_responded.append(node)
707 else:
708 _r[node] = _nr
709
710 return _r
711
712 def _exec_script(self, params):
713 """
714 Threadsafe method to get shell to node,
715 check/copy script and get results
716 [
717 node_name,
718 src_path,
719 tgt_path,
720 conf,
721 args
722 ]
723 """
724 _name = params[0]
725 _src = params[1]
726 _tgt = params[2]
727 _conf = params[3]
728 _args = params[4]
729 _port = params[5]
730 _log_name = "["+_name+"]:"
731 _check = "echo $(if [[ -s '{}' ]]; then echo True; " \
732 "else echo False; fi)"
733 _fwd_sh, _sh = self.node_shell(
734 _name,
735 use_sudo=False,
736 fport=_port
737 )
738 # check python3
739 _python = _sh.do("which python3")
740 _python = utils.to_bool(
741 _sh.do(_check.format(_python))
742 )
743 if not _python:
744 _sh.do("apt install python3", sudo=True)
745 # check if script already there
746 _folder = os.path.join(
747 self.env_config.node_homepath,
748 _conf.kube_scripts_folder
749 )
750 # check if folder exists
751 _folder_exists = utils.to_bool(
752 _sh.do(_check.format(_folder))
753 )
754 if not _folder_exists:
755 _sh.do("mkdir " + _folder)
756 logger.info("{} Syncing file".format(_log_name))
757 _code, _r, _e = _sh.scp(
758 _src,
759 _sh.get_host_path(_tgt),
760 )
761 # handle error code
762 if _code:
763 logger_cli.warn(
764 "{} Error in scp:\n"
765 "\tstdout:'{}'\n"
766 "\tstderr:'{}'".format(_log_name, _r, _e)
767 )
768
769 # execute script
770 logger.debug("{} Running script".format(_log_name))
771 _out = _sh.do(
772 "python3 {}{}".format(
773 _tgt,
774 _args
775 ),
776 sudo=True
777 )
778
779 if _fwd_sh:
780 _fwd_sh.kill()
781 _sh.kill()
782
783 return [_name, _out]
784
785 def execute_script_on_active_nodes(self, script_filename, args=[]):
786 # Prepare script
787 _source_path = os.path.join(pkg_dir, 'scripts', script_filename)
788 _target_path = os.path.join(
789 self.env_config.node_homepath,
790 self.env_config.kube_scripts_folder,
791 script_filename
792 )
793 # handle results for each node
794 _script_arguments = " ".join(args) if args else ""
795 if _script_arguments:
796 _script_arguments = " " + _script_arguments
797 self.not_responded = []
798 _results = {}
799 logger_cli.debug(
800 "...running '{}' on active nodes, {} worker threads".format(
801 script_filename,
802 self.env_config.threads
803 )
804 )
805 # Workers pool
806 pool = Pool(self.env_config.threads)
807
808 # init the parameters
809 # node_name,
810 # src_path,
811 # tgt_path,
812 # conf,
813 # args
814 _params = []
815 _port = 10022
816 for node in self._active:
817 # build parameter blocks
818 _p_list = [
819 node,
820 _source_path,
821 _target_path,
822 self.env_config,
823 _script_arguments,
824 _port
825 ]
826 _params.append(_p_list)
827 _port += 1
828
829 _progress = Progress(len(_params))
830 results = pool.imap_unordered(self._exec_script, _params)
831
832 for ii in enumerate(results, start=1):
833 if not ii[1][1]:
834 self.not_responded.append(ii[1][0])
835 else:
836 _results[ii[1][0]] = ii[1][1]
837 _progress.write_progress(ii[0])
838
839 _progress.end()
840 pool.close()
841 pool.join()
842
843 # return path on nodes, just in case
844 return _results
845
846 def prepare_json_on_node(self, node, _dict, filename):
847 # this function assumes that all folders are created
848 _dumps = json.dumps(_dict, indent=2).splitlines()
849
850 _source_path = create_temp_file_with_content(_dumps)
851 _target_path = os.path.join(
852 self.env_config.node_homepath,
853 self.env_config.kube_scripts_folder,
854 filename
855 )
856 _folder = os.path.join(
857 self.env_config.node_homepath,
858 self.env_config.kube_scripts_folder
859 )
860 _check = "echo $(if [[ -s '{}' ]]; then echo True; " \
861 "else echo False; fi)"
862 _fwd_sh, _sh = self.node_shell(
863 node,
864 use_sudo=False
865 )
866
867 # check if folder exists
868 _folder_exists = utils.to_bool(
869 _sh.do(_check.format(_folder))
870 )
871 if not _folder_exists:
872 _sh.do("mkdir " + _folder)
873 logger_cli.debug(
874 "...create data on node '{}':'{}'".format(node, _target_path)
875 )
876 _code, _r, _e = _sh.scp(
877 _source_path,
878 _sh.get_host_path(_target_path),
879 )
880 # handle error code
881 if _code:
882 logger_cli.warn(
883 "Error in scp:\n"
884 "\tstdout:'{}'\n"
885 "\tstderr:'{}'".format(_r, _e)
886 )
887
888 _fwd_sh.kill()
889 _sh.kill()
890 return _target_path