backup revision b4ac7fd47818fbcba8d344e3de41ca62e1473b94
0db2f3402b37df221fbf55486769989f82df479aPavel Březina#!/usr/bin/env ruby
0db2f3402b37df221fbf55486769989f82df479aPavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březinarequire 'tmpdir.rb'
0db2f3402b37df221fbf55486769989f82df479aPavel Březinarequire 'fileutils'
0db2f3402b37df221fbf55486769989f82df479aPavel Březinarequire 'pathname'
0db2f3402b37df221fbf55486769989f82df479aPavel Březinarequire 'open3'
0db2f3402b37df221fbf55486769989f82df479aPavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březinamodule Backup
0db2f3402b37df221fbf55486769989f82df479aPavel Březina class Backup
0db2f3402b37df221fbf55486769989f82df479aPavel Březina # Amount of backups that have to be there at least
0db2f3402b37df221fbf55486769989f82df479aPavel Březina BACKUPS_COUNT = 30
0db2f3402b37df221fbf55486769989f82df479aPavel Březina # Backups are kept for at least 365 days
0db2f3402b37df221fbf55486769989f82df479aPavel Březina BACKUPS_VALIDITY_TIME = 365 * 60 * 60 * 24
0db2f3402b37df221fbf55486769989f82df479aPavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březina MAINTENANCE_FILE = 'maintenance.txt'
0db2f3402b37df221fbf55486769989f82df479aPavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březina SQL_DUMP_FILE = 'ontohub_sql_dump.postgresql'
0db2f3402b37df221fbf55486769989f82df479aPavel Březina REPOSITORY_FILE = 'ontohub_repositories.tar.gz'
0db2f3402b37df221fbf55486769989f82df479aPavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březina DATA_DIRS = %w(data/repositories data/git_daemon)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březina attr_reader :db_name, :data_root, :backup_root, :backup_instance_dir
0db2f3402b37df221fbf55486769989f82df479aPavel Březina attr_reader :dry_run, :verbose, :sql_dump_as_postgres_user
0db2f3402b37df221fbf55486769989f82df479aPavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březina def initialize(db_name, data_root, backup_root,
0db2f3402b37df221fbf55486769989f82df479aPavel Březina verbose: false, dry_run: true, sql_dump_as_postgres_user: false)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina @db_name = db_name
0db2f3402b37df221fbf55486769989f82df479aPavel Březina @data_root = Pathname.new(data_root)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina @backup_root = Pathname.new(backup_root)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březina @dry_run = dry_run
0db2f3402b37df221fbf55486769989f82df479aPavel Březina @verbose = verbose
0db2f3402b37df221fbf55486769989f82df479aPavel Březina @sql_dump_as_postgres_user = sql_dump_as_postgres_user
0db2f3402b37df221fbf55486769989f82df479aPavel Březina end
0db2f3402b37df221fbf55486769989f82df479aPavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březina def create
0db2f3402b37df221fbf55486769989f82df479aPavel Březina puts 'Creating backup...'
0db2f3402b37df221fbf55486769989f82df479aPavel Březina enable_maintenance_mode
0db2f3402b37df221fbf55486769989f82df479aPavel Březina initialize_backup
0db2f3402b37df221fbf55486769989f82df479aPavel Březina create_sql_dump
0db2f3402b37df221fbf55486769989f82df479aPavel Březina create_repository_archive
0db2f3402b37df221fbf55486769989f82df479aPavel Březina # We needed to create the directory for the script to continue later on.
0db2f3402b37df221fbf55486769989f82df479aPavel Březina Dir.rmdir(backup_instance_dir) if dry_run
0db2f3402b37df221fbf55486769989f82df479aPavel Březina disable_maintenance_mode
0db2f3402b37df221fbf55486769989f82df479aPavel Březina puts "Created backup in #{backup_instance_dir}"
0db2f3402b37df221fbf55486769989f82df479aPavel Březina self.class.prune(backup_root)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina end
0db2f3402b37df221fbf55486769989f82df479aPavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březina def restore(backup_name)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina enable_maintenance_mode
0db2f3402b37df221fbf55486769989f82df479aPavel Březina initialize_restore(backup_name)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina restore_sql_dump
0db2f3402b37df221fbf55486769989f82df479aPavel Březina restore_repository_archive
0db2f3402b37df221fbf55486769989f82df479aPavel Březina disable_maintenance_mode
0db2f3402b37df221fbf55486769989f82df479aPavel Březina puts "Restored backup from #{backup_instance_dir}"
0db2f3402b37df221fbf55486769989f82df479aPavel Březina end
0db2f3402b37df221fbf55486769989f82df479aPavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březina def self.prune(backup_root)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina if !Dir.exists?(backup_root)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina $stderr.puts "Nothing to prune: There is no backup directory."
0db2f3402b37df221fbf55486769989f82df479aPavel Březina return
0db2f3402b37df221fbf55486769989f82df479aPavel Březina end
0db2f3402b37df221fbf55486769989f82df479aPavel Březina now = Time.now
0db2f3402b37df221fbf55486769989f82df479aPavel Březina backup_dirs_allowed_to_delete(Dir.new(backup_root).entries).each do |dir|
0db2f3402b37df221fbf55486769989f82df479aPavel Březina backup = backup_root.join(dir)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina if now - File.new(backup).ctime > BACKUPS_VALIDITY_TIME
0db2f3402b37df221fbf55486769989f82df479aPavel Březina puts "removing old backup: #{dir}"
0db2f3402b37df221fbf55486769989f82df479aPavel Březina FileUtils.rm_r(backup)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina end
0db2f3402b37df221fbf55486769989f82df479aPavel Březina end
0db2f3402b37df221fbf55486769989f82df479aPavel Březina end
0db2f3402b37df221fbf55486769989f82df479aPavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březina protected
0db2f3402b37df221fbf55486769989f82df479aPavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březina def new_backup_name
0db2f3402b37df221fbf55486769989f82df479aPavel Březina Time.now.strftime("%Y-%m-%d_%H-%M-%S")
0db2f3402b37df221fbf55486769989f82df479aPavel Březina end
0db2f3402b37df221fbf55486769989f82df479aPavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březina def initialize_backup
0db2f3402b37df221fbf55486769989f82df479aPavel Březina @backup_instance_dir = backup_root.join(new_backup_name)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina puts "FileUtils.mkdir_p #{backup_instance_dir}" if verbose
0db2f3402b37df221fbf55486769989f82df479aPavel Březina # Create directory even in dry run to let the script continue.
0db2f3402b37df221fbf55486769989f82df479aPavel Březina FileUtils.mkdir_p(backup_instance_dir)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina end
0db2f3402b37df221fbf55486769989f82df479aPavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březina def create_sql_dump
0db2f3402b37df221fbf55486769989f82df479aPavel Březina puts 'Creating SQL dump...'
0db2f3402b37df221fbf55486769989f82df479aPavel Březina Dir.chdir(backup_instance_dir) do
0db2f3402b37df221fbf55486769989f82df479aPavel Březina exec('pg_dump', *pg_user_switch, '-Fc', db_name,
0db2f3402b37df221fbf55486769989f82df479aPavel Březina file_dest: SQL_DUMP_FILE)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina end
0db2f3402b37df221fbf55486769989f82df479aPavel Březina end
0db2f3402b37df221fbf55486769989f82df479aPavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březina def create_repository_archive
0db2f3402b37df221fbf55486769989f82df479aPavel Březina puts 'Creating repository archive...'
0db2f3402b37df221fbf55486769989f82df479aPavel Březina Dir.chdir(data_root.join('..')) do
0db2f3402b37df221fbf55486769989f82df479aPavel Březina archive_file = backup_instance_dir.join(REPOSITORY_FILE)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina exec('tar', verbose ? '-v' : '', '-czf', archive_file.to_s,
0db2f3402b37df221fbf55486769989f82df479aPavel Březina *DATA_DIRS.map(&:to_s))
0db2f3402b37df221fbf55486769989f82df479aPavel Březina end
0db2f3402b37df221fbf55486769989f82df479aPavel Březina end
0db2f3402b37df221fbf55486769989f82df479aPavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březina def initialize_restore(backup_name)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina @backup_instance_dir = backup_root.join(backup_name)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina unless Dir.exists?(backup_instance_dir)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina $stderr.puts (
0db2f3402b37df221fbf55486769989f82df479aPavel Březina "Error: Backup '#{backup_name}' does not exist in #{backup_root}.")
0db2f3402b37df221fbf55486769989f82df479aPavel Březina exit
0db2f3402b37df221fbf55486769989f82df479aPavel Březina end
0db2f3402b37df221fbf55486769989f82df479aPavel Březina end
0db2f3402b37df221fbf55486769989f82df479aPavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březina def restore_sql_dump
0db2f3402b37df221fbf55486769989f82df479aPavel Březina 'Restoring SQL dump...'
0db2f3402b37df221fbf55486769989f82df479aPavel Březina Dir.chdir(backup_instance_dir) do
0db2f3402b37df221fbf55486769989f82df479aPavel Březina exec('pg_restore', '-c', *pg_user_switch, '-d', db_name, SQL_DUMP_FILE)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina end
0db2f3402b37df221fbf55486769989f82df479aPavel Březina end
0db2f3402b37df221fbf55486769989f82df479aPavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březina def restore_repository_archive
0db2f3402b37df221fbf55486769989f82df479aPavel Březina puts 'Restoring repository archive...'
0db2f3402b37df221fbf55486769989f82df479aPavel Březina Dir.chdir(data_root.join('..')) do
0db2f3402b37df221fbf55486769989f82df479aPavel Březina tmpdir = Dir.mktmpdir
0db2f3402b37df221fbf55486769989f82df479aPavel Březina move_data_dirs_to_tmpdir(tmpdir)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina begin
0db2f3402b37df221fbf55486769989f82df479aPavel Březina extract_archive
0db2f3402b37df221fbf55486769989f82df479aPavel Březina remove_tmpdir(tmpdir)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina rescue => e
0db2f3402b37df221fbf55486769989f82df479aPavel Březina puts <<-MSG
0db2f3402b37df221fbf55486769989f82df479aPavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel BřezinaAn error occured while restoring the repositories:
0db2f3402b37df221fbf55486769989f82df479aPavel Březina#{e.message}
0db2f3402b37df221fbf55486769989f82df479aPavel BřezinaYou can find the pre-restore repositories at #{tmpdir}
0db2f3402b37df221fbf55486769989f82df479aPavel BřezinaDo something about it.
0db2f3402b37df221fbf55486769989f82df479aPavel Březina MSG
0db2f3402b37df221fbf55486769989f82df479aPavel Březina raise e
0db2f3402b37df221fbf55486769989f82df479aPavel Březina end
0db2f3402b37df221fbf55486769989f82df479aPavel Březina end
0db2f3402b37df221fbf55486769989f82df479aPavel Březina end
0db2f3402b37df221fbf55486769989f82df479aPavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březina def move_data_dirs_to_tmpdir(tmpdir)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina puts "FileUtils.mv(#{DATA_DIRS}, #{tmpdir})" if verbose
0db2f3402b37df221fbf55486769989f82df479aPavel Březina FileUtils.mv(DATA_DIRS, tmpdir) unless dry_run
0db2f3402b37df221fbf55486769989f82df479aPavel Březina rescue Errno::EACCES
0db2f3402b37df221fbf55486769989f82df479aPavel Březina puts <<-MSG
0db2f3402b37df221fbf55486769989f82df479aPavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel BřezinaAs the current user I have no access to move the repository data
0db2f3402b37df221fbf55486769989f82df479aPavel Březinadirectories #{DATA_DIRS.join(' ')} to a temporary directory #{tmpdir}.
0db2f3402b37df221fbf55486769989f82df479aPavel BřezinaThis is used as a backup for the case of an error while restoring.
0db2f3402b37df221fbf55486769989f82df479aPavel BřezinaTo continue, I try the command again using sudo.
0db2f3402b37df221fbf55486769989f82df479aPavel Březina MSG
0db2f3402b37df221fbf55486769989f82df479aPavel Březina exec('sudo', 'mv', *DATA_DIRS.map(&:to_s), tmpdir)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina end
0db2f3402b37df221fbf55486769989f82df479aPavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březina def extract_archive
4df7aec645f87342f3a5146062abcb15f71f4fd9Pavel Březina archive_file = backup_instance_dir.join(REPOSITORY_FILE)
4df7aec645f87342f3a5146062abcb15f71f4fd9Pavel Březina puts <<-MSG
4df7aec645f87342f3a5146062abcb15f71f4fd9Pavel Březina
4df7aec645f87342f3a5146062abcb15f71f4fd9Pavel BřezinaSuper user privileges are needed to reset the file permissions as
4df7aec645f87342f3a5146062abcb15f71f4fd9Pavel Březinathey were before the backup. If you refuse to enter the password
4df7aec645f87342f3a5146062abcb15f71f4fd9Pavel Březina(Crtl-C) or enter a wrong password, only the permissions will not be
4df7aec645f87342f3a5146062abcb15f71f4fd9Pavel Březinarestored and all restored files will belong to the current user/group.
4df7aec645f87342f3a5146062abcb15f71f4fd9Pavel Březina MSG
4df7aec645f87342f3a5146062abcb15f71f4fd9Pavel Březina try_as_sudo_with_fallback('tar', verbose ? '-v' : '', '-xzf',
4df7aec645f87342f3a5146062abcb15f71f4fd9Pavel Březina archive_file.to_s, *DATA_DIRS.map(&:to_s))
4df7aec645f87342f3a5146062abcb15f71f4fd9Pavel Březina end
4df7aec645f87342f3a5146062abcb15f71f4fd9Pavel Březina
4df7aec645f87342f3a5146062abcb15f71f4fd9Pavel Březina def remove_tmpdir(tmpdir)
4df7aec645f87342f3a5146062abcb15f71f4fd9Pavel Březina puts "FileUtils.remove_entry(#{tmpdir})" if verbose
4df7aec645f87342f3a5146062abcb15f71f4fd9Pavel Březina FileUtils.remove_entry(tmpdir) # even do this in dry run
4df7aec645f87342f3a5146062abcb15f71f4fd9Pavel Březina rescue Errno::EACCES
4df7aec645f87342f3a5146062abcb15f71f4fd9Pavel Březina puts <<-MSG
4df7aec645f87342f3a5146062abcb15f71f4fd9Pavel Březina
4df7aec645f87342f3a5146062abcb15f71f4fd9Pavel BřezinaAs the current user I have no access to remove the temporary
4df7aec645f87342f3a5146062abcb15f71f4fd9Pavel Březinadirectory #{tmpdir}.
4df7aec645f87342f3a5146062abcb15f71f4fd9Pavel BřezinaTo continue, I try the command again using sudo.
4df7aec645f87342f3a5146062abcb15f71f4fd9Pavel Březina MSG
8f895983e8d24b3edde4f695621f6b9a2fd20923Pavel Březina exec('sudo', 'rm', '-r', tmpdir)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina end
0db2f3402b37df221fbf55486769989f82df479aPavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březina def enable_maintenance_mode
ddfd1900b26c66a062457d4fcc1a48bafd3eadf6Pavel Březina puts 'Enabling maintenance mode...'
0db2f3402b37df221fbf55486769989f82df479aPavel Březina puts "FileUtils.touch #{maintenance_file}" if verbose
0db2f3402b37df221fbf55486769989f82df479aPavel Březina FileUtils.touch maintenance_file unless dry_run
3df5c41c19ef852021819954a2db1d067844d136Pavel Březina end
9c98397b6431b6b02bdfdb0540bac6a3eb00b0e3Pavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březina def disable_maintenance_mode
0db2f3402b37df221fbf55486769989f82df479aPavel Březina puts 'Disabling maintenance mode...'
0db2f3402b37df221fbf55486769989f82df479aPavel Březina puts "FileUtils.rm #{maintenance_file}" if verbose
0db2f3402b37df221fbf55486769989f82df479aPavel Březina FileUtils.rm maintenance_file unless dry_run
0db2f3402b37df221fbf55486769989f82df479aPavel Březina end
7be55c7de03da57f43fae3db7e6114eebb117a2ePavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březina def exec(*args, file_dest: nil)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina puts "[executing next command in #{Dir.getwd}]" if verbose
0db2f3402b37df221fbf55486769989f82df479aPavel Březina out = args.join(' ')
0db2f3402b37df221fbf55486769989f82df479aPavel Březina out << " > #{file_dest}" if file_dest
0db2f3402b37df221fbf55486769989f82df479aPavel Březina puts out if verbose
0db2f3402b37df221fbf55486769989f82df479aPavel Březina Subprocess.run(*args, file_dest: file_dest) unless dry_run
4df7aec645f87342f3a5146062abcb15f71f4fd9Pavel Březina end
4df7aec645f87342f3a5146062abcb15f71f4fd9Pavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březina def try_as_sudo_with_fallback(*args)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina _out, _err, exit_code = exec('sudo', *args)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina unless exit_code.success?
0db2f3402b37df221fbf55486769989f82df479aPavel Březina sudo_not_given_fallback(*args) # Wrong sudo password
0db2f3402b37df221fbf55486769989f82df479aPavel Březina end
0db2f3402b37df221fbf55486769989f82df479aPavel Březina rescue Exception => e
0db2f3402b37df221fbf55486769989f82df479aPavel Březina raise e unless e.is_a?(Interrupt) # Ctrl-C when asked for password
0db2f3402b37df221fbf55486769989f82df479aPavel Březina sudo_not_given_fallback(*args)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina end
0db2f3402b37df221fbf55486769989f82df479aPavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březina def sudo_not_given_fallback(*args)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina puts 'Super user privileges not granted. Trying as normal user.'
0db2f3402b37df221fbf55486769989f82df479aPavel Březina exec(*args)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina end
0db2f3402b37df221fbf55486769989f82df479aPavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březina def maintenance_file
0db2f3402b37df221fbf55486769989f82df479aPavel Březina data_root.join(MAINTENANCE_FILE)
0db2f3402b37df221fbf55486769989f82df479aPavel Březina end
0db2f3402b37df221fbf55486769989f82df479aPavel Březina
0db2f3402b37df221fbf55486769989f82df479aPavel Březina def pg_user_switch
0db2f3402b37df221fbf55486769989f82df479aPavel Březina sql_dump_as_postgres_user ? %w(-U postgres) : []
end
def self.backup_dirs_allowed_to_delete(entries)
entries.reject{ |entry| %w(. ..).include?(entry) }[0..-(BACKUPS_COUNT+1)]
end
end
class Subprocess
def self.run(*args, file_dest: nil)
stdin, stdout, stderr, wait_thr, io_dest = run_streaming(*args,
file_dest: file_dest)
exit_code = wait_thr.value # wait for the process to finish
io_dest.close if io_dest
[stdout.read, stderr.read, exit_code]
end
def self.run_streaming(*args, file_dest: nil)
stdin, stdout, stderr, wait_thr = Open3.popen3(*args)
io_dest = nil
if file_dest
io_dest = File.open(file_dest, 'w')
IO.copy_stream(stdout, io_dest)
end
[stdin, stdout, stderr, wait_thr, io_dest]
end
end
end
def data_root(rails_root)
File.realpath(rails_root.join('data'))
end
def on_development_system?(rails_root)
!File.symlink?(rails_root.join('data'))
end
# We assume, this script runs in "RAILS_ROOT/script/".
RAILS_ROOT = Pathname.new(__FILE__).dirname.join('..')
DATABASE = if on_development_system?(RAILS_ROOT)
'ontohub_development'
else
'ontohub'
end
BACKUP_ROOT = if on_development_system?(RAILS_ROOT)
RAILS_ROOT.join('tmp', 'backup')
else
File.realpath('/home/ontohub/ontohub_data_backup')
end
backup = Backup::Backup.new(DATABASE, data_root(RAILS_ROOT), BACKUP_ROOT,
sql_dump_as_postgres_user: on_development_system?(RAILS_ROOT),
dry_run: false, verbose: true)
case ARGV.first
when 'create'
backup.create
when 'restore'
if ARGV.length == 1
$stderr.puts(
'To restore a backup, you need to specify one with the arguments')
$stderr.puts('"restore backup_name"')
exit
end
backup_name = ARGV[1]
backup.restore(backup_name)
when 'prune'
Backup::Backup.prune(BACKUP_ROOT)
else
$stderr.puts 'unknown or missing parameter'
$stderr.puts 'use parameter "create" or "restore <backup_name>" or "prune"'
exit
end