Skip to content

Instantly share code, notes, and snippets.

@stbenjam
Created October 5, 2015 15:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stbenjam/40942ca0cf7104d3f23f to your computer and use it in GitHub Desktop.
Save stbenjam/40942ca0cf7104d3f23f to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
# This is the external nodes script to allow Salt to retrieve info about a host
# from Foreman. It also uploads a node's grains to Foreman, if the setting is
# enabled.
require 'yaml'
$settings_file = '/etc/salt/foreman.yaml'
SETTINGS = YAML.load_file($settings_file)
require 'net/http'
require 'net/https'
require 'etc'
require 'timeout'
begin
require 'json'
rescue LoadError
# Debian packaging guidelines state to avoid needing rubygems, so
# we only try to load it if the first require fails (for RPMs)
begin
require 'rubygems' rescue nil
require 'json'
rescue LoadError => e
puts 'You need the `json` gem to use the Foreman ENC script'
# code 1 is already used below
exit 2
end
end
def foreman_url
"#{SETTINGS[:proto]}://#{SETTINGS[:host]}:#{SETTINGS[:port]}"
end
def valid_hostname?(hostname)
hostname =~ /\A(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\z/
end
def get_grains(minion)
begin
grains = {
:name => minion,
:facts => plain_grains(minion).merge({:_timestamp => Time.now, :_type => 'foreman_salt'})
}
grains[:facts][:operatingsystem] = grains[:facts]['os']
grains[:facts][:operatingsystemrelease] = grains[:facts]['osrelease']
JSON.pretty_generate(grains)
rescue => e
puts e.backtrace
puts minion
puts plain_grains(minion)
puts "Could not get grains: #{e}"
exit 1
end
end
def plain_grains(minion)
# We have to get the grains from the cache, because the client
# is probably running 'state.highstate' right now.
#
# salt-run doesn't support directly outputting to json,
# so we have resort to python to extract the grains.
# Based on https://github.com/saltstack/salt/issues/9444
script = <<-EOF
#!/usr/bin/env python
import json
import os
import sys
import salt.config
import salt.runner
if __name__ == '__main__':
__opts__ = salt.config.master_config(
os.environ.get('SALT_MASTER_CONFIG', '/etc/salt/minion'))
runner = salt.runner.Runner(__opts__)
stdout_bak = sys.stdout
with open(os.devnull, 'wb') as f:
sys.stdout = f
ret = runner.cmd('cache.grains', ['#{minion}'])
sys.stdout = stdout_bak
print json.dumps(ret)
EOF
result = IO.popen('python', mode='r+') do |python|
python.write script
python.close_write
result = python.read
end
grains = JSON.load(result)
plainify(grains[minion]).flatten.inject(&:merge)
end
def plainify(hash, prefix = nil)
result = []
hash.each_pair do |key, value|
if value.is_a?(Hash)
result.push plainify(value, get_key(key, prefix))
elsif value.is_a?(Array)
result.push plainify(array_to_hash(value), get_key(key, prefix))
else
new = {}
new[get_key(key, prefix)] = value
result.push new
end
end
result
end
def array_to_hash(array)
new = {}
array.each_with_index { |v, index| new[index.to_s] = v }
new
end
def get_key(key, prefix)
[prefix, key].compact.join('::')
end
def upload_grains(minion)
begin
grains = get_grains(minion)
uri = URI.parse("#{foreman_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 = grains
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.start { |http| http.request(req) }
rescue => e
raise "Could not send facts to Foreman: #{e}"
end
end
def enc(minion)
url = "#{foreman_url}/salt/node/#{minion}?format=yml"
uri = URI.parse(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 "Error retrieving node #{minion}: #{res.class}\nCheck Foreman's /var/log/foreman/production.log for more information." unless res.code == '200'
res.body
end
minion = ARGV[0] || raise('Must provide minion as an argument')
raise 'Invalid hostname' unless valid_hostname? minion
begin
result = ''
if SETTINGS[:upload_grains]
timeout(SETTINGS[:timeout]) do
upload_grains(minion)
end
end
timeout(SETTINGS[:timeout]) do
result = enc(minion)
end
puts result
rescue => e
puts "Couldn't retrieve ENC data: #{e}"
exit 1
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment