#!/usr/bin/env ruby

# CLI client for Hiera.
#
# To lookup the 'release' key for a node given Puppet YAML facts:
#
# $ hiera release 'rel/%{location}' --yaml some.node.yaml
#
# If the node yaml had a location fact the default would match that
# else you can supply scope values on the command line
#
# $ hiera release 'rel/%{location}' location=dc2 --yaml some.node.yaml

# Bundler and rubygems maintain a set of directories from which to
# load gems. If Bundler is loaded, let it determine what can be
# loaded. If it's not loaded, then use rubygems. But do this before
# loading any hiera code, so that our gem loading system is sane.
if not defined? ::Bundler
  begin
    require 'rubygems'
  rescue LoadError
  end
end
require 'hiera'
require 'hiera/util'
require 'optparse'
require 'pp'

options = {
  :default => nil,
  :config  => File.join(Hiera::Util.config_dir, 'hiera.yaml'),
  :scope   => {},
  :key     => nil,
  :verbose => false,
  :resolution_type => :priority
}

# Loads the scope from YAML or JSON files
def load_scope(source, type=:yaml)
  case type
  when :mcollective
    begin
      require 'mcollective'

      include MCollective::RPC

      util = rpcclient("rpcutil")
      util.progress = false
      nodestats = util.custom_request("inventory", {}, source, {"identity" => source}).first

      raise "Failed to retrieve facts for node #{source}: #{nodestats[:statusmsg]}" unless nodestats[:statuscode] == 0

      scope = nodestats[:data][:facts]
    rescue Exception => e
      STDERR.puts "MCollective lookup failed: #{e.class}: #{e}"
      exit 1
    end

  when :yaml
    raise "Cannot find scope #{type} file #{source}" unless File.exist?(source)

    require 'yaml'

    # Attempt to load puppet in case we're going to be fed
    # Puppet yaml files
    begin
        require 'puppet'
    rescue
    end

    scope = YAML.load_file(source)

    # Puppet makes dumb yaml files that do not promote data reuse.
    scope = scope.values if scope.is_a?(Puppet::Node::Facts)

  when :json
    raise "Cannot find scope #{type} file #{source}" unless File.exist?(source)

    require 'json'

    scope = JSON.load(File.read(source))

  when :inventory_service
    # For this to work the machine running the hiera command needs access to
    # /facts REST endpoint on your inventory server.  This access is
    # controlled in auth.conf and identification is by the certname of the
    # machine running hiera commands.
    #
    # Another caveat is that if your inventory server isn't at the short dns
    # name of 'puppet' you will need to set the inventory_sever option in
    # your puppet.conf.  Set it in either the master or main sections.  It
    # is fine to have the inventory_server option set even if the config
    # doesn't have the fact_terminus set to rest.
    begin
      require 'puppet/util/run_mode'
      $puppet_application_mode = Puppet::Util::RunMode[:master]
      require 'puppet'
      Puppet.settings.parse
      Puppet::Node::Facts.indirection.terminus_class = :rest
      scope = YAML.load(Puppet::Node::Facts.indirection.find(source).to_yaml)
      # Puppet makes dumb yaml files that do not promote data reuse.
      scope = scope.values if scope.is_a?(Puppet::Node::Facts)
    rescue Exception => e
        STDERR.puts "Puppet inventory service lookup failed: #{e.class}: #{e}"
        exit 1
    end
  else
    raise "Don't know how to load data type #{type}"
  end

  raise "Scope from #{type} file #{source} should be a Hash" unless scope.is_a?(Hash)

  scope
end

OptionParser.new do |opts|
  opts.banner = "Usage: hiera [options] key [default value] [variable='text'...]\n\nThe default value will be used if no value is found for the key. Scope variables\nwill be interpolated into %{variable} placeholders in the hierarchy and in\nreturned values.\n\n"

  opts.on("--version", "-V", "Version information") do
    puts Hiera.version
    exit
  end

  opts.on("--debug", "-d", "Show debugging information") do
    options[:verbose] = true
  end

  opts.on("--array", "-a", "Return all values as an array") do
    options[:resolution_type] = :array
  end

  opts.on("--hash", "-h", "Return all values as a hash") do
    options[:resolution_type] = :hash
  end

  opts.on("--config CONFIG", "-c", "Configuration file") do |v|
    if File.exist?(v)
      options[:config] = v
    else
      STDERR.puts "Cannot find config file: #{v}"
      exit 1
    end
  end

  opts.on("--json SCOPE", "-j", "JSON format file to load scope from") do |v|
    begin
      options[:scope] = load_scope(v, :json)
    rescue Exception => e
      STDERR.puts "Could not load JSON scope: #{e.class}: #{e}"
      exit 1
    end
  end

  opts.on("--yaml SCOPE", "-y", "YAML format file to load scope from") do |v|
    begin
      options[:scope] = load_scope(v)
    rescue Exception => e
      STDERR.puts "Could not load YAML scope: #{e.class}: #{e}"
      exit 1
    end
  end

  opts.on("--mcollective IDENTITY", "-m", "Use facts from a node (via mcollective) as scope") do |v|
    begin
      options[:scope] = load_scope(v, :mcollective)
    rescue Exception => e
      STDERR.puts "Could not load MCollective scope: #{e.class}: #{e}"
      exit 1
    end
  end

  opts.on("--inventory_service IDENTITY", "-i", "Use facts from a node (via Puppet's inventory service) as scope") do |v|
    begin
      options[:scope] = load_scope(v, :inventory_service)
    rescue Exception => e
      STDERR.puts "Could not load Puppet inventory service scope: #{e.class}: #{e}"
      exit 1
    end
  end
end.parse!

# arguments can be:
#
# key default var=val another=val
#
# The var=val's assign scope
unless ARGV.empty?
  options[:key] = ARGV.delete_at(0)

  ARGV.each do |arg|
    if arg =~ /^(.+?)=(.+?)$/
      options[:scope][$1] = $2
    else
      unless options[:default]
        options[:default] = arg.dup
      else
        STDERR.puts "Don't know how to parse scope argument: #{arg}"
      end
    end
  end
else
  STDERR.puts "Please supply a data item to look up"
  exit 1
end

begin
  hiera = Hiera.new(:config => options[:config])
rescue Exception => e
  if options[:verbose]
    raise
  else
    STDERR.puts "Failed to start Hiera: #{e.class}: #{e}"
    exit 1
  end
end

unless options[:verbose]
  Hiera.logger = "noop"
end

ans = hiera.lookup(options[:key], options[:default], options[:scope], nil, options[:resolution_type])

if ans.is_a?(String)
  puts ans
else
  pp ans
end
