#!/usr/bin/ruby193-ruby

require 'optparse'
class NilClass; def empty?; true; end; end
begin
  require 'rubygems'
  require 'net/ssh/multi'
rescue LoadError
  warn "failed to load ssh_multi library - try: gem install net-ssh-multi"
  exit 1
end

foreman_user = ENV['FOREMAN_USER']     || "admin"
foreman_pass = ENV['FOREMAN_PASSWORD'] || "changeme"
foreman_url  = ENV['FOREMAN_SERVER']   || "https://foreman"

require 'uri'
require 'net/http'
require 'net/https'
require 'yaml'

options = {}
optparse = OptionParser.new do |opts|
  # Set a banner, displayed at the top
  # of the help screen.
  opts.define_head "execute commands on multiple hosts
  Query Foreman to provide host list based on collections of facts and classes"
  opts.banner = "Usage: #{$0}"
  opts.separator ""

   # Define the options, and what they do
  options[:command] = ""
  opts.on( '-c', '--command CMD', 'command to execute' ) do |cmd|
    options[:command] = cmd
  end

  options[:user] = "root"
  opts.on( '-u', '--user USER', "User to use - defaults to #{options[:user]}" ) do |user|
    options[:user] = user
  end

  options[:facts] = {}
  opts.on( '-f', '--facts fact=x,fact=y..', 'one or more facts to filter the host list' ) do |facts|
    facts.split(",").each do |f|
      name, value = f.split("=")
      options[:facts].merge!({ name => value })
    end
  end

  options[:klass] = []
  opts.on( '-p', '--puppetclass CLASSA,CLASSB', 'one or more classes to filter the host list') do |k|
    options[:klass] = k.split(",")
  end

  options[:state] = "active"
  opts.on( '-s', '--state STATE', "Filter base of host state - active, out_of_sync,all, errors, pending, disabled  defaults to active hosts" ) do |state|
    options[:state] = state
  end

  options[:filter] = ""
  opts.on('--filter FILTER', "Custom Filter to append to hosts search" ) do |f|
    options[:filter] = f
  end

  opts.on( '-h', '--help', 'Display this screen' ) do
    puts opts
    exit
  end
end

optparse.parse!

if options[:command].empty? or (options[:facts].empty? and options[:klass].empty? and options[:filter].empty?)
  puts optparse
  exit 1
end

unless ARGV.empty?
  warn "unknown parameter #{ARGV.join(" ")}"
  exit 1
end

states = %w[errors active pending out_of_sync disabled all]
unless states.include? options[:state]
  warn "invalid state #{options[:state]}"
  exit 1
end

puts "About to execute: #{options[:command]}"

# translate our options into a foreman search string
query = []
query << options[:facts].map{|n,v| "facts.#{n} = \"#{v}\""}.join(" and ") unless options[:facts].empty?
query << options[:klass].map{|c| "class = #{c}"}.join(" and ")            unless options[:klass].empty?
query << options[:filter]                                                 unless options[:filter].empty?
query = query.join(" and ")

foreman_url += "/hosts"
foreman_url += "/#{options[:state]}" if options[:state] != "all"

uri = URI.parse(foreman_url)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if uri.scheme == 'https'

path = URI.escape("#{uri.path}?search=#{query}")
req  = Net::HTTP::Get.new(path)
req.basic_auth(foreman_user, foreman_pass)
req['Accept'] = 'application/x-yaml'

begin
  hosts = YAML.load(http.request(req).body)
  rescue => e
  warn "Failed to process response: #{e}"
  exit 1
end

unless hosts.is_a?(Array)
  warn "invalid response from Foreman: #{hosts}"
  exit 1
end
if hosts.empty?
  warn "unable to find any hosts"
  exit 1
end

puts "on the following #{hosts.size} hosts: #{hosts.join(" ")}"
puts "ctrl-c to abort or any key to continue"
$stdin.gets

ssh_options = {:user => options[:user], :auth_methods => "publickey"}

Net::SSH::Multi.start(:on_error => :warn) do |session|
  hosts.each { |s| session.use s, ssh_options }
  session.exec options[:command]
  session.loop
end
