Merge branch '2018.3' into fluorine

Conflicts:
	salt/modules/acme.py
This commit is contained in:
Gareth J. Greenaway 2018-12-05 21:54:14 +01:00
commit 1b4d398579
No known key found for this signature in database
GPG key ID: 10B62F8A7CAD7A41
17 changed files with 584 additions and 86 deletions

120
.ci/lint
View file

@ -3,7 +3,7 @@ pipeline {
options { options {
timestamps() timestamps()
ansiColor('xterm') ansiColor('xterm')
timeout(time: 1, unit: 'HOURS') timeout(time: 3, unit: 'HOURS')
} }
environment { environment {
PYENV_ROOT = "/usr/local/pyenv" PYENV_ROOT = "/usr/local/pyenv"
@ -14,7 +14,7 @@ pipeline {
stage('github-pending') { stage('github-pending') {
steps { steps {
githubNotify credentialsId: 'test-jenkins-credentials', githubNotify credentialsId: 'test-jenkins-credentials',
description: 'Testing lint...', description: 'Python lint on changes begins...',
status: 'PENDING', status: 'PENDING',
context: "jenkins/pr/lint" context: "jenkins/pr/lint"
} }
@ -24,12 +24,15 @@ pipeline {
sh ''' sh '''
# Need -M to detect renames otherwise they are reported as Delete and Add, need -C to detect copies, -C includes -M # 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+ # -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. # 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-changed.log
gawk 'BEGIN {FS="\\t"} {if ($1 == "D") {print $NF}}' file-list-status.log > file-list-deleted.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-salt-full.exit # assume failure
echo 254 > pylint-tests-chg.exit # assume failure
echo 254 > pylint-tests-full.exit # assume failure
eval "$(pyenv init -)" eval "$(pyenv init -)"
pyenv --version pyenv --version
pyenv install --skip-existing 2.7.14 pyenv install --skip-existing 2.7.14
@ -41,63 +44,126 @@ pipeline {
archiveArtifacts artifacts: 'file-list-status.log,file-list-changed.log,file-list-deleted.log,file-list-experiment.log' archiveArtifacts artifacts: 'file-list-status.log,file-list-changed.log,file-list-deleted.log,file-list-experiment.log'
} }
} }
stage('linting') { stage('linting chg') {
failFast false
parallel { parallel {
stage('salt linting') { stage('lint salt chg') {
when { when {
expression { return readFile('file-list-changed.log') =~ /(?i)(^|\n)(salt\/.*\.py|setup\.py)\n/ } expression { return readFile('file-list-changed.log') =~ /(?i)(^|\n)(salt\/.*\.py|setup\.py)\n/ }
} }
steps { steps {
sh ''' sh '''
eval "$(pyenv init - --no-rehash)" 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-chg.log
# remove color escape coding # remove color escape coding
sed -ri 's/\\x1B\\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]//g' pylint-report-salt.log sed -ri 's/\\x1B\\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]//g' pylint-report-salt-chg.log
read rc_exit < pylint-salt-chg.exit
exit "$rc_exit"
''' '''
archiveArtifacts artifacts: 'pylint-report-salt.log'
} }
} }
stage('test linting') { stage('lint test chg') {
when { when {
expression { return readFile('file-list-changed.log') =~ /(?i)(^|\n)tests\/.*\.py\n/ } expression { return readFile('file-list-changed.log') =~ /(?i)(^|\n)tests\/.*\.py\n/ }
} }
steps { steps {
sh ''' sh '''
eval "$(pyenv init - --no-rehash)" 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-chg.log
# remove color escape coding # remove color escape coding
sed -ri 's/\\x1B\\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]//g' pylint-report-tests.log sed -ri 's/\\x1B\\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]//g' pylint-report-tests-chg.log
read rc_exit < pylint-tests-chg.exit
exit "$rc_exit"
''' '''
archiveArtifacts artifacts: 'pylint-report-tests.log'
} }
} }
} }
post {
always {
archiveArtifacts artifacts: 'pylint-report-*-chg.log', allowEmptyArchive: true
step([$class: 'WarningsPublisher',
parserConfigurations: [[
parserName: 'PyLint',
pattern: 'pylint-report-*-chg.log'
]],
failedTotalAll: '0',
useDeltaValues: false,
canRunOnFailed: true,
usePreviousBuildAsReference: true
])
}
}
}
stage('linting all') {
// perform a full linit if this is a merge forward and the change only lint passed.
when {
expression { return params.CHANGE_BRANCH =~ /(?i)^merge[._-]/ }
}
parallel {
stage('setup full') {
steps {
githubNotify credentialsId: 'test-jenkins-credentials',
description: 'Python lint on everything begins...',
status: 'PENDING',
context: "jenkins/pr/lint"
}
}
stage('lint salt full') {
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"
'''
}
}
stage('lint test full') {
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"
'''
}
}
}
post {
always {
archiveArtifacts artifacts: 'pylint-report-*-full.log', allowEmptyArchive: true
step([$class: 'WarningsPublisher',
parserConfigurations: [[
parserName: 'PyLint',
pattern: 'pylint-report-*-full.log'
]],
failedTotalAll: '0',
useDeltaValues: false,
canRunOnFailed: true,
usePreviousBuildAsReference: true
])
}
}
} }
} }
post { post {
always { always {
step([$class: 'WarningsPublisher',
parserConfigurations: [[
parserName: 'PyLint',
pattern: 'pylint-report*.log'
]],
failedTotalAll: '0',
useDeltaValues: false,
canRunOnFailed: true,
usePreviousBuildAsReference: true
])
cleanWs() cleanWs()
} }
success { success {
githubNotify credentialsId: 'test-jenkins-credentials', githubNotify credentialsId: 'test-jenkins-credentials',
description: 'The lint job has passed', description: 'Python lint test has passed',
status: 'SUCCESS', status: 'SUCCESS',
context: "jenkins/pr/lint" context: "jenkins/pr/lint"
} }
failure { failure {
githubNotify credentialsId: 'test-jenkins-credentials', githubNotify credentialsId: 'test-jenkins-credentials',
description: 'The lint job has failed', description: 'Python lint test has failed',
status: 'FAILURE', status: 'FAILURE',
context: "jenkins/pr/lint" context: "jenkins/pr/lint"
slackSend channel: "#jenkins-prod-pr", slackSend channel: "#jenkins-prod-pr",

3
.github/CODEOWNERS vendored
View file

@ -74,3 +74,6 @@ salt/modules/reg.py @saltstack/team-windows
salt/states/reg.py @saltstack/team-windows salt/states/reg.py @saltstack/team-windows
tests/*/*win* @saltstack/team-windows tests/*/*win* @saltstack/team-windows
tests/*/test_reg.py @saltstack/team-windows tests/*/test_reg.py @saltstack/team-windows
# Jenkins Integration
.ci/* @saltstack/saltstack-sre-team @saltstack/team-core

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>`. 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. 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

@ -140,6 +140,18 @@ def static_loader(
return ret return ret
def _format_entrypoint_target(ep):
'''
Makes a string describing the target of an EntryPoint object.
Base strongly on EntryPoint.__str__().
'''
s = ep.module_name
if ep.attrs:
s += ':' + '.'.join(ep.attrs)
return s
def _module_dirs( def _module_dirs(
opts, opts,
ext_type, ext_type,
@ -162,9 +174,13 @@ def _module_dirs(
ext_type_types.extend(opts[ext_type_dirs]) ext_type_types.extend(opts[ext_type_dirs])
if HAS_PKG_RESOURCES and ext_type_dirs: if HAS_PKG_RESOURCES and ext_type_dirs:
for entry_point in pkg_resources.iter_entry_points('salt.loader', ext_type_dirs): for entry_point in pkg_resources.iter_entry_points('salt.loader', ext_type_dirs):
loaded_entry_point = entry_point.load() try:
for path in loaded_entry_point(): loaded_entry_point = entry_point.load()
ext_type_types.append(path) for path in loaded_entry_point():
ext_type_types.append(path)
except Exception as exc:
log.error("Error getting module directories from %s: %s", _format_entrypoint_target(entry_point), exc)
log.debug("Full backtrace for module directories error", exc_info=True)
cli_module_dirs = [] cli_module_dirs = []
# The dirs can be any module dir, or a in-tree _{ext_type} dir # The dirs can be any module dir, or a in-tree _{ext_type} dir

View file

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

View file

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

View file

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

View file

@ -5,6 +5,7 @@ Manage the information in the hosts file
# Import Python libs # Import Python libs
from __future__ import absolute_import, print_function, unicode_literals from __future__ import absolute_import, print_function, unicode_literals
import errno
import os import os
# Import salt libs # Import salt libs
@ -22,7 +23,12 @@ def __get_hosts_filename():
''' '''
Return the path to the appropriate hosts file 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(): def _get_or_create_hostfile():
@ -43,26 +49,35 @@ def _list_hosts():
''' '''
Return the hosts found in the hosts file in as an OrderedDict Return the hosts found in the hosts file in as an OrderedDict
''' '''
count = 0 try:
hfn = __get_hosts_filename() return __context__['hosts._list_hosts']
ret = odict.OrderedDict() except KeyError:
if not os.path.isfile(hfn): 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 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(): def list_hosts():
@ -133,7 +148,10 @@ def has_pair(ip, alias):
salt '*' hosts.has_pair <ip> <alias> salt '*' hosts.has_pair <ip> <alias>
''' '''
hosts = _list_hosts() 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): def set_host(ip, alias):
@ -157,6 +175,9 @@ def set_host(ip, alias):
if not os.path.isfile(hfn): if not os.path.isfile(hfn):
return False 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( line_to_add = salt.utils.stringutils.to_bytes(
ip + '\t\t' + alias + os.linesep ip + '\t\t' + alias + os.linesep
) )
@ -203,6 +224,8 @@ def rm_host(ip, alias):
''' '''
if not has_pair(ip, alias): if not has_pair(ip, alias):
return True 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() hfn = _get_or_create_hostfile()
with salt.utils.files.fopen(hfn, 'rb') as fp_: with salt.utils.files.fopen(hfn, 'rb') as fp_:
lines = fp_.readlines() lines = fp_.readlines()
@ -251,6 +274,10 @@ def add_host(ip, alias):
return True return True
hosts = _list_hosts() hosts = _list_hosts()
# Make sure future calls to _list_hosts() will re-read the file
__context__.pop('hosts._list_hosts', None)
inserted = False inserted = False
for i, h in six.iteritems(hosts): for i, h in six.iteritems(hosts):
for j in range(len(h)): 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 from __future__ import absolute_import, unicode_literals, print_function
import salt.utils
import re
import logging import logging
log = logging.getLogger(__name__) 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' 'Use of argument ruby found, but neither rvm or rbenv is installed'
) )
gems = __salt__['gem.list'](name, ruby, gem_bin=gem_bin, runas=user) 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]: if name in gems and version is not None:
ret['result'] = True match = re.match(r'(>=|>|<|<=)', version)
ret['comment'] = 'Gem is already installed.' if match:
return ret # 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: elif name in gems and version is None:
ret['result'] = True ret['result'] = True
ret['comment'] = 'Gem is already installed.' ret['comment'] = 'Gem is already installed.'

View file

@ -247,6 +247,7 @@ def absent(name, orgname=None, profile='grafana'):
_IGNORED_DASHBOARD_FIELDS = [ _IGNORED_DASHBOARD_FIELDS = [
'id', 'id',
'uid',
'originalTitle', 'originalTitle',
'version', 'version',
] ]

View file

@ -67,7 +67,7 @@ from salt.ext import six
import salt.utils.validate.net 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 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 The host to assign an ip to
ip 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, ret = {'name': name,
'changes': {}, 'changes': {},
'result': None, 'result': None if __opts__['test'] else True,
'comment': ''} 'comment': ''}
if not isinstance(ip, list): if not isinstance(ip, list):
ip = [ip] ip = [ip]
all_hosts = __salt__['hosts.list_hosts']()
comments = [] comments = []
for _ip in ip: to_add = set()
if __salt__['hosts.has_pair'](_ip, name): to_remove = set()
ret['result'] = True
comments.append('Host {0} ({1}) already present'.format(name, _ip)) # 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: else:
if __opts__['test']: if name in aliases:
comments.append('Host {0} ({1}) needs to be added/updated'.format(name, _ip)) # No changes needed for this IP address and hostname
comments.append(
'Host {0} ({1}) already present'.format(name, addr)
)
else: else:
if salt.utils.validate.net.ipv4_addr(_ip) or salt.utils.validate.net.ipv6_addr(_ip): # IP address listed in hosts file, but hostname is not present.
if __salt__['hosts.add_host'](_ip, name): # We will need to add it.
ret['changes'] = {'host': name} if salt.utils.validate.net.ip_addr(addr):
ret['result'] = True to_add.add((addr, name))
comments.append('Added host {0} ({1})'.format(name, _ip))
else:
ret['result'] = False
comments.append('Failed to set host')
else: else:
ret['result'] = False 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) ret['comment'] = '\n'.join(comments)
return ret return ret

View file

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

View file

@ -205,10 +205,22 @@ def rename(src, dst):
os.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 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: if exc.errno == errno.ENOENT:
raise CommandExecutionError('{0} does not exist'.format(path)) raise CommandExecutionError('{0} does not exist'.format(path))
elif exc.errno == errno.EACCES: elif exc.errno == errno.EACCES:

View file

@ -81,6 +81,14 @@ def ipv6_addr(addr):
return __ip_addr(addr, socket.AF_INET6) 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): def netmask(mask):
''' '''
Returns True if the value passed is a valid netmask, otherwise return False 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) 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 FileCommentLineTestCase(TestCase, LoaderModuleMockMixin): class FileCommentLineTestCase(TestCase, LoaderModuleMockMixin):
def setup_loader_modules(self): def setup_loader_modules(self):

View file

@ -47,6 +47,19 @@ class TestGemState(TestCase, LoaderModuleMockMixin):
ri=False, gem_bin=None 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): def test_removed(self):
gems = ['foo', 'bar'] gems = ['foo', 'bar']
gem_list = MagicMock(return_value=gems) gem_list = MagicMock(return_value=gems)

View file

@ -15,7 +15,8 @@ from tests.support.mock import (
NO_MOCK, NO_MOCK,
NO_MOCK_REASON, NO_MOCK_REASON,
MagicMock, MagicMock,
patch call,
patch,
) )
@ -25,19 +26,260 @@ class HostTestCase(TestCase, LoaderModuleMockMixin):
Validate the host state Validate the host state
''' '''
def setup_loader_modules(self): def setup_loader_modules(self):
return {host: {}} return {
host: {
'__opts__': {
'test': False,
},
},
}
def test_present(self): def test_present(self):
''' '''
Test to ensures that the named host is present with the given ip Test to ensures that the named host is present with the given ip
''' '''
ret = {'changes': {}, add_host = MagicMock(return_value=True)
'comment': 'Host salt (127.0.0.1) already present', rm_host = MagicMock(return_value=True)
'name': 'salt', 'result': True} hostname = 'salt'
ip_str = '127.0.0.1'
ip_list = ['10.1.2.3', '10.4.5.6']
mock = MagicMock(return_value=True) # Case 1: No match for hostname. Single IP address passed to the state.
with patch.dict(host.__salt__, {'hosts.has_pair': mock}): list_hosts = MagicMock(return_value={
self.assertDictEqual(host.present("salt", "127.0.0.1"), ret) '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): def test_absent(self):
''' '''