#!/usr/bin/env python3 # # Copyright (C) 2018 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 . # # import sys import os import stat import pwd import jinja2 import ipaddress import random import binascii import re 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 = """ ### Autogenerated by snmp.py ### {% if trap_source -%} clientaddr {{ trap_source }} {% endif %} """ # 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 master agentx agentXPerms 0755 0755 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 smuxpeer .1.3.6.1.4.1.3317.1.2.2 smuxpeer .1.3.6.1.4.1.3317.1.2.3 smuxpeer .1.3.6.1.4.1.3317.1.2.5 smuxpeer .1.3.6.1.4.1.3317.1.2.8 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 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 ######################## # configurable section # ######################## {% if v3_tsm_key %} [snmp] localCert {{ v3_tsm_key }} {% endif %} # Default system description is VyOS version sysDescr VyOS {{ version }} {% if description -%} # Description SysDescr {{ description }} {% endif %} # Listen agentaddress unix:/run/snmpd.socket{% for li in listen_on %},{{ li }}{% endfor %}{% if v3_tsm_key %},tlstcp:{{ v3_tsm_port }},dtlsudp::{{ v3_tsm_port }}{% endif %} # SNMP communities {% if communities -%} {% for c in communities %} {% if c.network -%} {% for network in c.network %} {{ c.authorization }}community {{ c.name }} {{ network }} {% endfor %} {% else %} {{ c.authorization }}community {{ c.name }} {% endif %} {% endfor %} {% endif %} {% 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 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 {{ '-Ci' if t.type == 'inform' }} -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' : '', 'trap_source': '', '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 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: conf.set_level('service snmp') version_data = vyos.version.get_version_data() 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'): community = { 'name': name, 'authorization': 'ro', 'network': [] } if conf.exists('community {0} authorization'.format(name)): community['authorization'] = conf.return_value('community {0} authorization'.format(name)) if conf.exists('community {0} network'.format(name)): community['network'] = conf.return_values('community {0} network'.format(name)) 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'): listen = '' port = '161' if conf.exists('listen-address {0} port'.format(addr)): port = conf.return_value('listen-address {0} port'.format(addr)) if ipaddress.ip_address(addr).version == 4: # udp:127.0.0.1:161 listen = 'udp:' + addr + ':' + port elif ipaddress.ip_address(addr).version == 6: # udp6:[::1]:161 listen = 'udp6:' + '[' + addr + ']' + ':' + port else: raise ConfigError('Invalid IP address version') snmp['listen_on'].append(listen) if conf.exists('location'): snmp['location'] = conf.return_value('location') 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) ######################################################################### # ____ _ _ __ __ ____ _____ # # / ___|| \ | | \/ | _ \ __ _|___ / # # \___ \| \| | |\/| | |_) | \ \ / / |_ \ # # ___) | |\ | | | | __/ \ 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)): 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 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': '', 'engineID': '', 'group': '', 'mode': 'ro', 'privMasterKey': '', 'privPassword': '', 'privTsmKey': '', 'privProtocol': '' } # # 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 # bail out early if SNMP v3 is not configured if not snmp['v3_enabled']: return None tsmKeyPattern = re.compile('^[0-9A-F]{2}(:[0-9A-F]{2}){19}$', re.IGNORECASE) if snmp['v3_tsm_key']: if not tsmKeyPattern.match(snmp['v3_tsm_key']): if not os.path.isfile('/etc/snmp/tls/certs/' + snmp['v3_tsm_key']): if not os.path.isfile('/config/snmp/tls/certs/' + snmp['v3_tsm_key']): raise ConfigError('TSM key must be fingerprint or filename in "/config/snmp/tls/certs/" folder') 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-key/plaintext-key for trap auth') if trap['authPassword'] == '' and trap['authMasterKey'] == '': raise ConfigError('Must specify encrypted-key or plaintext-key for trap auth') if trap['privPassword'] and trap['privMasterKey']: raise ConfigError('Must specify only one of encrypted-key/plaintext-key for trap privacy') if trap['privPassword'] == '' and trap['privMasterKey'] == '': raise ConfigError('Must specify encrypted-key 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 'type' in trap.keys(): if trap['type'] == 'trap' and trap['engineID'] == '': raise ConfigError('must specify engineid if type is "trap"') else: raise ConfigError('"type" must be specified') 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 user['privPassword'] == '' and user['privMasterKey'] == '': raise ConfigError('Must specify encrypted-key or plaintext-key for user privacy') if user['authPassword'] == '' and user['authMasterKey'] == '' and user['privTsmKey'] == '': raise ConfigError('Must specify auth or tsm-key for user auth') if user['privProtocol'] == '': raise ConfigError('Must specify privacy type') if user['mode'] == '': raise ConfigError('Must specify user mode ro/rw') if user['privTsmKey']: if not tsmKeyPattern.match(snmp['v3_tsm_key']): if not os.path.isfile('/etc/snmp/tls/certs/' + snmp['v3_tsm_key']): if not os.path.isfile('/config/snmp/tls/certs/' + snmp['v3_tsm_key']): raise ConfigError('User TSM key must be fingerprint or filename in "/config/snmp/tls/certs/" folder') if user['group']: # # Group must exist prior to mapping it into a group # error = True if 'v3_groups' in snmp.keys(): for group in snmp['v3_groups']: if group['name'] == user['group']: error = False if error: raise ConfigError('You must create group "{0}" first'.format(user['group'])) 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 os.system("sudo systemctl stop snmpd.service") rmfile(config_file_client) rmfile(config_file_daemon) rmfile(config_file_access) rmfile(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: if not os.path.exists('/config/snmp/tls'): os.makedirs('/config/snmp/tls') os.chmod('/config/snmp/tls', stat.S_IWUSR | stat.S_IRUSR) # get uid for user 'snmp' snmp_uid = pwd.getpwnam('snmp').pw_uid os.chown('/config/snmp/tls', snmp_uid, -1) # start SNMP daemon os.system("sudo systemctl restart snmpd.service") return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) sys.exit(1)