Merge pull request #25685 from twangboy/fix_25594

Fixed regex issues with comment and uncomment
This commit is contained in:
Thomas S Hatch 2015-07-28 09:29:49 -06:00
commit 1fae76d53c
4 changed files with 225 additions and 52 deletions

View file

@ -982,12 +982,11 @@ def uncomment(path,
salt '*' file.uncomment /etc/hosts.deny 'ALL: PARANOID'
'''
pattern = '^{0}{1}'.format(char, regex.lstrip('^').rstrip('$'))
repl = "{0}".format(regex.lstrip('^').rstrip('$'))
return replace(path=path,
pattern=pattern,
repl=repl,
backup=backup)
return comment_line(path=path,
regex=regex,
char=char,
cmnt=False,
backup=backup)
def comment(path,
@ -1025,11 +1024,174 @@ def comment(path,
salt '*' file.comment /etc/modules pcspkr
'''
repl = "{0}{1}".format(char, regex.lstrip('^').rstrip('$'))
return replace(path=path,
pattern=regex,
repl=repl,
backup=backup)
return comment_line(path=path,
regex=regex,
char=char,
cmnt=True,
backup=backup)
def comment_line(path,
regex,
char='#',
cmnt=True,
backup='.bak'):
r'''
Comment or Uncomment a line in a text file.
:param path: string
The full path to the text file.
:param regex: string
A regex expression that begins with ``^`` that will find the line you wish
to comment. Can be as simple as ``^color =``
:param char: string
The character used to comment a line in the type of file you're referencing.
Default is ``#``
:param cmnt: boolean
True to comment the line. False to uncomment the line. Default is True.
:param backup: string
The file extension to give the backup file. Default is ``.bak``
:return: boolean
Returns True if successful, False if not
CLI Example:
The following example will comment out the ``pcspkr`` line in the
``/etc/modules`` file using the default ``#`` character and create a backup
file named ``modules.bak``
.. code-block:: bash
salt '*' file.comment_line '/etc/modules' '^pcspkr'
CLI Example:
The following example will uncomment the ``log_level`` setting in ``minion``
config file if it is set to either ``warning``, ``info``, or ``debug`` using
the ``#`` character and create a backup file named ``minion.bk``
.. code-block:: bash
salt '*' file.comment_line 'C:\salt\conf\minion' '^log_level: (warning|info|debug)' '#' False '.bk'
'''
# Get the regex for comment or uncomment
if cmnt:
regex = '{0}({1}){2}'.format(
'^' if regex.startswith('^') else '',
regex.lstrip('^').rstrip('$'),
'$' if regex.endswith('$') else '')
else:
regex = '^{0}({1}){2}'.format(
char,
regex.lstrip('^').rstrip('$'),
'$' if regex.endswith('$') else '')
# Load the real path to the file
path = os.path.realpath(os.path.expanduser(path))
# Make sure the file exists
if not os.path.exists(path):
raise SaltInvocationError('File not found: {0}'.format(path))
# Make sure it is a text file
if not salt.utils.istextfile(path):
raise SaltInvocationError(
'Cannot perform string replacements on a binary file: {0}'.format(path))
# First check the whole file, determine whether to make the replacement
# Searching first avoids modifying the time stamp if there are no changes
found = False
# Dictionaries for comparing changes
orig_file = []
new_file = []
# Buffer size for fopen
bufsize = os.path.getsize(path)
try:
# Use a read-only handle to open the file
with salt.utils.fopen(path,
mode='rb',
buffering=bufsize) as r_file:
# Loop through each line of the file and look for a match
for line in r_file:
# Is it in this line
if re.match(regex, line):
# Load lines into dictionaries, set found to True
orig_file.append(line)
if cmnt:
new_file.append('{0}{1}'.format(char, line))
else:
new_file.append(line.lstrip(char))
found = True
except (OSError, IOError) as exc:
raise CommandExecutionError(
"Unable to open file '{0}'. "
"Exception: {1}".format(path, exc)
)
# We've searched the whole file. If we didn't find anything, return False
if not found:
return False
# Create a copy to read from and to use as a backup later
try:
temp_file = _mkstemp_copy(path=path, preserve_inode=False)
except (OSError, IOError) as exc:
raise CommandExecutionError("Exception: {0}".format(exc))
try:
# Open the file in write mode
with salt.utils.fopen(path,
mode='wb',
buffering=bufsize) as w_file:
try:
# Open the temp file in read mode
with salt.utils.fopen(temp_file,
mode='rb',
buffering=bufsize) as r_file:
# Loop through each line of the file and look for a match
for line in r_file:
try:
# Is it in this line
if re.match(regex, line):
# Write the new line
if cmnt:
w_file.write('{0}{1}'.format(char, line))
else:
w_file.write(line.lstrip(char))
else:
# Write the existing line (no change)
w_file.write(line)
except (OSError, IOError) as exc:
raise CommandExecutionError(
"Unable to write file '{0}'. Contents may "
"be truncated. Temporary file contains copy "
"at '{1}'. "
"Exception: {2}".format(path, temp_file, exc)
)
except (OSError, IOError) as exc:
raise CommandExecutionError("Exception: {0}".format(exc))
except (OSError, IOError) as exc:
raise CommandExecutionError("Exception: {0}".format(exc))
# Move the backup file to the original directory
backup_name = '{0}{1}'.format(path, backup)
try:
shutil.move(temp_file, backup_name)
except (OSError, IOError) as exc:
raise CommandExecutionError(
"Unable to move the temp file '{0}' to the "
"backup file '{1}'. "
"Exception: {2}".format(path, temp_file, exc)
)
# Return a diff using the two dictionaries
return ''.join(difflib.unified_diff(orig_file, new_file))
def _get_flags(flags):

View file

@ -57,7 +57,7 @@ from salt.modules.file import (check_hash, # pylint: disable=W0611
access, copy, readdir, rmdir, truncate, replace, delete_backup,
search, _get_flags, extract_hash, _error, _sed_esc, _psed,
RE_FLAG_TABLE, blockreplace, prepend, seek_read, seek_write, rename,
lstat, path_exists_glob, HASHES, comment, uncomment)
lstat, path_exists_glob, HASHES, comment, uncomment, comment_line)
from salt.utils import namespaced_function as _namespaced_function
@ -79,7 +79,7 @@ def __virtual__():
global remove, append, _error, directory_exists, touch, contains
global contains_regex, contains_regex_multiline, contains_glob
global find, psed, get_sum, check_hash, get_hash, delete_backup
global get_diff, _get_flags, extract_hash
global get_diff, _get_flags, extract_hash, comment_line
global access, copy, readdir, rmdir, truncate, replace, search
global _binary_replace, _get_bkroot, list_backups, restore_backup
global blockreplace, prepend, seek_read, seek_write, rename, lstat
@ -135,6 +135,7 @@ def __virtual__():
path_exists_glob = _namespaced_function(path_exists_glob, globals())
comment = _namespaced_function(comment, globals())
uncomment = _namespaced_function(uncomment, globals())
comment_line = _namespaced_function(comment_line, globals())
_mkstemp_copy = _namespaced_function(_mkstemp_copy, globals())
return __virtualname__

View file

@ -287,7 +287,6 @@ def _load_accumulators():
def _persist_accummulators(accumulators, accumulators_deps):
accumm_data = {'accumulators': accumulators,
'accumulators_deps': accumulators_deps}
@ -918,17 +917,17 @@ def symlink(
if os.path.lexists(backupname):
if not force:
return _error(ret, ((
'File exists where the backup target {0} should go'
).format(backupname)))
'File exists where the backup target {0} should go'
).format(backupname)))
elif os.path.isfile(backupname):
os.remove(backupname)
elif os.path.isdir(backupname):
shutil.rmtree(backupname)
else:
return _error(ret, ((
'Something exists where the backup target {0}'
'should go'
).format(backupname)))
'Something exists where the backup target {0}'
'should go'
).format(backupname)))
os.rename(name, backupname)
elif force:
# Remove whatever is in the way
@ -945,8 +944,8 @@ def symlink(
.format(name)))
else:
return _error(ret, ((
'Directory exists where the symlink {0} should be'
).format(name)))
'Directory exists where the symlink {0} should be'
).format(name)))
if not os.path.exists(name):
# The link is not present, make it
@ -1424,7 +1423,8 @@ def managed(name,
if not replace and os.path.exists(name):
# Check and set the permissions if necessary
ret, _ = __salt__['file.check_perms'](name, ret, user, group, mode, follow_symlinks)
ret, _ = __salt__['file.check_perms'](name, ret, user, group, mode,
follow_symlinks)
if __opts__['test']:
ret['comment'] = 'File {0} not updated'.format(name)
elif not ret['changes'] and ret['result']:
@ -1763,8 +1763,8 @@ def directory(name,
if os.path.lexists(backupname):
if not force:
return _error(ret, ((
'File exists where the backup target {0} should go'
).format(backupname)))
'File exists where the backup target {0} should go'
).format(backupname)))
elif os.path.isfile(backupname):
os.remove(backupname)
elif os.path.islink(backupname):
@ -1773,9 +1773,9 @@ def directory(name,
shutil.rmtree(backupname)
else:
return _error(ret, ((
'Something exists where the backup target {0}'
'should go'
).format(backupname)))
'Something exists where the backup target {0}'
'should go'
).format(backupname)))
os.rename(name, backupname)
elif force:
# Remove whatever is in the way
@ -1790,10 +1790,12 @@ def directory(name,
else:
if os.path.isfile(name):
return _error(
ret, 'Specified location {0} exists and is a file'.format(name))
ret,
'Specified location {0} exists and is a file'.format(name))
elif os.path.islink(name):
return _error(
ret, 'Specified location {0} exists and is a symlink'.format(name))
ret,
'Specified location {0} exists and is a symlink'.format(name))
if __opts__['test']:
ret['result'], ret['comment'] = _check_directory(
@ -1854,7 +1856,8 @@ def directory(name,
if not isinstance(recurse, list):
ret['result'] = False
ret['comment'] = '"recurse" must be formed as a list of strings'
elif not set(['user', 'group', 'mode', 'ignore_files', 'ignore_dirs']) >= set(recurse):
elif not set(['user', 'group', 'mode', 'ignore_files',
'ignore_dirs']) >= set(recurse):
ret['result'] = False
ret['comment'] = 'Types for "recurse" limited to "user", ' \
'"group", "mode", "ignore_files, and "ignore_dirs"'
@ -2307,7 +2310,7 @@ def recurse(name,
vdir = set()
srcpath = source[7:]
if not srcpath.endswith('/'):
#we're searching for things that start with this *directory*.
# we're searching for things that start with this *directory*.
# use '/' since #master only runs on POSIX
srcpath = srcpath + '/'
fns_ = __salt__['cp.list_master'](__env__, srcpath)
@ -2339,7 +2342,7 @@ def recurse(name,
if len(relpieces) > maxdepth + 1:
continue
#- Check if it is to be excluded. Match only part of the path
# - Check if it is to be excluded. Match only part of the path
# relative to the target directory
if not salt.utils.check_include_exclude(
relname, include_pat, exclude_pat):
@ -2722,7 +2725,10 @@ def comment(name, regex, char='#', backup='.bak'):
'''
name = os.path.expanduser(name)
ret = {'name': name, 'changes': {}, 'result': False, 'comment': ''}
ret = {'name': name,
'changes': {},
'result': False,
'comment': ''}
if not name:
return _error(ret, 'Must provide name to file.comment')
@ -2733,8 +2739,8 @@ def comment(name, regex, char='#', backup='.bak'):
unanchor_regex = regex.lstrip('^').rstrip('$')
# Make sure the pattern appears in the file before continuing
if not __salt__['file.contains_regex_multiline'](name, regex):
if __salt__['file.contains_regex_multiline'](name, unanchor_regex):
if not __salt__['file.search'](name, regex):
if __salt__['file.search'](name, unanchor_regex):
ret['comment'] = 'Pattern already commented'
ret['result'] = True
return ret
@ -2747,15 +2753,15 @@ def comment(name, regex, char='#', backup='.bak'):
return ret
with salt.utils.fopen(name, 'rb') as fp_:
slines = fp_.readlines()
# Perform the edit
__salt__['file.comment'](name, regex, char, backup)
__salt__['file.comment_line'](name, regex, char, True, backup)
with salt.utils.fopen(name, 'rb') as fp_:
nlines = fp_.readlines()
# Check the result
ret['result'] = __salt__['file.contains_regex_multiline'](name,
unanchor_regex)
ret['result'] = __salt__['file.search'](name, unanchor_regex)
if slines != nlines:
if not salt.utils.istextfile(name):
@ -2810,7 +2816,10 @@ def uncomment(name, regex, char='#', backup='.bak'):
'''
name = os.path.expanduser(name)
ret = {'name': name, 'changes': {}, 'result': False, 'comment': ''}
ret = {'name': name,
'changes': {},
'result': False,
'comment': ''}
if not name:
return _error(ret, 'Must provide name to file.uncomment')
@ -2819,12 +2828,12 @@ def uncomment(name, regex, char='#', backup='.bak'):
return _error(ret, check_msg)
# Make sure the pattern appears in the file
if __salt__['file.contains_regex_multiline'](
if __salt__['file.search'](
name, '^[ \t]*{0}'.format(regex.lstrip('^'))):
ret['comment'] = 'Pattern already uncommented'
ret['result'] = True
return ret
elif __salt__['file.contains_regex_multiline'](
elif __salt__['file.search'](
name, '{0}[ \t]*{1}'.format(char, regex.lstrip('^'))):
# Line exists and is commented
pass
@ -2840,13 +2849,13 @@ def uncomment(name, regex, char='#', backup='.bak'):
slines = fp_.readlines()
# Perform the edit
__salt__['file.uncomment'](name, regex, char, backup)
__salt__['file.comment_line'](name, regex, char, False, backup)
with salt.utils.fopen(name, 'rb') as fp_:
nlines = fp_.readlines()
# Check the result
ret['result'] = __salt__['file.contains_regex_multiline'](
ret['result'] = __salt__['file.search'](
name, '^[ \t]*{0}'.format(regex.lstrip('^'))
)
@ -3057,7 +3066,7 @@ def append(name,
if not retry_res:
return _error(ret, check_msg)
#Follow the original logic and re-assign 'text' if using source(s)...
# Follow the original logic and re-assign 'text' if using source(s)...
if sl_:
tmpret = _get_template_texts(source_list=sl_,
template=template,
@ -3222,7 +3231,7 @@ def prepend(name,
if not check_res:
return _error(ret, check_msg)
#Follow the original logic and re-assign 'text' if using source(s)...
# Follow the original logic and re-assign 'text' if using source(s)...
if sl_:
tmpret = _get_template_texts(source_list=sl_,
template=template,
@ -4113,7 +4122,8 @@ def serialize(name,
if ret['changes']:
ret['result'] = None
ret['comment'] = 'Dataset will be serialized and stored into {0}'.format(name)
ret['comment'] = 'Dataset will be serialized and stored into {0}'.format(
name)
else:
ret['result'] = True
ret['comment'] = 'The file {0} is in the correct state'.format(name)
@ -4246,7 +4256,7 @@ def mknod(name, ntype, major=0, minor=0, user=None, group=None, mode='0600'):
ret['comment'] = (
'Character device {0} exists and has a different '
'major/minor {1}/{2}. Cowardly refusing to continue'
.format(name, devmaj, devmin)
.format(name, devmaj, devmin)
)
# Check the perms
else:

View file

@ -864,7 +864,7 @@ class FileTestCase(TestCase):
with patch.object(os.path, 'isabs', mock_t):
with patch.dict(filestate.__salt__,
{'file.contains_regex_multiline': mock}):
{'file.search': mock}):
comt = ('Pattern already commented')
ret.update({'comment': comt, 'result': True})
self.assertDictEqual(filestate.comment(name, regex), ret)
@ -874,8 +874,8 @@ class FileTestCase(TestCase):
self.assertDictEqual(filestate.comment(name, regex), ret)
with patch.dict(filestate.__salt__,
{'file.contains_regex_multiline': mock_t,
'file.comment': mock_t}):
{'file.search': mock_t,
'file.comment_line': mock_t}):
with patch.dict(filestate.__opts__, {'test': True}):
comt = ('File {0} is set to be updated'.format(name))
ret.update({'comment': comt, 'result': None})
@ -918,8 +918,8 @@ class FileTestCase(TestCase):
with patch.object(os.path, 'isabs', mock_t):
with patch.dict(filestate.__salt__,
{'file.contains_regex_multiline': mock,
'file.uncomment': mock_t}):
{'file.search': mock,
'file.comment_line': mock_t}):
comt = ('Pattern already uncommented')
ret.update({'comment': comt, 'result': True})
self.assertDictEqual(filestate.uncomment(name, regex), ret)