Mini Shell
"""
Watch NAPALM functions and fire events on specific triggers
===========================================================
.. versionadded:: 2018.3.0
.. note::
The ``NAPALM`` beacon only works only when running under
a regular Minion or a Proxy Minion, managed via NAPALM_.
Check the documentation for the
:mod:`NAPALM proxy module <salt.proxy.napalm>`.
.. _NAPALM: http://napalm.readthedocs.io/en/latest/index.html
The configuration accepts a list of Salt functions to be
invoked, and the corresponding output hierarchy that should
be matched against. To invoke a function with certain
arguments, they can be specified using the ``_args`` key, or
``_kwargs`` for more specific key-value arguments.
The match structure follows the output hierarchy of the NAPALM
functions, under the ``out`` key.
For example, the following is normal structure returned by the
:mod:`ntp.stats <salt.modules.napalm_ntp.stats>` execution function:
.. code-block:: json
{
"comment": "",
"result": true,
"out": [
{
"referenceid": ".GPSs.",
"remote": "172.17.17.1",
"synchronized": true,
"reachability": 377,
"offset": 0.461,
"when": "860",
"delay": 143.606,
"hostpoll": 1024,
"stratum": 1,
"jitter": 0.027,
"type": "-"
},
{
"referenceid": ".INIT.",
"remote": "172.17.17.2",
"synchronized": false,
"reachability": 0,
"offset": 0.0,
"when": "-",
"delay": 0.0,
"hostpoll": 1024,
"stratum": 16,
"jitter": 4000.0,
"type": "-"
}
]
}
In order to fire events when the synchronization is lost with
one of the NTP peers, e.g., ``172.17.17.2``, we can match it explicitly as:
.. code-block:: yaml
ntp.stats:
remote: 172.17.17.2
synchronized: false
There is one single nesting level, as the output of ``ntp.stats`` is
just a list of dictionaries, and this beacon will compare each dictionary
from the list with the structure examplified above.
.. note::
When we want to match on any element at a certain level, we can
configure ``*`` to match anything.
Considering a more complex structure consisting on multiple nested levels,
e.g., the output of the :mod:`bgp.neighbors <salt.modules.napalm_bgp.neighbors>`
execution function, to check when any neighbor from the ``global``
routing table is down, the match structure would have the format:
.. code-block:: yaml
bgp.neighbors:
global:
'*':
up: false
The match structure above will match any BGP neighbor, with
any network (``*`` matches any AS number), under the ``global`` VRF.
In other words, this beacon will push an event on the Salt bus
when there's a BGP neighbor down.
The right operand can also accept mathematical operations
(i.e., ``<``, ``<=``, ``!=``, ``>``, ``>=`` etc.) when comparing
numerical values.
Configuration Example:
.. code-block:: yaml
beacons:
napalm:
- net.interfaces:
# fire events when any interfaces is down
'*':
is_up: false
- net.interfaces:
# fire events only when the xe-0/0/0 interface is down
'xe-0/0/0':
is_up: false
- ntp.stats:
# fire when there's any NTP peer unsynchornized
synchronized: false
- ntp.stats:
# fire only when the synchronization
# with with the 172.17.17.2 NTP server is lost
_args:
- 172.17.17.2
synchronized: false
- ntp.stats:
# fire only when there's a NTP peer with
# synchronization stratum > 5
stratum: '> 5'
Event structure example:
.. code-block:: json
{
"_stamp": "2017-09-05T09:51:09.377202",
"args": [],
"data": {
"comment": "",
"out": [
{
"delay": 0.0,
"hostpoll": 1024,
"jitter": 4000.0,
"offset": 0.0,
"reachability": 0,
"referenceid": ".INIT.",
"remote": "172.17.17.1",
"stratum": 16,
"synchronized": false,
"type": "-",
"when": "-"
}
],
"result": true
},
"fun": "ntp.stats",
"id": "edge01.bjm01",
"kwargs": {},
"match": {
"stratum": "> 5"
}
}
The event examplified above has been fired when the device
identified by the Minion id ``edge01.bjm01`` has been synchronized
with a NTP server at a stratum level greater than 5.
"""
import logging
import re
import salt.utils.beacons
import salt.utils.napalm
log = logging.getLogger(__name__)
_numeric_regex = re.compile(r"^(<|>|<=|>=|==|!=)\s*(\d+(\.\d+){0,1})$")
# the numeric regex will match the right operand, e.g '>= 20', '< 100', '!= 20', '< 1000.12' etc.
_numeric_operand = {
"<": "__lt__",
">": "__gt__",
">=": "__ge__",
"<=": "__le__",
"==": "__eq__",
"!=": "__ne__",
} # mathematical operand - private method map
__virtualname__ = "napalm"
def __virtual__():
"""
This beacon can only work when running under a regular or a proxy minion, managed through napalm.
"""
if salt.utils.napalm.virtual(__opts__, __virtualname__, __file__):
return __virtualname__
else:
err_msg = "NAPALM is not installed."
log.error("Unable to load %s beacon: %s", __virtualname__, err_msg)
return False, err_msg
def _compare(cur_cmp, cur_struct):
"""
Compares two objects and return a boolean value
when there's a match.
"""
if isinstance(cur_cmp, dict) and isinstance(cur_struct, dict):
log.debug("Comparing dict to dict")
for cmp_key, cmp_value in cur_cmp.items():
if cmp_key == "*":
# matches any key from the source dictionary
if isinstance(cmp_value, dict):
found = False
for _, cur_struct_val in cur_struct.items():
found |= _compare(cmp_value, cur_struct_val)
return found
else:
found = False
if isinstance(cur_struct, (list, tuple)):
for cur_ele in cur_struct:
found |= _compare(cmp_value, cur_ele)
elif isinstance(cur_struct, dict):
for _, cur_ele in cur_struct.items():
found |= _compare(cmp_value, cur_ele)
return found
else:
if isinstance(cmp_value, dict):
if cmp_key not in cur_struct:
return False
return _compare(cmp_value, cur_struct[cmp_key])
if isinstance(cmp_value, list):
found = False
for _, cur_struct_val in cur_struct.items():
found |= _compare(cmp_value, cur_struct_val)
return found
else:
return _compare(cmp_value, cur_struct[cmp_key])
elif isinstance(cur_cmp, (list, tuple)) and isinstance(cur_struct, (list, tuple)):
log.debug("Comparing list to list")
found = False
for cur_cmp_ele in cur_cmp:
for cur_struct_ele in cur_struct:
found |= _compare(cur_cmp_ele, cur_struct_ele)
return found
elif isinstance(cur_cmp, dict) and isinstance(cur_struct, (list, tuple)):
log.debug("Comparing dict to list (of dicts?)")
found = False
for cur_struct_ele in cur_struct:
found |= _compare(cur_cmp, cur_struct_ele)
return found
elif isinstance(cur_cmp, bool) and isinstance(cur_struct, bool):
log.debug("Comparing booleans: %s ? %s", cur_cmp, cur_struct)
return cur_cmp == cur_struct
elif isinstance(cur_cmp, ((str,), str)) and isinstance(cur_struct, ((str,), str)):
log.debug("Comparing strings (and regex?): %s ? %s", cur_cmp, cur_struct)
# Trying literal match
matched = re.match(cur_cmp, cur_struct, re.I)
if matched:
return True
return False
elif isinstance(cur_cmp, ((int,), float)) and isinstance(
cur_struct, ((int,), float)
):
log.debug("Comparing numeric values: %d ? %d", cur_cmp, cur_struct)
# numeric compare
return cur_cmp == cur_struct
elif isinstance(cur_struct, ((int,), float)) and isinstance(cur_cmp, ((str,), str)):
# Comparing the numerical value against a presumably mathematical value
log.debug(
"Comparing a numeric value (%d) with a string (%s)", cur_struct, cur_cmp
)
numeric_compare = _numeric_regex.match(cur_cmp)
# determine if the value to compare against is a mathematical operand
if numeric_compare:
compare_value = numeric_compare.group(2)
return getattr(
float(cur_struct), _numeric_operand[numeric_compare.group(1)]
)(float(compare_value))
return False
return False
def validate(config):
"""
Validate the beacon configuration.
"""
# Must be a list of dicts.
if not isinstance(config, list):
return False, "Configuration for napalm beacon must be a list."
for mod in config:
fun, fun_cfg = next(iter(mod.items()))
if not isinstance(fun_cfg, dict):
return (
False,
"The match structure for the {} execution function output must be a"
" dictionary".format(fun),
)
if fun not in __salt__:
return False, f"Execution function {fun} is not availabe!"
return True, "Valid configuration for the napal beacon!"
def beacon(config):
"""
Watch napalm function and fire events.
"""
whitelist = []
config = salt.utils.beacons.remove_hidden_options(config, whitelist)
log.debug("Executing napalm beacon with config:")
log.debug(config)
ret = []
for mod in config:
if not mod:
continue
event = {}
fun, fun_cfg = next(iter(mod.items()))
args = fun_cfg.pop("_args", [])
kwargs = fun_cfg.pop("_kwargs", {})
log.debug("Executing %s with %s and %s", fun, args, kwargs)
fun_ret = __salt__[fun](*args, **kwargs)
log.debug("Got the reply from the minion:")
log.debug(fun_ret)
if not fun_ret.get("result", False):
log.error("Error whilst executing %s", fun)
log.error(fun_ret)
continue
fun_ret_out = fun_ret["out"]
log.debug("Comparing to:")
log.debug(fun_cfg)
try:
fun_cmp_result = _compare(fun_cfg, fun_ret_out)
except Exception as err: # pylint: disable=broad-except
log.error(err, exc_info=True)
# catch any exception and continue
# to not jeopardise the execution of the next function in the list
continue
log.debug("Result of comparison: %s", fun_cmp_result)
if fun_cmp_result:
log.info("Matched %s with %s", fun, fun_cfg)
event["tag"] = "{os}/{fun}".format(os=__grains__["os"], fun=fun)
event["fun"] = fun
event["args"] = args
event["kwargs"] = kwargs
event["data"] = fun_ret
event["match"] = fun_cfg
log.debug("Queueing event:")
log.debug(event)
ret.append(event)
log.debug("NAPALM beacon generated the events:")
log.debug(ret)
return ret
Zerion Mini Shell 1.0