Introduce pip installable package

* Added configuration support
  * Drop identical notifications
  * Provide install routine for pip
This commit is contained in:
Jan 2019-04-03 22:46:29 +02:00 committed by Jan-Jonas Sämann
parent 1af5befe0c
commit 8d64c39019
9 changed files with 384 additions and 202 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
**.pyc

View File

@ -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 = <path of galeranotify.py>' 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
-------

196
galeranotify.py Normal file → Executable file
View File

@ -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 <gabeguillen@outlook.com>
# 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 <sprinterfreak@binary-kitchen.de>, 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 <status str>"
usage += " --uuid <state UUID> --primary <yes/no> --members <comma-seperated"
usage += " list of the component member UUIDs> --index <n>"
try:
opts, args = getopt.getopt(argv, "h", ["status=","uuid=",'primary=','members=','index='])
except getopt.GetoptError:
print usage
sys.exit(2)
if(len(opts) > 0):
message_obj = GaleraStatus(THIS_SERVER)
for opt, arg in opts:
if opt == '-h':
print usage
sys.exit()
elif opt in ("--status"):
message_obj.set_status(arg)
elif opt in ("--uuid"):
message_obj.set_uuid(arg)
elif opt in ("--primary"):
message_obj.set_primary(arg)
elif opt in ("--members"):
message_obj.set_members(arg)
elif opt in ("--index"):
message_obj.set_index(arg)
try:
send_notification(MAIL_FROM, MAIL_TO, 'Galera Notification: ' + THIS_SERVER, DATE,
str(message_obj), SMTP_SERVER, SMTP_PORT, SMTP_SSL, SMTP_AUTH,
SMTP_USERNAME, SMTP_PASSWORD)
except Exception, e:
print "Unable to send notification: %s" % e
sys.exit(1)
else:
print usage
sys.exit(2)
sys.exit(0)
def send_notification(from_email, to_email, subject, date, message, smtp_server,
smtp_port, use_ssl, use_auth, smtp_user, smtp_pass):
msg = MIMEText(message)
msg['From'] = from_email
msg['To'] = ', '.join(to_email)
msg['Subject'] = subject
msg['Date'] = date
if(use_ssl):
mailer = smtplib.SMTP_SSL(smtp_server, smtp_port)
else:
mailer = smtplib.SMTP(smtp_server, smtp_port)
if(use_auth):
mailer.login(smtp_user, smtp_pass)
mailer.sendmail(from_email, to_email, msg.as_string())
mailer.close()
class GaleraStatus:
def __init__(self, server):
self._server = server
self._status = ""
self._uuid = ""
self._primary = ""
self._members = ""
self._index = ""
self._count = 0
def set_status(self, status):
self._status = status
self._count += 1
def set_uuid(self, uuid):
self._uuid = uuid
self._count += 1
def set_primary(self, primary):
self._primary = primary.capitalize()
self._count += 1
def set_members(self, members):
self._members = members.split(',')
self._count += 1
def set_index(self, index):
self._index = index
self._count += 1
def __str__(self):
message = "Galera running on " + self._server + " has reported the following"
message += " cluster membership change"
if(self._count > 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:])

12
galeranotify.yml Normal file
View File

@ -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

187
galeranotify/__init__.py Executable file
View File

@ -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 <gabeguillen@outlook.com>, 2015
# Copyright (c) Jan-Jonas Sämann <sprinterfreak@binary-kitchen.de>, 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 <status str>"
usage += " --uuid <state UUID> --primary <yes/no> --members <comma-seperated"
usage += " list of the component member UUIDs> --index <n> --config <file>"
try:
opts, args = getopt.getopt(argv, "h", ["status=","uuid=",'primary=','members=','index=','config='])
except getopt.GetoptError:
print usage
sys.exit(2)
config = configuration.ConfigFactory('/etc/mysql/galeranotify.yml')
# Need Date in Header for SMTP RFC Compliance
DATE = email.utils.formatdate()
if(len(opts) > 0):
message_obj = GaleraStatus(config['hostname'])
for opt, arg in opts:
if opt == '-h':
print usage
sys.exit()
elif opt in ("--status"):
message_obj.set_status(arg)
elif opt in ("--uuid"):
message_obj.set_uuid(arg)
elif opt in ("--primary"):
message_obj.set_primary(arg)
elif opt in ("--members"):
message_obj.set_members(arg)
elif opt in ("--index"):
message_obj.set_index(arg)
elif opt in ("--config"):
raise NotImplementedError("Custom configuration path isn't supported yet. Configuration MUST be present in /etc/mysql/galeranotify.yml")
if not message_obj.state_changed():
sys.stderr.write('Skip notification. Information didn\'t change\n')
sys.exit(0)
try:
send_notification(config['email_from'], config['email_to'], 'Galera Notification: ' + config['hostname'], DATE,
str(message_obj), config['smtp','server'], config['smtp','port'], bool(config['smtp','ssl']),
bool(config['smtp','auth_enable']), config['smtp','username'], config['smtp','password'])
except Exception, e:
sys.stderr.write('Unable to send notification: %s'.format(e))
sys.exit(0)
else:
print usage
sys.exit(2)
sys.exit(0)
def send_notification(from_email, to_email, subject, date, message, smtp_server,
smtp_port, use_ssl, use_auth, smtp_user, smtp_pass):
msg = MIMEText(message)
msg['From'] = from_email
msg['To'] = ', '.join(to_email)
msg['Subject'] = subject
msg['Date'] = date
if(use_ssl):
mailer = smtplib.SMTP_SSL(smtp_server, smtp_port)
else:
mailer = smtplib.SMTP(smtp_server, smtp_port)
if(use_auth):
mailer.login(smtp_user, smtp_pass)
mailer.sendmail(from_email, to_email, msg.as_string())
mailer.close()
class GaleraStatus():
def __init__(self, server):
self._server = server
self._status = ""
self._uuid = ""
self._primary = ""
self._members = ""
self._index = ""
self._count = 0
self.persistance = persistance.DatabaseFactory('/tmp/galeranotify_persistance.yml')
def set_status(self, status):
self._status = status
self.persistance['status'] = status
self._count += 1
def set_uuid(self, uuid):
self._uuid = uuid
self.persistance['uuid'] = uuid
self._count += 1
def set_primary(self, primary):
self._primary = primary.capitalize()
self.persistance['primary'] = self._primary
self._count += 1
def set_members(self, members):
self._members = members.split(',')
self.persistance['members'] = self._members
self._count += 1
def set_index(self, index):
self._index = index
self.persistance['index'] = index
self._count += 1
def state_changed(self):
return self.persistance.changed()
def __str__(self):
message = "Galera running on " + self._server + " has reported the following"
message += " cluster membership change"
if(self._count > 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

14
galeranotify/__main__.py Executable file
View File

@ -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 <sprinterfreak@binary-kitchen.de>, 2019
# available under the ISC licence
import galeranotify
if __name__ == '__main__':
galeranotify.main()

63
galeranotify/configuration.py Executable file
View File

@ -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 <sprinterfreak@binary-kitchen.de>, 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)

52
galeranotify/persistance.py Executable file
View File

@ -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 <sprinterfreak@binary-kitchen.de>, 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

31
setup.py Normal file
View File

@ -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'])
],
)