blob: b91353121f3e8c6ff76b20ee02e5f2dadc48c91a [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
6import requests
7import time
8
Alex Savatieiev63576832019-02-27 15:46:26 -06009from cfg_checker.common import logger, logger_cli, config
10from cfg_checker.common.other import shell
11from cfg_checker.common.exception import SaltException, InvalidReturnException
12
13
14def _extract_password(_raw):
15 if not isinstance(_raw, unicode):
16 raise InvalidReturnException(_raw)
17 else:
18 try:
19 _json = json.loads(_raw)
20 except ValueError as e:
21 raise SaltException(
Alex Savatieievf808cd22019-03-01 13:17:59 -060022 "# Return value is not a json: '{}'".format(_raw)
Alex Savatieiev63576832019-02-27 15:46:26 -060023 )
24
25 return _json["local"]
26
27
28def get_remote_env_password():
29 """Uses ssh call with configured options to get password from salt master
30
31 :return: password string
32 """
33 _salt_cmd = "salt-call --out=json pillar.get _param:salt_api_password"
34 _ssh_cmd = ["ssh"]
35 # Build SSH cmd
36 if config.ssh_key:
37 _ssh_cmd.append("-i " + config.ssh_key)
38 if config.ssh_user:
39 _ssh_cmd.append(config.ssh_user+'@'+config.ssh_host)
40 else:
41 _ssh_cmd.append(config.ssh_host)
42 if config.ssh_uses_sudo:
43 _ssh_cmd.append("sudo")
44
45 _ssh_cmd.append(_salt_cmd)
46 _ssh_cmd = " ".join(_ssh_cmd)
Alex Savatieiev42b89fa2019-03-07 18:45:26 -060047 logger_cli.debug("...calling salt: '{}'".format(_ssh_cmd))
Alex Savatieiev63576832019-02-27 15:46:26 -060048 _result = shell(_ssh_cmd)
49 if len(_result) < 1:
Alex Savatieievf808cd22019-03-01 13:17:59 -060050 raise InvalidReturnException("# Empty value returned for '{}".format(
Alex Savatieiev63576832019-02-27 15:46:26 -060051 _ssh_cmd
52 ))
53 else:
54 return _extract_password(_result)
55
56def get_local_password():
57 """Calls salt locally to get password from the pillar
58
59 :return: password string
60 """
61 _cmd = "salt-call --out=json pillar.get _param:salt_api_password"
62 _result = shell(_cmd)
63 return _extract_password(_result)
savex4448e132018-04-25 15:51:14 +020064
65
66def list_to_target_string(node_list, separator):
67 result = ''
68 for node in node_list:
69 result += node + ' ' + separator + ' '
70 return result[:-(len(separator)+2)]
71
72
73class SaltRest(object):
74 _host = config.salt_host
75 _port = config.salt_port
76 uri = "http://" + config.salt_host + ":" + config.salt_port
77 _auth = {}
78
79 default_headers = {
80 'Accept': 'application/json',
81 'Content-Type': 'application/json',
82 'X-Auth-Token': None
83 }
84
85 def __init__(self):
86 self._token = self._login()
87 self.last_response = None
88
Alex Savatieievefa79c42019-03-14 19:14:04 -050089 def get(self, path='', headers=default_headers, cookies=None, timeout=None):
savex4448e132018-04-25 15:51:14 +020090 _path = os.path.join(self.uri, path)
Alex Savatieievf808cd22019-03-01 13:17:59 -060091 logger.debug("# GET '{}'\nHeaders: '{}'\nCookies: {}".format(
savex4448e132018-04-25 15:51:14 +020092 _path,
93 headers,
94 cookies
95 ))
96 return requests.get(
97 _path,
98 headers=headers,
Alex Savatieievefa79c42019-03-14 19:14:04 -050099 cookies=cookies,
100 timeout=timeout
savex4448e132018-04-25 15:51:14 +0200101 )
102
103 def post(self, data, path='', headers=default_headers, cookies=None):
104 if data is None:
105 data = {}
106 _path = os.path.join(self.uri, path)
107 if path == 'login':
Alex Savatieiev63576832019-02-27 15:46:26 -0600108 _data = str(data).replace(self._pass, "*****")
savex4448e132018-04-25 15:51:14 +0200109 else:
110 _data = data
Alex Savatieievf808cd22019-03-01 13:17:59 -0600111 logger.debug("# POST '{}'\nHeaders: '{}'\nCookies: {}\nBody: {}".format(
savex4448e132018-04-25 15:51:14 +0200112 _path,
113 headers,
114 cookies,
115 _data
116 ))
117 return requests.post(
118 os.path.join(self.uri, path),
119 headers=headers,
120 json=data,
121 cookies=cookies
122 )
123
124 def _login(self):
Alex Savatieiev63576832019-02-27 15:46:26 -0600125 # if there is no password - try to get local, if this available
126 if config.salt_env == "local":
127 _pass = get_local_password()
128 else:
129 _pass = get_remote_env_password()
savex4448e132018-04-25 15:51:14 +0200130 login_payload = {
131 'username': config.salt_user,
Alex Savatieiev63576832019-02-27 15:46:26 -0600132 'password': _pass,
savex4448e132018-04-25 15:51:14 +0200133 'eauth': 'pam'
134 }
Alex Savatieiev63576832019-02-27 15:46:26 -0600135 self._pass = _pass
Alex Savatieievf808cd22019-03-01 13:17:59 -0600136 logger.debug("# Logging in to salt master...")
savex4448e132018-04-25 15:51:14 +0200137 _response = self.post(login_payload, path='login')
138
139 if _response.ok:
140 self._auth['response'] = _response.json()['return'][0]
141 self._auth['cookies'] = _response.cookies
142 self.default_headers['X-Auth-Token'] = \
143 self._auth['response']['token']
144 return self._auth['response']['token']
145 else:
146 raise EnvironmentError(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600147 "# HTTP:{}, Not authorized?".format(_response.status_code)
savex4448e132018-04-25 15:51:14 +0200148 )
149
150 def salt_request(self, fn, *args, **kwargs):
151 # if token will expire in 5 min, re-login
152 if self._auth['response']['expire'] < time.time() + 300:
153 self._auth['response']['X-Auth-Token'] = self._login()
154
155 _method = getattr(self, fn)
156 _response = _method(*args, **kwargs)
157 self.last_response = _response
158 _content = "..."
159 _len = len(_response.content)
160 if _len < 1024:
161 _content = _response.content
162 logger.debug(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600163 "# Response (HTTP {}/{}), {}: {}".format(
savex4448e132018-04-25 15:51:14 +0200164 _response.status_code,
165 _response.reason,
166 _len,
167 _content
168 )
169 )
170 if _response.ok:
171 return _response.json()['return']
172 else:
173 raise EnvironmentError(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600174 "# Salt Error: HTTP:{}, '{}'".format(
savex4448e132018-04-25 15:51:14 +0200175 _response.status_code,
176 _response.reason
177 )
178 )
179
180
181class SaltRemote(SaltRest):
182 def __init__(self):
183 super(SaltRemote, self).__init__()
184
185 def cmd(
186 self,
187 tgt,
188 fun,
189 param=None,
190 client='local',
191 kwarg=None,
192 expr_form=None,
193 tgt_type=None,
194 timeout=None
195 ):
196 _timeout = timeout if timeout is not None else config.salt_timeout
197 _payload = {
198 'fun': fun,
199 'tgt': tgt,
200 'client': client,
201 'timeout': _timeout
202 }
203
204 if expr_form:
205 _payload['expr_form'] = expr_form
206 if tgt_type:
207 _payload['tgt_type'] = tgt_type
208 if param:
209 _payload['arg'] = param
210 if kwarg:
211 _payload['kwarg'] = kwarg
212
213 _response = self.salt_request('post', [_payload])
214 if isinstance(_response, list):
215 return _response[0]
216 else:
217 raise EnvironmentError(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600218 "# Unexpected response from from salt-api/LocalClient: "
savex4448e132018-04-25 15:51:14 +0200219 "{}".format(_response)
220 )
221
222 def run(self, fun, kwarg=None):
223 _payload = {
224 'client': 'runner',
225 'fun': fun,
226 'timeout': config.salt_timeout
227 }
228
229 if kwarg:
230 _payload['kwarg'] = kwarg
231
232 _response = self.salt_request('post', [_payload])
233 if isinstance(_response, list):
234 return _response[0]
235 else:
236 raise EnvironmentError(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600237 "# Unexpected response from from salt-api/RunnerClient: "
savex4448e132018-04-25 15:51:14 +0200238 "{}".format(_response)
239 )
240
241 def wheel(self, fun, arg=None, kwarg=None):
242 _payload = {
243 'client': 'wheel',
244 'fun': fun,
245 'timeout': config.salt_timeout
246 }
247
248 if arg:
249 _payload['arg'] = arg
250 if kwarg:
251 _payload['kwarg'] = kwarg
252
253 _response = self.salt_request('post', _payload)['data']
254 if _response['success']:
255 return _response
256 else:
257 raise EnvironmentError(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600258 "# Salt Error: '{}'".format(_response['return']))
savex4448e132018-04-25 15:51:14 +0200259
260 def pillar_request(self, node_target, pillar_submodule, argument):
261 # example cli: 'salt "ctl01*" pillar.keys rsyslog'
262 _type = "compound"
263 if isinstance(node_target, list):
264 _type = "list"
265 return self.cmd(
266 node_target,
267 "pillar." + pillar_submodule,
268 argument,
269 expr_form=_type
270 )
271
272 def pillar_keys(self, node_target, argument):
273 return self.pillar_request(node_target, 'keys', argument)
274
275 def pillar_get(self, node_target, argument):
276 return self.pillar_request(node_target, 'get', argument)
277
278 def pillar_data(self, node_target, argument):
279 return self.pillar_request(node_target, 'data', argument)
280
281 def pillar_raw(self, node_target, argument):
282 return self.pillar_request(node_target, 'raw', argument)
283
284 def list_minions(self):
285 """
286 Fails in salt version 2016.3.8
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600287 Works starting from 2017.7.7
savex4448e132018-04-25 15:51:14 +0200288 api returns dict of minions with grains
289 """
Alex Savatieievefa79c42019-03-14 19:14:04 -0500290 try:
291 _r = self.salt_request('get', 'minions', timeout=10)
292 except requests.exceptions.ReadTimeout as e:
293 logger_cli.debug("... timeout waiting list minions from Salt API")
294 _r = None
295 return _r[0] if _r else None
savex4448e132018-04-25 15:51:14 +0200296
297 def list_keys(self):
298 """
299 Fails in salt version 2016.3.8
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600300 Works starting from 2017.7.7
savex4448e132018-04-25 15:51:14 +0200301 api should return dict:
302 {
303 'local': [],
304 'minions': [],
305 'minions_denied': [],
306 'minions_pre': [],
307 'minions_rejected': [],
308 }
309 """
310 return self.salt_request('get', path='keys')
311
312 def get_status(self):
313 """
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600314 Fails in salt version 2017.7.7
savex4448e132018-04-25 15:51:14 +0200315 'runner' client is the equivalent of 'salt-run'
316 Returns the
317 """
318 return self.run(
319 'manage.status',
320 kwarg={'timeout': 10}
321 )
322
323 def get_active_nodes(self):
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600324 """Used when other minion list metods fail
325
326 :return: json result from salt test.ping
327 """
savex4448e132018-04-25 15:51:14 +0200328 if config.skip_nodes:
Alex Savatieievf808cd22019-03-01 13:17:59 -0600329 logger.info("# Nodes to be skipped: {0}".format(config.skip_nodes))
Alex Savatieievefa79c42019-03-14 19:14:04 -0500330 _r = self.cmd(
savex4448e132018-04-25 15:51:14 +0200331 '* and not ' + list_to_target_string(
332 config.skip_nodes,
333 'and not'
334 ),
335 'test.ping',
336 expr_form='compound')
337 else:
Alex Savatieievefa79c42019-03-14 19:14:04 -0500338 _r = self.cmd('*', 'test.ping')
339 # Return all nodes that responded
340 return [node for node in _r.keys() if _r[node]]
savex4448e132018-04-25 15:51:14 +0200341
342 def get_monitoring_ip(self, param_name):
343 salt_output = self.cmd(
344 'docker:client:stack:monitoring',
345 'pillar.get',
346 param=param_name,
347 expr_form='pillar')
348 return salt_output[salt_output.keys()[0]]
349
350 def f_touch_master(self, path, makedirs=True):
351 _kwarg = {
352 "makedirs": makedirs
353 }
354 salt_output = self.cmd(
355 "cfg01*",
356 "file.touch",
357 param=path,
358 kwarg=_kwarg
359 )
360 return salt_output[salt_output.keys()[0]]
361
362 def f_append_master(self, path, strings_list, makedirs=True):
363 _kwarg = {
364 "makedirs": makedirs
365 }
366 _args = [path]
367 _args.extend(strings_list)
368 salt_output = self.cmd(
369 "cfg01*",
370 "file.write",
371 param=_args,
372 kwarg=_kwarg
373 )
374 return salt_output[salt_output.keys()[0]]
375
376 def mkdir(self, target, path, tgt_type=None):
377 salt_output = self.cmd(
378 target,
379 "file.mkdir",
380 param=path,
381 expr_form=tgt_type
382 )
383 return salt_output
384
385 def f_manage_file(self, target_path, source,
386 sfn='', ret='{}',
387 source_hash={},
388 user='root', group='root', backup_mode='755',
389 show_diff='base',
390 contents='', makedirs=True):
391 """
392 REST variation of file.get_managed
393 CLI execution goes like this (10 agrs):
394 salt cfg01\* file.manage_file /root/test_scripts/pkg_versions.py
395 '' '{}' /root/diff_pkg_version.py
396 '{hash_type: 'md5', 'hsum': <md5sum>}' root root '755' base ''
397 makedirs=True
398 param: name - target file placement when managed
399 param: source - source for the file
400 """
401 _source_hash = {
402 "hash_type": "md5",
403 "hsum": 000
404 }
405 _arg = [
406 target_path,
407 sfn,
408 ret,
409 source,
410 _source_hash,
411 user,
412 group,
413 backup_mode,
414 show_diff,
415 contents
416 ]
417 _kwarg = {
418 "makedirs": makedirs
419 }
420 salt_output = self.cmd(
421 "cfg01*",
422 "file.manage_file",
423 param=_arg,
424 kwarg=_kwarg
425 )
426 return salt_output[salt_output.keys()[0]]
427
428 def cache_file(self, target, source_path):
429 salt_output = self.cmd(
430 target,
431 "cp.cache_file",
432 param=source_path
433 )
434 return salt_output[salt_output.keys()[0]]
435
436 def get_file(self, target, source_path, target_path, tgt_type=None):
437 return self.cmd(
438 target,
439 "cp.get_file",
440 param=[source_path, target_path],
441 expr_form=tgt_type
442 )
443
444 @staticmethod
445 def compound_string_from_list(nodes_list):
446 return " or ".join(nodes_list)