From 8d64c39019be321a11c24c1423878734ac69ad0e Mon Sep 17 00:00:00 2001 From: Sprinterfreak Date: Wed, 3 Apr 2019 22:46:29 +0200 Subject: [PATCH] Introduce pip installable package * Added configuration support * Drop identical notifications * Provide install routine for pip --- .gitignore | 1 + README.md | 30 +++--- galeranotify.py | 196 ++-------------------------------- galeranotify.yml | 12 +++ galeranotify/__init__.py | 187 ++++++++++++++++++++++++++++++++ galeranotify/__main__.py | 14 +++ galeranotify/configuration.py | 63 +++++++++++ galeranotify/persistance.py | 52 +++++++++ setup.py | 31 ++++++ 9 files changed, 384 insertions(+), 202 deletions(-) create mode 100644 .gitignore mode change 100644 => 100755 galeranotify.py create mode 100644 galeranotify.yml create mode 100755 galeranotify/__init__.py create mode 100755 galeranotify/__main__.py create mode 100755 galeranotify/configuration.py create mode 100755 galeranotify/persistance.py create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..04a41a2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +**.pyc diff --git a/README.md b/README.md index f821244..32f1009 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,25 @@ galeranotify ============ -Python E-Mail script for use with Galera wsrep_notify_cmd +Python E-Mail script for use with Galera wsrep\_notify\_cmd -Why do I need / want this? --------------------------- +This script immediately generates an E-Mail if Your cluster state changes, so You won't miss a second. +Also duplicates are dropped. Using Galera for instance in combination with commercial backup software like Comvault leads to a whole buch of notifications without a state change. +This is caused by Comvault illegaly LOCKing slave-tables while dumping data out of the cluster. Since the LOCK didn't last too long, the locked slave is pulled back to synced shortly after. +Each locked table causes an E-Mail. Therefore galeranotify remembers its previous state and drops the message if no state change has really happend. -[Galera](http://codership.com/products/galera_replication) makes my life easier with near synchronous replication for MySQL. We have monitoring tools in place, but its nice to get updates in real time about how the cluster is operating. So I wrote galeranotify. -I've been using this on our [Percona XtraDB Cluster](http://www.percona.com/software/percona-xtradb-cluster) for quite a while now with no issues. - -I hope someone finds it useful. - -Set up +Installation ------ -1. Edit galeranotify.py to change the configuration options. They should be pretty straightforward. +Install it via pip is the easiest way +``` +pip install git+https://git.binary-kitchen.de/sprinterfreak/galeranotify +``` -2. Place galeranotify.py in a common location and make sure you and your MySql user have execute permissions. - -3. Manually execute galeranotify.py with several of the options set (check usage) and check to make sure the script executes with no errors and that you receive the notification e-mail. - -4. Set 'wsrep_notify_cmd = ' in your my.cnf file. - -5. Restart MySql. +- Place the galeranotify.yml under /etc/mysql and configure it. +- Set 'wsrep\_notify\_cmd = galeranotify' in your my.cnf file +- Restart MySql. SELinux ------- diff --git a/galeranotify.py b/galeranotify.py old mode 100644 new mode 100755 index 462ae13..e2f173f --- a/galeranotify.py +++ b/galeranotify.py @@ -1,193 +1,19 @@ #!/usr/bin/python +# -*- coding: utf-8 -*- +# vim: ts=4 sw=4 sts=4 et # -# Script to send email notifications when a change in Galera cluster membership -# occurs. -# -# Complies with http://www.codership.com/wiki/doku.php?id=notification_command -# -# Author: Gabe Guillen -# Version: 1.5 -# Release: 3/5/2015 -# Use at your own risk. No warranties expressed or implied. +# galeranotify - python module wrapper # +# Copyright (c) Jan-Jonas Sämann , 2019 +# available under the ISC licence -import os -import sys -import getopt +import galeranotify -import smtplib +if __name__ == '__main__': + galeranotify.main() -try: from email.mime.text import MIMEText -except ImportError: - # Python 2.4 (CentOS 5.x) - from email.MIMEText import MIMEText +import galeranotify -import socket -import email.utils +if __name__ == '__main__': + galeranotify.main() -# Change this to some value if you don't want your server hostname to show in -# the notification emails -THIS_SERVER = socket.gethostname() - -# Server hostname or IP address -SMTP_SERVER = 'YOUR_SMTP_HERE' -SMTP_PORT = 25 - -# Set to True if you need SMTP over SSL -SMTP_SSL = False - -# Set to True if you need to authenticate to your SMTP server -SMTP_AUTH = False -# Fill in authorization information here if True above -SMTP_USERNAME = '' -SMTP_PASSWORD = '' - -# Takes a single sender -MAIL_FROM = 'YOUR_EMAIL_HERE' -# Takes a list of recipients -MAIL_TO = ['SOME_OTHER_EMAIL_HERE'] - -# Need Date in Header for SMTP RFC Compliance -DATE = email.utils.formatdate() - -# Edit below at your own risk -################################################################################ -def main(argv): - str_status = '' - str_uuid = '' - str_primary = '' - str_members = '' - str_index = '' - message = '' - - usage = "Usage: " + os.path.basename(sys.argv[0]) + " --status " - usage += " --uuid --primary --members 1): - message += "s" - - message += ":\n\n" - - if(self._status): - message += "Status of this node: " + self._status + "\n\n" - - if(self._uuid): - message += "Cluster state UUID: " + self._uuid + "\n\n" - - if(self._primary): - message += "Current cluster component is primary: " + self._primary + "\n\n" - - if(self._members): - message += "Current members of the component:\n" - - if(self._index): - for i in range(len(self._members)): - if(i == int(self._index)): - message += "-> " - else: - message += "-- " - - message += self._members[i] + "\n" - else: - message += "\n".join((" " + str(x)) for x in self._members) - - message += "\n" - - if(self._index): - message += "Index of this node in the member list: " + self._index + "\n" - - return message - -if __name__ == "__main__": - main(sys.argv[1:]) diff --git a/galeranotify.yml b/galeranotify.yml new file mode 100644 index 0000000..1f34cca --- /dev/null +++ b/galeranotify.yml @@ -0,0 +1,12 @@ +--- +smtp: + server: mailserver.dns + port: 25 + ssl: False + auth_enable: False + username: '' + password: '' +email_to: + - example@invalid.tld + - admin@help.me +email_from: galera@localhost diff --git a/galeranotify/__init__.py b/galeranotify/__init__.py new file mode 100755 index 0000000..0e5117f --- /dev/null +++ b/galeranotify/__init__.py @@ -0,0 +1,187 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# vim: ts=4 sw=4 sts=4 et +# +# galeranotify - wsrep_notify_cmd applications main functions +# +# Script to send email notifications when a change in Galera cluster membership +# occurs. +# +# Complies with http://www.codership.com/wiki/doku.php?id=notification_command +# +# Copyright (c) Gabe Guillen , 2015 +# Copyright (c) Jan-Jonas Sämann , 2019 +# available under the GPLv2 licence, see LICENSE + +import os +import sys +import getopt +from galeranotify import configuration, persistance + +try: + from email.mime.text import MIMEText +except ImportError: + # Python 2.4 (CentOS 5.x) + from email.MIMEText import MIMEText +import smtplib +import email.utils + +def main(): + argv = sys.argv[1:] + str_status = '' + str_uuid = '' + str_primary = '' + str_members = '' + str_index = '' + message = '' + + usage = "Usage: " + os.path.basename(sys.argv[0]) + " --status " + usage += " --uuid --primary --members 1): + message += "s" + + message += ":\n\n" + + if(self._status): + message += "Status of this node: " + self._status + "\n\n" + + if(self._uuid): + message += "Cluster state UUID: " + self._uuid + "\n\n" + + if(self._primary): + message += "Current cluster component is primary: " + self._primary + "\n\n" + + if(self._members): + message += "Current members of the component:\n" + + if(self._index): + for i in range(len(self._members)): + if(i == int(self._index)): + message += "-> " + else: + message += "-- " + + message += self._members[i] + "\n" + else: + message += "\n".join((" " + str(x)) for x in self._members) + + message += "\n" + + if(self._index): + message += "Index of this node in the member list: " + self._index + "\n" + + return message diff --git a/galeranotify/__main__.py b/galeranotify/__main__.py new file mode 100755 index 0000000..5f4f945 --- /dev/null +++ b/galeranotify/__main__.py @@ -0,0 +1,14 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# vim: ts=4 sw=4 sts=4 et +# +# galeranotify - python module wrapper +# +# Copyright (c) Jan-Jonas Sämann , 2019 +# available under the ISC licence + +import galeranotify + +if __name__ == '__main__': + galeranotify.main() + diff --git a/galeranotify/configuration.py b/galeranotify/configuration.py new file mode 100755 index 0000000..4c7f3a7 --- /dev/null +++ b/galeranotify/configuration.py @@ -0,0 +1,63 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# vim: ts=4 sw=4 sts=4 et +# +# galeranotify - configuration backend +# +# Copyright (c) Jan-Jonas Sämann , 2019 +# available under the ISC licence + +import sys +import yaml +import socket +import operator +from functools import reduce + +class ConfigFactory(dict): + _defaults = { + 'hostname': socket.gethostname(), + 'smtp': { + 'server':'127.0.0.1', + 'port': 25, + 'ssl': True, + 'auth_enable': False, + 'username': None, + 'password': None + }, + 'email_from': 'galera@localhost', + 'email_to': 'root@localhost' + } + def __init__(self, cf): + try: + with file(cf, 'r') as fh: + prop = yaml.load(fh, Loader=yaml.SafeLoader) + except IOError, e: + sys.stderr.write('Load configuration from {} failed with {}'.format(cf, e)) + prop = self._defaults + pass + + self._prop = prop + + def __getitem__(self, key): + if isinstance(key, tuple): + try: + return reduce(operator.getitem, key, self._prop) + except: + return reduce(operator.getitem, key, self._defaults) + else: + if key in self._prop: + return self._prop[key] + elif key in self._defaults: + return self._defaults[key] + + def __setitem__(self, key, value): + raise NotImplementedError('Config modify') + + def __iter__(self): + return iter(self._prop) + + def __len__(self): + return len(self._prop) + + def __str__(self): + return repr(self._prop) diff --git a/galeranotify/persistance.py b/galeranotify/persistance.py new file mode 100755 index 0000000..4a42958 --- /dev/null +++ b/galeranotify/persistance.py @@ -0,0 +1,52 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# vim: ts=4 sw=4 sts=4 et +# +# galeranotify - persistant storage backend +# +# Copyright (c) Jan-Jonas Sämann , 2019 +# available under the ISC licence + +import sys +import yaml +import tempfile +import collections + +class DatabaseFactory(collections.MutableMapping,dict): + _state_changed = False + _prop = {} + + def __init__(self, persistance_file): + self.persistance_file = persistance_file + try: + with file(self.persistance_file, 'r') as fh: + self._prop = yaml.load(fh, Loader=yaml.SafeLoader) + except IOError, e: + self._prop = {} + self._state_changed = True + pass + + def __getitem__(self, key): + return self._prop[key] + + def __setitem__(self, key, value): + if key not in self._prop or self._prop[key] != value: + self._state_changed = True + self._prop[key] = value + + def __iter__(self): + return iter(self._prop) + + def __len__(self): + return len(self._prop) + + def changed(self): + return (self._state_changed or False) + + def __del__(self): + try: + with file(self.persistance_file, 'w') as fh: + yaml.dump(self._prop, fh, explicit_start=True) + except IOError, e: + sys.stderr.write('Can not write database file: {} ({})\n'.format(self.persistance_file, e)) + pass diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ed58768 --- /dev/null +++ b/setup.py @@ -0,0 +1,31 @@ +from setuptools import setup, find_packages + +setup( + name='galeranotify', + author='Jan-Jonas Sämann', + author_email='sprinterfreak@binary-kitchen.de', + version='2.0', + license='GPLv2', + keywords='galeranotify mysql mariadb', + description='A realtime email notifier for MySQL Galera-Cluster changes', + url='https://git.binary-kitchen.de/sprinterfreak/galeranotify', + classifiers=[ + 'Environment :: No Input/Output (Daemon)', + 'Intended Audience :: System Administrators', + 'Programming Language :: Python', + 'Topic :: Database', + ], + packages=find_packages(), + install_requires=[ + 'PyYAML' + ], + entry_points={ + 'console_scripts': { + 'galeranotify=galeranotify:main', + }, + }, + data_files=[ + ('readme', ['README.md']), + ('config', ['galeranotify.yml']) + ], +)