require 'flydata-core/errors' require 'flydata-core/mysql/config' require 'mysql2' module FlydataCore module Mysql class CompatibilityChecker def initialize(option = {}) option ||= {} @option = option.merge FlydataCore::Mysql::Config.build_mysql_db_opts(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 BASE_SYS_VAR_TO_CHECK = { # parameter => expected value 'binlog_format' => 'ROW', #'binlog_checksum' => 'NONE', # no longer necessary with mysql-replication-listener 0.2.0 'log_bin_use_v1_row_events' => 'ON', 'log_slave_updates' => 'ON', } ADDITIONAL_SYS_VAR_TO_CHECK = { # parameter => {"comparison sign"=>minimum value} 'net_read_timeout'=>{'>='=>600}, 'net_write_timeout'=>{'>='=>600}, #'wait_timeout'=>{'>='=>60}, } SYS_VAR_TO_CHECK = BASE_SYS_VAR_TO_CHECK.merge(ADDITIONAL_SYS_VAR_TO_CHECK) def check_result(result, option = @option) param_hash = convert_result_to_hash(result) errors = compare_sys_var_values(param_hash, self.class::SYS_VAR_TO_CHECK) unless errors.empty? raise FlydataCore::MysqlCompatibilityError, build_error_message(errors) end end def build_error_message(errors) err_msg = errors.inject('') {|m, (k, v)| m << "\n * #{v[:err_reason]}"} "These system variable(s) are not the correct value: #{err_msg}\n" + " Please change these system variables for FlyData Sync to run" end private def compare_sys_var_values(input_value_hash, expected_value_hash) # { '' => {actual_val: '', err_reason: ''} } errors = {} expected_value_hash.each do |param_name, exp_val_info| # Ignore parameters if not returned # binlog_checksum and log_bin_use_v1_row_events are supported only for Mysql v5.6 next unless input_value_hash.has_key?(param_name) actual_val = input_value_hash[param_name] err_reason = nil if exp_val_info.kind_of?(Hash) exp_val_info.each do |compare_sym, exp_val| case compare_sym when '=' unless actual_val.to_s != exp_val.to_s err_reason = "#{param_name} is '#{actual_val}' but should be '#{exp_val}'" end when '>=' unless actual_val.to_i >= exp_val.to_i err_reason = "#{param_name} is '#{actual_val}' but should be at least '#{exp_val}'" end else raise "Unsupported compare symbol - #{compare_sym}" end break # support just one key-value for now end elsif exp_val_info.to_s != actual_val.to_s err_reason = "#{param_name} is '#{actual_val}' but should be '#{exp_val_info}'" end errors[param_name] = {actual_val: actual_val, err_reason: err_reason} if err_reason end errors end end class RequiredBinlogParameterChecker < BinlogParameterChecker SYS_VAR_TO_CHECK = BASE_SYS_VAR_TO_CHECK end class OptionalBinlogParameterChecker < BinlogParameterChecker SYS_VAR_TO_CHECK = ADDITIONAL_SYS_VAR_TO_CHECK def build_error_message(errors) err_msg = errors.inject('') {|m, (k, v)| m << "\n * #{v[:err_reason]}"} "[WARNING] These system variable(s) are not the recommended values: #{err_msg}\n" + " These settings are not required but highly recommended for the FlyData Agent to run as efficiently as possible" end end class TableExistenceChecker < MysqlCompatibilityChecker TABLE_EXISTENCE_CHECK_QUERY_TMPLT = < call mysql.rds_set_configuration('binlog retention hours', 94);" end end end end end end