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