mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
Merge pull request #25685 from twangboy/fix_25594
Fixed regex issues with comment and uncomment
This commit is contained in:
commit
1fae76d53c
4 changed files with 225 additions and 52 deletions
|
@ -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):
|
||||
|
|
|
@ -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__
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue