Foreman has a script to update it's facts by going through the fact's in /var/lib/puppet/yaml/facts/*.yaml and posting them to the Foreman server. The only issue is that the script does this one fact at a time and waits for the server response before proceeding which was taking long enough that the hosts had all reported back in again before the script could loop through all the hosts once.

To shorten this I added a queue and multiple threads to the script. The initial thread script I found was here which did the main jist of what I wanted to accomplish by letting me spin out any number of threads and then join them again at the end. Here's the script:

#! /usr/bin/env ruby

require 'thread'

elements = [1,2,3,4,5,6,7,8,9,10]

def process(element)
    puts "working on #{element} \n"
    sleep rand * 5
end

queue = Queue.new
elements.each{|e| queue << e }

threads = []
4.times do
    threads << Thread.new do
      while (e = queue.pop(true) rescue nil)
        process(e)
      end
    end
end

threads.each {|t| t.join }

I then incorporated this with the foreman fact import script. The final copy is here:

#! /usr/bin/env ruby
#
# This scripts runs on remote puppetmasters that you wish to import their nodes facts into Foreman
# it uploads all of the new facts its encounter based on a control file which is stored in /tmp directory.
# This script can run in cron, e.g. once every minute
# ohadlevy@gmail.com

# puppet config dir
puppetdir="/var/lib/puppet"

# URL where Foreman lives
url="https://foreman"

# Temp file keeping the last run time
stat_file = "/tmp/foreman_fact_import"

require 'fileutils'
require 'net/http'
require 'net/https'
require 'uri'
require 'thread'

last_run = File.exists?(stat_file) ? File.stat(stat_file).mtime.utc : Time.now - 365*60*60

facts = Dir["#{puppetdir}/yaml/facts/*.yaml"]


def process(filename, last_run)
  last_fact = File.stat(filename).mtime.utc
  if last_fact > last_run
    fact = File.read(filename)
    puts "Importing #{filename}"
    begin
      uri = URI.parse(url)
      http = Net::HTTP.new(uri.host, uri.port)
      if uri.scheme == 'https' then
        http.use_ssl = true
        http.verify_mode = OpenSSL::SSL::VERIFY_NONE
      end
      req = Net::HTTP::Post.new("/fact_values/create?format=yml")
      req.set_form_data({'facts' => fact})
      response = http.request(req)
    rescue Exception => e
      raise "Could not send facts to Foreman: #{e}"
    end
  end
end

queue = Queue.new
facts.each{|e| queue << e }

threads = []
10.times do
  threads << Thread.new do
    while (e = queue.pop(true) rescue nil)
      process(e, last_run)
    end
  end
end

threads.each {|t| t.join }
puts "All Threads Joined. Fact Import Done"
FileUtils.touch stat_file

The end result of this was about a 90% reduction in time taken to import facts into Foreman. I had to monitor both the foreman server and the puppet server to fine-tune the number of threads to spin out, I had the most luck with 10. This brought CPU on Foreman up to about 80% and on the Puppet server to 20%.

Any questions? Let me know!