1# -*- coding: utf-8 -*- 2# Based on local.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com> 3# Based on chroot.py (c) 2013, Maykel Moya <mmoya@speedyrails.com> 4# Based on func.py 5# (c) 2014, Michael Scherer <misc@zarb.org> 6# (c) 2017 Ansible Project 7# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 8 9from __future__ import (absolute_import, division, print_function) 10__metaclass__ = type 11 12DOCUMENTATION = ''' 13 author: Michael Scherer (@mscherer) <misc@zarb.org> 14 name: saltstack 15 short_description: Allow ansible to piggyback on salt minions 16 description: 17 - This allows you to use existing Saltstack infrastructure to connect to targets. 18''' 19 20import os 21import base64 22 23from ansible import errors 24from ansible.plugins.connection import ConnectionBase 25 26HAVE_SALTSTACK = False 27try: 28 import salt.client as sc 29 HAVE_SALTSTACK = True 30except ImportError: 31 pass 32 33 34class Connection(ConnectionBase): 35 """ Salt-based connections """ 36 37 has_pipelining = False 38 # while the name of the product is salt, naming that module salt cause 39 # trouble with module import 40 transport = 'community.general.saltstack' 41 42 def __init__(self, play_context, new_stdin, *args, **kwargs): 43 super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs) 44 self.host = self._play_context.remote_addr 45 46 def _connect(self): 47 if not HAVE_SALTSTACK: 48 raise errors.AnsibleError("saltstack is not installed") 49 50 self.client = sc.LocalClient() 51 self._connected = True 52 return self 53 54 def exec_command(self, cmd, in_data=None, sudoable=False): 55 """ run a command on the remote minion """ 56 super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable) 57 58 if in_data: 59 raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining") 60 61 self._display.vvv("EXEC %s" % cmd, host=self.host) 62 # need to add 'true;' to work around https://github.com/saltstack/salt/issues/28077 63 res = self.client.cmd(self.host, 'cmd.exec_code_all', ['bash', 'true;' + cmd]) 64 if self.host not in res: 65 raise errors.AnsibleError("Minion %s didn't answer, check if salt-minion is running and the name is correct" % self.host) 66 67 p = res[self.host] 68 return p['retcode'], p['stdout'], p['stderr'] 69 70 @staticmethod 71 def _normalize_path(path, prefix): 72 if not path.startswith(os.path.sep): 73 path = os.path.join(os.path.sep, path) 74 normpath = os.path.normpath(path) 75 return os.path.join(prefix, normpath[1:]) 76 77 def put_file(self, in_path, out_path): 78 """ transfer a file from local to remote """ 79 80 super(Connection, self).put_file(in_path, out_path) 81 82 out_path = self._normalize_path(out_path, '/') 83 self._display.vvv("PUT %s TO %s" % (in_path, out_path), host=self.host) 84 with open(in_path, 'rb') as in_fh: 85 content = in_fh.read() 86 self.client.cmd(self.host, 'hashutil.base64_decodefile', [base64.b64encode(content), out_path]) 87 88 # TODO test it 89 def fetch_file(self, in_path, out_path): 90 """ fetch a file from remote to local """ 91 92 super(Connection, self).fetch_file(in_path, out_path) 93 94 in_path = self._normalize_path(in_path, '/') 95 self._display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self.host) 96 content = self.client.cmd(self.host, 'cp.get_file_str', [in_path])[self.host] 97 open(out_path, 'wb').write(content) 98 99 def close(self): 100 """ terminate the connection; nothing to do here """ 101 pass 102