Merge branch 'develop' into proxycaller

This commit is contained in:
Nicole Thomas 2018-07-31 16:30:22 -04:00 committed by GitHub
commit ed3d85be98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1497 additions and 173 deletions

4
.github/stale.yml vendored
View File

@ -1,8 +1,8 @@
# Probot Stale configuration file
# Number of days of inactivity before an issue becomes stale
# 620 is approximately 1 year and 8 months
daysUntilStale: 620
# 610 is approximately 1 year and 8 months
daysUntilStale: 610
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7

View File

@ -26,14 +26,14 @@
#no_proxy: []
# If multiple masters are specified in the 'master' setting, the default behavior
# is to always try to connect to them in the order they are listed. If random_master is
# set to True, the order will be randomized instead. This can be helpful in distributing
# the load of many minions executing salt-call requests, for example, from a cron job.
# If only one master is listed, this setting is ignored and a warning will be logged.
# NOTE: If master_type is set to failover, use master_shuffle instead.
# is to always try to connect to them in the order they are listed. If random_master
# is set to True, the order will be randomized upon Minion startup instead. This can
# be helpful in distributing the load of many minions executing salt-call requests,
# for example, from a cron job. If only one master is listed, this setting is ignored
# and a warning will be logged.
#random_master: False
# Use if master_type is set to failover.
# NOTE: Deprecated in Salt Fluorine. Use 'random_master' instead.
#master_shuffle: False
# Minions can connect to multiple masters simultaneously (all masters

View File

@ -286,13 +286,14 @@ to the next master in the list if it finds the existing one is dead.
------------------
.. versionadded:: 2014.7.0
.. deprecated:: Fluorine
Default: ``False``
If :conf_minion:`master` is a list of addresses and :conf_minion`master_type`
is ``failover``, shuffle them before trying to connect to distribute the
minions over all available masters. This uses Python's :func:`random.shuffle
<python2:random.shuffle>` method.
.. warning::
This option has been deprecated in Salt ``Fluorine``. Please use
:conf_minion:`random_master` instead.
.. code-block:: yaml
@ -303,17 +304,38 @@ minions over all available masters. This uses Python's :func:`random.shuffle
``random_master``
-----------------
.. versionadded:: 2014.7.0
.. versionchanged:: Fluorine
The :conf_minion:`master_failback` option can be used in conjunction with
``random_master`` to force the minion to fail back to the first master in the
list if the first master is back online. Note that :conf_minion:`master_type`
must be set to ``failover`` in order for the ``master_failback`` setting to
work.
Default: ``False``
If :conf_minion:`master` is a list of addresses, and :conf_minion`master_type`
is set to ``failover`` shuffle them before trying to connect to distribute the
minions over all available masters. This uses Python's :func:`random.shuffle
<python2:random.shuffle>` method.
If :conf_minion:`master` is a list of addresses, shuffle them before trying to
connect to distribute the minions over all available masters. This uses Python's
:func:`random.shuffle <python2:random.shuffle>` method.
If multiple masters are specified in the 'master' setting as a list, the default
behavior is to always try to connect to them in the order they are listed. If
``random_master`` is set to True, the order will be randomized instead upon Minion
startup. This can be helpful in distributing the load of many minions executing
``salt-call`` requests, for example, from a cron job. If only one master is listed,
this setting is ignored and a warning is logged.
.. code-block:: yaml
random_master: True
.. note::
When the ``failover``, ``master_failback``, and ``random_master`` options are
used together, only the "secondary masters" will be shuffled. The first master
in the list is ignored in the :func:`random.shuffle <python2:random.shuffle>`
call. See :conf_minion:`master_failback` for more information.
.. conf_minion:: retry_dns
``retry_dns``

View File

@ -87,6 +87,7 @@ execution modules
chocolatey
chronos
cimc
ciscoconfparse_mod
cisconso
cloud
cmdmod
@ -271,9 +272,9 @@ execution modules
namecheap_ns
namecheap_ssl
namecheap_users
napalm
napalm_acl
napalm_bgp
napalm_mod
napalm_network
napalm_ntp
napalm_probes

View File

@ -0,0 +1,7 @@
======================================
salt.modules.ciscoconfparse_mod module
======================================
.. automodule:: salt.modules.ciscoconfparse_mod
:members:

View File

@ -1,7 +0,0 @@
==========================
salt.modules.napalm module
==========================
.. automodule:: salt.modules.napalm
:members:

View File

@ -0,0 +1,7 @@
==============================
salt.modules.napalm_mod module
==============================
.. automodule:: salt.modules.napalm_mod
:members:

View File

@ -12,7 +12,7 @@ Requirements
.. note::
Support ``winexe`` and ``impacket`` has been deprecated and will be removed in
Fluorine. These dependencies are replaced by ``pypsexec`` and ``smbprotocol``
Sodium. These dependencies are replaced by ``pypsexec`` and ``smbprotocol``
respectivly. These are pure python alternatives that are compatible with all
supported python versions.

View File

@ -521,6 +521,12 @@ their code to use ``tgt_type``.
>>> local.cmd('*', 'cmd.run', ['whoami'], tgt_type='glob')
{'jerry': 'root'}
Minion Configuration Deprecations
---------------------------------
The :conf_minion:`master_shuffle` configuration option is deprecated as of the
``Fluorine`` release. Please use the :conf_minion:`random_master` option instead.
Module Deprecations
-------------------
@ -677,6 +683,11 @@ State Deprecations
Utils Deprecations
------------------
The ``cloud`` utils module had the following changes:
- Support for the ``cache_nodes_ip`` function in :mod:`salt utils module <salt.utils.cloud>`
has been removed. The function was incomplete and non-functional.
The ``vault`` utils module had the following changes:
- Support for specifying Vault connection data within a 'profile' has been removed.

View File

@ -64,7 +64,7 @@ To match a nodegroup on the CLI, use the ``-N`` command-line option:
The ``N@`` classifier historically could not be used in compound matches
within the CLI or :term:`top file`, it was only recognized in the
:conf_master:`nodegroups` master config file parameter. As of Fluorine
:conf_master:`nodegroups` master config file parameter. As of the Fluorine
release, this limitation no longer exists.
To match a nodegroup in your :term:`top file`, make sure to put ``- match:

View File

@ -0,0 +1,37 @@
- Feature Name: adding_python_package_logic_to_loader
- Start Date: 2018-06-29
- RFC PR:
- Salt Issue:
# Summary
[summary]: #summary
Add support for python packages to the loader. Instead of just having
apache.py, we would have `salt/modules/apache/__init__.py`, and all the other
modules under that directory loaded in the `apache.<function>` namespace that
we already have.
# Motivation
[motivation]: #motivation
Split modules out into different git repositories, and allow the community more control to help maintain them.
# Design
[design]: #detailed-design
This should be pretty straight forward, we should be able to use the same
_module_dirs that we are using now, and iterate over the files, and if they are
a directory, then we load all of the modules underneath it into the namespace
of the directory.
Heirarchy would be alphabetical, and we would need to log messages if the
function exists in an earlier file that has been loaded.
## Alternatives
[alternatives]: #alternatives
## Unresolved questions
[unresolved]: #unresolved-questions
# Drawbacks
[drawbacks]: #drawbacks

217
salt/beacons/watchdog.py Normal file
View File

@ -0,0 +1,217 @@
# -*- coding: utf-8 -*-
'''
watchdog beacon
.. versionadded:: Fluorine
Watch files and translate the changes into salt events
:depends: - watchdog Python module >= 0.8.3
'''
# Import Python libs
from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
import collections
import logging
from salt.ext.six.moves import map
# Import third party libs
try:
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
HAS_WATCHDOG = True
except ImportError:
HAS_WATCHDOG = False
class FileSystemEventHandler(object):
""" A dummy class to make the import work """
def __init__(self):
pass
__virtualname__ = 'watchdog'
log = logging.getLogger(__name__)
DEFAULT_MASK = [
'create',
'delete',
'modify',
'move',
]
class Handler(FileSystemEventHandler):
def __init__(self, queue, masks=None):
super(Handler, self).__init__()
self.masks = masks or DEFAULT_MASK
self.queue = queue
def on_created(self, event):
self._append_if_mask(event, 'create')
def on_modified(self, event):
self._append_if_mask(event, 'modify')
def on_deleted(self, event):
self._append_if_mask(event, 'delete')
def on_moved(self, event):
self._append_if_mask(event, 'move')
def _append_if_mask(self, event, mask):
logging.debug(event)
self._append_path_if_mask(event, mask)
def _append_path_if_mask(self, event, mask):
if mask in self.masks:
self.queue.append(event)
def __virtual__():
if HAS_WATCHDOG:
return __virtualname__
return False
def _get_queue(config):
'''
Check the context for the notifier and construct it if not present
'''
if 'watchdog.observer' not in __context__:
queue = collections.deque()
observer = Observer()
for path in config.get('directories', {}):
path_params = config.get('directories').get(path)
masks = path_params.get('mask', DEFAULT_MASK)
event_handler = Handler(queue, masks)
observer.schedule(event_handler, path)
observer.start()
__context__['watchdog.observer'] = observer
__context__['watchdog.queue'] = queue
return __context__['watchdog.queue']
class ValidationError(Exception):
pass
def validate(config):
'''
Validate the beacon configuration
'''
try:
_validate(config)
return True, 'Valid beacon configuration'
except ValidationError as error:
return False, str(error)
def _validate(config):
if not isinstance(config, list):
raise ValidationError(
'Configuration for watchdog beacon must be a list.')
_config = {}
for part in config:
_config.update(part)
if 'directories' not in _config:
raise ValidationError(
'Configuration for watchdog beacon must include directories.')
if not isinstance(_config['directories'], dict):
raise ValidationError(
'Configuration for watchdog beacon directories must be a '
'dictionary.')
for path in _config['directories']:
_validate_path(_config['directories'][path])
def _validate_path(path_config):
if not isinstance(path_config, dict):
raise ValidationError(
'Configuration for watchdog beacon directory path must be '
'a dictionary.')
if 'mask' in path_config:
_validate_mask(path_config['mask'])
def _validate_mask(mask_config):
valid_mask = [
'create',
'modify',
'delete',
'move',
]
if not isinstance(mask_config, list):
raise ValidationError(
'Configuration for watchdog beacon mask must be list.')
if any(mask not in valid_mask for mask in mask_config):
raise ValidationError(
'Configuration for watchdog beacon contains invalid mask')
def to_salt_event(event):
return {
'tag': __virtualname__,
'path': event.src_path,
'change': event.event_type,
}
def beacon(config):
'''
Watch the configured directories
Example Config
.. code-block:: yaml
beacons:
watchdog:
- directories:
/path/to/dir:
mask:
- create
- modify
- delete
- move
The mask list can contain the following events (the default mask is create,
modify delete, and move):
* create - File or directory is created in watched directory
* modify - The watched directory is modified
* delete - File or directory is deleted from watched directory
* move - File or directory is moved or renamed in the watched directory
'''
_config = {}
list(map(_config.update, config))
queue = _get_queue(_config)
ret = []
while queue:
ret.append(to_salt_event(queue.popleft()))
return ret
def close(config):
observer = __context__.pop('watchdog.observer', None)
if observer:
observer.stop()

View File

@ -351,7 +351,9 @@ class SSH(object):
return
hostname = self.opts['tgt'].split('@')[-1]
needs_expansion = '*' not in hostname and salt.utils.network.is_reachable_host(hostname)
needs_expansion = '*' not in hostname and \
salt.utils.network.is_reachable_host(hostname) and \
salt.utils.network.is_ip(hostname)
if needs_expansion:
hostname = salt.utils.network.ip_to_host(hostname)
if hostname is None:

View File

@ -134,7 +134,8 @@ VALID_OPTS = {
# a master fingerprint with `salt-key -F master`
'master_finger': six.string_types,
# Selects a random master when starting a minion up in multi-master mode
# Deprecated in Fluorine. Use 'random_master' instead.
# Do not remove! Keep as an alias for usability.
'master_shuffle': bool,
# When in multi-master mode, temporarily remove a master from the list if a conenction
@ -967,6 +968,8 @@ VALID_OPTS = {
# Never give up when trying to authenticate to a master
'auth_safemode': bool,
# Selects a random master when starting a minion up in multi-master mode or
# when starting a minion with salt-call. ``master`` must be a list.
'random_master': bool,
# An upper bound for the amount of time for a minion to sleep before attempting to

View File

@ -90,6 +90,10 @@ if salt.utils.platform.is_windows():
'will be missing'
)
HAS_UNAME = True
if not hasattr(os, 'uname'):
HAS_UNAME = False
_INTERFACES = {}
@ -693,7 +697,7 @@ def _virtual(osdata):
# Quick backout for BrandZ (Solaris LX Branded zones)
# Don't waste time trying other commands to detect the virtual grain
if osdata['kernel'] == 'Linux' and 'BrandZ virtual linux' in os.uname():
if HAS_UNAME and osdata['kernel'] == 'Linux' and 'BrandZ virtual linux' in os.uname():
grains['virtual'] = 'zone'
return grains
@ -1846,7 +1850,10 @@ def os_data():
elif grains['kernel'] == 'SunOS':
if salt.utils.platform.is_smartos():
# See https://github.com/joyent/smartos-live/issues/224
uname_v = os.uname()[3] # format: joyent_20161101T004406Z
if HAS_UNAME:
uname_v = os.uname()[3] # format: joyent_20161101T004406Z
else:
uname_v = os.name
uname_v = uname_v[uname_v.index('_')+1:]
grains['os'] = grains['osfullname'] = 'SmartOS'
# store a parsed version of YYYY.MM.DD as osrelease
@ -1875,7 +1882,10 @@ def os_data():
else:
if development is not None:
osname = ' '.join((osname, development))
uname_v = os.uname()[3]
if HAS_UNAME:
uname_v = os.uname()[3]
else:
uname_v = os.name
grains['os'] = grains['osfullname'] = osname
if osname in ['Oracle Solaris'] and uname_v.startswith(osmajorrelease):
# Oracla Solars 11 and up have minor version in uname

View File

@ -507,12 +507,12 @@ class MinionBase(object):
log.warning('master_type = distributed needs more than 1 master.')
if opts['master_shuffle']:
if opts['master_failback']:
secondary_masters = opts['master'][1:]
shuffle(secondary_masters)
opts['master'][1:] = secondary_masters
else:
shuffle(opts['master'])
log.warning(
'Use of \'master_shuffle\' detected. \'master_shuffle\' is deprecated in favor '
'of \'random_master\'. Please update your minion config file.'
)
opts['random_master'] = opts['master_shuffle']
opts['auth_tries'] = 0
if opts['master_failback'] and opts['master_failback_interval'] == 0:
opts['master_failback_interval'] = opts['master_alive_interval']
@ -578,12 +578,19 @@ class MinionBase(object):
# happy with the first one that allows us to connect
if isinstance(opts['master'], list):
conn = False
# shuffle the masters and then loop through them
opts['local_masters'] = copy.copy(opts['master'])
if opts['random_master']:
shuffle(opts['local_masters'])
last_exc = None
opts['master_uri_list'] = list()
opts['master_uri_list'] = []
opts['local_masters'] = copy.copy(opts['master'])
# shuffle the masters and then loop through them
if opts['random_master']:
# master_failback is only used when master_type is set to failover
if opts['master_type'] == 'failover' and opts['master_failback']:
secondary_masters = opts['local_masters'][1:]
shuffle(secondary_masters)
opts['local_masters'][1:] = secondary_masters
else:
shuffle(opts['local_masters'])
# This sits outside of the connection loop below because it needs to set
# up a list of master URIs regardless of which masters are available

View File

@ -0,0 +1,443 @@
# -*- coding: utf-8 -*-
'''
Execution module for `ciscoconfparse <http://www.pennington.net/py/ciscoconfparse/index.html>`_
.. versionadded:: Fluorine
This module can be used for basic configuration parsing, audit or validation
for a variety of network platforms having Cisco IOS style configuration (one
space indentation), including: Cisco IOS, Cisco Nexus, Cisco IOS-XR,
Cisco IOS-XR, Cisco ASA, Arista EOS, Brocade, HP Switches, Dell PowerConnect
Switches, or Extreme Networks devices. In newer versions, ``ciscoconfparse``
provides support for brace-delimited configuration style as well, for platforms
such as: Juniper Junos, Palo Alto, or F5 Networks.
See http://www.pennington.net/py/ciscoconfparse/index.html for further details.
:depends: ciscoconfparse
This module depends on the Python library with the same name,
``ciscoconfparse`` - to install execute: ``pip install ciscoconfparse``.
'''
# Import Python Libs
from __future__ import absolute_import, unicode_literals, print_function
# Import Salt modules
from salt.ext import six
from salt.exceptions import SaltException
try:
import ciscoconfparse
HAS_CISCOCONFPARSE = True
except ImportError:
HAS_CISCOCONFPARSE = False
# ------------------------------------------------------------------------------
# module properties
# ------------------------------------------------------------------------------
__virtualname__ = 'ciscoconfparse'
# ------------------------------------------------------------------------------
# property functions
# ------------------------------------------------------------------------------
def __virtual__():
return HAS_CISCOCONFPARSE
# ------------------------------------------------------------------------------
# helper functions -- will not be exported
# ------------------------------------------------------------------------------
def _get_ccp(config=None, config_path=None, saltenv='base'):
'''
'''
if config_path:
config = __salt__['cp.get_file_str'](config_path, saltenv=saltenv)
if config is False:
raise SaltException('{} is not available'.format(config_path))
if isinstance(config, six.string_types):
config = config.splitlines()
ccp = ciscoconfparse.CiscoConfParse(config)
return ccp
# ------------------------------------------------------------------------------
# callable functions
# ------------------------------------------------------------------------------
def find_objects(config=None, config_path=None, regex=None, saltenv='base'):
'''
Return all the line objects that match the expression in the ``regex``
argument.
.. warning::
This function is mostly valuable when invoked from other Salt
components (i.e., execution modules, states, templates etc.). For CLI
usage, please consider using
:py:func:`ciscoconfparse.find_lines <salt.ciscoconfparse_mod.find_lines>`
config
The configuration sent as text.
.. note::
This argument is ignored when ``config_path`` is specified.
config_path
The absolute or remote path to the file with the configuration to be
parsed. This argument supports the usual Salt filesystem URIs, e.g.,
``salt://``, ``https://``, ``ftp://``, ``s3://``, etc.
regex
The regular expression to match the lines against.
saltenv: ``base``
Salt fileserver environment from which to retrieve the file. This
argument is ignored when ``config_path`` is not a ``salt://`` URL.
Usage example:
.. code-block:: python
objects = __salt__['ciscoconfparse.find_objects'](config_path='salt://path/to/config.txt',
regex='Gigabit')
for obj in objects:
print(obj.text)
'''
ccp = _get_ccp(config=config, config_path=config_path, saltenv=saltenv)
lines = ccp.find_objects(regex)
return lines
def find_lines(config=None, config_path=None, regex=None, saltenv='base'):
'''
Return all the lines (as text) that match the expression in the ``regex``
argument.
config
The configuration sent as text.
.. note::
This argument is ignored when ``config_path`` is specified.
config_path
The absolute or remote path to the file with the configuration to be
parsed. This argument supports the usual Salt filesystem URIs, e.g.,
``salt://``, ``https://``, ``ftp://``, ``s3://``, etc.
regex
The regular expression to match the lines against.
saltenv: ``base``
Salt fileserver environment from which to retrieve the file. This
argument is ignored when ``config_path`` is not a ``salt://`` URL.
CLI Example:
.. code-block:: bash
salt '*' ciscoconfparse.find_lines config_path=https://bit.ly/2mAdq7z regex='ip address'
Output example:
.. code-block:: text
cisco-ios-router:
- ip address dhcp
- ip address 172.20.0.1 255.255.255.0
- no ip address
'''
lines = find_objects(config=config,
config_path=config_path,
regex=regex,
saltenv=saltenv)
return [line.text for line in lines]
def find_objects_w_child(config=None,
config_path=None,
parent_regex=None,
child_regex=None,
ignore_ws=False,
saltenv='base'):
'''
Parse through the children of all parent lines matching ``parent_regex``,
and return a list of child objects, which matched the ``child_regex``.
.. warning::
This function is mostly valuable when invoked from other Salt
components (i.e., execution modules, states, templates etc.). For CLI
usage, please consider using
:py:func:`ciscoconfparse.find_lines_w_child <salt.ciscoconfparse_mod.find_lines_w_child>`
config
The configuration sent as text.
.. note::
This argument is ignored when ``config_path`` is specified.
config_path
The absolute or remote path to the file with the configuration to be
parsed. This argument supports the usual Salt filesystem URIs, e.g.,
``salt://``, ``https://``, ``ftp://``, ``s3://``, etc.
parent_regex
The regular expression to match the parent lines against.
child_regex
The regular expression to match the child lines against.
ignore_ws: ``False``
Whether to ignore the white spaces.
saltenv: ``base``
Salt fileserver environment from which to retrieve the file. This
argument is ignored when ``config_path`` is not a ``salt://`` URL.
Usage example:
.. code-block:: python
objects = __salt__['ciscoconfparse.find_objects_w_child'](config_path='https://bit.ly/2mAdq7z',
parent_regex='line con',
child_regex='stopbits')
for obj in objects:
print(obj.text)
'''
ccp = _get_ccp(config=config, config_path=config_path, saltenv=saltenv)
lines = ccp.find_objects_w_child(parent_regex, child_regex, ignore_ws=ignore_ws)
return lines
def find_lines_w_child(config=None,
config_path=None,
parent_regex=None,
child_regex=None,
ignore_ws=False,
saltenv='base'):
r'''
Return a list of parent lines (as text) matching the regular expression
``parent_regex`` that have children lines matching ``child_regex``.
config
The configuration sent as text.
.. note::
This argument is ignored when ``config_path`` is specified.
config_path
The absolute or remote path to the file with the configuration to be
parsed. This argument supports the usual Salt filesystem URIs, e.g.,
``salt://``, ``https://``, ``ftp://``, ``s3://``, etc.
parent_regex
The regular expression to match the parent lines against.
child_regex
The regular expression to match the child lines against.
ignore_ws: ``False``
Whether to ignore the white spaces.
saltenv: ``base``
Salt fileserver environment from which to retrieve the file. This
argument is ignored when ``config_path`` is not a ``salt://`` URL.
CLI Example:
.. code-block:: bash
salt '*' ciscoconfparse.find_lines_w_child config_path=https://bit.ly/2mAdq7z parent_line='line con' child_line='stopbits'
salt '*' ciscoconfparse.find_lines_w_child config_path=https://bit.ly/2uIRxau parent_regex='ge-(.*)' child_regex='unit \d+'
'''
lines = find_objects_w_child(config=config,
config_path=config_path,
parent_regex=parent_regex,
child_regex=child_regex,
ignore_ws=ignore_ws,
saltenv=saltenv)
return [line.text for line in lines]
def find_objects_wo_child(config=None,
config_path=None,
parent_regex=None,
child_regex=None,
ignore_ws=False,
saltenv='base'):
'''
Return a list of parent ``ciscoconfparse.IOSCfgLine`` objects, which matched
the ``parent_regex`` and whose children did *not* match ``child_regex``.
Only the parent ``ciscoconfparse.IOSCfgLine`` objects will be returned. For
simplicity, this method only finds oldest ancestors without immediate
children that match.
.. warning::
This function is mostly valuable when invoked from other Salt
components (i.e., execution modules, states, templates etc.). For CLI
usage, please consider using
:py:func:`ciscoconfparse.find_lines_wo_child <salt.ciscoconfparse_mod.find_lines_wo_child>`
config
The configuration sent as text.
.. note::
This argument is ignored when ``config_path`` is specified.
config_path
The absolute or remote path to the file with the configuration to be
parsed. This argument supports the usual Salt filesystem URIs, e.g.,
``salt://``, ``https://``, ``ftp://``, ``s3://``, etc.
parent_regex
The regular expression to match the parent lines against.
child_regex
The regular expression to match the child lines against.
ignore_ws: ``False``
Whether to ignore the white spaces.
saltenv: ``base``
Salt fileserver environment from which to retrieve the file. This
argument is ignored when ``config_path`` is not a ``salt://`` URL.
Usage example:
.. code-block:: python
objects = __salt__['ciscoconfparse.find_objects_wo_child'](config_path='https://bit.ly/2mAdq7z',
parent_regex='line con',
child_regex='stopbits')
for obj in objects:
print(obj.text)
'''
ccp = _get_ccp(config=config, config_path=config_path, saltenv=saltenv)
lines = ccp.find_objects_wo_child(parent_regex, child_regex, ignore_ws=ignore_ws)
return lines
def find_lines_wo_child(config=None,
config_path=None,
parent_regex=None,
child_regex=None,
ignore_ws=False,
saltenv='base'):
'''
Return a list of parent ``ciscoconfparse.IOSCfgLine`` lines as text, which
matched the ``parent_regex`` and whose children did *not* match ``child_regex``.
Only the parent ``ciscoconfparse.IOSCfgLine`` text lines will be returned.
For simplicity, this method only finds oldest ancestors without immediate
children that match.
config
The configuration sent as text.
.. note::
This argument is ignored when ``config_path`` is specified.
config_path
The absolute or remote path to the file with the configuration to be
parsed. This argument supports the usual Salt filesystem URIs, e.g.,
``salt://``, ``https://``, ``ftp://``, ``s3://``, etc.
parent_regex
The regular expression to match the parent lines against.
child_regex
The regular expression to match the child lines against.
ignore_ws: ``False``
Whether to ignore the white spaces.
saltenv: ``base``
Salt fileserver environment from which to retrieve the file. This
argument is ignored when ``config_path`` is not a ``salt://`` URL.
CLI Example:
.. code-block:: bash
salt '*' ciscoconfparse.find_lines_wo_child config_path=https://bit.ly/2mAdq7z parent_line='line con' child_line='stopbits'
'''
lines = find_objects_wo_child(config=config,
config_path=config_path,
parent_regex=parent_regex,
child_regex=child_regex,
ignore_ws=ignore_ws,
saltenv=saltenv)
return [line.text for line in lines]
def filter_lines(config=None,
config_path=None,
parent_regex=None,
child_regex=None,
saltenv='base'):
'''
Return a list of detailed matches, for the configuration blocks (parent-child
relationship) whose parent respects the regular expressions configured via
the ``parent_regex`` argument, and the child matches the ``child_regex``
regular expression. The result is a list of dictionaries with the following
keys:
- ``match``: a boolean value that tells whether ``child_regex`` matched any
children lines.
- ``parent``: the parent line (as text).
- ``child``: the child line (as text). If no child line matched, this field
will be ``None``.
Note that the return list contains the elements that matched the parent
condition, the ``parent_regex`` regular expression. Therefore, the ``parent``
field will always have a valid value, while ``match`` and ``child`` may
default to ``False`` and ``None`` respectively when there is not child match.
CLI Example:
.. code-block:: bash
salt '*' ciscoconfparse.filter_lines config_path=https://bit.ly/2mAdq7z parent_regex='Gigabit' child_regex='shutdown'
Example output (for the example above):
.. code-block:: python
[
{
'parent': 'interface GigabitEthernet1',
'match': False,
'child': None
},
{
'parent': 'interface GigabitEthernet2',
'match': True,
'child': ' shutdown'
},
{
'parent': 'interface GigabitEthernet3',
'match': True,
'child': ' shutdown'
}
]
'''
ret = []
ccp = _get_ccp(config=config, config_path=config_path, saltenv=saltenv)
parent_lines = ccp.find_objects(parent_regex)
for parent_line in parent_lines:
child_lines = parent_line.re_search_children(child_regex)
if child_lines:
for child_line in child_lines:
ret.append({
'match': True,
'parent': parent_line.text,
'child': child_line.text
})
else:
ret.append({
'match': False,
'parent': parent_line.text,
'child': None
})
return ret

View File

@ -462,6 +462,7 @@ def gather_bootstrap_script(bootstrap=None):
if 'Success' in ret and len(ret['Success']['Files updated']) > 0:
return ret['Success']['Files updated'][0]
def items():
'''
Return the complete config from the currently running minion process.

View File

@ -85,6 +85,10 @@ def usage(args=None):
'''
Return usage information for volumes mounted on this minion
.. versionchanged:: Fluorine
Default for SunOS changed to 1 kilobyte blocks
CLI Example:
.. code-block:: bash
@ -103,6 +107,8 @@ def usage(args=None):
cmd = 'df -P'
elif __grains__['kernel'] == 'OpenBSD' or __grains__['kernel'] == 'AIX':
cmd = 'df -kP'
elif __grains__['kernel'] == 'SunOS':
cmd = 'df -k'
else:
cmd = 'df'
if flags:

View File

@ -20,6 +20,7 @@ from salt.utils.napalm import proxy_napalm_wrap
# Import Salt modules
from salt.ext import six
from salt.utils.decorators import depends
from salt.exceptions import CommandExecutionError
try:
from netmiko import BaseConnection
@ -39,6 +40,12 @@ try:
except ImportError:
HAS_JXMLEASE = False
try:
import ciscoconfparse # pylint: disable=unused-import
HAS_CISCOCONFPARSE = True
except ImportError:
HAS_CISCOCONFPARSE = False
# ----------------------------------------------------------------------------------------------------------------------
# module properties
# ----------------------------------------------------------------------------------------------------------------------
@ -1286,3 +1293,156 @@ def rpc(command, **kwargs):
napalm_map.update(default_map)
fun = napalm_map.get(__grains__['os'], 'napalm.netmiko_commands')
return __salt__[fun](command, **kwargs)
@depends(HAS_CISCOCONFPARSE)
def config_find_lines(regex, source='running'):
r'''
.. versionadded:: Fluorine
Return the configuration lines that match the regular expressions from the
``regex`` argument. The configuration is read from the network device
interrogated.
regex
The regular expression to match the configuration lines against.
source: ``running``
The configuration type to retrieve from the network device. Default:
``running``. Available options: ``running``, ``startup``, ``candidate``.
CLI Example:
.. code-block:: bash
salt '*' napalm.config_find_lines '^interface Ethernet1\d'
'''
config_txt = __salt__['net.config'](source=source)['out'][source]
return __salt__['ciscoconfparse.find_lines'](config=config_txt,
regex=regex)
@depends(HAS_CISCOCONFPARSE)
def config_lines_w_child(parent_regex, child_regex, source='running'):
r'''
.. versionadded:: Fluorine
Return the configuration lines that match the regular expressions from the
``parent_regex`` argument, having child lines matching ``child_regex``.
The configuration is read from the network device interrogated.
.. note::
This function is only available only when the underlying library
`ciscoconfparse <http://www.pennington.net/py/ciscoconfparse/index.html>`_
is installed. See
:py:func:`ciscoconfparse module <salt.modules.ciscoconfparse_mod>` for
more details.
parent_regex
The regular expression to match the parent configuration lines against.
child_regex
The regular expression to match the child configuration lines against.
source: ``running``
The configuration type to retrieve from the network device. Default:
``running``. Available options: ``running``, ``startup``, ``candidate``.
CLI Example:
.. code-block:: bash
salt '*' napalm.config_lines_w_child '^interface' 'ip address'
salt '*' napalm.config_lines_w_child '^interface' 'shutdown' source=candidate
'''
config_txt = __salt__['net.config'](source=source)['out'][source]
return __salt__['ciscoconfparse.find_lines_w_child'](config=config_txt,
parent_regex=parent_regex,
child_regex=child_regex)
@depends(HAS_CISCOCONFPARSE)
def config_lines_wo_child(parent_regex, child_regex, source='running'):
'''
.. versionadded:: Fluorine
Return the configuration lines that match the regular expressions from the
``parent_regex`` argument, having the child lines *not* matching
``child_regex``.
The configuration is read from the network device interrogated.
.. note::
This function is only available only when the underlying library
`ciscoconfparse <http://www.pennington.net/py/ciscoconfparse/index.html>`_
is installed. See
:py:func:`ciscoconfparse module <salt.modules.ciscoconfparse_mod>` for
more details.
parent_regex
The regular expression to match the parent configuration lines against.
child_regex
The regular expression to match the child configuration lines against.
source: ``running``
The configuration type to retrieve from the network device. Default:
``running``. Available options: ``running``, ``startup``, ``candidate``.
CLI Example:
.. code-block:: bash
salt '*' napalm.config_lines_wo_child '^interface' 'ip address'
salt '*' napalm.config_lines_wo_child '^interface' 'shutdown' source=candidate
'''
config_txt = __salt__['net.config'](source=source)['out'][source]
return __salt__['ciscoconfparse.find_lines_wo_child'](config=config_txt,
parent_regex=parent_regex,
child_regex=child_regex)
@depends(HAS_CISCOCONFPARSE)
def config_filter_lines(parent_regex, child_regex, source='running'):
r'''
.. versionadded:: Fluorine
Return a list of detailed matches, for the configuration blocks (parent-child
relationship) whose parent respects the regular expressions configured via
the ``parent_regex`` argument, and the child matches the ``child_regex``
regular expression. The result is a list of dictionaries with the following
keys:
- ``match``: a boolean value that tells whether ``child_regex`` matched any
children lines.
- ``parent``: the parent line (as text).
- ``child``: the child line (as text). If no child line matched, this field
will be ``None``.
.. note::
This function is only available only when the underlying library
`ciscoconfparse <http://www.pennington.net/py/ciscoconfparse/index.html>`_
is installed. See
:py:func:`ciscoconfparse module <salt.modules.ciscoconfparse_mod>` for
more details.
parent_regex
The regular expression to match the parent configuration lines against.
child_regex
The regular expression to match the child configuration lines against.
source: ``running``
The configuration type to retrieve from the network device. Default:
``running``. Available options: ``running``, ``startup``, ``candidate``.
CLI Example:
.. code-block:: bash
salt '*' napalm.config_filter_lines '^interface' 'ip address'
salt '*' napalm.config_filter_lines '^interface' 'shutdown' source=candidate
'''
config_txt = __salt__['net.config'](source=source)['out'][source]
return __salt__['ciscoconfparse.filter_lines'](config=config_txt,
parent_regex=parent_regex,
child_regex=child_regex)

View File

@ -54,6 +54,7 @@ INI_FILE = '/etc/natinst/share/ni-rt.ini'
_CONFIG_TRUE = ['yes', 'on', 'true', '1', True]
IFF_LOOPBACK = 0x8
IFF_RUNNING = 0x40
NIRTCFG_ETHERCAT = 'EtherCAT'
def _assume_condition(condition, err):
@ -258,25 +259,47 @@ def _remove_quotes(value):
return value
def _get_requestmode_info(interface):
def _load_config(section, options, default_value='', filename=INI_FILE):
'''
Get values for some options and a given section from a config file.
:param section: Section Name
:param options: List of options
:param default_value: Default value if an option doesn't have a value. Default is empty string.
:param filename: config file. Default is INI_FILE.
:return:
'''
results = {}
if not options:
return results
with salt.utils.files.fopen(filename, 'r') as config_file:
config_parser = configparser.RawConfigParser(dict_type=CaseInsensitiveDict)
config_parser.readfp(config_file)
for option in options:
if six.PY2:
results[option] = _remove_quotes(config_parser.get(section, option)) \
if config_parser.has_option(section, option) else default_value
else:
results[option] = _remove_quotes(config_parser.get(section, option, fallback=default_value))
return results
def _get_request_mode_info(interface):
'''
return requestmode for given interface
'''
with salt.utils.files.fopen(INI_FILE, 'r') as config_file:
config_parser = configparser.RawConfigParser(dict_type=CaseInsensitiveDict)
config_parser.read_file(config_file)
link_local_enabled = '' if not config_parser.has_option(interface, 'linklocalenabled') else \
int(_remove_quotes(config_parser.get(interface, 'linklocalenabled')))
dhcp_enabled = '' if not config_parser.has_option(interface, 'dhcpenabled') else \
int(_remove_quotes(config_parser.get(interface, 'dhcpenabled')))
settings = _load_config(interface, ['linklocalenabled', 'dhcpenabled'], -1)
link_local_enabled = int(settings['linklocalenabled'])
dhcp_enabled = int(settings['dhcpenabled'])
if dhcp_enabled == 1:
return 'dhcp_linklocal' if link_local_enabled == 1 else 'dhcp_only'
else:
if link_local_enabled == 1:
return 'linklocal_only'
if link_local_enabled == 0:
return 'static'
if dhcp_enabled == 1:
return 'dhcp_linklocal' if link_local_enabled == 1 else 'dhcp_only'
else:
if link_local_enabled == 1:
return 'linklocal_only'
if link_local_enabled == 0:
return 'static'
# some versions of nirtcfg don't set the dhcpenabled/linklocalenabled variables
# when selecting "DHCP or Link Local" from MAX, so return it by default to avoid
@ -284,19 +307,54 @@ def _get_requestmode_info(interface):
return 'dhcp_linklocal'
def _get_adapter_mode_info(interface):
'''
return adaptermode for given interface
'''
mode = _load_config(interface, ['mode'])['mode'].lower()
return mode if mode in ['disabled', 'ethercat'] else 'tcpip'
def _get_possible_adapter_modes(interface, blacklist):
'''
Return possible adapter modes for a given interface using a blacklist.
:param interface: interface name
:param blacklist: given blacklist
:return: list of possible adapter modes
'''
adapter_modes = []
protocols = _load_config('lvrt', ['AdditionalNetworkProtocols'])['AdditionalNetworkProtocols'].lower()
sys_interface_path = os.readlink('/sys/class/net/{0}'.format(interface))
with salt.utils.files.fopen('/sys/class/net/{0}/uevent'.format(interface)) as uevent_file:
uevent_lines = uevent_file.readlines()
uevent_devtype = ""
for line in uevent_lines:
if line.startswith("DEVTYPE="):
uevent_devtype = line.split('=')[1].strip()
break
for adapter_mode in blacklist:
if adapter_mode == '_':
continue
value = blacklist.get(adapter_mode, {})
if value.get('additional_protocol') and adapter_mode not in protocols:
continue
if interface not in value['name'] \
and not any((blacklist['_'][iface_type] == 'sys' and iface_type in sys_interface_path) or
(blacklist['_'][iface_type] == 'uevent' and iface_type == uevent_devtype)
for iface_type in value['type']):
adapter_modes += [adapter_mode]
return adapter_modes
def _get_static_info(interface):
'''
Return information about an interface from config file.
:param interface: interface label
'''
parser = configparser.ConfigParser()
if os.path.exists(INTERFACES_CONFIG):
try:
with salt.utils.files.fopen(INTERFACES_CONFIG, 'r') as config_file:
parser.read_file(config_file)
except configparser.MissingSectionHeaderError:
pass
data = {
'connectionid': interface.name,
'label': interface.name,
@ -309,12 +367,14 @@ def _get_static_info(interface):
'wireless': False
}
hwaddr_section_number = ''.join(data['hwaddr'].split(':'))
if parser.has_section('interface_{0}'.format(hwaddr_section_number)):
ipv4_information = parser.get('interface_{0}'.format(hwaddr_section_number), 'IPv4').split('/')
data['ipv4']['address'] = ipv4_information[0]
data['ipv4']['dns'] = parser.get('interface_{0}'.format(hwaddr_section_number), 'Nameservers').split(',')
data['ipv4']['netmask'] = ipv4_information[1]
data['ipv4']['gateway'] = ipv4_information[2]
if os.path.exists(INTERFACES_CONFIG):
information = _load_config(hwaddr_section_number, ['IPv4', 'Nameservers'], filename=INTERFACES_CONFIG)
if information['IPv4'] != '':
ipv4_information = information['IPv4'].split('/')
data['ipv4']['address'] = ipv4_information[0]
data['ipv4']['dns'] = information['Nameservers'].split(',')
data['ipv4']['netmask'] = ipv4_information[1]
data['ipv4']['gateway'] = ipv4_information[2]
return data
@ -322,16 +382,46 @@ def _get_interface_info(interface):
'''
return details about given interface
'''
blacklist = {
'tcpip': {
'name': [],
'type': [],
'additional_protocol': False
},
'disabled': {
'name': ['eth0'],
'type': ['gadget'],
'additional_protocol': False
},
'ethercat': {
'name': ['eth0'],
'type': ['gadget', 'usb', 'wlan'],
'additional_protocol': True
},
'_': {
'usb': 'sys',
'gadget': 'uevent',
'wlan': 'uevent'
}
}
data = {
'label': interface.name,
'connectionid': interface.name,
'supported_adapter_modes': _get_possible_adapter_modes(interface.name, blacklist),
'adapter_mode': _get_adapter_mode_info(interface.name),
'up': False,
'ipv4': {
'supportedrequestmodes': ['dhcp_linklocal', 'dhcp_only', 'linklocal_only', 'static'],
'requestmode': _get_requestmode_info(interface.name)
'requestmode': _get_request_mode_info(interface.name)
},
'hwaddr': interface.hwaddr[:-1]
}
needed_settings = []
if data['ipv4']['requestmode'] == 'static':
needed_settings += ['IP_Address', 'Subnet_Mask', 'Gateway', 'DNS_Address']
if data['adapter_mode'] == 'ethercat':
needed_settings += ['MasterID']
settings = _load_config(interface.name, needed_settings)
if interface.flags & IFF_RUNNING != 0:
data['up'] = True
data['ipv4']['address'] = interface.sockaddrToStr(interface.addr)
@ -339,13 +429,10 @@ def _get_interface_info(interface):
data['ipv4']['gateway'] = '0.0.0.0'
data['ipv4']['dns'] = _get_dns_info()
elif data['ipv4']['requestmode'] == 'static':
with salt.utils.files.fopen(INI_FILE, 'r') as config_file:
config_parser = configparser.RawConfigParser(dict_type=CaseInsensitiveDict)
config_parser.read_file(config_file)
data['ipv4']['address'] = _remove_quotes(config_parser.get(interface.name, 'IP_Address'))
data['ipv4']['netmask'] = _remove_quotes(config_parser.get(interface.name, 'Subnet_Mask'))
data['ipv4']['gateway'] = _remove_quotes(config_parser.get(interface.name, 'Gateway'))
data['ipv4']['dns'] = [_remove_quotes(config_parser.get(interface.name, 'DNS_Address'))]
data['ipv4']['address'] = settings['IP_Address']
data['ipv4']['netmask'] = settings['Subnet_Mask']
data['ipv4']['gateway'] = settings['Gateway']
data['ipv4']['dns'] = [settings['DNS_Address']]
with salt.utils.files.fopen('/proc/net/route', 'r') as route_file:
pattern = re.compile(r'^{interface}\t[0]{{8}}\t([0-9A-Z]{{8}})'.format(interface=interface.name), re.MULTILINE)
@ -353,6 +440,10 @@ def _get_interface_info(interface):
iface_gateway_hex = None if not match else match.group(1)
if iface_gateway_hex is not None and len(iface_gateway_hex) == 8:
data['ipv4']['gateway'] = '.'.join([str(int(iface_gateway_hex[i:i+2], 16)) for i in range(6, -1, -2)])
if data['adapter_mode'] == 'ethercat':
data['ethercat'] = {
'masterid': settings['MasterID']
}
return data
@ -404,10 +495,49 @@ def get_interfaces_details():
return {'interfaces': list(map(_get_info, _interfaces))}
def _change_state(interface, new_state):
'''
Enable or disable an interface
Change adapter mode to TCP/IP. If previous adapter mode was EtherCAT, the target will need reboot.
:param interface: interface label
:param new_state: up or down
:return: True if the service was enabled, otherwise an exception will be thrown.
:rtype: bool
'''
if __grains__['lsb_distrib_id'] == 'nilrt':
initial_mode = _get_adapter_mode_info(interface)
_save_config(interface, 'Mode', 'TCPIP')
if initial_mode == 'ethercat':
__salt__['system.set_reboot_required_witnessed']()
else:
out = __salt__['cmd.run_all']('ip link set {0} {1}'.format(interface, new_state))
if out['retcode'] != 0:
msg = 'Couldn\'t {0} interface {1}. Error: {2}'.format('enable' if new_state == 'up' else 'disable',
interface, out['stderr'])
raise salt.exceptions.CommandExecutionError(msg)
return True
service = _interface_to_service(interface)
if not service:
raise salt.exceptions.CommandExecutionError('Invalid interface name: {0}'.format(interface))
if not _connected(service):
service = pyconnman.ConnService(os.path.join(SERVICE_PATH, service))
try:
state = service.connect() if new_state == 'up' else service.disconnect()
return state is None
except Exception:
raise salt.exceptions.CommandExecutionError('Couldn\'t {0} service: {1}\n'
.format('enable' if new_state == 'up' else 'disable', service))
return True
def up(interface, iface_type=None): # pylint: disable=invalid-name,unused-argument
'''
Enable the specified interface
Change adapter mode to TCP/IP. If previous adapter mode was EtherCAT, the target will need reboot.
:param str interface: interface label
:return: True if the service was enabled, otherwise an exception will be thrown.
:rtype: bool
@ -418,29 +548,15 @@ def up(interface, iface_type=None): # pylint: disable=invalid-name,unused-argum
salt '*' ip.up interface-label
'''
if __grains__['lsb_distrib_id'] == 'nilrt':
out = __salt__['cmd.run_all']('ip link set {0} up'.format(interface))
if out['retcode'] != 0:
msg = 'Couldn\'t enable interface {0}. Error: {1}'.format(interface, out['stderr'])
raise salt.exceptions.CommandExecutionError(msg)
return True
service = _interface_to_service(interface)
if not service:
raise salt.exceptions.CommandExecutionError('Invalid interface name: {0}'.format(interface))
if not _connected(service):
service = pyconnman.ConnService(os.path.join(SERVICE_PATH, service))
try:
state = service.connect()
return state is None
except Exception:
raise salt.exceptions.CommandExecutionError('Couldn\'t enable service: {0}\n'.format(service))
return True
return _change_state(interface, 'up')
def enable(interface):
'''
Enable the specified interface
Change adapter mode to TCP/IP. If previous adapter mode was EtherCAT, the target will need reboot.
:param str interface: interface label
:return: True if the service was enabled, otherwise an exception will be thrown.
:rtype: bool
@ -458,6 +574,8 @@ def down(interface, iface_type=None): # pylint: disable=unused-argument
'''
Disable the specified interface
Change adapter mode to Disabled. If previous adapter mode was EtherCAT, the target will need reboot.
:param str interface: interface label
:return: True if the service was disabled, otherwise an exception will be thrown.
:rtype: bool
@ -468,29 +586,15 @@ def down(interface, iface_type=None): # pylint: disable=unused-argument
salt '*' ip.down interface-label
'''
if __grains__['lsb_distrib_id'] == 'nilrt':
out = __salt__['cmd.run_all']('ip link set {0} down'.format(interface))
if out['retcode'] != 0:
msg = 'Couldn\'t disable interface {0}. Error: {1}'.format(interface, out['stderr'])
raise salt.exceptions.CommandExecutionError(msg)
return True
service = _interface_to_service(interface)
if not service:
raise salt.exceptions.CommandExecutionError('Invalid interface name: {0}'.format(interface))
if _connected(service):
service = pyconnman.ConnService(os.path.join(SERVICE_PATH, service))
try:
state = service.disconnect()
return state is None
except Exception:
raise salt.exceptions.CommandExecutionError('Couldn\'t disable service: {0}\n'.format(service))
return True
return _change_state(interface, 'down')
def disable(interface):
'''
Disable the specified interface
Change adapter mode to Disabled. If previous adapter mode was EtherCAT, the target will need reboot.
:param str interface: interface label
:return: True if the service was disabled, otherwise an exception will be thrown.
:rtype: bool
@ -504,7 +608,7 @@ def disable(interface):
return down(interface)
def _persist_config(section, token, value):
def _save_config(section, token, value):
'''
Helper function to persist a configuration in the ini file
'''
@ -515,12 +619,47 @@ def _persist_config(section, token, value):
raise salt.exceptions.CommandExecutionError(exc_msg)
def set_ethercat(interface, master_id):
'''
Configure specified adapter to use EtherCAT adapter mode. If successful, the target will need reboot if it doesn't
already use EtherCAT adapter mode, otherwise will return true.
:param interface: interface label
:param master_id: EtherCAT Master ID
:return: True if the settings were applied, otherwise an exception will be thrown.
CLI Example:
.. code-block:: bash
salt '*' ip.set_ethercat interface-label master-id
'''
if __grains__['lsb_distrib_id'] == 'nilrt':
initial_mode = _get_adapter_mode_info(interface)
_save_config(interface, 'Mode', NIRTCFG_ETHERCAT)
_save_config(interface, 'MasterID', master_id)
if initial_mode != 'ethercat':
__salt__['system.set_reboot_required_witnessed']()
return True
raise salt.exceptions.CommandExecutionError('EtherCAT is not supported')
def _restart(interface):
'''
Disable and enable an interface
'''
disable(interface)
enable(interface)
def set_dhcp_linklocal_all(interface):
'''
Configure specified adapter to use DHCP with linklocal fallback
Change adapter mode to TCP/IP. If previous adapter mode was EtherCAT, the target will need reboot.
:param str interface: interface label
:return: True if the settings ware applied, otherwise an exception will be thrown.
:return: True if the settings were applied, otherwise an exception will be thrown.
:rtype: bool
CLI Example:
@ -530,10 +669,14 @@ def set_dhcp_linklocal_all(interface):
salt '*' ip.set_dhcp_linklocal_all interface-label
'''
if __grains__['lsb_distrib_id'] == 'nilrt':
_persist_config(interface, 'dhcpenabled', '1')
_persist_config(interface, 'linklocalenabled', '1')
disable(interface)
enable(interface)
initial_mode = _get_adapter_mode_info(interface)
_save_config(interface, 'Mode', 'TCPIP')
_save_config(interface, 'dhcpenabled', '1')
_save_config(interface, 'linklocalenabled', '1')
if initial_mode == 'ethercat':
__salt__['system.set_reboot_required_witnessed']()
else:
_restart(interface)
return True
service = _interface_to_service(interface)
if not service:
@ -557,8 +700,10 @@ def set_dhcp_only_all(interface):
'''
Configure specified adapter to use DHCP only
Change adapter mode to TCP/IP. If previous adapter mode was EtherCAT, the target will need reboot.
:param str interface: interface label
:return: True if the settings ware applied, otherwise an exception will be thrown.
:return: True if the settings were applied, otherwise an exception will be thrown.
:rtype: bool
CLI Example:
@ -569,10 +714,14 @@ def set_dhcp_only_all(interface):
'''
if not __grains__['lsb_distrib_id'] == 'nilrt':
raise salt.exceptions.CommandExecutionError('Not supported in this version')
_persist_config(interface, 'dhcpenabled', '1')
_persist_config(interface, 'linklocalenabled', '0')
disable(interface)
enable(interface)
initial_mode = _get_adapter_mode_info(interface)
_save_config(interface, 'Mode', 'TCPIP')
_save_config(interface, 'dhcpenabled', '1')
_save_config(interface, 'linklocalenabled', '0')
if initial_mode == 'ethercat':
__salt__['system.set_reboot_required_witnessed']()
else:
_restart(interface)
return True
@ -580,8 +729,10 @@ def set_linklocal_only_all(interface):
'''
Configure specified adapter to use linklocal only
Change adapter mode to TCP/IP. If previous adapter mode was EtherCAT, the target will need reboot.
:param str interface: interface label
:return: True if the settings ware applied, otherwise an exception will be thrown.
:return: True if the settings were applied, otherwise an exception will be thrown.
:rtype: bool
CLI Example:
@ -592,10 +743,14 @@ def set_linklocal_only_all(interface):
'''
if not __grains__['lsb_distrib_id'] == 'nilrt':
raise salt.exceptions.CommandExecutionError('Not supported in this version')
_persist_config(interface, 'dhcpenabled', '0')
_persist_config(interface, 'linklocalenabled', '1')
disable(interface)
enable(interface)
initial_mode = _get_adapter_mode_info(interface)
_save_config(interface, 'Mode', 'TCPIP')
_save_config(interface, 'dhcpenabled', '0')
_save_config(interface, 'linklocalenabled', '1')
if initial_mode == 'ethercat':
__salt__['system.set_reboot_required_witnessed']()
else:
_restart(interface)
return True
@ -618,7 +773,7 @@ def _configure_static_interface(interface, **settings):
if os.path.exists(INTERFACES_CONFIG):
try:
with salt.utils.files.fopen(INTERFACES_CONFIG, 'r') as config_file:
parser.read_file(config_file)
parser.readfp(config_file)
except configparser.MissingSectionHeaderError:
pass
hwaddr = interface.hwaddr[:-1]
@ -645,6 +800,8 @@ def set_static_all(interface, address, netmask, gateway, nameservers):
'''
Configure specified adapter to use ipv4 manual settings
Change adapter mode to TCP/IP. If previous adapter mode was EtherCAT, the target will need reboot.
:param str interface: interface label
:param str address: ipv4 address
:param str netmask: ipv4 netmask
@ -668,15 +825,19 @@ def set_static_all(interface, address, netmask, gateway, nameservers):
if not isinstance(nameservers, list):
nameservers = nameservers.split(' ')
if __grains__['lsb_distrib_id'] == 'nilrt':
_persist_config(interface, 'dhcpenabled', '0')
_persist_config(interface, 'linklocalenabled', '0')
_persist_config(interface, 'IP_Address', address)
_persist_config(interface, 'Subnet_Mask', netmask)
_persist_config(interface, 'Gateway', gateway)
initial_mode = _get_adapter_mode_info(interface)
_save_config(interface, 'Mode', 'TCPIP')
_save_config(interface, 'dhcpenabled', '0')
_save_config(interface, 'linklocalenabled', '0')
_save_config(interface, 'IP_Address', address)
_save_config(interface, 'Subnet_Mask', netmask)
_save_config(interface, 'Gateway', gateway)
if nameservers:
_persist_config(interface, 'DNS_Address', nameservers[0])
disable(interface)
enable(interface)
_save_config(interface, 'DNS_Address', nameservers[0])
if initial_mode == 'ethercat':
__salt__['system.set_reboot_required_witnessed']()
else:
_restart(interface)
return True
service = _interface_to_service(interface)
if not service:

View File

@ -1301,6 +1301,10 @@ def sls(mods, test=None, exclude=None, queue=False, sync_mods=None, **kwargs):
high_ = serial.load(fp_)
return st_.state.call_high(high_, orchestration_jid)
# If the state file is an integer, convert to a string then to unicode
if isinstance(mods, six.integer_types):
mods = salt.utils.stringutils.to_unicode(str(mods)) # future lint: disable=blacklisted-function
mods = salt.utils.args.split_input(mods)
st_.push_active()

View File

@ -664,6 +664,7 @@ def latest(name,
identity = [identity]
elif not isinstance(identity, list):
return _fail(ret, 'identity must be either a list or a string')
identity = [os.path.expanduser(x) for x in identity]
for ident_path in identity:
if 'salt://' in ident_path:
try:
@ -2315,6 +2316,7 @@ def detached(name,
identity = [identity]
elif not isinstance(identity, list):
return _fail(ret, 'Identity must be either a list or a string')
identity = [os.path.expanduser(x) for x in identity]
for ident_path in identity:
if 'salt://' in ident_path:
try:
@ -2701,7 +2703,7 @@ def cloned(name,
https_pass=None,
output_encoding=None):
'''
.. versionadded:: 2018.3.3, Fluorine
.. versionadded:: 2018.3.3,Fluorine
Ensure that a repository has been cloned to the specified target directory.
If not, clone that repository. No fetches will be performed once cloned.

View File

@ -1224,8 +1224,8 @@ def deploy_windows(host,
if not use_winrm and has_winexe() and not HAS_PSEXEC:
salt.utils.versions.warn_until(
'Fluorine',
'Support for winexe has been depricated and will be remove in '
'Sodium',
'Support for winexe has been deprecated and will be removed in '
'Sodium, please install pypsexec instead.'
)
@ -2848,22 +2848,6 @@ def list_cache_nodes_full(opts=None, provider=None, base=None):
return minions
def cache_nodes_ip(opts, base=None):
'''
Retrieve a list of all nodes from Salt Cloud cache, and any associated IP
addresses. Returns a dict.
'''
salt.utils.versions.warn_until(
'Fluorine',
'This function is incomplete and non-functional '
'and will be removed in Salt Fluorine.'
)
if base is None:
base = opts['cachedir']
minions = list_cache_nodes_full(opts, base=base)
def update_bootstrap(config, url=None):
'''
Update the salt-bootstrap script

View File

@ -488,7 +488,8 @@ def query(url,
data = _urlencode(data)
if verify_ssl:
req_kwargs['ca_certs'] = ca_bundle
# tornado requires a str, cannot be unicode str in py2
req_kwargs['ca_certs'] = salt.utils.stringutils.to_str(ca_bundle)
max_body = opts.get('http_max_body', salt.config.DEFAULT_MINION_OPTS['http_max_body'])
connect_timeout = opts.get('http_connect_timeout', salt.config.DEFAULT_MINION_OPTS['http_connect_timeout'])
@ -497,9 +498,18 @@ def query(url,
client_argspec = None
proxy_host = opts.get('proxy_host', None)
if proxy_host:
# tornado requires a str for proxy_host, cannot be a unicode str in py2
proxy_host = salt.utils.stringutils.to_str(proxy_host)
proxy_port = opts.get('proxy_port', None)
proxy_username = opts.get('proxy_username', None)
if proxy_username:
# tornado requires a str, cannot be unicode str in py2
proxy_username = salt.utils.stringutils.to_str(proxy_username)
proxy_password = opts.get('proxy_password', None)
if proxy_password:
# tornado requires a str, cannot be unicode str in py2
proxy_password = salt.utils.stringutils.to_str(proxy_password)
no_proxy = opts.get('no_proxy', [])
# Since tornado doesnt support no_proxy, we'll always hand it empty proxies or valid ones

View File

@ -188,7 +188,7 @@ def set_pidfile(pidfile, user):
uid = pwnam[2]
gid = pwnam[3]
#groups = [g.gr_gid for g in grp.getgrall() if user in g.gr_mem]
except IndexError:
except (KeyError, IndexError):
sys.stderr.write(
'Failed to set the pid to user: {0}. The user is not '
'available.\n'.format(

View File

@ -179,7 +179,7 @@ def get_conn(host='', username=None, password=None, port=445):
'''
if HAS_IMPACKET and not HAS_SMBPROTOCOL:
salt.utils.versions.warn_until(
'Fluorine',
'Sodium',
'Support of impacket has been depricated and will be '
'removed in Sodium. Please install smbprotocol instead.'
)

View File

@ -0,0 +1,2 @@
always-passes:
test.succeed_without_changes

View File

@ -2016,3 +2016,19 @@ class StateModuleTest(ModuleCase, SaltReturnAssertsMixin):
state_file = os.path.join(TMP, 'test.txt')
if os.path.isfile(state_file):
os.remove(state_file)
def test_state_sls_integer_name(self):
'''
This tests the case where the state file is named
only with integers
'''
state_run = self.run_function(
'state.sls',
mods='12345'
)
state_id = 'test_|-always-passes_|-always-passes_|-succeed_without_changes'
self.assertIn(state_id, state_run)
self.assertEqual(state_run[state_id]['comment'],
'Success!')
self.assertTrue(state_run[state_id]['result'])

View File

@ -22,9 +22,11 @@ import tests.integration.utils
from tests.support.case import ShellCase
from tests.support.paths import TMP
from tests.support.mixins import ShellCaseCommonTestsMixin
from tests.support.unit import skipIf
from tests.integration.utils import testprogram
@skipIf(True, 'This test file should be in an isolated test space.')
class MasterTest(ShellCase, testprogram.TestProgramCase, ShellCaseCommonTestsMixin):
_call_binary_ = 'salt-master'

View File

@ -710,8 +710,8 @@ class FileTest(ModuleCase, SaltReturnAssertsMixin):
This is a regression test for Issue #38914 and Issue #48230 (test=true use).
'''
name = os.path.join(TMP, 'source_hash_indifferent_case')
state_name = 'file_|-/tmp/salt-tests-tmpdir/source_hash_indifferent_case_|' \
'-/tmp/salt-tests-tmpdir/source_hash_indifferent_case_|-managed'
state_name = 'file_|-{0}_|' \
'-{0}_|-managed'.format(name)
local_path = os.path.join(FILES, 'file', 'base', 'hello_world.txt')
actual_hash = 'c98c24b677eff44860afea6f493bbaec5bb1c4cbb209c6fc2bbb47f66ff2ad31'
uppercase_hash = actual_hash.upper()

View File

@ -42,9 +42,19 @@ class ServiceTest(ModuleCase, SaltReturnAssertsMixin):
self.stopped = ''
self.running = '[0-9]'
self.pre_srv_enabled = True if self.service_name in self.run_function('service.get_enabled') else False
self.post_srv_disable = False
if not self.pre_srv_enabled:
self.run_function('service.enable', name=self.service_name)
self.post_srv_disable = True
if salt.utils.path.which(cmd_name) is None:
self.skipTest('{0} is not installed'.format(cmd_name))
def tearDown(self):
if self.post_srv_disable:
self.run_function('service.disable', name=self.service_name)
def check_service_status(self, exp_return):
'''
helper method to check status of service

View File

@ -0,0 +1,189 @@
# coding: utf-8
# Python libs
from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
import os
import shutil
import tempfile
import time
# Salt libs
import salt.utils.files
from salt.beacons import watchdog
from salt.ext.six.moves import range
# Salt testing libs
from tests.support.unit import skipIf, TestCase
from tests.support.mixins import LoaderModuleMockMixin
def check_events(config):
total_delay = 1
delay_per_loop = 20e-3
for _ in range(int(total_delay / delay_per_loop)):
events = watchdog.beacon(config)
if events:
return events
time.sleep(delay_per_loop)
return []
def create(path, content=None):
with salt.utils.files.fopen(path, 'w') as f:
if content:
f.write(content)
os.fsync(f)
@skipIf(not watchdog.HAS_WATCHDOG, 'watchdog is not available')
class IWatchdogBeaconTestCase(TestCase, LoaderModuleMockMixin):
'''
Test case for salt.beacons.watchdog
'''
def setup_loader_modules(self):
return {watchdog: {}}
def setUp(self):
self.tmpdir = tempfile.mkdtemp()
def tearDown(self):
watchdog.close({})
shutil.rmtree(self.tmpdir, ignore_errors=True)
def assertValid(self, config):
ret = watchdog.validate(config)
self.assertEqual(ret, (True, 'Valid beacon configuration'))
def test_empty_config(self):
config = [{}]
ret = watchdog.beacon(config)
self.assertEqual(ret, [])
def test_file_create(self):
path = os.path.join(self.tmpdir, 'tmpfile')
config = [{'directories': {self.tmpdir: {'mask': ['create']}}}]
self.assertValid(config)
self.assertEqual(watchdog.beacon(config), [])
create(path)
ret = check_events(config)
self.assertEqual(len(ret), 1)
self.assertEqual(ret[0]['path'], path)
self.assertEqual(ret[0]['change'], 'created')
def test_file_modified(self):
path = os.path.join(self.tmpdir, 'tmpfile')
config = [{'directories': {self.tmpdir: {'mask': ['modify']}}}]
self.assertValid(config)
self.assertEqual(watchdog.beacon(config), [])
create(path, 'some content')
ret = check_events(config)
self.assertEqual(len(ret), 2)
self.assertEqual(ret[0]['path'], os.path.dirname(path))
self.assertEqual(ret[0]['change'], 'modified')
self.assertEqual(ret[1]['path'], path)
self.assertEqual(ret[1]['change'], 'modified')
def test_file_deleted(self):
path = os.path.join(self.tmpdir, 'tmpfile')
create(path)
config = [{'directories': {self.tmpdir: {'mask': ['delete']}}}]
self.assertValid(config)
self.assertEqual(watchdog.beacon(config), [])
os.remove(path)
ret = check_events(config)
self.assertEqual(len(ret), 1)
self.assertEqual(ret[0]['path'], path)
self.assertEqual(ret[0]['change'], 'deleted')
def test_file_moved(self):
path = os.path.join(self.tmpdir, 'tmpfile')
create(path)
config = [{'directories': {self.tmpdir: {'mask': ['move']}}}]
self.assertValid(config)
self.assertEqual(watchdog.beacon(config), [])
os.rename(path, path + '_moved')
ret = check_events(config)
self.assertEqual(len(ret), 1)
self.assertEqual(ret[0]['path'], path)
self.assertEqual(ret[0]['change'], 'moved')
def test_file_create_in_directory(self):
config = [{'directories': {self.tmpdir: {'mask': ['create', 'modify']}}}]
self.assertValid(config)
self.assertEqual(watchdog.beacon(config), [])
path = os.path.join(self.tmpdir, 'tmpfile')
create(path)
ret = check_events(config)
self.assertEqual(len(ret), 2)
self.assertEqual(ret[0]['path'], path)
self.assertEqual(ret[0]['change'], 'created')
self.assertEqual(ret[1]['path'], self.tmpdir)
self.assertEqual(ret[1]['change'], 'modified')
def test_trigger_all_possible_events(self):
path = os.path.join(self.tmpdir, 'tmpfile')
moved = path + '_moved'
config = [{'directories': {
self.tmpdir: {},
}}]
self.assertValid(config)
self.assertEqual(watchdog.beacon(config), [])
# create
create(path)
# modify
create(path, 'modified content')
# move
os.rename(path, moved)
# delete
os.remove(moved)
ret = check_events(config)
self.assertEqual(len(ret), 8)
# create
self.assertEqual(ret[0]['path'], path)
self.assertEqual(ret[0]['change'], 'created')
self.assertEqual(ret[1]['path'], self.tmpdir)
self.assertEqual(ret[1]['change'], 'modified')
# modify
self.assertEqual(ret[2]['path'], path)
self.assertEqual(ret[2]['change'], 'modified')
self.assertEqual(ret[3]['path'], path)
self.assertEqual(ret[3]['change'], 'modified')
# move
self.assertEqual(ret[4]['path'], path)
self.assertEqual(ret[4]['change'], 'moved')
self.assertEqual(ret[5]['path'], self.tmpdir)
self.assertEqual(ret[5]['change'], 'modified')
# delete
self.assertEqual(ret[6]['path'], moved)
self.assertEqual(ret[6]['change'], 'deleted')
self.assertEqual(ret[7]['path'], self.tmpdir)
self.assertEqual(ret[7]['change'], 'modified')

View File

@ -1098,7 +1098,7 @@ class FilemodLineTests(TestCase, LoaderModuleMockMixin):
else:
return salt.utils.data.decode_list(ret, to_str=True)
@patch('os.path.realpath', MagicMock())
@patch('os.path.realpath', MagicMock(wraps=lambda x: x))
@patch('os.path.isfile', MagicMock(return_value=True))
def test_delete_line_in_empty_file(self):
'''
@ -1140,7 +1140,7 @@ class FilemodLineTests(TestCase, LoaderModuleMockMixin):
with patch('salt.utils.atomicfile.atomic_open', atomic_opener):
self.assertFalse(filemod.line('foo', content='foo', match=match, mode=mode))
@patch('os.path.realpath', MagicMock())
@patch('os.path.realpath', MagicMock(wraps=lambda x: x))
@patch('os.path.isfile', MagicMock(return_value=True))
def test_line_modecheck_failure(self):
'''
@ -1153,7 +1153,7 @@ class FilemodLineTests(TestCase, LoaderModuleMockMixin):
filemod.line('foo', mode=mode)
self.assertIn(err_msg, six.text_type(cmd_err))
@patch('os.path.realpath', MagicMock())
@patch('os.path.realpath', MagicMock(wraps=lambda x: x))
@patch('os.path.isfile', MagicMock(return_value=True))
def test_line_no_content(self):
'''
@ -1166,7 +1166,7 @@ class FilemodLineTests(TestCase, LoaderModuleMockMixin):
self.assertIn('Content can only be empty if mode is "delete"',
six.text_type(cmd_err))
@patch('os.path.realpath', MagicMock())
@patch('os.path.realpath', MagicMock(wraps=lambda x: x))
@patch('os.path.isfile', MagicMock(return_value=True))
@patch('os.stat', MagicMock())
def test_line_insert_no_location_no_before_no_after(self):
@ -1297,10 +1297,13 @@ class FilemodLineTests(TestCase, LoaderModuleMockMixin):
See issue #48113
:return:
'''
file_content = 'This is a line\nThis is another line'
file_modified = salt.utils.stringutils.to_str('This is a line\n'
'This is another line\n'
'This is a line with unicode Ŷ')
file_content = 'This is a line{}This is another line'.format(os.linesep)
file_modified = salt.utils.stringutils.to_str('This is a line{}'
'This is another line{}'
'This is a line with unicode Ŷ'.format(
os.linesep, os.linesep
)
)
cfg_content = "This is a line with unicode Ŷ"
isfile_mock = MagicMock(side_effect=lambda x: True if x == name else DEFAULT)
for after_line in ['This is another line']:
@ -1367,7 +1370,7 @@ class FilemodLineTests(TestCase, LoaderModuleMockMixin):
expected = self._get_body(file_modified)
assert writelines_content[0] == expected, (writelines_content[0], expected)
@patch('os.path.realpath', MagicMock())
@patch('os.path.realpath', MagicMock(wraps=lambda x: x))
@patch('os.path.isfile', MagicMock(return_value=True))
@patch('os.stat', MagicMock())
def test_line_assert_exception_pattern(self):
@ -1701,7 +1704,7 @@ class FilemodLineTests(TestCase, LoaderModuleMockMixin):
# No changes should have been made
assert result is False
@patch('os.path.realpath', MagicMock())
@patch('os.path.realpath', MagicMock(wraps=lambda x: x))
@patch('os.path.isfile', MagicMock(return_value=True))
@patch('os.stat', MagicMock())
def test_line_insert_ensure_beforeafter_rangelines(self):
@ -1711,8 +1714,10 @@ class FilemodLineTests(TestCase, LoaderModuleMockMixin):
'''
cfg_content = 'EXTRA_GROUPS="dialout cdrom floppy audio video plugdev users"'
# pylint: disable=W1401
file_content = 'NAME_REGEX="^[a-z][-a-z0-9_]*\$"\nSETGID_HOME=no\nADD_EXTRA_GROUPS=1\n' \
'SKEL_IGNORE_REGEX="dpkg-(old|new|dist|save)"'
file_content = 'NAME_REGEX="^[a-z][-a-z0-9_]*\$"{}SETGID_HOME=no{}ADD_EXTRA_GROUPS=1{}' \
'SKEL_IGNORE_REGEX="dpkg-(old|new|dist|save)"'.format(
os.linesep, os.linesep, os.linesep
)
# pylint: enable=W1401
after, before = file_content.split(os.linesep)[0], file_content.split(os.linesep)[-1]
for (_after, _before) in [(after, before), ('NAME_.*', 'SKEL_.*')]:

View File

@ -241,6 +241,18 @@ class WinServiceTestCase(TestCase, LoaderModuleMockMixin):
self.assertTrue(win_service.enabled('spongebob'))
self.assertFalse(win_service.enabled('squarepants'))
def test_enabled_with_space_in_name(self):
'''
Test to check to see if the named
service is enabled to start on boot
when have space in service name
'''
mock = MagicMock(side_effect=[{'StartType': 'Auto'},
{'StartType': 'Disabled'}])
with patch.object(win_service, 'info', mock):
self.assertTrue(win_service.enabled('spongebob test'))
self.assertFalse(win_service.enabled('squarepants test'))
def test_disabled(self):
'''
Test to check to see if the named