blob: 615a1a48b267c33cabe7ac129e16538d68d11085 [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
201
Alexe9908f72020-05-19 16:04:53 -0500202 else:
203 self.domain = _domains[0]
Alex Savatieievefa79c42019-03-14 19:14:04 -0500204 logger_cli.info("-> {} nodes inactive".format(len(self.skip_list)))
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600205 logger_cli.info("-> {} nodes collected".format(len(self.nodes)))
206
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600207 # form an all nodes compound string to use in salt
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600208 self.active_nodes_compound = self.salt.compound_string_from_list(
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600209 filter(
Alexe9908f72020-05-19 16:04:53 -0500210 lambda nd: self.nodes[nd]['status'] == NODE_UP,
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600211 self.nodes
212 )
213 )
Alex41485522019-04-12 17:26:18 -0500214 # get master node fqdn
Alex3bc95f62020-03-05 17:00:04 -0600215 # _filtered = filter(
216 # lambda nd: self.nodes[nd]['role'] == const.all_roles_map['cfg'],
217 # self.nodes
218 # )
Alex9a4ad212020-10-01 18:04:25 -0500219 _role = all_salt_roles_map['cfg']
Alex3bc95f62020-03-05 17:00:04 -0600220 _filtered = [n for n, v in self.nodes.items() if v['role'] == _role]
Alexe0c5b9e2019-04-23 18:51:23 -0500221 if len(_filtered) < 1:
222 raise SaltException(
223 "No master node detected! Check/Update node role map."
224 )
225 else:
226 self.salt.master_node = _filtered[0]
Alex3ebc5632019-04-18 16:47:18 -0500227
Alex41485522019-04-12 17:26:18 -0500228 # OpenStack versions
229 self.mcp_release = self.salt.pillar_get(
Alexe0c5b9e2019-04-23 18:51:23 -0500230 self.salt.master_node,
Alex41485522019-04-12 17:26:18 -0500231 "_param:apt_mk_version"
Alexe0c5b9e2019-04-23 18:51:23 -0500232 )[self.salt.master_node]
Alex41485522019-04-12 17:26:18 -0500233 self.openstack_release = self.salt.pillar_get(
Alexe0c5b9e2019-04-23 18:51:23 -0500234 self.salt.master_node,
Alex41485522019-04-12 17:26:18 -0500235 "_param:openstack_version"
Alexe0c5b9e2019-04-23 18:51:23 -0500236 )[self.salt.master_node]
Alexd0391d42019-05-21 18:48:55 -0500237 # Preload codenames
238 # do additional queries to get linux codename and arch for each node
239 self.get_specific_pillar_for_nodes("_param:linux_system_codename")
240 self.get_specific_pillar_for_nodes("_param:linux_system_architecture")
241 for _name in self.nodes.keys():
Alexe9547d82019-06-03 15:22:50 -0500242 _n = self.nodes[_name]
243 if _name not in self.skip_list:
244 _p = _n['pillars']['_param']
245 _n['linux_codename'] = _p['linux_system_codename']
246 _n['linux_arch'] = _p['linux_system_architecture']
Alex41485522019-04-12 17:26:18 -0500247
Alex1839bbf2019-08-22 17:17:21 -0500248 def get_cmd_for_nodes(self, cmd, target_key, target_dict=None, nodes=None):
Alex836fac82019-08-22 13:36:16 -0500249 """Function runs. cmd.run and parses result into place
250 or into dict structure provided
251
252 :return: no return value, data pulished internally
253 """
254 logger_cli.debug(
255 "... collecting results for '{}'".format(cmd)
256 )
257 if target_dict:
258 _nodes = target_dict
259 else:
260 _nodes = self.nodes
Alex1839bbf2019-08-22 17:17:21 -0500261 _result = self.execute_cmd_on_active_nodes(cmd, nodes=nodes)
Alex3bc95f62020-03-05 17:00:04 -0600262 for node, data in _nodes.items():
Alexf3dbe862019-10-07 15:17:04 -0500263
Alex836fac82019-08-22 13:36:16 -0500264 if node in self.skip_list:
265 logger_cli.debug(
266 "... '{}' skipped while collecting '{}'".format(
267 node,
268 cmd
269 )
270 )
271 continue
272 # Prepare target key
273 if target_key not in data:
274 data[target_key] = None
275 # Save data
Alexe9908f72020-05-19 16:04:53 -0500276 if data['status'] in [NODE_DOWN, NODE_SKIP]:
Alex836fac82019-08-22 13:36:16 -0500277 data[target_key] = None
Alex1839bbf2019-08-22 17:17:21 -0500278 elif node not in _result:
279 continue
Alex836fac82019-08-22 13:36:16 -0500280 elif not _result[node]:
281 logger_cli.debug(
282 "... '{}' not responded after '{}'".format(
283 node,
Alex9a4ad212020-10-01 18:04:25 -0500284 self.env_config.salt_timeout
Alex836fac82019-08-22 13:36:16 -0500285 )
286 )
287 data[target_key] = None
288 else:
289 data[target_key] = _result[node]
290
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600291 def get_specific_pillar_for_nodes(self, pillar_path):
292 """Function gets pillars on given path for all nodes
293
294 :return: no return value, data pulished internally
295 """
Alex3ebc5632019-04-18 16:47:18 -0500296 logger_cli.debug(
Alexb151fbe2019-04-22 16:53:30 -0500297 "... collecting node pillars for '{}'".format(pillar_path)
Alex3ebc5632019-04-18 16:47:18 -0500298 )
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600299 _result = self.salt.pillar_get(self.active_nodes_compound, pillar_path)
Alex Savatieievefa79c42019-03-14 19:14:04 -0500300 self.not_responded = []
Alex3bc95f62020-03-05 17:00:04 -0600301 for node, data in self.nodes.items():
Alex Savatieievefa79c42019-03-14 19:14:04 -0500302 if node in self.skip_list:
303 logger_cli.debug(
304 "... '{}' skipped while collecting '{}'".format(
305 node,
306 pillar_path
307 )
308 )
309 continue
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600310 _pillar_keys = pillar_path.split(':')
311 _data = data['pillars']
312 # pre-create nested dict
313 for idx in range(0, len(_pillar_keys)-1):
314 _key = _pillar_keys[idx]
315 if _key not in _data:
316 _data[_key] = {}
317 _data = _data[_key]
Alexe9908f72020-05-19 16:04:53 -0500318 if data['status'] in [NODE_DOWN, NODE_SKIP]:
Alex Savatieievefa79c42019-03-14 19:14:04 -0500319 _data[_pillar_keys[-1]] = None
320 elif not _result[node]:
321 logger_cli.debug(
322 "... '{}' not responded after '{}'".format(
323 node,
Alex9a4ad212020-10-01 18:04:25 -0500324 self.env_config.salt_timeout
Alex Savatieievefa79c42019-03-14 19:14:04 -0500325 )
326 )
327 _data[_pillar_keys[-1]] = None
328 self.not_responded.append(node)
329 else:
330 _data[_pillar_keys[-1]] = _result[node]
Alex3ebc5632019-04-18 16:47:18 -0500331
Alexe0c5b9e2019-04-23 18:51:23 -0500332 def prepare_json_on_node(self, node, _dict, filename):
333 # this function assumes that all folders are created
334 _dumps = json.dumps(_dict, indent=2).splitlines()
335 _storage_path = os.path.join(
Alex9a4ad212020-10-01 18:04:25 -0500336 self.env_config.salt_file_root, self.env_config.salt_scripts_folder
Alexe0c5b9e2019-04-23 18:51:23 -0500337 )
338 logger_cli.debug(
339 "... uploading data as '{}' "
340 "to master's file cache folder: '{}'".format(
341 filename,
342 _storage_path
343 )
344 )
345 _cache_path = os.path.join(_storage_path, filename)
346 _source_path = os.path.join(
347 'salt://',
Alex9a4ad212020-10-01 18:04:25 -0500348 self.env_config.salt_scripts_folder,
Alexe0c5b9e2019-04-23 18:51:23 -0500349 filename
350 )
351 _target_path = os.path.join(
352 '/root',
Alex9a4ad212020-10-01 18:04:25 -0500353 self.env_config.salt_scripts_folder,
Alexe0c5b9e2019-04-23 18:51:23 -0500354 filename
355 )
356
357 logger_cli.debug("... creating file in cache '{}'".format(_cache_path))
358 self.salt.f_touch_master(_cache_path)
359 self.salt.f_append_master(_cache_path, _dumps)
360 logger.debug("... syncing file to '{}'".format(node))
361 self.salt.get_file(
362 node,
363 _source_path,
364 _target_path,
365 tgt_type="compound"
366 )
367 return _target_path
368
369 def prepare_script_on_active_nodes(self, script_filename):
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600370 # Prepare script
371 _p = os.path.join(pkg_dir, 'scripts', script_filename)
372 with open(_p, 'rt') as fd:
373 _script = fd.read().splitlines()
374 _storage_path = os.path.join(
Alex9a4ad212020-10-01 18:04:25 -0500375 self.env_config.salt_file_root, self.env_config.salt_scripts_folder
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600376 )
377 logger_cli.debug(
Alexb151fbe2019-04-22 16:53:30 -0500378 "... uploading script {} "
Alex3ebc5632019-04-18 16:47:18 -0500379 "to master's file cache folder: '{}'".format(
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600380 script_filename,
381 _storage_path
382 )
383 )
Alexe0c5b9e2019-04-23 18:51:23 -0500384 self.salt.mkdir(self.salt.master_node, _storage_path)
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600385 # Form cache, source and target path
386 _cache_path = os.path.join(_storage_path, script_filename)
387 _source_path = os.path.join(
388 'salt://',
Alex9a4ad212020-10-01 18:04:25 -0500389 self.env_config.salt_scripts_folder,
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600390 script_filename
391 )
392 _target_path = os.path.join(
393 '/root',
Alex9a4ad212020-10-01 18:04:25 -0500394 self.env_config.salt_scripts_folder,
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600395 script_filename
396 )
397
Alexb151fbe2019-04-22 16:53:30 -0500398 logger_cli.debug("... creating file in cache '{}'".format(_cache_path))
Alex3ebc5632019-04-18 16:47:18 -0500399 self.salt.f_touch_master(_cache_path)
400 self.salt.f_append_master(_cache_path, _script)
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600401 # command salt to copy file to minions
Alex3ebc5632019-04-18 16:47:18 -0500402 logger_cli.debug(
Alexb151fbe2019-04-22 16:53:30 -0500403 "... creating script target folder '{}'".format(
Alex3ebc5632019-04-18 16:47:18 -0500404 _cache_path
405 )
406 )
407 self.salt.mkdir(
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600408 self.active_nodes_compound,
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600409 os.path.join(
410 '/root',
Alex9a4ad212020-10-01 18:04:25 -0500411 self.env_config.salt_scripts_folder
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600412 ),
413 tgt_type="compound"
414 )
Alex3ebc5632019-04-18 16:47:18 -0500415 logger.debug("... syncing file to nodes")
416 self.salt.get_file(
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600417 self.active_nodes_compound,
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600418 _source_path,
419 _target_path,
420 tgt_type="compound"
421 )
Alexe0c5b9e2019-04-23 18:51:23 -0500422 # return path on nodes, just in case
423 return _target_path
424
425 def execute_script_on_node(self, node, script_filename, args=[]):
426 # Prepare path
427 _target_path = os.path.join(
428 '/root',
Alex9a4ad212020-10-01 18:04:25 -0500429 self.env_config.salt_scripts_folder,
Alexe0c5b9e2019-04-23 18:51:23 -0500430 script_filename
431 )
432
433 # execute script
434 logger.debug("... running script on '{}'".format(node))
435 # handle results for each node
436 _script_arguments = " ".join(args) if args else ""
437 self.not_responded = []
438 _r = self.salt.cmd(
439 node,
440 'cmd.run',
441 param='python {} {}'.format(_target_path, _script_arguments),
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
449 def execute_script_on_active_nodes(self, script_filename, args=[]):
450 # Prepare path
451 _target_path = os.path.join(
452 '/root',
Alex9a4ad212020-10-01 18:04:25 -0500453 self.env_config.salt_scripts_folder,
Alexe0c5b9e2019-04-23 18:51:23 -0500454 script_filename
455 )
456
457 # execute script
Alexd0391d42019-05-21 18:48:55 -0500458 logger_cli.debug("... running script")
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600459 # handle results for each node
460 _script_arguments = " ".join(args) if args else ""
Alex Savatieievefa79c42019-03-14 19:14:04 -0500461 self.not_responded = []
462 _r = self.salt.cmd(
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600463 self.active_nodes_compound,
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600464 'cmd.run',
465 param='python {} {}'.format(_target_path, _script_arguments),
466 expr_form="compound"
467 )
468
Alex Savatieievefa79c42019-03-14 19:14:04 -0500469 # all false returns means that there is no response
Alex3ebc5632019-04-18 16:47:18 -0500470 self.not_responded = [_n for _n in _r.keys() if not _r[_n]]
Alex Savatieievefa79c42019-03-14 19:14:04 -0500471 return _r
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600472
Alex1839bbf2019-08-22 17:17:21 -0500473 def execute_cmd_on_active_nodes(self, cmd, nodes=None):
Alex836fac82019-08-22 13:36:16 -0500474 # execute cmd
475 self.not_responded = []
476 _r = self.salt.cmd(
Alex1839bbf2019-08-22 17:17:21 -0500477 nodes if nodes else self.active_nodes_compound,
Alex836fac82019-08-22 13:36:16 -0500478 'cmd.run',
479 param=cmd,
480 expr_form="compound"
481 )
482
483 # all false returns means that there is no response
484 self.not_responded = [_n for _n in _r.keys() if not _r[_n]]
485 return _r
486
Alex9a4ad212020-10-01 18:04:25 -0500487
488class KubeNodes(Nodes):
489 def __init__(self, config):
490 super(KubeNodes, self).__init__(config)
491 logger_cli.info("# Gathering environment information")
492 # simple salt rest client
493 self.kube = get_kube_remote(self.env_config)
494 self.env_type = ENV_TYPE_KUBE
495
496 def gather_node_info(self, skip_list, skip_list_file):
497 # Gather nodes info and query pod lists for each node
498 logger_cli.debug("... collecting node names existing in the cloud")
499
500 # Gather node names and info
501 _nodes = self.kube.get_node_info()
502 _node_names = list(_nodes.keys())
503 # Skip nodes if needed
504 _skipped_nodes = \
505 _prepare_skipped_nodes(_node_names, skip_list, skip_list_file)
506
507 # Count how many nodes active
508 self._active = [n for n, v in _nodes.items()
509 if v['conditions']['ready']['status']]
510
511 # iterate through all accepted nodes and create a dict for it
512 self.nodes = {}
513 self.skip_list = []
514 # _domains = set()
515 for _name in _node_names:
516 if _name in _skipped_nodes:
517 _status = NODE_SKIP
518 self.skip_list.append(_name)
519 else:
520 _status = NODE_UP if _name in self._active else NODE_DOWN
521 if _status == NODE_DOWN:
522 self.skip_list.append(_name)
523 logger_cli.info(
524 "-> '{}' shows 'Ready' as 'False', "
525 "added to skip list".format(
526 _name
527 )
528 )
529 _roles = {}
530 _labels = {}
531 for _label, _value in _nodes[_name]['labels'].items():
532 if _label in all_kube_roles_map:
533 _roles[all_kube_roles_map[_label]] = _value
534 else:
535 _labels[_label] = _value
536
537 self.nodes[_name] = deepcopy(node_tmpl)
538 self.nodes[_name].pop("grains")
539 self.nodes[_name].pop("pillars")
540
541 # hostname
542 self.nodes[_name]['shortname'] = \
543 _nodes[_name]['addresses']['hostname']['address']
544 self.nodes[_name]['internalip'] = \
545 _nodes[_name]['addresses']['internalip']['address']
546 # _domains.add(_name.split(".", 1)[1])
547 self.nodes[_name]['node_group'] = None
548 self.nodes[_name]['labels'] = _labels
549 self.nodes[_name]['roles'] = _roles
550 self.nodes[_name]['status'] = _status
551 # Backward compatibility
552 _info = _nodes[_name]['status']['node_info']
553 self.nodes[_name]['linux_image'] = _info['os_image']
554 self.nodes[_name]['linux_arch'] = _info['architecture']
555
556 _codename = "unknown"
557 _n, _v, _c = _info['os_image'].split()
558 if _n.lower() == 'ubuntu':
559 _v, _, _ = _v.rpartition('.') if '.' in _v else (_v, "", "")
560 if _v in ubuntu_versions:
561 _codename = ubuntu_versions[_v].split()[0].lower()
562 self.nodes[_name]['linux_codename'] = _codename
563
564 # Consider per-data type transfer
565 self.nodes[_name]["raw"] = _nodes[_name]
566 # TODO: Investigate how to handle domains in Kube, probably - skip
567 # _domains = list(_domains)
568 # if len(_domains) > 1:
569 # logger_cli.warning(
570 # "Multiple domains detected: {}".format(",".join(_domains))
571 # )
572 # else:
573 # self.domain = _domains[0]
574 logger_cli.info(
575 "-> {} nodes collected: {} - active, {} - not active".format(
576 len(self.nodes),
577 len(self._active),
578 len(self.skip_list)
579 )
580 )
581
582 _role = "k8s-master"
583 _filtered = [n for n, v in self.nodes.items() if _role in v['roles']]
584 if len(_filtered) < 1:
585 raise KubeException(
586 "No k8s-master nodes detected! Check/Update node role map."
587 )
Alex Savatieievefa79c42019-03-14 19:14:04 -0500588 else:
Alex9a4ad212020-10-01 18:04:25 -0500589 _r = [n for n, v in self.nodes.items()
590 if v['status'] != NODE_UP and _role in v['roles']]
591 if len(_r) > 0:
592 logger_cli.warn(
593 "Master nodes are reporting 'NotReady:\n{}".format(
594 "\n".join(_r)
595 )
596 )
597 self.kube.master_node = _filtered[0]
Alexe0c5b9e2019-04-23 18:51:23 -0500598
Alex9a4ad212020-10-01 18:04:25 -0500599 # get specific data upfront
600 # OpenStack versions
601 self.mcp_release = ""
602 # Quick and Dirty way to detect OS release
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]
Alexe0c5b9e2019-04-23 18:51:23 -0500610
Alex9a4ad212020-10-01 18:04:25 -0500611 return
612
613 @staticmethod
614 def _get_ssh_shell(_h, _u, _k, _p, _q, _pipe):
615 _ssh = SshShell(
616 _h,
617 user=_u,
618 keypath=_k,
619 port=_p,
620 silent=_q,
621 piped=_pipe
622 )
623 return _ssh.connect()
624
625 @staticmethod
626 def _do_ssh_cmd(_cmd, _h, _u, _k, _p, _q, _pipe):
627 with SshShell(
628 _h,
629 user=_u,
630 keypath=_k,
631 port=_p,
632 silent=_q,
633 piped=_pipe
634 ) as ssh:
635 _r = ssh.do(_cmd)
636 logger_cli.debug("'{}'".format(_r))
637 return _r
638
639 def node_shell(
640 self,
641 node,
642 silent=True,
643 piped=True,
644 use_sudo=True,
645 fport=None
646 ):
647 _u = self.env_config.kube_node_user
648 _k = self.env_config.kube_node_keypath
649 _h = self.nodes[node]['internalip']
650 _p = 22
651 if self.kube.is_local:
652 return None, self._get_ssh_shell(_h, _u, _k, _p, silent, piped)
653 else:
654 _fh = "localhost"
655 _p = 10022 if not fport else fport
656 _pfwd = PortForward(
657 self.env_config.ssh_host,
658 _h,
659 user=_u,
660 keypath=self.env_config.ssh_key,
661 loc_port=_p
662 )
663 _pfwd.connect()
664 _ssh = self._get_ssh_shell(_fh, _u, _k, _p, silent, piped)
665 return _pfwd, _ssh
666
667 def execute_script_on_node(self, node, script_filename, args=[]):
668 # Prepare path
669 _target_path = os.path.join(
670 self.env_config.node_homepath,
671 self.env_config.kube_scripts_folder,
672 script_filename
673 )
674
675 # execute script
676 logger_cli.debug("... running script on '{}'".format(node))
677 # handle results for each node
678 _script_arguments = " ".join(args) if args else ""
679 self.not_responded = []
680 # get result
681 _nr = self.node_shell(
682 node,
683 "python {} {}".format(
684 _target_path,
685 _script_arguments
686 )
687 )
688
689 if not _nr:
690 self.not_responded.append(node)
691 return {}
692 else:
693 return {node: _nr}
694
695 def execute_cmd_on_active_nodes(self, cmd, nodes=None):
696 # execute script
697 logger_cli.debug("...running '{}' on active nodes".format(cmd))
698 # handle results for each node
699 self.not_responded = []
700 _r = {}
701 # TODO: Use threading and pool
702 for node in self._active:
703 _nr = self.node_shell(
704 node,
705 cmd
706 )
707
708 if not _nr:
709 self.not_responded.append(node)
710 else:
711 _r[node] = _nr
712
713 return _r
714
715 def _exec_script(self, params):
716 """
717 Threadsafe method to get shell to node,
718 check/copy script and get results
719 [
720 node_name,
721 src_path,
722 tgt_path,
723 conf,
724 args
725 ]
726 """
727 _name = params[0]
728 _src = params[1]
729 _tgt = params[2]
730 _conf = params[3]
731 _args = params[4]
732 _port = params[5]
733 _log_name = "["+_name+"]:"
734 _check = "echo $(if [[ -s '{}' ]]; then echo True; " \
735 "else echo False; fi)"
736 _fwd_sh, _sh = self.node_shell(
737 _name,
738 use_sudo=False,
739 fport=_port
740 )
741 # check python3
742 _python = _sh.do("which python3")
743 _python = utils.to_bool(
744 _sh.do(_check.format(_python))
745 )
746 if not _python:
747 _sh.do("apt install python3", sudo=True)
748 # check if script already there
749 _folder = os.path.join(
750 self.env_config.node_homepath,
751 _conf.kube_scripts_folder
752 )
753 # check if folder exists
754 _folder_exists = utils.to_bool(
755 _sh.do(_check.format(_folder))
756 )
757 if not _folder_exists:
758 _sh.do("mkdir " + _folder)
759 logger.info("{} Syncing file".format(_log_name))
760 _code, _r, _e = _sh.scp(
761 _src,
762 _sh.get_host_path(_tgt),
763 )
764 # handle error code
765 if _code:
766 logger_cli.warn(
767 "{} Error in scp:\n"
768 "\tstdout:'{}'\n"
769 "\tstderr:'{}'".format(_log_name, _r, _e)
770 )
771
772 # execute script
773 logger.debug("{} Running script".format(_log_name))
774 _out = _sh.do(
775 "python3 {}{}".format(
776 _tgt,
777 _args
778 ),
779 sudo=True
780 )
781
782 if _fwd_sh:
783 _fwd_sh.kill()
784 _sh.kill()
785
786 return [_name, _out]
787
788 def execute_script_on_active_nodes(self, script_filename, args=[]):
789 # Prepare script
790 _source_path = os.path.join(pkg_dir, 'scripts', script_filename)
791 _target_path = os.path.join(
792 self.env_config.node_homepath,
793 self.env_config.kube_scripts_folder,
794 script_filename
795 )
796 # handle results for each node
797 _script_arguments = " ".join(args) if args else ""
798 if _script_arguments:
799 _script_arguments = " " + _script_arguments
800 self.not_responded = []
801 _results = {}
802 logger_cli.debug(
803 "...running '{}' on active nodes, {} worker threads".format(
804 script_filename,
805 self.env_config.threads
806 )
807 )
808 # Workers pool
809 pool = Pool(self.env_config.threads)
810
811 # init the parameters
812 # node_name,
813 # src_path,
814 # tgt_path,
815 # conf,
816 # args
817 _params = []
818 _port = 10022
819 for node in self._active:
820 # build parameter blocks
821 _p_list = [
822 node,
823 _source_path,
824 _target_path,
825 self.env_config,
826 _script_arguments,
827 _port
828 ]
829 _params.append(_p_list)
830 _port += 1
831
832 _progress = Progress(len(_params))
833 results = pool.imap_unordered(self._exec_script, _params)
834
835 for ii in enumerate(results, start=1):
836 if not ii[1][1]:
837 self.not_responded.append(ii[1][0])
838 else:
839 _results[ii[1][0]] = ii[1][1]
840 _progress.write_progress(ii[0])
841
842 _progress.end()
843 pool.close()
844 pool.join()
845
846 # return path on nodes, just in case
847 return _results
848
849 def prepare_json_on_node(self, node, _dict, filename):
850 # this function assumes that all folders are created
851 _dumps = json.dumps(_dict, indent=2).splitlines()
852
853 _source_path = create_temp_file_with_content(_dumps)
854 _target_path = os.path.join(
855 self.env_config.node_homepath,
856 self.env_config.kube_scripts_folder,
857 filename
858 )
859 _folder = os.path.join(
860 self.env_config.node_homepath,
861 self.env_config.kube_scripts_folder
862 )
863 _check = "echo $(if [[ -s '{}' ]]; then echo True; " \
864 "else echo False; fi)"
865 _fwd_sh, _sh = self.node_shell(
866 node,
867 use_sudo=False
868 )
869
870 # check if folder exists
871 _folder_exists = utils.to_bool(
872 _sh.do(_check.format(_folder))
873 )
874 if not _folder_exists:
875 _sh.do("mkdir " + _folder)
876 logger_cli.debug(
877 "...create data on node '{}':'{}'".format(node, _target_path)
878 )
879 _code, _r, _e = _sh.scp(
880 _source_path,
881 _sh.get_host_path(_target_path),
882 )
883 # handle error code
884 if _code:
885 logger_cli.warn(
886 "Error in scp:\n"
887 "\tstdout:'{}'\n"
888 "\tstderr:'{}'".format(_r, _e)
889 )
890
891 _fwd_sh.kill()
892 _sh.kill()
893 return _target_path