|
# /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
|