require 'flydata-core/errors' require 'mysql2' module FlydataCore module Mysql class CompatibilityChecker def initialize(option = {}) @option = option || {} end def do_check(option = @option, &block) result = block.call create_query(option) check_result(result, option) end # Override #def create_query(option = @option) #end # Override #def validate_result(result, option = @option) #end end class MysqlCompatibilityChecker < CompatibilityChecker def do_check(option = @option, &block) query = create_query(option) result = if block block.call query else exec_query(query) end check_result(result, option) end def exec_query(query) begin client = Mysql2::Client.new(@option) client.query(query) ensure client.close rescue nil if client end end end class SyncPermissionChecker < MysqlCompatibilityChecker def create_query(option = @option) "SHOW GRANTS" end def check_result(result, option = @option) databases = ['mysql', @option[:database]] get_grant_regex = /GRANT (?.*) ON (`)?(?[^`]*)(`)?\.\* TO '#{@option[:username]}/ necessary_permission_fields = ["SELECT","RELOAD","LOCK TABLES","REPLICATION SLAVE","REPLICATION CLIENT"] all_privileges_field = ["ALL PRIVILEGES"] found_priv = Hash[databases.map {|d| [d,[]]}] missing_priv = {} result.each do |res| # SHOW GRANTS should only return one column res_value = res.values.first matched_values = res_value.match(get_grant_regex) next unless matched_values line_priv = matched_values["privs"].split(", ") if matched_values["db_name"] == "*" return true if (all_privileges_field - line_priv).empty? databases.each {|d| found_priv[d] << line_priv } elsif databases.include? matched_values["db_name"] if (all_privileges_field - line_priv).empty? found_priv[matched_values["db_name"]] = necessary_permission_fields else found_priv[matched_values["db_name"]] << line_priv end end missing_priv = get_missing_privileges(found_priv, necessary_permission_fields) return true if missing_priv.empty? end error_text = "The user '#{@option[:username]}' does not have the correct permissions to run FlyData Sync\n" error_text << " * These privileges are missing...\n" missing_priv.each_key {|db| error_text << " for the database '#{db}': #{missing_priv[db].join(", ")}\n"} raise MysqlCompatibilityError, error_text end private def get_missing_privileges(found_priv, all_priv) return_hash = {} found_priv.each_key do |key| missing_priv = all_priv - found_priv[key].flatten.uniq return_hash[key] = missing_priv unless missing_priv.empty? end return_hash end end module MysqlVariablesHandling def create_query(option = @option) "SHOW VARIABLES;" end def convert_result_to_hash(result) ret = {} result.each do |record| k = record['Variable_name'] v = record['Value'] ret[k] = v end ret end end class BinlogParameterChecker < MysqlCompatibilityChecker include MysqlVariablesHandling SYS_VAR_TO_CHECK = { # parameter => expected value 'binlog_format'=>'ROW', 'binlog_checksum'=>'NONE', 'log_bin_use_v1_row_events'=>'ON', 'log_slave_updates'=>'ON' } def check_result(result, option = @option) errors = {} param_hash = convert_result_to_hash(result) SYS_VAR_TO_CHECK.each_key do |sys_var| if param_hash.has_key?(sys_var) actual_val = param_hash[sys_var] expected_val = SYS_VAR_TO_CHECK[sys_var] unless actual_val == expected_val errors[sys_var] = actual_val end elsif not %w(binlog_checksum log_bin_use_v1_row_events).include?(sys_var) # Mark variables that do not exist as error errors[sys_var] = false end end unless errors.empty? error_explanation = "" errors.each_key do |err_key| error_explanation << "\n * #{err_key} is #{errors[err_key]} but should be #{SYS_VAR_TO_CHECK[err_key]}" end raise FlydataCore::MysqlCompatibilityError, "These system variable(s) are not the correct value: #{error_explanation}\n" + " Please change these system variables for FlyData Sync to run correctly" end end end class TableTypeChecker < MysqlCompatibilityChecker SELECT_TABLE_INFO_TMPLT = "SELECT table_name, table_type, engine " + "FROM information_schema.tables " + "WHERE table_schema = '%s' and table_name in (%s)" UNSUPPORTED_ENGINES = %w(MEMORY BLACKHOLE) # option[:client] : Mysql2::Client object # option[:tables] : target table list def create_query(option = @option) SELECT_TABLE_INFO_TMPLT % [ Mysql2::Client.escape(option[:database]), option[:tables].collect{|t| "'#{Mysql2::Client.escape(t)}'"}.join(", ") ] end def check_result(result, option = @option) invalid_tables = [] result.each do |r| invalid_tables.push(r['table_name']) if r['table_type'] == 'VIEW' || UNSUPPORTED_ENGINES.include?(r['engine']) end unless invalid_tables.empty? raise FlydataCore::MysqlCompatibilityError, "FlyData does not support VIEW and #{UNSUPPORTED_ENGINES.join(',')} STORAGE ENGINE table. " + "Remove following tables from data entry: #{invalid_tables.join(", ")}" end end end class NonRdsRetentionChecker < MysqlCompatibilityChecker include MysqlVariablesHandling BINLOG_RETENTION_HOURS = 24 EXPIRE_LOGS_DAYS_LIMIT = BINLOG_RETENTION_HOURS / 24 def check_result(result, option = @option) param_hash = convert_result_to_hash(result) if param_hash["expire_logs_days"].to_s != '0' && param_hash["expire_logs_days"].to_i <= EXPIRE_LOGS_DAYS_LIMIT raise FlydataCore::MysqlCompatibilityError, "Binary log retention is too short\n " + " We recommend the system variable '@@expire_logs_days' to be either set to 0 or at least #{EXPIRE_LOGS_DAYS_LIMIT} day(s)" end end end class RdsRetentionChecker < MysqlCompatibilityChecker BINLOG_RETENTION_HOURS = NonRdsRetentionChecker::BINLOG_RETENTION_HOURS def create_query(option = @option) "call mysql.rds_show_configuration;" end def check_result(result, option = @option) if result.first["name"] == "binlog retention hours" if result.first["value"].nil? || result.first["value"].to_i <= BINLOG_RETENTION_HOURS raise FlydataCore::MysqlCompatibilityError, "Binary log retention is too short\n" + " We recommend setting RDS binlog retention " + "to be at least #{BINLOG_RETENTION_HOURS} hours. To do this, " + "run this on your RDS MySQL database:\n" + " $> call mysql.rds_set_configuration('binlog retention hours', 94);" end end end end end end