#!/usr/bin/ruby
#
# Destroy current slapd installation and restore all databases
# and slapd configurations (cn=config) from the dump files.
#
# Usage: puavo-restore-all-databases [options] --dump-directory DIR
#

require 'fileutils'
require 'optparse'
require 'puavo-ds/database_config'
require 'puavo/execute'
require 'tempfile'

def update_schemas(linelist)
  filter_out = false
  new_lines  = []

  linelist.each do |line|
    filter_out = false if line.match(/^$/)

    matchdata = line.match(/^dn: cn={.*}(.*),cn=schema,cn=config/)
    if matchdata then
      schema = $1
      # The "ppolicy"-module has internal schema in recent OpenLDAP versions
      # and we must drop it from our database.
      if schema != 'ppolicy' then
        new_lines << "include: file:///etc/ldap/schema/#{ schema }.ldif"
      end
      filter_out = true
    end

    new_lines << line unless filter_out
  end

  new_lines
end

def update_configurations(linelist)
  db_config = PuavoDatabaseConfig::CONFIG
  apply_db_configurations(linelist, db_config, 'olcMdbConfig')
end

def update_indexes(linelist)
  db_config = { 'olcDbIndex' => PuavoDatabaseConfig::INDEXES }
  apply_db_configurations(linelist, db_config, 'olcMdbConfig')
end

def update_modules(linelist)
  db_config = { 'olcModuleLoad' => PuavoDatabaseConfig::MODULES }
  apply_db_configurations(linelist, db_config, 'olcModuleList')
end


def apply_db_configurations(linelist, db_config, db_config_objectclass)
  in_dbconf_section = false
  new_linelist = []

  linelist.each do |line|
    if line.match(/^objectClass: #{ db_config_objectclass }$/) then
      in_dbconf_section = true
      new_linelist << line
      next
    end

    if line.match(/^$/) then
      if in_dbconf_section then
        db_config.each do |key, valuelist|
          valuelist.each do |value|
            new_linelist << "#{ key }: #{ value }"
          end
        end
      end
      in_dbconf_section = false
      new_linelist << line
      next
    end

    key = line.split(':').first
    next if db_config.has_key?(key)

    new_linelist << line
  end

  new_linelist
end

def handle_db_config_conversions(source, target)
  lines = source.readlines
  database_dirs = lookup_db_dirs(lines)

  lines = update_schemas(lines) if $options[:update_schemas]

  if $options[:update_configurations] then
    lines = update_configurations(lines)
  end

  lines = update_indexes(lines) if $options[:update_indexes]

  lines.each do |line|
    target.puts line
  end

  database_dirs
end

def lookup_db_dirs(db_conf_lines)
  database_dirs = []
  db_conf_lines.each do |line|
    matchdata = line.match(/olcDbDirectory: (.*)/)
    if matchdata then
      database_dirs << matchdata[1]
    end
  end
  database_dirs
end

def restore_databases(dumpdir, cn_config_path)
  puts
  puts ">>> restoring configurations (cn=config)"

  FileUtils.mkdir_p('/etc/ldap/slapd.d')
  response = Puavo::Execute.run([
               '/usr/sbin/slapadd', '-l', cn_config_path,
                 '-F', '/etc/ldap/slapd.d', '-b', 'cn=config' ])
  warn response.stderr if $options[:verbose]

  Dir.foreach(dumpdir) do |db_dump_file|
    next if db_dump_file == 'cn=config.ldif'

    match_data = db_dump_file.match(/(.*)\.ldif$/)
    if match_data then
      ldap_base = match_data[1]

      puts ">>> restoring database #{ ldap_base }"

      db_ldif_path = File.join(dumpdir, db_dump_file)
      response = Puavo::Execute.run([
                   '/usr/sbin/slapadd', '-q', '-b', ldap_base,
                   '-l', db_ldif_path, '-F', '/etc/ldap/slapd.d' ])
      warn response.stderr if $options[:verbose]
    end
  end

  FileUtils.chown_R('openldap', 'openldap', '/etc/ldap/slapd.d/')
  FileUtils.chown_R('openldap', 'openldap', '/var/lib/ldap')
end

def shutdown_and_remove_databases
  begin
    response = Puavo::Execute.run([ '/usr/sbin/service', 'slapd', 'stop' ])
    warn response.stderr if $options[:verbose]
  rescue Puavo::ExitStatusError => exception
    warn "Could not stop slapd service: " + exception.response.stderr.to_s
  end

  backup_timestamp = Time.now.strftime('%FT%T%z')

  if File.exist?('/etc/ldap/slapd.d') then
    conf_target_dir = "/etc/ldap/slapd.d-backup.#{ backup_timestamp }"
    puts ">>> moving /etc/ldap/slapd.d to #{ conf_target_dir }"
    FileUtils.mv('/etc/ldap/slapd.d', conf_target_dir)
  end

  if File.exist?('/var/lib/ldap') then
    db_target_dir = "/var/lib/ldap-backup.#{ backup_timestamp }"
    puts ">>> moving /var/lib/ldap to #{ db_target_dir }"
    FileUtils.mv('/var/lib/ldap', db_target_dir)
  end
end

$options = {}

OptionParser.new do |opts|
  opts.banner = 'Usage: puavo-restore-all-databases [options]'

  opts.on('-d', '--dump-directory [DUMPDIR]',
          'source directory of dump files') do |dir|
    $options[:dumpdir] = dir
  end

  opts.on('--debug', 'Use debugging mode') do
    $options[:debug] = true
  end

  opts.on('--update-configurations', 'Update DB configurations') do
    $options[:update_configurations] = true
  end

  opts.on('--update-indexes', 'Update DB indexes') do
    $options[:update_indexes] = true
  end

  opts.on('--update-modules', 'Update DB modules') do
    $options[:update_modules] = true
  end

  opts.on('--update-schemas', 'Use system ldap schemas') do
    $options[:update_schemas] = true
  end

  opts.on('-v', '--verbose', 'Run verbosely') do
    $options[:verbose] = true
  end

  opts.on_tail('-h', '--help', 'Show this message') do
    puts opts
    exit 0
  end
end.parse!

if $options[:dumpdir].nil? then
  warn 'Missing argument --dump-directory'
  exit 1
end

unless File.directory?($options[:dumpdir]) then
  warn "Can not find dump directory: #{ $options[:dumpdir] }"
  exit 1
end

puts 'This will destroy previous databases permanently and restore databases' \
        + ' from the dump files!'
puts 'ARE YOU SURE YOU WANT TO DO THIS?'

loop do
  print 'Type YES to continue (or press CTRL-C to abort)> '
  break if gets.strip == 'YES'
end

Tempfile.open('cn_config.ldif') do |cnconf_target|
  # Check cn=config.ldif first before destroying anything,
  # in case we have some issues.
  cn_config_source_path = File.join($options[:dumpdir], 'cn=config.ldif')
  database_dirs = nil
  File.open(cn_config_source_path) do |cnconf_source|
    database_dirs = handle_db_config_conversions(cnconf_source, cnconf_target)
  end
  cnconf_target.close
  modified_cn_config_path = cnconf_target.path

  if $options[:debug] then
    puts
    puts ">>> leaving modified cn=config to /tmp/cn=config.ldif for inspection"
    puts ">>> (due to --debug mode)"
    puts
    FileUtils.cp(modified_cn_config_path, '/tmp/cn=config.ldif')
    sleep 1
  end

  # we should now be ready for the operation

  shutdown_and_remove_databases

  database_dirs.each do |db_dir|
    FileUtils.mkdir_p(db_dir)
  end

  restore_databases($options[:dumpdir], modified_cn_config_path)
end

puts
puts ">>> All databases were successfully restored!"
puts
