blob: 08f2d2b47a4cea68ebdac599530323a4c0a22d2d [file] [log] [blame]
savex4448e132018-04-25 15:51:14 +02001"""
2Module to handle interaction with salt
3"""
Alex Savatieiev63576832019-02-27 15:46:26 -06004import json
savex4448e132018-04-25 15:51:14 +02005import os
savex4448e132018-04-25 15:51:14 +02006import time
7
Alex3bc95f62020-03-05 17:00:04 -06008import requests
9
Alex9a4ad212020-10-01 18:04:25 -050010from cfg_checker.common import logger, logger_cli
Alex3ebc5632019-04-18 16:47:18 -050011from cfg_checker.common.exception import InvalidReturnException, SaltException
Alex Savatieiev63576832019-02-27 15:46:26 -060012from cfg_checker.common.other import shell
Alex9a4ad212020-10-01 18:04:25 -050013from cfg_checker.common.ssh_utils import ssh_shell_p
Alex3ebc5632019-04-18 16:47:18 -050014
Alex Savatieiev63576832019-02-27 15:46:26 -060015
Alex9a4ad212020-10-01 18:04:25 -050016def _extract_salt_password(_raw):
Alex3bc95f62020-03-05 17:00:04 -060017 if not isinstance(_raw, str):
Alex Savatieiev63576832019-02-27 15:46:26 -060018 raise InvalidReturnException(_raw)
19 else:
20 try:
21 _json = json.loads(_raw)
Alex3ebc5632019-04-18 16:47:18 -050022 except ValueError:
Alex Savatieiev63576832019-02-27 15:46:26 -060023 raise SaltException(
Alex Savatieievf808cd22019-03-01 13:17:59 -060024 "# Return value is not a json: '{}'".format(_raw)
Alex Savatieiev63576832019-02-27 15:46:26 -060025 )
Alex3ebc5632019-04-18 16:47:18 -050026
Alex Savatieiev63576832019-02-27 15:46:26 -060027 return _json["local"]
28
29
Alex9a4ad212020-10-01 18:04:25 -050030def get_remote_salt_env_password(config):
Alex Savatieiev63576832019-02-27 15:46:26 -060031 """Uses ssh call with configured options to get password from salt master
32
33 :return: password string
34 """
35 _salt_cmd = "salt-call --out=json pillar.get _param:salt_api_password"
Alex9a4ad212020-10-01 18:04:25 -050036 logger_cli.debug("... calling salt using ssh: '{}'".format(_salt_cmd))
Alexd0391d42019-05-21 18:48:55 -050037 try:
Alex9a4ad212020-10-01 18:04:25 -050038 _result = ssh_shell_p(
39 _salt_cmd,
40 config.ssh_host,
41 username=config.ssh_user,
42 keypath=config.ssh_key,
43 piped=False,
Alex7b0ee9a2021-09-21 17:16:17 -050044 use_sudo=config.ssh_uses_sudo
Alex9a4ad212020-10-01 18:04:25 -050045 )
Alexd0391d42019-05-21 18:48:55 -050046 if len(_result) < 1:
47 raise InvalidReturnException(
48 "# Empty value returned for '{}".format(
Alex9a4ad212020-10-01 18:04:25 -050049 _salt_cmd
Alexd0391d42019-05-21 18:48:55 -050050 )
51 )
52 else:
Alex9a4ad212020-10-01 18:04:25 -050053 return _extract_salt_password(_result)
Alexd0391d42019-05-21 18:48:55 -050054 except OSError as e:
55 raise SaltException(
56 "Salt error calling '{}': '{}'\n"
Alex9a4ad212020-10-01 18:04:25 -050057 "\nConsider checking 'MCP_ENV' "
58 "and '<pkg>/etc/<env>.env' files".format(_salt_cmd, e.strerror)
Alexd0391d42019-05-21 18:48:55 -050059 )
Alex Savatieiev63576832019-02-27 15:46:26 -060060
Alex3ebc5632019-04-18 16:47:18 -050061
Alex9a4ad212020-10-01 18:04:25 -050062def get_salt_local_password(config):
Alex Savatieiev63576832019-02-27 15:46:26 -060063 """Calls salt locally to get password from the pillar
64
65 :return: password string
66 """
Alex3bc95f62020-03-05 17:00:04 -060067 _cmd = []
68 if config.ssh_uses_sudo:
69 _cmd = ["sudo"]
70 # salt commands
71 _cmd.append("salt-call")
72 _cmd.append("--out=json pillar.get _param:salt_api_password")
Alexd0391d42019-05-21 18:48:55 -050073 try:
Alex3bc95f62020-03-05 17:00:04 -060074 _result = shell(" ".join(_cmd))
Alexd0391d42019-05-21 18:48:55 -050075 except OSError as e:
76 raise SaltException(
77 "Salt error calling '{}': '{}'\n"
Alex9a4ad212020-10-01 18:04:25 -050078 "\nConsider checking 'MCP_ENV' "
Alexd0391d42019-05-21 18:48:55 -050079 "and '<pkg>/etc/<env>.env' files".format(_cmd, e.strerror)
80 )
Alex9a4ad212020-10-01 18:04:25 -050081 return _extract_salt_password(_result)
savex4448e132018-04-25 15:51:14 +020082
83
84def list_to_target_string(node_list, separator):
85 result = ''
86 for node in node_list:
87 result += node + ' ' + separator + ' '
88 return result[:-(len(separator)+2)]
89
90
91class SaltRest(object):
savex4448e132018-04-25 15:51:14 +020092 _auth = {}
93
94 default_headers = {
95 'Accept': 'application/json',
96 'Content-Type': 'application/json',
97 'X-Auth-Token': None
98 }
99
Alex9a4ad212020-10-01 18:04:25 -0500100 def __init__(self, config):
101 self.config = config
102
103 self._host = config.mcp_host
104 self._port = config.salt_port
105 self.uri = "http://" + config.mcp_host + ":" + config.salt_port
106
savex4448e132018-04-25 15:51:14 +0200107 self._token = self._login()
108 self.last_response = None
109
Alex3ebc5632019-04-18 16:47:18 -0500110 def get(
111 self,
112 path='',
113 headers=default_headers,
114 cookies=None,
115 timeout=None
116 ):
savex4448e132018-04-25 15:51:14 +0200117 _path = os.path.join(self.uri, path)
Alex Savatieievf808cd22019-03-01 13:17:59 -0600118 logger.debug("# GET '{}'\nHeaders: '{}'\nCookies: {}".format(
savex4448e132018-04-25 15:51:14 +0200119 _path,
120 headers,
121 cookies
122 ))
123 return requests.get(
124 _path,
125 headers=headers,
Alex Savatieievefa79c42019-03-14 19:14:04 -0500126 cookies=cookies,
127 timeout=timeout
savex4448e132018-04-25 15:51:14 +0200128 )
129
130 def post(self, data, path='', headers=default_headers, cookies=None):
131 if data is None:
132 data = {}
133 _path = os.path.join(self.uri, path)
134 if path == 'login':
Alex Savatieiev63576832019-02-27 15:46:26 -0600135 _data = str(data).replace(self._pass, "*****")
savex4448e132018-04-25 15:51:14 +0200136 else:
137 _data = data
Alex3ebc5632019-04-18 16:47:18 -0500138 logger.debug(
139 "# POST '{}'\nHeaders: '{}'\nCookies: {}\nBody: {}".format(
140 _path,
141 headers,
142 cookies,
143 _data
144 )
145 )
savex4448e132018-04-25 15:51:14 +0200146 return requests.post(
147 os.path.join(self.uri, path),
148 headers=headers,
149 json=data,
150 cookies=cookies
151 )
152
153 def _login(self):
Alex Savatieiev63576832019-02-27 15:46:26 -0600154 # if there is no password - try to get local, if this available
Alex9a4ad212020-10-01 18:04:25 -0500155 if self.config.env_name == "local":
156 _pass = get_salt_local_password(self.config)
Alex Savatieiev63576832019-02-27 15:46:26 -0600157 else:
Alex9a4ad212020-10-01 18:04:25 -0500158 _pass = get_remote_salt_env_password(self.config)
savex4448e132018-04-25 15:51:14 +0200159 login_payload = {
Alex9a4ad212020-10-01 18:04:25 -0500160 'username': self.config.salt_user,
Alex Savatieiev63576832019-02-27 15:46:26 -0600161 'password': _pass,
savex4448e132018-04-25 15:51:14 +0200162 'eauth': 'pam'
163 }
Alex Savatieiev63576832019-02-27 15:46:26 -0600164 self._pass = _pass
Alex Savatieievf808cd22019-03-01 13:17:59 -0600165 logger.debug("# Logging in to salt master...")
savex4448e132018-04-25 15:51:14 +0200166 _response = self.post(login_payload, path='login')
167
168 if _response.ok:
169 self._auth['response'] = _response.json()['return'][0]
170 self._auth['cookies'] = _response.cookies
171 self.default_headers['X-Auth-Token'] = \
172 self._auth['response']['token']
173 return self._auth['response']['token']
174 else:
175 raise EnvironmentError(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600176 "# HTTP:{}, Not authorized?".format(_response.status_code)
savex4448e132018-04-25 15:51:14 +0200177 )
178
179 def salt_request(self, fn, *args, **kwargs):
180 # if token will expire in 5 min, re-login
181 if self._auth['response']['expire'] < time.time() + 300:
182 self._auth['response']['X-Auth-Token'] = self._login()
183
184 _method = getattr(self, fn)
185 _response = _method(*args, **kwargs)
186 self.last_response = _response
187 _content = "..."
188 _len = len(_response.content)
189 if _len < 1024:
190 _content = _response.content
191 logger.debug(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600192 "# Response (HTTP {}/{}), {}: {}".format(
savex4448e132018-04-25 15:51:14 +0200193 _response.status_code,
194 _response.reason,
195 _len,
196 _content
197 )
198 )
199 if _response.ok:
200 return _response.json()['return']
201 else:
202 raise EnvironmentError(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600203 "# Salt Error: HTTP:{}, '{}'".format(
savex4448e132018-04-25 15:51:14 +0200204 _response.status_code,
205 _response.reason
206 )
207 )
208
209
210class SaltRemote(SaltRest):
Alexe0c5b9e2019-04-23 18:51:23 -0500211 master_node = ""
212
Alex9a4ad212020-10-01 18:04:25 -0500213 def __init__(self, config):
214 super(SaltRemote, self).__init__(config)
savex4448e132018-04-25 15:51:14 +0200215
216 def cmd(
217 self,
218 tgt,
219 fun,
220 param=None,
221 client='local',
222 kwarg=None,
223 expr_form=None,
224 tgt_type=None,
225 timeout=None
226 ):
Alex9a4ad212020-10-01 18:04:25 -0500227 _timeout = timeout if timeout is not None else self.config.salt_timeout
savex4448e132018-04-25 15:51:14 +0200228 _payload = {
229 'fun': fun,
230 'tgt': tgt,
231 'client': client,
232 'timeout': _timeout
233 }
234
235 if expr_form:
236 _payload['expr_form'] = expr_form
237 if tgt_type:
238 _payload['tgt_type'] = tgt_type
239 if param:
240 _payload['arg'] = param
241 if kwarg:
242 _payload['kwarg'] = kwarg
Alexac2a2732020-09-11 11:00:26 -0500243 logger_cli.debug("SaltRequest: POST '{}'".format(_payload))
savex4448e132018-04-25 15:51:14 +0200244 _response = self.salt_request('post', [_payload])
245 if isinstance(_response, list):
246 return _response[0]
247 else:
248 raise EnvironmentError(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600249 "# Unexpected response from from salt-api/LocalClient: "
savex4448e132018-04-25 15:51:14 +0200250 "{}".format(_response)
251 )
252
253 def run(self, fun, kwarg=None):
254 _payload = {
255 'client': 'runner',
256 'fun': fun,
Alex9a4ad212020-10-01 18:04:25 -0500257 'timeout': self.config.salt_timeout
savex4448e132018-04-25 15:51:14 +0200258 }
259
260 if kwarg:
261 _payload['kwarg'] = kwarg
262
263 _response = self.salt_request('post', [_payload])
264 if isinstance(_response, list):
265 return _response[0]
266 else:
267 raise EnvironmentError(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600268 "# Unexpected response from from salt-api/RunnerClient: "
savex4448e132018-04-25 15:51:14 +0200269 "{}".format(_response)
270 )
271
272 def wheel(self, fun, arg=None, kwarg=None):
273 _payload = {
274 'client': 'wheel',
275 'fun': fun,
Alex9a4ad212020-10-01 18:04:25 -0500276 'timeout': self.config.salt_timeout
savex4448e132018-04-25 15:51:14 +0200277 }
278
279 if arg:
280 _payload['arg'] = arg
281 if kwarg:
282 _payload['kwarg'] = kwarg
283
284 _response = self.salt_request('post', _payload)['data']
285 if _response['success']:
286 return _response
287 else:
288 raise EnvironmentError(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600289 "# Salt Error: '{}'".format(_response['return']))
savex4448e132018-04-25 15:51:14 +0200290
291 def pillar_request(self, node_target, pillar_submodule, argument):
292 # example cli: 'salt "ctl01*" pillar.keys rsyslog'
293 _type = "compound"
294 if isinstance(node_target, list):
295 _type = "list"
296 return self.cmd(
297 node_target,
298 "pillar." + pillar_submodule,
299 argument,
300 expr_form=_type
301 )
302
303 def pillar_keys(self, node_target, argument):
304 return self.pillar_request(node_target, 'keys', argument)
305
306 def pillar_get(self, node_target, argument):
307 return self.pillar_request(node_target, 'get', argument)
308
309 def pillar_data(self, node_target, argument):
310 return self.pillar_request(node_target, 'data', argument)
311
312 def pillar_raw(self, node_target, argument):
313 return self.pillar_request(node_target, 'raw', argument)
314
315 def list_minions(self):
316 """
317 Fails in salt version 2016.3.8
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600318 Works starting from 2017.7.7
savex4448e132018-04-25 15:51:14 +0200319 api returns dict of minions with grains
320 """
Alex Savatieievefa79c42019-03-14 19:14:04 -0500321 try:
322 _r = self.salt_request('get', 'minions', timeout=10)
Alex3ebc5632019-04-18 16:47:18 -0500323 except requests.exceptions.ReadTimeout:
Alex Savatieievefa79c42019-03-14 19:14:04 -0500324 logger_cli.debug("... timeout waiting list minions from Salt API")
325 _r = None
326 return _r[0] if _r else None
savex4448e132018-04-25 15:51:14 +0200327
328 def list_keys(self):
329 """
330 Fails in salt version 2016.3.8
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600331 Works starting from 2017.7.7
savex4448e132018-04-25 15:51:14 +0200332 api should return dict:
333 {
334 'local': [],
335 'minions': [],
336 'minions_denied': [],
337 'minions_pre': [],
338 'minions_rejected': [],
339 }
340 """
341 return self.salt_request('get', path='keys')
342
343 def get_status(self):
344 """
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600345 Fails in salt version 2017.7.7
savex4448e132018-04-25 15:51:14 +0200346 'runner' client is the equivalent of 'salt-run'
347 Returns the
348 """
349 return self.run(
350 'manage.status',
351 kwarg={'timeout': 10}
352 )
353
354 def get_active_nodes(self):
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600355 """Used when other minion list metods fail
Alex3ebc5632019-04-18 16:47:18 -0500356
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600357 :return: json result from salt test.ping
358 """
Alex9a4ad212020-10-01 18:04:25 -0500359 if self.config.skip_nodes:
360 logger.info(
361 "# Nodes to be skipped: {0}".format(self.config.skip_nodes)
362 )
Alex Savatieievefa79c42019-03-14 19:14:04 -0500363 _r = self.cmd(
savex4448e132018-04-25 15:51:14 +0200364 '* and not ' + list_to_target_string(
Alex9a4ad212020-10-01 18:04:25 -0500365 self.config.skip_nodes,
savex4448e132018-04-25 15:51:14 +0200366 'and not'
367 ),
368 'test.ping',
369 expr_form='compound')
370 else:
Alex Savatieievefa79c42019-03-14 19:14:04 -0500371 _r = self.cmd('*', 'test.ping')
Alex3ebc5632019-04-18 16:47:18 -0500372 # Return all nodes that responded
Alex Savatieievefa79c42019-03-14 19:14:04 -0500373 return [node for node in _r.keys() if _r[node]]
savex4448e132018-04-25 15:51:14 +0200374
375 def get_monitoring_ip(self, param_name):
376 salt_output = self.cmd(
377 'docker:client:stack:monitoring',
378 'pillar.get',
379 param=param_name,
380 expr_form='pillar')
381 return salt_output[salt_output.keys()[0]]
382
383 def f_touch_master(self, path, makedirs=True):
384 _kwarg = {
385 "makedirs": makedirs
386 }
387 salt_output = self.cmd(
Alexe0c5b9e2019-04-23 18:51:23 -0500388 self.master_node,
savex4448e132018-04-25 15:51:14 +0200389 "file.touch",
390 param=path,
391 kwarg=_kwarg
392 )
Alex3bc95f62020-03-05 17:00:04 -0600393 return [*salt_output.values()][0]
savex4448e132018-04-25 15:51:14 +0200394
395 def f_append_master(self, path, strings_list, makedirs=True):
396 _kwarg = {
397 "makedirs": makedirs
398 }
399 _args = [path]
400 _args.extend(strings_list)
401 salt_output = self.cmd(
Alexe0c5b9e2019-04-23 18:51:23 -0500402 self.master_node,
savex4448e132018-04-25 15:51:14 +0200403 "file.write",
404 param=_args,
405 kwarg=_kwarg
406 )
Alex3bc95f62020-03-05 17:00:04 -0600407 return [*salt_output.values()][0]
savex4448e132018-04-25 15:51:14 +0200408
409 def mkdir(self, target, path, tgt_type=None):
410 salt_output = self.cmd(
411 target,
412 "file.mkdir",
413 param=path,
414 expr_form=tgt_type
415 )
416 return salt_output
417
418 def f_manage_file(self, target_path, source,
419 sfn='', ret='{}',
420 source_hash={},
421 user='root', group='root', backup_mode='755',
422 show_diff='base',
423 contents='', makedirs=True):
424 """
425 REST variation of file.get_managed
426 CLI execution goes like this (10 agrs):
Alex3ebc5632019-04-18 16:47:18 -0500427 salt cfg01\\* file.manage_file /root/test_scripts/pkg_versions.py
savex4448e132018-04-25 15:51:14 +0200428 '' '{}' /root/diff_pkg_version.py
429 '{hash_type: 'md5', 'hsum': <md5sum>}' root root '755' base ''
430 makedirs=True
431 param: name - target file placement when managed
432 param: source - source for the file
433 """
434 _source_hash = {
435 "hash_type": "md5",
436 "hsum": 000
437 }
438 _arg = [
439 target_path,
440 sfn,
441 ret,
442 source,
443 _source_hash,
444 user,
445 group,
446 backup_mode,
447 show_diff,
448 contents
449 ]
450 _kwarg = {
451 "makedirs": makedirs
452 }
453 salt_output = self.cmd(
Alexe0c5b9e2019-04-23 18:51:23 -0500454 self.master_node,
savex4448e132018-04-25 15:51:14 +0200455 "file.manage_file",
456 param=_arg,
457 kwarg=_kwarg
458 )
Alex3bc95f62020-03-05 17:00:04 -0600459 return [*salt_output.values()][0]
savex4448e132018-04-25 15:51:14 +0200460
461 def cache_file(self, target, source_path):
462 salt_output = self.cmd(
463 target,
464 "cp.cache_file",
465 param=source_path
466 )
Alex3bc95f62020-03-05 17:00:04 -0600467 return [*salt_output.values()][0]
savex4448e132018-04-25 15:51:14 +0200468
469 def get_file(self, target, source_path, target_path, tgt_type=None):
470 return self.cmd(
471 target,
472 "cp.get_file",
473 param=[source_path, target_path],
474 expr_form=tgt_type
475 )
476
477 @staticmethod
478 def compound_string_from_list(nodes_list):
479 return " or ".join(nodes_list)