diff --git a/_states/ietf_snmp.py b/_states/ietf_snmp.py new file mode 100644 index 0000000..e1a0bb1 --- /dev/null +++ b/_states/ietf_snmp.py @@ -0,0 +1,114 @@ +"""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__) + + +## +# Some utils +## + + +def __virtual__(): + return _get_os() in ["eos", "junos", "sonic"] + + +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(ietf, _, saltenv): + # TODO: handle when no data + os = _get_os() + config = _apply_template( + "salt://states/afk/templates/snmp/{}/snmp.j2".format(os), + ietf, + saltenv, + ) + + return config + + +def apply(name, ietf_config=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) + + if not ietf_config: + ret["comment"].append("No configuration provided") + return ret + + 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 + config = _generate_snmp_config(ietf_config, False, saltenv) + + if __opts__["test"]: + ret["result"] = None + return ret + + res = __salt__["net.load_config"]( + text=config, + test=__opts__["test"], + debug=True, + ) + ret["comment"].append("- loaded:\n{}".format(config)) + + elif nos == "sonic": + res = __salt__["sonic.snmp_config"]( + template_name="salt://states/afk/templates/snmp/{}/snmp.j2".format(nos), + saltenv=saltenv, + context=ietf_config, + test=__opts__["test"], + ) + res["diff"] = res["changes"] + + ret["comment"].append(res["comment"]) + if res["diff"]: + ret["changes"] = {"diff": res["diff"]} + ret["result"] = res["result"] + + return ret diff --git a/states/afk/init.sls b/states/afk/init.sls index 465382b..d6c1b35 100644 --- a/states/afk/init.sls +++ b/states/afk/init.sls @@ -11,6 +11,11 @@ bgp_sessions: - openconfig_routing_policy: route_policies - saltenv: {{ saltenv }} +snmp_config: + ietf_snmp.apply: + - ietf_config: {{ pillar["ietf"] | yaml }} + - saltenv: {{ saltenv }} + clear_bgp_soft: afk_bgp.clear_soft_all: - onchanges: diff --git a/states/afk/templates/snmp/eos/snmp.j2 b/states/afk/templates/snmp/eos/snmp.j2 new file mode 100644 index 0000000..ab4ef7c --- /dev/null +++ b/states/afk/templates/snmp/eos/snmp.j2 @@ -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 %} diff --git a/states/afk/templates/snmp/junos/snmp.j2 b/states/afk/templates/snmp/junos/snmp.j2 new file mode 100644 index 0000000..a943ace --- /dev/null +++ b/states/afk/templates/snmp/junos/snmp.j2 @@ -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 %} +} \ No newline at end of file diff --git a/states/afk/templates/snmp/sonic/snmp.j2 b/states/afk/templates/snmp/sonic/snmp.j2 new file mode 100644 index 0000000..9001ff4 --- /dev/null +++ b/states/afk/templates/snmp/sonic/snmp.j2 @@ -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 }} diff --git a/test_first.py b/test_first.py new file mode 100644 index 0000000..60744a0 --- /dev/null +++ b/test_first.py @@ -0,0 +1,3 @@ +"""integration test of ietf_snmp for eos.""" + +import _states as STATE_MOD diff --git a/tests/states/ietf_snmp/data/integration_tests/full_config/expected_result_eos.txt b/tests/states/ietf_snmp/data/integration_tests/full_config/expected_result_eos.txt new file mode 100644 index 0000000..cff2cb1 --- /dev/null +++ b/tests/states/ietf_snmp/data/integration_tests/full_config/expected_result_eos.txt @@ -0,0 +1,5 @@ +snmp-server contact prod-network +snmp-server location DC1;01.00 +snmp-server community communityro ro +snmp-server community communityro2 ro +snmp-server community communityrw rw \ No newline at end of file diff --git a/tests/states/ietf_snmp/data/integration_tests/full_config/expected_result_junos.txt b/tests/states/ietf_snmp/data/integration_tests/full_config/expected_result_junos.txt new file mode 100644 index 0000000..67559f1 --- /dev/null +++ b/tests/states/ietf_snmp/data/integration_tests/full_config/expected_result_junos.txt @@ -0,0 +1,13 @@ +snmp { + location "DC1;01.00"; + contact "prod-network"; + community communityro { + authorization read-only; + } + community communityro2 { + authorization read-only; + } + community communityrw { + authorization read-write; + } +} \ No newline at end of file diff --git a/tests/states/ietf_snmp/data/integration_tests/full_config/expected_result_sonic.txt b/tests/states/ietf_snmp/data/integration_tests/full_config/expected_result_sonic.txt new file mode 100644 index 0000000..6ac0322 --- /dev/null +++ b/tests/states/ietf_snmp/data/integration_tests/full_config/expected_result_sonic.txt @@ -0,0 +1,4 @@ +snmp_rocommunity: communityro +snmp_rocommunity: communityro2 +snmp_location: DC1;01.00 +snmp_contact: prod-network \ No newline at end of file diff --git a/tests/states/ietf_snmp/data/integration_tests/full_config/ietf.json b/tests/states/ietf_snmp/data/integration_tests/full_config/ietf.json new file mode 100644 index 0000000..99c9da1 --- /dev/null +++ b/tests/states/ietf_snmp/data/integration_tests/full_config/ietf.json @@ -0,0 +1,25 @@ +{ + "snmp":{ + "community":[ + { + "index":"global_ro", + "security-name":"readonly", + "text-name":"communityro" + }, + { + "index":"global_ro_n2", + "security-name":"readonly", + "text-name":"communityro2" + }, + { + "index":"globalrw", + "security-name":"readwrite", + "text-name":"communityrw" + } + ] + }, + "system":{ + "contact":"prod-network", + "location":"DC1;01.00" + } + } \ No newline at end of file diff --git a/tests/states/ietf_snmp/integration_tests/__init__.py b/tests/states/ietf_snmp/integration_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/states/ietf_snmp/integration_tests/test_ietf_integration.py b/tests/states/ietf_snmp/integration_tests/test_ietf_integration.py new file mode 100644 index 0000000..41e713e --- /dev/null +++ b/tests/states/ietf_snmp/integration_tests/test_ietf_integration.py @@ -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