Puppet⚓︎
Links⚓︎
Jobs⚓︎
Run the Puppet agent on a remote node⚓︎
Facts⚓︎
Resolving custom Facts with facter⚓︎
Place fact file in a directory such as /tmp/facts
Tasks⚓︎
Return Task output only⚓︎
The below uses jq to parse the JSON output of a Task, to return only the result.
$ puppet task run --format json service action=status name=puppet -n <certificate> | jq -r '.items[].results'
{
"status": "running",
"enabled": "true"
}
$ puppet task run --format json service action=status name=puppet -n <certificate> | jq -r '.items[].results[]'
running
true
Start a system service on a remote node⚓︎
Puppet Query Language (PQL)⚓︎
Limit query results⚓︎
Puppet environments⚓︎
List all the different Puppet environments⚓︎
List all the different Puppet environments based upon a fact value⚓︎
$ puppet query 'inventory[environment]{ facts.operatingsystem = "AIX" }' | jq -r '.[] | .environment' | sort | uniq
mwark
goats
List hosts in a specific Puppet environment⚓︎
$ puppet query 'inventory[certname]{ environment = "mwark" }' | jq -r '.[] | .certname'
<certname1>
<certname2>
<certname3>
Count of hosts in each Puppet environments⚓︎
$ puppet query -k 'inventory[environment]{ }' | jq -r '.[] | .environment' | sort | uniq | while read -r env; do
printf "%-5s %s\n" $(puppet query -k "inventory[certname]{ environment = \""${env}"\" }" | jq -r '.[] | .certname' | wc -l) "${env}"
done | sort -nrk1
12 mwark
3 production
1 goats
Miscellaneous⚓︎
Node certificate⚓︎
Clean⚓︎
Clean node certificates when a client has been rebuilt, or if a certificate has expired and needs to be renewed.
Show and verify⚓︎
Show the client certificate and verify it.
You an also use openssl to read the certificate.
noop mode⚓︎
A Puppet agent in noop will report on the changes it would make. You can enable/disable noop from either the client, or via the puppet_conf Task.
pxp-agent debug logging⚓︎
Update /etc/puppetlabs/pxp-agent/pxp-agent.conf and set loglevel to debug. Log files written /var/log/puppetlabs/pxp-agent/pxp-agent.log.
Puppet manages pxp-agent.conf, so changes will be reverted during the next agent run.
Expire a specific Puppet environment⚓︎
# environment=mwark
# curl --include \
--cert $(puppet config print hostcert) \
--key $(puppet config print hostprivkey) \
--cacert $(puppet config print localcacert) \
-X DELETE "https://$(puppet config print server):8140/puppet-admin-api/v1/environment-cache?environment=${environment}"
A successful request to this endpoint will return an 'HTTP 204: No Content'. The response body will be empty.
Show free puppetserver JRuby processes⚓︎
# curl -s -k https://localhost:8140/status/v1/services?level=debug | python3 -m json.tool | grep "jrubies"
"average-free-jrubies": 20.16288164135997,
"average-requested-jrubies": 6.113589418575067,
"num-free-jrubies": 23,
"num-jrubies": 30,
"average-free-jrubies": 20.16288164135997,
"average-requested-jrubies": 6.113589418575067,
"num-free-jrubies": 23,
"num-jrubies": 30,
Query RBAC logs⚓︎
Excel dump of all log data.
curl --cacert /etc/puppetlabs/puppet/ssl/certs/ca.pem -X GET -H "X-Authentication: $(puppet-access show)" -G "https://$(puppet config print server):4433/activity-api/v2/events.csv"
Removing the .csv will give you JSON format.
curl --cacert /etc/puppetlabs/puppet/ssl/certs/ca.pem -X GET -H "X-Authentication: $(puppet-access show)" -G "https://$(puppet config print server):4433/activity-api/v2/events"
Scripts⚓︎
Get facts for a host⚓︎
Script that uses puppet query to get facts for a single host.
$ facts_for_host
Usage:
./facts_for_host <certname> [fact_path] [--color=auto|always|never]
Examples:
./facts_for_host <certname>
→ pretty prints all facts for '<certname>'
./facts_for_host <certname> os
→ pretty prints the 'os' nested fact
./facts_for_host <certname> os.release.major
→ prints the raw value without quotes or color
Color:
--color=auto : enable colors when stdout is a TTY (default)
--color=always : force color
--color=never : disable color
Environment:
NO_COLOR : if set, disables color
$ facts_for_host gandalf
<< entire fact output for gandalf >>
$ facts_for_host gandalf os
{
"architecture": "PowerPC_POWER10",
"family": "AIX",
"hardware": "IBM,9080-HEX",
"name": "AIX",
"release": {
"full": "7300-03-01-2520",
"major": "7300"
}
}
$ facts_for_host gandalf os.release.major
7300
#!/opt/puppetlabs/puppet/bin/ruby
# frozen_string_literal: true
require 'json'
require 'open3'
USAGE = <<~USAGE
Usage:
./facts_for_host <certname> [fact_path] [--color=auto|always|never]
Examples:
./facts_for_host <certname>
→ pretty prints all facts for '<certname>'
./facts_for_host <certname> os
→ pretty prints the 'os' nested fact
./facts_for_host <certname> os.release.major
→ prints the raw value without quotes or color
Color:
--color=auto : enable colors when stdout is a TTY (default)
--color=always : force color
--color=never : disable color
Environment:
NO_COLOR : if set, disables color
USAGE
def die(msg, code = 1)
warn(msg)
exit(code)
end
# --- Color handling (ANSI) ---
module Color
ESC = "\e["
RESET = "#{ESC}0m"
# Palette inspired by jq:
PALETTE = {
key: "#{ESC}1;36m", # bold cyan
string: "#{ESC}32m", # green
number: "#{ESC}35m", # magenta
boolean: "#{ESC}33m", # yellow
null: "#{ESC}90m", # bright black (gray)
punct: "#{ESC}0m", # default
}
def self.enabled?(force = nil)
return false if ENV.key?('NO_COLOR')
case force
when 'always' then true
when 'never' then false
when 'auto', nil
$stdout.tty?
else
$stdout.tty?
end
end
def self.wrap(s, role, enabled)
return s unless enabled
code = PALETTE[role] || PALETTE[:punct]
"#{code}#{s}#{RESET}"
end
end
def parse_args(argv)
color_mode = 'auto'
args = []
argv.each do |a|
if a =~ /^--color=(auto|always|never)$/
color_mode = a.split('=')[1]
else
args << a
end
end
if args.length < 1
puts USAGE
exit(1)
end
certname = args[0]
fact_path = args[1] # optional
[certname, fact_path, color_mode]
end
def fetch_facts(certname)
query = %Q{facts[certname,name,value] { certname = "#{certname}" }}
stdout, stderr, status = Open3.capture3('puppet', 'query', query)
die("puppet query failed:\n#{stderr.strip}", 2) unless status.success?
rows = JSON.parse(stdout)
facts = {}
rows.each do |row|
facts[row['name']] = row['value']
end
facts
rescue Errno::ENOENT
die("puppet command not found in PATH.", 3)
rescue JSON::ParserError => e
die("JSON parse error: #{e.message}", 4)
end
def deep_fetch(obj, path)
return obj if path.nil? || path.empty?
current = obj
path.split('.').each do |seg|
if current.is_a?(Hash)
return :not_found unless current.key?(seg)
current = current[seg]
elsif current.is_a?(Array)
return :not_found unless seg =~ /^\d+$/
idx = seg.to_i
return :not_found if idx < 0 || idx >= current.length
current = current[idx]
else
return :not_found
end
end
current
end
# Pretty printer with color
def colorized_pretty(obj, indent = 0, enabled: true)
sp = ' ' * indent
case obj
when Hash
return Color.wrap("{}", :punct, enabled) if obj.empty?
parts = ["{"]
keys = obj.keys
keys.each_with_index do |k, i|
key_str = Color.wrap(%("#{k}"), :key, enabled)
val_str = colorized_pretty(obj[k], indent + 1, enabled: enabled)
parts << "#{sp} #{key_str}: #{val_str}#{i == keys.length - 1 ? '' : ','}"
end
parts << "#{sp}}"
parts.join("\n")
when Array
return Color.wrap("[]", :punct, enabled) if obj.empty?
parts = ["["]
obj.each_with_index do |v, i|
val_str = colorized_pretty(v, indent + 1, enabled: enabled)
parts << "#{sp} #{val_str}#{i == obj.length - 1 ? '' : ','}"
end
parts << "#{sp}]"
parts.join("\n")
when String
Color.wrap(%("#{obj}"), :string, enabled)
when Numeric
Color.wrap(obj.to_s, :number, enabled)
when TrueClass, FalseClass
Color.wrap(obj.to_s, :boolean, enabled)
when NilClass
Color.wrap('null', :null, enabled)
else
# Fallback: JSON-encode unknown types
s = JSON.generate(obj)
Color.wrap(s, :string, enabled)
end
end
def primitive?(v)
v.is_a?(String) || v.is_a?(Numeric) || v == true || v == false || v.nil?
end
def print_value(value, color_mode, raw_leaf: false)
# If it's a leaf selection and primitive → print raw (no quotes, no color)
if raw_leaf && primitive?(value)
# nil as 'null' to remain JSON-compatible for scripting
puts(value.nil? ? 'null' : value)
return
end
# Otherwise, use colorized pretty printer
enabled = Color.enabled?(color_mode)
puts colorized_pretty(value, 0, enabled: enabled)
end
# --- main ---
certname, fact_path, color_mode = parse_args(ARGV)
facts = fetch_facts(certname)
die("No facts found for '#{certname}'.", 5) if facts.empty?
if fact_path.nil?
# Pretty print entire facts (color)
print_value(facts, color_mode, raw_leaf: false)
else
value = deep_fetch(facts, fact_path)
if value == :not_found
hint = facts.keys.sort.join(', ')
die("Fact path '#{fact_path}' not found. Top-level keys include: #{hint}", 6)
else
# If the selection resolves to a primitive, print raw (no quotes/no color)
print_value(value, color_mode, raw_leaf: true)
end
end
Get facts from all hosts⚓︎
Script that uses puppet query to get facts for all hosts.
$ facts_for_all_hosts
Usage:
facts_for_hosts <facts> [--tsv]
Examples:
# Default CSV output
facts_for_hosts os.release.major,networking.ip
# TSV output
facts_for_hosts os.release.major,networking.ip --tsv
Notes:
- Facts can be top-level (e.g., 'os') or dot-paths (e.g., 'os.release.major').
- Non-primitive values (arrays/hashes) are serialized as JSON in the cell.
#!/opt/puppetlabs/puppet/bin/ruby
# frozen_string_literal: true
require 'csv'
require 'json'
USAGE = <<~USAGE
Usage:
facts_for_hosts <facts> [--tsv]
Examples:
# Default CSV output
facts_for_hosts os.release.major,networking.ip
# TSV output
facts_for_hosts os.release.major,networking.ip --tsv
Notes:
- Facts can be top-level (e.g., 'os') or dot-paths (e.g., 'os.release.major').
- Non-primitive values (arrays/hashes) are serialized as JSON in the cell.
USAGE
def die(msg, code = 1)
$stderr.puts(msg)
exit(code)
end
# --- Parse arguments ---
if ARGV.empty?
die(USAGE)
end
# Support a --tsv flag; default is CSV
tsv = ARGV.any? { |a| a == '--tsv' }
args = ARGV.reject { |a| a == '--tsv' }
# Each arg can itself be a CSV string; use CSV.parse_line to properly split
facts = args.flat_map { |a| CSV.parse_line(a) || [] }.map(&:strip).reject(&:empty?)
die("No facts parsed from arguments.\n\n#{USAGE}") if facts.empty?
# We only need to query PuppetDB for the top-level fact names
facts_to_query = facts.map { |f| f.sub(/\..*/, '') }.uniq
# --- Fetch data via puppet-query ---
# Note: keeping your original command name 'puppet-query' here.
query = "facts[certname,name,value]{name in #{facts_to_query.to_json} }"
raw = `puppet-query '#{query}'`
if $?.exitstatus != 0 || raw.nil? || raw.empty?
die("puppet-query failed or returned no data for query:\n#{query}", 2)
end
data = JSON.parse(raw)
# --- Massage into { certname => { fact_name => value } } ---
results = Hash.new { |h, k| h[k] = {} }
data.each do |factinfo|
cert = factinfo['certname']
name = factinfo['name']
val = factinfo['value']
results[cert][name] = val
end
# --- Helpers ---
def primitive?(v)
v.is_a?(String) || v.is_a?(Numeric) || v == true || v == false || v.nil?
end
def extract_value(values_hash, fact_path)
if fact_path.include?('.')
top = fact_path.split('.')[0]
rest = fact_path.split('.')[1..-1]
v = values_hash[top]
begin
# Use dig for nested traversal; works on Hash/Array with appropriate keys/indexes
v = v.dig(*rest) unless v.nil?
rescue
v = nil
end
v
else
values_hash[fact_path]
end
end
def cell_value_for_output(v)
# For empty hashes (meaning "not present" in the original script), return nil
return nil if v == {}
# For primitive values, return as-is (CSV/TSV library will quote strings if needed)
return v if primitive?(v)
# For arrays/hashes, serialize as compact JSON in the cell
JSON.generate(v)
end
# --- Output (CSV by default; TSV if flag provided) ---
col_sep = tsv ? "\t" : ","
# Header
header = ['certname'] + facts
puts CSV.generate_line(header, col_sep: col_sep)
# Rows
results.each do |certname, values|
row = [certname] + facts.map { |f| cell_value_for_output(extract_value(values, f)) }
puts CSV.generate_line(row, col_sep: col_sep)
end
Hosts assigned to a class⚓︎
Lists hosts that contain a specific class. Each class name must start with a capital letter (e.g. Parent_class::Child_class)
#!/bin/ksh
set -ue
if (( $# != 1 ))
then
print -u2 "Usage: $0 Classname"
exit 1
fi
class="$1"
if [[ $class != [A-Z]* ]]
then
print -u2 "Class name must be capitalised"
exit 1
fi
puppet-query 'resources[certname] {
type = "Class"
and title = "'$class'"
group by certname }' |
jq -r '.[] | .certname'
Watch Puppet server code deployments⚓︎
$ watch /home/kristian/deploy_status.rb
Every 2.0s: /home/kristian/deploy_status.rb
deploys-status:
0 queued
0 deploying
0 new
0 failed
file-sync-storage-status:
deployed: 6
file-sync-client-status: all-synced: true
true : puppetserver
true : puppetserver-orchestrator
true : compiler1
true : compiler2
#!/opt/puppetlabs/puppet/bin/ruby
require 'net/http'
require 'uri'
require 'openssl'
require 'json'
require 'pp'
token = File.read(".puppetlabs/token").chomp # (1)!
uri = URI.parse("https://puppetserver:8170/code-manager/v1/deploys/status") # (2)!
request = Net::HTTP::Get.new(uri)
request.content_type = "application/json"
request["X-Authentication"] = token
req_options = {
use_ssl: uri.scheme == "https",
verify_mode: OpenSSL::SSL::VERIFY_NONE,
}
response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
http.request(request)
end
if response.code != "200"
puts response.to_s
puts response.body
exit 1
end
res = JSON.parse(response.body)
puts "deploys-status: "
res['deploys-status'].each_pair do |k,v|
count = res['deploys-status'][k].length
puts "%5d %s" % [count, k]
printcount = k == "failed" ? 7 : 5
v.take(printcount).each do |e|
if k == "failed"
timestamp = e['queued-at'][11,8]
# pp e
message = e['error']['msg'].tr("\n"," ").sub(/.*-> /,'')
puts " #{e['environment']} @#{timestamp}: #{message}"
else
puts " #{e['environment']}"
end
end
end
puts "file-sync-storage-status: "
res['file-sync-storage-status'].each_pair do |k,v|
puts " #{k}: #{res['file-sync-storage-status'][k].length}"
end
puts "file-sync-client-status: all-synced: #{res['file-sync-client-status']['all-synced']}"
res['file-sync-client-status']['file-sync-clients'].keys.sort.each do |server|
status = res['file-sync-client-status']['file-sync-clients'][server]['synced-with-file-sync-storage']
puts " %-5s: %s" % [ status, server ]
end
- The location of your puppet token file.
- URL of your Puppet server.