Introduce pip installable package
* Added configuration support * Drop identical notifications * Provide install routine for pip
This commit is contained in:
parent
1af5befe0c
commit
8d64c39019
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
**.pyc
|
30
README.md
30
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 = <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
196
galeranotify.py
Normal file → Executable 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
12
galeranotify.yml
Normal 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
187
galeranotify/__init__.py
Executable 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
14
galeranotify/__main__.py
Executable 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
63
galeranotify/configuration.py
Executable 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
52
galeranotify/persistance.py
Executable 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
31
setup.py
Normal 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'])
|
||||
],
|
||||
)
|
Loading…
Reference in New Issue
Block a user