move MySQLService to a separate file
lgz committed
Oct 13, 2017 at 00:16 UTC
d983e0f0f26fe2c026c5d8acbad37016388fc90e
1 file changed
+158
python.d/python_modules/bases/FrameworkServices/MySQLService.py
new
+158
@@ -0,0 +1,158 @@
1
+# -*- coding: utf-8 -*-
2
+# Description:
3
+# Author: Ilya Mashchenko (l2isbad)
4
+
5
+from sys import exc_info
6
+
7
+try:
8
+ import MySQLdb
9
+
10
+ PY_MYSQL = True
11
+except ImportError:
12
+ try:
13
+ import pymysql as MySQLdb
14
+
15
+ PY_MYSQL = True
16
+ except ImportError:
17
+ PY_MYSQL = False
18
+
19
+from bases.FrameworkServices.SimpleService import SimpleService
20
+
21
+
22
+class MySQLService(SimpleService):
23
+ def __init__(self, configuration=None, name=None):
24
+ SimpleService.__init__(self, configuration=configuration, name=name)
25
+ self.__connection = None
26
+ self.__conn_properties = dict()
27
+ self.extra_conn_properties = dict()
28
+ self.__queries = self.configuration.get('queries', dict())
29
+ self.queries = dict()
30
+
31
+ def __connect(self):
32
+ try:
33
+ connection = MySQLdb.connect(connect_timeout=self.update_every, **self.__conn_properties)
34
+ except (MySQLdb.MySQLError, TypeError, AttributeError) as error:
35
+ return None, str(error)
36
+ else:
37
+ return connection, None
38
+
39
+ def check(self):
40
+ def get_connection_properties(conf, extra_conf):
41
+ properties = dict()
42
+ if conf.get('user'):
43
+ properties['user'] = conf['user']
44
+ if conf.get('pass'):
45
+ properties['passwd'] = conf['pass']
46
+ if conf.get('socket'):
47
+ properties['unix_socket'] = conf['socket']
48
+ elif conf.get('host'):
49
+ properties['host'] = conf['host']
50
+ properties['port'] = int(conf.get('port', 3306))
51
+ elif conf.get('my.cnf'):
52
+ if MySQLdb.__name__ == 'pymysql':
53
+ self.error('"my.cnf" parsing is not working for pymysql')
54
+ else:
55
+ properties['read_default_file'] = conf['my.cnf']
56
+ if isinstance(extra_conf, dict) and extra_conf:
57
+ properties.update(extra_conf)
58
+
59
+ return properties or None
60
+
61
+ def is_valid_queries_dict(raw_queries, log_error):
62
+ """
63
+ :param raw_queries: dict:
64
+ :param log_error: function:
65
+ :return: dict or None
66
+
67
+ raw_queries is valid when: type <dict> and not empty after is_valid_query(for all queries)
68
+ """
69
+
70
+ def is_valid_query(query):
71
+ return all([isinstance(query, str),
72
+ query.startswith(('SELECT', 'select', 'SHOW', 'show'))])
73
+
74
+ if hasattr(raw_queries, 'keys') and raw_queries:
75
+ valid_queries = dict([(n, q) for n, q in raw_queries.items() if is_valid_query(q)])
76
+ bad_queries = set(raw_queries) - set(valid_queries)
77
+
78
+ if bad_queries:
79
+ log_error('Removed query(s): {queries}'.format(queries=bad_queries))
80
+ return valid_queries
81
+ else:
82
+ log_error('Unsupported "queries" format. Must be not empty <dict>')
83
+ return None
84
+
85
+ if not PY_MYSQL:
86
+ self.error('MySQLdb or PyMySQL module is needed to use mysql.chart.py plugin')
87
+ return False
88
+
89
+ # Preference: 1. "queries" from the configuration file 2. "queries" from the module
90
+ self.queries = self.__queries or self.queries
91
+ # Check if "self.queries" exist, not empty and all queries are in valid format
92
+ self.queries = is_valid_queries_dict(self.queries, self.error)
93
+ if not self.queries:
94
+ return None
95
+
96
+ # Get connection properties
97
+ self.__conn_properties = get_connection_properties(self.configuration, self.extra_conn_properties)
98
+ if not self.__conn_properties:
99
+ self.error('Connection properties are missing')
100
+ return False
101
+
102
+ # Create connection to the database
103
+ self.__connection, error = self.__connect()
104
+ if error:
105
+ self.error('Can\'t establish connection to MySQL: {error}'.format(error=error))
106
+ return False
107
+
108
+ try:
109
+ data = self._get_data()
110
+ except Exception as error:
111
+ self.error('_get_data() failed. Error: {error}'.format(error=error))
112
+ return False
113
+
114
+ if isinstance(data, dict) and data:
115
+ return True
116
+ self.error("_get_data() returned no data or type is not <dict>")
117
+ return False
118
+
119
+ def _get_raw_data(self, description=None):
120
+ """
121
+ Get raw data from MySQL server
122
+ :return: dict: fetchall() or (fetchall(), description)
123
+ """
124
+
125
+ if not self.__connection:
126
+ self.__connection, error = self.__connect()
127
+ if error:
128
+ return None
129
+
130
+ raw_data = dict()
131
+ queries = dict(self.queries)
132
+ try:
133
+ with self.__connection as cursor:
134
+ for name, query in queries.items():
135
+ try:
136
+ cursor.execute(query)
137
+ except (MySQLdb.ProgrammingError, MySQLdb.OperationalError) as error:
138
+ if self.__is_error_critical(err_class=exc_info()[0], err_text=str(error)):
139
+ raise RuntimeError
140
+ self.error('Removed query: {name}[{query}]. Error: error'.format(name=name,
141
+ query=query,
142
+ error=error))
143
+ self.queries.pop(name)
144
+ continue
145
+ else:
146
+ raw_data[name] = (cursor.fetchall(), cursor.description) if description else cursor.fetchall()
147
+ self.__connection.commit()
148
+ except (MySQLdb.MySQLError, RuntimeError, TypeError, AttributeError):
149
+ self.__connection.close()
150
+ self.__connection = None
151
+ return None
152
+ else:
153
+ return raw_data or None
154
+
155
+ @staticmethod
156
+ def __is_error_critical(err_class, err_text):
157
+ return err_class == MySQLdb.OperationalError and all(['denied' not in err_text,
158
+ 'Unknown column' not in err_text])