mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
Merge branch '2017.7' into pop
This commit is contained in:
commit
0efb90b6f7
28 changed files with 1054 additions and 60 deletions
|
@ -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
|
||||
===============================
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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_:
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 **
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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': [
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
localhost:
|
||||
host: 127.0.0.1
|
||||
port: 2827
|
||||
mine_functions:
|
||||
test.arg: ['itworked']
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
return_changes:
|
||||
test.succeed_with_changes:
|
||||
- watch_in:
|
||||
- test: watch_states
|
||||
|
||||
watch_states:
|
||||
test.succeed_without_changes
|
|
@ -0,0 +1,7 @@
|
|||
return_changes:
|
||||
test.fail_with_changes:
|
||||
- watch_in:
|
||||
- test: watch_states
|
||||
|
||||
watch_states:
|
||||
test.succeed_without_changes
|
|
@ -1,4 +1,5 @@
|
|||
{% set jinja = 'test' %}
|
||||
ssh-file-test:
|
||||
file.managed:
|
||||
- name: /tmp/test
|
||||
- name: /tmp/{{ jinja }}
|
||||
- contents: 'test'
|
||||
|
|
|
@ -6,3 +6,6 @@ base:
|
|||
- generic
|
||||
- blackout
|
||||
- sub
|
||||
'localhost':
|
||||
- generic
|
||||
- blackout
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
34
tests/integration/ssh/test_mine.py
Normal file
34
tests/integration/ssh/test_mine.py
Normal 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)
|
41
tests/integration/ssh/test_pillar.py
Normal file
41
tests/integration/ssh/test_pillar.py
Normal 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, '')
|
|
@ -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):
|
||||
'''
|
||||
|
|
|
@ -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,
|
||||
|
|
383
tests/unit/modules/test_napalm_network.py
Normal file
383
tests/unit/modules/test_napalm_network.py
Normal 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, '')
|
|
@ -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
|
||||
|
|
|
@ -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'])
|
||||
|
|
Loading…
Add table
Reference in a new issue