Merge branch '2018.3' into update-tests

This commit is contained in:
Mike Place 2018-12-03 13:04:16 -07:00 committed by GitHub
commit 36a4a1af49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 563 additions and 77 deletions

109
.ci/lint
View File

@ -3,7 +3,7 @@ pipeline {
options {
timestamps()
ansiColor('xterm')
timeout(time: 1, unit: 'HOURS')
timeout(time: 3, unit: 'HOURS')
}
environment {
PYENV_ROOT = "/usr/local/pyenv"
@ -14,7 +14,7 @@ pipeline {
stage('github-pending') {
steps {
githubNotify credentialsId: 'test-jenkins-credentials',
description: 'Testing lint...',
description: 'Python lint on changes...',
status: 'PENDING',
context: "jenkins/pr/lint"
}
@ -24,12 +24,14 @@ pipeline {
sh '''
# Need -M to detect renames otherwise they are reported as Delete and Add, need -C to detect copies, -C includes -M
# -M is on by default in git 2.9+
git diff --name-status -l99999 -C "origin/$CHANGE_TARGET" "origin/$BRANCH_NAME" > file-list-status.log
git diff --name-status -l99999 -C "origin/$CHANGE_TARGET" > file-list-status.log
# the -l increase the search limit, lets use awk so we do not need to repeat the search above.
gawk 'BEGIN {FS="\\t"} {if ($1 != "D") {print $NF}}' file-list-status.log > file-list-changed.log
gawk 'BEGIN {FS="\\t"} {if ($1 == "D") {print $NF}}' file-list-status.log > file-list-deleted.log
(git diff --name-status -l99999 -C "origin/$CHANGE_TARGET";echo "---";git diff --name-status -l99999 -C "origin/$BRANCH_NAME";printenv|grep -E '=[0-9a-z]{40,}+$|COMMIT=|BRANCH') > file-list-experiment.log
(git diff --name-status -l99999 -C "origin/$CHANGE_TARGET" "origin/$BRANCH_NAME";echo "---";git diff --name-status -l99999 -C "origin/$BRANCH_NAME";printenv|grep -E '=[0-9a-z]{40,}+$|COMMIT=|BRANCH') > file-list-experiment.log
touch pylint-report-salt.log pylint-report-tests.log
echo 254 > pylint-salt-chg.exit # assume failure
echo 254 > pylint-tests-chg.exit # assume failure
eval "$(pyenv init -)"
pyenv --version
pyenv install --skip-existing 2.7.14
@ -41,52 +43,117 @@ pipeline {
archiveArtifacts artifacts: 'file-list-status.log,file-list-changed.log,file-list-deleted.log,file-list-experiment.log'
}
}
stage('linting') {
failFast false
stage('linting chg') {
parallel {
stage('salt linting') {
stage('lint salt') {
when {
expression { return readFile('file-list-changed.log') =~ /(?i)(^|\n)(salt\/.*\.py|setup\.py)\n/ }
}
steps {
sh '''
eval "$(pyenv init - --no-rehash)"
grep -Ei '^salt/.*\\.py$|^setup\\.py$' file-list-changed.log | xargs -r '--delimiter=\\n' tox -e pylint-salt | tee pylint-report-salt.log
# tee makes the exit/return code always 0
grep -Ei '^salt/.*\\.py$|^setup\\.py$' file-list-changed.log | (xargs -r '--delimiter=\\n' tox -e pylint-salt;echo "$?" > pylint-salt-chg.exit) | tee pylint-report-salt.log
# remove color escape coding
sed -ri 's/\\x1B\\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]//g' pylint-report-salt.log
read rc_exit < pylint-salt-chg.exit
exit "$rc_exit"
'''
archiveArtifacts artifacts: 'pylint-report-salt.log'
}
}
stage('test linting') {
stage('lint test') {
when {
expression { return readFile('file-list-changed.log') =~ /(?i)(^|\n)tests\/.*\.py\n/ }
}
steps {
sh '''
eval "$(pyenv init - --no-rehash)"
grep -Ei '^tests/.*\\.py$' file-list-changed.log | xargs -r '--delimiter=\\n' tox -e pylint-tests | tee pylint-report-tests.log
# tee makes the exit/return code always 0
grep -Ei '^tests/.*\\.py$' file-list-changed.log | (xargs -r '--delimiter=\\n' tox -e pylint-tests;echo "$?" > pylint-tests-chg.exit) | tee pylint-report-tests.log
# remove color escape coding
sed -ri 's/\\x1B\\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]//g' pylint-report-tests.log
read rc_exit < pylint-tests-chg.exit
exit "$rc_exit"
'''
archiveArtifacts artifacts: 'pylint-report-tests.log'
}
}
}
post {
always {
step([$class: 'WarningsPublisher',
parserConfigurations: [[
parserName: 'PyLint',
pattern: 'pylint-report*chg.log'
]],
failedTotalAll: '0',
useDeltaValues: false,
canRunOnFailed: true,
usePreviousBuildAsReference: true
])
}
}
}
stage('lint all') {
// perform a full linit if this is a merge forward and the change only lint passed.
when {
expression { return params.BRANCH_NAME =~ /(?i)^merge-/ && readFile('file-list-changed.log') =~ /^0/ }
}
parallel {
stage('begin') {
steps {
githubNotify credentialsId: 'test-jenkins-credentials',
description: 'Python lint on everything...',
status: 'PENDING',
context: "jenkins/pr/lint"
}
}
stage('lint salt') {
steps {
sh '''
eval "$(pyenv init - --no-rehash)"
(tox -e pylint-salt ; echo "$?" > pylint-salt-full.exit) | tee pylint-report-salt-full.log
# remove color escape coding
sed -ri 's/\\x1B\\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]//g' pylint-report-salt-full.log
read rc_exit < pylint-salt-full.exit
exit "$rc_exit"
'''
archiveArtifacts artifacts: 'pylint-report-salt-full.log'
}
}
stage('lint test') {
steps {
sh '''
eval "$(pyenv init - --no-rehash)"
(tox -e pylint-tests ; echo "$?" > pylint-tests-full.exit) | tee pylint-report-tests-full.log
# remove color escape coding
sed -ri 's/\\x1B\\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]//g' pylint-report-tests-full.log
read rc_exit < pylint-tests-full.exit
exit "$rc_exit"
'''
archiveArtifacts artifacts: 'pylint-report-tests-full.log'
}
}
}
post {
always {
step([$class: 'WarningsPublisher',
parserConfigurations: [[
parserName: 'PyLint',
pattern: 'pylint-report*full.log'
]],
failedTotalAll: '0',
useDeltaValues: false,
canRunOnFailed: true,
usePreviousBuildAsReference: true
])
}
}
}
}
post {
always {
step([$class: 'WarningsPublisher',
parserConfigurations: [[
parserName: 'PyLint',
pattern: 'pylint-report*.log'
]],
failedTotalAll: '0',
useDeltaValues: false,
canRunOnFailed: true,
usePreviousBuildAsReference: true
])
cleanWs()
}
success {
@ -97,7 +164,7 @@ pipeline {
}
failure {
githubNotify credentialsId: 'test-jenkins-credentials',
description: 'The lint job has failed',
description: 'The lint test has failed',
status: 'FAILURE',
context: "jenkins/pr/lint"
slackSend channel: "#jenkins-prod-pr",

4
.github/CODEOWNERS vendored
View File

@ -13,6 +13,7 @@ salt/*/*boto* @saltstack/team-boto
# Team Core
requirements/* @saltstack/team-core
rfcs/* @saltstack/team-core
salt/auth/* @saltstack/team-core
salt/cache/* @saltstack/team-core
salt/cli/* @saltstack/team-core
@ -73,3 +74,6 @@ salt/modules/reg.py @saltstack/team-windows
salt/states/reg.py @saltstack/team-windows
tests/*/*win* @saltstack/team-windows
tests/*/test_reg.py @saltstack/team-windows
# Jenkins Integration
.ci/* @saltstack/saltstack-release-engineering @saltstack/team-core @saltstack/team-windows

View File

@ -4,3 +4,11 @@ In Progress: Salt 2018.3.4 Release Notes
Version 2018.3.4 is an **unreleased** bugfix release for :ref:`2018.3.0 <release-2018-3-0>`.
This release is still in progress and has not been released yet.
State Changes
=============
- The :py:func:`host.present <salt.states.host.present>` state can now remove
the specified hostname from IPs not specified in the state. This can be done
by setting the newly-added ``clean`` argument to ``True``.

View File

@ -169,7 +169,13 @@ def cert(name,
res = __salt__['cmd.run_all'](' '.join(cmd))
if res['retcode'] != 0:
return {'result': False, 'comment': 'Certificate {0} renewal failed with:\n{1}'.format(name, res['stderr'])}
if 'expand' in res['stderr']:
cmd.append('--expand')
res = __salt__['cmd.run_all'](' '.join(cmd))
if res['retcode'] != 0:
return {'result': False, 'comment': 'Certificate {0} renewal failed with:\n{1}'.format(name, res['stderr'])}
else:
return {'result': False, 'comment': 'Certificate {0} renewal failed with:\n{1}'.format(name, res['stderr'])}
if 'no action taken' in res['stdout']:
comment = 'Certificate {0} unchanged'.format(cert_file)

View File

@ -43,6 +43,7 @@ except ImportError:
# Import salt libs
import salt.utils.args
import salt.utils.data
import salt.utils.stringutils
from salt.exceptions import SaltInvocationError
log = logging.getLogger(__name__)
@ -494,7 +495,7 @@ def ls(path, load_path=None): # pylint: disable=C0103
def _match(path):
''' Internal match function '''
try:
matches = aug.match(path)
matches = aug.match(salt.utils.stringutils.to_str(path))
except RuntimeError:
return {}

View File

@ -2277,6 +2277,8 @@ def replace(path,
# Just search; bail as early as a match is found
if re.search(cpattern, r_data):
return True # `with` block handles file closure
else:
return False
else:
result, nrepl = re.subn(cpattern,
repl.replace('\\', '\\\\') if backslash_literal else repl,

View File

@ -5,6 +5,7 @@ Manage the information in the hosts file
# Import Python libs
from __future__ import absolute_import, print_function, unicode_literals
import errno
import os
# Import salt libs
@ -22,7 +23,12 @@ def __get_hosts_filename():
'''
Return the path to the appropriate hosts file
'''
return __salt__['config.option']('hosts.file')
try:
return __context__['hosts.__get_hosts_filename']
except KeyError:
__context__['hosts.__get_hosts_filename'] = \
__salt__['config.option']('hosts.file')
return __context__['hosts.__get_hosts_filename']
def _get_or_create_hostfile():
@ -43,26 +49,35 @@ def _list_hosts():
'''
Return the hosts found in the hosts file in as an OrderedDict
'''
count = 0
hfn = __get_hosts_filename()
ret = odict.OrderedDict()
if not os.path.isfile(hfn):
try:
return __context__['hosts._list_hosts']
except KeyError:
count = 0
hfn = __get_hosts_filename()
ret = odict.OrderedDict()
try:
with salt.utils.files.fopen(hfn) as ifile:
for line in ifile:
line = salt.utils.stringutils.to_unicode(line).strip()
if not line:
continue
if line.startswith('#'):
ret.setdefault('comment-{0}'.format(count), []).append(line)
count += 1
continue
if '#' in line:
line = line[:line.index('#')].strip()
comps = line.split()
ip = comps.pop(0)
ret.setdefault(ip, []).extend(comps)
except (IOError, OSError) as exc:
salt.utils.files.process_read_exception(exc, hfn, ignore=errno.ENOENT)
# Don't set __context__ since we weren't able to read from the
# hosts file.
return ret
__context__['hosts._list_hosts'] = ret
return ret
with salt.utils.files.fopen(hfn) as ifile:
for line in ifile:
line = salt.utils.stringutils.to_unicode(line).strip()
if not line:
continue
if line.startswith('#'):
ret.setdefault('comment-{0}'.format(count), []).append(line)
count += 1
continue
if '#' in line:
line = line[:line.index('#')].strip()
comps = line.split()
ip = comps.pop(0)
ret.setdefault(ip, []).extend(comps)
return ret
def list_hosts():
@ -133,7 +148,10 @@ def has_pair(ip, alias):
salt '*' hosts.has_pair <ip> <alias>
'''
hosts = _list_hosts()
return ip in hosts and alias in hosts[ip]
try:
return alias in hosts[ip]
except KeyError:
return False
def set_host(ip, alias):
@ -157,6 +175,9 @@ def set_host(ip, alias):
if not os.path.isfile(hfn):
return False
# Make sure future calls to _list_hosts() will re-read the file
__context__.pop('hosts._list_hosts', None)
line_to_add = salt.utils.stringutils.to_bytes(
ip + '\t\t' + alias + os.linesep
)
@ -203,6 +224,8 @@ def rm_host(ip, alias):
'''
if not has_pair(ip, alias):
return True
# Make sure future calls to _list_hosts() will re-read the file
__context__.pop('hosts._list_hosts', None)
hfn = _get_or_create_hostfile()
with salt.utils.files.fopen(hfn, 'rb') as fp_:
lines = fp_.readlines()
@ -251,6 +274,10 @@ def add_host(ip, alias):
return True
hosts = _list_hosts()
# Make sure future calls to _list_hosts() will re-read the file
__context__.pop('hosts._list_hosts', None)
inserted = False
for i, h in six.iteritems(hosts):
for j in range(len(h)):

View File

@ -16,6 +16,9 @@ you can specify what ruby version and gemset to target.
'''
from __future__ import absolute_import, unicode_literals, print_function
import salt.utils
import re
import logging
log = logging.getLogger(__name__)
@ -84,10 +87,29 @@ def installed(name, # pylint: disable=C0103
'Use of argument ruby found, but neither rvm or rbenv is installed'
)
gems = __salt__['gem.list'](name, ruby, gem_bin=gem_bin, runas=user)
if name in gems and version is not None and str(version) in gems[name]:
ret['result'] = True
ret['comment'] = 'Gem is already installed.'
return ret
if name in gems and version is not None:
match = re.match(r'(>=|>|<|<=)', version)
if match:
# Grab the comparison
cmpr = match.group()
# Clear out 'default:' and any whitespace
installed_version = re.sub('default: ', '', gems[name][0]).strip()
# Clear out comparison from version and whitespace
desired_version = re.sub(cmpr, '', version).strip()
if salt.utils.compare_versions(installed_version,
cmpr,
desired_version):
ret['result'] = True
ret['comment'] = 'Installed Gem meets version requirements.'
return ret
else:
if str(version) in gems[name]:
ret['result'] = True
ret['comment'] = 'Gem is already installed.'
return ret
elif name in gems and version is None:
ret['result'] = True
ret['comment'] = 'Gem is already installed.'

View File

@ -67,7 +67,7 @@ from salt.ext import six
import salt.utils.validate.net
def present(name, ip): # pylint: disable=C0103
def present(name, ip, clean=False): # pylint: disable=C0103
'''
Ensures that the named host is present with the given ip
@ -75,36 +75,92 @@ def present(name, ip): # pylint: disable=C0103
The host to assign an ip to
ip
The ip addr(s) to apply to the host
The ip addr(s) to apply to the host. Can be a single IP or a list of IP
addresses.
clean : False
Remove any entries which don't match those configured in the ``ip``
option.
.. versionadded:: 2018.3.4
'''
ret = {'name': name,
'changes': {},
'result': None,
'result': None if __opts__['test'] else True,
'comment': ''}
if not isinstance(ip, list):
ip = [ip]
all_hosts = __salt__['hosts.list_hosts']()
comments = []
for _ip in ip:
if __salt__['hosts.has_pair'](_ip, name):
ret['result'] = True
comments.append('Host {0} ({1}) already present'.format(name, _ip))
to_add = set()
to_remove = set()
# First check for IPs not currently in the hosts file
to_add.update([(addr, name) for addr in ip if addr not in all_hosts])
# Now sweep through the hosts file and look for entries matching either the
# IP address(es) or hostname.
for addr, aliases in six.iteritems(all_hosts):
if addr not in ip:
if name in aliases:
# Found match for hostname, but the corresponding IP is not in
# our list, so we need to remove it.
if clean:
to_remove.add((addr, name))
else:
ret.setdefault('warnings', []).append(
'Host {0} present for IP address {1}. To get rid of '
'this warning, either run this state with \'clean\' '
'set to True to remove {0} from {1}, or add {1} to '
'the \'ip\' argument.'.format(name, addr)
)
else:
if __opts__['test']:
comments.append('Host {0} ({1}) needs to be added/updated'.format(name, _ip))
if name in aliases:
# No changes needed for this IP address and hostname
comments.append(
'Host {0} ({1}) already present'.format(name, addr)
)
else:
if salt.utils.validate.net.ipv4_addr(_ip) or salt.utils.validate.net.ipv6_addr(_ip):
if __salt__['hosts.add_host'](_ip, name):
ret['changes'] = {'host': name}
ret['result'] = True
comments.append('Added host {0} ({1})'.format(name, _ip))
else:
ret['result'] = False
comments.append('Failed to set host')
# IP address listed in hosts file, but hostname is not present.
# We will need to add it.
if salt.utils.validate.net.ip_addr(addr):
to_add.add((addr, name))
else:
ret['result'] = False
comments.append('Invalid IP Address for {0} ({1})'.format(name, _ip))
comments.append(
'Invalid IP Address for {0} ({1})'.format(name, addr)
)
for addr, name in to_add:
if __opts__['test']:
comments.append(
'Host {0} ({1}) would be added'.format(name, addr)
)
else:
if __salt__['hosts.add_host'](addr, name):
comments.append('Added host {0} ({1})'.format(name, addr))
else:
ret['result'] = False
comments.append('Failed to add host {0} ({1})'.format(name, addr))
continue
ret['changes'].setdefault('added', {}).setdefault(addr, []).append(name)
for addr, name in to_remove:
if __opts__['test']:
comments.append(
'Host {0} ({1}) would be removed'.format(name, addr)
)
else:
if __salt__['hosts.rm_host'](addr, name):
comments.append('Removed host {0} ({1})'.format(name, addr))
else:
ret['result'] = False
comments.append('Failed to remove host {0} ({1})'.format(name, addr))
continue
ret['changes'].setdefault('removed', {}).setdefault(addr, []).append(name)
ret['comment'] = '\n'.join(comments)
return ret

View File

@ -338,6 +338,7 @@ def dead(name,
else:
# process name doesn't exist
ret['comment'] = "Service {0} doesn't exist".format(name)
return ret
if is_stopped is True:
ret['comment'] = "Service {0} is not running".format(name)

View File

@ -205,10 +205,22 @@ def rename(src, dst):
os.rename(src, dst)
def process_read_exception(exc, path):
def process_read_exception(exc, path, ignore=None):
'''
Common code for raising exceptions when reading a file fails
The ignore argument can be an iterable of integer error codes (or a single
integer error code) that should be ignored.
'''
if ignore is not None:
if isinstance(ignore, six.integer_types):
ignore = (ignore,)
else:
ignore = ()
if exc.errno in ignore:
return
if exc.errno == errno.ENOENT:
raise CommandExecutionError('{0} does not exist'.format(path))
elif exc.errno == errno.EACCES:

View File

@ -81,6 +81,14 @@ def ipv6_addr(addr):
return __ip_addr(addr, socket.AF_INET6)
def ip_addr(addr):
'''
Returns True if the IPv4 or IPv6 address (and optional subnet) are valid,
otherwise returns False.
'''
return ipv4_addr(addr) or ipv6_addr(addr)
def netmask(mask):
'''
Returns True if the value passed is a valid netmask, otherwise return False

View File

@ -226,6 +226,22 @@ class FileReplaceTestCase(TestCase, LoaderModuleMockMixin):
'''
filemod.replace(self.tfile.name, r'Etiam', 123)
def test_search_only_return_true(self):
ret = filemod.replace(self.tfile.name,
r'Etiam', 'Salticus',
search_only=True)
self.assertIsInstance(ret, bool)
self.assertEqual(ret, True)
def test_search_only_return_false(self):
ret = filemod.replace(self.tfile.name,
r'Etian', 'Salticus',
search_only=True)
self.assertIsInstance(ret, bool)
self.assertEqual(ret, False)
class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin):
def setup_loader_modules(self):

View File

@ -47,6 +47,19 @@ class TestGemState(TestCase, LoaderModuleMockMixin):
ri=False, gem_bin=None
)
def test_installed_version(self):
gems = {'foo': ['1.0'], 'bar': ['2.0']}
gem_list = MagicMock(return_value=gems)
gem_install_succeeds = MagicMock(return_value=True)
with patch.dict(gem.__salt__, {'gem.list': gem_list}):
with patch.dict(gem.__salt__,
{'gem.install': gem_install_succeeds}):
ret = gem.installed('foo', version='>= 1.0')
self.assertEqual(True, ret['result'])
self.assertEqual('Installed Gem meets version requirements.',
ret['comment'])
def test_removed(self):
gems = ['foo', 'bar']
gem_list = MagicMock(return_value=gems)

View File

@ -15,7 +15,8 @@ from tests.support.mock import (
NO_MOCK,
NO_MOCK_REASON,
MagicMock,
patch
call,
patch,
)
@ -25,19 +26,260 @@ class HostTestCase(TestCase, LoaderModuleMockMixin):
Validate the host state
'''
def setup_loader_modules(self):
return {host: {}}
return {
host: {
'__opts__': {
'test': False,
},
},
}
def test_present(self):
'''
Test to ensures that the named host is present with the given ip
'''
ret = {'changes': {},
'comment': 'Host salt (127.0.0.1) already present',
'name': 'salt', 'result': True}
add_host = MagicMock(return_value=True)
rm_host = MagicMock(return_value=True)
hostname = 'salt'
ip_str = '127.0.0.1'
ip_list = ['10.1.2.3', '10.4.5.6']
mock = MagicMock(return_value=True)
with patch.dict(host.__salt__, {'hosts.has_pair': mock}):
self.assertDictEqual(host.present("salt", "127.0.0.1"), ret)
# Case 1: No match for hostname. Single IP address passed to the state.
list_hosts = MagicMock(return_value={
'127.0.0.1': ['localhost'],
})
with patch.dict(host.__salt__,
{'hosts.list_hosts': list_hosts,
'hosts.add_host': add_host,
'hosts.rm_host': rm_host}):
ret = host.present(hostname, ip_str)
assert ret['result'] is True
assert ret['comment'] == 'Added host {0} ({1})'.format(hostname, ip_str), ret['comment']
assert ret['changes'] == {
'added': {
ip_str: [hostname],
}
}, ret['changes']
expected = [call(ip_str, hostname)]
assert add_host.mock_calls == expected, add_host.mock_calls
assert rm_host.mock_calls == [], rm_host.mock_calls
# Case 2: No match for hostname. Multiple IP addresses passed to the
# state.
list_hosts = MagicMock(return_value={
'127.0.0.1': ['localhost'],
})
add_host.reset_mock()
rm_host.reset_mock()
with patch.dict(host.__salt__,
{'hosts.list_hosts': list_hosts,
'hosts.add_host': add_host,
'hosts.rm_host': rm_host}):
ret = host.present(hostname, ip_list)
assert ret['result'] is True
assert 'Added host {0} ({1})'.format(hostname, ip_list[0]) in ret['comment']
assert 'Added host {0} ({1})'.format(hostname, ip_list[1]) in ret['comment']
assert ret['changes'] == {
'added': {
ip_list[0]: [hostname],
ip_list[1]: [hostname],
}
}, ret['changes']
expected = sorted([call(x, hostname) for x in ip_list])
assert sorted(add_host.mock_calls) == expected, add_host.mock_calls
assert rm_host.mock_calls == [], rm_host.mock_calls
# Case 3: Match for hostname, but no matching IP. Single IP address
# passed to the state.
list_hosts = MagicMock(return_value={
'127.0.0.1': ['localhost'],
ip_list[0]: [hostname],
})
add_host.reset_mock()
rm_host.reset_mock()
with patch.dict(host.__salt__,
{'hosts.list_hosts': list_hosts,
'hosts.add_host': add_host,
'hosts.rm_host': rm_host}):
ret = host.present(hostname, ip_str)
assert ret['result'] is True
assert 'Added host {0} ({1})'.format(hostname, ip_str) in ret['comment']
assert 'Host {0} present for IP address {1}'.format(hostname, ip_list[0]) in ret['warnings'][0]
assert ret['changes'] == {
'added': {
ip_str: [hostname],
},
}, ret['changes']
expected = [call(ip_str, hostname)]
assert add_host.mock_calls == expected, add_host.mock_calls
assert rm_host.mock_calls == [], rm_host.mock_calls
# Case 3a: Repeat the above with clean=True
add_host.reset_mock()
rm_host.reset_mock()
with patch.dict(host.__salt__,
{'hosts.list_hosts': list_hosts,
'hosts.add_host': add_host,
'hosts.rm_host': rm_host}):
ret = host.present(hostname, ip_str, clean=True)
assert ret['result'] is True
assert 'Added host {0} ({1})'.format(hostname, ip_str) in ret['comment']
assert 'Removed host {0} ({1})'.format(hostname, ip_list[0]) in ret['comment']
assert ret['changes'] == {
'added': {
ip_str: [hostname],
},
'removed': {
ip_list[0]: [hostname],
}
}, ret['changes']
expected = [call(ip_str, hostname)]
assert add_host.mock_calls == expected, add_host.mock_calls
expected = [call(ip_list[0], hostname)]
assert rm_host.mock_calls == expected, rm_host.mock_calls
# Case 4: Match for hostname, but no matching IP. Multiple IP addresses
# passed to the state.
cur_ip = '1.2.3.4'
list_hosts = MagicMock(return_value={
'127.0.0.1': ['localhost'],
cur_ip: [hostname],
})
add_host.reset_mock()
rm_host.reset_mock()
with patch.dict(host.__salt__,
{'hosts.list_hosts': list_hosts,
'hosts.add_host': add_host,
'hosts.rm_host': rm_host}):
ret = host.present(hostname, ip_list)
assert ret['result'] is True
assert 'Added host {0} ({1})'.format(hostname, ip_list[0]) in ret['comment']
assert 'Added host {0} ({1})'.format(hostname, ip_list[1]) in ret['comment']
assert ret['changes'] == {
'added': {
ip_list[0]: [hostname],
ip_list[1]: [hostname],
},
}, ret['changes']
expected = sorted([call(x, hostname) for x in ip_list])
assert sorted(add_host.mock_calls) == expected, add_host.mock_calls
assert rm_host.mock_calls == [], rm_host.mock_calls
# Case 4a: Repeat the above with clean=True
add_host.reset_mock()
rm_host.reset_mock()
with patch.dict(host.__salt__,
{'hosts.list_hosts': list_hosts,
'hosts.add_host': add_host,
'hosts.rm_host': rm_host}):
ret = host.present(hostname, ip_list, clean=True)
assert ret['result'] is True
assert 'Added host {0} ({1})'.format(hostname, ip_list[0]) in ret['comment']
assert 'Added host {0} ({1})'.format(hostname, ip_list[1]) in ret['comment']
assert 'Removed host {0} ({1})'.format(hostname, cur_ip) in ret['comment']
assert ret['changes'] == {
'added': {
ip_list[0]: [hostname],
ip_list[1]: [hostname],
},
'removed': {
cur_ip: [hostname],
}
}, ret['changes']
expected = sorted([call(x, hostname) for x in ip_list])
assert sorted(add_host.mock_calls) == expected, add_host.mock_calls
expected = [call(cur_ip, hostname)]
assert rm_host.mock_calls == expected, rm_host.mock_calls
# Case 5: Multiple IP addresses passed to the state. One of them
# matches, the other does not. There is also a non-matching IP that
# must be removed.
cur_ip = '1.2.3.4'
list_hosts = MagicMock(return_value={
'127.0.0.1': ['localhost'],
cur_ip: [hostname],
ip_list[0]: [hostname],
})
add_host.reset_mock()
rm_host.reset_mock()
with patch.dict(host.__salt__,
{'hosts.list_hosts': list_hosts,
'hosts.add_host': add_host,
'hosts.rm_host': rm_host}):
ret = host.present(hostname, ip_list)
assert ret['result'] is True
assert 'Added host {0} ({1})'.format(hostname, ip_list[1]) in ret['comment']
assert ret['changes'] == {
'added': {
ip_list[1]: [hostname],
},
}, ret['changes']
expected = [call(ip_list[1], hostname)]
assert add_host.mock_calls == expected, add_host.mock_calls
assert rm_host.mock_calls == [], rm_host.mock_calls
# Case 5a: Repeat the above with clean=True
add_host.reset_mock()
rm_host.reset_mock()
with patch.dict(host.__salt__,
{'hosts.list_hosts': list_hosts,
'hosts.add_host': add_host,
'hosts.rm_host': rm_host}):
ret = host.present(hostname, ip_list, clean=True)
assert ret['result'] is True
assert 'Added host {0} ({1})'.format(hostname, ip_list[1]) in ret['comment']
assert 'Removed host {0} ({1})'.format(hostname, cur_ip) in ret['comment']
assert ret['changes'] == {
'added': {
ip_list[1]: [hostname],
},
'removed': {
cur_ip: [hostname],
}
}, ret['changes']
expected = [call(ip_list[1], hostname)]
assert add_host.mock_calls == expected, add_host.mock_calls
expected = [call(cur_ip, hostname)]
assert rm_host.mock_calls == expected, rm_host.mock_calls
# Case 6: Single IP address passed to the state, which matches the
# current configuration for that hostname. No changes should be made.
list_hosts = MagicMock(return_value={
ip_str: [hostname],
})
add_host.reset_mock()
rm_host.reset_mock()
with patch.dict(host.__salt__,
{'hosts.list_hosts': list_hosts,
'hosts.add_host': add_host,
'hosts.rm_host': rm_host}):
ret = host.present(hostname, ip_str)
assert ret['result'] is True
assert ret['comment'] == 'Host {0} ({1}) already present'.format(hostname, ip_str) in ret['comment']
assert ret['changes'] == {}, ret['changes']
assert add_host.mock_calls == [], add_host.mock_calls
assert rm_host.mock_calls == [], rm_host.mock_calls
# Case 7: Multiple IP addresses passed to the state, which both match
# the current configuration for that hostname. No changes should be
# made.
list_hosts = MagicMock(return_value={
ip_list[0]: [hostname],
ip_list[1]: [hostname],
})
add_host.reset_mock()
rm_host.reset_mock()
with patch.dict(host.__salt__,
{'hosts.list_hosts': list_hosts,
'hosts.add_host': add_host,
'hosts.rm_host': rm_host}):
ret = host.present(hostname, ip_list)
assert ret['result'] is True
assert 'Host {0} ({1}) already present'.format(hostname, ip_list[0]) in ret['comment']
assert 'Host {0} ({1}) already present'.format(hostname, ip_list[1]) in ret['comment']
assert ret['changes'] == {}, ret['changes']
assert add_host.mock_calls == [], add_host.mock_calls
assert rm_host.mock_calls == [], rm_host.mock_calls
def test_absent(self):
'''

View File

@ -31,6 +31,7 @@ import salt.config
import salt.log.setup
from salt.ext import six
import salt.utils.process
import salt.utils.platform
import salt.transport.server
import salt.transport.client
import salt.exceptions
@ -434,7 +435,7 @@ class PubServerChannel(TestCase, AdaptedConfigurationTestCaseMixin):
results.append(payload['jid'])
return results
@skipIf(salt.utils.is_windows(), 'Skip on Windows OS')
@skipIf(salt.utils.platform.is_windows(), 'Skip on Windows OS')
def test_publish_to_pubserv_ipc(self):
'''
Test sending 10K messags to ZeroMQPubServerChannel using IPC transport
@ -534,7 +535,7 @@ class PubServerChannel(TestCase, AdaptedConfigurationTestCaseMixin):
server_channel.pub_close()
assert len(results) == send_num, (len(results), set(expect).difference(results))
@skipIf(salt.utils.is_windows(), 'Skip on Windows OS')
@skipIf(salt.utils.platform.is_windows(), 'Skip on Windows OS')
def test_issue_36469_udp(self):
'''
Test sending both large and small messags to publisher using UDP