Skip to content

Instantly share code, notes, and snippets.

@leschenko
Last active August 29, 2015 14:06
Show Gist options
  • Save leschenko/3de940689e6787165de1 to your computer and use it in GitHub Desktop.
Save leschenko/3de940689e6787165de1 to your computer and use it in GitHub Desktop.
#!/usr/bin/ruby
require 'optparse'
require 'yaml'
require 'fileutils'
# === config/config.yml ===
#dump:
# database:
# environments:
# production: true
# development: false
# storage: '/var/backups/mysql'
# remote_storage: 'backup:/var/backups/mysql'
# expire: 20
# content:
# locations: []
# storage: '/var/backups/content'
# remote_storage: 'backup:/var/backups/content'
# expire: 10
# emails:
# - email1@example.com
# - email2@example.com
# Usage
# /dump.rb -t content --dry_run --rsync '--bwlimit=3000'
$tenant = ENV['TENANT']
class Hash
def symbolize_keys
each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
end
end
def say(str)
puts "[#{Time.now}] #{str}"
end
def error(msg)
puts msg
mail(msg)
end
def mail(msg)
emails = $opts[:emails] ? $opts[:emails].join(' ') : 'email1@example.com'
run(%Q[echo "[#{Time.now}] #{msg}" | sendmail -f email1@example.com #{emails}])
end
def run(cmd, must=false)
if $opts[:dry_run]
puts cmd
true
else
puts cmd if $opts[:v]
res = system(cmd)
error("run #{cmd}") if !res && must
res
end
end
def check_dir(dir)
run("mkdir -p #{dir}", true)
end
def rsync(from, to, after_opts='')
cmd = ''
cmd << "trickle -u #{$opts[:speed_limit]} -d #{$opts[:speed_limit]} " if $opts[:speed_limit]
cmd << "rsync -a #{$opts[:rsync]} #{from} #{to} #{after_opts}"
if run(cmd)
say "success dump sync #{from} #{to}"
else
error "error dump sync #{from} #{to}"
end
end
class BackupProjects
TYPES = %w(db content)
def initialize
parse_opts
find_projects
backup!
end
def find_projects(projects_dir = '/var/www')
@projects = []
Dir["#{projects_dir}/*"].each do |dir|
conf_path = File.join(dir, "config/config#{$tenant}.yml")
@projects << Project.new(dir, YAML.load_file(conf_path)['dump']) if File.exists?(conf_path)
end
end
def backup!
@projects.each do |project|
$opts[:t].each do |t|
project.send("backup_#{t}")
end
end
end
def parse_opts
opts = ARGV.getopts('t:', 'rsync:', 'speed_limit:', 'dry_run', 'v').symbolize_keys
if opts[:t]
opts[:t] = opts[:t].split(',')
else
opts[:t] = TYPES
end
unless opts[:t].all? { |t| TYPES.include?(t) }
raise "invalid types #{opts[:t]}"
end
p opts if opts[:v]
$opts = opts
end
class Project
attr_reader :path, :config
def initialize(path, config)
@path = path
@config = config
@db = @config['database']
@content = @config['content']
end
def backup_db
say "start db backup #{@path}"
@db_conf = YAML.load_file(File.join(@path, "config/database#{$tenant}.yml"))
@db['environments'].each do |env, perform|
next unless perform
dump_db(env)
end
if @db['environments'].any?
remove_old_db
rsync("#{@db['storage']}/", @db['remote_storage']) if @db['remote_storage']
end
end
def dump_db(env)
conf = @db_conf[env]
unless conf
say "#{@path} no db config #{env}"
return
end
check_dir(@db['storage'])
mysqldump(conf, env)
end
def remove_old_db
if @db['expire'] == false
say("skip db #{@path} expiration")
return
end
run "find #{@db['storage']} -name '*.sql.gz' -mtime +#{@db['expire'] || 20} -delete"
end
def mysqldump(conf, env='production')
file_name = File.join(@db['storage'], "#{Time.now.strftime('%d_%m_%Y_%H_%M')}_#{conf['database']}_#{env}.sql")
options = []
options << "-u #{conf['username']}" if conf['username']
options << "-h #{conf['host']}" if conf['host']
options << "-p'#{conf['password']}'" if conf['password']
if run("mysqldump --single-transaction #{options.join(' ')} #{conf['database']} > #{file_name} && gzip -5 #{file_name}")
say "success dump #{conf['database']}"
else
error "error dump #{conf['database']}"
end
end
def backup_content
say "start content backup #{@path}"
@content['locations'].each do |location|
from = File.join(@path, location).sub(/\/$/, '')
excl = '--exclude tmp'
rsync(from, @content['storage'], excl) if @content['storage']
rsync(from, @content['remote_storage'], excl) if @content['remote_storage']
end
remove_old_content unless @content['locations'].empty?
end
def remove_old_content
if @content['expire'] == false
say("skip content #{@path} expiration")
return
end
return unless @content['storage']
run "find #{@content['storage']} -maxdepth 1 -mtime +#{@content['expire'] || 10} -type d -print0 | xargs -0 /bin/rm -rf"
end
end
end
BackupProjects.new
# clean old dumps in remote ctorage
# 30 1 * * * find /var/backups -mtime +20 -name '*.sql.gz' | grep -v 2014_03 | xargs rm -f
# 0 5 * * * /root/scripts/dump.rb -v -t db >> /var/backups/backup.log
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment