File 0031-Port-rsync-state-from-2016.3.patch of Package salt
From c7a06f6029ea1c8c7e61916d5bd09992d25702e3 Mon Sep 17 00:00:00 2001
From: Bo Maryniuk <bo@suse.de>
Date: Fri, 25 Nov 2016 15:34:33 +0100
Subject: [PATCH 31/38] Port rsync state from 2016.3
* Port rsync state from 2016.3
---
salt/modules/rsync.py | 115 +++++++++++++++++++++-----------------
salt/states/rsync.py | 149 ++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 214 insertions(+), 50 deletions(-)
create mode 100644 salt/states/rsync.py
diff --git a/salt/modules/rsync.py b/salt/modules/rsync.py
index 36763e2..35e518f 100644
--- a/salt/modules/rsync.py
+++ b/salt/modules/rsync.py
@@ -10,16 +10,17 @@ Options passed into opts will overwrite options passed into pillar.
from __future__ import absolute_import
# Import python libs
+import errno
import logging
-import os
# Import salt libs
-from salt.exceptions import CommandExecutionError
+import salt.utils
+from salt.exceptions import CommandExecutionError, SaltInvocationError
log = logging.getLogger(__name__)
-def _check(delete, force, update, passwordfile, exclude, excludefrom):
+def _check(delete, force, update, passwordfile, exclude, excludefrom, dryrun):
'''
Generate rsync options
'''
@@ -32,15 +33,16 @@ def _check(delete, force, update, passwordfile, exclude, excludefrom):
if update:
options.append('--update')
if passwordfile:
- options.append('--password-file={0}'.format(passwordfile))
+ options.extend(['--password-file', passwordfile])
if excludefrom:
- options.append('--exclude-from={0}'.format(excludefrom))
+ options.extend(['--exclude-from', excludefrom])
if exclude:
- exclude = None
+ exclude = False
if exclude:
- options.append('--exclude={0}'.format(exclude))
-
- return ' '.join(options)
+ options.extend(['--exclude', exclude])
+ if dryrun:
+ options.append('--dry-run')
+ return options
def rsync(src,
@@ -51,8 +53,12 @@ def rsync(src,
passwordfile=None,
exclude=None,
excludefrom=None,
- ):
+ dryrun=False):
'''
+ Return data now contains just the output of the rsync command, instead
+ of a dictionary as returned from :py:func:`cmd.run_all
+ <salt.modules.cmdmod.run_all>`.
+
Rsync files from src to dst
CLI Example:
@@ -78,30 +84,27 @@ def rsync(src,
exclude = __salt__['config.option']('rsync.exclude')
if not excludefrom:
excludefrom = __salt__['config.option']('rsync.excludefrom')
+ if not dryrun:
+ dryrun = __salt__['config.option']('rsync.dryrun')
if not src or not dst:
- raise CommandExecutionError('ERROR: src and dst cannot be empty.')
-
- option = _check(delete, force, update, passwordfile, exclude, excludefrom)
- cmd = (
- r'''rsync {option} {src} {dst}'''
- .format(
- option=option,
- src=src,
- dst=dst,
- )
- )
+ raise SaltInvocationError('src and dst cannot be empty')
+ option = _check(delete, force, update, passwordfile, exclude, excludefrom, dryrun)
+ cmd = ['rsync'] + option + [src, dst]
+ log.debug('Running rsync command: {0}'.format(cmd))
try:
- ret = __salt__['cmd.run_all'](cmd, python_shell=False)
+ return __salt__['cmd.run_all'](cmd, python_shell=False)
except (IOError, OSError) as exc:
raise CommandExecutionError(exc.strerror)
- return ret
-
def version():
'''
- Return rsync version
+ Return data now contains just the version number as a string, instead
+ of a dictionary as returned from :py:func:`cmd.run_all
+ <salt.modules.cmdmod.run_all>`.
+
+ Returns rsync version
CLI Example:
@@ -109,21 +112,28 @@ def version():
salt '*' rsync.version
'''
-
- cmd = (r'''rsync --version''')
-
try:
- ret = __salt__['cmd.run_all'](cmd)
+ out = __salt__['cmd.run_stdout'](
+ ['rsync', '--version'],
+ python_shell=False)
except (IOError, OSError) as exc:
raise CommandExecutionError(exc.strerror)
-
- ret['stdout'] = ret['stdout'].split('\n')[0].split()[2]
- return ret
+ try:
+ return out.split('\n')[0].split()[2]
+ except IndexError:
+ raise CommandExecutionError('Unable to determine rsync version')
-def config(confile='/etc/rsyncd.conf'):
+def config(conf_path='/etc/rsyncd.conf'):
'''
- Return rsync config
+ Return data now contains just the contents of the rsyncd.conf as a
+ string, instead of a dictionary as returned from :py:func:`cmd.run_all
+ <salt.modules.cmdmod.run_all>`.
+
+ Returns the contents of the rsync config file
+
+ conf_path : /etc/rsyncd.conf
+ Path to the config file
CLI Example:
@@ -131,20 +141,25 @@ def config(confile='/etc/rsyncd.conf'):
salt '*' rsync.config
'''
-
- if not os.path.isfile(confile):
- raise CommandExecutionError('{0!r} does not exit'.format(confile))
-
- cmd = (
- r'''cat {confile}'''
- .format(
- confile=confile
- )
- )
-
+ ret = ''
try:
- ret = __salt__['cmd.run_all'](cmd, python_shell=False)
- except (IOError, OSError) as exc:
- raise CommandExecutionError(exc.strerror)
-
- return ret
+ with salt.utils.fopen(conf_path, 'r') as fp_:
+ for line in fp_:
+ ret += line
+ except IOError as exc:
+ if exc.errno == errno.ENOENT:
+ raise CommandExecutionError('{0} does not exist'.format(conf_path))
+ elif exc.errno == errno.EACCES:
+ raise CommandExecutionError(
+ 'Unable to read {0}, access denied'.format(conf_path)
+ )
+ elif exc.errno == errno.EISDIR:
+ raise CommandExecutionError(
+ 'Unable to read {0}, path is a directory'.format(conf_path)
+ )
+ else:
+ raise CommandExecutionError(
+ 'Error {0}: {1}'.format(exc.errno, exc.strerror)
+ )
+ else:
+ return ret
diff --git a/salt/states/rsync.py b/salt/states/rsync.py
new file mode 100644
index 0000000..7485da8
--- /dev/null
+++ b/salt/states/rsync.py
@@ -0,0 +1,149 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2015 SUSE LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+'''
+State to synchronize files and directories with rsync.
+
+.. versionadded:: 2016.3.0
+
+.. code-block:: yaml
+
+ /opt/user-backups:
+ rsync.synchronized:
+ - source: /home
+ - force: True
+
+'''
+
+from __future__ import absolute_import
+import salt.utils
+import os
+
+
+def __virtual__():
+ '''
+ Only if Rsync is available.
+
+ :return:
+ '''
+ return salt.utils.which('rsync') and 'rsync' or False
+
+
+def _get_summary(rsync_out):
+ '''
+ Get summary from the rsync successfull output.
+ '''
+
+ return "- " + "\n- ".join([elm for elm in rsync_out.split("\n\n")[-1].replace(" ", "\n").split("\n") if elm])
+
+
+def _get_changes(rsync_out):
+ '''
+ Get changes from the rsync successfull output.
+ '''
+ copied = list()
+ deleted = list()
+
+ for line in rsync_out.split("\n\n")[0].split("\n")[1:]:
+ if line.startswith("deleting "):
+ deleted.append(line.split(" ", 1)[-1])
+ else:
+ copied.append(line)
+
+ return {
+ 'copied': os.linesep.join(sorted(copied)) or "N/A",
+ 'deleted': os.linesep.join(sorted(deleted)) or "N/A",
+ }
+
+
+def synchronized(name, source,
+ delete=False,
+ force=False,
+ update=False,
+ passwordfile=None,
+ exclude=None,
+ excludefrom=None,
+ prepare=False,
+ dryrun=False):
+ '''
+ Guarantees that the source directory is always copied to the target.
+
+ name
+ Name of the target directory.
+
+ source
+ Source directory.
+
+ prepare
+ Create destination directory if it does not exists.
+
+ delete
+ Delete extraneous files from the destination dirs (True or False)
+
+ force
+ Force deletion of dirs even if not empty
+
+ update
+ Skip files that are newer on the receiver (True or False)
+
+ passwordfile
+ Read daemon-access password from the file (path)
+
+ exclude
+ Exclude files, that matches pattern.
+
+ excludefrom
+ Read exclude patterns from the file (path)
+
+ dryrun
+ Perform a trial run with no changes made. Is the same as
+ doing test=True
+
+ .. versionadded:: 2016.3.1
+ '''
+
+ ret = {'name': name, 'changes': {}, 'result': True, 'comment': ''}
+
+ if not os.path.exists(source):
+ ret['result'] = False
+ ret['comment'] = "Source directory {src} was not found.".format(src=source)
+ elif not os.path.exists(name) and not force and not prepare:
+ ret['result'] = False
+ ret['comment'] = "Destination directory {dest} was not found.".format(dest=name)
+ else:
+ if not os.path.exists(name) and prepare:
+ os.makedirs(name)
+
+ if __opts__['test']:
+ dryrun = True
+
+ result = __salt__['rsync.rsync'](source, name, delete=delete, force=force, update=update,
+ passwordfile=passwordfile, exclude=exclude, excludefrom=excludefrom,
+ dryrun=dryrun)
+
+ if __opts__['test'] or dryrun:
+ ret['result'] = None
+ ret['comment'] = _get_summary(result['stdout'])
+ return ret
+
+ if result.get('retcode'):
+ ret['result'] = False
+ ret['comment'] = result['stderr']
+ ret['changes'] = result['stdout']
+ else:
+ ret['comment'] = _get_summary(result['stdout'])
+ ret['changes'] = _get_changes(result['stdout'])
+
+ return ret
--
2.10.2