require 'mysql2' require 'flydata/command_logger' require 'flydata/util/mysql_util' module Flydata class CompatibilityCheck include CommandLoggable class CompatibilityError < StandardError end def initialize(dp_hash, de_hash=nil, options={}) @dp = dp_hash @errors=[] end def check self.methods.grep(/^check_/).each do |m| begin send(m) rescue ArgumentError => e # ignore rescue CompatibilityError => e @errors << e end end print_errors end def print_errors return if @errors.empty? log_error_stderr "There may be some compatibility issues with this current server : " @errors.each do |error| log_error_stderr " * #{error.message}" end raise "Please correct these errors if you wish to run FlyData Agent" end end class AgentCompatibilityCheck < CompatibilityCheck class AgentCompatibilityError < StandardError end TCP_PORT=45326 SSL_PORT=45327 def check_outgoing_ports ports = [TCP_PORT] ports << SSL_PORT unless ENV['FLYDATA_ENV_KEY'] == 'development' url = @dp["servers"].first errors = {} ports.each do |port| begin e = TCPSocket.new(url, port) e.close rescue => e errors[port] = e end end unless errors.empty? message = "Cannot connect to outside ports. Please check to make sure you have these outgoing ports open." errors.each do |port, e| message += " Port #{port}, Error #{e.class.name}: #{e.to_s}" end raise AgentCompatibilityError, message end end end class MysqlCompatibilityCheck < CompatibilityCheck class MysqlCompatibilityError < StandardError end SELECT_QUERY_TMPLT = "SELECT %s" BINLOG_RETENTION_HOURS = 24 #def initialize(de_hash, dump_dir=nil) def initialize(dp_hash, de_hash, options={}) super @db_opts = [:host, :port, :username, :password, :database].inject({}) {|h, sym| h[sym] = de_hash[sym.to_s]; h} @dump_dir = options[:dump_dir] || nil @backup_dir = options[:backup_dir] || nil end def print_errors return if @errors.empty? log_error_stderr "There may be some compatibility issues with your MySQL credentials: " @errors.each do |error| log_error_stderr " * #{error.message}" end raise "Please correct these errors if you wish to run FlyData Sync" end def check_mysql_user_compat client = Mysql2::Client.new(@db_opts) grants_sql = "SHOW GRANTS" correct_db = ["ON (\\*|#{@db_opts[:database]})","TO '#{@db_opts[:username]}"] necessary_permission_fields= ["SELECT","RELOAD","LOCK TABLES","REPLICATION SLAVE","REPLICATION CLIENT"] all_privileges_field= ["ALL PRIVILEGES"] result = client.query(grants_sql) # Do not catch MySQL connection problem because check should stop if no MySQL connection can be made. client.close missing_priv = [] result.each do |res| # SHOW GRANTS should only return one column res_value = res.values.first if correct_db.all? {|perm| res_value.match(perm)} necessary_permission_fields.each do |priv| missing_priv << priv unless res_value.match(priv) end return true if missing_priv.empty? or all_privileges_field.all? {|d| res_value.match(d)} end end raise MysqlCompatibilityError, "The user '#{@db_opts[:username]}' does not have the correct permissions to run FlyData Sync\n * These privileges are missing: #{missing_priv.join(", ")}" end def check_mysql_protocol_tcp_compat query = Util::MysqlUtil.generate_mysql_show_grants_cmd(@db_opts) Open3.popen3(query) do |stdin, stdout, stderr| stdin.close while !stderr.eof? lines = [] while line = stderr.gets; lines << line.strip; end err_reason = lines.join(" ") log_error("Error occured during access to mysql server.", {err: err_reason}) unless /Warning: Using a password on the command line interface can be insecure/ === err_reason raise MysqlCompatibilityError, "Cannot connect to MySQL database. Please make sure you can connect with this command:\n $ mysql -u #{@db_opts[:username]} -h #{@db_opts[:host]} -P #{@db_opts[:port]} #{@db_opts[:database]} --protocol=tcp -p" end end end end def check_mysql_row_mode_compat sys_var_to_check = {'@@binlog_format'=>'ROW', '@@binlog_checksum'=>'NONE', '@@log_bin_use_v1_row_events'=>1} errors={} client = Mysql2::Client.new(@db_opts) begin sys_var_to_check.each_key do |sys_var| sel_query = SELECT_QUERY_TMPLT % sys_var begin result = client.query(sel_query) unless result.first[sys_var] == sys_var_to_check[sys_var] errors[sys_var]=result.first[sys_var] end rescue Mysql2::Error => e if e.message =~ /Unknown system variable/ unless e.message =~ /(binlog_checksum|log_bin_use_v1_row_events)/ errors[sys_var] = false end else raise e end end end ensure client.close 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 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 def check_mysql_binlog_retention client = Mysql2::Client.new(@db_opts) begin if is_rds?(@db_opts[:host]) run_rds_retention_check(client) else run_mysql_retention_check(client) end ensure client.close end end def check_writing_permissions write_errors = [] paths_to_check = ["~/.flydata"] paths_to_check << @dump_dir unless @dump_dir.to_s.empty? paths_to_check << @backup_dir unless @backup_dir.to_s.empty? paths_to_check.each do |path| full_path = File.expand_path(path) full_path = File.dirname(full_path) unless File.directory?(full_path) write_errors << full_path unless File.writable?(full_path) and File.executable?(full_path) end unless write_errors.empty? error_dir = write_errors.join(", ") raise MysqlCompatibilityError, "We cannot access the directories: #{error_dir}" end end def run_mysql_retention_check(mysql_client) expire_logs_days_limit = BINLOG_RETENTION_HOURS / 24 sel_query = SELECT_QUERY_TMPLT % '@@expire_logs_days' result = mysql_client.query(sel_query) if result.first["@@expire_logs_days"]!=0 and result.first["@@expire_logs_days"] <= expire_logs_days_limit raise 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} days" end end def run_rds_retention_check(mysql_client) sql_query = "call mysql.rds_show_configuration;" begin result = mysql_client.query(sql_query) if result.first["name"]=="binlog retention hours" if result.first["value"].nil? or result.first["value"].to_i <= BINLOG_RETENTION_HOURS raise 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 rescue Mysql2::Error => e if e.message =~ /command denied to user/ log_warn_stderr("[WARNING]Cannot verify RDS retention period on currend MySQL user account.\n" + "To see retention period, please run this on your RDS:\n" + " $> call mysql.rds_show_configuration;\n" + "Please verify that the hours is not nil and is at least #{BINLOG_RETENTION_HOURS} hours\n" + "To set binlog retention hours, you can run this on your RDS:\n" + " $> call mysql.rds_set_configuration('binlog retention hours', #{BINLOG_RETENTION_HOURS});\n" ) else raise e end end end def is_rds?(hostname) hostname.match(/rds.amazonaws.com$/) != nil end end end