#!/usr/bin/ruby -w
##############################################################################
# tpkg package management and deployment tool
##############################################################################

# Ensure we can find tpkg.rb
$:.unshift File.join(File.dirname(__FILE__), "..", "lib")

require 'optparse'
require 'tpkg'

#
# Parse the command line options
#

@action = nil
@action_value = nil
@debug = false
@prompt = true
@quiet = false
@sudo = true
@lockforce = false
@force = false
@deploy = false
@deploy_params = ARGV   # hold parameters for how to invoke tpkg on the machines we're deploying to
@deploy_options = {}    # options for how to run the deployer
@servers = nil
@worker_count = 10
@rerun_with_sudo = false
@tpkg_options = {}	# options for instantiating Tpkg object
@init_options = {}      # options for how to run init scripts
@other_options = {}     
@compress = "gzip"


def rerun_with_sudo_if_necessary
  if Process.euid != 0 && @sudo
    warn "Executing with sudo"
    # Depending on how sudo is configured it might remove TPKG_HOME from the
    # environment.  As such we set the base as a command line option to ensure
    # it survives the sudo process.
    if ENV['TPKG_HOME']
      exec('sudo', $0, '--base', ENV['TPKG_HOME'], *ARGV)
    else
      exec('sudo', $0, *ARGV)
    end
  end
end

opts = OptionParser.new(nil, 24, '  ')
opts.banner = 'Usage: tpkg [options]'
opts.on('--servers', '-s', '=SERVERS', Array, 'Servers to apply the actions to') do |opt|
  @servers = opt
  @deploy = true
  @deploy_params = @deploy_params - ['--servers', '-s', @servers.join(","), "--servers=#{@servers.join(',')}"]
end
opts.on('--make', '-m', '=DIRECTORY', 'Make a package out of the contents of the directory') do |opt|
  @action = :make
  @action_value = opt
end
opts.on('--extract', '-x', '=DIRECTORY', 'Extract the metadata for a directory of packages') do |opt|
  @action = :extract
  @action_value = opt
end
installexample = "      --install pkgname=version=package_version\n          (Example: tpkg --install hive=2.1)  Will install hive version 2.1"
opts.on('--install', '-i', '=PACKAGES', "Install one or more packages\n#{installexample}", Array) do |opt|
  @rerun_with_sudo = true
  @action = :install
  @action_value = opt
end
opts.on('--upgrade', '-u', '=PACKAGES', 'Upgrade one or more packages', Array) do |opt|
  @rerun_with_sudo = true
  @action = :upgrade
  @action_value = opt
end
opts.on('--downgrade', '=PACKAGES', 'Downgrade one or more packages', Array) do |opt|
  @other_options[:downgrade] = true
  @rerun_with_sudo = true
  @action = :upgrade
  @action_value = opt
end
opts.on('--ua', 'Upgrade all packages') do |opt|
  @rerun_with_sudo = true
  @action = :upgrade
end
opts.on('--remove', '-r', '=PACKAGES', 'Remove one or more packages', Array) do |opt|
  @rerun_with_sudo = true
  @action = :remove
  @action_value = opt
end
opts.on('--rd', '=PACKAGES', 'Similar to -r but also remove depending packages', Array) do |opt|
  @rerun_with_sudo = true
  @other_options[:remove_all_dep] = true
  @action = :remove
  @action_value = opt
end
opts.on('--rp', '=PACKAGES', 'Similar to -r but also remove prerequisites', Array) do |opt|
  @rerun_with_sudo = true
  @other_options[:remove_all_prereq] = true
  @action = :remove
  @action_value = opt
end
opts.on('--ra', 'Remove all packages') do |opt|
  @rerun_with_sudo = true
  @action = :remove
end
opts.on('--verify', '-V', '=NAME', 'Verify packages') do |opt|
  @rerun_with_sudo = true
  @action = :verify
  @action_value = opt
end
opts.on('--start', '=NAME', 'Start the init script for specified package', Array) do |opt|
  @rerun_with_sudo = true
  @action = :execute_init
  @init_options[:packages] = opt
  @init_options[:cmd] = 'start'
end
opts.on('--stop', '=NAME', 'Stop the init script for specified package', Array) do |opt|
  @rerun_with_sudo = true
  @action = :execute_init
  @init_options[:packages] = opt
  @init_options[:cmd] = 'stop'
end
opts.on('--restart', '=NAME', 'Restart the init script for specified package', Array) do |opt|
  @rerun_with_sudo = true
  @action = :execute_init
  @init_options[:packages] = opt
  @init_options[:cmd] = 'restart'
end
opts.on('--reload', '=NAME', 'Reload the init script for specified package', Array) do |opt|
  @rerun_with_sudo = true
  @action = :execute_init
  @init_options[:packages] = opt
  @init_options[:cmd] = 'reload'
end
opts.on('--status', '=NAME', 'Get status from init script for specified package', Array) do |opt|
  @rerun_with_sudo = true
  @action = :execute_init
  @init_options[:packages] = opt
  @init_options[:cmd] = 'status'
end
opts.on('--start-all', 'Start the init scripts for all packages') do |opt|
  @rerun_with_sudo = true
  @action = :execute_init
  @init_options[:cmd] = 'start'
end
opts.on('--stop-all', 'Stop the init script for all packages') do |opt|
  @rerun_with_sudo = true
  @action = :execute_init
  @init_options[:cmd] = 'stop'
end
opts.on('--restart-all', 'Restart the init script for all packages') do |opt|
  @rerun_with_sudo = true
  @action = :execute_init
  @init_options[:cmd] = 'restart'
end
opts.on('--reload-all', 'Reload the init script for all packages') do |opt|
  @rerun_with_sudo = true
  @action = :execute_init
  @init_options[:cmd] = 'reload'
end
opts.on('--exec-init', '=NAME', 'Execute init scripts for specified packages', Array) do |opt|
  @rerun_with_sudo = true
  @init_options[:packages] = opt
  @action = :execute_init
end
opts.on('--init-script', '=NAME', 'What init scripts to execute', Array) do |opt|
  @rerun_with_sudo = true
  @init_options[:scripts] = opt
end
opts.on('--init-cmd', '=CMD', 'Invoke specified init script command') do |opt|
  @rerun_with_sudo = true
  @init_options[:cmd] = opt
end
opts.on('--query', '-q', '=NAMES', 'List installed packages', Array) do |opt|
  # People mistype -qa instead of --qa frequently
  if opt == ['a']
    warn "NOTE: tpkg -qa queries for a pkg named 'a', you probably want --qa for all pkgs"
  end
  @action = :query_installed
  @action_value = opt
end
opts.on('--qa', 'List all installed packages') do |opt|
  @action = :query_installed
end
opts.on('--qi', '=NAME', 'Info for packages') do |opt|
  @action = :query_info
  @action_value = opt
end
opts.on('--ql', '=NAME', 'List files in installed packages') do |opt|
  @action = :query_list_files
  @action_value = opt
end
opts.on('--qf', '=FILE', 'List the package that owns a file') do |opt|
  @action = :query_who_owns_file
  @action_value = opt
end
opts.on('--qv', '=NAME', 'List available packages') do |opt|
  @action = :query_available
  @action_value = opt
end
opts.on('--qva', 'List all available packages') do |opt|
  @action = :query_available
end
opts.on('--qr', '=NAME', 'List installed packages that require package') do |opt|
  @action = :query_requires
  @action_value = opt
end
opts.on('--qd', '=NAME', 'List the packages that package depends on') do |opt|
  @action = :query_depends
  @action_value = opt
end
opts.on('--qld', '=NAME', 'Similar to --qd, but only look at local packages') do |opt|
  @action = :query_local_depends
  @action_value = opt
end

opts.on('--dw', '=INTEGER', 'Number of workers for deploying') do |opt|
  @worker_count = opt.to_i
  @deploy_params = @deploy_params - ['--dw', @worker_count, "--dw=#{opt}"]
end
opts.on('--qX', '=FILENAME', 'Display tpkg.xml or tpkg.yml of the given package') do |opt|
  @action = :query_tpkg_metadata
  @action_value = opt
end
opts.on('--history', 'Display package installation history') do |opt|
  @action = :query_history
end
opts.on('--qenv', "Display machine's information") do |opt|
  @action = :query_env
end
opts.on('--qconf', "Display tpkg's configuration settings") do |opt|
  @action = :query_conf
end
opts.on('--base', '=BASE', 'Base directory for tpkg operations') do |opt|
  @tpkg_options[:base] = opt
end
opts.on('--source', '=NAME', 'Sources where packages are located', Array) do |opt|
  @tpkg_options[:sources] = opt
end
opts.on('--download', '=PACKAGES', 'Download one or more packages', Array) do |opt|
  @action = :download
  @action_value = opt
end
opts.on('-n', '--no-prompt', 'No confirmation prompts') do |opt|
  @prompt = opt
  Tpkg::set_prompt(@prompt)
end
opts.on('--quiet', 'Reduce informative but non-essential output') do |opt|
  @quiet = opt
end
opts.on('--no-sudo', 'No calls to sudo for operations that might need root') do |opt|
  @sudo = opt
end
opts.on('--lock-force', 'Force the removal of an existing lockfile') do |opt|
  @lockforce = opt
end
opts.on('--force-replace', 'Replace conflicting pkgs with the new one(s)') do |opt|
  @other_options[:force_replace] = opt
end
opts.on('--force', 'Force the execution of a given task') do |opt|
  @force = opt
end
opts.on('-o', '--out', '=DIR', 'Output directory for the -m option') do |opt|
  @other_options[:out] = opt
end
opts.on('--use-ssh-key [FILE]', 'Use ssh key for deploying instead of password') do |opt|
  @deploy_options["use-ssh-key"] = true
  @deploy_options["ssh-key"] = opt
  @deploy_params = @deploy_params - ['--use-ssh-key', opt, "--use-ssh-key=#{opt}"]
end
opts.on('--deploy-as', '=USERNAME', 'What username to use for deploying to remote server') do |opt|
  @deploy_options["deploy-as"] = opt
  @deploy_params = @deploy_params - ['--deploy-as']
end
opts.on('--compress', '=[TYPE]', 'Compress files when making packages') do |opt|
  if opt == "no"
    @compress= false
  else
    @compress = opt
  end
end
opts.on('--test-root TESTDIR', 'For use by the test suite only.') do |opt|
  @tpkg_options[:file_system_root] = opt
end
opts.on('--debug', 'Print lots of messages about what tpkg is doing') do |opt|
  @debug = opt
  Tpkg::set_debug(@debug)
end
opts.on('--version', 'Show tpkg version') do |opt|
  @action = :query_version
end
opts.on_tail("-h", "--help", "Show this message") do
  puts opts
  exit
end

leftovers = opts.parse(ARGV)

# Rerun with sudo if necessary, unless it's a deploy, then
# we don't need to run with sudo on this machine. It will run with sudo
# on the remote machine
if @rerun_with_sudo && !@deploy
  rerun_with_sudo_if_necessary
end

# Display a usage message if the user did not specify a valid action to perform.
if !@action
  puts opts
  exit
end

#
# Figure out base directory, sources and other configuration
#

def instantiate_tpkg(options = {})
  base = options[:base]
  sources = options[:sources] || []
  report_server = nil
  
  # base can come from four possible places.  They take precedence in this
  # order:
  # - command line option
  # - TPKG_HOME environment variable
  # - config file
  # - Tpkg::DEFAULT_BASE
  
  if ENV['TPKG_HOME']
    if !base
      base = ENV['TPKG_HOME']
      # Warn the user, as this could potentially be confusing
      # if they don't realize there's an environment variable set.
      warn "Using base '#{base}' base from $TPKG_HOME"
    else
      warn "Ignoring TPKG_HOME" if @debug
    end
  end
  
  # FIXME: Move config file parsing to tpkg.rb
  # http://sourceforge.net/apps/trac/tpkg/ticket/28
  fsroot = options[:file_system_root] ? options[:file_system_root] : ''
  [File.join(fsroot, Tpkg::DEFAULT_CONFIGDIR, 'tpkg.conf'), File.join(fsroot, ENV['HOME'], ".tpkg.conf")].each do |configfile|
    if File.exist?(configfile)
      IO.foreach(configfile) do |line|
        line.chomp!
        next if (line =~ /^\s*$/);  # Skip blank lines
        next if (line =~ /^\s*#/);  # Skip comments
        line.strip!  # Remove leading/trailing whitespace
        key, value = line.split(/\s*=\s*/, 2)
        if key == 'base'
          if !base
            # Warn the user, as this could potentially be confusing
            # if they don't realize there's a config file lying
            # around
            base = value
            warn "Using base #{base} from #{configfile}"
          else
            warn "Ignoring 'base' option in #{@configfile}" if @debug
          end
        elsif key == 'source'
          sources << value
          puts "Loaded source #{value} from #{configfile}" if (@debug)
        elsif key == 'report_server'
          report_server = value
          puts "Loaded report server #{report_server} from #{configfile}" if (@debug)
        end
      end
    end
  end
  
  if !base
    base = Tpkg::DEFAULT_BASE
  end
  
  if !@sudo
    curruid = Process.euid
    if curruid == 0
      # Besides there being no point to running with --no-sudo when root, we
      # don't want users to accidentally create files/directories that can't be
      # modified by other users who properly run --no-sudo as a regular user.
      raise "--no-sudo cannot be used as 'root' user or via sudo"
    end
    baseuid = File.stat(base).uid
    # We want to ensure that all --no-sudo usage within a given base directory
    # is done under the same account.
    if baseuid != curruid
      raise "Base dir #{base} owned by UID #{baseuid}, not your UID #{curruid}"
    end
  end
  
  # FIXME: This is ugly.  We would set the appropriate things in options and
  # call Tpkg.new(options)
  tpkg = Tpkg.new(:file_system_root => options[:file_system_root],
                  :base => base,
                  :sources => sources,
                  :report_server => report_server,
                  :lockforce => @lockforce,
                  :force => @force,
                  :sudo => @sudo)
end

passphrase_callback = lambda do | package |
#  ask("Passphrase for #{package}: ", true)
  begin
    system 'stty -echo;' 
    print "Passphrase for #{package}: "
    input = STDIN.gets.chomp
  ensure
    system 'stty echo; echo ""'
  end
  input
end

#
# Do stuff
#
if @deploy
#  puts "Creating deployer with #{@worker_count} number of worker"
  @deploy_options["max-worker"] = @worker_count
  @deploy_options["abort-on-fail"] = false

  # Check to see if ssh-key is accessible
  # Net::SSH doesn't warn the user about problems with the key file
  # (i.e. if it doesn't exist or isn't readable), resulting in the user
  # just getting a generic "Bad username/password combination" error
  # message because authentication fails.  As such we do some of our own
  # checking and warning here so that the user gets a more specific
  # error message.
  if @deploy_options["use-ssh-key"] && @deploy_options["ssh-key"]
    ssh_key = @deploy_options["ssh-key"]
    if !File.readable?(ssh_key) && (Process.euid == 0 || !@sudo)
      raise "Unable to read ssh key from #{ssh_key}"
    elsif !File.readable?(ssh_key)
      warn "Warning: Unable to read ssh key from #{ssh_key}. Attempting to rerun tpkg with sudo."
      rerun_with_sudo_if_necessary
    end
  end

  Tpkg::deploy(@deploy_params, @deploy_options,  @servers) 
  exit
end

if @action_value.is_a?(Array)
  @action_value.uniq!
end

# tell tpkg not to prompt if stdin is not tty
if !$stdin.tty?
  Tpkg::set_prompt(false)
end

ret_val = 0
case @action
when :make
  @other_options[:force] = @force
  @other_options[:compress] = @compress
  pkgfile = Tpkg::make_package(@action_value, passphrase_callback, @other_options)
  if pkgfile
    puts "Package is #{pkgfile}"
  else
    puts "Package build aborted or failed"
  end
when :extract
  Tpkg::extract_metadata(@action_value)
when :install
  tpkg = instantiate_tpkg(@tpkg_options)
  ret_val = tpkg.install(@action_value, passphrase_callback, @other_options)
when :upgrade
  tpkg = instantiate_tpkg(@tpkg_options)
  ret_val = tpkg.upgrade(@action_value, passphrase_callback, @other_options)
when :remove
  tpkg = instantiate_tpkg(@tpkg_options)
  ret_val = tpkg.remove(@action_value, @other_options)
when :download
  tpkg = instantiate_tpkg(@tpkg_options)
  ret_val = tpkg.download_pkgs(@action_value, @other_options)
when :verify
  result = nil
  # Verify a given .tpkg file
  if File.exist?(@action_value)
    Tpkg::verify_package_checksum(@action_value)
  # Verify an installed pkg
  else
    tpkg = instantiate_tpkg(@tpkg_options)
    results = tpkg.verify_file_metadata(@action_value)
    if results.length == 0
      puts "No package found"
    end
    success = true
    results.each do | file, errors |
      if errors.length == 0
        puts "#{file}: Passed"
      else
        puts "#{file}: Failed (Reasons: #{errors.join(", ")})"
        success = false
      end
    end 
    puts "Package verification failed" unless success 
  end
when :execute_init
  tpkg = instantiate_tpkg(@tpkg_options)
  if @init_options[:cmd].nil?
    raise "You didn't specify what init command to run"
  end
  ret_val = tpkg.execute_init(@init_options)
when :query_installed
  tpkg = instantiate_tpkg(@tpkg_options)
  req = nil
  matches = []
  if @action_value
    @action_value.each do | value |
      req = Tpkg::parse_request(value, tpkg.installed_directory)
      match = tpkg.installed_packages_that_meet_requirement(req)

      # If the user requested specific packages and we found no matches
      # then exit with a non-zero value to indicate failure.  This allows
      # command-line syntax like "tpkg -q foo || tpkg -i foo" to ensure
      # that a package is installed.
      ret_val = 1 if match.empty?
      matches |= match
    end
  else
    matches = tpkg.installed_packages_that_meet_requirement(req)
  end

  if !@quiet
    matches.sort(&Tpkg::SORT_PACKAGES).each do |pkg|
      puts pkg[:metadata][:filename]
    end
  end
when :query_info
  metadatas = nil
  if File.exist?(@action_value)
    metadatas = [Tpkg::metadata_from_package(@action_value)]
  else
    tpkg = instantiate_tpkg(@tpkg_options)
    metadatas = []
    requirements = []
    packages = {}
    tpkg.parse_requests([@action_value], requirements, packages)
    packages.each do | name, pkgs |
      pkgs.each do | pkg |
        metadatas << pkg[:metadata]
      end
    end
  end
  already_displayed = {}
  metadatas.each do |metadata|
    next if already_displayed[metadata[:filename]]
    already_displayed[metadata[:filename]] = true
    [:name, :version, :package_version, :operatingsystem, :architecture, :maintainer, :description, :bugreporting].each do |field|
      metadata[field] = 'any' if field == :operatingsystem && metadata[field].nil?
      metadata[field] = 'any' if field == :architecture && metadata[field].nil?
      if metadata[field]
        if metadata[field].kind_of?(Array)
          puts "#{field}: #{metadata[field].join(',')}"
        else
          puts "#{field}: #{metadata[field]}"
        end
      end
    end
    tpkg_version = metadata[:tpkg_version] || "< 1.26.1"
    puts "This package was built with tpkg version #{tpkg_version}."
    if metadata[:dependencies] 
      puts "This package depends on other packages, use --qd/--qld to view the dependencies."
    end
    puts "================================================================================"
  end
when :query_list_files
  tpkg = instantiate_tpkg(@tpkg_options)
  pkgfiles = nil
  if File.exist?(@action_value)
    fip = Tpkg::files_in_package(@action_value)
    tpkg.normalize_paths(fip)
    puts "#{@action_value}:"
    fip[:normalized].each { |file| puts file }
  else
    pkgfiles = []
    metadatas = []
    requirements = []
    packages = {}

    req = Tpkg::parse_request(@action_value)
    pkgs = tpkg.installed_packages_that_meet_requirement(req)
    if pkgs.nil? or pkgs.empty?
      ret_val = 1
      puts "Could not find any installed packages that meet the request \"#{@action_value}\""
    end
    pkgs.each do | pkg |
      pkgfiles << pkg[:metadata][:filename] 
    end

    files = tpkg.files_for_installed_packages(pkgfiles)
    files.each do |pkgfile, fip|
      puts "#{pkgfile}:"
      fip[:normalized].each { |file| puts file }
    end
  end
when :query_who_owns_file
  tpkg = instantiate_tpkg(@tpkg_options)
  tpkg.files_for_installed_packages.each do |pkgfile, fip|
    fip[:normalized].each do |file|
      if file == File.expand_path(@action_value)
        puts "#{file}: #{pkgfile}"
      end
    end
  end
when :query_available
  tpkg = instantiate_tpkg(@tpkg_options)
  req = nil
  if @action_value
    req = Tpkg::parse_request(@action_value)
  end
  tpkg.available_packages_that_meet_requirement(req).each do |pkg|
    next if pkg[:source] == :native_installed
    next if pkg[:source] == :native_available
    puts "#{pkg[:metadata][:filename]} (#{pkg[:source]})"
  end
when :query_requires
  tpkg = instantiate_tpkg(@tpkg_options)

  # parse the request
  requirements = [] 
  packages = {}
  tpkg.parse_requests([@action_value], requirements, packages)

  # get dependencies of all installed packages
  dependencies = {}
  tpkg.metadata_for_installed_packages.each do |metadata|
    dependencies[metadata[:filename]] = metadata[:dependencies]
  end

  # check to see if the any required dependencies match with what the
  # user specified in the request
  packages.each do |name, pkgs|
    pkgs.each do |pkg|
      next if pkg[:source] != :currently_installed
      puts "The following package(s) require #{pkg[:metadata][:filename]}:"
      dependencies.each do | requiree, deps |
        next if deps.nil?
        deps.each do | dep | 
          if Tpkg::package_meets_requirement?(pkg, dep)
            puts "  #{requiree}"
          end
        end
      end
    end
  end
when :query_local_depends
  tpkg = instantiate_tpkg(@tpkg_options)
  req = Tpkg::parse_request(@action_value)
  pkgs = tpkg.installed_packages_that_meet_requirement(req)
  pkgs.each do | pkg |
    puts pkg[:metadata][:filename] + ':'
    if pkg[:metadata][:dependencies]
      pkg[:metadata][:dependencies].each do |req|
        puts "  Requires #{req[:name]}"
        req.each do |field, value|
          next if field == :name
          puts "    #{field}: #{value}"
         end
       end
    end
  end
when :query_depends
  tpkg = instantiate_tpkg(@tpkg_options)
  requirements = []
  packages = {}
  tpkg.parse_requests([@action_value], requirements, packages)
  packages.each do |name, pkgs|
    already_displayed = {}
    pkgs.each do |pkg|
      # parse_requests returns both installed and available packages.
      # The same package may show up in both, skip any duplicates.
      next if already_displayed[pkg[:filename]]
      already_displayed[pkg[:metadata][:filename]] = true
      puts pkg[:metadata][:filename] + ':'
      if pkg[:metadata][:dependencies]
        pkg[:metadata][:dependencies].each do |req|
          puts "  Requires #{req[:name]}"
          req.each do |field, value|
            next if field == :name
            puts "    #{field}: #{value}"
          end
        end
      end
    end
  end
when :query_tpkg_metadata
  tpkg = instantiate_tpkg(@tpkg_options)
  if File.exist?(@action_value)
    puts Tpkg::extract_tpkg_metadata_file(@action_value)
  elsif File.exists?(File.join(tpkg.installed_directory, @action_value))
    puts Tpkg::extract_tpkg_metadata_file(File.join(tpkg.installed_directory, @action_value))
  else
    puts "File #{@action_value} doesn't exist."
  end
when :query_env
  puts "Operating System: #{Tpkg::get_os}"
  puts "Architecture: #{Tpkg::get_arch}"
when :query_conf
  # This is somewhat arbitrarily limited to the options read from the
  # tpkg.conf config files.  The reason it exists at all is that it is
  # difficult for users to programatically find out what these will be set to
  # without recreating all of our logic about deciding which config files to
  # read, which order to read them in, what environment variables override the
  # config files, etc.
  tpkg = instantiate_tpkg(@tpkg_options)
  puts "Base: #{tpkg.base}"
  puts "Sources: #{tpkg.sources.inspect}"
  puts "Report server: #{tpkg.report_server}"
when :query_history
  tpkg = instantiate_tpkg(@tpkg_options)
  tpkg.installation_history
when :query_version
  puts Tpkg::VERSION
end
exit ret_val