blob: f7ea50befdc071abdddeef7851a14e2040a7c760 [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,
44 use_sudo=config.ssh_uses_sudo,
45 silent=True
46 )
Alexd0391d42019-05-21 18:48:55 -050047 if len(_result) < 1:
48 raise InvalidReturnException(
49 "# Empty value returned for '{}".format(
Alex9a4ad212020-10-01 18:04:25 -050050 _salt_cmd
Alexd0391d42019-05-21 18:48:55 -050051 )
52 )
53 else:
Alex9a4ad212020-10-01 18:04:25 -050054 return _extract_salt_password(_result)
Alexd0391d42019-05-21 18:48:55 -050055 except OSError as e:
56 raise SaltException(
57 "Salt error calling '{}': '{}'\n"
Alex9a4ad212020-10-01 18:04:25 -050058 "\nConsider checking 'MCP_ENV' "
59 "and '<pkg>/etc/<env>.env' files".format(_salt_cmd, e.strerror)
Alexd0391d42019-05-21 18:48:55 -050060 )
Alex Savatieiev63576832019-02-27 15:46:26 -060061
Alex3ebc5632019-04-18 16:47:18 -050062
Alex9a4ad212020-10-01 18:04:25 -050063def get_salt_local_password(config):
Alex Savatieiev63576832019-02-27 15:46:26 -060064 """Calls salt locally to get password from the pillar
65
66 :return: password string
67 """
Alex3bc95f62020-03-05 17:00:04 -060068 _cmd = []
69 if config.ssh_uses_sudo:
70 _cmd = ["sudo"]
71 # salt commands
72 _cmd.append("salt-call")
73 _cmd.append("--out=json pillar.get _param:salt_api_password")
Alexd0391d42019-05-21 18:48:55 -050074 try:
Alex3bc95f62020-03-05 17:00:04 -060075 _result = shell(" ".join(_cmd))
Alexd0391d42019-05-21 18:48:55 -050076 except OSError as e:
77 raise SaltException(
78 "Salt error calling '{}': '{}'\n"
Alex9a4ad212020-10-01 18:04:25 -050079 "\nConsider checking 'MCP_ENV' "
Alexd0391d42019-05-21 18:48:55 -050080 "and '<pkg>/etc/<env>.env' files".format(_cmd, e.strerror)
81 )
Alex9a4ad212020-10-01 18:04:25 -050082 return _extract_salt_password(_result)
savex4448e132018-04-25 15:51:14 +020083
84
85def list_to_target_string(node_list, separator):
86 result = ''
87 for node in node_list:
88 result += node + ' ' + separator + ' '
89 return result[:-(len(separator)+2)]
90
91
92class SaltRest(object):
savex4448e132018-04-25 15:51:14 +020093 _auth = {}
94
95 default_headers = {
96 'Accept': 'application/json',
97 'Content-Type': 'application/json',
98 'X-Auth-Token': None
99 }
100
Alex9a4ad212020-10-01 18:04:25 -0500101 def __init__(self, config):
102 self.config = config
103
104 self._host = config.mcp_host
105 self._port = config.salt_port
106 self.uri = "http://" + config.mcp_host + ":" + config.salt_port
107
savex4448e132018-04-25 15:51:14 +0200108 self._token = self._login()
109 self.last_response = None
110
Alex3ebc5632019-04-18 16:47:18 -0500111 def get(
112 self,
113 path='',
114 headers=default_headers,
115 cookies=None,
116 timeout=None
117 ):
savex4448e132018-04-25 15:51:14 +0200118 _path = os.path.join(self.uri, path)
Alex Savatieievf808cd22019-03-01 13:17:59 -0600119 logger.debug("# GET '{}'\nHeaders: '{}'\nCookies: {}".format(
savex4448e132018-04-25 15:51:14 +0200120 _path,
121 headers,
122 cookies
123 ))
124 return requests.get(
125 _path,
126 headers=headers,
Alex Savatieievefa79c42019-03-14 19:14:04 -0500127 cookies=cookies,
128 timeout=timeout
savex4448e132018-04-25 15:51:14 +0200129 )
130
131 def post(self, data, path='', headers=default_headers, cookies=None):
132 if data is None:
133 data = {}
134 _path = os.path.join(self.uri, path)
135 if path == 'login':
Alex Savatieiev63576832019-02-27 15:46:26 -0600136 _data = str(data).replace(self._pass, "*****")
savex4448e132018-04-25 15:51:14 +0200137 else:
138 _data = data
Alex3ebc5632019-04-18 16:47:18 -0500139 logger.debug(
140 "# POST '{}'\nHeaders: '{}'\nCookies: {}\nBody: {}".format(
141 _path,
142 headers,
143 cookies,
144 _data
145 )
146 )
savex4448e132018-04-25 15:51:14 +0200147 return requests.post(
148 os.path.join(self.uri, path),
149 headers=headers,
150 json=data,
151 cookies=cookies
152 )
153
154 def _login(self):
Alex Savatieiev63576832019-02-27 15:46:26 -0600155 # if there is no password - try to get local, if this available
Alex9a4ad212020-10-01 18:04:25 -0500156 if self.config.env_name == "local":
157 _pass = get_salt_local_password(self.config)
Alex Savatieiev63576832019-02-27 15:46:26 -0600158 else:
Alex9a4ad212020-10-01 18:04:25 -0500159 _pass = get_remote_salt_env_password(self.config)
savex4448e132018-04-25 15:51:14 +0200160 login_payload = {
Alex9a4ad212020-10-01 18:04:25 -0500161 'username': self.config.salt_user,
Alex Savatieiev63576832019-02-27 15:46:26 -0600162 'password': _pass,
savex4448e132018-04-25 15:51:14 +0200163 'eauth': 'pam'
164 }
Alex Savatieiev63576832019-02-27 15:46:26 -0600165 self._pass = _pass
Alex Savatieievf808cd22019-03-01 13:17:59 -0600166 logger.debug("# Logging in to salt master...")
savex4448e132018-04-25 15:51:14 +0200167 _response = self.post(login_payload, path='login')
168
169 if _response.ok:
170 self._auth['response'] = _response.json()['return'][0]
171 self._auth['cookies'] = _response.cookies
172 self.default_headers['X-Auth-Token'] = \
173 self._auth['response']['token']
174 return self._auth['response']['token']
175 else:
176 raise EnvironmentError(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600177 "# HTTP:{}, Not authorized?".format(_response.status_code)
savex4448e132018-04-25 15:51:14 +0200178 )
179
180 def salt_request(self, fn, *args, **kwargs):
181 # if token will expire in 5 min, re-login
182 if self._auth['response']['expire'] < time.time() + 300:
183 self._auth['response']['X-Auth-Token'] = self._login()
184
185 _method = getattr(self, fn)
186 _response = _method(*args, **kwargs)
187 self.last_response = _response
188 _content = "..."
189 _len = len(_response.content)
190 if _len < 1024:
191 _content = _response.content
192 logger.debug(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600193 "# Response (HTTP {}/{}), {}: {}".format(
savex4448e132018-04-25 15:51:14 +0200194 _response.status_code,
195 _response.reason,
196 _len,
197 _content
198 )
199 )
200 if _response.ok:
201 return _response.json()['return']
202 else:
203 raise EnvironmentError(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600204 "# Salt Error: HTTP:{}, '{}'".format(
savex4448e132018-04-25 15:51:14 +0200205 _response.status_code,
206 _response.reason
207 )
208 )
209
210
211class SaltRemote(SaltRest):
Alexe0c5b9e2019-04-23 18:51:23 -0500212 master_node = ""
213
Alex9a4ad212020-10-01 18:04:25 -0500214 def __init__(self, config):
215 super(SaltRemote, self).__init__(config)
savex4448e132018-04-25 15:51:14 +0200216
217 def cmd(
218 self,
219 tgt,
220 fun,
221 param=None,
222 client='local',
223 kwarg=None,
224 expr_form=None,
225 tgt_type=None,
226 timeout=None
227 ):
Alex9a4ad212020-10-01 18:04:25 -0500228 _timeout = timeout if timeout is not None else self.config.salt_timeout
savex4448e132018-04-25 15:51:14 +0200229 _payload = {
230 'fun': fun,
231 'tgt': tgt,
232 'client': client,
233 'timeout': _timeout
234 }
235
236 if expr_form:
237 _payload['expr_form'] = expr_form
238 if tgt_type:
239 _payload['tgt_type'] = tgt_type
240 if param:
241 _payload['arg'] = param
242 if kwarg:
243 _payload['kwarg'] = kwarg
Alexac2a2732020-09-11 11:00:26 -0500244 logger_cli.debug("SaltRequest: POST '{}'".format(_payload))
savex4448e132018-04-25 15:51:14 +0200245 _response = self.salt_request('post', [_payload])
246 if isinstance(_response, list):
247 return _response[0]
248 else:
249 raise EnvironmentError(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600250 "# Unexpected response from from salt-api/LocalClient: "
savex4448e132018-04-25 15:51:14 +0200251 "{}".format(_response)
252 )
253
254 def run(self, fun, kwarg=None):
255 _payload = {
256 'client': 'runner',
257 'fun': fun,
Alex9a4ad212020-10-01 18:04:25 -0500258 'timeout': self.config.salt_timeout
savex4448e132018-04-25 15:51:14 +0200259 }
260
261 if kwarg:
262 _payload['kwarg'] = kwarg
263
264 _response = self.salt_request('post', [_payload])
265 if isinstance(_response, list):
266 return _response[0]
267 else:
268 raise EnvironmentError(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600269 "# Unexpected response from from salt-api/RunnerClient: "
savex4448e132018-04-25 15:51:14 +0200270 "{}".format(_response)
271 )
272
273 def wheel(self, fun, arg=None, kwarg=None):
274 _payload = {
275 'client': 'wheel',
276 'fun': fun,
Alex9a4ad212020-10-01 18:04:25 -0500277 'timeout': self.config.salt_timeout
savex4448e132018-04-25 15:51:14 +0200278 }
279
280 if arg:
281 _payload['arg'] = arg
282 if kwarg:
283 _payload['kwarg'] = kwarg
284
285 _response = self.salt_request('post', _payload)['data']
286 if _response['success']:
287 return _response
288 else:
289 raise EnvironmentError(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600290 "# Salt Error: '{}'".format(_response['return']))
savex4448e132018-04-25 15:51:14 +0200291
292 def pillar_request(self, node_target, pillar_submodule, argument):
293 # example cli: 'salt "ctl01*" pillar.keys rsyslog'
294 _type = "compound"
295 if isinstance(node_target, list):
296 _type = "list"
297 return self.cmd(
298 node_target,
299 "pillar." + pillar_submodule,
300 argument,
301 expr_form=_type
302 )
303
304 def pillar_keys(self, node_target, argument):
305 return self.pillar_request(node_target, 'keys', argument)
306
307 def pillar_get(self, node_target, argument):
308 return self.pillar_request(node_target, 'get', argument)
309
310 def pillar_data(self, node_target, argument):
311 return self.pillar_request(node_target, 'data', argument)
312
313 def pillar_raw(self, node_target, argument):
314 return self.pillar_request(node_target, 'raw', argument)
315
316 def list_minions(self):
317 """
318 Fails in salt version 2016.3.8
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600319 Works starting from 2017.7.7
savex4448e132018-04-25 15:51:14 +0200320 api returns dict of minions with grains
321 """
Alex Savatieievefa79c42019-03-14 19:14:04 -0500322 try:
323 _r = self.salt_request('get', 'minions', timeout=10)
Alex3ebc5632019-04-18 16:47:18 -0500324 except requests.exceptions.ReadTimeout:
Alex Savatieievefa79c42019-03-14 19:14:04 -0500325 logger_cli.debug("... timeout waiting list minions from Salt API")
326 _r = None
327 return _r[0] if _r else None
savex4448e132018-04-25 15:51:14 +0200328
329 def list_keys(self):
330 """
331 Fails in salt version 2016.3.8
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600332 Works starting from 2017.7.7
savex4448e132018-04-25 15:51:14 +0200333 api should return dict:
334 {
335 'local': [],
336 'minions': [],
337 'minions_denied': [],
338 'minions_pre': [],
339 'minions_rejected': [],
340 }
341 """
342 return self.salt_request('get', path='keys')
343
344 def get_status(self):
345 """
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600346 Fails in salt version 2017.7.7
savex4448e132018-04-25 15:51:14 +0200347 'runner' client is the equivalent of 'salt-run'
348 Returns the
349 """
350 return self.run(
351 'manage.status',
352 kwarg={'timeout': 10}
353 )
354
355 def get_active_nodes(self):
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600356 """Used when other minion list metods fail
Alex3ebc5632019-04-18 16:47:18 -0500357
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600358 :return: json result from salt test.ping
359 """
Alex9a4ad212020-10-01 18:04:25 -0500360 if self.config.skip_nodes:
361 logger.info(
362 "# Nodes to be skipped: {0}".format(self.config.skip_nodes)
363 )
Alex Savatieievefa79c42019-03-14 19:14:04 -0500364 _r = self.cmd(
savex4448e132018-04-25 15:51:14 +0200365 '* and not ' + list_to_target_string(
Alex9a4ad212020-10-01 18:04:25 -0500366 self.config.skip_nodes,
savex4448e132018-04-25 15:51:14 +0200367 'and not'
368 ),
369 'test.ping',
370 expr_form='compound')
371 else:
Alex Savatieievefa79c42019-03-14 19:14:04 -0500372 _r = self.cmd('*', 'test.ping')
Alex3ebc5632019-04-18 16:47:18 -0500373 # Return all nodes that responded
Alex Savatieievefa79c42019-03-14 19:14:04 -0500374 return [node for node in _r.keys() if _r[node]]
savex4448e132018-04-25 15:51:14 +0200375
376 def get_monitoring_ip(self, param_name):
377 salt_output = self.cmd(
378 'docker:client:stack:monitoring',
379 'pillar.get',
380 param=param_name,
381 expr_form='pillar')
382 return salt_output[salt_output.keys()[0]]
383
384 def f_touch_master(self, path, makedirs=True):
385 _kwarg = {
386 "makedirs": makedirs
387 }
388 salt_output = self.cmd(
Alexe0c5b9e2019-04-23 18:51:23 -0500389 self.master_node,
savex4448e132018-04-25 15:51:14 +0200390 "file.touch",
391 param=path,
392 kwarg=_kwarg
393 )
Alex3bc95f62020-03-05 17:00:04 -0600394 return [*salt_output.values()][0]
savex4448e132018-04-25 15:51:14 +0200395
396 def f_append_master(self, path, strings_list, makedirs=True):
397 _kwarg = {
398 "makedirs": makedirs
399 }
400 _args = [path]
401 _args.extend(strings_list)
402 salt_output = self.cmd(
Alexe0c5b9e2019-04-23 18:51:23 -0500403 self.master_node,
savex4448e132018-04-25 15:51:14 +0200404 "file.write",
405 param=_args,
406 kwarg=_kwarg
407 )
Alex3bc95f62020-03-05 17:00:04 -0600408 return [*salt_output.values()][0]
savex4448e132018-04-25 15:51:14 +0200409
410 def mkdir(self, target, path, tgt_type=None):
411 salt_output = self.cmd(
412 target,
413 "file.mkdir",
414 param=path,
415 expr_form=tgt_type
416 )
417 return salt_output
418
419 def f_manage_file(self, target_path, source,
420 sfn='', ret='{}',
421 source_hash={},
422 user='root', group='root', backup_mode='755',
423 show_diff='base',
424 contents='', makedirs=True):
425 """
426 REST variation of file.get_managed
427 CLI execution goes like this (10 agrs):
Alex3ebc5632019-04-18 16:47:18 -0500428 salt cfg01\\* file.manage_file /root/test_scripts/pkg_versions.py
savex4448e132018-04-25 15:51:14 +0200429 '' '{}' /root/diff_pkg_version.py
430 '{hash_type: 'md5', 'hsum': <md5sum>}' root root '755' base ''
431 makedirs=True
432 param: name - target file placement when managed
433 param: source - source for the file
434 """
435 _source_hash = {
436 "hash_type": "md5",
437 "hsum": 000
438 }
439 _arg = [
440 target_path,
441 sfn,
442 ret,
443 source,
444 _source_hash,
445 user,
446 group,
447 backup_mode,
448 show_diff,
449 contents
450 ]
451 _kwarg = {
452 "makedirs": makedirs
453 }
454 salt_output = self.cmd(
Alexe0c5b9e2019-04-23 18:51:23 -0500455 self.master_node,
savex4448e132018-04-25 15:51:14 +0200456 "file.manage_file",
457 param=_arg,
458 kwarg=_kwarg
459 )
Alex3bc95f62020-03-05 17:00:04 -0600460 return [*salt_output.values()][0]
savex4448e132018-04-25 15:51:14 +0200461
462 def cache_file(self, target, source_path):
463 salt_output = self.cmd(
464 target,
465 "cp.cache_file",
466 param=source_path
467 )
Alex3bc95f62020-03-05 17:00:04 -0600468 return [*salt_output.values()][0]
savex4448e132018-04-25 15:51:14 +0200469
470 def get_file(self, target, source_path, target_path, tgt_type=None):
471 return self.cmd(
472 target,
473 "cp.get_file",
474 param=[source_path, target_path],
475 expr_form=tgt_type
476 )
477
478 @staticmethod
479 def compound_string_from_list(nodes_list):
480 return " or ".join(nodes_list)