summaryrefslogtreecommitdiff
path: root/src/conf_mode/snmp.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode/snmp.py')
-rwxr-xr-xsrc/conf_mode/snmp.py581
1 files changed, 581 insertions, 0 deletions
diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py
new file mode 100755
index 000000000..e9806ef47
--- /dev/null
+++ b/src/conf_mode/snmp.py
@@ -0,0 +1,581 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-2020 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, plaintext_to_sha1, random
+from vyos.template import render
+from vyos.util import call
+from vyos.validate import is_ipv4, is_addr_assigned
+from vyos.version import get_version_data
+from vyos import ConfigError, 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' : '',
+ '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://phabricator.vyos.net/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('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
+ }
+ 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]
+
+ if is_ipv4(addr):
+ # example: udp:127.0.0.1:161
+ listen = 'udp:' + addr + ':' + port
+ elif snmp['ipv6_enabled']:
+ # example: udp6:[::1]:161
+ listen = 'udp6:' + '[' + 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):
+ snmp['listen_on'].append(listen)
+ else:
+ print('WARNING: SNMP listen address {0} not configured!'.format(addr))
+
+ 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 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)