#!/usr/bin/ruby
#
# NAME
#
#   sympl-dns-generate - Generate DNS snippet files for Sympl domains.
#
# USAGE
#
#   sympl-dns-generate [ --sleep SEC | -s SEC ] [ --template TEMPLATE | -t TEMPLATE ]
#          [ --uploadscript-dir DIR | -b DIR ] [ --force | -f ] [ --verbose | -v ]
#          [ --help | -h ] [ DOMAIN ]
#
# SYNOPSIS
#
#   --sleep SEC            sleep for a random amount of time before doing
#                          anything, up to a maximum of SEC seconds.
#
#   --template TEMPLATE    Specify an alternative template file to read.
#
#   --uploadscript DIR     Specify where the upload zip file has been
#                          unpacked to.  Defaults to /root/BytemarkDNS/
#
#   --force                Force the re-creation of all DNS data.
#   --upload               Force the upload of all DNS data.
#   --help                 Show the help information for this script.
#   --verbose              Show debugging information.
#
# DETAILS
#
# This script is designed to iterate over the domains hosted upon a Sympl
# system, and create TinyDNS snippets for each one. This can then be uploaded
# to the Bytemark content DNS service.
#
# Domains can also be specified manually on the command line, in which case
# only those domains will be processed.
#
# SEE ALSO
#
# http://www.bytemark.co.uk/dnsc
#
# AUTHOR
#
# Steve Kemp <steve@bytemark.co.uk>
#


require 'getoptlong'


#
#  Entry point to the code
#
force    = false
help     = false
$VERBOSELOCAL  = false

#
# Do we need to re-upload the data?
#
upload=false

#
# The root directory -- '/' by default.
#
root             = "/"
dns_template     = nil
bytemarkdns_dir  = nil
sleep_for        = nil

opts = GetoptLong.new(
         [ '--uploadscript', '-b', GetoptLong::REQUIRED_ARGUMENT ],
         [ '--force',    '-f', GetoptLong::NO_ARGUMENT ],
         [ '--help',     '-h', GetoptLong::NO_ARGUMENT ],
         [ '--sleep',    '-s', GetoptLong::REQUIRED_ARGUMENT ],
         [ '--template', '-t', GetoptLong::REQUIRED_ARGUMENT ],
         [ '--upload',   '-u', GetoptLong::NO_ARGUMENT ],
         [ '--verbose',  '-v', GetoptLong::NO_ARGUMENT ]
       )

opts.each do |opt, arg|
  case opt
  when '--help'
    help = true
  when '--verbose'
    $VERBOSELOCAL = true
  when '--template'
    dns_template = arg
  when '--uploadscript'
    bytemarkdns_dir = arg
  when '--sleep'
    sleep_for = arg
  when '--root'
    root = arg
  when '--force'
    force = true
  when '--upload'
    upload = true
  end
end

#
# CAUTION! Here be quality kode.
#
if help
  # Open the file, stripping the shebang line
  lines = File.open(__FILE__){|fh| fh.readlines}[2..-1]

  lines.each do |line|
    line.chomp!
    break if line.empty?
    puts line[2..-1].to_s
  end

  exit 0
end

def verbose(s)
  puts s if $VERBOSELOCAL
end

require 'symbiosis/domains'
require 'symbiosis/domain'
require 'symbiosis/config_files/tinydns'

#
# Set the default paths.
#
dns_template    = File.join(root, "/etc/sympl/dns.d/tinydns.template.erb") if dns_template.nil?
bytemarkdns_dir = File.join(root, "/root/BytemarkDNS") if bytemarkdns_dir.nil?

#
# Bail out if the template is missing
#
unless File.file?(dns_template)
  verbose "Unable generate DNS data because the template #{dns_template.inspect} is missing."
  exit 1
end

#
# Bail out if the BytemarkDNS directory is missing
#
unless File.directory?(bytemarkdns_dir)
  verbose "Unable generate DNS data because the BytemarkDNS directory #{bytemarkdns_dir.inspect} is missing."
  exit 1
end

#
# Work out if we need to sleep.
#
unless sleep_for.nil?
  sleep_for = (sleep_for =~ /(\d+)/ ? rand($1.to_i) : 0)
  verbose "Sleeping for #{sleep_for}s before starting work"
  sleep sleep_for
end

#
# Any arguments on the command line specify which domains to do.
#
domains_to_configure = ARGV
string_to_hash = []

#
# For each domain.
#
Symbiosis::Domains.each do |domain|

  verbose "Domain: #{domain.name} "

  next unless domains_to_configure.empty? or domains_to_configure.include?(domain.name)

  begin
    output        = File.join(domain.config_dir, "dns", domain.name+".txt")
    output_dir    = File.dirname(output)
    config        = Symbiosis::ConfigFiles::Tinydns.new(output, "#")
    config.domain   = domain
    config.template = dns_template

    #
    # Should the snippet be created?
    #
    do_create = false

    if ( force )
      verbose "\tForcing re-creation of snippet due to --force."
      do_create = true

    elsif config.exists?

      if config.changed?
        verbose "\tNot updating snippet, as it has been edited by hand."

      elsif config.outdated?
        verbose "\tRe-creating snippet as it is out of date."
        do_create = true

      else
        verbose "\tDomain already present and up-to date."

      end

    else
      verbose "\tConfiguring site for the first time"
      do_create = true

    end

    #
    #
    # Check the TinyDNS syntax.. TODO!
    #
    if do_create
      if config.ok?

        verbose "\tWriting snippet to #{output}"

        #
        # Create directory with the same ownership as the parent
        #
        domain.create_dir(output_dir) unless File.exist?(output_dir)

        #
        # Write the snippet
        #
        config.write

        #
        # Make sure the ownership is correct.
        #
        File.chown(domain.uid, domain.gid, config.filename)

      else
        verbose "\tThe new DNS snippet is invalid -- no changes have been made."
      end
    end

    #
    # Make sure the BytemarkDNS/data directory exists, and create if not.
    #
    unless File.directory?(File.join(bytemarkdns_dir, "data"))
      Symbiosis::Utils.mkdir_p(File.join(bytemarkdns_dir, "data"))
    end

    #
    # Copy all the DNS data across to BytemarkDNS/data
    #
    # FIXME: At this point more checks are needed, to prevent overwriting of
    # another user's DNS data.
    #
    Dir.glob(File.join(output_dir,"*.txt")).each do |file|
      input = File.read(file)

      string_to_hash << file
      string_to_hash << input

      new_filename = File.join(bytemarkdns_dir, 'data', File.basename(file))
      new_filename_tmp = File.join(bytemarkdns_dir, 'data',
        '.' + File.basename(file) + '.tmp')

      verbose "\tWriting data to #{new_filename_tmp}"
      begin
        Symbiosis::Utils.safe_open(new_filename_tmp,
          File::WRONLY|File::CREAT) do |fh|
          fh.truncate(0)

          fh.puts <<EOF
#
# DO NOT EDIT THIS FILE - CHANGES MAY BE OVERWRITTEN
#
# Edit the source of this file instead:
#
#  #{file}
#
EOF
          fh.print input
        end
      rescue StandardError => err
        File.unlink(new_filename_tmp)
        raise
      end
      verbose "\tRenaming #{new_filename_tmp} to #{new_filename}"
      File.rename(new_filename_tmp, new_filename)

    end

    #
    # Rescue errors for this domain, but continue for others.
    #
  rescue StandardError => err
    verbose "\tUnable to create DNS data for #{domain.name} because #{err.to_s}"
    verbose "\t"+err.backtrace.join("\n\t")
  end
end

begin

  verbose "All new data have been written.  Checking for changes."

  #
  # Check to see if our new hash is the same as the one we have recorded.
  #
  new_hash = Digest::MD5.new.hexdigest(string_to_hash.join("\n"))

  #
  # This is where we expect to find the current hash.
  #
  hash_file = File.expand_path(File.join(bytemarkdns_dir,".hash"))

  if File.exist?(hash_file)
    old_hash = File.readlines(hash_file).first.to_s.chomp
  else
    old_hash = nil
  end

  #
  # Upload if the flag was set in the options, if there is no old hash, or the
  # new hash doesn't match the old one.
  #
  if upload or (old_hash.nil? or new_hash != old_hash )
    upload_script = File.expand_path(File.join(bytemarkdns_dir,"upload"))

    verbose "Uploading using #{upload_script}"


    #
    # Change directory before uploading.
    #
    FileUtils.chdir(File.dirname(upload_script))

    #
    # Run the upload script -- this will raise an appropriate error if the
    # script doesn't exist or fails.
    #
    IO.popen("/bin/bash #{upload_script} 2>&1","r") do |io|
      while !io.eof? do
        verbose io.readline
      end
    end

    #
    # Don't write the hash out.
    #
    unless 0 == $?
      raise StandardError, "#{upload_script.inspect} failed."
    end

    verbose "Writing hash to #{hash_file}"
    #
    # Finally record the new hash
    #
    Symbiosis::Utils.safe_open(hash_file, "a"){|fh| fh.truncate(0) ; fh.puts new_hash }
  else
    verbose "No need to upload as no changes in the data have been detected."
  end
rescue StandardError => err
  warn "Unable to upload DNS data because #{err.to_s}"
  verbose "\t"+err.backtrace.join("\n\t")
  exit 1
end

#
#  All done.
#

exit 0

