# -*- coding: utf-8 -*- # Description: # Author: Ilya Mashchenko (ilyam8) # SPDX-License-Identifier: GPL-3.0-or-later from sys import exc_info try: import MySQLdb PY_MYSQL = True except ImportError: try: import pymysql as MySQLdb PY_MYSQL = True except ImportError: PY_MYSQL = False from bases.FrameworkServices.SimpleService import SimpleService class MySQLService(SimpleService): def __init__(self, configuration=None, name=None): SimpleService.__init__(self, configuration=configuration, name=name) self.__connection = None self.__conn_properties = dict() self.extra_conn_properties = dict() self.__queries = self.configuration.get('queries', dict()) self.queries = dict() def __connect(self): try: connection = MySQLdb.connect(connect_timeout=self.update_every, **self.__conn_properties) except (MySQLdb.MySQLError, TypeError, AttributeError) as error: return None, str(error) else: return connection, None def check(self): def get_connection_properties(conf, extra_conf): properties = dict() if conf.get('user'): properties['user'] = conf['user'] if conf.get('pass'): properties['passwd'] = conf['pass'] if conf.get('socket'): properties['unix_socket'] = conf['socket'] elif conf.get('host'): properties['host'] = conf['host'] properties['port'] = int(conf.get('port', 3306)) elif conf.get('my.cnf'): properties['read_default_file'] = conf['my.cnf'] if conf.get('ssl'): properties['ssl'] = conf['ssl'] if isinstance(extra_conf, dict) and extra_conf: properties.update(extra_conf) return properties or None def is_valid_queries_dict(raw_queries, log_error): """ :param raw_queries: dict: :param log_error: function: :return: dict or None raw_queries is valid when: type and not empty after is_valid_query(for all queries) """ def is_valid_query(query): return all([isinstance(query, str), query.startswith(('SELECT', 'select', 'SHOW', 'show'))]) if hasattr(raw_queries, 'keys') and raw_queries: valid_queries = dict([(n, q) for n, q in raw_queries.items() if is_valid_query(q)]) bad_queries = set(raw_queries) - set(valid_queries) if bad_queries: log_error('Removed query(s): {queries}'.format(queries=bad_queries)) return valid_queries else: log_error('Unsupported "queries" format. Must be not empty ') return None if not PY_MYSQL: self.error('MySQLdb or PyMySQL module is needed to use mysql.chart.py plugin') return False # Preference: 1. "queries" from the configuration file 2. "queries" from the module self.queries = self.__queries or self.queries # Check if "self.queries" exist, not empty and all queries are in valid format self.queries = is_valid_queries_dict(self.queries, self.error) if not self.queries: return None # Get connection properties self.__conn_properties = get_connection_properties(self.configuration, self.extra_conn_properties) if not self.__conn_properties: self.error('Connection properties are missing') return False # Create connection to the database self.__connection, error = self.__connect() if error: self.error('Can\'t establish connection to MySQL: {error}'.format(error=error)) return False try: data = self._get_data() except Exception as error: self.error('_get_data() failed. Error: {error}'.format(error=error)) return False if isinstance(data, dict) and data: return True self.error("_get_data() returned no data or type is not ") return False def _get_raw_data(self, description=None): """ Get raw data from MySQL server :return: dict: fetchall() or (fetchall(), description) """ if not self.__connection: self.__connection, error = self.__connect() if error: return None raw_data = dict() queries = dict(self.queries) try: cursor = self.__connection.cursor() for name, query in queries.items(): try: cursor.execute(query) except (MySQLdb.ProgrammingError, MySQLdb.OperationalError) as error: if self.__is_error_critical(err_class=exc_info()[0], err_text=str(error)): cursor.close() raise RuntimeError self.error('Removed query: {name}[{query}]. Error: error'.format(name=name, query=query, error=error)) self.queries.pop(name) continue else: raw_data[name] = (cursor.fetchall(), cursor.description) if description else cursor.fetchall() cursor.close() self.__connection.commit() except (MySQLdb.MySQLError, RuntimeError, TypeError, AttributeError): self.__connection.close() self.__connection = None return None else: return raw_data or None @staticmethod def __is_error_critical(err_class, err_text): return err_class == MySQLdb.OperationalError and all(['denied' not in err_text, 'Unknown column' not in err_text])