Skip to content

Commit

Permalink
snmp WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
cpaillet committed Jun 24, 2024
1 parent d2768c2 commit 3c97adb
Show file tree
Hide file tree
Showing 11 changed files with 269 additions and 0 deletions.
111 changes: 111 additions & 0 deletions _states/ietf_snmp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""Module to maintain ietf:snmp.
:codeauthor: Criteo Network team
:maturity: new
:platform: SONiC, Arista EOS, Juniper JunOS
"""

import logging

from salt.exceptions import CommandExecutionError

log = logging.getLogger(__name__)


def __virtual__():
return _get_os() in ["eos", "junos", "sonic"]


##
# Some utils
##


def _get_os():
return __salt__["grains.get"]("nos", __salt__["grains.get"]("os"))


def _apply_template(template_name, context, saltenv):
"""Define a helper to generate config from template file."""
template_content = __salt__["cp.get_file_str"](template_name, saltenv=saltenv)
context["deep_get"] = __utils__["jinja_filters.deep_get"]

if not template_content:
raise CommandExecutionError("Unable to get {}".format(template_name))

result = __salt__["file.apply_template_on_contents"](
contents=template_content,
template="jinja",
context=context,
defaults=None,
saltenv=saltenv,
)

return "\n".join([line for line in result.splitlines() if line.strip() != ""])


def _generate_snmp_config(ietfconfig, _, saltenv):
# TODO: handle when no data
os = _get_os()
config = _apply_template(
"salt://states/afk/templates/snmp/{}/snmp.j2".format(os),
ietfconfig,
saltenv,
)

return config


def apply(name, ietfconfig=None, saltenv="base"):
"""Apply and maintain Routing Policies configuration from openconfig format (JSON is expected).
.. warning::
Be careful with dry run, in some conditions napalm apply the config instead of
discarding it.
Did not find the root cause yet.
:param name: name of the task
:param openconfig_routing_policy: Routing Policy configuration in JSON in openconfig
(routing-policy)
:param openconfig_bgp: BGP configuration in JSON in openconfig (bgp)
:param saltenv: salt environment
"""
ret = {"name": name, "result": False, "changes": {}, "comment": []}

# generate command to apply on the device using the templates
log.debug("%s starting", name)

# get candidate config
config = _generate_snmp_config(ietfconfig, False, saltenv)

nos = _get_os()

if nos in ["eos", "junos"]:
# only return generated commands/config during tests
# there is an ongoing bug with napalm making dry-run really applying the config sometimes
if __opts__["test"]:
ret["result"] = None
return ret

res = __salt__["net.load_config"](
text=config,
test=__opts__["test"],
debug=True,
)
elif nos == "sonic":
# TODO: modify .managed to support pushing raw config without template
res = __salt__["sonic.snmp"](
template_name="salt://templates/snmp.j2",
context={"raw": config},
push_only_if_changes=True,
test=__opts__["test"],
)
res["diff"] = res["changes"]

ret["comment"].append("- loaded:\n{}".format(config))
ret["comment"].append(res["comment"])
if res["diff"]:
ret["changes"] = {"diff": res["diff"]}
ret["result"] = res["result"]

return ret
9 changes: 9 additions & 0 deletions states/afk/templates/snmp/eos/snmp.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
snmp-server contact {{ system.contact }}
snmp-server location {{ system.location }}
{% for community in snmp.community %}
{% if community['security-name'] == "readonly" %}
snmp-server community {{ community['text-name'] }} ro
{% elif community['security-name'] == "readwrite" %}
snmp-server community {{ community['text-name'] }} rw
{% endif %}
{% endfor %}
15 changes: 15 additions & 0 deletions states/afk/templates/snmp/junos/snmp.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
snmp {
location "{{ system.location }}";
contact "{{ system.contact }}";
{% for community in snmp.community %}
{% if community['security-name'] == "readonly" %}
community {{ community['text-name'] }} {
authorization read-only;
}
{% elif community['security-name'] == "readwrite" %}
community {{ community['text-name'] }} {
authorization read-write;
}
{%- endif %}
{%- endfor %}
}
7 changes: 7 additions & 0 deletions states/afk/templates/snmp/sonic/snmp.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% for community in snmp.community %}
{% if community['security-name'] == "readonly" %}
snmp_rocommunity: {{ community['text-name'] }}
{% endif %}
{% endfor %}
snmp_location: {{ system.location }}
snmp_contact: {{ system.contact }}
3 changes: 3 additions & 0 deletions test_first.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""integration test of ietf_snmp for eos."""

import _states as STATE_MOD
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
snmp-server contact prod-network
snmp-server location DC1;01.00
snmp-server community communityro ro
snmp-server community communityrw rw
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
snmp {
location "DC1;01.00";
contact "prod-network";
community communityro {
authorization read-only;
}
community communityrw {
authorization read-write;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
snmp_rocommunity: communityro
snmp_location: DC1;01.00
snmp_contact: prod-network
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"snmp":{
"community":[
{
"index":"global_ro",
"security-name":"readonly",
"text-name":"communityro"
},
{
"index":"globalrw",
"security-name":"readwrite",
"text-name":"communityrw"
}
]
},
"system":{
"contact":"prod-network",
"location":"DC1;01.00"
}
}
Empty file.
87 changes: 87 additions & 0 deletions tests/states/ietf_snmp/integration_tests/test_ietf_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""integration test of ietf_snmp for eos."""

import functools
import json
import pytest

import _states.ietf_snmp as STATE_MOD
from _utils import frr_detect_diff, jinja_filters
from jinja2 import BaseLoader, Environment

##
# Tests setup
##


def _get_data_and_expected_result(os_name):
test_path = "tests/states/ietf_snmp/data/integration_tests/full_config"
with open(
f"{test_path}/ietf.json",
encoding="utf-8",
) as fd:
fake_data = json.load(fd)

with open(
f"{test_path}/expected_result_{os_name}.txt",
encoding="utf-8",
) as fd:
expected_result = fd.read()

return fake_data, expected_result


def _mock_apply_template_on_contents(contents, template, context, *_, **__):
assert template == "jinja"
loader = Environment(loader=BaseLoader)
template = loader.from_string(contents)
return template.render(**context)


def _mock_get_file_str(template_name, *_, **__):
# removing salt:// prefix in path file
template_name = template_name[7:]
with open(template_name, encoding="utf-8") as fd:
content = fd.read()
return content


def _apply_common_mock():
STATE_MOD.__salt__ = {
"file.apply_template_on_contents": _mock_apply_template_on_contents,
"cp.get_file_str": _mock_get_file_str,
"eos.get_bgp_config": lambda *_: (""),
}
STATE_MOD.__utils__ = {
"frr_detect_diff.get_objects": frr_detect_diff.get_objects,
"jinja_filters.format_route_policy_name": jinja_filters.format_route_policy_name,
"jinja_filters.deep_get": jinja_filters.deep_get,
}


def _mock_then_clean(func):
@functools.wraps(func)
def wrapper(mocker, *args, **kwargs):
# some mocking
_apply_common_mock()
try:
return func(mocker, *args, **kwargs)
finally:
# some cleaning
del STATE_MOD.__salt__

return wrapper


##
# Tests
##


@pytest.mark.parametrize("os", ["eos", "junos", "sonic"])
@_mock_then_clean
def test_apply__generate_routing_policy_config__full_config(mocker, os): # pylint: disable=W0613
"""Test the entire config generation with full and valid config for eos."""

mocker.patch("_states.ietf_snmp._get_os", return_value=os)
fake_data, expected_result = _get_data_and_expected_result(os)
assert STATE_MOD._generate_snmp_config(fake_data, None, saltenv="base") == expected_result

0 comments on commit 3c97adb

Please sign in to comment.