From Pojo.us

Jump to: navigation, search

#!/usr/bin/env ruby
#
# Amazon EBS Snapshot creation and management
# http://blog.nrgup.net/2009/04/amazon-ec2-mysql-script-for-gfs-snapshots/
# Original Author: Jonathan Bradshaw
# Other Contributors:
# Ben La Monica - Modified to use a config file; Allow multiple EBS volumes per mount point (for LVM/Raid Volumes); Only flush database if it is running;
# Michael Glass - Fixed bug with config values and made general ruby improvements; Made db-user and db-password optional in config file;
 
%w(logger optparse rubygems uuidtools right_aws sdb/active_sdb mysql net/http).each { |l| require l }
 
# hack to eliminate the SSL certificate verification notification
class Net::HTTP
  alias_method :old_initialize, :initialize
  def initialize(*args)
    old_initialize(*args)
    @ssl_context = OpenSSL::SSL::SSLContext.new
    @ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
  end
end
 
# class used for logging snapshot meta data to simpledb
class Snapshot < RightAws::ActiveSdb::Base
    set_domain_name :snapshots
end

class MySQLDatabaseType
  @username
  @password
  @expire
  @connection
  @logger
  @is_running
  
  def initialize(username, password, expire, logger)
    @username = username
    @password = password
    @expire = expire
    @logger = logger
    
    running = `ps aux |grep "/mysqld " | grep -v grep`
    @is_running = (!running.nil? and running.strip != "")
  end
  
  def running?
    return @is_running  
  end

  def connect
     @connection = Mysql::new('localhost', @username, @password)
  end

  def lock
    self.connect if @connection.nil?
    @logger.info("Locking and flushing MySQL tables")
    @connection.query('FLUSH TABLES WITH READ LOCK;')
    @connection.query('FLUSH LOGS;')
  end
  
  def unlock
    self.connect if @connection.nil?
    @logger.info("Unlocking MySQL tables")
    @connection.query('UNLOCK TABLES;')
  end
  
  def cleanup
    self.connect if @connection.nil?
    purgetime = (Time.now - (86400 * @expire.to_i)).strftime('%Y-%m-%d %H:%M:%S')
    @logger.info("Purging log files older than #{purgetime}")
    @connection.query("PURGE BINARY LOGS BEFORE '#{purgetime}';")
  end
  
  def close
    @connection.close unless @connection.nil?
  end
end

#
# Main application
#
logger = Logger.new("/var/log/snapshot.log")
logger.datetime_format = "%Y-%m-%d %H:%M:%S"
logger.level = Logger::INFO

key_names = {"sdb-access-key" => :aws_sdb_access_key_id,
        "sdb-secret-key" => :aws_sdb_secret_access_key,
        "ebs-access-key" => :aws_ebs_access_key_id,
        "ebs-secret-key" => :aws_ebs_secret_access_key,
        "devices" => :ebs_devices,
        "mountpoint" => :mountpoint,
        "db-user" => :db_user,
        "db-password" => :db_password,
        "expire" => :expire} 

 
options = {}
parser = OptionParser.new do |p|
    p.banner = "Usage: snapshot.rb [options]"
    p.separator ""
    p.separator "Specific options:"
 
    p.on("-c", "--config CONFIG_FILE", "name=value config file; Keys are:\n\t" + key_names.keys.join("\n\t")) do |conf|
            File.open(conf).each do |line|
                line.strip!
                next if line.empty?
                args = line.split("=");
                val = args[1].strip
                key = key_names[args[0]]
                unless key.nil?
                  puts("Setting #{key}.")
                  options[key] = val
                else
                  puts("Key '#{args[0]}' was not found")
                end
            end
            # convert the comma separated list into an array
            options[:ebs_devices] = options[:ebs_devices].squeeze(" ").strip.split(",")
          end
    p.on("-e",
         "--expire number",
         "The number of days for this snapshot to live (0 = forever)") do |expire|
            options[:expire] = expire
        end
 
    p.on_tail("-h", "--help", "Show this message") {
        puts(p)
        exit
    }
    p.parse!(ARGV) rescue puts(p)
end
 

# check and make sure all of our parameters are set
all_vars_set = true
missing_keys = []
key_names.keys.each do |key_name|
  unless options.key?(key_names[key_name])
		if ['db-user', 'db-password'].include? key_name
      next if !options.key?( key_names['db-user'] ) && !options.key?( key_names['db-password'] )
		end
    missing_keys << key_name
    all_vars_set = false
  end
end

#  all_vars_set &= options.key?(key_names[key_name])

unless all_vars_set
  puts(parser)
  puts("Missing keys: #{missing_keys.join(', ')}\n")
  exit(1)
end

# Determine our instance ID
instance_id = Net::HTTP.get('169.254.169.254', '/latest/meta-data/instance-id')

puts("InstanceId = #{instance_id}")

# Create SimpleDB connection to AWS
RightAws::ActiveSdb.establish_connection(options[:aws_sdb_access_key_id], options[:aws_sdb_secret_access_key], :logger => logger)
 
# Create (if it doesn't exist) our SimpleDB database domain
Snapshot.create_domain
 
# Create a connection to AWS
ec2 = RightAws::Ec2.new(options[:aws_ebs_access_key_id], options[:aws_ebs_secret_access_key], :logger => logger)
 
# Locate the volume attached to the specified device (e.g. /dev/sdj)
volumes = ec2.describe_volumes.find_all { |vol| vol[:aws_instance_id] == instance_id and
                                           options[:ebs_devices].include?(vol[:aws_device]) }
if (volumes.nil? or volumes.length == 0)
   logger.error("Error: Unable to find #{options[:ebs_devices]} device attached to this instance #{instance_id}")
   exit(1)
end
 
# Determine which file system is mounted on the EBS device
mountpoint = options[:mountpoint]
 
# Log the volume id, devie and mountpoint
volumes.each do |volume|
  logger.info("Volume #{volume[:aws_id]} attached as #{volume[:aws_device]} at #{mountpoint}")
end

db = MySQLDatabaseType.new(options[:db_user], options[:db_password], options[:expire], logger) if options.key? :db_user

begin
  # if the database is running, lock it so that nobody can write to it
  db.lock if db && db.running?
   
  # freeze the filesystem, to prevent writes while the snapshot is taking place
  logger.info("Freezing filesystem #{mountpoint}")
  system("xfs_freeze -f #{mountpoint}")
  
  # loop through the EBS volumes, there may be several for LVM or raided volumes
  volumes.each do |volume|
    # Create EBS snapshot
    result = ec2.create_snapshot(volume[:aws_id])
     
    # Create snapshot entry in domain with expiration date
    if (options[:expire].to_i > 0)
       expires = Time.now + (86400 * options[:expire].to_i)
       Snapshot.create 'snapshotid' => result[:aws_id], 'volumeid' =>  result[:aws_volume_id], 'expires' => expires.to_i
       logger.info("Created snapshot #{result[:aws_id]} for #{result[:aws_volume_id]} expires #{expires}")
    else
       logger.info("Created snapshot #{result[:aws_id]} for #{result[:aws_volume_id]} (no expiration)")
    end
  end
ensure
  #make sure we make the filesystem writable again!
  logger.info("Unfreezing filesystem #{mountpoint}")
  system("xfs_freeze -u #{mountpoint}")
  
  # and unfreeze the database as well if it is running
  if (db && db.running?)
    begin
      db.unlock
      db.cleanup
    ensure
      db.close
    end
  end
end
  
# Purge expired snapshots
volumes.each do |volume|
  logger.info("Querying for old snapshots for volume: #{volume[:aws_id]}")
  # only delete snapshots for the volumes that we were working on
  Snapshot.select(:all, :conditions=> [ "expires<? and volumeid=?", Time.now.to_i, volume[:aws_id]]).each do |snap|
     snap.reload
     logger.info("Removing expired EBS snapshot #{snap[:snapshotid]}")
     begin
       ec2.delete_snapshot(snap[:snapshotid])
     rescue
       logger.error("Unable to delete snapshot #{snap[:snapshotid]}, please delete manually.")
       logger.error($!);
     ensure
       snap.delete
     end
  end  
end

Example Configuration File


sdb-access-key=<INSERT_YOUR_SIMPLE_DB_ACCESS_KEY_HERE>
sdb-secret-key=<INSERT_YOUR_SIMPLE_DB_SECRET_ACCESS_KEY_HERE>
ebs-access-key=<INSERT_YOUR_EC2_ACCESS_KEY_HERE>
ebs-secret-key=<INSERT_YOUR_EC2_SECRET_ACCESS_KEY_HERE>
devices=/dev/sdb,/dev/sdc,/dev/sdd
mountpoint=/data
db-user=<INSERT YOUR DATABASE DBA USER HERE>
db-password=<INSERT YOUR DATABASE DBA PASSWORD HERE>

Crontab

Add this to your root crontab for automated backups. This script will take hourly/daily/weekly/monthly snapshots of your EBS storage. (make sure to change the path to your script and config file if it is in a different place)


# m h  dom mon dow   command
# Keep 1 day of hourly, 7 days of daily, 4 weeks of weekly and 1 year of monthly snapshots
00 * * * * /data/backup/snapshot.rb -e 1 -c /data/backup/snapshot.cfg
15 0 * * * /data/backup/snapshot.rb -e 7 -c /data/backup/snapshot.cfg
30 0 * * 0 /data/backup/snapshot.rb -e 28 -c /data/backup/snapshot.cfg
45 0 1 * * /data/backup/snapshot.rb -e 365 -c /data/backup/snapshot.cfg