Merge branch '2017.7' into pop

This commit is contained in:
Mike Place 2017-11-30 16:52:44 -07:00 committed by GitHub
commit 0efb90b6f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1054 additions and 60 deletions

View file

@ -80,12 +80,21 @@ same way as in the above example, only without a top-level ``grains:`` key:
.. note::
The content of ``/etc/salt/grains`` is ignored if you specify grains in the minion config.
Grains in ``/etc/salt/grains`` are ignored if you specify the same grains in the minion config.
.. note::
Grains are static, and since they are not often changed, they will need a grains refresh when they are updated. You can do this by calling: ``salt minion saltutil.refresh_modules``
.. note::
You can equally configure static grains for Proxy Minions.
As multiple Proxy Minion processes can run on the same machine, you need
to index the files using the Minion ID, under ``/etc/salt/proxy.d/<minion ID>/grains``.
For example, the grains for the Proxy Minion ``router1`` can be defined
under ``/etc/salt/proxy.d/router1/grains``, while the grains for the
Proxy Minion ``switch7`` can be put in ``/etc/salt/proxy.d/switch7/grains``.
Matching Grains in the Top File
===============================

View file

@ -57,7 +57,15 @@ Writing Thorium Formulas
========================
Like some other Salt subsystems, Thorium uses its own directory structure. The
default location for this structure is ``/srv/thorium/``, but it can be changed
using the ``thorium_roots_dir`` setting in the ``master`` configuration file.
using the ``thorium_roots`` setting in the ``master`` configuration file.
Example ``thorium_roots`` configuration:
.. code-block:: yaml
thorium_roots:
base:
- /etc/salt/thorium
The Thorium top.sls File

View file

@ -6,6 +6,7 @@ Create ssh executor system
from __future__ import absolute_import
# Import python libs
import os
import time
import copy
import json
import logging
@ -21,6 +22,8 @@ import salt.loader
import salt.minion
import salt.log
from salt.ext.six import string_types
import salt.ext.six as six
from salt.exceptions import SaltInvocationError
__func_alias__ = {
'apply_': 'apply'
@ -28,6 +31,47 @@ __func_alias__ = {
log = logging.getLogger(__name__)
def _set_retcode(ret, highstate=None):
'''
Set the return code based on the data back from the state system
'''
# Set default retcode to 0
__context__['retcode'] = 0
if isinstance(ret, list):
__context__['retcode'] = 1
return
if not salt.utils.check_state_result(ret, highstate=highstate):
__context__['retcode'] = 2
def _check_pillar(kwargs, pillar=None):
'''
Check the pillar for errors, refuse to run the state if there are errors
in the pillar and return the pillar errors
'''
if kwargs.get('force'):
return True
pillar_dict = pillar if pillar is not None else __pillar__
if '_errors' in pillar_dict:
return False
return True
def _wait(jid):
'''
Wait for all previously started state jobs to finish running
'''
if jid is None:
jid = salt.utils.jid.gen_jid()
states = _prior_running_states(jid)
while states:
time.sleep(1)
states = _prior_running_states(jid)
def _merge_extra_filerefs(*args):
'''
Takes a list of filerefs and returns a merged list
@ -127,6 +171,100 @@ def sls(mods, saltenv='base', test=None, exclude=None, **kwargs):
return stdout
def running(concurrent=False):
'''
Return a list of strings that contain state return data if a state function
is already running. This function is used to prevent multiple state calls
from being run at the same time.
CLI Example:
.. code-block:: bash
salt '*' state.running
'''
ret = []
if concurrent:
return ret
active = __salt__['saltutil.is_running']('state.*')
for data in active:
err = (
'The function "{0}" is running as PID {1} and was started at '
'{2} with jid {3}'
).format(
data['fun'],
data['pid'],
salt.utils.jid.jid_to_time(data['jid']),
data['jid'],
)
ret.append(err)
return ret
def _prior_running_states(jid):
'''
Return a list of dicts of prior calls to state functions. This function is
used to queue state calls so only one is run at a time.
'''
ret = []
active = __salt__['saltutil.is_running']('state.*')
for data in active:
try:
data_jid = int(data['jid'])
except ValueError:
continue
if data_jid < int(jid):
ret.append(data)
return ret
def _check_queue(queue, kwargs):
'''
Utility function to queue the state run if requested
and to check for conflicts in currently running states
'''
if queue:
_wait(kwargs.get('__pub_jid'))
else:
conflict = running(concurrent=kwargs.get('concurrent', False))
if conflict:
__context__['retcode'] = 1
return conflict
def _get_opts(**kwargs):
'''
Return a copy of the opts for use, optionally load a local config on top
'''
opts = copy.deepcopy(__opts__)
if 'localconfig' in kwargs:
return salt.config.minion_config(kwargs['localconfig'], defaults=opts)
if 'saltenv' in kwargs:
saltenv = kwargs['saltenv']
if saltenv is not None and not isinstance(saltenv, six.string_types):
opts['environment'] = str(kwargs['saltenv'])
else:
opts['environment'] = kwargs['saltenv']
if 'pillarenv' in kwargs:
pillarenv = kwargs['pillarenv']
if pillarenv is not None and not isinstance(pillarenv, six.string_types):
opts['pillarenv'] = str(kwargs['pillarenv'])
else:
opts['pillarenv'] = kwargs['pillarenv']
return opts
def _get_initial_pillar(opts):
return __pillar__ if __opts__['__cli'] == 'salt-call' \
and opts['pillarenv'] == __opts__['pillarenv'] \
else None
def low(data, **kwargs):
'''
Execute a single low data call
@ -199,6 +337,21 @@ def low(data, **kwargs):
return stdout
def _get_test_value(test=None, **kwargs):
'''
Determine the correct value for the test flag.
'''
ret = True
if test is None:
if salt.utils.test_mode(test=test, **kwargs):
ret = True
else:
ret = __opts__.get('test', None)
else:
ret = test
return ret
def high(data, **kwargs):
'''
Execute the compound calls stored in a single set of high data
@ -615,6 +768,99 @@ def show_lowstate():
return st_.compile_low_chunks()
def sls_id(id_, mods, test=None, queue=False, **kwargs):
'''
Call a single ID from the named module(s) and handle all requisites
The state ID comes *before* the module ID(s) on the command line.
id
ID to call
mods
Comma-delimited list of modules to search for given id and its requisites
.. versionadded:: 2017.7.3
saltenv : base
Specify a salt fileserver environment to be used when applying states
pillarenv
Specify a Pillar environment to be used when applying states. This
can also be set in the minion config file using the
:conf_minion:`pillarenv` option. When neither the
:conf_minion:`pillarenv` minion config option nor this CLI argument is
used, all Pillar environments will be merged together.
CLI Example:
.. code-block:: bash
salt '*' state.sls_id my_state my_module
salt '*' state.sls_id my_state my_module,a_common_module
'''
conflict = _check_queue(queue, kwargs)
if conflict is not None:
return conflict
orig_test = __opts__.get('test', None)
opts = _get_opts(**kwargs)
opts['test'] = _get_test_value(test, **kwargs)
# Since this is running a specific ID within a specific SLS file, fall back
# to the 'base' saltenv if none is configured and none was passed.
if opts['environment'] is None:
opts['environment'] = 'base'
try:
st_ = salt.state.HighState(opts,
proxy=__proxy__,
initial_pillar=_get_initial_pillar(opts))
except NameError:
st_ = salt.state.HighState(opts,
initial_pillar=_get_initial_pillar(opts))
if not _check_pillar(kwargs, st_.opts['pillar']):
__context__['retcode'] = 5
err = ['Pillar failed to render with the following messages:']
err += __pillar__['_errors']
return err
if isinstance(mods, six.string_types):
split_mods = mods.split(',')
st_.push_active()
try:
high_, errors = st_.render_highstate({opts['environment']: split_mods})
finally:
st_.pop_active()
errors += st_.state.verify_high(high_)
# Apply requisites to high data
high_, req_in_errors = st_.state.requisite_in(high_)
if req_in_errors:
# This if statement should not be necessary if there were no errors,
# but it is required to get the unit tests to pass.
errors.extend(req_in_errors)
if errors:
__context__['retcode'] = 1
return errors
chunks = st_.state.compile_high_data(high_)
ret = {}
for chunk in chunks:
if chunk.get('__id__', '') == id_:
ret.update(st_.state.call_chunk(chunk, {}, chunks))
_set_retcode(ret, highstate=highstate)
# Work around Windows multiprocessing bug, set __opts__['test'] back to
# value from before this function was run.
__opts__['test'] = orig_test
if not ret:
raise SaltInvocationError(
'No matches for ID \'{0}\' found in SLS \'{1}\' within saltenv '
'\'{2}\''.format(id_, mods, opts['environment'])
)
return ret
def show_sls(mods, saltenv='base', test=None, **kwargs):
'''
Display the state data from a specific sls or list of sls files on the

View file

@ -12,6 +12,7 @@ import logging
# Import salt libs
import salt.utils
__proxyenabled__ = ['*']
log = logging.getLogger(__name__)
@ -31,16 +32,33 @@ def config():
if 'conf_file' not in __opts__:
return {}
if os.path.isdir(__opts__['conf_file']):
gfn = os.path.join(
__opts__['conf_file'],
'grains'
)
if salt.utils.is_proxy():
gfn = os.path.join(
__opts__['conf_file'],
'proxy.d',
__opts__['id'],
'grains'
)
else:
gfn = os.path.join(
__opts__['conf_file'],
'grains'
)
else:
gfn = os.path.join(
os.path.dirname(__opts__['conf_file']),
'grains'
)
if salt.utils.is_proxy():
gfn = os.path.join(
os.path.dirname(__opts__['conf_file']),
'proxy.d',
__opts__['id'],
'grains'
)
else:
gfn = os.path.join(
os.path.dirname(__opts__['conf_file']),
'grains'
)
if os.path.isfile(gfn):
log.debug('Loading static grains from %s', gfn)
with salt.utils.fopen(gfn, 'rb') as fp_:
try:
return yaml.safe_load(fp_.read())

View file

@ -3127,6 +3127,12 @@ def run_bg(cmd,
Note that ``env`` represents the environment variables for the command, and
should be formatted as a dict, or a YAML string which resolves to a dict.
.. note::
If the init system is systemd and the backgrounded task should run even if the salt-minion process
is restarted, prepend ``systemd-run --scope`` to the command. This will reparent the process in its
own scope separate from salt-minion, and will not be affected by restarting the minion service.
:param str cmd: The command to run. ex: 'ls -lart /home'
:param str cwd: The current working directory to execute the command in.

View file

@ -1,6 +1,13 @@
# -*- coding: utf-8 -*-
'''
Return/control aspects of the grains data
Grains set or altered with this module are stored in the 'grains'
file on the minions. By default, this file is located at: ``/etc/salt/grains``
.. Note::
This does **NOT** override any grains set in the minion config file.
'''
# Import python libs
@ -222,20 +229,44 @@ def setvals(grains, destructive=False):
raise SaltException('setvals grains must be a dictionary.')
grains = {}
if os.path.isfile(__opts__['conf_file']):
gfn = os.path.join(
os.path.dirname(__opts__['conf_file']),
'grains'
)
if salt.utils.is_proxy():
gfn = os.path.join(
os.path.dirname(__opts__['conf_file']),
'proxy.d',
__opts__['id'],
'grains'
)
else:
gfn = os.path.join(
os.path.dirname(__opts__['conf_file']),
'grains'
)
elif os.path.isdir(__opts__['conf_file']):
gfn = os.path.join(
__opts__['conf_file'],
'grains'
)
if salt.utils.is_proxy():
gfn = os.path.join(
__opts__['conf_file'],
'proxy.d',
__opts__['id'],
'grains'
)
else:
gfn = os.path.join(
__opts__['conf_file'],
'grains'
)
else:
gfn = os.path.join(
os.path.dirname(__opts__['conf_file']),
'grains'
)
if salt.utils.is_proxy():
gfn = os.path.join(
os.path.dirname(__opts__['conf_file']),
'proxy.d',
__opts__['id'],
'grains'
)
else:
gfn = os.path.join(
os.path.dirname(__opts__['conf_file']),
'grains'
)
if os.path.isfile(gfn):
with salt.utils.fopen(gfn, 'rb') as fp_:

View file

@ -29,6 +29,7 @@ log = logging.getLogger(__name__)
from salt.ext import six
import salt.utils.templates
import salt.utils.napalm
import salt.utils.versions
from salt.utils.napalm import proxy_napalm_wrap
# ----------------------------------------------------------------------------------------------------------------------
@ -228,7 +229,7 @@ def _config_logic(napalm_device,
@proxy_napalm_wrap
def connected(**kwarvs): # pylint: disable=unused-argument
def connected(**kwargs): # pylint: disable=unused-argument
'''
Specifies if the connection to the device succeeded.
@ -932,6 +933,7 @@ def load_config(filename=None,
debug=False,
replace=False,
inherit_napalm_device=None,
saltenv='base',
**kwargs): # pylint: disable=unused-argument
'''
Applies configuration changes on the device. It can be loaded from a file or from inline string.
@ -947,10 +949,21 @@ def load_config(filename=None,
To replace the config, set ``replace`` to ``True``.
filename
Path to the file containing the desired configuration. By default is None.
Path to the file containing the desired configuration.
This can be specified using the absolute path to the file,
or using one of the following URL schemes:
- ``salt://``, to fetch the template from the Salt fileserver.
- ``http://`` or ``https://``
- ``ftp://``
- ``s3://``
- ``swift://``
.. versionchanged:: 2017.7.3
text
String containing the desired configuration.
This argument is ignored when ``filename`` is specified.
test: False
Dry run? If set as ``True``, will apply the config, discard and return the changes. Default: ``False``
@ -970,6 +983,11 @@ def load_config(filename=None,
.. versionadded:: 2016.11.2
saltenv: ``base``
Specifies the Salt environment name.
.. versionadded:: 2017.7.3
:return: a dictionary having the following keys:
* result (bool): if the config was applied successfully. It is ``False`` only in case of failure. In case \
@ -999,7 +1017,6 @@ def load_config(filename=None,
'diff': '[edit interfaces xe-0/0/5]+ description "Adding a description";'
}
'''
fun = 'load_merge_candidate'
if replace:
fun = 'load_replace_candidate'
@ -1012,21 +1029,28 @@ def load_config(filename=None,
# compare_config, discard / commit
# which have to be over the same session
napalm_device['CLOSE'] = False # pylint: disable=undefined-variable
if filename:
text = __salt__['cp.get_file_str'](filename, saltenv=saltenv)
if text is False:
# When using salt:// or https://, if the resource is not available,
# it will either raise an exception, or return False.
ret = {
'result': False,
'out': None
}
ret['comment'] = 'Unable to read from {}. Please specify a valid file or text.'.format(filename)
log.error(ret['comment'])
return ret
_loaded = salt.utils.napalm.call(
napalm_device, # pylint: disable=undefined-variable
fun,
**{
'filename': filename,
'config': text
}
)
loaded_config = None
if debug:
if filename:
with salt.utils.fopen(filename) as rfh:
loaded_config = rfh.read()
else:
loaded_config = text
loaded_config = text
return _config_logic(napalm_device, # pylint: disable=undefined-variable
_loaded,
test=test,
@ -1072,6 +1096,10 @@ def load_template(template_name,
To replace the config, set ``replace`` to ``True``.
.. warning::
The support for native NAPALM templates will be dropped in Salt Fluorine.
Implicitly, the ``template_path`` argument will be removed.
template_name
Identifies path to the template source.
The template can be either stored on the local machine, either remotely.
@ -1108,6 +1136,9 @@ def load_template(template_name,
in order to find the template, this argument must be provided:
``template_path: /absolute/path/to/``.
.. note::
This argument will be deprecated beginning with release codename ``Fluorine``.
template_hash: None
Hash of the template file. Format: ``{hash_type: 'md5', 'hsum': <md5sum>}``
@ -1274,7 +1305,11 @@ def load_template(template_name,
'out': None
}
loaded_config = None
if template_path:
salt.utils.versions.warn_until(
'Fluorine',
'Use of `template_path` detected. This argument will be removed in Salt Fluorine.'
)
# prechecks
if template_engine not in salt.utils.templates.TEMPLATE_REGISTRY:
_loaded.update({

View file

@ -48,7 +48,7 @@ def _load_state():
pck = open(FILENAME, 'r') # pylint: disable=W8470
DETAILS = pickle.load(pck)
pck.close()
except IOError:
except EOFError:
DETAILS = {}
DETAILS['initialized'] = False
_save_state(DETAILS)

View file

@ -3836,11 +3836,11 @@ def replace(name,
If you need to match a literal string that contains regex special
characters, you may want to use salt's custom Jinja filter,
``escape_regex``.
``regex_escape``.
.. code-block:: jinja
{{ 'http://example.com?foo=bar%20baz' | escape_regex }}
{{ 'http://example.com?foo=bar%20baz' | regex_escape }}
repl
The replacement text

View file

@ -4,10 +4,13 @@ Manage grains on the minion
===========================
This state allows for grains to be set.
Grains set or altered this way are stored in the 'grains'
file on the minions, by default at: /etc/salt/grains
Note: This does NOT override any grains set in the minion file.
Grains set or altered with this module are stored in the 'grains'
file on the minions, By default, this file is located at: ``/etc/salt/grains``
.. Note::
This does **NOT** override any grains set in the minion config file.
'''
# Import Python libs

View file

@ -25,6 +25,7 @@ log = logging.getLogger(__name__)
# import NAPALM utils
import salt.utils.napalm
import salt.utils.versions
# ----------------------------------------------------------------------------------------------------------------------
# state properties
@ -133,6 +134,10 @@ def managed(name,
To replace the config, set ``replace`` to ``True``. This option is recommended to be used with caution!
.. warning::
The spport for NAPALM native templates will be dropped beginning with Salt Fluorine.
Implicitly, the ``template_path`` argument will be depreacted and removed.
template_name
Identifies path to the template source. The template can be either stored on the local machine,
either remotely.
@ -320,7 +325,11 @@ def managed(name,
}
}
'''
if template_path:
salt.utils.versions.warn_until(
'Fluorine',
'Use of `template_path` detected. This argument will be removed in Salt Fluorine.'
)
ret = salt.utils.napalm.default_ret(name)
# the user can override the flags the equivalent CLI args

View file

@ -637,11 +637,11 @@ class SerializerExtension(Extension, object):
.. code-block:: jinja
escape_regex = {{ 'https://example.com?foo=bar%20baz' | escape_regex }}
regex_escape = {{ 'https://example.com?foo=bar%20baz' | regex_escape }}
will be rendered as::
escape_regex = https\\:\\/\\/example\\.com\\?foo\\=bar\\%20baz
regex_escape = https\\:\\/\\/example\\.com\\?foo\\=bar\\%20baz
** Set Theory Filters **

View file

@ -1244,8 +1244,27 @@ class Schedule(object):
run = False
seconds = data['_next_fire_time'] - now
if data['_splay']:
seconds = data['_splay'] - now
if 'splay' in data:
# Got "splay" configured, make decision to run a job based on that
if not data['_splay']:
# Try to add "splay" time only if next job fire time is
# still in the future. We should trigger job run
# immediately otherwise.
splay = _splay(data['splay'])
if now < data['_next_fire_time'] + splay:
log.debug('schedule.handle_func: Adding splay of '
'{0} seconds to next run.'.format(splay))
data['_splay'] = data['_next_fire_time'] + splay
if 'when' in data:
data['_run'] = True
else:
run = True
if data['_splay']:
# The "splay" configuration has been already processed, just use it
seconds = data['_splay'] - now
if seconds <= 0:
if '_seconds' in data:
run = True
@ -1264,16 +1283,6 @@ class Schedule(object):
run = True
data['_run_on_start'] = False
elif run:
if 'splay' in data and not data['_splay']:
splay = _splay(data['splay'])
if now < data['_next_fire_time'] + splay:
log.debug('schedule.handle_func: Adding splay of '
'{0} seconds to next run.'.format(splay))
run = False
data['_splay'] = data['_next_fire_time'] + splay
if 'when' in data:
data['_run'] = True
if 'range' in data:
if not _RANGE_SUPPORTED:
log.error('Missing python-dateutil. Ignoring job {0}'.format(job))

View file

@ -805,6 +805,12 @@ class TestDaemon(object):
os.path.join(FILES, 'pillar', 'base'),
]
}
minion_opts['pillar_roots'] = {
'base': [
RUNTIME_VARS.TMP_PILLAR_TREE,
os.path.join(FILES, 'pillar', 'base'),
]
}
master_opts['file_roots'] = syndic_master_opts['file_roots'] = {
'base': [
os.path.join(FILES, 'file', 'base'),
@ -818,6 +824,19 @@ class TestDaemon(object):
RUNTIME_VARS.TMP_PRODENV_STATE_TREE
]
}
minion_opts['file_roots'] = {
'base': [
os.path.join(FILES, 'file', 'base'),
# Let's support runtime created files that can be used like:
# salt://my-temp-file.txt
RUNTIME_VARS.TMP_STATE_TREE
],
# Alternate root to test __env__ choices
'prod': [
os.path.join(FILES, 'file', 'prod'),
RUNTIME_VARS.TMP_PRODENV_STATE_TREE
]
}
master_opts.setdefault('reactor', []).append(
{
'salt/minion/*/start': [

View file

@ -1,3 +1,5 @@
localhost:
host: 127.0.0.1
port: 2827
mine_functions:
test.arg: ['itworked']

View file

@ -0,0 +1,7 @@
return_changes:
test.succeed_with_changes:
- watch_in:
- test: watch_states
watch_states:
test.succeed_without_changes

View file

@ -0,0 +1,7 @@
return_changes:
test.fail_with_changes:
- watch_in:
- test: watch_states
watch_states:
test.succeed_without_changes

View file

@ -1,4 +1,5 @@
{% set jinja = 'test' %}
ssh-file-test:
file.managed:
- name: /tmp/test
- name: /tmp/{{ jinja }}
- contents: 'test'

View file

@ -6,3 +6,6 @@ base:
- generic
- blackout
- sub
'localhost':
- generic
- blackout

View file

@ -586,6 +586,33 @@ class StateModuleTest(ModuleCase, SaltReturnAssertsMixin):
#result = self.normalize_ret(ret)
#self.assertEqual(expected_result, result)
def test_watch_in(self):
'''
test watch_in requisite when there is a success
'''
ret = self.run_function('state.sls', mods='requisites.watch_in')
changes = 'test_|-return_changes_|-return_changes_|-succeed_with_changes'
watch = 'test_|-watch_states_|-watch_states_|-succeed_without_changes'
self.assertEqual(ret[changes]['__run_num__'], 0)
self.assertEqual(ret[watch]['__run_num__'], 2)
self.assertEqual('Watch statement fired.', ret[watch]['comment'])
self.assertEqual('Something pretended to change',
ret[changes]['changes']['testing']['new'])
def test_watch_in_failure(self):
'''
test watch_in requisite when there is a failure
'''
ret = self.run_function('state.sls', mods='requisites.watch_in_failure')
fail = 'test_|-return_changes_|-return_changes_|-fail_with_changes'
watch = 'test_|-watch_states_|-watch_states_|-succeed_without_changes'
self.assertEqual(False, ret[fail]['result'])
self.assertEqual('One or more requisite failed: requisites.watch_in_failure.return_changes',
ret[watch]['comment'])
def normalize_ret(self, ret):
'''
Normalize the return to the format that we'll use for result checking

View file

@ -441,6 +441,17 @@ class CallTest(ShellCase, testprogram.TestProgramCase, ShellCaseCommonTestsMixin
log.debug('salt-call output:\n\n%s', '\n'.join(ret))
self.fail('CLI pillar override not found in pillar data')
def test_pillar_items_masterless(self):
'''
Test to ensure we get expected output
from pillar.items with salt-call
'''
get_items = self.run_call('pillar.items', local=True)
exp_out = [' - Lancelot', ' - Galahad', ' - Bedevere',
' monty:', ' python']
for out in exp_out:
self.assertIn(out, get_items)
def tearDown(self):
'''
Teardown method to remove installed packages
@ -477,6 +488,21 @@ class CallTest(ShellCase, testprogram.TestProgramCase, ShellCaseCommonTestsMixin
stdout=stdout, stderr=stderr
)
def test_masterless_highstate(self):
'''
test state.highstate in masterless mode
'''
ret = self.run_call('state.highstate', local=True)
destpath = os.path.join(TMP, 'testfile')
exp_out = [' Function: file.managed', ' Result: True',
' ID: {0}'.format(destpath)]
for out in exp_out:
self.assertIn(out, ret)
self.assertTrue(os.path.exists(destpath))
def test_exit_status_correct_usage(self):
'''
Ensure correct exit status when salt-call starts correctly.

View file

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Import Python libs
from __future__ import absolute_import
import os
import shutil
# Import Salt Testing Libs
from tests.support.case import SSHCase
from tests.support.unit import skipIf
# Import Salt Libs
import salt.utils
@skipIf(salt.utils.is_windows(), 'salt-ssh not available on Windows')
class SSHMineTest(SSHCase):
'''
testing salt-ssh with mine
'''
def test_ssh_mine_get(self):
'''
test salt-ssh with mine
'''
ret = self.run_function('mine.get', ['localhost test.arg'], wipe=False)
self.assertEqual(ret['localhost']['args'], ['itworked'])
def tearDown(self):
'''
make sure to clean up any old ssh directories
'''
salt_dir = self.run_function('config.get', ['thin_dir'], wipe=False)
if os.path.exists(salt_dir):
shutil.rmtree(salt_dir)

View file

@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
# Import Python libs
from __future__ import absolute_import
# Import Salt Testing Libs
from tests.support.case import SSHCase
from tests.support.unit import skipIf
# Import Salt Libs
import salt.utils
@skipIf(salt.utils.is_windows(), 'salt-ssh not available on Windows')
class SSHPillarTest(SSHCase):
'''
testing pillar with salt-ssh
'''
def test_pillar_items(self):
'''
test pillar.items with salt-ssh
'''
ret = self.run_function('pillar.items')
self.assertDictContainsSubset({'monty': 'python'}, ret)
self.assertDictContainsSubset(
{'knights': ['Lancelot', 'Galahad', 'Bedevere', 'Robin']},
ret)
def test_pillar_get(self):
'''
test pillar.get with salt-ssh
'''
ret = self.run_function('pillar.get', ['monty'])
self.assertEqual(ret, 'python')
def test_pillar_get_doesnotexist(self):
'''
test pillar.get when pillar does not exist with salt-ssh
'''
ret = self.run_function('pillar.get', ['doesnotexist'])
self.assertEqual(ret, '')

View file

@ -42,6 +42,16 @@ class SSHStateTest(SSHCase):
check_file = self.run_function('file.file_exists', ['/tmp/test'])
self.assertTrue(check_file)
def test_state_sls_id(self):
'''
test state.sls_id with salt-ssh
'''
ret = self.run_function('state.sls_id', ['ssh-file-test', SSH_SLS])
self._check_dict_ret(ret=ret, val='__sls__', exp_ret=SSH_SLS)
check_file = self.run_function('file.file_exists', ['/tmp/test'])
self.assertTrue(check_file)
def test_state_show_sls(self):
'''
test state.show_sls with salt-ssh
@ -57,7 +67,7 @@ class SSHStateTest(SSHCase):
test state.show_top with salt-ssh
'''
ret = self.run_function('state.show_top')
self.assertEqual(ret, {u'base': [u'master_tops_test', u'core']})
self.assertEqual(ret, {u'base': list(set([u'master_tops_test']).union([u'core']))})
def test_state_single(self):
'''

View file

@ -210,8 +210,10 @@ class ShellTestCase(TestCase, AdaptedConfigurationTestCaseMixin):
arg_str = '--config-dir {0} {1}'.format(self.get_config_dir(), arg_str)
return self.run_script('salt-cp', arg_str, with_retcode=with_retcode, catch_stderr=catch_stderr)
def run_call(self, arg_str, with_retcode=False, catch_stderr=False):
arg_str = '--config-dir {0} {1}'.format(self.get_config_dir(), arg_str)
def run_call(self, arg_str, with_retcode=False, catch_stderr=False, local=False):
arg_str = '{0} --config-dir {1} {2}'.format('--local' if local else '',
self.get_config_dir(), arg_str)
return self.run_script('salt-call', arg_str, with_retcode=with_retcode, catch_stderr=catch_stderr)
def run_cloud(self, arg_str, catch_stderr=False, timeout=None):
@ -549,11 +551,12 @@ class ShellCase(ShellTestCase, AdaptedConfigurationTestCaseMixin, ScriptPathMixi
catch_stderr=catch_stderr,
timeout=60)
def run_call(self, arg_str, with_retcode=False, catch_stderr=False):
def run_call(self, arg_str, with_retcode=False, catch_stderr=False, local=False):
'''
Execute salt-call.
'''
arg_str = '--config-dir {0} {1}'.format(self.get_config_dir(), arg_str)
arg_str = '{0} --config-dir {1} {2}'.format('--local' if local else '',
self.get_config_dir(), arg_str)
return self.run_script('salt-call',
arg_str,
with_retcode=with_retcode,

View file

@ -0,0 +1,383 @@
# -*- coding: utf-8 -*-
'''
:codeauthor: :email:`Anthony Shaw <anthonyshaw@apache.org>`
'''
# Import Python Libs
from __future__ import absolute_import
from functools import wraps
# Import Salt Testing Libs
from tests.support.mixins import LoaderModuleMockMixin
from tests.support.unit import TestCase, skipIf
from tests.support.mock import (
MagicMock,
NO_MOCK,
NO_MOCK_REASON
)
# Test data
TEST_FACTS = {
'__opts__': {},
'OPTIONAL_ARGS': {},
'uptime': 'Forever',
'UP': True,
'HOSTNAME': 'test-device.com'
}
TEST_ENVIRONMENT = {
'hot': 'yes'
}
TEST_COMMAND_RESPONSE = {
'show run': 'all the command output'
}
TEST_TRACEROUTE_RESPONSE = {
'success': {
1: {
'probes': {
1: {
'rtt': 1.123,
'ip_address': u'206.223.116.21',
'host_name': u'eqixsj-google-gige.google.com'
}
}
}
}
}
TEST_PING_RESPONSE = {
'success': {
'probes_sent': 5,
'packet_loss': 0,
'rtt_min': 72.158,
'rtt_max': 72.433,
'rtt_avg': 72.268,
'rtt_stddev': 0.094,
'results': [
{
'ip_address': '1.1.1.1',
'rtt': 72.248
}
]
}
}
TEST_ARP_TABLE = [
{
'interface': 'MgmtEth0/RSP0/CPU0/0',
'mac': '5C:5E:AB:DA:3C:F0',
'ip': '172.17.17.1',
'age': 1454496274.84
}
]
TEST_IPADDRS = {
'FastEthernet8': {
'ipv4': {
'10.66.43.169': {
'prefix_length': 22
}
}
}
}
TEST_INTERFACES = {
'Management1': {
'is_up': False,
'is_enabled': False,
'description': u'',
'last_flapped': -1,
'speed': 1000,
'mac_address': u'dead:beef:dead',
}
}
TEST_LLDP_NEIGHBORS = {
u'Ethernet2':
[
{
'hostname': u'junos-unittest',
'port': u'520',
}
]
}
TEST_MAC_TABLE = [
{
'mac': '00:1C:58:29:4A:71',
'interface': 'Ethernet47',
'vlan': 100,
'static': False,
'active': True,
'moves': 1,
'last_move': 1454417742.58
}
]
TEST_RUNNING_CONFIG = {
'one': 'two'
}
TEST_OPTICS = {
'et1': {
'physical_channels': {
'channel': [
{
'index': 0,
'state': {
'input_power': {
'instant': 0.0,
'avg': 0.0,
'min': 0.0,
'max': 0.0,
},
'output_power': {
'instant': 0.0,
'avg': 0.0,
'min': 0.0,
'max': 0.0,
},
'laser_bias_current': {
'instant': 0.0,
'avg': 0.0,
'min': 0.0,
'max': 0.0,
},
}
}
]
}
}
}
class MockNapalmDevice(object):
'''Setup a mock device for our tests'''
def get_facts(self):
return TEST_FACTS
def get_environment(self):
return TEST_ENVIRONMENT
def get_arp_table(self):
return TEST_ARP_TABLE
def get(self, key, default=None, *args, **kwargs):
try:
if key == 'DRIVER':
return self
return TEST_FACTS[key]
except KeyError:
return default
def cli(self, commands, *args, **kwargs):
assert commands[0] == 'show run'
return TEST_COMMAND_RESPONSE
def traceroute(self, destination, **kwargs):
assert destination == 'destination.com'
return TEST_TRACEROUTE_RESPONSE
def ping(self, destination, **kwargs):
assert destination == 'destination.com'
return TEST_PING_RESPONSE
def get_config(self, retrieve='all'):
assert retrieve == 'running'
return TEST_RUNNING_CONFIG
def get_interfaces_ip(self, **kwargs):
return TEST_IPADDRS
def get_interfaces(self, **kwargs):
return TEST_INTERFACES
def get_lldp_neighbors_detail(self, **kwargs):
return TEST_LLDP_NEIGHBORS
def get_mac_address_table(self, **kwargs):
return TEST_MAC_TABLE
def get_optics(self, **kwargs):
return TEST_OPTICS
def load_merge_candidate(self, filename=None, config=None):
assert config == 'new config'
return TEST_RUNNING_CONFIG
def load_replace_candidate(self, filename=None, config=None):
assert config == 'new config'
return TEST_RUNNING_CONFIG
def commit_config(self, **kwargs):
return TEST_RUNNING_CONFIG
def discard_config(self, **kwargs):
return TEST_RUNNING_CONFIG
def compare_config(self, **kwargs):
return TEST_RUNNING_CONFIG
def rollback(self, **kwargs):
return TEST_RUNNING_CONFIG
def mock_proxy_napalm_wrap(func):
'''
The proper decorator checks for proxy minions. We don't care
so just pass back to the origination function
'''
@wraps(func)
def func_wrapper(*args, **kwargs):
func.__globals__['napalm_device'] = MockNapalmDevice()
return func(*args, **kwargs)
return func_wrapper
import salt.utils.napalm as napalm_utils # NOQA
napalm_utils.proxy_napalm_wrap = mock_proxy_napalm_wrap # pylint: disable=E9502
import salt.modules.napalm_network as napalm_network # NOQA
def true(name):
assert name == 'set_ntp_peers'
return True
def random_hash(source, method):
return 12346789
def join(*files):
return True
def get_managed_file(*args, **kwargs):
return 'True'
@skipIf(NO_MOCK, NO_MOCK_REASON)
class NapalmNetworkModuleTestCase(TestCase, LoaderModuleMockMixin):
def setup_loader_modules(self):
module_globals = {
'__salt__': {
'config.option': MagicMock(return_value={
'test': {
'driver': 'test',
'key': '2orgk34kgk34g'
}
}),
'file.file_exists': true,
'file.join': join,
'file.get_managed': get_managed_file,
'random.hash': random_hash
}
}
return {napalm_network: module_globals}
def test_connected_pass(self):
ret = napalm_network.connected()
assert ret['out'] is True
def test_facts(self):
ret = napalm_network.facts()
assert ret['out'] == TEST_FACTS
def test_environment(self):
ret = napalm_network.environment()
assert ret['out'] == TEST_ENVIRONMENT
def test_cli_single_command(self):
'''
Test that CLI works with 1 arg
'''
ret = napalm_network.cli("show run")
assert ret['out'] == TEST_COMMAND_RESPONSE
def test_cli_multi_command(self):
'''
Test that CLI works with 2 arg
'''
ret = napalm_network.cli("show run", "show run")
assert ret['out'] == TEST_COMMAND_RESPONSE
def test_traceroute(self):
ret = napalm_network.traceroute('destination.com')
assert list(ret['out'].keys())[0] == 'success'
def test_ping(self):
ret = napalm_network.ping('destination.com')
assert list(ret['out'].keys())[0] == 'success'
def test_arp(self):
ret = napalm_network.arp()
assert ret['out'] == TEST_ARP_TABLE
def test_ipaddrs(self):
ret = napalm_network.ipaddrs()
assert ret['out'] == TEST_IPADDRS
def test_interfaces(self):
ret = napalm_network.interfaces()
assert ret['out'] == TEST_INTERFACES
def test_lldp(self):
ret = napalm_network.lldp()
assert ret['out'] == TEST_LLDP_NEIGHBORS
def test_mac(self):
ret = napalm_network.mac()
assert ret['out'] == TEST_MAC_TABLE
def test_config(self):
ret = napalm_network.config('running')
assert ret['out'] == TEST_RUNNING_CONFIG
def test_optics(self):
ret = napalm_network.optics()
assert ret['out'] == TEST_OPTICS
def test_load_config(self):
ret = napalm_network.load_config(text='new config')
assert ret['result']
def test_load_config_replace(self):
ret = napalm_network.load_config(text='new config', replace=True)
assert ret['result']
def test_load_template(self):
ret = napalm_network.load_template('set_ntp_peers',
peers=['192.168.0.1'])
assert ret['out'] is None
def test_commit(self):
ret = napalm_network.commit()
assert ret['out'] == TEST_RUNNING_CONFIG
def test_discard_config(self):
ret = napalm_network.discard_config()
assert ret['out'] == TEST_RUNNING_CONFIG
def test_compare_config(self):
ret = napalm_network.compare_config()
assert ret['out'] == TEST_RUNNING_CONFIG
def test_rollback(self):
ret = napalm_network.rollback()
assert ret['out'] == TEST_RUNNING_CONFIG
def test_config_changed(self):
ret = napalm_network.config_changed()
assert ret == (True, '')
def test_config_control(self):
ret = napalm_network.config_control()
assert ret == (True, '')

View file

@ -26,10 +26,12 @@ from salt.ext.six.moves import range
# Import Salt Testing libs
from tests.support.mock import MagicMock
from tests.support.paths import TMP
from tests.support.unit import skipIf
log = logging.getLogger(__name__)
@skipIf(salt.utils.is_windows(), 'Windows does not support Posix IPC')
class BaseIPCReqCase(tornado.testing.AsyncTestCase):
'''
Test the req server/client pair

View file

@ -5,8 +5,9 @@
# Import python libs
from __future__ import absolute_import
import os
import copy
import os
import time
# Import Salt Testing Libs
from tests.support.unit import skipIf, TestCase
@ -17,6 +18,15 @@ import tests.integration as integration
import salt.config
from salt.utils.schedule import Schedule
# pylint: disable=import-error,unused-import
try:
import croniter
_CRON_SUPPORTED = True
except ImportError:
_CRON_SUPPORTED = False
# pylint: enable=import-error
ROOT_DIR = os.path.join(integration.TMP, 'schedule-unit-tests')
SOCK_DIR = os.path.join(ROOT_DIR, 'test-socks')
@ -28,6 +38,7 @@ DEFAULT_CONFIG['pki_dir'] = os.path.join(ROOT_DIR, 'pki')
DEFAULT_CONFIG['cachedir'] = os.path.join(ROOT_DIR, 'cache')
# pylint: disable=too-many-public-methods,invalid-name
@skipIf(NO_MOCK, NO_MOCK_REASON)
class ScheduleTestCase(TestCase):
'''
@ -276,3 +287,47 @@ class ScheduleTestCase(TestCase):
'''
self.schedule.opts.update({'schedule': {}, 'pillar': {'schedule': ''}})
self.assertRaises(ValueError, Schedule.eval, self.schedule)
def test_eval_schedule_time(self):
'''
Tests eval if the schedule setting time is in the future
'''
self.schedule.opts.update({'pillar': {'schedule': {}}})
self.schedule.opts.update({'schedule': {'testjob': {'function': 'test.true', 'seconds': 60}}})
now = int(time.time())
self.schedule.eval()
self.assertTrue(self.schedule.opts['schedule']['testjob']['_next_fire_time'] > now)
def test_eval_schedule_time_eval(self):
'''
Tests eval if the schedule setting time is in the future plus splay
'''
self.schedule.opts.update({'pillar': {'schedule': {}}})
self.schedule.opts.update(
{'schedule': {'testjob': {'function': 'test.true', 'seconds': 60, 'splay': 5}}})
now = int(time.time())
self.schedule.eval()
self.assertTrue(self.schedule.opts['schedule']['testjob']['_splay'] - now > 60)
@skipIf(not _CRON_SUPPORTED, 'croniter module not installed')
def test_eval_schedule_cron(self):
'''
Tests eval if the schedule is defined with cron expression
'''
self.schedule.opts.update({'pillar': {'schedule': {}}})
self.schedule.opts.update({'schedule': {'testjob': {'function': 'test.true', 'cron': '* * * * *'}}})
now = int(time.time())
self.schedule.eval()
self.assertTrue(self.schedule.opts['schedule']['testjob']['_next_fire_time'] > now)
@skipIf(not _CRON_SUPPORTED, 'croniter module not installed')
def test_eval_schedule_cron_splay(self):
'''
Tests eval if the schedule is defined with cron expression plus splay
'''
self.schedule.opts.update({'pillar': {'schedule': {}}})
self.schedule.opts.update(
{'schedule': {'testjob': {'function': 'test.true', 'cron': '* * * * *', 'splay': 5}}})
self.schedule.eval()
self.assertTrue(self.schedule.opts['schedule']['testjob']['_splay'] >
self.schedule.opts['schedule']['testjob']['_next_fire_time'])