Created
October 5, 2015 15:01
-
-
Save stbenjam/40942ca0cf7104d3f23f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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