diff options
-rw-r--r-- | interface-definitions/snmp.xml | 55 | ||||
-rwxr-xr-x | src/conf_mode/snmp.py | 412 |
2 files changed, 440 insertions, 27 deletions
diff --git a/interface-definitions/snmp.xml b/interface-definitions/snmp.xml index 698fb34a1..4bfb61576 100644 --- a/interface-definitions/snmp.xml +++ b/interface-definitions/snmp.xml @@ -20,7 +20,15 @@ <children> <leafNode name="authorization"> <properties> - <help>Authorization type (rw or ro) (default: 'ro')</help> + <help>Authorization type (default: 'ro')</help> + <valueHelp> + <format>ro</format> + <description>read only</description> + </valueHelp> + <valueHelp> + <format>rw</format> + <description>read write</description> + </valueHelp> <constraint> <regex>(ro|rw)</regex> </constraint> @@ -202,10 +210,17 @@ <leafNode name="seclevel"> <properties> <help>Defines security level</help> + <valueHelp> + <format>auth</format> + <description>Requests must be authenticated</description> + </valueHelp> + <valueHelp> + <format>priv</format> + <description>Enforce use of encryption</description> + </valueHelp> <constraint> <regex>(auth|priv)</regex> </constraint> - <multi/> </properties> </leafNode> <leafNode name="view"> @@ -538,6 +553,42 @@ </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> + </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> </children> diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index 227c00335..ef7ac12fc 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -20,14 +20,20 @@ import sys import os import jinja2 -import vyos.version import ipaddress +import random +import binascii +import os + +import vyos.version from vyos.config import Config from vyos import ConfigError 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' # SNMPS template - be careful if you edit the template. client_config_tmpl = """ @@ -39,38 +45,53 @@ clientaddr {{ trap_source }} """ # SNMPS template - be careful if you edit the template. +access_config_tmpl = """ +### Autogenerated by snmp.py ### +{% if v3_users %} +{% for u in v3_users %} +{{ u.mode }}user {{ u.name }} +{% endfor %} +{% endif -%} +rwuser {{ vyos_user }} + +""" + +# SNMPS template - be careful if you edit the template. +user_config_tmpl = """ +### Autogenerated by snmp.py ### +# user +{% if v3_users %} +{% for u in v3_users %} +createUser {{ u.name }} {{ u.authProtocol | upper }} {% if u.authPassword %} "{{ u.authPassword }}" {% elif u.authMasterKey %} "{{ u.authMasterKey }}"{% endif %} {{ u.privProtocol | upper }}{% if u.privPassword %} {{ u.privPassword }}{% elif u.privMasterKey %} {{ u.privMasterKey }}{% endif %} +{% endfor %} +{% endif %} + +createUser {{ vyos_user }} MD5 "{{ vyos_user_pass }}" DES +""" + +# SNMPS template - be careful if you edit the template. daemon_config_tmpl = """ ### Autogenerated by snmp.py ### + # non configurable defaults sysObjectID 1.3.6.1.4.1.44641 sysServices 14 -# maybe needed by lldpd master agentx agentXPerms 0755 0755 -# add hook to read IF-MIB::ifAlias from sysfs pass .1.3.6.1.2.1.31.1.1.1.18 /opt/vyatta/sbin/if-mib-alias -# ospfd +smuxpeer .1.3.6.1.2.1.83 +smuxpeer .1.3.6.1.2.1.157 smuxpeer .1.3.6.1.4.1.3317.1.2.2 -# bgpd -smuxpeer .1.3.6.1.4.1.3317.1.2.5 -# ripd smuxpeer .1.3.6.1.4.1.3317.1.2.3 -# mribd -smuxpeer .1.3.6.1.4.1.3317.1.2.9 -# mribd -smuxpeer .1.3.6.1.2.1.83 -# pimd +smuxpeer .1.3.6.1.4.1.3317.1.2.5 smuxpeer .1.3.6.1.4.1.3317.1.2.8 -# pimd -smuxpeer .1.3.6.1.2.1.157 +smuxpeer .1.3.6.1.4.1.3317.1.2.9 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 -# create an internal snmpv3 user of the form 'vyattaxxxxxxxxxxxxxxxx' -## TODO!!!! -iquerySecName vyatta3392514e4189da84 +iquerySecName {{ vyos_user }} # Modified from the default linkUpDownNotification # to include more OIDs and poll more frequently @@ -125,16 +146,75 @@ smuxpeer {{ sp }} {% endfor %} {% endif %} +{% if trap_targets -%} +# if there is a problem - tell someone! +{% for t in trap_targets %} +trap2sink {{ t.target }}{% if t.port -%}:{{ t.port }}{% endif %} {{ t.community }} +{% endfor %} +{% endif %} + +# +# SNMPv3 stuff goes here +# +{% if v3_enabled %} + +# views +{% if v3_views -%} +{% for v in v3_views %} +{% for oid in v.oids %} +view {{ v.name }} included .{{ oid.oid }} +{% endfor %} +{% endfor %} +{% endif %} + +# access +# context sec.model sec.level match read write notif +{% if v3_groups -%} +{% for g in v3_groups %} +access {{ g.name }} "" usm {{ g.seclevel }} exact {{ g.view }} none none +access {{ g.name }} "" tsm {{ g.seclevel }} exact {{ g.view }} none none +{% endfor -%} +{% endif %} + +# trap-target +{% if v3_traps -%} +{% for t in v3_traps %} +trapsess -v 3 -e {{ t.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 -%} +{% endif %} + +# group +{% if v3_users -%} +{% for u in v3_users %} +group {{ u.group }} usm {{ u.name }} +group {{ u.group }} tsm {{ u.name }} +{% endfor %} +{% endif %} + +{% endif %} + """ default_config_data = { 'listen_on': [], + 'communities': [], + 'smux_peers': [], 'location' : '', 'description' : '', 'contact' : '', - 'communities': [], 'trap_source': '', - 'smux_peers': [] + 'trap_targets': [], + 'vyos_user': '', + 'vyos_user_pass': '', + 'version': '999', + 'v3_enabled': 'False', + 'v3_engineid': '', + 'v3_groups': [], + 'v3_traps': [], + 'v3_tsm_key': '', + 'v3_tsm_port': '10161', + 'v3_users': [], + 'v3_views': [] } def get_config(): @@ -146,7 +226,12 @@ def get_config(): conf.set_level('service snmp') version_data = vyos.version.get_version_data() - snmp.setdefault('version', version_data['version']) + snmp['version'] = version_data['version'] + + # create an internal snmpv3 user of the form 'vyattaxxxxxxxxxxxxxxxx' + # os.urandom(8) returns 8 bytes of random data + snmp['vyos_user'] = 'vyatta' + binascii.hexlify(os.urandom(8)).decode('utf-8') + snmp['vyos_user_pass'] = binascii.hexlify(os.urandom(16)).decode('utf-8') if conf.exists('community'): for name in conf.list_nodes('community'): @@ -197,35 +282,312 @@ def get_config(): 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) + + ######################################################################### + # ____ _ _ __ __ ____ _____ # + # / ___|| \ | | \/ | _ \ __ _|___ / # + # \___ \| \| | |\/| | |_) | \ \ / / |_ \ # + # ___) | |\ | | | | __/ \ 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, + 'engineID': '', + 'secName': '', + 'authProtocol': 'md5', + 'authPassword': '', + 'authMasterKey': '', + 'privProtocol': 'des', + 'privPassword': '', + 'privMasterKey': '', + 'ipProto': 'udp', + 'ipPort': '162', + 'type': '', + 'secLevel': 'noAuthNoPriv' + } + + if conf.exists('v3 trap-target {0} engineid'.format(trap)): + # Set the context engineID used for SNMPv3 REQUEST messages scopedPdu. + # If not specified, this will default to the authoritative engineID. + trap_cfg['engineID'] = conf.return_value('v3 trap-target {0} engineid'.format(trap)) + + 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-key'.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-key'.format(trap)) + + if conf.exists('v3 trap-target {0} auth encrypted-key'.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-key'.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-key'.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-key'.format(trap)) + + if conf.exists('v3 trap-target {0} privacy encrypted-key'.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-key'.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)): + tmp = conf.return_value('v3 trap-target {0} type'.format(trap)) + # see http://www.net-snmp.org/docs/man/snmpd.conf.html + # The option -Ci can be used (with -v2c or -v3) to generate an INFORM + # notification rather than an unacknowledged TRAP. + if tmp == 'inform': + trap_cfg['type'] = ' -Ci' + + # 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 tsm' + # + if conf.exists('v3 tsm'): + if conf.exists('v3 tsm local-key'): + snmp['v3_tsm_key'] = conf.return_value('v3 tsm local-key') + + if conf.exists('v3 tsm port'): + snmp['v3_tsm_port'] = conf.return_value('v3 tsm port') + + # + # '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', + 'engineID': '', + 'group': '', + 'mode': 'ro', + 'privMasterKey': '', + 'privPassword': '', + 'privTsmKey': '', + 'privProtocol': 'des' + } + + # + # v3 user {0} auth + # + if conf.exists('v3 user {0} auth encrypted-key'.format(user)): + user_cfg['authMasterKey'] = conf.return_value('v3 user {0} auth encrypted-key'.format(user)) + + if conf.exists('v3 user {0} auth plaintext-key'.format(user)): + user_cfg['authPassword'] = conf.return_value('v3 user {0} auth plaintext-key'.format(user)) + + if conf.exists('v3 user {0} auth type'.format(user)): + user_cfg['authProtocol'] = conf.return_value('v3 user {0} auth type'.format(user)) + + # + # v3 user {0} engineid + # + if conf.exists('v3 user {0} engineid'.format(user)): + user_cfg['engineID'] = conf.return_value('v3 user {0} engineid'.format(user)) + + # + # 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-key'.format(user)): + user_cfg['privMasterKey'] = conf.return_value('v3 user {0} privacy encrypted-key'.format(user)) + + if conf.exists('v3 user {0} privacy plaintext-key'.format(user)): + user_cfg['privPassword'] = conf.return_value('v3 user {0} privacy plaintext-key'.format(user)) + + if conf.exists('v3 user {0} privacy tsm-key'.format(user)): + user_cfg['privTsmKey'] = conf.return_value('v3 user {0} privacy tsm-key'.format(user)) + + if conf.exists('v3 user {0} privacy type'.format(user)): + user_cfg['privProtocol'] = conf.return_value('v3 user {0} privacy type'.format(user)) + + 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 + } + view_cfg['oids'].append(oid_cfg) + snmp['v3_views'].append(view_cfg) + return snmp def verify(snmp): + if snmp is None: + return None + + if 'v3_traps' in snmp.keys(): + for trap in snmp['v3_traps']: + if trap['authPassword'] and trap['authMasterKey']: + raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for trap auth') + if trap['privPassword'] and trap['privMasterKey']: + raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for trap privacy') + + if 'v3_users' in snmp.keys(): + for user in snmp['v3_users']: + if user['authPassword'] and user['authMasterKey']: + raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for user auth') + if user['privPassword'] and user['privMasterKey']: + raise ConfigError('Can not mix "encrypted-key" and "plaintext-key" for user privacy') + + if 'v3_group' in snmp.keys(): + for group in snmp['v3_group']: + if not group['view']: + raise ConfigError('You must create a view first') + 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 + os.system("sudo systemctl stop snmpd.service") + os.unlink(config_file_client) + os.unlink(config_file_daemon) + os.unlink(config_file_access) + os.unlink(config_file_user) + if snmp is None: return None + # Write client config file tmpl = jinja2.Template(client_config_tmpl, trim_blocks=True) config_text = tmpl.render(snmp) with open(config_file_client, 'w') as f: f.write(config_text) + # Write server config file tmpl = jinja2.Template(daemon_config_tmpl, trim_blocks=True) config_text = tmpl.render(snmp) with open(config_file_daemon, 'w') as f: f.write(config_text) + # Write access rights config file + tmpl = jinja2.Template(access_config_tmpl, trim_blocks=True) + config_text = tmpl.render(snmp) + with open(config_file_access, 'w') as f: + f.write(config_text) + + # Write access rights config file + tmpl = jinja2.Template(user_config_tmpl, trim_blocks=True) + config_text = tmpl.render(snmp) + with open(config_file_user, 'w') as f: + f.write(config_text) + return None def apply(snmp): if snmp is not None: os.system("sudo systemctl restart snmpd.service") - else: - # SNMP is removed in the commit - os.system("sudo systemctl stop snmpd.service") - os.unlink(config_file_client) - os.unlink(config_file_daemon) return None |