Project

General

Profile

Feature #8518 » foreman.rb

foreman indirector interface - Michael Messmore, 11/26/2014 11:18 AM

 
# /usr/lib/ruby/site_ruby/1.8/puppet/indirector/foreman.rb
require 'puppet/indirector/terminus'
require 'puppet/util'
require 'etc'
require 'net/http'
require 'net/https'
require 'fileutils'
require 'timeout'
require 'yaml'
require 'json'

class Puppet::Indirector::Foreman < Puppet::Indirector::Terminus

# Look for external node definitions.
def find(request)
name = request.key
do_enc(name)
end

private
# Turn our outputted objects into a Puppet::Node instance.
def create_node(name, result)
node = Puppet::Node.new(name)
set = false
[:parameters, :classes, :environment].each do |param|
if value = result[param]
node.send(param.to_s + "=", value)
set = true
end
end

node.fact_merge
node
end

# Translate the yaml string into Ruby objects.
def translate(name, output)
YAML.load(output).inject({}) do |hash, data|
case data[0]
when String
hash[data[0].intern] = data[1]
when Symbol
hash[data[0]] = data[1]
else
raise Puppet::Error, "Key is a #{data[0].class}, not a string or symbol"
end

hash
end

rescue => detail
raise Puppet::Error, "Could not load external node results for #{name}: #{detail}", detail.backtrace
end

def initialize()
@SETTINGS = Puppet::Util::Yaml.load_file("/etc/puppet/foreman.yaml")
end

def url
@SETTINGS[:url] || (raise Puppet::Error, "Must provide URL in #{$settings_file}")
end

def puppetdir
@SETTINGS[:puppetdir] || (raise Puppet::Error, "Must provide puppet base directory in #{$settings_file}")
end

def stat_file(certname)
FileUtils.mkdir_p "#{puppetdir}/yaml/foreman/"
"#{puppetdir}/yaml/foreman/#{certname}.yaml"
end

def tsecs
@SETTINGS[:timeout] || 10
end

def thread_count
return @SETTINGS[:threads].to_i if not @SETTINGS[:threads].nil? and @SETTINGS[:threads].to_i > 0
require 'facter'
processors = Facter.value(:processorcount).to_i
processors > 0 ? processors : 1
end

def build_body(certname,filename)
# Strip the Puppet:: ruby objects and keep the plain hash
facts = File.read(filename)
puppet_facts = YAML::load(facts.gsub(/\!ruby\/object.*$/,''))
hostname = puppet_facts['values']['fqdn'] || certname
{'facts' => puppet_facts['values'], 'name' => hostname, 'certname' => certname}
end

def initialize_http(uri)
res = Net::HTTP.new(uri.host, uri.port)
res.use_ssl = uri.scheme == 'https'
if res.use_ssl?
if @SETTINGS[:ssl_ca] && !@SETTINGS[:ssl_ca].empty?
res.ca_file = @SETTINGS[:ssl_ca]
res.verify_mode = OpenSSL::SSL::VERIFY_PEER
else
res.verify_mode = OpenSSL::SSL::VERIFY_NONE
end
if @SETTINGS[:ssl_cert] && !@SETTINGS[:ssl_cert].empty? && @SETTINGS[:ssl_key] && !@SETTINGS[:ssl_key].empty?
res.cert = OpenSSL::X509::Certificate.new(File.read(@SETTINGS[:ssl_cert]))
res.key = OpenSSL::PKey::RSA.new(File.read(@SETTINGS[:ssl_key]), nil)
end
end
res
end

def generate_fact_request(certname, filename)
# Temp file keeping the last run time
stat = stat_file("#{certname}-push-facts")
last_run = File.exists?(stat) ? File.stat(stat).mtime.utc : Time.now - 365*24*60*60
last_fact = File.stat(filename).mtime.utc
if last_fact > last_run
begin
uri = URI.parse("#{url}/api/hosts/facts")
req = Net::HTTP::Post.new(uri.request_uri)
req.add_field('Accept', 'application/json,version=2' )
req.content_type = 'application/json'
req.body = build_body(certname, filename).to_json
req
rescue => e
raise Puppet::Error, "Could not generate facts for Foreman: #{e}"
end
end
end

def cache(certname, result)
File.open(stat_file(certname), 'w') {|f| f.write(result) }
end

def read_cache(certname)
File.read(stat_file(certname))
rescue => e
raise Puppet::Error, "Unable to read from Foreman cache file: #{e}"
end

def enc(certname)
foreman_url = "#{url}/node/#{certname}?format=yml"
uri = URI.parse(foreman_url)
req = Net::HTTP::Get.new(uri.request_uri)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
if http.use_ssl?
if @SETTINGS[:ssl_ca] && !@SETTINGS[:ssl_ca].empty?
http.ca_file = @SETTINGS[:ssl_ca]
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
else
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
end
if @SETTINGS[:ssl_cert] && !@SETTINGS[:ssl_cert].empty? && @SETTINGS[:ssl_key] && !@SETTINGS[:ssl_key].empty?
http.cert = OpenSSL::X509::Certificate.new(File.read(@SETTINGS[:ssl_cert]))
http.key = OpenSSL::PKey::RSA.new(File.read(@SETTINGS[:ssl_key]), nil)
end
end
res = http.start { |http| http.request(req) }

raise Puppet::Error, "Error retrieving node #{certname}: #{res.class}\nCheck Foreman's /var/log/foreman/production.log for more information." unless res.code == "200"
res.body
end

def upload_facts(certname, req)
return nil if req.nil?
uri = URI.parse("#{url}/api/hosts/facts")
begin
res = initialize_http(uri)
res.start { |http| http.request(req) }
cache("#{certname}-push-facts", "Facts from this host were last pushed to #{uri} at #{Time.now}\n")
rescue => e
raise Puppet::Error, "Could not send facts to Foreman: #{e}"
end
end

def do_enc(certname)
if @SETTINGS[:facts]
req = generate_fact_request certname, "#{puppetdir}/yaml/facts/#{certname}.yaml"
upload_facts(certname, req)
end
# query external node
begin
result = ""
timeout(tsecs) do
result = enc(certname)
cache(certname, result)
end
rescue TimeoutError, SocketError, Errno::EHOSTUNREACH, Errno::ECONNREFUSED
# Read from cache, we got some sort of an error.
result = read_cache(certname)
end
result
end
end
(1-1/2)