Last active
August 29, 2015 14:06
-
-
Save leschenko/3de940689e6787165de1 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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