require 'mysql2' require 'flydata/command_loggable' require 'flydata-core/mysql/command_generator' require 'flydata-core/mysql/compatibility_checker' require 'flydata-core/errors' module Flydata class CompatibilityCheck include CommandLoggable 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 FlydataCore::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 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 FlydataCore::AgentCompatibilityError, message end end end class MysqlCompatibilityCheck < CompatibilityCheck def initialize(dp_hash, de_hash, options={}) super @db_opts = [:host, :port, :username, :password, :database, :ssl_ca].inject({}) {|h, sym| h[sym] = de_hash[sym.to_s]; h} @db_opts[:sslca] = @db_opts[:ssl_ca] # for mysql2 gem @dump_dir = options[:dump_dir] || nil @backup_dir = options[:backup_dir] || nil @tables = de_hash['tables'] 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 FlydataCore::Mysql::SyncPermissionChecker.new(@db_opts).do_check end def check_mysql_protocol_tcp_compat query = FlydataCore::Mysql::CommandGenerator.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 FlydataCore::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_parameters_compat begin FlydataCore::Mysql::OptionalBinlogParameterChecker.new(@db_opts).do_check rescue FlydataCore::MysqlCompatibilityError => e log_warn_stderr(e.to_s) end FlydataCore::Mysql::RequiredBinlogParameterChecker.new(@db_opts).do_check end def check_mysql_binlog_retention if is_rds?(@db_opts[:host]) run_rds_retention_check else run_mysql_retention_check end end def check_writing_permissions write_errors = [] paths_to_check = [FLYDATA_HOME] 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 FlydataCore::MysqlCompatibilityError, "We cannot access the directories: #{error_dir}" end end # If table_type='VIEW' or engine='MEMORY', raise error. def check_mysql_table_types return if @tables.empty? option = @db_opts.dup.merge(tables: @tables) FlydataCore::Mysql::TableTypeChecker.new(option).do_check end def run_mysql_retention_check FlydataCore::Mysql::NonRdsRetentionChecker.new(@db_opts).do_check end def run_rds_retention_check FlydataCore::Mysql::RdsRetentionChecker.new(@db_opts).do_check rescue Mysql2::Error => e if e.message =~ /command denied to user/ retention_hours = FlydataCore::Mysql::RdsRetentionChecker::BINLOG_RETENTION_HOURS log_warn_stderr("[WARNING]Cannot verify RDS retention period on current 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 #{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', #{retention_hours});\n" ) else raise e end end def is_rds?(hostname) hostname.match(/rds.amazonaws.com$/) != nil end end end