#!/usr/bin/ruby
#
# NAME
#
#  sympl-web-configure - Auto-configure Apache2 sites with specified IPs
#
# SYNOPSIS
#  sympl-web-configure [ --ssl-template | -s file ]
#                         [ --non-ssl-template | -t file ]
#                         [ --apache2-dir | -a directory ]
#                         [ --force | -f ] [ --no-reload | -n ]
#                         [ --help | -h ] [ --manual | -m ] [ --verbose | -v ] [domain domain ...]
#
# OPTIONS
#
# --ssl-template, -s file      Specify the template file for SSL sites.
#                                Defaults to
#                                /etc/sympl/apache.d/non_ssl.template.erb
#
# --non-ssl-template, -t file  Specify the template file for non-SSL sites.
#                                Defaults to
#                                /etc/sympl/apache.d/non_ssl.template.erb
#
# --apache2-dir, -a directory  Specify the location of the Apache
#                                configuration directory. Defaults to
#                                /etc/apache2.
#
# --mass-ssl-template, -S file   Specify the template file for mass-hosted SSL
#                                  sites. Defaults to
#                                  /etc/sympl/apache.d/zz-mass-hosting.ssl.template.erb
#
# --mass-non-ssl-template, -T file  Specify the template file for mass-hosted
#                                     non-SSL sites. Defaults to
#                                     /etc/sympl/apache.d/zz-mass-hosting.template.erb
#
# --diff-only, -d   Show the differences in the new configuration, without
#                   making any changes.
#
# --force, -f       Force the re-creation of all sites.
#
# --no-reload, -n   Do not reload Apache even if changes have taken place.
#
# --manual, -m      Show the manual for this script.
#
# --help, -h        Show brief usage instructions for this script.
#
# --verbose, -v     Show debugging information.
#
# USAGE
#
# This script is designed to iterate over the domains hosted upon a Sympl
# system, and configure Apache appropriately. Specifically it
#  * creates and enables configurations based on the mass-hosting templates,
#    using the sympl-vhost Apache module;
#  * creates and enables configurations for domains with requirements that mean
#    they can't be hosted with the sympl-vhost Apache module, specifically if
#    they have an allocated IP address or SSL certificates;
#  * updates templated configurations if the template changes;
#  * preserves changes to manually edited configurations, or disabled/enabled sites;
#  * prunes orphaned configurations that Sympl had previously managed;
#  * checks Apache will accept the new configuration before enabling it.
#
# If a domain or template file is specified at as an argument, the script will
# work solely on that one domain or template specified.
#
# This script can be disabled by creating the file
# /etc/sympl/apache.d/disabled. This will also prevent any further package
# updates from recreating these sites in the Apache configuration. However it
# will not disable any sites that are currently in place. These should be
# removed manually.
#
# EXAMPLES
#
# To maintain all domains/templates on a server:
#
#     sympl-web-configure
#
# To specifically configure (if appropriate) and re-enable one domain
# (example.com):
#
#     sympl-web-configure example.com
#
# To reset all domains back to their original, templated configuration, and
# re-enable them all:
#
#     sympl-web-configure --force
#
# A list of one or more domains can be added to this last command to reset just
# those domains.
#
# If further output is required, add the --verbose flag to any of these examples.
#
# AUTHORS
#
#   Patrick J Cherry <patrick@bytemark.co.uk>
#   Steve Kemp <steve@bytemark.co.uk>
#


require 'getoptlong'

#
#  Entry point to the code
#
$FORCE    = false
$VERBOSELOCAL  = false
help      = false
manual    = false
# Temporary storage for $VERBOSELOCAL, to avoid warnings when using '--verbose' flag.
# Not needed now since we don't use 'VERBOSE' any more.
verbosetmp = false
#
#  Do we need to reload apache?
#
$RELOAD=false

#
# The root directory -- '/' by default.
#
root = "/"
non_ssl_template = nil
ssl_template     = nil
mass_non_ssl_template = nil
mass_ssl_template     = nil
apache2_dir      = nil
no_reload       = false
diff_only       = false

opts = GetoptLong.new(
         [ '--help',             '-h', GetoptLong::NO_ARGUMENT ],
         [ '--manual',           '-m', GetoptLong::NO_ARGUMENT ],
         [ '--verbose',          '-v', GetoptLong::NO_ARGUMENT ],
         [ '--force',            '-f', GetoptLong::NO_ARGUMENT ],
         [ '--diff-only',        '-d', GetoptLong::NO_ARGUMENT ],
         [ '--no-reload',        '-n', GetoptLong::NO_ARGUMENT],
         [ '--no-restart',       '-N', GetoptLong::NO_ARGUMENT],
         [ '--ssl-template',     '-s', GetoptLong::REQUIRED_ARGUMENT ],
         [ '--non-ssl-template', '-t', GetoptLong::REQUIRED_ARGUMENT ],
         [ '--mass-ssl-template',     '-S', GetoptLong::REQUIRED_ARGUMENT ],
         [ '--mass-non-ssl-template', '-T', GetoptLong::REQUIRED_ARGUMENT ],
         [ '--apache2-dir',      '-a', GetoptLong::REQUIRED_ARGUMENT ],
         [ '--root-dir',         '-r', GetoptLong::REQUIRED_ARGUMENT ]
       )

begin
  opts.each do |opt, arg|
    case opt
    when '--ssl-template'
      ssl_template = arg
    when '--non-ssl-template'
      non_ssl_template = arg
    when '--mass-ssl-template'
      mass_ssl_template = arg
    when '--mass-non-ssl-template'
      mass_non_ssl_template = arg
    when '--apache2-dir'
      apache2_dir = arg
    when '--root-dir'
      root = arg
    when '--no-reload'
      no_reload = true
      # This is a legacy option.
    when '--no-restart'
      no_reload = true
    when '--diff-only'
      diff_only = true
    when '--help'
      help = true
    when '--manual'
      manual = true
    when '--verbose'
      verbosetmp = true
    when '--force'
      $FORCE = true
    end
  end
rescue => err
  # any errors, show the help
  warn err.to_s
  help = true
end

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

  found_synopsis = false

  lines.each do |line|

    line.chomp!
    break if line.empty?

    if help and !found_synopsis
      found_synopsis = (line =~ /^#\s+SYNOPSIS\s*$/)
      next
    end

    puts line[2..-1].to_s

    break if help and found_synopsis and line =~ /^#\s*$/

  end

  exit 0
end

# Moved until after help/manual to avoid build dependency.
require 'symbiosis/utils'

def verbose(s)
  puts s if $VERBOSELOCAL
end

#
# Requirements after the help clause has finished.
#
require 'symbiosis/domains'
require 'symbiosis/domains/http'
require 'symbiosis/domain/ssl'
require 'symbiosis/domain/http'
require 'symbiosis/config_files/apache'

#
# Set verbosity here to avoid warnings when using '--verbose' flag. 
#
$VERBOSELOCAL = verbosetmp

#
# Set the default paths.
#
non_ssl_template = File.join(root, "/etc/sympl/apache.d/non_ssl.template.erb") if non_ssl_template.nil?
ssl_template     = File.join(root, "/etc/sympl/apache.d/ssl.template.erb") if ssl_template.nil?

#
# Mass hosting paths.
#
mass_non_ssl_template = File.join(root, "/etc/sympl/apache.d/zz-mass-hosting.template.erb") if mass_non_ssl_template.nil?
mass_ssl_template     = File.join(root, "/etc/sympl/apache.d/zz-mass-hosting.ssl.template.erb") if mass_ssl_template.nil?

apache2_dir           = File.join(root, "/etc/apache2") if apache2_dir.nil?

prefix                = File.join(root, "/srv")

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

primary_ips = [Symbiosis::Host.primary_ipv4, Symbiosis::Host.primary_ipv6]

configurations = []

#
# Disable creation of mass hosting sites.
#
unless Symbiosis::Domains.apache_configuration_enabled?(File.join(root,"etc","sympl"))
  verbose "*** Sympl automatic web configuration disabled. Exiting."
  exit 0
end

#
# Perform this check just once.
#
apache_mass_hosting_enabled = Symbiosis::Domains.apache_mass_hosting_enabled?(File.join(root,"etc","sympl"))

unless apache_mass_hosting_enabled
  verbose "Sympl automatic mass-hosting configuration disabled. Explicitly configuring all sites."
end

domains = Symbiosis::Domains.all(prefix)

if domains_to_configure.length > 0
  domains = domains.select{|domain| domains_to_configure.include?(domain.name)}
end

#
#  For each domain.
#
domains.each do |domain|
  verbose "Domain: #{domain.name}"

  if domain.is_alias?
    verbose "\t#{domain.symlink} is a link to #{domain.directory}.  Skipping."
    next
  end

  if apache_mass_hosting_enabled and domain.ips.any?{|ip| primary_ips.include?(ip)}
    if domain.ssl_enabled?
      verbose "\tThis site has SSL enabled, and is using the host's primary IPs -- continuing with SNI."
    else
      verbose "\tThis site is using the host's primary IPs -- it is covered by the mass-hosting config.  Skipping."
      next
    end
  end

  this_config = domain.apache_configuration(ssl_template, non_ssl_template, apache2_dir)

  unless this_config.is_a?(Symbiosis::ConfigFiles::Apache)
    verbose "\tA valid configuration could not be created for this site.  Skipping."
    next
  end

  verbose "\tAdding to configurations"

  configurations << this_config
end

#
# Now see to the mass hosting stuff.
#
[mass_non_ssl_template, mass_ssl_template].each do |template|

  #
  # Skip if there were domains on the cmd line, and they don't include this template name.
  #
  unless domains_to_configure.empty? or domains_to_configure.include?(File.basename(template, ".template.erb"))
    next
  end

  verbose "Mass hosting template: #{template}"

  this_config = Symbiosis::Domains.apache_configuration(template, apache2_dir)

  unless this_config.is_a?(Symbiosis::ConfigFiles::Apache)
    verbose "\tA valid configuration could not be created for this mass-hosting template.  Skipping."
    next
  end

  #
  # Check if mass hosting is supposed to be enabled.
  #
  if apache_mass_hosting_enabled
    verbose "\tAdding to configurations"
    configurations << this_config

  else

    #
    # Otherwise remove it.
    #
    begin
      verbose "\tDisabling from configuration"
      this_config.disable(nil, $FORCE)
    rescue StandardError => err
      verbose "\tUnable to check configuration #{filename} because #{err.to_s}"
    end

  end

end

#
# Disable any site that looks like it belonged to a deleted domain
#
filenames_available = configurations.collect{|c| c.filename }
filenames_enabled   = filenames_available.collect{|filename| filename.sub("sites-available","sites-enabled") }


Dir.foreach(File.join(apache2_dir, "sites-enabled")) do |filename|

  #
  # Skip if there were domains on the cmd line, and they don't include this filename.
  #
  unless domains_to_configure.empty? or domains_to_configure.include?(File.basename(filename, ".conf"))
    next
  end

  #
  # Prepend full path before the filename
  #
  filename = File.join(apache2_dir, "sites-enabled", filename)

  #
  # Make sure this isn't a file we're expecting to create/enable
  #
  next if filenames_enabled.include?(filename)

  next if File.directory?(filename)

  begin
    verbose "Configuration file: #{filename}"

    #
    # We only deal with symlinks.
    #
    unless File.lstat(filename).symlink?
      verbose "\tis not a symlink -- skipping"
      next
    end

    filename_source = File.expand_path(File.readlink(filename), File.join(apache2_dir, "sites-enabled"))

    config = Symbiosis::ConfigFiles::Apache.new(filename_source, "#")

    unless config.is_a?(Symbiosis::ConfigFiles::Apache)
      verbose "\tFailed to parse #{filename_source} as a config file -- skipping."
      next
    end

    #
    # If the configuration has changed, then remove it.
    #
    unless config.managed?
      verbose "\tConfiguration #{filename_source} not managed by Sympl -- skipping."
      next
    end

    verbose "\tRemoving orphaned configuration file."
    config.disable(filename, $FORCE)

  rescue StandardError => err
    verbose "\tUnable to check configuration #{filename} because #{err.to_s}"
  end
end

configurations.each do |config|
  begin
    #
    #  If there is already a site enabled we only
    # need to touch it if one of the SSL-files is more
    # recent than the generated file.
    #
    #  e.g. User adds /config/ssl.combined and a site
    # is generated but broken because a mandatory bundle is missing.
    #
    verbose "Configuration: #{File.basename(config.filename)}"

    sites_enabled_file = config.filename.sub("sites-available", "sites-enabled")


    #
    # Always re-enable mass-hosting templates (which have a nil domain) and
    # domains explicitly mentioned on the command line.
    #
    reenable = domains_to_configure.include?(File.basename(config.filename,".conf"))

    #
    # Don't re-create configurations by default (this is set out below)
    #
    recreate = false

    if ( $FORCE )
      verbose "\tForcing re-creation of configuration due to --force."
      recreate = true
      reenable = true

    elsif config.exists?

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

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

      else
        verbose "\tConfiguration is up-to date."

      end

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

    end

    #
    # This gets apache2 to check the configuration using a temporary file.
    #
    if recreate

      #
      # If we're only showing the diff, then check the config, and continue to
      # the next domain.
      #
      if diff_only
        # Disable $VERBOSELOCAL for config.diff call to avoid warnings. 
        $VERBOSELOCAL = false
        puts config.diff(:color)
        $VERBOSELOCAL = verbosetmp
        puts "\tW: Apache has rejected this configuration change" unless config.ok?
        next
      end

      if config.ok?

        verbose "\tWriting configuration"
        config.write

        # Definitely reload if we've rewritten the config, and the site is enabled.
        $RELOAD = config.enabled?(sites_enabled_file)

      else
        verbose "\tApache has rejected the new configuration -- no changes have been made."
        next
      end
    end

    if config.enabled?(sites_enabled_file) and !$FORCE
        verbose "\tAlready enabled."
    elsif reenable
        verbose "\tEnabling configuration."
        config.enable(sites_enabled_file, $FORCE)

        # Definitely reload if we've enabled the site
        $RELOAD = true
    else
      verbose "\t!! Configuration has been manually disabled."
    end

  #
  # Rescue errors for this domain, but continue for others.
  #
  rescue StandardError => err
    verbose "\t!! Unable to configure site for #{File.basename(config.filename)} because #{err.to_s}"
  end

end

#
#  All done.
#
if ( $RELOAD and !no_reload )
  verbose "Reloading Apache"

  system( "service apache2 reload" )
end
