blob: 4aba178a7a37781c4c159ba9fe661ddd76c69b4b [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(
22 "Return value is not a json: '{}'".format(_raw)
23 )
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)
47 logger_cli.debug("### Calling salt: '{}'".format(_ssh_cmd))
48 _result = shell(_ssh_cmd)
49 if len(_result) < 1:
50 raise InvalidReturnException("Empty value returned for '{}".format(
51 _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
89 def get(self, path='', headers=default_headers, cookies=None):
90 _path = os.path.join(self.uri, path)
91 logger.debug("GET '{}'\nHeaders: '{}'\nCookies: {}".format(
92 _path,
93 headers,
94 cookies
95 ))
96 return requests.get(
97 _path,
98 headers=headers,
99 cookies=cookies
100 )
101
102 def post(self, data, path='', headers=default_headers, cookies=None):
103 if data is None:
104 data = {}
105 _path = os.path.join(self.uri, path)
106 if path == 'login':
Alex Savatieiev63576832019-02-27 15:46:26 -0600107 _data = str(data).replace(self._pass, "*****")
savex4448e132018-04-25 15:51:14 +0200108 else:
109 _data = data
110 logger.debug("POST '{}'\nHeaders: '{}'\nCookies: {}\nBody: {}".format(
111 _path,
112 headers,
113 cookies,
114 _data
115 ))
116 return requests.post(
117 os.path.join(self.uri, path),
118 headers=headers,
119 json=data,
120 cookies=cookies
121 )
122
123 def _login(self):
Alex Savatieiev63576832019-02-27 15:46:26 -0600124 # if there is no password - try to get local, if this available
125 if config.salt_env == "local":
126 _pass = get_local_password()
127 else:
128 _pass = get_remote_env_password()
savex4448e132018-04-25 15:51:14 +0200129 login_payload = {
130 'username': config.salt_user,
Alex Savatieiev63576832019-02-27 15:46:26 -0600131 'password': _pass,
savex4448e132018-04-25 15:51:14 +0200132 'eauth': 'pam'
133 }
Alex Savatieiev63576832019-02-27 15:46:26 -0600134 self._pass = _pass
savex4448e132018-04-25 15:51:14 +0200135 logger.debug("Logging in to salt master...")
136 _response = self.post(login_payload, path='login')
137
138 if _response.ok:
139 self._auth['response'] = _response.json()['return'][0]
140 self._auth['cookies'] = _response.cookies
141 self.default_headers['X-Auth-Token'] = \
142 self._auth['response']['token']
143 return self._auth['response']['token']
144 else:
145 raise EnvironmentError(
146 "HTTP:{}, Not authorized?".format(_response.status_code)
147 )
148
149 def salt_request(self, fn, *args, **kwargs):
150 # if token will expire in 5 min, re-login
151 if self._auth['response']['expire'] < time.time() + 300:
152 self._auth['response']['X-Auth-Token'] = self._login()
153
154 _method = getattr(self, fn)
155 _response = _method(*args, **kwargs)
156 self.last_response = _response
157 _content = "..."
158 _len = len(_response.content)
159 if _len < 1024:
160 _content = _response.content
161 logger.debug(
162 "Response (HTTP {}/{}), {}: {}".format(
163 _response.status_code,
164 _response.reason,
165 _len,
166 _content
167 )
168 )
169 if _response.ok:
170 return _response.json()['return']
171 else:
172 raise EnvironmentError(
173 "Salt Error: HTTP:{}, '{}'".format(
174 _response.status_code,
175 _response.reason
176 )
177 )
178
179
180class SaltRemote(SaltRest):
181 def __init__(self):
182 super(SaltRemote, self).__init__()
183
184 def cmd(
185 self,
186 tgt,
187 fun,
188 param=None,
189 client='local',
190 kwarg=None,
191 expr_form=None,
192 tgt_type=None,
193 timeout=None
194 ):
195 _timeout = timeout if timeout is not None else config.salt_timeout
196 _payload = {
197 'fun': fun,
198 'tgt': tgt,
199 'client': client,
200 'timeout': _timeout
201 }
202
203 if expr_form:
204 _payload['expr_form'] = expr_form
205 if tgt_type:
206 _payload['tgt_type'] = tgt_type
207 if param:
208 _payload['arg'] = param
209 if kwarg:
210 _payload['kwarg'] = kwarg
211
212 _response = self.salt_request('post', [_payload])
213 if isinstance(_response, list):
214 return _response[0]
215 else:
216 raise EnvironmentError(
217 "Unexpected response from from salt-api/LocalClient: "
218 "{}".format(_response)
219 )
220
221 def run(self, fun, kwarg=None):
222 _payload = {
223 'client': 'runner',
224 'fun': fun,
225 'timeout': config.salt_timeout
226 }
227
228 if kwarg:
229 _payload['kwarg'] = kwarg
230
231 _response = self.salt_request('post', [_payload])
232 if isinstance(_response, list):
233 return _response[0]
234 else:
235 raise EnvironmentError(
236 "Unexpected response from from salt-api/RunnerClient: "
237 "{}".format(_response)
238 )
239
240 def wheel(self, fun, arg=None, kwarg=None):
241 _payload = {
242 'client': 'wheel',
243 'fun': fun,
244 'timeout': config.salt_timeout
245 }
246
247 if arg:
248 _payload['arg'] = arg
249 if kwarg:
250 _payload['kwarg'] = kwarg
251
252 _response = self.salt_request('post', _payload)['data']
253 if _response['success']:
254 return _response
255 else:
256 raise EnvironmentError(
257 "Salt Error: '{}'".format(_response['return']))
258
259 def pillar_request(self, node_target, pillar_submodule, argument):
260 # example cli: 'salt "ctl01*" pillar.keys rsyslog'
261 _type = "compound"
262 if isinstance(node_target, list):
263 _type = "list"
264 return self.cmd(
265 node_target,
266 "pillar." + pillar_submodule,
267 argument,
268 expr_form=_type
269 )
270
271 def pillar_keys(self, node_target, argument):
272 return self.pillar_request(node_target, 'keys', argument)
273
274 def pillar_get(self, node_target, argument):
275 return self.pillar_request(node_target, 'get', argument)
276
277 def pillar_data(self, node_target, argument):
278 return self.pillar_request(node_target, 'data', argument)
279
280 def pillar_raw(self, node_target, argument):
281 return self.pillar_request(node_target, 'raw', argument)
282
283 def list_minions(self):
284 """
285 Fails in salt version 2016.3.8
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600286 Works starting from 2017.7.7
savex4448e132018-04-25 15:51:14 +0200287 api returns dict of minions with grains
288 """
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600289 return self.salt_request('get', 'minions')[0]
savex4448e132018-04-25 15:51:14 +0200290
291 def list_keys(self):
292 """
293 Fails in salt version 2016.3.8
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600294 Works starting from 2017.7.7
savex4448e132018-04-25 15:51:14 +0200295 api should return dict:
296 {
297 'local': [],
298 'minions': [],
299 'minions_denied': [],
300 'minions_pre': [],
301 'minions_rejected': [],
302 }
303 """
304 return self.salt_request('get', path='keys')
305
306 def get_status(self):
307 """
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600308 Fails in salt version 2017.7.7
savex4448e132018-04-25 15:51:14 +0200309 'runner' client is the equivalent of 'salt-run'
310 Returns the
311 """
312 return self.run(
313 'manage.status',
314 kwarg={'timeout': 10}
315 )
316
317 def get_active_nodes(self):
Alex Savatieiev9df93a92019-02-27 17:40:16 -0600318 """Used when other minion list metods fail
319
320 :return: json result from salt test.ping
321 """
savex4448e132018-04-25 15:51:14 +0200322 if config.skip_nodes:
323 logger.info("Nodes to be skipped: {0}".format(config.skip_nodes))
324 return self.cmd(
325 '* and not ' + list_to_target_string(
326 config.skip_nodes,
327 'and not'
328 ),
329 'test.ping',
330 expr_form='compound')
331 else:
332 return self.cmd('*', 'test.ping')
333
334 def get_monitoring_ip(self, param_name):
335 salt_output = self.cmd(
336 'docker:client:stack:monitoring',
337 'pillar.get',
338 param=param_name,
339 expr_form='pillar')
340 return salt_output[salt_output.keys()[0]]
341
342 def f_touch_master(self, path, makedirs=True):
343 _kwarg = {
344 "makedirs": makedirs
345 }
346 salt_output = self.cmd(
347 "cfg01*",
348 "file.touch",
349 param=path,
350 kwarg=_kwarg
351 )
352 return salt_output[salt_output.keys()[0]]
353
354 def f_append_master(self, path, strings_list, makedirs=True):
355 _kwarg = {
356 "makedirs": makedirs
357 }
358 _args = [path]
359 _args.extend(strings_list)
360 salt_output = self.cmd(
361 "cfg01*",
362 "file.write",
363 param=_args,
364 kwarg=_kwarg
365 )
366 return salt_output[salt_output.keys()[0]]
367
368 def mkdir(self, target, path, tgt_type=None):
369 salt_output = self.cmd(
370 target,
371 "file.mkdir",
372 param=path,
373 expr_form=tgt_type
374 )
375 return salt_output
376
377 def f_manage_file(self, target_path, source,
378 sfn='', ret='{}',
379 source_hash={},
380 user='root', group='root', backup_mode='755',
381 show_diff='base',
382 contents='', makedirs=True):
383 """
384 REST variation of file.get_managed
385 CLI execution goes like this (10 agrs):
386 salt cfg01\* file.manage_file /root/test_scripts/pkg_versions.py
387 '' '{}' /root/diff_pkg_version.py
388 '{hash_type: 'md5', 'hsum': <md5sum>}' root root '755' base ''
389 makedirs=True
390 param: name - target file placement when managed
391 param: source - source for the file
392 """
393 _source_hash = {
394 "hash_type": "md5",
395 "hsum": 000
396 }
397 _arg = [
398 target_path,
399 sfn,
400 ret,
401 source,
402 _source_hash,
403 user,
404 group,
405 backup_mode,
406 show_diff,
407 contents
408 ]
409 _kwarg = {
410 "makedirs": makedirs
411 }
412 salt_output = self.cmd(
413 "cfg01*",
414 "file.manage_file",
415 param=_arg,
416 kwarg=_kwarg
417 )
418 return salt_output[salt_output.keys()[0]]
419
420 def cache_file(self, target, source_path):
421 salt_output = self.cmd(
422 target,
423 "cp.cache_file",
424 param=source_path
425 )
426 return salt_output[salt_output.keys()[0]]
427
428 def get_file(self, target, source_path, target_path, tgt_type=None):
429 return self.cmd(
430 target,
431 "cp.get_file",
432 param=[source_path, target_path],
433 expr_form=tgt_type
434 )
435
436 @staticmethod
437 def compound_string_from_list(nodes_list):
438 return " or ".join(nodes_list)