CVQuesty
07/09/2026, 11:17 AM{
"name": "openvox_enc",
"description": "Dynamic Bolt inventory from the OpenVox GUI Node Classifier",
"version": "1.0.0",
"hooks": {
"resolve_reference": {
"task": "openvox_enc::resolve_reference"
}
}
}
The task:
{
"description": "Resolve Bolt inventory targets from the OpenVox GUI ENC database",
"supports_noop": true,
"parameters": {
"api_url": {
"description": "OpenVox GUI API base URL",
"type": "String",
"default": "<https://localhost:4567>"
},
"group": {
"description": "Optional: only return targets from this ENC group",
"type": "Optional[String]"
},
"transport": {
"description": "Default transport for targets",
"type": "String",
"default": "ssh"
},
"ssl_verify": {
"description": "Verify SSL certificates when calling the API",
"type": "Boolean",
"default": false
},
"api_token": {
"description": "Raw API token to authenticate with the OpenVox GUI",
"type": "Optional[String]"
},
"token_file": {
"description": "Path to a file containing the API token (defaults to /etc/puppetlabs/bolt/.bolt_token)",
"type": "Optional[String]",
"default": "/etc/puppetlabs/bolt/.bolt_token"
}
}
}
The Ruby task:
#!/opt/puppetlabs/puppet/bin/ruby
# frozen_string_literal: true
###############################################################################
# OpenVox ENC Bolt Inventory Plugin — resolve_reference task
#
# Queries the OpenVox GUI API to dynamically resolve Bolt inventory
# targets from the ENC (External Node Classifier) database.
#
# When Bolt encounters '_plugin: openvox_enc' in inventory.yaml, it
# calls this task. The task hits the /api/enc/inventory/bolt endpoint,
# which returns all classified nodes organized by ENC group. The task
# transforms that into the array of target hashes that Bolt expects.
#
# This eliminates manual inventory.yaml maintenance — nodes and groups
# are managed in the GUI's Node Classifier, and Bolt reads them live.
###############################################################################
require 'json'
require 'net/http'
require 'uri'
require 'openssl'
params = JSON.parse($stdin.read)
api_url = params['api_url'] || '<https://localhost:4567>'
group_filter = params['group']
transport = params['transport'] || 'ssh'
ssl_verify = params.fetch('ssl_verify', false)
api_token = params['api_token']
token_file = params['token_file'] || '/etc/puppetlabs/bolt/.bolt_token'
# Support reading token from a file (preferred for the local bolt user)
if (api_token.nil? || api_token.empty?) && File.exist?(token_file)
begin
api_token = File.read(token_file).strip
rescue => e
# If we can't read the token file, continue without it (will likely 401)
end
end
# ─── Query the OpenVox GUI API ────────────────────────────────
uri = URI.parse("#{api_url}/api/enc/inventory/bolt")
http = Net::HTTP.new(uri.host, uri.port)
if uri.scheme == 'https'
http.use_ssl = true
http.verify_mode = ssl_verify ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
end
begin
request = Net::HTTP::Get.new(uri.request_uri)
request['Accept'] = 'application/json'
if api_token && !api_token.empty?
request['Authorization'] = "Bearer #{api_token}"
end
response = http.request(request)
unless response.code.to_i == 200
raise "API returned HTTP #{response.code}: #{response.body}"
end
inventory = JSON.parse(response.body)
rescue StandardError => e
# Return error in Bolt's expected format
result = { '_error' => {
'msg' => "Failed to query OpenVox GUI ENC API: #{e.message}",
'kind' => 'openvox_enc/api_error',
'details' => { 'api_url' => api_url },
} }
puts result.to_json
exit 1
end
# ─── Build Bolt target list from ENC groups ───────────────────
targets = []
seen = {}
groups = inventory['groups'] || []
groups.each do |grp|
grp_name = grp['name']
# Skip PuppetDB plugin groups (Bolt handles those natively)
next if grp['targets'].is_a?(Array) && grp['targets'].any? { |t| t.is_a?(Hash) && t.key?('_plugin') }
# If a group filter is specified, only include matching groups
next if group_filter && grp_name != group_filter
grp_targets = grp['targets'] || []
grp_targets.each do |certname|
next if certname.is_a?(Hash) # Skip plugin references
next if seen[certname] # Deduplicate across groups
seen[certname] = true
target = {
'uri' => certname,
'name' => certname,
'config' => {
'transport' => transport,
transport => { 'host-key-check' => false },
},
'vars' => {
'enc_groups' => [],
},
}
# Collect all groups this node belongs to
groups.each do |g|
ts = g['targets'] || []
if ts.include?(certname)
target['vars']['enc_groups'] << g['name']
end
end
targets << target
end
end
# ─── Return targets to Bolt ──────────────────────────────────
puts({ 'value' => targets }.to_json)
Now I don’t have to manually manage inventory. Granted, I have groups of nodes and such all laid out in my DB, but its an example of just how extensible Bolt can be.