MySQLService.py 6.0 KB

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