module EY module Backup module CLI #rename to option parser and config loader extend self def run(argv) options = default_options.merge(opt_parse(argv)) verify_restore if options[:command] == :restore and !options[:force] config_path = options[:config] || "/etc/.#{options[:engine]}.backups.yml" options = config_for(config_path).merge(options) options = late_options.merge(options) options[:tmp_dir] = '/mnt/tmp' if options[:tmp_dir].nil? options = delayed_engine(File.join(options[:tmp_dir], '/chef/dna.json')).merge(options){|k, v1, v2| v2.nil? ? v1 : v2} abort("Unable to determine database stack from dna; specify the engine with -e") if options[:engine].nil? options end def default_options { :command => :new_backup, :format => "gzip", :engine => db_stack_name('/etc/chef/dna.json'), :verbose => false, } end def late_options { :log_coordinates => false, :skip_analyze => false, :allow_concurrent => false, } end def delayed_engine(path) { :engine => db_stack_name(path), } end def db_stack_name(file) if File.exist?(file) case %x{cat #{file} | grep db_stack_name} when /mysql/ 'mysql' when /postgres/ 'postgresql' end end end def verify_restore res = '' puts "This will overwrite your database, are you sure you would like to proceed (Y/n)?" Timeout::timeout(30){ res = gets.strip } abort("You indicated '#{res}', exiting!") unless res.upcase == 'Y' end def opt_parse(argv) # Build a parser for the command line arguments options = {} optparse = OptionParser.new do |opts| opts.version = EY::CloudServer::VERSION opts.banner = "\nUsage: eybackup [-flag] [argument]" opts.define_head " eybackup: manage logical (mysqldump/pg_dump) style backups of your database.", " When backing up multiple databases, each database is backed up separately." opts.separator '*'*80 opts.on("-l", "--list-backup DATABASE_NAME", "List backups for DATABASE_NAME; ", " accepts 'all' to list all databases in the config (/etc/.*.backups.yml)") do |db| if db == "all" db = nil end options[:db] = db options[:command] = :list end opts.on("-e", "--engine DATABASE_ENGINE", "The database engine. ex: mysql, postgresql.") do |engine| options[:engine] = engine end opts.on("-n", "--new-backup [DATABASE_NAME]", "Create a new backup (default).", " Backs up each database in '/etc/.*.backups.yml' if DATABASE_NAME not set.") do |db| options[:db] = db.split(',') unless db.nil? or db == '' options[:command] = :new_backup end opts.on("-c", "--config CONFIG", "Use config file.") do |config| options[:config] = config end opts.on("-b", "--bucket BUCKET", "Override default S3 bucket name. (Be Careful!)") do |bucket| options[:backup_bucket] = bucket end opts.on("-t", "--tmp_dir TMPDIR", "Use the given directory for temporary storage.") do |tmp_dir| options[:tmp_dir] = tmp_dir end opts.on("-k", "--key KEYID", "Public key ID to use for the backup operation") do |key_id| options[:key_id] = key_id end opts.on("-q", "--quiet", "Suppress output to STDOUT") do options[:quiet] = true end opts.on("-s", "--split_size INTEGER", "Maximum size of a single backup file before splitting.") do |split_size| options[:split_size] = split_size.to_i end opts.on("-v", "--verbose", "Show verbose output") do options[:verbose] = true end opts.on("-d", "--download BACKUP_INDEX", "Download the backup specified by index.", ' Run `eybackup -l #{db_name}` to get the index.', ' BACKUP_INDEX uses the format #{index_number}:#{db_name}') do |index_and_db| options[:command] = :download index, db = split_index(index_and_db) index = index.to_i options[:index] = index options[:db] = db raise OptionParser::InvalidArgument, "Index '#{index_and_db}' is not a valid format (hint: :)!" if index.nil? or not index.is_a? Numeric end opts.on("-r", "--restore BACKUP_INDEX", "Download and apply the backup specified by index.", " **WARNING!** will overwrite the current db with the backup.", ' Run `eybackup -l #{db_name}` to get the index.', ' BACKUP_INDEX uses the format #{index_number}:#{db_name}') do |index_and_db| options[:command] = :restore index, db = split_index(index_and_db) index = index.to_i options[:index] = index options[:db] = db raise OptionParser::InvalidArgument, "Index '#{index_and_db}' is not a valid format (hint: :)!" if index.nil? or not index.is_a? Numeric end options[:force] = false opts.on("-f", "--force", "Force backup restore, bypass confirmation prompts.", " For use with automated restore operations (e.g. Staging).") do options[:force] = true end opts.on("--log_coordinates", "Record MySQL binary log position so backup can be used with replication or Point in Time functions.") do options[:log_coordinates] = true end opts.on("--allow_concurrent", "Allow eybackup process to run concurrently with other eybackup processes.") do options[:allow_concurrent] = true end opts.on("--skip_analyze", "Skip automatic analyze during PostgreSQL Restore operations.") do options[:skip_analyze] = true end end begin optparse.parse!(argv) rescue => e puts optparse raise end options end def split_index(index) index.split(':') end def config_for(filename) if File.exist?(filename) # Make the loaded config have symbols rather than strings config_orig = YAML::load(File.read(filename)) config = {} config_orig.each do |key, val| config[key.to_sym] = val end config[:environment] ||= config[:env] config else abort "You need to have a backup file at #{filename}" end end end end end