Skip to content

Instantly share code, notes, and snippets.

@SeanHood
Created May 25, 2022 10:22
Show Gist options
  • Save SeanHood/25bf4704d554a938ebb632c1aab97f1b to your computer and use it in GitHub Desktop.
Save SeanHood/25bf4704d554a938ebb632c1aab97f1b to your computer and use it in GitHub Desktop.

PuppetDB based inventory for Ansible

Based on: https://github.com/nexcess/ansible-inventory-puppetdb with tweaks to add groups

Groups are based on puppet environment + dataregion fact (one of our custom facts).

To test:

This uses a special Ansible pattern <group>:\&<group> which takes the intersection of both groups. https://docs.ansible.com/ansible/latest/user_guide/intro_patterns.html#common-patterns

ansible dev:\&uk -i puppetdb.rb -m ping -u root

Caching

There's redis caching, you'll need to install the redis gem. I didn't put this here, for a single script I'd like to change it to just a file based cache.

## example configuration for the inventory
## copy this file as 'config.yml' and replace the values with your own
hostname: puppetdb ## (str) the hostname of the PuppetDB server to use
port: 8081 ## (int) the port PuppetDB is listening on
ssl: true ## (bool) whether to use SSL/TLS for the connection (https)
cacert: /Users/name/.config/puppetdb.ca.crt ## (str) path to the ca certificate
cert: /Users/name/.config/puppetdb.cer ## (str) path to the servers certificate
key: /Users/name/.config/puppetdb.key ## (str) path to the servers certificate key
use_redis: true ## (bool) whether to use redis for caching
redis_host: '127.0.0.1' ## (str) the host to use when connecting to redis
redis_port: 6379 ## (int) the port to use when connecting to redis
redis_index: 0 ## (int) the redis index to use for storage
redis_ttl: 8640 ## (int) the time in seconds to consider cached data valid
#!/usr/bin/env ruby
require 'bundler/setup'
require 'optparse'
require 'json'
require 'yaml'
require 'cgi'
require 'net/http'
## load the config
CONFIG = YAML.load_file(File.expand_path(__dir__) <<
'/config.yml')
## create the PuppetDB host string
PROTO = CONFIG['ssl'] ? 'https://' : 'http://'
PDBHOST = "#{PROTO}#{CONFIG['hostname']}:#{CONFIG['port']}".freeze
## load redis and create connector if used
if CONFIG['use_redis']
require 'redis'
REDIS = Redis.new(host: CONFIG['redis_host'],
port: CONFIG['redis_port'],
db: CONFIG['redis_index'])
RKEY = 'aipdb'.freeze
end
## method to query puppetdb
def query_pdb(url)
uri = URI("#{PDBHOST}#{url}")
http = Net::HTTP.new(uri.host, uri.port)
if CONFIG['ssl']
http.use_ssl = true
http.ca_file = CONFIG['cacert'].to_s
http.cert = OpenSSL::X509::Certificate.new(File.read(CONFIG['cert'].to_s))
http.key = OpenSSL::PKey::RSA.new(File.read(CONFIG['key'].to_s))
end
req = Net::HTTP::Get.new(uri)
res = http.request(req)
res.body if res.is_a?(Net::HTTPSuccess)
end
## method to rearrange facts returned from puppetdb
## so that the key for 'value' is the fact name
## instead of the literal string 'value' and
## deletes the entry for 'name'
def format_facts(array, factname)
array.each do |h|
h.store(factname, h.delete('value'))
h.delete('name')
end
end
## method to merge arrays of hashes on a common field
def merge_hasharray(array1, array2, commonfield)
xref = {}
array2.each { |hash| xref[hash[commonfield]] = hash }
array1.each do |hash|
next if xref[hash[commonfield]].empty?
xref[hash[commonfield]].each_pair do |kk, vv|
next if commonfield == kk
hash[kk] = vv
end
end
end
## method to make a hacky json inventory for Ansible
def hacky_json(nodes)
meta = {}
hosts = {}
nodes.each do |node|
# Add nodes into environment groups
hosts[node['environment']] ||= {'hosts' => []}
hosts[node['environment']]['hosts'].push(node['fqdn'])
# Add nodes into region groups. Can then be targeted like: dev:\&uk
hosts[node['dataregion'].downcase] ||= {'hosts' => []}
hosts[node['dataregion'].downcase]['hosts'].push(node['fqdn'])
meta[node['fqdn']] = { 'ansible_host' => node['ipaddress'] }
end
meta = { '_meta' => { 'hostvars' => meta } }
JSON.generate(hosts.merge(meta))
end
## method that binds everything
def build_rsp
fqdn_query = '/pdb/query/v4/facts/fqdn'
json_hosts_fqdn = JSON.parse(query_pdb(fqdn_query))
format_facts(json_hosts_fqdn, 'fqdn')
ip_query = '/pdb/query/v4/facts/ipaddress'
json_hosts_ip = JSON.parse(query_pdb(ip_query))
format_facts(json_hosts_ip, 'ipaddress')
json_hosts_region = JSON.parse(query_pdb('/pdb/query/v4/facts/dataregion'))
format_facts(json_hosts_region, 'dataregion')
merged_facts = merge_hasharray(json_hosts_fqdn, json_hosts_ip, 'certname')
merged_facts = merge_hasharray(merged_facts, json_hosts_region, 'certname')
hacky_json(merged_facts)
end
## handle options
options = {}
OptionParser.new do |opts|
opts.banner = 'Usage: puppetdb.rb [options]'
opts.on('--list', 'List Hosts') do
options[:list] = true
end
opts.on('--host', 'List Host Vars') do
options[:host] = true
end
opts.on('--clear', 'Clear Redis Cache') do
options[:clear] = true
end
opts.on('--build', 'Build Redis Cache') do
options[:build] = true
end
end.parse!
## handle switches
if options[:list] || options.empty?
if CONFIG['use_redis']
REDIS.set(RKEY, build_rsp, ex: CONFIG['redis_ttl']) unless REDIS.get(RKEY)
puts REDIS.get(RKEY)
else
puts build_rsp
end
end
if options[:host]
## individual hostvars aren't currently supported
puts '{}'
end
if options[:clear]
REDIS.del(RKEY) if CONFIG['use_redis']
end
if options[:build]
REDIS.set(RKEY, build_rsp, ex: CONFIG['redis_ttl']) if CONFIG['use_redis']
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment