From 4464293e1552084f13d5a97960a4df04bc6b9916 Mon Sep 17 00:00:00 2001 From: Volker Theile Date: Mon, 13 May 2019 17:48:56 +0200 Subject: [PATCH] Add support for global configuration from /etc/prometheus_webhook_snmp.conf. - Default values are not shown anymore in the app help message - Config settings are set according this priority chain: default values, global config, CLI args. This means, args passed via CLI have the highest priority. - The config file is written in YAML, as all Prometheus config files. Signed-off-by: Volker Theile --- README.md | 14 +++++ debian/changelog | 6 ++ debian/control | 3 +- debian/prometheus-webhook-snmp.default | 1 + prometheus-webhook-snmp | 28 ++-------- prometheus-webhook-snmp.service | 3 +- prometheus-webhook-snmp.spec | 6 +- prometheus_webhook_snmp/utils.py | 76 +++++++++++++++++++++++--- requirements.txt | 11 ++-- tests/test_misc.py | 32 ++++++++++- 10 files changed, 140 insertions(+), 40 deletions(-) create mode 100644 debian/prometheus-webhook-snmp.default diff --git a/README.md b/README.md index b47f70f..779fcb6 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ If you want to send a test SNMP trap, then simply execute the following command. $ ./prometheus-webhook-snmp test # Command line parameters +Command line parameters have precedence over global configuration settings. ## Global @@ -94,6 +95,19 @@ scrape_configs: - targets: ['localhost:9099'] ``` +# Global configuration file +The Prometheus Alertmanager receiver can be configured via configuration file, too. The file ``/etc/prometheus-webhook-snmp.conf`` is written in YAML format. Parameters in this file have precedence over default configuration settings. Please replace hyphens in parameter names with underscores. + +Example configuration: + +```yaml +debug: True +snmp_retries: 1 +snmp_community: private +host: promalertmgr.foo.com +port: 9101 +``` + # SNMP schema ## Traps diff --git a/debian/changelog b/debian/changelog index a67e3e1..b4c8014 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +prometheus-webhook-snmp (1.1-1) stable; urgency=low + + * Add support for global configuration file. + + -- Volker Theile Mon, 13 May 2019 16:01:19 +0200 + prometheus-webhook-snmp (1.0-1) stable; urgency=low * Initial Debian packaging. diff --git a/debian/control b/debian/control index 065e008..cafe29c 100644 --- a/debian/control +++ b/debian/control @@ -12,7 +12,8 @@ Depends: ${misc:Depends}, python3-click, python3-cherrypy3, python3-dateutil, - python3-pysnmp4 + python3-pysnmp4, + python3-yaml Priority: optional Description: Prometheus Alertmanager receiver for SNMP traps prometheus-webhook-snmp is a Prometheus Alertmanager receiver that diff --git a/debian/prometheus-webhook-snmp.default b/debian/prometheus-webhook-snmp.default new file mode 100644 index 0000000..ee366ca --- /dev/null +++ b/debian/prometheus-webhook-snmp.default @@ -0,0 +1 @@ +PROMETHEUS_WEBHOOK_SNMP_OPTIONS="" diff --git a/prometheus-webhook-snmp b/prometheus-webhook-snmp index 61898ab..5828a88 100755 --- a/prometheus-webhook-snmp +++ b/prometheus-webhook-snmp @@ -8,7 +8,8 @@ import click from prometheus_webhook_snmp import utils -__version__ = "1.0" +__version__ = "1.1" + pass_context = click.make_pass_decorator(utils.Context, ensure=True) @@ -19,42 +20,27 @@ pass_context = click.make_pass_decorator(utils.Context, ensure=True) is_flag=True, help='Enable debug output.') @click.option('--snmp-host', - default='localhost', - show_default=True, help='The host (IP or FQDN) of the SNMP trap receiver.') @click.option('--snmp-port', - default=162, - show_default=True, help='The port of the SNMP trap receiver.') @click.option('--snmp-community', - default='public', - show_default=True, help='The SNMP community string.') @click.option('--snmp-retries', - default=5, - show_default=True, help='Maximum number of request retries.') @click.option('--snmp-timeout', - default=1, - show_default=True, help='Response timeout in seconds.') @click.option('--alert-oid-label', - default='oid', - show_default=True, help='The label where to find the OID.') @click.option('--trap-oid-prefix', - default='1.3.6.1.4.1.50495.15', - show_default=True, help='The OID prefix for trap variable bindings.') @click.option('--trap-default-oid', - default='1.3.6.1.4.1.50495.15.1.2.1', - show_default=True, help='The trap OID if none is found in the Prometheus alert labels.') @click.version_option(__version__, message="%(version)s") @pass_context def cli(ctx, debug, snmp_host, snmp_port, snmp_community, snmp_retries, snmp_timeout, alert_oid_label, trap_oid_prefix, trap_default_oid): - ctx.config['debug'] = debug + ctx.config.load(click.get_current_context().info_name) + ctx.config['debug'] = True if debug else None ctx.config['snmp_host'] = snmp_host ctx.config['snmp_port'] = snmp_port ctx.config['snmp_community'] = snmp_community @@ -72,12 +58,8 @@ def cli(ctx, debug, snmp_host, snmp_port, snmp_community, snmp_retries, @cli.command(name='run', help='Start the HTTP server.') @click.option('--host', - default='0.0.0.0', - show_default=True, help='Host to use.') @click.option('--port', - default=9099, - show_default=True, help='Port to listen for Prometheus Alertmanager notifications.') @click.option('--metrics', is_flag=True, @@ -86,7 +68,7 @@ def cli(ctx, debug, snmp_host, snmp_port, snmp_community, snmp_retries, def run(ctx, host, port, metrics): ctx.config['host'] = host ctx.config['port'] = port - ctx.config['metrics'] = metrics + ctx.config['metrics'] = True if metrics else None utils.run_http_server(ctx) sys.exit(0) diff --git a/prometheus-webhook-snmp.service b/prometheus-webhook-snmp.service index 9fca627..45317d2 100644 --- a/prometheus-webhook-snmp.service +++ b/prometheus-webhook-snmp.service @@ -3,7 +3,8 @@ Description=Prometheus Alertmanager receiver for SNMP traps After=network.target [Service] -ExecStart=/usr/bin/prometheus-webhook-snmp run +EnvironmentFile=-/etc/default/prometheus-webhook-snmp +ExecStart=/usr/bin/prometheus-webhook-snmp $PROMETHEUS_WEBHOOK_SNMP_OPTIONS run ExecReload=/bin/kill -HUP $MAINPID Restart=on-failure diff --git a/prometheus-webhook-snmp.spec b/prometheus-webhook-snmp.spec index b0912a6..f96226f 100644 --- a/prometheus-webhook-snmp.spec +++ b/prometheus-webhook-snmp.spec @@ -15,7 +15,7 @@ # Please submit bugfixes or comments via http://bugs.opensuse.org/ Name: prometheus-webhook-snmp -Version: 1.0 +Version: 1.1 Release: 0 Summary: Prometheus Alertmanager receiver for SNMP traps License: GPL-3.0 @@ -34,11 +34,13 @@ Requires: python3-prometheus-client Requires: python3-click %if 0%{?suse_version} Requires: python3-CherryPy +Requires: python3-PyYAML %else Requires: python3-cherrypy +Requires: python3-yaml %endif Requires: python3-dateutil -Requires: python3-pysnmp +Requires: python3-pysnmp >= 4.3.2 %description prometheus-webhook-snmp is a Prometheus Alertmanager receiver that diff --git a/prometheus_webhook_snmp/utils.py b/prometheus_webhook_snmp/utils.py index 0ca036a..a4e9a99 100755 --- a/prometheus_webhook_snmp/utils.py +++ b/prometheus_webhook_snmp/utils.py @@ -6,6 +6,7 @@ import cherrypy import dateutil.parser import prometheus_client +import yaml from pysnmp import hlapi @@ -159,6 +160,72 @@ def run_http_server(ctx): cherrypy.quickstart(Root(ctx), config=get_http_server_config()) +class Config(dict): + def __init__(self): + super().__init__() + self.reset() + + @staticmethod + def defaults(): + """ + Get the default configuration values. + :return: Returns a dictionary containing the default values. + :rtype: dict + """ + return { + 'debug': False, + 'snmp_host': 'localhost', + 'snmp_port': 162, + 'snmp_community': 'public', + 'snmp_retries': 5, + 'snmp_timeout': 1, + 'alert_oid_label': 'oid', + 'trap_oid_prefix': '1.3.6.1.4.1.50495.15', + 'trap_default_oid': '1.3.6.1.4.1.50495.15.1.2.1', + 'host': '0.0.0.0', + 'port': 9099, + 'metrics': False + } + + def reset(self, name=None): + """ + Reset to default values. If a name is specified, only the named + configuration setting is reset to default. + :param name: The name of the configuration setting. Defaults to 'None'. + :type name: str + """ + if name is None: + self.clear() + self.update(Config.defaults()) + else: + self[name] = Config.defaults()[name] + + def load(self, prog_name): + """ + Load a configuration file from disk. + :param prog_name: The name of the program. + :type prog_name: str + """ + file_name = '/etc/{}.conf'.format(prog_name) + try: + with open(file_name, 'r') as stream: + config = yaml.safe_load(stream) + self.update(config) + except (IOError, FileNotFoundError): + pass + + def __setitem__(self, key, value): + """ + Set self[key] to value. Ignore 'None' values. + :param key: The name of the key. + :type key: str + :param value: The value of the key. + :type value: bool|int|str + """ + if value is not None: + super().__setitem__(key, value) + + class Telemetry: def __init__(self): self.metrics = { @@ -183,14 +250,7 @@ def generate(self): class Context: def __init__(self): - self.config = { - key: None - for key in [ - 'host', 'port', 'metrics', 'snmp_host', 'snmp_port', - 'snmp_community', 'snmp_retries', 'snmp_timeout', - 'alert_oid_label', 'trap_default_oid', 'trap_oid_prefix' - ] - } + self.config = Config() self.telemetry = Telemetry() diff --git a/requirements.txt b/requirements.txt index 440b07b..6ba80eb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,10 @@ CherryPy==18.1.1 Click==7.0 -pysnmp==4.4.9 -python-dateutil==2.8.0 -pytest==4.4.0 -pylint==2.3.1 +PyYAML==5.1 +mock==3.0.5 prometheus_client==0.6.0 +pyfakefs==3.5.8 +pylint==2.3.1 +pysnmp>=4.3.2 +pytest==4.4.0 +python-dateutil==2.8.0 diff --git a/tests/test_misc.py b/tests/test_misc.py index ce2276e..b225f6b 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -1,6 +1,9 @@ import unittest +import mock -from prometheus_webhook_snmp.utils import parse_notification +from pyfakefs import fake_filesystem + +from prometheus_webhook_snmp.utils import parse_notification, Config NOTIFICATION_FIRING = { 'receiver': 'test-01', @@ -110,3 +113,30 @@ def test_parse_notification_no_oid(self): self.assertEqual(trap_data['labels'], {'foo': 'abc', 'bar': 123}) self.assertEqual(trap_data['timestamp'], 1554110387) self.assertIsInstance(trap_data['rawdata'], dict) + + +class ConfigTestCase(unittest.TestCase): + fs = fake_filesystem.FakeFilesystem() + f_open = fake_filesystem.FakeFileOpen(fs) + + def test_defaults(self): + self.assertIsInstance(Config.defaults(), dict) + + def test_reset(self): + config = Config() + config['snmp_community'] = 'private' + config.reset('snmp_community') + self.assertEqual(config['snmp_community'], Config.defaults()['snmp_community']) + + def test_reset_all(self): + config = Config() + config['foo'] = 'bar' + config.reset() + self.assertDictEqual(config, Config.defaults()) + + @mock.patch('builtins.open', new=f_open) + def test_load(self): + self.fs.create_file('/etc/abc.conf', contents='''foo: bar\n''') + config = Config() + config.load('abc') + self.assertEqual(config['foo'], 'bar')