Skip to content

Commit 4bdf854

Browse files
committed
MySQLService class added
1 parent 696fe43 commit 4bdf854

1 file changed

Lines changed: 141 additions & 6 deletions

File tree

python.d/python_modules/base.py

Lines changed: 141 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,29 @@
1818
# using ".encode()" in one thread can block other threads as well (only in python2)
1919

2020
import time
21-
# import sys
2221
import os
2322
import socket
2423
import select
24+
import threading
25+
import msg
26+
import ssl
27+
from subprocess import Popen, PIPE
28+
from sys import exc_info
29+
2530
try:
2631
import urllib.request as urllib2
2732
except ImportError:
2833
import urllib2
2934

30-
from subprocess import Popen, PIPE
31-
32-
import threading
33-
import msg
34-
import ssl
35+
try:
36+
import MySQLdb
37+
PYMYSQL = True
38+
except ImportError:
39+
try:
40+
import pymysql as MySQLdb
41+
PYMYSQL = True
42+
except ImportError:
43+
PYMYSQL = False
3544

3645
try:
3746
PATH = os.getenv('PATH').split(':')
@@ -930,3 +939,129 @@ def check(self):
930939
else:
931940
self.error("Command", str(self.command), "returned no data")
932941
return False
942+
943+
944+
class MySQLService(SimpleService):
945+
946+
def __init__(self, configuration=None, name=None):
947+
SimpleService.__init__(self, configuration=configuration, name=name)
948+
self.__connection = None
949+
self.conn_properties = dict()
950+
self.queries = dict()
951+
952+
def __connect(self):
953+
try:
954+
connection = MySQLdb.connect(connect_timeout=self.update_every, **self.conn_properties)
955+
except (MySQLdb.MySQLError, TypeError) as error:
956+
return None, str(error)
957+
else:
958+
return connection, None
959+
960+
def check(self):
961+
def get_connection_properties(conf):
962+
properties = dict()
963+
if 'user' in conf and conf['user']:
964+
properties['user'] = conf['user']
965+
if 'pass' in conf and conf['pass']:
966+
properties['passwd'] = conf['pass']
967+
if 'socket' in conf and conf['socket']:
968+
properties['unix_socket'] = conf['socket']
969+
elif 'host' in conf and conf['host']:
970+
properties['host'] = conf['host']
971+
properties['port'] = int(conf['port']) if conf.get('port') else 3306
972+
elif 'my.cnf' in conf and conf['my.cnf']:
973+
properties['read_default_file'] = conf['my.cnf']
974+
975+
return properties or None
976+
977+
def is_valid_queries_dict(raw_queries, log_error):
978+
"""
979+
:param raw_queries: dict:
980+
:param log_error: function:
981+
:return: dict or None
982+
983+
raw_queries is valid when: type <dict> and not empty after is_valid_query(for all queries)
984+
"""
985+
def is_valid_query(query):
986+
return all([isinstance(query, str),
987+
query.startswith(('SELECT', 'select', 'SHOW', 'show'))])
988+
989+
if isinstance(raw_queries, dict) and raw_queries:
990+
valid_queries = dict([(n, q) for n, q in raw_queries.items() if is_valid_query(q)])
991+
bad_queries = set(raw_queries) - set(valid_queries)
992+
993+
if bad_queries:
994+
log_error('Removed query(s): %s' % bad_queries)
995+
return valid_queries
996+
else:
997+
log_error('Unsupported "queries" format. Must be not empty <dict>')
998+
return None
999+
1000+
if not PYMYSQL:
1001+
self.error('MySQLdb or PyMySQL module is needed to use mysql.chart.py plugin')
1002+
return False
1003+
1004+
# Check if "self.queries" exist, not empty and all queries are in valid format
1005+
self.queries = is_valid_queries_dict(self.queries, self.error)
1006+
if not self.queries:
1007+
return None
1008+
1009+
# Get connection properties
1010+
self.conn_properties = get_connection_properties(self.configuration)
1011+
if not self.conn_properties:
1012+
self.error('Connection properties are missing')
1013+
return False
1014+
1015+
# Create connection to the database
1016+
self.__connection, error = self.__connect()
1017+
if error:
1018+
self.error('Can\'t establish connection to MySQL: %s' % error)
1019+
return False
1020+
1021+
try:
1022+
data = self._get_data()
1023+
except Exception as error:
1024+
self.error('_get_data() failed. Error: %s' % error)
1025+
return False
1026+
1027+
if isinstance(data, dict) and data:
1028+
# We need this for create() method
1029+
self._data_from_check = data
1030+
return True
1031+
else:
1032+
self.error("_get_data() returned no data or type is not <dict>")
1033+
return False
1034+
1035+
def _get_raw_data(self, description=None):
1036+
"""
1037+
Get raw data from MySQL server
1038+
:return: dict: fetchall() or (fetchall(), description)
1039+
"""
1040+
1041+
if not self.__connection:
1042+
self.__connection, error = self.__connect()
1043+
if error:
1044+
return None
1045+
1046+
raw_data = dict()
1047+
try:
1048+
with self.__connection as cursor:
1049+
for name, query in self.queries.items():
1050+
try:
1051+
cursor.execute(query)
1052+
except (MySQLdb.ProgrammingError, MySQLdb.OperationalError) as error:
1053+
if exc_info()[0] == MySQLdb.OperationalError and 'denied' not in str(error):
1054+
raise RuntimeError
1055+
self.error('Removed query: %s[%s]. Error: %s'
1056+
% (name, query, error))
1057+
self.queries.pop(name)
1058+
continue
1059+
else:
1060+
raw_data[name] = (cursor.fetchall(), cursor.description) if description else cursor.fetchall()
1061+
self.__connection.commit()
1062+
except (MySQLdb.MySQLError, RuntimeError, TypeError, AttributeError):
1063+
self.__connection.close()
1064+
self.__connection = None
1065+
return None
1066+
else:
1067+
return raw_data or None

0 commit comments

Comments
 (0)