Compare commits

...

10 commits

Author SHA1 Message Date
4f854bca8b
fix: Remove $SERVICEDESC$ from host notification
Signed-off-by: Julien Riou <julien@riou.xyz>
2022-05-09 09:57:37 +02:00
df5296af42
Escape '=' to produce valid markdown (#1)
Signed-off-by: Julien Riou <julien@riou.xyz>
2021-03-25 10:39:11 +01:00
0cd101954c
Remove --service-desc argument for hosts
Signed-off-by: Julien Riou <julien@riou.xyz>
2020-11-26 15:35:59 +01:00
7cdc46954e
Add requirements.txt file
Signed-off-by: Julien Riou <julien@riou.xyz>
2020-11-26 15:32:42 +01:00
ca3adb00ce
Add a contribution/lint guide
Signed-off-by: Julien Riou <julien@riou.xyz>
2020-11-26 15:25:09 +01:00
7fa647c953
Update README with template variables
Signed-off-by: Julien Riou <julien@riou.xyz>
2020-11-26 15:09:28 +01:00
2b5a263d48
Add configuration file to README
Signed-off-by: Julien Riou <julien@riou.xyz>
2020-11-26 14:58:40 +01:00
17f0948aeb
Add configuration validation with jsonschema
Signed-off-by: Julien Riou <julien@riou.xyz>
2020-11-26 14:50:59 +01:00
a7c9f1455e
Factorize payload generation
Signed-off-by: Julien Riou <julien@riou.xyz>
2020-11-26 14:19:54 +01:00
a0507d546a
Update typo in README
Signed-off-by: Julien Riou <julien@riou.xyz>
2020-11-26 13:02:08 +01:00
9 changed files with 154 additions and 45 deletions

View file

@ -11,6 +11,8 @@ repos:
args: ['--remove'] args: ['--remove']
- id: requirements-txt-fixer - id: requirements-txt-fixer
- id: trailing-whitespace - id: trailing-whitespace
- id: check-json
- id: check-yaml
- repo: https://gitlab.com/pycqa/flake8 - repo: https://gitlab.com/pycqa/flake8
rev: master rev: master
hooks: hooks:

5
Dockerfile Normal file
View file

@ -0,0 +1,5 @@
FROM python:3.8-slim
RUN apt-get update \
&& apt-get install -y git \
&& apt-get autoclean \
&& pip install pre-commit

View file

@ -15,19 +15,25 @@ Clone the repository:
git clone https://github.com/jouir/notify-by-telegram.git /opt/notify-by-telegram git clone https://github.com/jouir/notify-by-telegram.git /opt/notify-by-telegram
``` ```
Install dependencies using the package manager: Install dependencies using pip:
``` ```
sudo apt install python3-jinja2 python3-requests pip install -r requirements.txt
```
Or via the package manager:
```
sudo apt install python3-jinja2 python3-requests python3-jsonschema
``` ```
## Configuration ## Configuration
Copy and update the configuration file example: Copy and update the configuration file example:
``` ```
cp -p config.json.example telegram.json cp -p config.example.json telegram.json
vim telegram.json vim telegram.json
sudo mv telegram.json /etc/nagios4/telegram.json sudo mv telegram.json /etc/nagios4/telegram.json
sudo chmod 640 root:nagios /etc/nagios4/telegram.json sudo chown root:nagios /etc/nagios4/telegram.json
sudo chmod 640 /etc/nagios4/telegram.json
``` ```
Ensure Nagios reads the configuration file: Ensure Nagios reads the configuration file:
@ -40,7 +46,16 @@ Then reload service:
systemctl reload nagios4 systemctl reload nagios4
``` ```
## Logs ## Configuration file
Format used is JSON with the following keys:
* `chat_id`: where to send message on Telegram
* `auth_key`: key used to authenticate on Telegram
* `host_template` (optional): path to Markdown template file used for sending host notifications
* `service_template` (optional): path to Markdown template file used for sending service notifications
## Logging
Errors logs can be set with the `--logfile` argument. Errors logs can be set with the `--logfile` argument.
@ -70,3 +85,37 @@ They can be overriden in the configuration file:
``` ```
Both options are optional. Both options are optional.
### Host variables
Variables replaced in the host template:
* `notification_type` (= `$NOTIFICATIONTYPE$`)
* `host_name` (= `$HOSTNAME$`)
* `host_state` (= `$HOSTSTATE$`)
* `host_address` (= `$HOSTADDRESS$`)
* `host_output` (= `$HOSTOUTPUT$`)
* `long_date_time` (= `$LONGDATETIME$`)
### Service variables
Variables replaced in the service template:
* `notification_type` (= `$NOTIFICATIONTYPE$`)
* `service_desc` (= `$SERVICEDESC$`)
* `host_alias` (= `$HOSTALIAS$`)
* `host_address` (= `$HOSTADDRESS$`)
* `service_state` (= `$SERVICESTATE$`)
* `long_date_time` (= `$LONGDATETIME$`)
* `service_output` (= `$SERVICEOUTPUT$`)
## How to contribute
Contributions are welcomed! Feel free to update the code and create a pull-request.
Be sure to lint the code before:
```
docker build -t pre-commit .
docker run -it -v $(pwd):/mnt/ --rm pre-commit bash
# cd /mnt/
# pre-commit run --all-files
# exit
```

View file

@ -1,4 +0,0 @@
Factorize the template rendering functions
Use JSON schema to validate configuration file
Add pre-commit Dockerfile and a doc to easily lint the code
Add requirements.txt file

21
config.schema.json Normal file
View file

@ -0,0 +1,21 @@
{
"type": "object",
"properties": {
"chat_id": {
"type": "number"
},
"auth_key": {
"type": "string"
},
"host_template": {
"type": "string"
},
"service_template": {
"type": "string"
}
},
"required": [
"chat_id",
"auth_key"
]
}

View file

@ -1,6 +1,6 @@
define command { define command {
command_name notify-host-by-telegram command_name notify-host-by-telegram
command_line /opt/notify-by-telegram/notify-by-telegram.py -c /etc/nagios4/telegram.json --logfile /var/log/nagios4/telegram.log host --notification-type "$NOTIFICATIONTYPE$" --service-desc "$SERVICEDESC$" --host-name "$HOSTNAME$" --host-state "$HOSTSTATE$" --host-address "$HOSTADDRESS$" --host-output "$HOSTOUTPUT$" --long-date-time "$LONGDATETIME$" command_line /opt/notify-by-telegram/notify-by-telegram.py -c /etc/nagios4/telegram.json --logfile /var/log/nagios4/telegram.log host --notification-type "$NOTIFICATIONTYPE$" --host-name "$HOSTNAME$" --host-state "$HOSTSTATE$" --host-address "$HOSTADDRESS$" --host-output "$HOSTOUTPUT$" --long-date-time "$LONGDATETIME$"
} }
define command { define command {

View file

@ -6,8 +6,10 @@ import os
import requests import requests
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from jsonschema import validate
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
absolute_path = os.path.split(os.path.abspath(__file__))[0]
class InvalidConfigException(Exception): class InvalidConfigException(Exception):
@ -28,7 +30,6 @@ def parse_arguments():
# host notifications # host notifications
host_parser = subparsers.add_parser('host') host_parser = subparsers.add_parser('host')
host_parser.add_argument('--notification-type', help='nagios $NOTIFICATIONTYPE$', required=True) host_parser.add_argument('--notification-type', help='nagios $NOTIFICATIONTYPE$', required=True)
host_parser.add_argument('--service-desc', help='nagios $SERVICEDESC$', required=True)
host_parser.add_argument('--host-name', help='nagios $HOSTNAME$', required=True) host_parser.add_argument('--host-name', help='nagios $HOSTNAME$', required=True)
host_parser.add_argument('--host-state', help='nagios $HOSTSTATE$', required=True) host_parser.add_argument('--host-state', help='nagios $HOSTSTATE$', required=True)
host_parser.add_argument('--host-address', help='nagios $HOSTADDRESS$', required=True) host_parser.add_argument('--host-address', help='nagios $HOSTADDRESS$', required=True)
@ -60,9 +61,9 @@ def read_config(filename=None):
def validate_config(config): def validate_config(config):
if config is None: if config is None:
raise InvalidConfigException('config is not a dict') raise InvalidConfigException('config is not a dict')
for key in ['chat_id', 'auth_key']: with open(os.path.join(absolute_path, 'config.schema.json'), 'r') as fd:
if key not in config: schema = json.loads(fd.read())
raise InvalidConfigException(f'missing "{key}" key in config') validate(instance=config, schema=schema)
def setup_logging(args): def setup_logging(args):
@ -71,48 +72,56 @@ def setup_logging(args):
def markdown_escape(text): def markdown_escape(text):
for special_char in ['\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '#', '+', '-', '.', '!']: for special_char in ['\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '#', '+', '-', '.', '!', '=']:
text = text.replace(special_char, fr'\{special_char}') text = text.replace(special_char, fr'\{special_char}')
return text return text
def generate_host_payload(chat_id, args, template_file_name=None): def generate_payload(chat_id, message_type, message_variables, template_file_name=None):
payload = {'chat_id': chat_id, 'parse_mode': 'MarkdownV2'} payload = {'chat_id': chat_id, 'parse_mode': 'MarkdownV2'}
# define jinja template name
if not template_file_name: if not template_file_name:
absolute_path = os.path.split(os.path.abspath(__file__))[0] template_name = 'host.md.j2' if message_type == 'host' else 'service.md.j2'
template_file_name = os.path.join(absolute_path, 'templates', 'host.md.j2') template_file_name = os.path.join(absolute_path, 'templates', template_name)
template_path = os.path.dirname(os.path.abspath(template_file_name)) template_path = os.path.dirname(os.path.abspath(template_file_name))
template_name = os.path.basename(os.path.abspath(template_file_name)) template_name = os.path.basename(os.path.abspath(template_file_name))
# create a jinja file template
loader = FileSystemLoader(template_path) loader = FileSystemLoader(template_path)
env = Environment(loader=loader) env = Environment(loader=loader)
template = env.get_template(template_name) template = env.get_template(template_name)
text = template.render(notification_type=markdown_escape(args.notification_type),
host_name=markdown_escape(args.host_name), host_state=markdown_escape(args.host_state), # escape message variables
host_address=markdown_escape(args.host_address), template_variables = {}
host_output=markdown_escape(args.host_output), for key, value in message_variables.items():
long_date_time=markdown_escape(args.long_date_time)) template_variables[key] = markdown_escape(value)
# render template and update payload
text = template.render(**template_variables)
payload['text'] = text payload['text'] = text
return payload return payload
def generate_service_payload(chat_id, args, template_file_name=None): def generate_host_payload(chat_id, notification_type, host_name, host_state, host_address, host_output, long_date_time,
payload = {'chat_id': chat_id, 'parse_mode': 'MarkdownV2'} template_file_name=None):
if not template_file_name: message_variables = {
absolute_path = os.path.split(os.path.abspath(__file__))[0] 'notification_type': notification_type, 'host_name': host_name, 'host_state': host_state,
template_file_name = os.path.join(absolute_path, 'templates', 'service.md.j2') 'host_address': host_address, 'host_output': host_output, 'long_date_time': long_date_time
template_path = os.path.dirname(os.path.abspath(template_file_name)) }
template_name = os.path.basename(os.path.abspath(template_file_name)) return generate_payload(chat_id=chat_id, message_type='host', message_variables=message_variables,
loader = FileSystemLoader(template_path) template_file_name=template_file_name)
env = Environment(loader=loader)
template = env.get_template(template_name)
text = template.render(notification_type=markdown_escape(args.notification_type), def generate_service_payload(chat_id, notification_type, service_desc, host_alias, host_address, service_state,
service_desc=markdown_escape(args.service_desc), host_alias=markdown_escape(args.host_alias), long_date_time, service_output, template_file_name=None):
host_address=markdown_escape(args.host_address), message_variables = {
service_state=markdown_escape(args.service_state), 'notification_type': notification_type, 'service_desc': service_desc, 'host_alias': host_alias,
long_date_time=markdown_escape(args.long_date_time), 'host_address': host_address, 'service_state': service_state, 'long_date_time': long_date_time,
service_output=markdown_escape(args.service_output)) 'service_output': service_output
payload['text'] = text }
return payload return generate_payload(chat_id=chat_id, message_type='service', message_variables=message_variables,
template_file_name=template_file_name)
def send_message(auth_key, payload): def send_message(auth_key, payload):
@ -134,17 +143,23 @@ def main():
logger.info('generating payload') logger.info('generating payload')
if args.command == 'host': if args.command == 'host':
payload = generate_host_payload(chat_id=config['chat_id'], args=args, payload = generate_host_payload(chat_id=config['chat_id'], notification_type=args.notification_type,
host_name=args.host_name, host_state=args.host_state,
host_address=args.host_address, host_output=args.host_output,
long_date_time=args.long_date_time,
template_file_name=config.get('host_template')) template_file_name=config.get('host_template'))
elif args.command == 'service': elif args.command == 'service':
payload = generate_service_payload(chat_id=config['chat_id'], args=args, payload = generate_service_payload(chat_id=config['chat_id'], notification_type=args.notification_type,
template_file_name=config.get('service_template')) service_desc=args.service_desc, host_alias=args.host_alias,
host_address=args.host_address, service_state=args.service_state,
long_date_time=args.long_date_time, service_output=args.service_output,
template_file_name=config.get('service_template'))
else: else:
raise NotImplementedError(f'command {args.command} not supported') raise NotImplementedError(f'command {args.command} not supported')
logger.info('sending message to telegram api') logger.info('sending message to telegram api')
send_message(auth_key=config['auth_key'], payload=payload) send_message(auth_key=config['auth_key'], payload=payload)
except Exception as err: except Exception:
logger.exception('cannot execute program') logger.exception('cannot execute program')

21
requirements.txt Normal file
View file

@ -0,0 +1,21 @@
appdirs==1.4.4
attrs==20.3.0
certifi==2020.11.8
cfgv==3.2.0
chardet==3.0.4
distlib==0.3.1
filelock==3.0.12
identify==1.5.10
idna==2.10
Jinja2==2.11.2
jsonschema==3.2.0
MarkupSafe==1.1.1
nodeenv==1.5.0
pre-commit==2.9.2
pyrsistent==0.17.3
PyYAML==5.3.1
requests==2.25.0
six==1.15.0
toml==0.10.2
urllib3==1.26.2
virtualenv==20.2.1