blob: 4dcbd30208e4ea5518efabacdf14ac665b92260e [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
Alex3ebc5632019-04-18 16:47:18 -05008from cfg_checker.common import config, logger, logger_cli
9from cfg_checker.common.exception import InvalidReturnException, SaltException
Alex Savatieiev63576832019-02-27 15:46:26 -060010from cfg_checker.common.other import shell
Alex3ebc5632019-04-18 16:47:18 -050011
12import requests
Alex Savatieiev63576832019-02-27 15:46:26 -060013
14
15def _extract_password(_raw):
16 if not isinstance(_raw, unicode):
17 raise InvalidReturnException(_raw)
18 else:
19 try:
20 _json = json.loads(_raw)
Alex3ebc5632019-04-18 16:47:18 -050021 except ValueError:
Alex Savatieiev63576832019-02-27 15:46:26 -060022 raise SaltException(
Alex Savatieievf808cd22019-03-01 13:17:59 -060023 "# Return value is not a json: '{}'".format(_raw)
Alex Savatieiev63576832019-02-27 15:46:26 -060024 )
Alex3ebc5632019-04-18 16:47:18 -050025
Alex Savatieiev63576832019-02-27 15:46:26 -060026 return _json["local"]
27
28
29def get_remote_env_password():
30 """Uses ssh call with configured options to get password from salt master
31
32 :return: password string
33 """
34 _salt_cmd = "salt-call --out=json pillar.get _param:salt_api_password"
35 _ssh_cmd = ["ssh"]
36 # Build SSH cmd
37 if config.ssh_key:
38 _ssh_cmd.append("-i " + config.ssh_key)
39 if config.ssh_user:
40 _ssh_cmd.append(config.ssh_user+'@'+config.ssh_host)
41 else:
42 _ssh_cmd.append(config.ssh_host)
43 if config.ssh_uses_sudo:
44 _ssh_cmd.append("sudo")
Alex3ebc5632019-04-18 16:47:18 -050045
Alex Savatieiev63576832019-02-27 15:46:26 -060046 _ssh_cmd.append(_salt_cmd)
47 _ssh_cmd = " ".join(_ssh_cmd)
Alexb151fbe2019-04-22 16:53:30 -050048 logger_cli.debug("... calling salt: '{}'".format(_ssh_cmd))
Alex Savatieiev63576832019-02-27 15:46:26 -060049 _result = shell(_ssh_cmd)
50 if len(_result) < 1:
Alex Savatieievf808cd22019-03-01 13:17:59 -060051 raise InvalidReturnException("# Empty value returned for '{}".format(
Alex Savatieiev63576832019-02-27 15:46:26 -060052 _ssh_cmd
53 ))
54 else:
55 return _extract_password(_result)
56
Alex3ebc5632019-04-18 16:47:18 -050057
Alex Savatieiev63576832019-02-27 15:46:26 -060058def get_local_password():
59 """Calls salt locally to get password from the pillar
60
61 :return: password string
62 """
63 _cmd = "salt-call --out=json pillar.get _param:salt_api_password"
64 _result = shell(_cmd)
65 return _extract_password(_result)
savex4448e132018-04-25 15:51:14 +020066
67
68def list_to_target_string(node_list, separator):
69 result = ''
70 for node in node_list:
71 result += node + ' ' + separator + ' '
72 return result[:-(len(separator)+2)]
73
74
75class SaltRest(object):
76 _host = config.salt_host
77 _port = config.salt_port
78 uri = "http://" + config.salt_host + ":" + config.salt_port
79 _auth = {}
80
81 default_headers = {
82 'Accept': 'application/json',
83 'Content-Type': 'application/json',
84 'X-Auth-Token': None
85 }
86
87 def __init__(self):
88 self._token = self._login()
89 self.last_response = None
90
Alex3ebc5632019-04-18 16:47:18 -050091 def get(
92 self,
93 path='',
94 headers=default_headers,
95 cookies=None,
96 timeout=None
97 ):
savex4448e132018-04-25 15:51:14 +020098 _path = os.path.join(self.uri, path)
Alex Savatieievf808cd22019-03-01 13:17:59 -060099 logger.debug("# GET '{}'\nHeaders: '{}'\nCookies: {}".format(
savex4448e132018-04-25 15:51:14 +0200100 _path,
101 headers,
102 cookies
103 ))
104 return requests.get(
105 _path,
106 headers=headers,
Alex Savatieievefa79c42019-03-14 19:14:04 -0500107 cookies=cookies,
108 timeout=timeout
savex4448e132018-04-25 15:51:14 +0200109 )
110
111 def post(self, data, path='', headers=default_headers, cookies=None):
112 if data is None:
113 data = {}
114 _path = os.path.join(self.uri, path)
115 if path == 'login':
Alex Savatieiev63576832019-02-27 15:46:26 -0600116 _data = str(data).replace(self._pass, "*****")
savex4448e132018-04-25 15:51:14 +0200117 else:
118 _data = data
Alex3ebc5632019-04-18 16:47:18 -0500119 logger.debug(
120 "# POST '{}'\nHeaders: '{}'\nCookies: {}\nBody: {}".format(
121 _path,
122 headers,
123 cookies,
124 _data
125 )
126 )
savex4448e132018-04-25 15:51:14 +0200127 return requests.post(
128 os.path.join(self.uri, path),
129 headers=headers,
130 json=data,
131 cookies=cookies
132 )
133
134 def _login(self):
Alex Savatieiev63576832019-02-27 15:46:26 -0600135 # if there is no password - try to get local, if this available
136 if config.salt_env == "local":
137 _pass = get_local_password()
138 else:
139 _pass = get_remote_env_password()
savex4448e132018-04-25 15:51:14 +0200140 login_payload = {
141 'username': config.salt_user,
Alex Savatieiev63576832019-02-27 15:46:26 -0600142 'password': _pass,
savex4448e132018-04-25 15:51:14 +0200143 'eauth': 'pam'
144 }
Alex Savatieiev63576832019-02-27 15:46:26 -0600145 self._pass = _pass
Alex Savatieievf808cd22019-03-01 13:17:59 -0600146 logger.debug("# Logging in to salt master...")
savex4448e132018-04-25 15:51:14 +0200147 _response = self.post(login_payload, path='login')
148
149 if _response.ok:
150 self._auth['response'] = _response.json()['return'][0]
151 self._auth['cookies'] = _response.cookies
152 self.default_headers['X-Auth-Token'] = \
153 self._auth['response']['token']
154 return self._auth['response']['token']
155 else:
156 raise EnvironmentError(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600157 "# HTTP:{}, Not authorized?".format(_response.status_code)
savex4448e132018-04-25 15:51:14 +0200158 )
159
160 def salt_request(self, fn, *args, **kwargs):
161 # if token will expire in 5 min, re-login
162 if self._auth['response']['expire'] < time.time() + 300:
163 self._auth['response']['X-Auth-Token'] = self._login()
164
165 _method = getattr(self, fn)
166 _response = _method(*args, **kwargs)
167 self.last_response = _response
168 _content = "..."
169 _len = len(_response.content)
170 if _len < 1024:
171 _content = _response.content
172 logger.debug(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600173 "# Response (HTTP {}/{}), {}: {}".format(
savex4448e132018-04-25 15:51:14 +0200174 _response.status_code,
175 _response.reason,
176 _len,
177 _content
178 )
179 )
180 if _response.ok:
181 return _response.json()['return']
182 else:
183 raise EnvironmentError(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600184 "# Salt Error: HTTP:{}, '{}'".format(
savex4448e132018-04-25 15:51:14 +0200185 _response.status_code,
186 _response.reason
187 )
188 )
189
190
191class SaltRemote(SaltRest):
Alexe0c5b9e2019-04-23 18:51:23 -0500192 master_node = ""
193
savex4448e132018-04-25 15:51:14 +0200194 def __init__(self):
195 super(SaltRemote, self).__init__()
196
197 def cmd(
198 self,
199 tgt,
200 fun,
201 param=None,
202 client='local',
203 kwarg=None,
204 expr_form=None,
205 tgt_type=None,
206 timeout=None
207 ):
208 _timeout = timeout if timeout is not None else config.salt_timeout
209 _payload = {
210 'fun': fun,
211 'tgt': tgt,
212 'client': client,
213 'timeout': _timeout
214 }
215
216 if expr_form:
217 _payload['expr_form'] = expr_form
218 if tgt_type:
219 _payload['tgt_type'] = tgt_type
220 if param:
221 _payload['arg'] = param
222 if kwarg:
223 _payload['kwarg'] = kwarg
224
225 _response = self.salt_request('post', [_payload])
226 if isinstance(_response, list):
227 return _response[0]
228 else:
229 raise EnvironmentError(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600230 "# Unexpected response from from salt-api/LocalClient: "
savex4448e132018-04-25 15:51:14 +0200231 "{}".format(_response)
232 )
233
234 def run(self, fun, kwarg=None):
235 _payload = {
236 'client': 'runner',
237 'fun': fun,
238 'timeout': config.salt_timeout
239 }
240
241 if kwarg:
242 _payload['kwarg'] = kwarg
243
244 _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/RunnerClient: "
savex4448e132018-04-25 15:51:14 +0200250 "{}".format(_response)
251 )
252
253 def wheel(self, fun, arg=None, kwarg=None):
254 _payload = {
255 'client': 'wheel',
256 'fun': fun,
257 'timeout': config.salt_timeout
258 }
259
260 if arg:
261 _payload['arg'] = arg
262 if kwarg:
263 _payload['kwarg'] = kwarg
264
265 _response = self.salt_request('post', _payload)['data']
266 if _response['success']:
267 return _response
268 else:
269 raise EnvironmentError(
Alex Savatieievf808cd22019-03-01 13:17:59 -0600270 "# Salt Error: '{}'".format(_response['return']))
savex4448e132018-04-25 15:51:14 +0200271
272 def pillar_request(self, node_target, pillar_submodule, argument):
273 # example cli: 'salt "ctl01*" pillar.keys rsyslog'
274 _type = "compound"
275 if isinstance(node_target, list):
276 _type = "list"
277 return self.cmd(
278 node_target,
279 "pillar." + pillar_submodule,
280 argument,
281 expr_form=_type
282 )
283
284 def pillar_keys(self, node_target, argument):
285 return self.pillar_request(node_target, 'keys', argument)
286
287 def pillar_get(self, node_target, argument):
288 return self.pillar_request(node_target, 'get', argument)
289
290 def pillar_data(self, node_target, argument):
291 return self.pillar_request(node_target, 'data', argument)
292
293 def pillar_raw(self, node_target, argument):
294 return self.pillar_request(node_target, 'raw', argument)
295
296 def list_minions(self):
297 """
298 Fails in salt version 2016.3.8
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600299 Works starting from 2017.7.7
savex4448e132018-04-25 15:51:14 +0200300 api returns dict of minions with grains
301 """
Alex Savatieievefa79c42019-03-14 19:14:04 -0500302 try:
303 _r = self.salt_request('get', 'minions', timeout=10)
Alex3ebc5632019-04-18 16:47:18 -0500304 except requests.exceptions.ReadTimeout:
Alex Savatieievefa79c42019-03-14 19:14:04 -0500305 logger_cli.debug("... timeout waiting list minions from Salt API")
306 _r = None
307 return _r[0] if _r else None
savex4448e132018-04-25 15:51:14 +0200308
309 def list_keys(self):
310 """
311 Fails in salt version 2016.3.8
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600312 Works starting from 2017.7.7
savex4448e132018-04-25 15:51:14 +0200313 api should return dict:
314 {
315 'local': [],
316 'minions': [],
317 'minions_denied': [],
318 'minions_pre': [],
319 'minions_rejected': [],
320 }
321 """
322 return self.salt_request('get', path='keys')
323
324 def get_status(self):
325 """
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600326 Fails in salt version 2017.7.7
savex4448e132018-04-25 15:51:14 +0200327 'runner' client is the equivalent of 'salt-run'
328 Returns the
329 """
330 return self.run(
331 'manage.status',
332 kwarg={'timeout': 10}
333 )
334
335 def get_active_nodes(self):
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600336 """Used when other minion list metods fail
Alex3ebc5632019-04-18 16:47:18 -0500337
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600338 :return: json result from salt test.ping
339 """
savex4448e132018-04-25 15:51:14 +0200340 if config.skip_nodes:
Alex Savatieievf808cd22019-03-01 13:17:59 -0600341 logger.info("# Nodes to be skipped: {0}".format(config.skip_nodes))
Alex Savatieievefa79c42019-03-14 19:14:04 -0500342 _r = self.cmd(
savex4448e132018-04-25 15:51:14 +0200343 '* and not ' + list_to_target_string(
344 config.skip_nodes,
345 'and not'
346 ),
347 'test.ping',
348 expr_form='compound')
349 else:
Alex Savatieievefa79c42019-03-14 19:14:04 -0500350 _r = self.cmd('*', 'test.ping')
Alex3ebc5632019-04-18 16:47:18 -0500351 # Return all nodes that responded
Alex Savatieievefa79c42019-03-14 19:14:04 -0500352 return [node for node in _r.keys() if _r[node]]
savex4448e132018-04-25 15:51:14 +0200353
354 def get_monitoring_ip(self, param_name):
355 salt_output = self.cmd(
356 'docker:client:stack:monitoring',
357 'pillar.get',
358 param=param_name,
359 expr_form='pillar')
360 return salt_output[salt_output.keys()[0]]
361
362 def f_touch_master(self, path, makedirs=True):
363 _kwarg = {
364 "makedirs": makedirs
365 }
366 salt_output = self.cmd(
Alexe0c5b9e2019-04-23 18:51:23 -0500367 self.master_node,
savex4448e132018-04-25 15:51:14 +0200368 "file.touch",
369 param=path,
370 kwarg=_kwarg
371 )
372 return salt_output[salt_output.keys()[0]]
373
374 def f_append_master(self, path, strings_list, makedirs=True):
375 _kwarg = {
376 "makedirs": makedirs
377 }
378 _args = [path]
379 _args.extend(strings_list)
380 salt_output = self.cmd(
Alexe0c5b9e2019-04-23 18:51:23 -0500381 self.master_node,
savex4448e132018-04-25 15:51:14 +0200382 "file.write",
383 param=_args,
384 kwarg=_kwarg
385 )
386 return salt_output[salt_output.keys()[0]]
387
388 def mkdir(self, target, path, tgt_type=None):
389 salt_output = self.cmd(
390 target,
391 "file.mkdir",
392 param=path,
393 expr_form=tgt_type
394 )
395 return salt_output
396
397 def f_manage_file(self, target_path, source,
398 sfn='', ret='{}',
399 source_hash={},
400 user='root', group='root', backup_mode='755',
401 show_diff='base',
402 contents='', makedirs=True):
403 """
404 REST variation of file.get_managed
405 CLI execution goes like this (10 agrs):
Alex3ebc5632019-04-18 16:47:18 -0500406 salt cfg01\\* file.manage_file /root/test_scripts/pkg_versions.py
savex4448e132018-04-25 15:51:14 +0200407 '' '{}' /root/diff_pkg_version.py
408 '{hash_type: 'md5', 'hsum': <md5sum>}' root root '755' base ''
409 makedirs=True
410 param: name - target file placement when managed
411 param: source - source for the file
412 """
413 _source_hash = {
414 "hash_type": "md5",
415 "hsum": 000
416 }
417 _arg = [
418 target_path,
419 sfn,
420 ret,
421 source,
422 _source_hash,
423 user,
424 group,
425 backup_mode,
426 show_diff,
427 contents
428 ]
429 _kwarg = {
430 "makedirs": makedirs
431 }
432 salt_output = self.cmd(
Alexe0c5b9e2019-04-23 18:51:23 -0500433 self.master_node,
savex4448e132018-04-25 15:51:14 +0200434 "file.manage_file",
435 param=_arg,
436 kwarg=_kwarg
437 )
438 return salt_output[salt_output.keys()[0]]
439
440 def cache_file(self, target, source_path):
441 salt_output = self.cmd(
442 target,
443 "cp.cache_file",
444 param=source_path
445 )
446 return salt_output[salt_output.keys()[0]]
447
448 def get_file(self, target, source_path, target_path, tgt_type=None):
449 return self.cmd(
450 target,
451 "cp.get_file",
452 param=[source_path, target_path],
453 expr_form=tgt_type
454 )
455
456 @staticmethod
457 def compound_string_from_list(nodes_list):
458 return " or ".join(nodes_list)