#!/usr/bin/ruby

# wallaby-agent:  the wallaby store
#
# Copyright (c) 2009--2010 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require 'spqr/spqr'
require 'spqr/app'

require 'mrg/grid/config'
require 'mrg/grid/util/daemon'
require 'mrg/grid/util/file'

require 'digest/sha1'

require 'logger'
require 'syslog'
require 'optparse'

include ::Mrg::Grid::Util::Daemon
include ::Mrg::Grid::Util::File

USE_PREPARED_STATEMENTS = ENV['WALLABY_USE_PREPARED_STATEMENTS'] && (ENV['WALLABY_USE_PREPARED_STATEMENTS'] == "1")

dbname = ENV['WALLABY_CONFIGDB_NAME'] || ":memory:"
snapdb = ENV['WALLABY_SNAPDB_NAME'] || ":memory:"
userdb = ENV['WALLABY_USERDB_NAME'] || ":memory:"
host = ENV['WALLABY_BROKER_HOST'] || "localhost"
port = (ENV['WALLABY_BROKER_PORT'] || 5672).to_i
username = ENV['WALLABY_BROKER_USER']
password = ENV['WALLABY_BROKER_PASSWORD']
explicit_mechanism = ENV['WALLABY_BROKER_MECHANISM']
logfile = ENV['WALLABY_LOGFILE']
debug = (ENV['WALLABY_LOGLEVEL'] && Logger.const_get(ENV['WALLABY_LOGLEVEL'].upcase)) || Logger::WARN
do_daemonify = !ENV['WALLABY_FOREGROUND']
explicit_secret = ENV['WALLABY_SECRET']
secret_file = ENV['WALLABY_SECRET_FILE']
run_as = nil
logoptions = nil

op = OptionParser.new do |opts|
  opts.banner = "Usage wallaby-agent [options]"
  
  opts.on("-l", "--logfile FILE", "file for wallaby-agent log") do |file|
    logfile = path_resolve(file)
  end
  
  opts.on("-d", "--dbname FILE", "file for persistent storage (will be created if it doesn't exist)") do |db| 
    dbname = path_resolve(db)
  end
  
  opts.on("-s", "--snapdb FILE", "file for store snapshots and versioned configurations (will be created if it doesn't exist)") do |db|
    snapdb = path_resolve(db)
  end
  
  opts.on("--userdb FILE", "file for user role information (will be created if it doesn't exist)") do |db|
    userdb = path_resolve(db)
  end
  
  opts.on("-h", "--help", "shows this message") do
    raise OptionParser::InvalidOption.new
  end

  opts.on("-H", "--host HOSTNAME", "qpid broker host (default localhost)") do |h|
    host = h
  end
  
  opts.on("-p", "--port NUM", "qpid broker port (default 5672)") do |num|
    port = num.to_i
  end
  
  opts.on("-U", "--user NAME", "qpid username") do |name|
    username = name
  end
  
  opts.on("-P", "--password PASS", "qpid password") do |pass|
    password = pass
  end

  opts.on("-M", "--auth-mechanism PASS", SPQR::App::VALID_MECHANISMS, "authentication mechanism (#{SPQR::App::VALID_MECHANISMS.join(", ")})") do |mechanism|
    explicit_mechanism = mechanism
  end
  
  opts.on("--enable-skeleton-group", "enable the \"skeleton group\" "  "  that is copied to each newly-created node") do
    Mrg::Grid::Config::Store.quiesce(:ENABLE_SKELETON_GROUP, true)
  end

  opts.on("--run-as USER", "unix user to execute wallaby-agent as") do |user|
    # NB:  Perhaps obviously, this only has an effect if we're running as root
    # Also, if we're running in the foreground, we'll run as the current user
    # unless a run-as user is explicitly specified
    run_as = user
  end
  
  opts.on("-v", "--verbose", "output verbose debugging info" "  (repeat for more verbosity)") do
    debug = debug - 1 if debug > 0
  end

  opts.on("-f", "--foreground", "run in the foreground") do
    do_daemonify = false
  end
end

begin
  op.parse!
rescue OptionParser::InvalidOption
  puts op
  exit
end

daemonify if do_daemonify
drop_privs(run_as) if (do_daemonify || run_as)

Syslog.open do |s| 
  s.notice "storing configuration to #{dbname}"
  s.notice "storing snapshots to #{snapdb}"
  puts "storing results to #{dbname}"
  puts "storing snapshots to #{snapdb}"
end

DO_CREATE = (dbname == ":memory:" || !File.exist?(dbname))
DO_SNAPCREATE = (snapdb == ":memory:" || !File.exist?(snapdb))
DO_USERCREATE = (userdb == ":memory:" || !File.exist?(userdb))
DO_SECRETFILECREATE = secret_file && !File.exist?(secret_file)

def get_secret(sfile, explicit)
  from_file = nil
  
  if sfile && File.exist?(sfile)
    stat = File.stat(sfile)
    ok = (stat.mode & 077 == 0 && stat.readable?)
    if ok
      from_file = open(sfile, "r") {|f| f.read}
    else
      Syslog.open do |s| 
        s.notice "ignoring secret file #{sfile} #{!stat.readable? ? "since it is not readable" : "due to insecure permissions"}"
      end
      puts "ignoring secret file #{sfile} #{!stat.readable? ? "since it is not readable" : "due to insecure permissions"}" unless do_daemonify
    end
  end
  
  return explicit || from_file
end

begin
  Rhubarb::Persistence::open(dbname,:default,USE_PREPARED_STATEMENTS)
  Rhubarb::Persistence::open(snapdb,:snapshot,USE_PREPARED_STATEMENTS)
  Rhubarb::Persistence::open(userdb,:user,USE_PREPARED_STATEMENTS)
  
  Mrg::Grid::Config::MAIN_DB_TABLES.each {|tab| tab.db = Rhubarb::Persistence::dbs[:default] }
  Mrg::Grid::Config::SNAP_DB_TABLES.each {|tab| tab.db = Rhubarb::Persistence::dbs[:snapshot] }
  Mrg::Grid::Config::USER_DB_TABLES.each {|tab| tab.db = Rhubarb::Persistence::dbs[:user] }
  
  if DO_USERCREATE
    puts "creating user tables"
    Mrg::Grid::Config::USER_DB_TABLES.each {|cl| cl.create_table(:user)}
    Rhubarb::Persistence::dbs[:user].execute("PRAGMA user_version = #{Mrg::Grid::Config::SNAPVERSION}")
  end
  
  if DO_CREATE
    classes = Mrg::Grid::Config::MAIN_DB_TABLES
    classes.each do |cl| 
      Syslog.open do |s| 
        s.notice "creating table for #{cl.name}..."
        puts "creating table for #{cl.name}..." unless do_daemonify
      end
      cl.create_table
    end
    Rhubarb::Persistence::db.execute("PRAGMA user_version = #{Mrg::Grid::Config::DBVERSION}")
    Mrg::Grid::Config::Store.find_by_id(0).internal_storeinit
  end
  
  if DO_SNAPCREATE
    puts "creating snapshot tables"
    Mrg::Grid::Config::SNAP_DB_TABLES.each {|cl| cl.create_table(:snapshot)}
    Rhubarb::Persistence::dbs[:snapshot].execute("PRAGMA user_version = #{Mrg::Grid::Config::SNAPVERSION}")
  end
  
  if DO_SECRETFILECREATE
    puts "creating secret file"
    old_umask = File.umask
    begin
      File.umask(0177)
      bytes = open("/dev/urandom", "r") {|f| f.read(512)}
      secret = Digest::SHA1.hexdigest(bytes)
      open(secret_file, "w") {|f| f.write(secret)}
    ensure
      File.umask(old_umask)
    end
  end
  
  $WALLABY_SECRET = nil
  
  options = {}
  options[:loglevel] = debug
  options[:logfile] = logfile if logfile
  options[:appname] = "com.redhat.grid.config:Store"
  options[:user] = username if username
  options[:password] = password if password
  options[:server] = host
  options[:port] = port
  options[:mechanism] = explicit_mechanism if explicit_mechanism

  $WALLABY_SECRET = (get_secret(secret_file, explicit_secret) rescue nil)

  app = SPQR::App.new(options)
  app.register Mrg::Grid::Config::Store,Mrg::Grid::Config::Node,Mrg::Grid::Config::ConfigVersion,Mrg::Grid::Config::Feature,Mrg::Grid::Config::Group,Mrg::Grid::Config::Parameter,Mrg::Grid::Config::Subsystem,Mrg::Grid::Config::Snapshot,Mrg::Grid::Config::NodeUpdatedNotice,Mrg::Grid::Config::ConfigVersion
  $wallaby_log = Mrg::Grid::Config::Store.log
  
  if ENV['WALLABY_OLDSTYLE_VERSIONED_CONFIGS']
    Syslog.open {|s| s.warning "You have set WALLABY_OLDSTYLE_VERSIONED_CONFIGS in your environment, but this option is no longer supported.  Old versioned configurations will continue to work, but new versioned configurations will be stored in the 'lightweight' style."}
  end  

  begin
    options = {}
    options["remove-spurious"] = "yes" if (ENV["WALLABY_REMOVE_SPURIOUS_CONFIGS"] && ENV["WALLABY_REMOVE_SPURIOUS_CONFIGS"] =~ /^(true|yes)/i)

    # because we're not running over QMF at this point, we must ensure that 
    oldauth = $WALLABY_SKIP_AUTH
    begin
      $WALLABY_SKIP_AUTH = true
      Mrg::Grid::Config::Store.find_by_id(0).internal_storeinit(options)
    ensure
      $WALLABY_SKIP_AUTH = oldauth
    end
    [:default, :snapshot, :user].each do |which|
      Rhubarb::Persistence::dbs[which].execute("vacuum")
    end
  rescue Exception => ex
    Syslog.open {|s| s.warning "exception encountered during routine cleanup:  #{ex.inspect}"}
    puts "exception encountered during routine cleanup:  #{ex.inspect}" unless do_daemonify
  end
  
  {:snapshot=>[::Mrg::Grid::Config::SNAP_DB_TABLES, snapdb, ::Mrg::Grid::Config::SNAPMIGRATIONS], 
   :default=>[::Mrg::Grid::Config::MAIN_DB_TABLES, dbname, ::Mrg::Grid::Config::DBMIGRATIONS],
   :user=>[::Mrg::Grid::Config::USER_DB_TABLES, userdb, ::Mrg::Grid::Config::USERMIGRATIONS]}.each do |ds, meta|
    begin
      table_list, db_file, migrations = meta
      
      $wallaby_log.info "checking for necessary #{ds} database migrations...."
      puts "checking for necessary #{ds} database migrations...." unless do_daemonify
      
      ::Mrg::Grid::Config::DBSchema.migrate(Rhubarb::Persistence::dbs[ds], table_list, migrations, $wallaby_log) do |version|
        Rhubarb::Persistence::dbs[ds].transaction(:immediate) do |db|
          unless db_file == ":memory:"
            # starting a transaction with ":immediate" means we get a shared lock
            # and thus any db writes (unlikely!) complete before we copy the file
            backup_file = unique_name(db_file, "v#{version}_")
            $wallaby_log.info "backing up db file to  #{backup_file} before applying migration...."
            FileUtils.cp(db_file, backup_file, :preserve=>true)
          end
        end
      end   
    end
  end
  
  app.main
rescue Exception => ex
  Syslog.open do |s|
    s.crit "agent exiting with exception #{ex.inspect}"
    puts "agent exiting with exception #{ex.inspect}\n#{ex.backtrace.join("\n")}" unless do_daemonify
  end
end
