|
18 | 18 | # using ".encode()" in one thread can block other threads as well (only in python2) |
19 | 19 |
|
20 | 20 | import time |
21 | | -# import sys |
22 | 21 | import os |
23 | 22 | import socket |
24 | 23 | import select |
| 24 | +import threading |
| 25 | +import msg |
| 26 | +import ssl |
| 27 | +from subprocess import Popen, PIPE |
| 28 | +from sys import exc_info |
| 29 | + |
25 | 30 | try: |
26 | 31 | import urllib.request as urllib2 |
27 | 32 | except ImportError: |
28 | 33 | import urllib2 |
29 | 34 |
|
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 |
35 | 44 |
|
36 | 45 | try: |
37 | 46 | PATH = os.getenv('PATH').split(':') |
@@ -930,3 +939,129 @@ def check(self): |
930 | 939 | else: |
931 | 940 | self.error("Command", str(self.command), "returned no data") |
932 | 941 | 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