fix various bugs in augeas modules

Fixes #29842
This commit is contained in:
Andrew Colin Kissa 2015-12-18 23:01:44 +02:00
parent 454efa1688
commit 98593a94d5
3 changed files with 307 additions and 56 deletions

View File

@ -49,6 +49,17 @@ log = logging.getLogger(__name__)
# Define the module's virtual name
__virtualname__ = 'augeas'
METHOD_MAP = {
'set': 'set',
'setm': 'setm',
'mv': 'move',
'move': 'move',
'ins': 'insert',
'insert': 'insert',
'rm': 'remove',
'remove': 'remove',
}
def __virtual__():
'''
@ -89,6 +100,13 @@ def _lstrip_word(word, prefix):
return word
def method_map():
'''
Make METHOD_MAP accessible via __salt__['augeas.method_map']()
'''
return METHOD_MAP
def execute(context=None, lens=None, commands=()):
'''
Execute Augeas commands
@ -103,17 +121,6 @@ def execute(context=None, lens=None, commands=()):
'''
ret = {'retval': False}
method_map = {
'set': 'set',
'setm': 'setm',
'mv': 'move',
'move': 'move',
'ins': 'insert',
'insert': 'insert',
'rm': 'remove',
'remove': 'remove',
}
arg_map = {
'set': (1, 2),
'setm': (2, 3),
@ -123,33 +130,42 @@ def execute(context=None, lens=None, commands=()):
}
def make_path(path):
'''
Return correct path
'''
if not context:
return path
path = path.lstrip('/')
if path:
if path.lstrip('/'):
if path.startswith(context):
return path
path = path.lstrip('/')
return os.path.join(context, path)
else:
return context
flags = _Augeas.NO_MODL_AUTOLOAD if lens else _Augeas.NONE
flags = _Augeas.NO_MODL_AUTOLOAD if lens and context else _Augeas.NONE
aug = _Augeas(flags=flags)
if lens:
if lens and context:
aug.add_transform(lens, re.sub('^/files', '', context))
aug.load()
for command in commands:
# first part up to space is always the command name (i.e.: set, move)
cmd, arg = command.split(' ', 1)
if cmd not in method_map:
ret['error'] = 'Command {0} is not supported (yet)'.format(cmd)
return ret
method = method_map[cmd]
nargs = arg_map[method]
try:
# first part up to space is always the command name (i.e.: set, move)
cmd, arg = command.split(' ', 1)
if cmd not in METHOD_MAP:
ret['error'] = 'Command {0} is not supported (yet)'.format(cmd)
return ret
method = METHOD_MAP[cmd]
nargs = arg_map[method]
parts = salt.utils.shlex_split(arg)
if len(parts) not in nargs:
err = '{0} takes {1} args: {2}'.format(method, nargs, parts)
raise ValueError(err)
@ -177,6 +193,9 @@ def execute(context=None, lens=None, commands=()):
args = {'path': path}
except ValueError as err:
log.error(str(err))
# if command.split fails arg will not be set
if 'arg' not in locals():
arg = command
ret['error'] = 'Invalid formatted command, ' \
'see debug log for details: {0}'.format(arg)
return ret

View File

@ -32,16 +32,79 @@ from __future__ import absolute_import
# Import python libs
import re
import os.path
import logging
import difflib
# Import Salt libs
import salt.utils
log = logging.getLogger(__name__)
def __virtual__():
return 'augeas' if 'augeas.execute' in __salt__ else False
def _workout_filename(filename):
'''
Recursively workout the file name from an augeas change
'''
if os.path.isfile(filename) or filename == '/':
if filename == '/':
filename = None
return filename
else:
return _workout_filename(os.path.dirname(filename))
def _check_filepath(changes):
'''
Ensure all changes are fully qualified and affect only one file.
This ensures that the diff output works and a state change is not
incorrectly reported.
'''
filename = None
for change_ in changes:
try:
cmd, arg = change_.split(' ', 1)
if cmd not in __salt__['augeas.method_map']():
error = 'Command {0} is not supported (yet)'.format(cmd)
raise ValueError(error)
method = __salt__['augeas.method_map']()[cmd]
parts = salt.utils.shlex_split(arg)
if method in ['set', 'setm', 'move', 'remove']:
filename_ = parts[0]
else:
_, _, filename_ = parts
if not filename_.startswith('/files'):
error = 'Changes should be prefixed with ' \
'/files if no context is provided,' \
' change: {0}'.format(change_)
raise ValueError(error)
filename_ = re.sub('^/files|/$', '', filename_)
if filename is not None:
if filename != filename_:
error = 'Changes should be made to one ' \
'file at a time, detected changes ' \
'to {0} and {1}'.format(filename, filename_)
raise ValueError(error)
filename = filename_
except (ValueError, IndexError) as err:
log.error(str(err))
if 'error' not in locals():
error = 'Invalid formatted command, ' \
'see debug log for details: {0}' \
.format(change_)
else:
error = str(err)
raise ValueError(error)
filename = _workout_filename(filename)
return filename
def change(name, context=None, changes=None, lens=None, **kwargs):
'''
.. versionadded:: 2014.7.0
@ -173,6 +236,16 @@ def change(name, context=None, changes=None, lens=None, **kwargs):
ret['comment'] = '\'changes\' must be specified as a list'
return ret
filename = None
if context is None:
try:
filename = _check_filepath(changes)
except ValueError as err:
ret['comment'] = 'Error: {0}'.format(str(err))
return ret
else:
filename = re.sub('^/files|/$', '', context)
if __opts__['test']:
ret['result'] = None
ret['comment'] = 'Executing commands'
@ -182,8 +255,7 @@ def change(name, context=None, changes=None, lens=None, **kwargs):
return ret
old_file = []
if context:
filename = re.sub('^/files|/$', '', context)
if filename is not None:
if os.path.isfile(filename):
with salt.utils.fopen(filename, 'r') as file_:
old_file = file_.readlines()

View File

@ -1,13 +1,17 @@
# -*- coding: utf-8 -*-
'''
:codeauthor: :email:`Jayesh Kariya <jayeshk@saltstack.com>`
:codeauthor: :email:`Andrew Colin Kissa <andrew@topdog.za.net>`
'''
# Import Python libs
from __future__ import absolute_import
import os
# Import Salt Testing Libs
from salttesting import skipIf, TestCase
from salttesting.mock import (
mock_open,
NO_MOCK,
NO_MOCK_REASON,
MagicMock,
@ -21,9 +25,6 @@ ensure_in_syspath('../../')
# Import Salt Libs
from salt.states import augeas
augeas.__opts__ = {}
augeas.__salt__ = {}
@skipIf(NO_MOCK, NO_MOCK_REASON)
class AugeasTestCase(TestCase):
@ -31,45 +32,204 @@ class AugeasTestCase(TestCase):
Test cases for salt.states.augeas
'''
# 'change' function tests: 1
def setUp(self):
augeas.__opts__ = {}
augeas.__salt__ = {}
self.name = 'zabbix'
self.context = '/files/etc/services'
self.changes = ['ins service-name after service-name[last()]',
'set service-name[last()] zabbix-agent']
self.fp_changes = [
'ins service-name after /files/etc/services/service-name[last()]',
'set /files/etc/services/service-name[last()] zabbix-agent']
self.ret = {'name': self.name,
'result': False,
'changes': {},
'comment': ''}
method_map = {
'set': 'set',
'setm': 'setm',
'mv': 'move',
'move': 'move',
'ins': 'insert',
'insert': 'insert',
'rm': 'remove',
'remove': 'remove',
}
self.mock_method_map = MagicMock(return_value=method_map)
def test_change(self):
def test_change_non_list_changes(self):
'''
Test to issue changes to Augeas, optionally for a specific context,
with a specific lens.
Test if none list changes handled correctly
'''
name = 'zabbix'
context = '/files/etc/services'
changes = ['ins service-name after service-name[last()]',
'set service-name[last()] zabbix-agent']
changes_ret = {'updates': changes}
ret = {'name': name,
'result': False,
'changes': {},
'comment': ''}
comt = ('\'changes\' must be specified as a list')
ret.update({'comment': comt})
self.assertDictEqual(augeas.change(name), ret)
self.ret.update({'comment': comt})
self.assertDictEqual(augeas.change(self.name), self.ret)
def test_change_in_test_mode(self):
'''
Test test mode handling
'''
comt = ('Executing commands in file "/files/etc/services":\n'
'ins service-name after service-name[last()]'
'\nset service-name[last()] zabbix-agent')
ret.update({'comment': comt, 'result': None})
self.ret.update({'comment': comt, 'result': None})
with patch.dict(augeas.__opts__, {'test': True}):
self.assertDictEqual(augeas.change(name, context, changes), ret)
self.assertDictEqual(
augeas.change(self.name, self.context, self.changes),
self.ret)
def test_change_no_context_without_full_path(self):
'''
Test handling of no context without full path
'''
comt = 'Error: Changes should be prefixed with /files if no ' \
'context is provided, change: {0}'\
.format(self.changes[0])
self.ret.update({'comment': comt, 'result': False})
with patch.dict(augeas.__opts__, {'test': False}):
mock = MagicMock(return_value={'retval': False, 'error': 'error'})
with patch.dict(augeas.__salt__, {'augeas.execute': mock}):
ret.update({'comment': 'Error: error', 'result': False})
self.assertDictEqual(augeas.change(name, changes=changes), ret)
mock_dict_ = {'augeas.method_map': self.mock_method_map}
with patch.dict(augeas.__salt__, mock_dict_):
self.assertDictEqual(
augeas.change(self.name, changes=self.changes),
self.ret)
mock = MagicMock(return_value={'retval': True})
with patch.dict(augeas.__salt__, {'augeas.execute': mock}):
ret.update({'comment': 'Changes have been saved',
'result': True, 'changes': changes_ret})
self.assertDictEqual(augeas.change(name, changes=changes), ret)
def test_change_no_context_with_full_path_fail(self):
'''
Test handling of no context with full path with execute fail
'''
self.ret.update({'comment': 'Error: error', 'result': False})
with patch.dict(augeas.__opts__, {'test': False}):
mock_execute = MagicMock(
return_value=dict(retval=False, error='error'))
mock_dict_ = {'augeas.execute': mock_execute,
'augeas.method_map': self.mock_method_map}
with patch.dict(augeas.__salt__, mock_dict_):
self.assertDictEqual(
augeas.change(self.name, changes=self.fp_changes),
self.ret)
def test_change_no_context_with_full_path_pass(self):
'''
Test handling of no context with full path with execute pass
'''
self.ret.update(dict(comment='Changes have been saved',
result=True,
changes={'diff': '+ zabbix-agent'}))
with patch.dict(augeas.__opts__, {'test': False}):
mock_execute = MagicMock(return_value=dict(retval=True))
mock_dict_ = {'augeas.execute': mock_execute,
'augeas.method_map': self.mock_method_map}
with patch.dict(augeas.__salt__, mock_dict_):
mock_filename = MagicMock(return_value='/etc/services')
with patch.object(augeas, '_workout_filename', mock_filename):
with patch('salt.utils.fopen', MagicMock(mock_open)):
mock_diff = MagicMock(return_value=['+ zabbix-agent'])
with patch('difflib.unified_diff', mock_diff):
self.assertDictEqual(augeas.change(self.name,
changes=self.fp_changes),
self.ret)
def test_change_no_context_without_full_path_invalid_cmd(self):
'''
Test handling of invalid commands when no context supplied
'''
self.ret.update(dict(comment='Error: Command det is not supported (yet)',
result=False))
with patch.dict(augeas.__opts__, {'test': False}):
mock_execute = MagicMock(return_value=dict(retval=True))
mock_dict_ = {'augeas.execute': mock_execute,
'augeas.method_map': self.mock_method_map}
with patch.dict(augeas.__salt__, mock_dict_):
changes = ['det service-name[last()] zabbix-agent']
self.assertDictEqual(augeas.change(self.name,
changes=changes),
self.ret)
def test_change_no_context_without_full_path_invalid_change(self):
'''
Test handling of invalid change when no context supplied
'''
comt = ('Error: Invalid formatted command, see '
'debug log for details: require')
self.ret.update(dict(comment=comt,
result=False))
changes = ['require']
with patch.dict(augeas.__opts__, {'test': False}):
mock_execute = MagicMock(return_value=dict(retval=True))
mock_dict_ = {'augeas.execute': mock_execute,
'augeas.method_map': self.mock_method_map}
with patch.dict(augeas.__salt__, mock_dict_):
self.assertDictEqual(augeas.change(self.name,
changes=changes),
self.ret)
def test_change_no_context_with_full_path_multiple_files(self):
'''
Test handling of different paths with no context supplied
'''
changes = ['set /files/etc/hosts/service-name test',
'set /files/etc/services/service-name test']
filename = '/etc/hosts/service-name'
filename_ = '/etc/services/service-name'
comt = 'Error: Changes should be made to one file at a time, ' \
'detected changes to {0} and {1}'.format(filename, filename_)
self.ret.update(dict(comment=comt,
result=False))
with patch.dict(augeas.__opts__, {'test': False}):
mock_execute = MagicMock(return_value=dict(retval=True))
mock_dict_ = {'augeas.execute': mock_execute,
'augeas.method_map': self.mock_method_map}
with patch.dict(augeas.__salt__, mock_dict_):
self.assertDictEqual(augeas.change(self.name,
changes=changes),
self.ret)
def test_change_with_context_without_full_path_fail(self):
'''
Test handling of context without full path fails
'''
self.ret.update(dict(comment='Error: error', result=False))
with patch.dict(augeas.__opts__, {'test': False}):
mock_execute = MagicMock(
return_value=dict(retval=False, error='error'))
mock_dict_ = {'augeas.execute': mock_execute,
'augeas.method_map': self.mock_method_map}
with patch.dict(augeas.__salt__, mock_dict_):
with patch('salt.utils.fopen', MagicMock(mock_open)):
self.assertDictEqual(augeas.change(self.name,
context=self.context,
changes=self.changes),
self.ret)
def test_change_with_context_without_old_file(self):
'''
Test handling of context without oldfile pass
'''
self.ret.update(dict(comment='Changes have been saved',
result=True,
changes={'updates': self.changes}))
with patch.dict(augeas.__opts__, {'test': False}):
mock_execute = MagicMock(return_value=dict(retval=True))
mock_dict_ = {'augeas.execute': mock_execute,
'augeas.method_map': self.mock_method_map}
with patch.dict(augeas.__salt__, mock_dict_):
mock_isfile = MagicMock(return_value=False)
with patch.object(os.path, 'isfile', mock_isfile):
self.assertDictEqual(augeas.change(self.name,
context=self.context,
changes=self.changes),
self.ret)
if __name__ == '__main__':