|
1 |
| -#!/opt/puppetlabs/puppet/bin/ruby |
2 |
| - |
3 |
| -require 'net/https' |
4 | 1 | require 'json'
|
5 | 2 | require 'uri'
|
6 | 3 | require 'time'
|
7 | 4 | require 'optparse'
|
8 | 5 | require 'yaml'
|
9 |
| - |
10 |
| -options = {} |
11 |
| - |
12 |
| -OptionParser.new { |opts| |
13 |
| - opts.banner = "Usage: #{File.basename(__FILE__)} [options]" |
14 |
| - opts.on('-p', '--[no-]print', 'Print to STDOUT') { |p| options[:print] = p } |
15 |
| - opts.on('-m [TYPE]', '--metrics_type [TYPE]', 'Type of metrics to collect') { |v| options[:metrics_type] = v } |
16 |
| - opts.on('-o [DIR]', '--output_dir [DIR]', 'Directory to save output to') { |o| options[:output_dir] = o } |
17 |
| - opts.on('--metrics_port [PORT]', 'The port the metrics service runs on') { |port| options[:metrics_port] = port } |
18 |
| - opts.on('--[no-]ssl', 'Use SSL when collecting metrics') { |ssl| options[:ssl] = ssl } |
19 |
| -}.parse! |
20 |
| - |
21 |
| -if options[:metrics_type].nil? |
22 |
| - STDERR.puts '--metrics_type (-m) is a required argument' |
23 |
| - exit 1 |
24 |
| -end |
25 |
| - |
26 |
| -# Allow scripts that require this script to access the options hash. |
27 |
| -OPTIONS = options |
28 |
| - |
29 |
| -config_file = File.expand_path("../../config/#{options[:metrics_type]}.yaml", __FILE__) |
30 |
| -config = YAML.load_file(config_file) |
31 |
| - |
32 |
| -def coalesce(higher_precedence, lower_precedence, default = nil) |
33 |
| - [higher_precedence, lower_precedence, default].find { |x| !x.nil? } |
34 |
| -end |
35 |
| - |
36 |
| -METRICS_TYPE = options[:metrics_type] |
37 |
| -OUTPUT_DIR = options[:output_dir] |
38 |
| -PE_VERSION = config['pe_version'] |
39 |
| -CERTNAME = config['clientcert'] |
40 |
| -HOSTS = config['hosts'] |
41 |
| -PORT = coalesce(options[:metrics_port], config['metrics_port']) |
42 |
| -USE_SSL = coalesce(options[:ssl], config['ssl'], true) |
43 |
| -EXCLUDES = config['excludes'] |
44 |
| -ADDITIONAL_METRICS = config['additional_metrics'] |
45 |
| -REMOTE_METRICS_ENABLED = config['remote_metrics_enabled'] |
46 |
| - |
47 |
| -# Metrics endpoints for our Puma services require a client certificate with SSL. |
48 |
| -# Metrics endpoints for our Trapper Keeper services do not require a client certificate. |
49 |
| - |
50 |
| -if USE_CLIENTCERT |
51 |
| - SSLDIR = `/opt/puppetlabs/bin/puppet config print ssldir`.chomp |
52 |
| -end |
53 |
| - |
54 |
| -$error_array = [] |
55 |
| - |
56 |
| -def generate_host_url(host, port, use_ssl) |
57 |
| - protocol = use_ssl ? 'https' : 'http' |
58 |
| - |
59 |
| - host_url = "#{protocol}://#{host}:#{port}" |
60 |
| -end |
61 |
| - |
62 |
| -def setup_connection(url, use_ssl) |
63 |
| - uri = URI.parse(url) |
64 |
| - http = Net::HTTP.new(uri.host, uri.port) |
65 |
| - |
66 |
| - if use_ssl |
67 |
| - http.use_ssl = true |
68 |
| - http.verify_mode = OpenSSL::SSL::VERIFY_NONE |
69 |
| - |
70 |
| - if USE_SSL && USE_CLIENTCERT |
71 |
| - # PE Puma services serve metrics from endpoints requiring a client certificate. |
72 |
| - # If https://github.com/puma/puma/pull/2098 is merged into the Puma used by PE, |
73 |
| - # we can collect metrics from /stats and /gc-stats without a client certificate. |
74 |
| - http.ca_path = "#{SSLDIR}/ca" |
75 |
| - http.ca_file = "#{SSLDIR}/certs/ca.pem" |
76 |
| - http.cert = OpenSSL::X509::Certificate.new(File.read("#{SSLDIR}/certs/#{CERTNAME}.pem")) |
77 |
| - http.key = OpenSSL::PKey::RSA.new(File.read("#{SSLDIR}/private_keys/#{CERTNAME}.pem")) |
78 |
| - end |
79 |
| - end |
80 |
| - |
81 |
| - [http, uri] |
82 |
| -end |
83 |
| - |
84 |
| -def get_endpoint(url, use_ssl) |
85 |
| - http, uri = setup_connection(url, use_ssl) |
86 |
| - |
87 |
| - endpoint_data = JSON.parse(http.get(uri.request_uri).body) |
88 |
| - if endpoint_data.key?('status') |
89 |
| - if endpoint_data['status'] == 200 |
90 |
| - endpoint_data = endpoint_data['value'] |
91 |
| - else |
92 |
| - $error_array << "HTTP Error #{endpoint_data['status']} for #{url}" |
93 |
| - endpoint_data = {} |
| 6 | +require 'puppet' |
| 7 | +require 'puppet/http' |
| 8 | + |
| 9 | +module PuppetX |
| 10 | + module Puppetlabs |
| 11 | + # Mixin module to provide instance variables and methods to the tk_metris script |
| 12 | + module PuppetMetricsCollector |
| 13 | + attr_accessor :client, :metrics_type, :output_dir, :certname, :hosts, :port, :excludes, :additional_metrics, :print, :errors |
| 14 | + |
| 15 | + def metrics_collector_setup |
| 16 | + # The Puppet HTTP client takes care of connection pooling, client cert auth, and more for us |
| 17 | + @client ||= Puppet.runtime[:http] |
| 18 | + @errors ||= [] |
| 19 | + |
| 20 | + OptionParser.new { |opts| |
| 21 | + opts.banner = "Usage: #{File.basename(__FILE__)} [options]" |
| 22 | + opts.on('-p', '--[no-]print', 'Print to STDOUT') { |p| @print = p } |
| 23 | + opts.on('-m [TYPE]', '--metrics_type [TYPE]', 'Type of metrics to collect') { |v| @metrics_type = v } |
| 24 | + opts.on('-o [DIR]', '--output_dir [DIR]', 'Directory to save output to') { |o| @output_dir = o } |
| 25 | + opts.on('--metrics_port [PORT]', 'The port the metrics service runs on') { |port| @metrics_port = port } |
| 26 | + }.parse! |
| 27 | + if @metrics_type.nil? |
| 28 | + STDERR.puts '--metrics_type (-m) is a required argument' |
| 29 | + exit 1 |
| 30 | + end |
| 31 | + |
| 32 | + config_file = File.expand_path("../../config/#{@metrics_type}.yaml", __FILE__) |
| 33 | + config = YAML.load_file(config_file) |
| 34 | + |
| 35 | + @certname = config['certname'] |
| 36 | + @hosts = config['hosts'] |
| 37 | + @excludes = config['excludes'] |
| 38 | + @additional_metrics = config['additional_metrics'] |
| 39 | + @port = @metrics_port ? @metrics_port : config['metrics_port'] |
| 40 | + rescue StandardError => e |
| 41 | + STDERR.puts "Failed to load config for #{@metrics_type}: #{e.message}" |
| 42 | + STDERR.puts e.backtrace |
| 43 | + nil |
| 44 | + end |
| 45 | + |
| 46 | + def get_endpoint(url) |
| 47 | + response = @client.get(url) |
| 48 | + |
| 49 | + if response.success? |
| 50 | + JSON.parse(response.body) |
| 51 | + else |
| 52 | + STDERR.puts "Received HTTP code '#{response.code}' with message '#{response.reason}' for #{url}" |
| 53 | + @errors << "HTTP Error #{response.code} for #{url}" |
| 54 | + {} |
| 55 | + end |
| 56 | + rescue StandardError => e |
| 57 | + STDERR.puts "Failed to query #{url}: #{e.message}" |
| 58 | + STDERR.puts e.backtrace |
| 59 | + |
| 60 | + @errors << e.to_s |
| 61 | + {} |
| 62 | + end |
| 63 | + |
| 64 | + def post_endpoint(url, body) |
| 65 | + response = @client.post(url, body, headers: { 'Content-Type' => 'application/json' }) |
| 66 | + if response.success? |
| 67 | + JSON.parse(response.body) |
| 68 | + else |
| 69 | + STDERR.puts "Received HTTP code '#{response.code}' with message '#{response.reason}' for #{url}" |
| 70 | + @errors << "HTTP Error #{response.code} for #{url}" |
| 71 | + {} |
| 72 | + end |
| 73 | + rescue StandardError => e |
| 74 | + STDERR.puts "Failed to post to #{url}: #{e.message}" |
| 75 | + STDERR.puts e.backtrace |
| 76 | + |
| 77 | + @errors << e.to_s |
| 78 | + {} |
| 79 | + end |
| 80 | + |
| 81 | + def retrieve_additional_metrics(url, _metrics_type, metrics) |
| 82 | + metrics_output = post_endpoint(url, metrics.to_json) |
| 83 | + return [] if metrics_output.empty? |
| 84 | + |
| 85 | + # For a status other than 200 or 404, add the HTTP code to the error array |
| 86 | + metrics_output.select { |m| m.key?('status') and ![200, 404].include?(m['status']) }.each do |m| |
| 87 | + @errors << "HTTP Error #{m['status']} for #{m['request']['mbean']}" |
| 88 | + end |
| 89 | + |
| 90 | + # Select metrics output that has a 'status' key |
| 91 | + metrics_output.select { |m| m.key?('status') }.map do |m| |
| 92 | + # Then merge the corresponding 'metrics' hash |
| 93 | + # e.g. for a metrics_output entry of |
| 94 | + # {"request"=>{"mbean"=>"puppetlabs.puppetdb.mq:name=global.command-parse-time", "type"=>"read"}, "value" => ...} |
| 95 | + # and a metrics entry of |
| 96 | + # {"type"=>"read", "name"=>"global_command-parse-time", "mbean"=>"puppetlabs.puppetdb.mq:name=global.command-parse-time"} |
| 97 | + # the result is that 'name' entry is added to the metris_output hash |
| 98 | + m.merge!(metrics.find { |n| n['mbean'] == m['request']['mbean'] }) |
| 99 | + |
| 100 | + status = m['status'] |
| 101 | + if status == 200 |
| 102 | + { 'name' => m['name'], 'data' => m['value'] } |
| 103 | + elsif status == 404 |
| 104 | + { 'name' => m['name'], 'data' => nil } |
| 105 | + end |
| 106 | + end |
| 107 | + end |
| 108 | + |
| 109 | + def filter_metrics(dataset, filters) |
| 110 | + return dataset if filters.empty? |
| 111 | + |
| 112 | + case dataset |
| 113 | + when Hash |
| 114 | + dataset = dataset.each_with_object({}) { |(k, v), m| m[k] = filter_metrics(v, filters) unless filters.include? k; } |
| 115 | + when Array |
| 116 | + dataset.map! { |e| filter_metrics(e, filters) } |
| 117 | + end |
| 118 | + |
| 119 | + dataset |
| 120 | + end |
94 | 121 | end
|
95 | 122 | end
|
96 |
| - endpoint_data |
97 |
| -rescue Exception => e |
98 |
| - $error_array << e.to_s |
99 |
| - endpoint_data = {} |
100 |
| -end |
101 |
| - |
102 |
| -def post_endpoint(url, use_ssl, post_data) |
103 |
| - http, uri = setup_connection(url, use_ssl) |
104 |
| - |
105 |
| - request = Net::HTTP::Post.new(uri.request_uri) |
106 |
| - request.content_type = 'application/json' |
107 |
| - request.body = post_data |
108 |
| - |
109 |
| - endpoint_data = JSON.parse(http.request(request).body) |
110 |
| - endpoint_data |
111 |
| -rescue Exception => e |
112 |
| - $error_array << e.to_s |
113 |
| - endpoint_data = {} |
114 |
| -end |
115 |
| - |
116 |
| -def retrieve_additional_metrics(host, port, use_ssl, metrics_type, metrics) |
117 |
| - if metrics_type == 'puppetdb' |
118 |
| - host = '127.0.0.1' if host == CERTNAME && !REMOTE_METRICS_ENABLED |
119 |
| - unless REMOTE_METRICS_ENABLED || ['127.0.0.1', 'localhost'].include?(host) |
120 |
| - # Puppet services released between May, 2020 and Feb 2021 had |
121 |
| - # the /metrics API disabled due to: |
122 |
| - # https://puppet.com/security/cve/CVE-2020-7943/ |
123 |
| - return [] |
124 |
| - end |
125 |
| - end |
126 |
| - |
127 |
| - host_url = generate_host_url(host, port, use_ssl) |
128 |
| - |
129 |
| - metrics_array = [] |
130 |
| - endpoint = "#{host_url}/metrics/v2/read" |
131 |
| - metrics_output = post_endpoint(endpoint, use_ssl, metrics.to_json) |
132 |
| - return metrics_array if metrics_output.empty? |
133 |
| - |
134 |
| - metrics.each_index do |index| |
135 |
| - metric_name = metrics[index]['name'] |
136 |
| - metric_data = metrics_output[index] |
137 |
| - |
138 |
| - metric_status = metric_data ? metric_data.dig('status') : nil |
139 |
| - next unless metric_status |
140 |
| - |
141 |
| - if metric_status == 200 |
142 |
| - metrics_array << { 'name' => metric_name, 'data' => metric_data['value'] } |
143 |
| - elsif metric_status == 404 |
144 |
| - metrics_array << { 'name' => metric_name, 'data' => nil } |
145 |
| - else |
146 |
| - metric_mbean = metrics[index]['mbean'] |
147 |
| - $error_array << "HTTP Error #{metric_status} for #{metric_mbean}" |
148 |
| - end |
149 |
| - end |
150 |
| - |
151 |
| - metrics_array |
152 |
| -end |
153 |
| - |
154 |
| -def filter_metrics(dataset, filters) |
155 |
| - return dataset if filters.empty? |
156 |
| - |
157 |
| - case dataset |
158 |
| - when Hash |
159 |
| - dataset = dataset.each_with_object({}) { |(k, v), m| m[k] = filter_metrics(v, filters) unless filters.include? k; } |
160 |
| - when Array |
161 |
| - dataset.map! { |e| filter_metrics(e, filters) } |
162 |
| - end |
163 |
| - |
164 |
| - dataset |
165 | 123 | end
|
0 commit comments