diff --git a/data/templates/snmp/etc.snmpd.conf.tmpl b/data/templates/snmp/etc.snmpd.conf.tmpl index 30806ce8a..ca9b4d617 100644 --- a/data/templates/snmp/etc.snmpd.conf.tmpl +++ b/data/templates/snmp/etc.snmpd.conf.tmpl @@ -1,119 +1,124 @@ ### Autogenerated by snmp.py ### # non configurable defaults sysObjectID 1.3.6.1.4.1.44641 sysServices 14 master agentx agentXPerms 0777 0777 pass .1.3.6.1.2.1.31.1.1.1.18 /opt/vyatta/sbin/if-mib-alias smuxpeer .1.3.6.1.2.1.83 smuxpeer .1.3.6.1.2.1.157 smuxsocket localhost # linkUp/Down configure the Event MIB tables to monitor # the ifTable for network interfaces being taken up or down # for making internal queries to retrieve any necessary information iquerySecName {{ vyos_user }} # Modified from the default linkUpDownNotification # to include more OIDs and poll more frequently notificationEvent linkUpTrap linkUp ifIndex ifDescr ifType ifAdminStatus ifOperStatus notificationEvent linkDownTrap linkDown ifIndex ifDescr ifType ifAdminStatus ifOperStatus monitor -r 10 -e linkUpTrap "Generate linkUp" ifOperStatus != 2 monitor -r 10 -e linkDownTrap "Generate linkDown" ifOperStatus == 2 # Remove all old ifTable entries with the same ifName as newly appeared # interface (with different ifIndex) - this is the case on e.g. ppp interfaces interface_replace_old yes ######################## # configurable section # ######################## # Default system description is VyOS version sysDescr VyOS {{ version }} {% if description %} # Description SysDescr {{ description }} {% endif %} # Listen agentaddress unix:/run/snmpd.socket{% if listen_on %}{% for li in listen_on %},{{ li }}{% endfor %}{% else %},{{protocol}}:161{% if ipv6_enabled %},{{protocol}}6:161{% endif %}{% endif %} # SNMP communities {% for c in communities %} {% if c.network_v4 %} {% for network in c.network_v4 %} {{ c.authorization }}community {{ c.name }} {{ network }} {% endfor %} {% elif not c.has_source %} {{ c.authorization }}community {{ c.name }} {% endif %} {% if c.network_v6 %} {% for network in c.network_v6 %} {{ c.authorization }}community6 {{ c.name }} {{ network }} {% endfor %} {% elif not c.has_source %} {{ c.authorization }}community6 {{ c.name }} {% endif %} {% endfor %} {% if contact %} # system contact information SysContact {{ contact }} {% endif %} {% if location %} # system location information SysLocation {{ location }} {% endif %} {% if smux_peers %} # additional smux peers {% for sp in smux_peers %} smuxpeer {{ sp }} {% endfor %} {% endif %} {% if trap_targets %} # if there is a problem - tell someone! {% for trap in trap_targets %} trap2sink {{ trap.target }}{{ ":" + trap.port if trap.port is defined }} {{ trap.community }} {% endfor %} {% endif %} {% if v3_enabled %} # # SNMPv3 stuff goes here # # views {% for view in v3_views %} {% for oid in view.oids %} view {{ view.name }} included .{{ oid.oid }} +{% if oid.exclude %} +{% for excl in oid.exclude %} +view {{ view.name }} excluded .{{ excl }} +{% endfor %} +{% endif %} {% endfor %} {% endfor %} # access # context sec.model sec.level match read write notif {% for group in v3_groups %} access {{ group.name }} "" usm {{ group.seclevel }} exact {{ group.view }} {% if group.mode == 'ro' %}none{% else %}{{ group.view }}{% endif %} none {% endfor %} # trap-target {% for t in v3_traps %} trapsess -v 3 {{ '-Ci' if t.type == 'inform' }} -e {{ v3_engineid }} -u {{ t.secName }} -l {{ t.secLevel }} -a {{ t.authProtocol }} {% if t.authPassword %}-A {{ t.authPassword }}{% elif t.authMasterKey %}-3m {{ t.authMasterKey }}{% endif %} -x {{ t.privProtocol }} {% if t.privPassword %}-X {{ t.privPassword }}{% elif t.privMasterKey %}-3M {{ t.privMasterKey }}{% endif %} {{ t.ipProto }}:{{ t.ipAddr }}:{{ t.ipPort }} {% endfor %} # group {% for u in v3_users %} group {{ u.group }} usm {{ u.name }} {% endfor %} {% endif %} {% if script_ext %} # extension scripts {% for ext in script_ext|sort(attribute='name') %} extend {{ ext.name }} {{ ext.script }} {% endfor %} {% endif %} diff --git a/interface-definitions/snmp.xml.in b/interface-definitions/snmp.xml.in index 1067aa8a8..48aee7c9a 100644 --- a/interface-definitions/snmp.xml.in +++ b/interface-definitions/snmp.xml.in @@ -1,654 +1,655 @@ <?xml version="1.0"?> <!-- SNMP forwarder configuration --> <interfaceDefinition> <node name="service"> <children> <node name="snmp" owner="${vyos_conf_scripts_dir}/snmp.py"> <properties> <help>Simple Network Management Protocol (SNMP)</help> <priority>900</priority> </properties> <children> <tagNode name="community"> <properties> <help>Community name</help> <constraint> <regex>^[a-zA-Z0-9\-_!@*#]{1,100}$</regex> </constraint> <constraintErrorMessage>Community string is limited to alphanumerical characters, !, @, * and # with a total lenght of 100</constraintErrorMessage> </properties> <children> <leafNode name="authorization"> <properties> <help>Authorization type (default: 'ro')</help> <completionHelp> <list>ro rw</list> </completionHelp> <valueHelp> <format>ro</format> <description>read only</description> </valueHelp> <valueHelp> <format>rw</format> <description>read write</description> </valueHelp> <constraint> <regex>^(ro|rw)$</regex> </constraint> <constraintErrorMessage>Authorization type must be either 'rw' or 'ro'</constraintErrorMessage> </properties> </leafNode> <leafNode name="client"> <properties> <help>IP address of SNMP client allowed to contact system</help> <constraint> <validator name="ipv4-address"/> <validator name="ipv6-address"/> </constraint> <multi/> </properties> </leafNode> <leafNode name="network"> <properties> <help>Subnet of SNMP client(s) allowed to contact system</help> <valueHelp> <format>ipv4net</format> <description>IP address and prefix length</description> </valueHelp> <valueHelp> <format>ipv6net</format> <description>IPv6 address and prefix length</description> </valueHelp> <constraint> <validator name="ip-prefix"/> </constraint> <multi/> </properties> </leafNode> </children> </tagNode> <leafNode name="contact"> <properties> <help>Contact information</help> <constraint> <regex>^.{1,255}$</regex> </constraint> <constraintErrorMessage>Contact information is limited to 255 characters or less</constraintErrorMessage> </properties> </leafNode> <leafNode name="description"> <properties> <help>Description information</help> <constraint> <regex>^.{1,255}$</regex> </constraint> <constraintErrorMessage>Description is limited to 255 characters or less</constraintErrorMessage> </properties> </leafNode> <tagNode name="listen-address"> <properties> <help>IP address to listen for incoming SNMP requests</help> <completionHelp> <script>${vyos_completion_dir}/list_local_ips.sh --both</script> </completionHelp> <valueHelp> <format>ipv4</format> <description>IPv4 address to listen for incoming SNMP requests</description> </valueHelp> <valueHelp> <format>ipv6</format> <description>IPv6 address to listen for incoming SNMP requests</description> </valueHelp> <constraint> <validator name="ipv4-address"/> <validator name="ipv6-address"/> </constraint> </properties> <children> <leafNode name="port"> <properties> <help>Port for SNMP service (default: '161')</help> <valueHelp> <format>u32:1-65535</format> <description>Numeric IP port</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-65535"/> </constraint> <constraintErrorMessage>Port number must be in range 1 to 65535</constraintErrorMessage> </properties> </leafNode> </children> </tagNode> <leafNode name="location"> <properties> <help>Location information</help> <constraint> <regex>^.{1,255}$</regex> </constraint> <constraintErrorMessage>Location is limited to 255 characters or less</constraintErrorMessage> </properties> </leafNode> <leafNode name="protocol"> <properties> <help>Listen protocol for SNMP</help> <completionHelp> <list>udp tcp</list> </completionHelp> <valueHelp> <format>udp</format> <description>Listen protocol UDP (default)</description> </valueHelp> <valueHelp> <format>tcp</format> <description>Listen protocol TCP</description> </valueHelp> <constraint> <regex>^(udp|tcp)$</regex> </constraint> </properties> <defaultValue>udp</defaultValue> </leafNode> <leafNode name="smux-peer"> <properties> <help>Register a subtree for SMUX-based processing</help> <valueHelp> <format>oid</format> <description>Object Identifier</description> </valueHelp> <multi/> </properties> </leafNode> <leafNode name="trap-source"> <properties> <help>SNMP trap source address</help> <valueHelp> <format>ipv4</format> <description>IPv4 address</description> </valueHelp> <valueHelp> <format>ipv6</format> <description>IPv6 address</description> </valueHelp> <constraint> <validator name="ipv4-address"/> <validator name="ipv6-address"/> </constraint> </properties> </leafNode> <tagNode name="trap-target"> <properties> <help>Address of trap target</help> <valueHelp> <format>ipv4</format> <description>IPv4 address</description> </valueHelp> <valueHelp> <format>ipv6</format> <description>IPv6 address</description> </valueHelp> <constraint> <validator name="ipv4-address"/> <validator name="ipv6-address"/> </constraint> </properties> <children> <leafNode name="community"> <properties> <help>Community used when sending trap information</help> </properties> </leafNode> <leafNode name="port"> <properties> <help>Destination port used for trap notification</help> <valueHelp> <format>u32:1-65535</format> <description>Numeric IP port</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-65535"/> </constraint> <constraintErrorMessage>Port number must be in range 1 to 65535</constraintErrorMessage> </properties> </leafNode> </children> </tagNode> <node name="v3"> <properties> <help>Simple Network Management Protocol (SNMP) v3</help> </properties> <children> <leafNode name="engineid"> <properties> <help>Specifies the EngineID that uniquely identify an agent (e.g. 000000000000000000000002)</help> <constraint> <regex>^([0-9a-f][0-9a-f]){1,18}$</regex> </constraint> <constraintErrorMessage>ID must contain an even number (from 2 to 36) of hex digits</constraintErrorMessage> </properties> </leafNode> <tagNode name="group"> <properties> <help>Specifies the group with name groupname</help> </properties> <children> <leafNode name="mode"> <properties> <help>Define group access permission (default: 'ro')</help> <completionHelp> <list>ro rw</list> </completionHelp> <valueHelp> <format>ro</format> <description>read only</description> </valueHelp> <valueHelp> <format>rw</format> <description>read write</description> </valueHelp> <constraint> <regex>^(ro|rw)$</regex> </constraint> <constraintErrorMessage>Authorization type must be either 'rw' or 'ro'</constraintErrorMessage> </properties> </leafNode> <leafNode name="seclevel"> <properties> <help>Security levels</help> <completionHelp> <list>noauth auth priv</list> </completionHelp> <valueHelp> <format>noauth</format> <description>Messages not authenticated and not encrypted (noAuthNoPriv)</description> </valueHelp> <valueHelp> <format>auth</format> <description>Messages are authenticated but not encrypted (authNoPriv)</description> </valueHelp> <valueHelp> <format>priv</format> <description>Messages are authenticated and encrypted (authPriv)</description> </valueHelp> <constraint> <regex>^(noauth|auth|priv)$</regex> </constraint> </properties> </leafNode> <leafNode name="view"> <properties> <help>Defines the name of view</help> <completionHelp> <path>service snmp v3 view</path> </completionHelp> </properties> </leafNode> </children> </tagNode> <tagNode name="trap-target"> <properties> <help>Defines SNMP target for inform or traps for IP</help> <valueHelp> <format>ipv4</format> <description>IP address of trap target</description> </valueHelp> <valueHelp> <format>ipv6</format> <description>IPv6 address of trap target</description> </valueHelp> <constraint> <validator name="ipv4-address"/> <validator name="ipv6-address"/> </constraint> </properties> <children> <node name="auth"> <properties> <help>Defines the privacy</help> </properties> <children> <leafNode name="encrypted-password"> <properties> <help>Defines the encrypted key for authentication</help> <constraint> <regex>^[0-9a-f]*$</regex> </constraint> <constraintErrorMessage>Encrypted key must only contain hex digits</constraintErrorMessage> </properties> </leafNode> <leafNode name="plaintext-password"> <properties> <help>Defines the clear text key for authentication</help> <constraint> <regex>^.{8,}$</regex> </constraint> <constraintErrorMessage>Key must contain 8 or more characters</constraintErrorMessage> </properties> </leafNode> <leafNode name="type"> <properties> <help>Defines the protocol used for authentication (default: 'md5')</help> <completionHelp> <list>md5 sha</list> </completionHelp> <valueHelp> <format>md5</format> <description>Message Digest 5</description> </valueHelp> <valueHelp> <format>sha</format> <description>Secure Hash Algorithm</description> </valueHelp> <constraint> <regex>^(md5|sha)$</regex> </constraint> </properties> </leafNode> </children> </node> <leafNode name="port"> <properties> <help>Specifies TCP/UDP port of destination SNMP traps/informs (default: '162')</help> <valueHelp> <format>u32:1-65535</format> <description>Numeric IP port</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-65535"/> </constraint> <constraintErrorMessage>Port number must be in range 1 to 65535</constraintErrorMessage> </properties> </leafNode> <node name="privacy"> <properties> <help>Defines the privacy</help> </properties> <children> <leafNode name="encrypted-password"> <properties> <help>Defines the encrypted key for privacy protocol</help> <constraint> <regex>^[0-9a-f]*$</regex> </constraint> <constraintErrorMessage>Encrypted key must only contain hex digits</constraintErrorMessage> </properties> </leafNode> <leafNode name="plaintext-password"> <properties> <help>Defines the clear text key for privacy protocol</help> <constraint> <regex>^.{8,}$</regex> </constraint> <constraintErrorMessage>Key must contain 8 or more characters</constraintErrorMessage> </properties> </leafNode> <leafNode name="type"> <properties> <help>Defines the protocol for privacy (default: 'des')</help> <completionHelp> <list>des aes</list> </completionHelp> <valueHelp> <format>des</format> <description>Data Encryption Standard</description> </valueHelp> <valueHelp> <format>aes</format> <description>Advanced Encryption Standard</description> </valueHelp> <constraint> <regex>^(des|aes)$</regex> </constraint> </properties> </leafNode> </children> </node> <leafNode name="protocol"> <properties> <help>Defines protocol for notification between TCP and UDP</help> <completionHelp> <list>tcp udp</list> </completionHelp> <valueHelp> <format>tcp</format> <description>Use Transmission Control Protocol for notifications</description> </valueHelp> <valueHelp> <format>udp</format> <description>Use User Datagram Protocol for notifications</description> </valueHelp> <constraint> <regex>^(tcp|udp)$</regex> </constraint> </properties> </leafNode> <leafNode name="type"> <properties> <help>Specifies the type of notification between inform and trap (default: 'inform')</help> <completionHelp> <list>inform trap</list> </completionHelp> <valueHelp> <format>inform</format> <description>Use INFORM</description> </valueHelp> <valueHelp> <format>trap</format> <description>Use TRAP</description> </valueHelp> <constraint> <regex>^(inform|trap)$</regex> </constraint> </properties> </leafNode> <leafNode name="user"> <properties> <help>Defines username for authentication</help> <completionHelp> <path>service snmp v3 user</path> </completionHelp> </properties> </leafNode> </children> </tagNode> <tagNode name="user"> <properties> <help>Specifies the user with name username</help> <constraint> <regex>[^\(\)\|\-]+$</regex> </constraint> <constraintErrorMessage>Illegal characters in name</constraintErrorMessage> </properties> <children> <node name="auth"> <properties> <help>Specifies the auth</help> </properties> <children> <leafNode name="encrypted-password"> <properties> <help>Defines the encrypted key for authentication</help> <constraint> <regex>^[0-9a-f]*$</regex> </constraint> <constraintErrorMessage>Encrypted key must only contain hex digits</constraintErrorMessage> </properties> </leafNode> <leafNode name="plaintext-password"> <properties> <help>Defines the clear text key for authentication</help> <constraint> <regex>^.{8,}$</regex> </constraint> <constraintErrorMessage>Key must contain 8 or more characters</constraintErrorMessage> </properties> </leafNode> <leafNode name="type"> <properties> <help>Defines the protocol used for authentication (default: 'md5')</help> <completionHelp> <list>md5 sha</list> </completionHelp> <valueHelp> <format>md5</format> <description>Message Digest 5</description> </valueHelp> <valueHelp> <format>sha</format> <description>Secure Hash Algorithm</description> </valueHelp> <constraint> <regex>^(md5|sha)$</regex> </constraint> </properties> </leafNode> </children> </node> <leafNode name="group"> <properties> <help>Specifies group for user name</help> <completionHelp> <path>service snmp v3 group</path> </completionHelp> </properties> </leafNode> <leafNode name="mode"> <properties> <help>Define users access permission (default: 'ro')</help> <completionHelp> <list>ro rw</list> </completionHelp> <valueHelp> <format>ro</format> <description>read only</description> </valueHelp> <valueHelp> <format>rw</format> <description>read write</description> </valueHelp> <constraint> <regex>^(ro|rw)$</regex> </constraint> <constraintErrorMessage>Authorization type must be either 'rw' or 'ro'</constraintErrorMessage> </properties> </leafNode> <node name="privacy"> <properties> <help>Defines the privacy</help> </properties> <children> <leafNode name="encrypted-password"> <properties> <help>Defines the encrypted key for privacy protocol</help> <constraint> <regex>^[0-9a-f]*$</regex> </constraint> <constraintErrorMessage>Encrypted key must only contain hex digits</constraintErrorMessage> </properties> </leafNode> <leafNode name="plaintext-password"> <properties> <help>Defines the clear text key for privacy protocol</help> <constraint> <regex>^.{8,}$</regex> </constraint> <constraintErrorMessage>Key must contain 8 or more characters</constraintErrorMessage> </properties> </leafNode> <leafNode name="type"> <properties> <help>Defines the protocol for privacy (default: 'des')</help> <completionHelp> <list>des aes</list> </completionHelp> <valueHelp> <format>des</format> <description>Data Encryption Standard</description> </valueHelp> <valueHelp> <format>aes</format> <description>Advanced Encryption Standard</description> </valueHelp> <constraint> <regex>^(des|aes)$</regex> </constraint> </properties> </leafNode> </children> </node> </children> </tagNode> <tagNode name="view"> <properties> <help>Specifies the view with name viewname</help> <constraint> <regex>[^\(\)\|\-]+$</regex> </constraint> <constraintErrorMessage>Illegal characters in name</constraintErrorMessage> </properties> <children> <tagNode name="oid"> <properties> <help>Specifies the oid</help> <constraint> <regex>^[0-9]+(\.[0-9]+)*$</regex> </constraint> <constraintErrorMessage>OID must start from a number</constraintErrorMessage> </properties> <children> <leafNode name="exclude"> <properties> <help>Exclude is an optional argument</help> + <multi/> </properties> </leafNode> <leafNode name="mask"> <properties> <help>Defines a bit-mask that is indicating which subidentifiers of the associated subtree OID should be regarded as significant</help> <constraint> <regex>^[0-9a-f]{2}([\.:][0-9a-f]{2})*$</regex> </constraint> <constraintErrorMessage>MASK is a list of hex octets, separated by '.' or ':'</constraintErrorMessage> </properties> </leafNode> </children> </tagNode> </children> </tagNode> </children> </node> <node name="script-extensions"> <properties> <help>SNMP script extensions</help> </properties> <children> <tagNode name="extension-name"> <properties> <help>Extension name</help> <constraint> <regex>^[a-z0-9\.\-\_]+</regex> </constraint> <constraintErrorMessage>Script extension contains invalid characters</constraintErrorMessage> </properties> <children> <leafNode name="script"> <properties> <help>Script location and name</help> <completionHelp> <script>ls /config/user-data</script> </completionHelp> <constraint> <regex>^[a-z0-9\.\-\_\/]+</regex> </constraint> <constraintErrorMessage>Script extension contains invalid characters</constraintErrorMessage> </properties> </leafNode> </children> </tagNode> </children> </node> #include <include/interface/vrf.xml.i> </children> </node> </children> </node> </interfaceDefinition> diff --git a/smoketest/scripts/cli/test_service_snmp.py b/smoketest/scripts/cli/test_service_snmp.py index 864097771..5066b7942 100755 --- a/smoketest/scripts/cli/test_service_snmp.py +++ b/smoketest/scripts/cli/test_service_snmp.py @@ -1,229 +1,249 @@ #!/usr/bin/env python3 # # Copyright (C) 2019-2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import re import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.template import is_ipv4 from vyos.template import address_from_cidr from vyos.util import read_file from vyos.util import process_named_running PROCESS_NAME = 'snmpd' SNMPD_CONF = '/etc/snmp/snmpd.conf' base_path = ['service', 'snmp'] def get_config_value(key): tmp = read_file(SNMPD_CONF) tmp = re.findall(r'\n?{}\s+(.*)'.format(key), tmp) return tmp[0] class TestSNMPService(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): super(cls, cls).setUpClass() # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) def tearDown(self): # delete testing SNMP config self.cli_delete(base_path) self.cli_commit() def test_snmp_basic(self): dummy_if = 'dum7312' dummy_addr = '100.64.0.1/32' self.cli_set(['interfaces', 'dummy', dummy_if, 'address', dummy_addr]) # Check if SNMP can be configured and service runs clients = ['192.0.2.1', '2001:db8::1'] networks = ['192.0.2.128/25', '2001:db8:babe::/48'] listen = ['127.0.0.1', '::1', address_from_cidr(dummy_addr)] port = '5000' for auth in ['ro', 'rw']: community = 'VyOS' + auth self.cli_set(base_path + ['community', community, 'authorization', auth]) for client in clients: self.cli_set(base_path + ['community', community, 'client', client]) for network in networks: self.cli_set(base_path + ['community', community, 'network', network]) for addr in listen: self.cli_set(base_path + ['listen-address', addr, 'port', port]) self.cli_set(base_path + ['contact', 'maintainers@vyos.io']) self.cli_set(base_path + ['location', 'qemu']) self.cli_commit() # verify listen address, it will be returned as # ['unix:/run/snmpd.socket,udp:127.0.0.1:161,udp6:[::1]:161'] # thus we need to transfor this into a proper list config = get_config_value('agentaddress') expected = 'unix:/run/snmpd.socket' self.assertIn(expected, config) for addr in listen: if is_ipv4(addr): expected = f'udp:{addr}:{port}' else: expected = f'udp6:[{addr}]:{port}' self.assertIn(expected, config) # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) self.cli_delete(['interfaces', 'dummy', dummy_if]) def test_snmp_tcp(self): dummy_if = 'dum123' dummy_addr = '100.64.0.1/32' self.cli_set(['interfaces', 'dummy', dummy_if, 'address', dummy_addr]) # Check if SNMP can be configured and service runs clients = ['192.0.2.1', '2001:db8::1'] networks = ['192.0.2.128/25', '2001:db8:babe::/48'] listen = ['127.0.0.1', '::1', address_from_cidr(dummy_addr)] port = '2325' for auth in ['ro', 'rw']: community = 'VyOS' + auth self.cli_set(base_path + ['community', community, 'authorization', auth]) for client in clients: self.cli_set(base_path + ['community', community, 'client', client]) for network in networks: self.cli_set(base_path + ['community', community, 'network', network]) for addr in listen: self.cli_set(base_path + ['listen-address', addr, 'port', port]) self.cli_set(base_path + ['contact', 'maintainers@vyos.io']) self.cli_set(base_path + ['location', 'qemu']) self.cli_set(base_path + ['protocol', 'tcp']) self.cli_commit() # verify listen address, it will be returned as # ['unix:/run/snmpd.socket,tcp:127.0.0.1:161,tcp6:[::1]:161'] # thus we need to transfor this into a proper list config = get_config_value('agentaddress') expected = 'unix:/run/snmpd.socket' self.assertIn(expected, config) for addr in listen: if is_ipv4(addr): expected = f'tcp:{addr}:{port}' else: expected = f'tcp6:[{addr}]:{port}' self.assertIn(expected, config) # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) self.cli_delete(['interfaces', 'dummy', dummy_if]) def test_snmpv3_sha(self): # Check if SNMPv3 can be configured with SHA authentication # and service runs self.cli_set(base_path + ['v3', 'engineid', '000000000000000000000002']) self.cli_set(base_path + ['v3', 'group', 'default', 'mode', 'ro']) # check validate() - a view must be created before this can be committed with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_set(base_path + ['v3', 'view', 'default', 'oid', '1']) self.cli_set(base_path + ['v3', 'group', 'default', 'view', 'default']) # create user self.cli_set(base_path + ['v3', 'user', 'vyos', 'auth', 'plaintext-password', 'vyos12345678']) self.cli_set(base_path + ['v3', 'user', 'vyos', 'auth', 'type', 'sha']) self.cli_set(base_path + ['v3', 'user', 'vyos', 'privacy', 'plaintext-password', 'vyos12345678']) self.cli_set(base_path + ['v3', 'user', 'vyos', 'privacy', 'type', 'aes']) self.cli_set(base_path + ['v3', 'user', 'vyos', 'group', 'default']) self.cli_commit() # commit will alter the CLI values - check if they have been updated: hashed_password = '4e52fe55fd011c9c51ae2c65f4b78ca93dcafdfe' tmp = self._session.show_config(base_path + ['v3', 'user', 'vyos', 'auth', 'encrypted-password']).split()[1] self.assertEqual(tmp, hashed_password) tmp = self._session.show_config(base_path + ['v3', 'user', 'vyos', 'privacy', 'encrypted-password']).split()[1] self.assertEqual(tmp, hashed_password) # TODO: read in config file and check values # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) def test_snmpv3_md5(self): # Check if SNMPv3 can be configured with MD5 authentication # and service runs snmpv3_group = 'default_group' snmpv3_view = 'default_view' snmpv3_view_oid = '1' snmpv3_user = 'vyos' self.cli_set(base_path + ['v3', 'engineid', '000000000000000000000002']) # create user self.cli_set(base_path + ['v3', 'user', snmpv3_user, 'auth', 'plaintext-password', 'vyos12345678']) self.cli_set(base_path + ['v3', 'user', snmpv3_user, 'auth', 'type', 'md5']) self.cli_set(base_path + ['v3', 'user', snmpv3_user, 'privacy', 'plaintext-password', 'vyos12345678']) self.cli_set(base_path + ['v3', 'user', snmpv3_user, 'privacy', 'type', 'des']) # check validate() - user requires a group to be created with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_set(base_path + ['v3', 'user', 'vyos', 'group', snmpv3_group]) self.cli_set(base_path + ['v3', 'group', snmpv3_group, 'mode', 'ro']) # check validate() - a view must be created before this can be comitted with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_set(base_path + ['v3', 'view', snmpv3_view, 'oid', snmpv3_view_oid]) self.cli_set(base_path + ['v3', 'group', snmpv3_group, 'view', snmpv3_view]) self.cli_commit() # commit will alter the CLI values - check if they have been updated: hashed_password = '4c67690d45d3dfcd33d0d7e308e370ad' tmp = self._session.show_config(base_path + ['v3', 'user', 'vyos', 'auth', 'encrypted-password']).split()[1] self.assertEqual(tmp, hashed_password) tmp = self._session.show_config(base_path + ['v3', 'user', 'vyos', 'privacy', 'encrypted-password']).split()[1] self.assertEqual(tmp, hashed_password) tmp = read_file(SNMPD_CONF) # views self.assertIn(f'view {snmpv3_view} included .{snmpv3_view_oid}', tmp) # group self.assertIn(f'group {snmpv3_group} usm {snmpv3_user}', tmp) # access self.assertIn(f'access {snmpv3_group} "" usm auth exact {snmpv3_view} none none', tmp) # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) + def test_snmpv3_view_exclude(self): + snmpv3_group = 'default_group' + snmpv3_view = 'default_view' + snmpv3_view_oid = '1' + snmpv3_view_oid_exclude = ['1.3.6.1.2.1.4.21', '1.3.6.1.2.1.4.24'] + + self.cli_set(base_path + ['v3', 'group', snmpv3_group, 'view', snmpv3_view]) + self.cli_set(base_path + ['v3', 'view', snmpv3_view, 'oid', snmpv3_view_oid]) + + for excluded in snmpv3_view_oid_exclude: + self.cli_set(base_path + ['v3', 'view', snmpv3_view, 'oid', snmpv3_view_oid, 'exclude', excluded]) + + self.cli_commit() + + tmp = read_file(SNMPD_CONF) + # views + self.assertIn(f'view {snmpv3_view} included .{snmpv3_view_oid}', tmp) + for excluded in snmpv3_view_oid_exclude: + self.assertIn(f'view {snmpv3_view} excluded .{excluded}', tmp) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index 0be0da3d1..7b3f9cbb7 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -1,596 +1,598 @@ #!/usr/bin/env python3 # # Copyright (C) 2018-2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os from sys import exit from vyos.config import Config from vyos.configverify import verify_vrf from vyos.snmpv3_hashgen import plaintext_to_md5 from vyos.snmpv3_hashgen import plaintext_to_sha1 from vyos.snmpv3_hashgen import random from vyos.template import render from vyos.template import is_ipv4 from vyos.util import call from vyos.util import chmod_755 from vyos.validate import is_addr_assigned from vyos.version import get_version_data from vyos import ConfigError from vyos import airbag airbag.enable() config_file_client = r'/etc/snmp/snmp.conf' config_file_daemon = r'/etc/snmp/snmpd.conf' config_file_access = r'/usr/share/snmp/snmpd.conf' config_file_user = r'/var/lib/snmp/snmpd.conf' default_script_dir = r'/config/user-data/' systemd_override = r'/etc/systemd/system/snmpd.service.d/override.conf' # SNMP OIDs used to mark auth/priv type OIDs = { 'md5' : '.1.3.6.1.6.3.10.1.1.2', 'sha' : '.1.3.6.1.6.3.10.1.1.3', 'aes' : '.1.3.6.1.6.3.10.1.2.4', 'des' : '.1.3.6.1.6.3.10.1.2.2', 'none': '.1.3.6.1.6.3.10.1.2.1' } default_config_data = { 'listen_on': [], 'listen_address': [], 'ipv6_enabled': 'True', 'communities': [], 'smux_peers': [], 'location' : '', 'protocol' : 'udp', 'description' : '', 'contact' : '', 'trap_source': '', 'trap_targets': [], 'vyos_user': '', 'vyos_user_pass': '', 'version': '', 'v3_enabled': 'False', 'v3_engineid': '', 'v3_groups': [], 'v3_traps': [], 'v3_users': [], 'v3_views': [], 'script_ext': [] } def rmfile(file): if os.path.isfile(file): os.unlink(file) def get_config(): snmp = default_config_data conf = Config() if not conf.exists('service snmp'): return None else: if conf.exists('system ipv6 disable'): snmp['ipv6_enabled'] = False conf.set_level('service snmp') version_data = get_version_data() snmp['version'] = version_data['version'] # create an internal snmpv3 user of the form 'vyosxxxxxxxxxxxxxxxx' snmp['vyos_user'] = 'vyos' + random(8) snmp['vyos_user_pass'] = random(16) if conf.exists('community'): for name in conf.list_nodes('community'): community = { 'name': name, 'authorization': 'ro', 'network_v4': [], 'network_v6': [], 'has_source' : False } if conf.exists('community {0} authorization'.format(name)): community['authorization'] = conf.return_value('community {0} authorization'.format(name)) # Subnet of SNMP client(s) allowed to contact system if conf.exists('community {0} network'.format(name)): for addr in conf.return_values('community {0} network'.format(name)): if is_ipv4(addr): community['network_v4'].append(addr) else: community['network_v6'].append(addr) # IP address of SNMP client allowed to contact system if conf.exists('community {0} client'.format(name)): for addr in conf.return_values('community {0} client'.format(name)): if is_ipv4(addr): community['network_v4'].append(addr) else: community['network_v6'].append(addr) if (len(community['network_v4']) > 0) or (len(community['network_v6']) > 0): community['has_source'] = True snmp['communities'].append(community) if conf.exists('contact'): snmp['contact'] = conf.return_value('contact') if conf.exists('description'): snmp['description'] = conf.return_value('description') if conf.exists('listen-address'): for addr in conf.list_nodes('listen-address'): port = '161' if conf.exists('listen-address {0} port'.format(addr)): port = conf.return_value('listen-address {0} port'.format(addr)) snmp['listen_address'].append((addr, port)) # Always listen on localhost if an explicit address has been configured # This is a safety measure to not end up with invalid listen addresses # that are not configured on this system. See https://vyos.dev/T850 if not '127.0.0.1' in conf.list_nodes('listen-address'): snmp['listen_address'].append(('127.0.0.1', '161')) if not '::1' in conf.list_nodes('listen-address'): snmp['listen_address'].append(('::1', '161')) if conf.exists('location'): snmp['location'] = conf.return_value('location') if conf.exists('protocol'): snmp['protocol'] = conf.return_value('protocol') if conf.exists('smux-peer'): snmp['smux_peers'] = conf.return_values('smux-peer') if conf.exists('trap-source'): snmp['trap_source'] = conf.return_value('trap-source') if conf.exists('trap-target'): for target in conf.list_nodes('trap-target'): trap_tgt = { 'target': target, 'community': '', 'port': '' } if conf.exists('trap-target {0} community'.format(target)): trap_tgt['community'] = conf.return_value('trap-target {0} community'.format(target)) if conf.exists('trap-target {0} port'.format(target)): trap_tgt['port'] = conf.return_value('trap-target {0} port'.format(target)) snmp['trap_targets'].append(trap_tgt) if conf.exists('script-extensions'): for extname in conf.list_nodes('script-extensions extension-name'): conf_script = conf.return_value('script-extensions extension-name {} script'.format(extname)) # if script has not absolute path, use pre configured path if "/" not in conf_script: conf_script = default_script_dir + conf_script extension = { 'name': extname, 'script' : conf_script } snmp['script_ext'].append(extension) if conf.exists('vrf'): # Append key to dict but don't place it in the default dictionary. # This is required to make the override.conf.tmpl work until we # migrate to get_config_dict(). snmp['vrf'] = conf.return_value('vrf') ######################################################################### # ____ _ _ __ __ ____ _____ # # / ___|| \ | | \/ | _ \ __ _|___ / # # \___ \| \| | |\/| | |_) | \ \ / / |_ \ # # ___) | |\ | | | | __/ \ V / ___) | # # |____/|_| \_|_| |_|_| \_/ |____/ # # # # now take care about the fancy SNMP v3 stuff, or bail out eraly # ######################################################################### if not conf.exists('v3'): return snmp else: snmp['v3_enabled'] = True # 'set service snmp v3 engineid' if conf.exists('v3 engineid'): snmp['v3_engineid'] = conf.return_value('v3 engineid') # 'set service snmp v3 group' if conf.exists('v3 group'): for group in conf.list_nodes('v3 group'): v3_group = { 'name': group, 'mode': 'ro', 'seclevel': 'auth', 'view': '' } if conf.exists('v3 group {0} mode'.format(group)): v3_group['mode'] = conf.return_value('v3 group {0} mode'.format(group)) if conf.exists('v3 group {0} seclevel'.format(group)): v3_group['seclevel'] = conf.return_value('v3 group {0} seclevel'.format(group)) if conf.exists('v3 group {0} view'.format(group)): v3_group['view'] = conf.return_value('v3 group {0} view'.format(group)) snmp['v3_groups'].append(v3_group) # 'set service snmp v3 trap-target' if conf.exists('v3 trap-target'): for trap in conf.list_nodes('v3 trap-target'): trap_cfg = { 'ipAddr': trap, 'secName': '', 'authProtocol': 'md5', 'authPassword': '', 'authMasterKey': '', 'privProtocol': 'des', 'privPassword': '', 'privMasterKey': '', 'ipProto': 'udp', 'ipPort': '162', 'type': '', 'secLevel': 'noAuthNoPriv' } if conf.exists('v3 trap-target {0} user'.format(trap)): # Set the securityName used for authenticated SNMPv3 messages. trap_cfg['secName'] = conf.return_value('v3 trap-target {0} user'.format(trap)) if conf.exists('v3 trap-target {0} auth type'.format(trap)): # Set the authentication protocol (MD5 or SHA) used for authenticated SNMPv3 messages # cmdline option '-a' trap_cfg['authProtocol'] = conf.return_value('v3 trap-target {0} auth type'.format(trap)) if conf.exists('v3 trap-target {0} auth plaintext-password'.format(trap)): # Set the authentication pass phrase used for authenticated SNMPv3 messages. # cmdline option '-A' trap_cfg['authPassword'] = conf.return_value('v3 trap-target {0} auth plaintext-password'.format(trap)) if conf.exists('v3 trap-target {0} auth encrypted-password'.format(trap)): # Sets the keys to be used for SNMPv3 transactions. These options allow you to set the master authentication keys. # cmdline option '-3m' trap_cfg['authMasterKey'] = conf.return_value('v3 trap-target {0} auth encrypted-password'.format(trap)) if conf.exists('v3 trap-target {0} privacy type'.format(trap)): # Set the privacy protocol (DES or AES) used for encrypted SNMPv3 messages. # cmdline option '-x' trap_cfg['privProtocol'] = conf.return_value('v3 trap-target {0} privacy type'.format(trap)) if conf.exists('v3 trap-target {0} privacy plaintext-password'.format(trap)): # Set the privacy pass phrase used for encrypted SNMPv3 messages. # cmdline option '-X' trap_cfg['privPassword'] = conf.return_value('v3 trap-target {0} privacy plaintext-password'.format(trap)) if conf.exists('v3 trap-target {0} privacy encrypted-password'.format(trap)): # Sets the keys to be used for SNMPv3 transactions. These options allow you to set the master encryption keys. # cmdline option '-3M' trap_cfg['privMasterKey'] = conf.return_value('v3 trap-target {0} privacy encrypted-password'.format(trap)) if conf.exists('v3 trap-target {0} protocol'.format(trap)): trap_cfg['ipProto'] = conf.return_value('v3 trap-target {0} protocol'.format(trap)) if conf.exists('v3 trap-target {0} port'.format(trap)): trap_cfg['ipPort'] = conf.return_value('v3 trap-target {0} port'.format(trap)) if conf.exists('v3 trap-target {0} type'.format(trap)): trap_cfg['type'] = conf.return_value('v3 trap-target {0} type'.format(trap)) # Determine securityLevel used for SNMPv3 messages (noAuthNoPriv|authNoPriv|authPriv). # Appropriate pass phrase(s) must provided when using any level higher than noAuthNoPriv. if trap_cfg['authPassword'] or trap_cfg['authMasterKey']: if trap_cfg['privProtocol'] or trap_cfg['privPassword']: trap_cfg['secLevel'] = 'authPriv' else: trap_cfg['secLevel'] = 'authNoPriv' snmp['v3_traps'].append(trap_cfg) # 'set service snmp v3 user' if conf.exists('v3 user'): for user in conf.list_nodes('v3 user'): user_cfg = { 'name': user, 'authMasterKey': '', 'authPassword': '', 'authProtocol': 'md5', 'authOID': 'none', 'group': '', 'mode': 'ro', 'privMasterKey': '', 'privPassword': '', 'privOID': '', 'privProtocol': 'des' } # v3 user {0} auth if conf.exists('v3 user {0} auth encrypted-password'.format(user)): user_cfg['authMasterKey'] = conf.return_value('v3 user {0} auth encrypted-password'.format(user)) if conf.exists('v3 user {0} auth plaintext-password'.format(user)): user_cfg['authPassword'] = conf.return_value('v3 user {0} auth plaintext-password'.format(user)) # load default value type = user_cfg['authProtocol'] if conf.exists('v3 user {0} auth type'.format(user)): type = conf.return_value('v3 user {0} auth type'.format(user)) # (re-)update with either default value or value from CLI user_cfg['authProtocol'] = type user_cfg['authOID'] = OIDs[type] # v3 user {0} group if conf.exists('v3 user {0} group'.format(user)): user_cfg['group'] = conf.return_value('v3 user {0} group'.format(user)) # v3 user {0} mode if conf.exists('v3 user {0} mode'.format(user)): user_cfg['mode'] = conf.return_value('v3 user {0} mode'.format(user)) # v3 user {0} privacy if conf.exists('v3 user {0} privacy encrypted-password'.format(user)): user_cfg['privMasterKey'] = conf.return_value('v3 user {0} privacy encrypted-password'.format(user)) if conf.exists('v3 user {0} privacy plaintext-password'.format(user)): user_cfg['privPassword'] = conf.return_value('v3 user {0} privacy plaintext-password'.format(user)) # load default value type = user_cfg['privProtocol'] if conf.exists('v3 user {0} privacy type'.format(user)): type = conf.return_value('v3 user {0} privacy type'.format(user)) # (re-)update with either default value or value from CLI user_cfg['privProtocol'] = type user_cfg['privOID'] = OIDs[type] snmp['v3_users'].append(user_cfg) # 'set service snmp v3 view' if conf.exists('v3 view'): for view in conf.list_nodes('v3 view'): view_cfg = { 'name': view, 'oids': [] } if conf.exists('v3 view {0} oid'.format(view)): for oid in conf.list_nodes('v3 view {0} oid'.format(view)): oid_cfg = { 'oid': oid } + if conf.exists('v3 view {0} oid {1} exclude'.format(view, oid)): + oid_cfg['exclude'] = conf.return_values('v3 view {0} oid {1} exclude'.format(view, oid)) view_cfg['oids'].append(oid_cfg) snmp['v3_views'].append(view_cfg) return snmp def verify(snmp): if snmp is None: # we can not delete SNMP when LLDP is configured with SNMP conf = Config() if conf.exists('service lldp snmp enable'): raise ConfigError('Can not delete SNMP service, as LLDP still uses SNMP!') return None ### check if the configured script actually exist if snmp['script_ext']: for ext in snmp['script_ext']: if not os.path.isfile(ext['script']): print ("WARNING: script: {} doesn't exist".format(ext['script'])) else: chmod_755(ext['script']) for listen in snmp['listen_address']: addr = listen[0] port = listen[1] protocol = snmp['protocol'] tmp = None if is_ipv4(addr): # example: udp:127.0.0.1:161 tmp = f'{protocol}:{addr}:{port}' elif snmp['ipv6_enabled']: # example: udp6:[::1]:161 tmp = f'{protocol}6:[{addr}]:{port}' # We only wan't to configure addresses that exist on the system. # Hint the user if they don't exist if is_addr_assigned(addr): if tmp: snmp['listen_on'].append(tmp) else: print(f'WARNING: SNMP listen address {addr} not configured!') verify_vrf(snmp) # bail out early if SNMP v3 is not configured if not snmp['v3_enabled']: return None if 'v3_groups' in snmp.keys(): for group in snmp['v3_groups']: # # A view must exist prior to mapping it into a group # if 'view' in group.keys(): error = True if 'v3_views' in snmp.keys(): for view in snmp['v3_views']: if view['name'] == group['view']: error = False if error: raise ConfigError('You must create view "{0}" first'.format(group['view'])) else: raise ConfigError('"view" must be specified') if not 'mode' in group.keys(): raise ConfigError('"mode" must be specified') if not 'seclevel' in group.keys(): raise ConfigError('"seclevel" must be specified') if 'v3_traps' in snmp.keys(): for trap in snmp['v3_traps']: if trap['authPassword'] and trap['authMasterKey']: raise ConfigError('Must specify only one of encrypted-password/plaintext-key for trap auth') if trap['authPassword'] == '' and trap['authMasterKey'] == '': raise ConfigError('Must specify encrypted-password or plaintext-key for trap auth') if trap['privPassword'] and trap['privMasterKey']: raise ConfigError('Must specify only one of encrypted-password/plaintext-key for trap privacy') if trap['privPassword'] == '' and trap['privMasterKey'] == '': raise ConfigError('Must specify encrypted-password or plaintext-key for trap privacy') if not 'type' in trap.keys(): raise ConfigError('v3 trap: "type" must be specified') if not 'authPassword' and 'authMasterKey' in trap.keys(): raise ConfigError('v3 trap: "auth" must be specified') if not 'authProtocol' in trap.keys(): raise ConfigError('v3 trap: "protocol" must be specified') if not 'privPassword' and 'privMasterKey' in trap.keys(): raise ConfigError('v3 trap: "user" must be specified') if 'v3_users' in snmp.keys(): for user in snmp['v3_users']: # # Group must exist prior to mapping it into a group # seclevel will be extracted from group # if 'group' not in user or user['group'] == '': username = user['name'] raise ConfigError(f'Group membership required for user "{username}"!') if user['group']: error = True if 'v3_groups' in snmp.keys(): for group in snmp['v3_groups']: if group['name'] == user['group']: seclevel = group['seclevel'] error = False if error: raise ConfigError('You must create group "{0}" first'.format(user['group'])) # Depending on the configured security level the user has to provide additional info if (not user['authPassword'] and not user['authMasterKey']): raise ConfigError('Must specify encrypted-password or plaintext-key for user auth') if user['privPassword'] == '' and user['privMasterKey'] == '': raise ConfigError('Must specify encrypted-password or plaintext-key for user privacy') if user['mode'] == '': raise ConfigError('Must specify user mode ro/rw') if 'v3_views' in snmp.keys(): for view in snmp['v3_views']: if not view['oids']: raise ConfigError('Must configure an oid') return None def generate(snmp): # # As we are manipulating the snmpd user database we have to stop it first! # This is even save if service is going to be removed call('systemctl stop snmpd.service') config_files = [config_file_client, config_file_daemon, config_file_access, config_file_user, systemd_override] for file in config_files: rmfile(file) if not snmp: return None if 'v3_users' in snmp.keys(): # net-snmp is now regenerating the configuration file in the background # thus we need to re-open and re-read the file as the content changed. # After that we can no read the encrypted password from the config and # replace the CLI plaintext password with its encrypted version. os.environ["vyos_libexec_dir"] = "/usr/libexec/vyos" for user in snmp['v3_users']: if user['authProtocol'] == 'sha': hash = plaintext_to_sha1 else: hash = plaintext_to_md5 if user['authPassword']: user['authMasterKey'] = hash(user['authPassword'], snmp['v3_engineid']) user['authPassword'] = '' call('/opt/vyatta/sbin/my_set service snmp v3 user "{name}" auth encrypted-password "{authMasterKey}" > /dev/null'.format(**user)) call('/opt/vyatta/sbin/my_delete service snmp v3 user "{name}" auth plaintext-password > /dev/null'.format(**user)) if user['privPassword']: user['privMasterKey'] = hash(user['privPassword'], snmp['v3_engineid']) user['privPassword'] = '' call('/opt/vyatta/sbin/my_set service snmp v3 user "{name}" privacy encrypted-password "{privMasterKey}" > /dev/null'.format(**user)) call('/opt/vyatta/sbin/my_delete service snmp v3 user "{name}" privacy plaintext-password > /dev/null'.format(**user)) # Write client config file render(config_file_client, 'snmp/etc.snmp.conf.tmpl', snmp) # Write server config file render(config_file_daemon, 'snmp/etc.snmpd.conf.tmpl', snmp) # Write access rights config file render(config_file_access, 'snmp/usr.snmpd.conf.tmpl', snmp) # Write access rights config file render(config_file_user, 'snmp/var.snmpd.conf.tmpl', snmp) # Write daemon configuration file render(systemd_override, 'snmp/override.conf.tmpl', snmp) return None def apply(snmp): # Always reload systemd manager configuration call('systemctl daemon-reload') if not snmp: return None # start SNMP daemon call('systemctl restart snmpd.service') # Enable AgentX in FRR call('vtysh -c "configure terminal" -c "agentx" >/dev/null') return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1)