backup revision 4fbbf628eda08ffdeb98cb41fb7e9001050eec3a
abc834c020080fe44a1ea4e34278327e99e3e12eEugen Kuksa# You can find more extensive documentation of this script at
abc834c020080fe44a1ea4e34278327e99e3e12eEugen Kuksa# https://github.com/ontohub/ontohub/blob/staging/doc/backup_and_restore_of_ontohub_data.md
f0264afd33a980b6584747fc8159ee950805d9e3Eugen Kuksa# Description
f0264afd33a980b6584747fc8159ee950805d9e3Eugen Kuksa# This backup script creates and restores backups of ontohub data. It includes:
f0264afd33a980b6584747fc8159ee950805d9e3Eugen Kuksa# * bare git repositories (data/repositories)
f0264afd33a980b6584747fc8159ee950805d9e3Eugen Kuksa# * named symlinks to git repositories (data/git_daemon)
f0264afd33a980b6584747fc8159ee950805d9e3Eugen Kuksa# * the postgres database
0978722f6ca27f5d5f5ed4ec8400703dfe211184Eugen Kuksa# First note: Run this as the root user, e.g. with sudo.
f0264afd33a980b6584747fc8159ee950805d9e3Eugen Kuksa# To create a backup, run this script with the argument `create`:
f0264afd33a980b6584747fc8159ee950805d9e3Eugen Kuksa# Then a backup named with the current date and time is created in the
f0264afd33a980b6584747fc8159ee950805d9e3Eugen Kuksa# backup directory (see below).
f0264afd33a980b6584747fc8159ee950805d9e3Eugen Kuksa# To restore a backup, run this script with the argument `restore <backup name>`
f0264afd33a980b6584747fc8159ee950805d9e3Eugen Kuksa# # script/backup restore 2015-01-01_00-00
f0264afd33a980b6584747fc8159ee950805d9e3Eugen Kuksa# Then the selected backup is fully restored
f0264afd33a980b6584747fc8159ee950805d9e3Eugen Kuksa# Backup directory
f0264afd33a980b6584747fc8159ee950805d9e3Eugen Kuksa# For development machines, the backup directory is:
f0264afd33a980b6584747fc8159ee950805d9e3Eugen Kuksa# <rails root>/tmp/backup/
f0264afd33a980b6584747fc8159ee950805d9e3Eugen Kuksa# And for production machines, the backup directory is:
f0264afd33a980b6584747fc8159ee950805d9e3Eugen Kuksa# Super user privileges
f0264afd33a980b6584747fc8159ee950805d9e3Eugen Kuksa# To create and restore, we need root privileges. Otherwise file modes are not
f0264afd33a980b6584747fc8159ee950805d9e3Eugen Kuksa# preserved. This script will call `sudo` when needed and inform you about the
f0264afd33a980b6584747fc8159ee950805d9e3Eugen Kuksa# reason for calling `sudo`. If you don't allow sudo, a backup will be created
f0264afd33a980b6584747fc8159ee950805d9e3Eugen Kuksa# or restored anyway, but the file modes and ownership are not preserved.
f0264afd33a980b6584747fc8159ee950805d9e3Eugen Kuksa# Then, you need to adjust them manually.
f0264afd33a980b6584747fc8159ee950805d9e3Eugen Kuksa# Maintenance mode
f0264afd33a980b6584747fc8159ee950805d9e3Eugen Kuksa# While backing up and restoring the data, the maintenance mode is activated.
f0264afd33a980b6584747fc8159ee950805d9e3Eugen Kuksa# This way we guarantee data consistency of the backup.
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksarequire 'fileutils'
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksarequire 'pathname'
28001d576e67ba46ed481c5695f1e0827ff26007Eugen Kuksarequire 'open3'
5efadb4662f2a63d5f5f1a5b303ab7c3371069a8Eugen Kuksa # Amount of backups that have to be there at least
d89f470f7da0b9f8295d0ac0defff09884894b8bEugen Kuksa # Backups are kept for at least 365 days
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa # Use 'sudo' on most systems
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa attr_reader :db_name, :data_root, :backup_root, :backup_instance_dir
366ce8d807067a97613cb23d49105d8a093c5015Eugen Kuksa attr_reader :dry_run, :verbose, :sql_dump_as_db_user
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa verbose: false, dry_run: true, sql_dump_as_db_user: nil, user: nil)
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa @data_dirs = DATA_DIRS.map { |dir| File.join(@data_root_basename, dir) }
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa # We needed to create the directory for the script to continue later on.
5efadb4662f2a63d5f5f1a5b303ab7c3371069a8Eugen Kuksa puts "Restored backup from #{backup_instance_dir}"
5efadb4662f2a63d5f5f1a5b303ab7c3371069a8Eugen Kuksa $stderr.puts "Nothing to prune: There is no backup directory."
5efadb4662f2a63d5f5f1a5b303ab7c3371069a8Eugen Kuksa backup_dirs_allowed_to_delete(Dir.new(backup_root).entries).each do |dir|
5efadb4662f2a63d5f5f1a5b303ab7c3371069a8Eugen Kuksa if now - File.new(backup).ctime > BACKUPS_VALIDITY_TIME
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa @backup_instance_dir = backup_root.join(new_backup_name)
4ec9d8b62c3c1a001548eb0883b6f81e00c391a0Eugen Kuksa puts "FileUtils.mkdir_p #{backup_instance_dir}" if verbose
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa # Create directory even in dry run to let the script continue.
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa exec('pg_dump', *pg_user_switch, '-Fc', db_name, '-f', SQL_DUMP_FILE,
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa archive_file = backup_instance_dir.join(REPOSITORY_FILE)
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa exec('tar', verbose ? '-v' : '', '-cf', archive_file.to_s, *@data_dirs,
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa @backup_instance_dir = backup_root.join(backup_name)
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa "Error: Backup '#{backup_name}' does not exist in #{backup_root}.")
b51057b860560bf3ee454c03a121af3d5d34f482Eugen KuksaAn error occured while restoring the repositories:
b51057b860560bf3ee454c03a121af3d5d34f482Eugen KuksaYou can find the pre-restore repositories at #{tmpdir}
b51057b860560bf3ee454c03a121af3d5d34f482Eugen KuksaDo something about it.
b51057b860560bf3ee454c03a121af3d5d34f482Eugen Kuksa def move_data_dirs_to_tmpdir(tmpdir)
b51057b860560bf3ee454c03a121af3d5d34f482Eugen Kuksa puts "FileUtils.mv(#{@data_dirs}, #{tmpdir})" if verbose
b51057b860560bf3ee454c03a121af3d5d34f482Eugen Kuksa FileUtils.mv(@data_dirs, tmpdir) unless dry_run
b51057b860560bf3ee454c03a121af3d5d34f482Eugen Kuksa rescue Errno::EACCES
284432981d641cf3d679841f75acbcf039d83062Eugen KuksaAs the current user I have no access to move the repository data
b51057b860560bf3ee454c03a121af3d5d34f482Eugen Kuksadirectories #{@data_dirs.join(' ')} to a temporary directory #{tmpdir}.
b51057b860560bf3ee454c03a121af3d5d34f482Eugen KuksaThis is used as a backup for the case of an error while restoring.
6cdc461aada609d57d50ff675d29b15378717ff2Eugen KuksaTo continue, I try the command again using sudo.
b51057b860560bf3ee454c03a121af3d5d34f482Eugen Kuksa exec('mv', *@data_dirs, tmpdir, user: 'root')
b51057b860560bf3ee454c03a121af3d5d34f482Eugen Kuksa def extract_archive
b51057b860560bf3ee454c03a121af3d5d34f482Eugen Kuksa archive_file = backup_instance_dir.join(REPOSITORY_FILE)
b51057b860560bf3ee454c03a121af3d5d34f482Eugen KuksaSuper user privileges are needed to reset the file permissions as
b51057b860560bf3ee454c03a121af3d5d34f482Eugen Kuksathey were before the backup. If you refuse to enter the password
b51057b860560bf3ee454c03a121af3d5d34f482Eugen Kuksa(Ctl-C) or enter a wrong password, only the permissions will not be
4ec9d8b62c3c1a001548eb0883b6f81e00c391a0Eugen Kuksarestored and all restored files will belong to the current user/group.
b51057b860560bf3ee454c03a121af3d5d34f482Eugen Kuksa try_as_sudo_with_fallback('tar', verbose ? '-v' : '', '-xf',
b51057b860560bf3ee454c03a121af3d5d34f482Eugen Kuksa archive_file.to_s, *@data_dirs)
b51057b860560bf3ee454c03a121af3d5d34f482Eugen Kuksa def remove_tmpdir(tmpdir)
b51057b860560bf3ee454c03a121af3d5d34f482Eugen Kuksa puts "FileUtils.remove_entry(#{tmpdir})" if verbose
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa FileUtils.remove_entry(tmpdir) # even do this in dry run
4ec9d8b62c3c1a001548eb0883b6f81e00c391a0Eugen Kuksa rescue Errno::EACCES
bacf95fddc53ab3107f380c4c816fa8072358bd9Eugen KuksaAs the current user I have no access to remove the temporary
bacf95fddc53ab3107f380c4c816fa8072358bd9Eugen Kuksadirectory #{tmpdir}.
bacf95fddc53ab3107f380c4c816fa8072358bd9Eugen KuksaTo continue, I try the command again using sudo.
486df98bbf3348cfb96e93c3e499d12435880bb5Eugen Kuksa exec('rm', '-r', tmpdir, user: 'root')
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa def enable_maintenance_mode
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa puts 'Enabling maintenance mode...'
4ec9d8b62c3c1a001548eb0883b6f81e00c391a0Eugen Kuksa if File.exist?(maintenance_file)
486df98bbf3348cfb96e93c3e499d12435880bb5Eugen Kuksa $stderr.puts 'Maintenance mode was already enabled.'
486df98bbf3348cfb96e93c3e499d12435880bb5Eugen Kuksa $stderr.puts "Please check the file #{maintenance_file}"
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa $stderr.puts 'Aborting.'
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa puts "FileUtils.touch #{maintenance_file}" if verbose
28001d576e67ba46ed481c5695f1e0827ff26007Eugen Kuksa FileUtils.touch maintenance_file unless dry_run
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa def disable_maintenance_mode
486df98bbf3348cfb96e93c3e499d12435880bb5Eugen Kuksa puts 'Disabling maintenance mode...'
28001d576e67ba46ed481c5695f1e0827ff26007Eugen Kuksa puts "FileUtils.rm #{maintenance_file}" if verbose
dd553f2f8b8abb774ba64a4fb9ebe3abea9f7f17Eugen Kuksa FileUtils.rm maintenance_file unless dry_run
b51057b860560bf3ee454c03a121af3d5d34f482Eugen Kuksa # Execute a command as the given user.
b51057b860560bf3ee454c03a121af3d5d34f482Eugen Kuksa def exec(*args, user: nil)
28001d576e67ba46ed481c5695f1e0827ff26007Eugen Kuksa puts "[executing next command in #{Dir.getwd}]" if verbose
28001d576e67ba46ed481c5695f1e0827ff26007Eugen Kuksa out = args.join(' ')
b51057b860560bf3ee454c03a121af3d5d34f482Eugen Kuksa puts out if verbose
28001d576e67ba46ed481c5695f1e0827ff26007Eugen Kuksa if user == 'root'
b51057b860560bf3ee454c03a121af3d5d34f482Eugen Kuksa system([sudo, *args])
b51057b860560bf3ee454c03a121af3d5d34f482Eugen Kuksa system([sudo, 'su', '-', user, '-c', escape_arguments(args)])
486df98bbf3348cfb96e93c3e499d12435880bb5Eugen Kuksa system(*args)
5efadb4662f2a63d5f5f1a5b303ab7c3371069a8Eugen Kuksa def escape_arguments(args)
5efadb4662f2a63d5f5f1a5b303ab7c3371069a8Eugen Kuksa ([args[0]] + *args[1..-1].map { |a| %("#{a}")}).join(' ')
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa def maintenance_file
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa data_root.join(MAINTENANCE_FILE)
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa def pg_user_switch
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa sql_dump_as_db_user ? %W(-U #{sql_dump_as_db_user}) : []
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa def self.backup_dirs_allowed_to_delete(entries)
0978722f6ca27f5d5f5ed4ec8400703dfe211184Eugen Kuksa entries.reject{ |entry| %w(. ..).include?(entry) }[0..-(BACKUPS_COUNT+1)]
0978722f6ca27f5d5f5ed4ec8400703dfe211184Eugen Kuksadef data_root(rails_root)
0978722f6ca27f5d5f5ed4ec8400703dfe211184Eugen Kuksa if on_development_system?(rails_root)
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa File.realpath(rails_root.join('data'))
1724834ae596f84e8237a3bba5a8af15916a8ee9Eugen Kuksa ENV['DATA_ROOT'] ||'/data/git'
df4ff7ec6be98e7ae5830731becc1a3d55105378Eugen Kuksadef on_development_system?(rails_root)
df4ff7ec6be98e7ae5830731becc1a3d55105378Eugen Kuksa data_path = rails_root.join('data')
df4ff7ec6be98e7ae5830731becc1a3d55105378Eugen Kuksa File.exist?(data_path) && !File.symlink?(data_path)
1724834ae596f84e8237a3bba5a8af15916a8ee9Eugen Kuksa# Don't allow this to be run as the root user.
1724834ae596f84e8237a3bba5a8af15916a8ee9Eugen Kuksaif ENV['USER'] != 'root'
df4ff7ec6be98e7ae5830731becc1a3d55105378Eugen Kuksa puts 'Running this script as a normal user is disabled.'
df4ff7ec6be98e7ae5830731becc1a3d55105378Eugen Kuksa puts 'Please run it as root.'
b1fe9054ad7c7192fe4c474363247dad15963e99Eugen Kuksa# We assume, this script runs in "RAILS_ROOT/script/".
366ce8d807067a97613cb23d49105d8a093c5015Eugen KuksaRAILS_ROOT = Pathname.new(__FILE__).dirname.join('..')
daf3e28fff47a65b53d6fb65155301763b9f166eEugen KuksaBACKUP_ROOT_PRODUCTION = '/local/home/ontohub/ontohub_data_backup'
daf3e28fff47a65b53d6fb65155301763b9f166eEugen KuksaUSER = 'ontohub'
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa if on_development_system?(RAILS_ROOT)
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa 'ontohub_development'
daf3e28fff47a65b53d6fb65155301763b9f166eEugen KuksaBACKUP_ROOT =
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa if on_development_system?(RAILS_ROOT)
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa RAILS_ROOT.join('tmp', 'backup')
5efadb4662f2a63d5f5f1a5b303ab7c3371069a8Eugen Kuksa File.realpath(BACKUP_ROOT_PRODUCTION)
923d69139038e74c0936e826bbfdc8717fbbc7b3Eugen Kuksabackup = Backup::Backup.new(DATABASE, data_root(RAILS_ROOT), BACKUP_ROOT,
daf3e28fff47a65b53d6fb65155301763b9f166eEugen Kuksa sql_dump_as_db_user: on_development_system?(RAILS_ROOT) ? 'postgres' : 'ontohub',