blob: 97d806f25cac8422b08a4340dd31a492137524a3 [file] [log] [blame]
Alex0989ecf2022-03-29 13:43:21 -05001# Author: Alex Savatieiev (osavatieiev@mirantis.com; a.savex@gmail.com)
2# Copyright 2019-2022 Mirantis, Inc.
savex4448e132018-04-25 15:51:14 +02003"""
4Module to handle interaction with salt
5"""
Alex Savatieiev63576832019-02-27 15:46:26 -06006import json
savex4448e132018-04-25 15:51:14 +02007import os
savex4448e132018-04-25 15:51:14 +02008import time
9
Alex3bc95f62020-03-05 17:00:04 -060010import requests
11
Alex9a4ad212020-10-01 18:04:25 -050012from cfg_checker.common import logger, logger_cli
Alex3ebc5632019-04-18 16:47:18 -050013from cfg_checker.common.exception import InvalidReturnException, SaltException
Alex Savatieiev63576832019-02-27 15:46:26 -060014from cfg_checker.common.other import shell
Alex9a4ad212020-10-01 18:04:25 -050015from cfg_checker.common.ssh_utils import ssh_shell_p
Alex3ebc5632019-04-18 16:47:18 -050016
Alex Savatieiev63576832019-02-27 15:46:26 -060017
Alex9a4ad212020-10-01 18:04:25 -050018def _extract_salt_password(_raw):
Alex3bc95f62020-03-05 17:00:04 -060019 if not isinstance(_raw, str):
Alex Savatieiev63576832019-02-27 15:46:26 -060020 raise InvalidReturnException(_raw)
21 else:
22 try:
23 _json = json.loads(_raw)
Alex3ebc5632019-04-18 16:47:18 -050024 except ValueError:
Alex Savatieiev63576832019-02-27 15:46:26 -060025 raise SaltException(
Alex Savatieievf808cd22019-03-01 13:17:59 -060026 "# Return value is not a json: '{}'".format(_raw)
Alex Savatieiev63576832019-02-27 15:46:26 -060027 )
Alex3ebc5632019-04-18 16:47:18 -050028
Alex Savatieiev63576832019-02-27 15:46:26 -060029 return _json["local"]
30
31
Alex9a4ad212020-10-01 18:04:25 -050032def get_remote_salt_env_password(config):
Alex Savatieiev63576832019-02-27 15:46:26 -060033 """Uses ssh call with configured options to get password from salt master
34
35 :return: password string
36 """
37 _salt_cmd = "salt-call --out=json pillar.get _param:salt_api_password"
Alex9a4ad212020-10-01 18:04:25 -050038 logger_cli.debug("... calling salt using ssh: '{}'".format(_salt_cmd))
Alexd0391d42019-05-21 18:48:55 -050039 try:
Alex9a4ad212020-10-01 18:04:25 -050040 _result = ssh_shell_p(
41 _salt_cmd,
42 config.ssh_host,
43 username=config.ssh_user,
44 keypath=config.ssh_key,
45 piped=False,
Alex7b0ee9a2021-09-21 17:16:17 -050046 use_sudo=config.ssh_uses_sudo
Alex9a4ad212020-10-01 18:04:25 -050047 )
Alexd0391d42019-05-21 18:48:55 -050048 if len(_result) < 1:
49 raise InvalidReturnException(
50 "# Empty value returned for '{}".format(
Alex9a4ad212020-10-01 18:04:25 -050051 _salt_cmd
Alexd0391d42019-05-21 18:48:55 -050052 )
53 )
54 else:
Alex9a4ad212020-10-01 18:04:25 -050055 return _extract_salt_password(_result)
Alexd0391d42019-05-21 18:48:55 -050056 except OSError as e:
57 raise SaltException(
58 "Salt error calling '{}': '{}'\n"
Alex9a4ad212020-10-01 18:04:25 -050059 "\nConsider checking 'MCP_ENV' "
60 "and '<pkg>/etc/<env>.env' files".format(_salt_cmd, e.strerror)
Alexd0391d42019-05-21 18:48:55 -050061 )
Alex Savatieiev63576832019-02-27 15:46:26 -060062
Alex3ebc5632019-04-18 16:47:18 -050063
Alex9a4ad212020-10-01 18:04:25 -050064def get_salt_local_password(config):
Alex Savatieiev63576832019-02-27 15:46:26 -060065 """Calls salt locally to get password from the pillar
66
67 :return: password string
68 """
Alex3bc95f62020-03-05 17:00:04 -060069 _cmd = []
70 if config.ssh_uses_sudo:
71 _cmd = ["sudo"]
72 # salt commands
73 _cmd.append("salt-call")
74 _cmd.append("--out=json pillar.get _param:salt_api_password")
Alexd0391d42019-05-21 18:48:55 -050075 try:
Alex3bc95f62020-03-05 17:00:04 -060076 _result = shell(" ".join(_cmd))
Alexd0391d42019-05-21 18:48:55 -050077 except OSError as e:
78 raise SaltException(
79 "Salt error calling '{}': '{}'\n"
Alex9a4ad212020-10-01 18:04:25 -050080 "\nConsider checking 'MCP_ENV' "
Alexd0391d42019-05-21 18:48:55 -050081 "and '<pkg>/etc/<env>.env' files".format(_cmd, e.strerror)
82 )
Alex9a4ad212020-10-01 18:04:25 -050083 return _extract_salt_password(_result)
savex4448e132018-04-25 15:51:14 +020084
85
86def list_to_target_string(node_list, separator):
87 result = ''
88 for node in node_list:
89 result += node + ' ' + separator + ' '
90 return result[:-(len(separator)+2)]
91
92
93class SaltRest(object):
savex4448e132018-04-25 15:51:14 +020094 _auth = {}
95
96 default_headers = {
97 'Accept': 'application/json',
98 'Content-Type': 'application/json',
99 'X-Auth-Token': None
100 }
101
Alex9a4ad212020-10-01 18:04:25 -0500102 def __init__(self, config):
103 self.config = config
104
105 self._host = config.mcp_host
106 self._port = config.salt_port
107 self.uri = "http://" + config.mcp_host + ":" + config.salt_port
108
savex4448e132018-04-25 15:51:14 +0200109 self._token = self._login()
110 self.last_response = None
111
Alex3ebc5632019-04-18 16:47:18 -0500112 def get(
113 self,
114 path='',
115 headers=default_headers,
116 cookies=None,
117 timeout=None
118 ):
savex4448e132018-04-25 15:51:14 +0200119 _path = os.path.join(self.uri, path)
Alex Savatieievf808cd22019-03-01 13:17:59 -0600120 logger.debug("# GET '{}'\nHeaders: '{}'\nCookies: {}".format(
savex4448e132018-04-25 15:51:14 +0200121 _path,
122 headers,
123 cookies
124 ))
125 return requests.get(
126 _path,
127 headers=headers,
Alex Savatieievefa79c42019-03-14 19:14:04 -0500128 cookies=cookies,
129 timeout=timeout
savex4448e132018-04-25 15:51:14 +0200130 )
131
132 def post(self, data, path='', headers=default_headers, cookies=None):
133 if data is None:
134 data = {}
135 _path = os.path.join(self.uri, path)
136 if path == 'login':
Alex Savatieiev63576832019-02-27 15:46:26 -0600137 _data = str(data).replace(self._pass, "*****")
savex4448e132018-04-25 15:51:14 +0200138 else:
139 _data = data
Alex3ebc5632019-04-18 16:47:18 -0500140 logger.debug(
141 "# POST '{}'\nHeaders: '{}'\nCookies: {}\nBody: {}".format(
142 _path,
143 headers,
144 cookies,
145 _data
146 )
147 )
savex4448e132018-04-25 15:51:14 +0200148 return requests.post(
149 os.path.join(self.uri, path),
150 headers=headers,
151 json=data,
152 cookies=cookies
153 )
154
155 def _login(self):
Alex Savatieiev63576832019-02-27 15:46:26 -0600156 # if there is no password - try to get local, if this available
Alex9a4ad212020-10-01 18:04:25 -0500157 if self.config.env_name == "local":
158 _pass = get_salt_local_password(self.config)
Alex Savatieiev63576832019-02-27 15:46:26 -0600159 else:
Alex9a4ad212020-10-01 18:04:25 -0500160 _pass = get_remote_salt_env_password(self.config)
savex4448e132018-04-25 15:51:14 +0200161 login_payload = {
Alex9a4ad212020-10-01 18:04:25 -0500162 'username': self.config.salt_user,
Alex Savatieiev63576832019-02-27 15:46:26 -0600163 'password': _pass,
savex4448e132018-04-25 15:51:14 +0200164 'eauth': 'pam'
165 }
Alex Savatieiev63576832019-02-27 15:46:26 -0600166 self._pass = _pass
Alex Savatieievf808cd22019-03-01 13:17:59 -0600167 logger.debug("# Logging in to salt master...")
savex4448e132018-04-25 15:51:14 +0200168 _response = self.post(login_payload, path='login')
169
170 if _response.ok:
171 self._auth['response'] = _response.json()['return'][0]
172 self._auth['cookies'] = _response.cookies
173 self.default_headers['X-Auth-Token'] = \
174 self._auth['response']['token']
175 return self._auth['response']['token']
176 else:
177 raise EnvironmentError(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600178 "# HTTP:{}, Not authorized?".format(_response.status_code)
savex4448e132018-04-25 15:51:14 +0200179 )
180
181 def salt_request(self, fn, *args, **kwargs):
182 # if token will expire in 5 min, re-login
183 if self._auth['response']['expire'] < time.time() + 300:
184 self._auth['response']['X-Auth-Token'] = self._login()
185
186 _method = getattr(self, fn)
187 _response = _method(*args, **kwargs)
188 self.last_response = _response
189 _content = "..."
190 _len = len(_response.content)
191 if _len < 1024:
192 _content = _response.content
193 logger.debug(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600194 "# Response (HTTP {}/{}), {}: {}".format(
savex4448e132018-04-25 15:51:14 +0200195 _response.status_code,
196 _response.reason,
197 _len,
198 _content
199 )
200 )
201 if _response.ok:
202 return _response.json()['return']
203 else:
204 raise EnvironmentError(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600205 "# Salt Error: HTTP:{}, '{}'".format(
savex4448e132018-04-25 15:51:14 +0200206 _response.status_code,
207 _response.reason
208 )
209 )
210
211
212class SaltRemote(SaltRest):
Alexe0c5b9e2019-04-23 18:51:23 -0500213 master_node = ""
214
Alex9a4ad212020-10-01 18:04:25 -0500215 def __init__(self, config):
216 super(SaltRemote, self).__init__(config)
savex4448e132018-04-25 15:51:14 +0200217
218 def cmd(
219 self,
220 tgt,
221 fun,
222 param=None,
223 client='local',
224 kwarg=None,
225 expr_form=None,
226 tgt_type=None,
227 timeout=None
228 ):
Alex9a4ad212020-10-01 18:04:25 -0500229 _timeout = timeout if timeout is not None else self.config.salt_timeout
savex4448e132018-04-25 15:51:14 +0200230 _payload = {
231 'fun': fun,
232 'tgt': tgt,
233 'client': client,
234 'timeout': _timeout
235 }
236
237 if expr_form:
238 _payload['expr_form'] = expr_form
239 if tgt_type:
240 _payload['tgt_type'] = tgt_type
241 if param:
242 _payload['arg'] = param
243 if kwarg:
244 _payload['kwarg'] = kwarg
Alexac2a2732020-09-11 11:00:26 -0500245 logger_cli.debug("SaltRequest: POST '{}'".format(_payload))
savex4448e132018-04-25 15:51:14 +0200246 _response = self.salt_request('post', [_payload])
247 if isinstance(_response, list):
248 return _response[0]
249 else:
250 raise EnvironmentError(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600251 "# Unexpected response from from salt-api/LocalClient: "
savex4448e132018-04-25 15:51:14 +0200252 "{}".format(_response)
253 )
254
255 def run(self, fun, kwarg=None):
256 _payload = {
257 'client': 'runner',
258 'fun': fun,
Alex9a4ad212020-10-01 18:04:25 -0500259 'timeout': self.config.salt_timeout
savex4448e132018-04-25 15:51:14 +0200260 }
261
262 if kwarg:
263 _payload['kwarg'] = kwarg
264
265 _response = self.salt_request('post', [_payload])
266 if isinstance(_response, list):
267 return _response[0]
268 else:
269 raise EnvironmentError(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600270 "# Unexpected response from from salt-api/RunnerClient: "
savex4448e132018-04-25 15:51:14 +0200271 "{}".format(_response)
272 )
273
274 def wheel(self, fun, arg=None, kwarg=None):
275 _payload = {
276 'client': 'wheel',
277 'fun': fun,
Alex9a4ad212020-10-01 18:04:25 -0500278 'timeout': self.config.salt_timeout
savex4448e132018-04-25 15:51:14 +0200279 }
280
281 if arg:
282 _payload['arg'] = arg
283 if kwarg:
284 _payload['kwarg'] = kwarg
285
286 _response = self.salt_request('post', _payload)['data']
287 if _response['success']:
288 return _response
289 else:
290 raise EnvironmentError(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600291 "# Salt Error: '{}'".format(_response['return']))
savex4448e132018-04-25 15:51:14 +0200292
293 def pillar_request(self, node_target, pillar_submodule, argument):
294 # example cli: 'salt "ctl01*" pillar.keys rsyslog'
295 _type = "compound"
296 if isinstance(node_target, list):
297 _type = "list"
298 return self.cmd(
299 node_target,
300 "pillar." + pillar_submodule,
301 argument,
302 expr_form=_type
303 )
304
305 def pillar_keys(self, node_target, argument):
306 return self.pillar_request(node_target, 'keys', argument)
307
308 def pillar_get(self, node_target, argument):
309 return self.pillar_request(node_target, 'get', argument)
310
311 def pillar_data(self, node_target, argument):
312 return self.pillar_request(node_target, 'data', argument)
313
314 def pillar_raw(self, node_target, argument):
315 return self.pillar_request(node_target, 'raw', argument)
316
317 def list_minions(self):
318 """
319 Fails in salt version 2016.3.8
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600320 Works starting from 2017.7.7
savex4448e132018-04-25 15:51:14 +0200321 api returns dict of minions with grains
322 """
Alex Savatieievefa79c42019-03-14 19:14:04 -0500323 try:
324 _r = self.salt_request('get', 'minions', timeout=10)
Alex3ebc5632019-04-18 16:47:18 -0500325 except requests.exceptions.ReadTimeout:
Alex Savatieievefa79c42019-03-14 19:14:04 -0500326 logger_cli.debug("... timeout waiting list minions from Salt API")
327 _r = None
328 return _r[0] if _r else None
savex4448e132018-04-25 15:51:14 +0200329
330 def list_keys(self):
331 """
332 Fails in salt version 2016.3.8
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600333 Works starting from 2017.7.7
savex4448e132018-04-25 15:51:14 +0200334 api should return dict:
335 {
336 'local': [],
337 'minions': [],
338 'minions_denied': [],
339 'minions_pre': [],
340 'minions_rejected': [],
341 }
342 """
343 return self.salt_request('get', path='keys')
344
345 def get_status(self):
346 """
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600347 Fails in salt version 2017.7.7
savex4448e132018-04-25 15:51:14 +0200348 'runner' client is the equivalent of 'salt-run'
349 Returns the
350 """
351 return self.run(
352 'manage.status',
353 kwarg={'timeout': 10}
354 )
355
356 def get_active_nodes(self):
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600357 """Used when other minion list metods fail
Alex3ebc5632019-04-18 16:47:18 -0500358
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600359 :return: json result from salt test.ping
360 """
Alex9a4ad212020-10-01 18:04:25 -0500361 if self.config.skip_nodes:
362 logger.info(
363 "# Nodes to be skipped: {0}".format(self.config.skip_nodes)
364 )
Alex Savatieievefa79c42019-03-14 19:14:04 -0500365 _r = self.cmd(
savex4448e132018-04-25 15:51:14 +0200366 '* and not ' + list_to_target_string(
Alex9a4ad212020-10-01 18:04:25 -0500367 self.config.skip_nodes,
savex4448e132018-04-25 15:51:14 +0200368 'and not'
369 ),
370 'test.ping',
371 expr_form='compound')
372 else:
Alex Savatieievefa79c42019-03-14 19:14:04 -0500373 _r = self.cmd('*', 'test.ping')
Alex3ebc5632019-04-18 16:47:18 -0500374 # Return all nodes that responded
Alex Savatieievefa79c42019-03-14 19:14:04 -0500375 return [node for node in _r.keys() if _r[node]]
savex4448e132018-04-25 15:51:14 +0200376
377 def get_monitoring_ip(self, param_name):
378 salt_output = self.cmd(
379 'docker:client:stack:monitoring',
380 'pillar.get',
381 param=param_name,
382 expr_form='pillar')
383 return salt_output[salt_output.keys()[0]]
384
385 def f_touch_master(self, path, makedirs=True):
386 _kwarg = {
387 "makedirs": makedirs
388 }
389 salt_output = self.cmd(
Alexe0c5b9e2019-04-23 18:51:23 -0500390 self.master_node,
savex4448e132018-04-25 15:51:14 +0200391 "file.touch",
392 param=path,
393 kwarg=_kwarg
394 )
Alex3bc95f62020-03-05 17:00:04 -0600395 return [*salt_output.values()][0]
savex4448e132018-04-25 15:51:14 +0200396
397 def f_append_master(self, path, strings_list, makedirs=True):
398 _kwarg = {
399 "makedirs": makedirs
400 }
401 _args = [path]
402 _args.extend(strings_list)
403 salt_output = self.cmd(
Alexe0c5b9e2019-04-23 18:51:23 -0500404 self.master_node,
savex4448e132018-04-25 15:51:14 +0200405 "file.write",
406 param=_args,
407 kwarg=_kwarg
408 )
Alex3bc95f62020-03-05 17:00:04 -0600409 return [*salt_output.values()][0]
savex4448e132018-04-25 15:51:14 +0200410
411 def mkdir(self, target, path, tgt_type=None):
412 salt_output = self.cmd(
413 target,
414 "file.mkdir",
415 param=path,
416 expr_form=tgt_type
417 )
418 return salt_output
419
420 def f_manage_file(self, target_path, source,
421 sfn='', ret='{}',
422 source_hash={},
423 user='root', group='root', backup_mode='755',
424 show_diff='base',
425 contents='', makedirs=True):
426 """
427 REST variation of file.get_managed
428 CLI execution goes like this (10 agrs):
Alex3ebc5632019-04-18 16:47:18 -0500429 salt cfg01\\* file.manage_file /root/test_scripts/pkg_versions.py
savex4448e132018-04-25 15:51:14 +0200430 '' '{}' /root/diff_pkg_version.py
431 '{hash_type: 'md5', 'hsum': <md5sum>}' root root '755' base ''
432 makedirs=True
433 param: name - target file placement when managed
434 param: source - source for the file
435 """
436 _source_hash = {
437 "hash_type": "md5",
438 "hsum": 000
439 }
440 _arg = [
441 target_path,
442 sfn,
443 ret,
444 source,
445 _source_hash,
446 user,
447 group,
448 backup_mode,
449 show_diff,
450 contents
451 ]
452 _kwarg = {
453 "makedirs": makedirs
454 }
455 salt_output = self.cmd(
Alexe0c5b9e2019-04-23 18:51:23 -0500456 self.master_node,
savex4448e132018-04-25 15:51:14 +0200457 "file.manage_file",
458 param=_arg,
459 kwarg=_kwarg
460 )
Alex3bc95f62020-03-05 17:00:04 -0600461 return [*salt_output.values()][0]
savex4448e132018-04-25 15:51:14 +0200462
463 def cache_file(self, target, source_path):
464 salt_output = self.cmd(
465 target,
466 "cp.cache_file",
467 param=source_path
468 )
Alex3bc95f62020-03-05 17:00:04 -0600469 return [*salt_output.values()][0]
savex4448e132018-04-25 15:51:14 +0200470
471 def get_file(self, target, source_path, target_path, tgt_type=None):
472 return self.cmd(
473 target,
474 "cp.get_file",
475 param=[source_path, target_path],
476 expr_form=tgt_type
477 )
478
479 @staticmethod
480 def compound_string_from_list(nodes_list):
481 return " or ".join(nodes_list)