diff options
Diffstat (limited to 'src')
-rwxr-xr-x | src/conf_mode/snmp.py | 709 |
1 files changed, 709 insertions, 0 deletions
diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py new file mode 100755 index 000000000..429181550 --- /dev/null +++ b/src/conf_mode/snmp.py @@ -0,0 +1,709 @@ +#!/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 <http://www.gnu.org/licenses/>. +# +# + +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) |