1"""
2Wrapper for rsync
3
4.. versionadded:: 2014.1.0
5
6This data can also be passed into :ref:`pillar <pillar-walk-through>`.
7Options passed into opts will overwrite options passed into pillar.
8"""
9
10import errno
11import logging
12import re
13import tempfile
14
15import salt.utils.files
16import salt.utils.path
17from salt.exceptions import CommandExecutionError, SaltInvocationError
18
19log = logging.getLogger(__name__)
20
21__virtualname__ = "rsync"
22
23
24def __virtual__():
25    """
26    Only load module if rsync binary is present
27    """
28    if salt.utils.path.which("rsync"):
29        return __virtualname__
30    return (
31        False,
32        "The rsync execution module cannot be loaded: "
33        "the rsync binary is not in the path.",
34    )
35
36
37def _check(delete, force, update, passwordfile, exclude, excludefrom, dryrun, rsh):
38    """
39    Generate rsync options
40    """
41    options = ["-avz"]
42
43    if delete:
44        options.append("--delete")
45    if force:
46        options.append("--force")
47    if update:
48        options.append("--update")
49    if rsh:
50        options.append("--rsh={}".format(rsh))
51    if passwordfile:
52        options.extend(["--password-file", passwordfile])
53    if excludefrom:
54        options.extend(["--exclude-from", excludefrom])
55        if exclude:
56            exclude = False
57    if exclude:
58        if isinstance(exclude, list):
59            for ex_ in exclude:
60                options.extend(["--exclude", ex_])
61        else:
62            options.extend(["--exclude", exclude])
63    if dryrun:
64        options.append("--dry-run")
65    return options
66
67
68def rsync(
69    src,
70    dst,
71    delete=False,
72    force=False,
73    update=False,
74    passwordfile=None,
75    exclude=None,
76    excludefrom=None,
77    dryrun=False,
78    rsh=None,
79    additional_opts=None,
80    saltenv="base",
81):
82    """
83    .. versionchanged:: 2016.3.0
84        Return data now contains just the output of the rsync command, instead
85        of a dictionary as returned from :py:func:`cmd.run_all
86        <salt.modules.cmdmod.run_all>`.
87
88    Rsync files from src to dst
89
90    src
91        The source location where files will be rsynced from.
92
93    dst
94        The destination location where files will be rsynced to.
95
96    delete : False
97        Whether to enable the rsync `--delete` flag, which
98        will delete extraneous files from dest dirs
99
100    force : False
101        Whether to enable the rsync `--force` flag, which
102        will force deletion of dirs even if not empty.
103
104    update : False
105        Whether to enable the rsync `--update` flag, which
106        forces rsync to skip any files which exist on the
107        destination and have a modified time that is newer
108        than the source file.
109
110    passwordfile
111        A file that contains a password for accessing an
112        rsync daemon.  The file should contain just the
113        password.
114
115    exclude
116        Whether to enable the rsync `--exclude` flag, which
117        will exclude files matching a PATTERN.
118
119    excludefrom
120        Whether to enable the rsync `--excludefrom` flag, which
121        will read exclude patterns from a file.
122
123    dryrun : False
124        Whether to enable the rsync `--dry-run` flag, which
125        will perform a trial run with no changes made.
126
127    rsh
128        Whether to enable the rsync `--rsh` flag, to
129        specify the remote shell to use.
130
131    additional_opts
132        Any additional rsync options, should be specified as a list.
133
134    saltenv
135        Specify a salt fileserver environment to be used.
136
137    CLI Example:
138
139    .. code-block:: bash
140
141        salt '*' rsync.rsync /path/to/src /path/to/dest delete=True update=True passwordfile=/etc/pass.crt exclude=exclude/dir
142        salt '*' rsync.rsync /path/to/src delete=True excludefrom=/xx.ini
143        salt '*' rsync.rsync /path/to/src delete=True exclude='[exclude1/dir,exclude2/dir]' additional_opts='["--partial", "--bwlimit=5000"]'
144    """
145    if not src:
146        src = __salt__["config.option"]("rsync.src")
147    if not dst:
148        dst = __salt__["config.option"]("rsync.dst")
149    if not delete:
150        delete = __salt__["config.option"]("rsync.delete")
151    if not force:
152        force = __salt__["config.option"]("rsync.force")
153    if not update:
154        update = __salt__["config.option"]("rsync.update")
155    if not passwordfile:
156        passwordfile = __salt__["config.option"]("rsync.passwordfile")
157    if not exclude:
158        exclude = __salt__["config.option"]("rsync.exclude")
159    if not excludefrom:
160        excludefrom = __salt__["config.option"]("rsync.excludefrom")
161    if not dryrun:
162        dryrun = __salt__["config.option"]("rsync.dryrun")
163    if not rsh:
164        rsh = __salt__["config.option"]("rsync.rsh")
165    if not src or not dst:
166        raise SaltInvocationError("src and dst cannot be empty")
167
168    tmp_src = None
169    if src.startswith("salt://"):
170        _src = src
171        _path = re.sub("salt://", "", _src)
172        src_is_dir = False
173        if _path in __salt__["cp.list_master_dirs"](saltenv=saltenv):
174            src_is_dir = True
175
176        if src_is_dir:
177            tmp_src = tempfile.mkdtemp()
178            dir_src = __salt__["cp.get_dir"](_src, tmp_src, saltenv)
179            if dir_src:
180                src = tmp_src
181                # Ensure src ends in / so we
182                # get the contents not the tmpdir
183                # itself.
184                if not src.endswith("/"):
185                    src = "{}/".format(src)
186            else:
187                raise CommandExecutionError("{} does not exist".format(src))
188        else:
189            tmp_src = salt.utils.files.mkstemp()
190            file_src = __salt__["cp.get_file"](_src, tmp_src, saltenv)
191            if file_src:
192                src = tmp_src
193            else:
194                raise CommandExecutionError("{} does not exist".format(src))
195
196    option = _check(
197        delete, force, update, passwordfile, exclude, excludefrom, dryrun, rsh
198    )
199
200    if additional_opts and isinstance(additional_opts, list):
201        option = option + additional_opts
202
203    cmd = ["rsync"] + option + [src, dst]
204    log.debug("Running rsync command: %s", cmd)
205    try:
206        return __salt__["cmd.run_all"](cmd, python_shell=False)
207    except OSError as exc:
208        raise CommandExecutionError(exc.strerror)
209    finally:
210        if tmp_src:
211            __salt__["file.remove"](tmp_src)
212
213
214def version():
215    """
216    .. versionchanged:: 2016.3.0
217        Return data now contains just the version number as a string, instead
218        of a dictionary as returned from :py:func:`cmd.run_all
219        <salt.modules.cmdmod.run_all>`.
220
221    Returns rsync version
222
223    CLI Example:
224
225    .. code-block:: bash
226
227        salt '*' rsync.version
228    """
229    try:
230        out = __salt__["cmd.run_stdout"](["rsync", "--version"], python_shell=False)
231    except OSError as exc:
232        raise CommandExecutionError(exc.strerror)
233    try:
234        return out.split("\n")[0].split()[2]
235    except IndexError:
236        raise CommandExecutionError("Unable to determine rsync version")
237
238
239def config(conf_path="/etc/rsyncd.conf"):
240    """
241    .. versionchanged:: 2016.3.0
242        Return data now contains just the contents of the rsyncd.conf as a
243        string, instead of a dictionary as returned from :py:func:`cmd.run_all
244        <salt.modules.cmdmod.run_all>`.
245
246    Returns the contents of the rsync config file
247
248    conf_path : /etc/rsyncd.conf
249        Path to the config file
250
251    CLI Example:
252
253    .. code-block:: bash
254
255        salt '*' rsync.config
256    """
257    ret = ""
258    try:
259        with salt.utils.files.fopen(conf_path, "r") as fp_:
260            for line in fp_:
261                ret += salt.utils.stringutils.to_unicode(line)
262    except OSError as exc:
263        if exc.errno == errno.ENOENT:
264            raise CommandExecutionError("{} does not exist".format(conf_path))
265        elif exc.errno == errno.EACCES:
266            raise CommandExecutionError(
267                "Unable to read {}, access denied".format(conf_path)
268            )
269        elif exc.errno == errno.EISDIR:
270            raise CommandExecutionError(
271                "Unable to read {}, path is a directory".format(conf_path)
272            )
273        else:
274            raise CommandExecutionError("Error {}: {}".format(exc.errno, exc.strerror))
275    else:
276        return ret
277