#!/usr/bin/env ruby

require 'rubygems'
require 'optparse'
require 'fileutils'
require 'date'
require 'yaml'
require 'find'
require 'json'
require 'highline/import'

EXCLUDED="--exclude goferd,foreman-proxy,squid,smart_proxy_dynflow_core,qdrouterd,qpidd"
DATABASES = ['pulp', 'pgsql', 'mongodb']

@options = {}
@dir = nil
@databases = DATABASES.dup

# keep as variables for easy backporting
FOREMAN_PROXY_CONTENT = "capsule"
FOREMAN_PROXY = "capsule"

optparse = OptionParser.new do |opts|
  opts.banner = "Usage: katello-backup /path/to/dir [options]\n eg: $ katello-backup /tmp/katello-backup"

  opts.on("--skip-pulp-content", "Create backup without Pulp content for debugging only") do |config_only|
    @options[:config_only] = config_only
    @databases.delete 'pulp'
  end

  opts.on("--incremental PREVIOUS_BACKUP_DIR", String, "Backup changes since previous backup") do |dir_path|
    opts.abort("Please specify the previous backup directory.") unless dir_path
    if File.directory?(dir_path)
      @options[:incremental] = dir_path
    else
      opts.abort("Previous backup directory does not exist: #{dir_path}")
    end
  end

  opts.on("--online-backup", "Keep services online during backup") do |online|
    @options[:online] = online
  end

  opts.on("--logical-db-backup", "Also dump full database schema during offline backup") do |logical|
    @options[:logical_backup] = logical
  end

  opts.on("--features FEATURES", Array, "#{FOREMAN_PROXY.capitalize} features to include in the backup, please specify a list with commas. " \
          "Valid features are tftp, dns, dhcp, openscap, and all.") do |features|
    @options[:features] = features.map { |f| f.to_s.downcase }
  end

  opts.on("--preserve-directory", "Do not create a time-stamped subdirectory") do |no_subdir|
    @options[:no_subdir] = no_subdir
  end

  opts.on("-y", "--assumeyes", "Bypass interaction by answering yes") do |confirm|
    @options[:confirm] = confirm
  end
end

begin optparse.parse! ARGV
  if ARGV.length == 0
    optparse.abort("**** ERROR: Please specify an export directory ****")
  elsif ARGV.length != 1
    puts optparse
    exit(-1)
  end

  @dir = File.expand_path(ARGV[0].dup)
rescue OptionParser::ParseError => e
  puts e
  puts optparse
  exit -1
end

def run_cmd(command, exit_codes=[0])
  result = `#{command}`
  unless exit_codes.include?($?.exitstatus)
    STDERR.puts "Failed '#{command}' with exit code #{$?.exitstatus}"
    cleanup($?.exitstatus)
  end
  result
end

def cleanup(exitstatus=-1)
  puts "Cleaning up backup folder and starting any stopped services... "
  Dir.chdir("/") do
    FileUtils.rm_rf @dir unless @options[:no_subdir]
    `katello-service start #{EXCLUDED}` unless @options[:online]
    puts "Done."
    exit(exitstatus)
  end
end

def specified_features
  @options[:features] || []
end

def feature_included?(feature)
  specified_features.include?(feature) || specified_features.include?("all")
end

def configure_configs
  scenario = File.basename(File.readlink("/etc/foreman-installer/scenarios.d/last_scenario.yaml")).split(".")[0]
  scenario_answers = YAML.load_file("/etc/foreman-installer/scenarios.d/#{scenario}-answers.yaml")

  base_configs = [
    "/etc/foreman-proxy",
    "/etc/httpd",
    "/etc/foreman-installer",
    "/etc/pki/katello",
    "/etc/pki/katello-certs-tools",
    "/etc/pki/pulp",
    "/etc/pulp",
    "/etc/puppet",
    "/etc/qpid",
    "/etc/qpid-dispatch",
    "/root/ssl-build",
    "/var/www/html/pub",
    "/etc/squid",
    "/etc/puppetlabs",
    '/opt/puppetlabs/puppet/cache/foreman_cache_data',
    '/opt/puppetlabs/puppet/ssl/',
    '/var/lib/puppet/foreman_cache_data',
    '/var/lib/puppet/ssl',
    scenario_answers["certs"]["server_cert"],
    scenario_answers["certs"]["server_key"],
    scenario_answers["certs"]["server_cert_req"],
    scenario_answers["certs"]["server_ca_cert"]
  ]

  katello_configs = [
    "/etc/candlepin",
    "/etc/foreman",
    "/etc/hammer",
    "/etc/sysconfig/tomcat*",
    "/etc/tomcat*",
    "/var/lib/candlepin",
  ]

  if @is_foreman_proxy_content
    fpc_certs_tar = run_cmd("cat /etc/foreman-installer/scenarios.d/#{FOREMAN_PROXY_CONTENT}-answers.yaml | grep certs_tar | awk '{print $2}'").chomp
    fpc_certs_tar_path = File.expand_path(fpc_certs_tar)
    unless File.exist?(fpc_certs_tar_path)
      puts "ERROR: #{FOREMAN_PROXY} certs tar file is not present on the system in path #{fpc_certs_tar} please check there is an absolute file path at the certs_tar parameter in /etc/foreman-installer/scenarios.d/capsule-answers.yaml and the file is present on the system."
      cleanup
    end
    backup_configs = base_configs.push(fpc_certs_tar_path)
  else
    backup_configs = base_configs + katello_configs
  end

  feature_configs = []
  feature_configs.push("/var/lib/tftpboot") if feature_included?("tftp")
  feature_configs += ["/var/named/", "/etc/named*"] if feature_included?("dns")
  feature_configs += ["/var/lib/dhcpd", "/etc/dhcp"] if feature_included?("dhcp")
  feature_configs.push("/usr/share/xml/scap") if feature_included?("openscap")

  backup_configs + feature_configs
end

def confirm
  unless agree("WARNING: This script will stop your services. Do you want to proceed(y/n)? ")
    puts "**** cancelled ****"
    FileUtils.rm_rf @dir
    exit(-1)
  end
end

def warning
  unless agree("*** WARNING: The online backup flag is intended for making a copy of the data\n" \
               "*** for debugging purposes only. The backup routine can not ensure 100% consistency while the\n" \
               "*** backup is taking place as there is a chance there may be data mismatch between\n" \
               "*** Mongo and Postgres databases while the services are live. If you wish to utilize the --online-backup\n" \
               "*** flag for production use you need to ensure that there are no modifications occurring during\n" \
               "*** your backup run.\n\nDo you want to proceed(y/n)? ")
    puts "**** cancelled ****"
    FileUtils.rm_rf @dir
    exit(-1)
  end
end

def create_directories(directory)
  @dir = File.join directory, "katello-backup-" + DateTime.now.strftime('%Y%m%d%H%M%S') unless @options[:no_subdir]
  puts "Creating backup folder #{@dir}"
  FileUtils.mkdir_p @dir
  unless @options[:no_subdir]
    FileUtils.chown_R nil, 'postgres', @dir if @databases.include? "pgsql"
    FileUtils.chmod_R 0770, @dir
  end
end

def online_backup(database)
  case database
  when 'pulp'
    FileUtils.cd '/var/lib/pulp' do
      puts "Backing up Pulp data... "
      matching = false
      until matching
        checksum1 = run_cmd("find . -printf '%T@\n' | md5sum")
        create_pulp_data_tar
        checksum2 = run_cmd("find . -printf '%T@\n' | md5sum")
        matching = (checksum1 == checksum2)
      end
    end
    puts "Done."
  when 'pgsql'
    puts "Backing up postgres online schema... "
    run_cmd("runuser - postgres -c 'pg_dumpall -g > #{File.join(@dir, 'pg_globals.dump')}'")
    run_cmd("runuser - postgres -c 'pg_dump -Fc foreman > #{File.join(@dir, 'foreman.dump')}'")
    run_cmd("runuser - postgres -c 'pg_dump -Fc candlepin > #{File.join(@dir, 'candlepin.dump')}'")
    puts "Done."
  when 'mongodb'
    puts "Backing up mongo online schema... "
    run_cmd("mongodump --host localhost --out #{File.join(@dir, 'mongo_dump')}")
    puts "Done."
  end
end

def offline_backup(database, dir_path = nil)
  case database
  when 'pulp'
    dir_path ||= '/var/lib/pulp'
    FileUtils.cd dir_path do
      puts "Backing up Pulp data... "
      create_pulp_data_tar
      puts "Done."
    end
  when 'mongodb'
    dir_path ||= '/var/lib/mongodb'
    FileUtils.cd dir_path do
      puts "Backing up mongo db... "
      run_cmd("tar --selinux --create --file=#{File.join(@dir, 'mongo_data.tar')} --listed-incremental=#{File.join(@dir, '.mongo.snar')} --exclude=mongod.lock --transform 's,^,var/lib/mongodb/,S' -S *")
      puts "Done."
    end
  when 'pgsql'
    dir_path ||= '/var/lib/pgsql/data'
    FileUtils.cd dir_path do
      puts "Backing up postgres db..."
      run_cmd("tar --selinux --create --file=#{File.join(@dir, 'pgsql_data.tar')} --listed-incremental=#{File.join(@dir, '.postgres.snar')} --transform 's,^,var/lib/pgsql/data/,S' -S *")
      puts "Done."
    end
  end
end

def compress_files
  `gzip -f pgsql_data.tar` if @databases.include? "pgsql"
  `gzip -f mongo_data.tar`
end

def plugin_list
  if @is_foreman_proxy_content
    JSON.parse(run_cmd("curl -k https://$(hostname):9090/features"))
  else
    plugins = []
    plugin_list = run_cmd("foreman-rake plugin:list | grep 'Foreman plugin: '", [0,1]).lines
    plugin_list.each do |line|
      plugin = line.split
      plugins << "#{plugin[2].chop}-#{plugin[3].chop}"
    end
    plugins
  end
end

def generate_metadata
  puts "Generating metadata ... "
  os_version = run_cmd("cat /etc/redhat-release").chomp
  plugins = plugin_list
  rpms = run_cmd("rpm -qa").split("\n")
  system_facts = {:os_version => os_version, :plugin_list => plugins, :rpms => rpms}
  File.open('metadata.yml', 'w') do |metadata_file|
    metadata_file.puts system_facts.to_yaml
  end
  puts "Done."
end

def create_pulp_data_tar
  run_cmd("tar --selinux --create --file=#{File.join(@dir, 'pulp_data.tar')} --exclude=var/lib/pulp/katello-export --listed-incremental=#{File.join(@dir, '.pulp.snar')} --transform 's,^,var/lib/pulp/,S' -S *")
end

def backup_config_files
  puts "Backing up config files... "
  run_cmd("tar --selinux --create --gzip --file=#{File.join(@dir, 'config_files.tar.gz')} --listed-incremental=#{File.join(@dir, '.config.snar')} #{configure_configs.join(' ')} 2>/dev/null", [0,2])
  puts "Done."
end

def validate_directory
  unless system("sudo -u postgres test -w #{@dir}")
    puts "****cancelled****"
    puts "Postgres user needs write access to the backup directory"
    puts "Please select a directory, such as /tmp or /var/tmp which allows Postgres write access"
    cleanup
  end
end

if @dir.nil?
  puts "**** ERROR: Please specify an export directory ****"
  puts optparse
  exit(-1)
end

puts "Starting backup: #{Time.now}"
@is_foreman_proxy_content = !system("rpm -q foreman > /dev/null")
@databases.delete 'pgsql' if @is_foreman_proxy_content
create_directories(@dir.dup)
validate_directory if @databases.include? "pgsql"

Dir.chdir(@dir) do
  generate_metadata
  if @options[:incremental]
    FileUtils.cp Dir.glob(File.join(@options[:incremental], '.*.snar')), @dir
  elsif @options[:no_subdir]
    FileUtils.rm Dir.glob(File.join(@dir, '.*.snar'))
  end
  backup_config_files

  if @options[:logical_backup]
    @databases.each do |database|
      online_backup(database)
    end
  end

  if @options[:online]
    warning unless @options[:confirm]
    @databases.each do |database|
      online_backup(database)
    end
  else
    confirm unless @options[:confirm]
    run_cmd("katello-service stop #{EXCLUDED}")
    @databases.each do |database|
      offline_backup(database)
    end
    run_cmd("katello-service start #{EXCLUDED}")
    compress_files
  end
end

puts "Done with backup: #{Time.now}"
puts "**** BACKUP Complete, contents can be found in: #{@dir} ****"
