Mini Shell
"""
Network SNMP
============
Manage the SNMP configuration on network devices.
:codeauthor: Mircea Ulinic <ping@mirceaulinic.net>
:maturity: new
:depends: napalm
:platform: unix
Dependencies
------------
- :mod:`napalm snmp management module (salt.modules.napalm_snmp) <salt.modules.napalm_snmp>`
.. versionadded:: 2016.11.0
"""
import logging
import salt.utils.json
import salt.utils.napalm
log = logging.getLogger(__name__)
# ----------------------------------------------------------------------------------------------------------------------
# state properties
# ----------------------------------------------------------------------------------------------------------------------
__virtualname__ = "netsnmp"
_COMMUNITY_MODE_MAP = {
"read-only": "ro",
"readonly": "ro",
"read-write": "rw",
"write": "rw",
}
# ----------------------------------------------------------------------------------------------------------------------
# global variables
# ----------------------------------------------------------------------------------------------------------------------
# ----------------------------------------------------------------------------------------------------------------------
# property functions
# ----------------------------------------------------------------------------------------------------------------------
def __virtual__():
"""
NAPALM library must be installed for this module to work and run in a (proxy) minion.
"""
return salt.utils.napalm.virtual(__opts__, __virtualname__, __file__)
# ----------------------------------------------------------------------------------------------------------------------
# helper functions -- will not be exported
# ----------------------------------------------------------------------------------------------------------------------
def _ordered_dict_to_dict(config):
"""
Forced the datatype to dict, in case OrderedDict is used.
"""
return salt.utils.json.loads(salt.utils.json.dumps(config))
def _expand_config(config, defaults):
"""
Completed the values of the expected config for the edge cases with the default values.
"""
defaults.update(config)
return defaults
def _valid_dict(dic):
"""
Valid dictionary?
"""
return isinstance(dic, dict) and len(dic) > 0
def _valid_str(value):
"""
Valid str?
"""
return isinstance(value, str) and len(value) > 0
def _community_defaults():
"""
Returns the default values of a community.
"""
return {"mode": "ro"}
def _clear_community_details(community_details):
"""
Clears community details.
"""
for key in ["acl", "mode"]:
_str_elem(community_details, key)
_mode = community_details.get["mode"] = community_details.get("mode").lower()
if _mode in _COMMUNITY_MODE_MAP:
community_details["mode"] = _COMMUNITY_MODE_MAP.get(_mode)
if community_details["mode"] not in ["ro", "rw"]:
community_details["mode"] = "ro" # default is read-only
return community_details
def _str_elem(config, key):
"""
Re-adds the value of a specific key in the dict, only in case of valid str value.
"""
_value = config.pop(key, "")
if _valid_str(_value):
config[key] = _value
def _check_config(config):
"""
Checks the desired config and clears interesting details.
"""
if not _valid_dict(config):
return True, ""
_community = config.get("community")
_community_tmp = {}
if not _community:
return False, "Must specify at least a community."
if _valid_str(_community):
_community_tmp[_community] = _community_defaults()
elif isinstance(_community, list):
# if the user specifies the communities as list
for _comm in _community:
if _valid_str(_comm):
# list of values
_community_tmp[_comm] = _community_defaults()
# default mode is read-only
if _valid_dict(_comm):
# list of dicts
for _comm_name, _comm_details in _comm.items():
if _valid_str(_comm_name):
_community_tmp[_comm_name] = _clear_community_details(
_comm_details
)
elif _valid_dict(_community):
# directly as dict of communities
# recommended way...
for _comm_name, _comm_details in _community.items():
if _valid_str(_comm_name):
_community_tmp[_comm_name] = _clear_community_details(_comm_details)
else:
return False, "Please specify a community or a list of communities."
if not _valid_dict(_community_tmp):
return False, "Please specify at least a valid community!"
config["community"] = _community_tmp
for key in ["location", "contact", "chassis_id"]:
# not mandatory, but should be here only if valid
_str_elem(config, key)
return True, ""
def _retrieve_device_config():
"""
Retrieves the SNMP config from the device.
"""
return __salt__["snmp.config"]()
def _create_diff_action(diff, diff_key, key, value):
"""
DRY to build diff parts (added, removed, updated).
"""
if diff_key not in diff:
diff[diff_key] = {}
diff[diff_key][key] = value
def _create_diff(diff, fun, key, prev, curr):
"""
Builds the diff dictionary.
"""
if not fun(prev):
_create_diff_action(diff, "added", key, curr)
elif fun(prev) and not fun(curr):
_create_diff_action(diff, "removed", key, prev)
elif not fun(curr):
_create_diff_action(diff, "updated", key, curr)
def _compute_diff(existing, expected):
"""
Computes the differences between the existing and the expected SNMP config.
"""
diff = {}
for key in ["location", "contact", "chassis_id"]:
if existing.get(key) != expected.get(key):
_create_diff(diff, _valid_str, key, existing.get(key), expected.get(key))
for key in ["community"]: # for the moment only onen
if existing.get(key) != expected.get(key):
_create_diff(diff, _valid_dict, key, existing.get(key), expected.get(key))
return diff
def _configure(changes):
"""
Calls the configuration template to apply the configuration changes on the device.
"""
cfgred = True
reasons = []
fun = "update_config"
for key in ["added", "updated", "removed"]:
_updated_changes = changes.get(key, {})
if not _updated_changes:
continue
_location = _updated_changes.get("location", "")
_contact = _updated_changes.get("contact", "")
_community = _updated_changes.get("community", {})
_chassis_id = _updated_changes.get("chassis_id", "")
if key == "removed":
fun = "remove_config"
_ret = __salt__[f"snmp.{fun}"](
location=_location,
contact=_contact,
community=_community,
chassis_id=_chassis_id,
commit=False,
)
cfgred = cfgred and _ret.get("result")
if not _ret.get("result") and _ret.get("comment"):
reasons.append(_ret.get("comment"))
return {"result": cfgred, "comment": "\n".join(reasons) if reasons else ""}
# ----------------------------------------------------------------------------------------------------------------------
# callable functions
# ----------------------------------------------------------------------------------------------------------------------
def managed(name, config=None, defaults=None):
"""
Configures the SNMP on the device as specified in the SLS file.
SLS Example:
.. code-block:: yaml
snmp_example:
netsnmp.managed:
- config:
location: Honolulu, HI, US
- defaults:
contact: noc@cloudflare.com
Output example (for the SLS above, e.g. called snmp.sls under /router/):
.. code-block:: bash
$ sudo salt edge01.hnl01 state.sls router.snmp test=True
edge01.hnl01:
----------
ID: snmp_example
Function: snmp.managed
Result: None
Comment: Testing mode: configuration was not changed!
Started: 13:29:06.872363
Duration: 920.466 ms
Changes:
----------
added:
----------
chassis_id:
None
contact:
noc@cloudflare.com
location:
Honolulu, HI, US
Summary for edge01.hnl01
------------
Succeeded: 1 (unchanged=1, changed=1)
Failed: 0
------------
Total states run: 1
Total run time: 920.466 ms
"""
result = False
comment = ""
changes = {}
ret = {"name": name, "changes": changes, "result": result, "comment": comment}
# make sure we're working only with dict
config = _ordered_dict_to_dict(config)
defaults = _ordered_dict_to_dict(defaults)
expected_config = _expand_config(config, defaults)
if not isinstance(expected_config, dict):
ret["comment"] = "User provided an empty SNMP config!"
return ret
valid, message = _check_config(expected_config)
if not valid: # check and clean
ret["comment"] = "Please provide a valid configuration: {error}".format(
error=message
)
return ret
# ----- Retrieve existing users configuration and determine differences ------------------------------------------->
_device_config = _retrieve_device_config()
if not _device_config.get("result"):
ret["comment"] = "Cannot retrieve SNMP config from the device: {reason}".format(
reason=_device_config.get("comment")
)
return ret
device_config = _device_config.get("out", {})
if device_config == expected_config:
ret.update({"comment": "SNMP already configured as needed.", "result": True})
return ret
diff = _compute_diff(device_config, expected_config)
changes.update(diff)
ret.update({"changes": changes})
if __opts__["test"] is True:
ret.update(
{"result": None, "comment": "Testing mode: configuration was not changed!"}
)
return ret
# <---- Retrieve existing NTP peers and determine peers to be added/removed --------------------------------------->
# ----- Call _set_users and _delete_users as needed ------------------------------------------------------->
expected_config_change = False
result = True
if diff:
_configured = _configure(diff)
if _configured.get("result"):
expected_config_change = True
else: # something went wrong...
result = False
comment = (
"Cannot push new SNMP config: \n{reason}".format(
reason=_configured.get("comment")
)
+ comment
)
if expected_config_change:
result, comment = __salt__["net.config_control"]()
# <---- Call _set_users and _delete_users as needed --------------------------------------------------------
ret.update({"result": result, "comment": comment})
return ret
Zerion Mini Shell 1.0