]>
Commit | Line | Data |
---|---|---|
1 | #!/usr/bin/env ruby | |
2 | # This file was modified from its original version at | |
3 | # https://github.com/ripienaar/puppet-reportprint/ | |
4 | # | |
5 | # Copyright 2013-2016 R.I.Pienaar and contributors | |
6 | # | |
7 | # Licensed under the Apache License, Version 2.0 (the "License"); | |
8 | # you may not use this file except in compliance with the License. | |
9 | # You may obtain a copy of the License at | |
10 | # | |
11 | # http://www.apache.org/licenses/LICENSE-2.0 | |
12 | # | |
13 | # Unless required by applicable law or agreed to in writing, software | |
14 | # distributed under the License is distributed on an "AS IS" BASIS, | |
15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
16 | # See the License for the specific language governing permissions and | |
17 | # limitations under the License. | |
18 | ||
19 | require 'puppet' | |
20 | require 'pp' | |
21 | require 'optparse' | |
22 | ||
23 | def get_server_reports_dir | |
24 | Puppet.settings[:reportdir] | |
25 | end | |
26 | ||
27 | class ::Numeric | |
28 | def bytes_to_human | |
29 | # Prevent nonsense values being returned for fractions | |
30 | if self >= 1 | |
31 | units = ['B', 'KB', 'MB' ,'GB' ,'TB'] | |
32 | e = (Math.log(self)/Math.log(1024)).floor | |
33 | # Cap at TB | |
34 | e = 4 if e > 4 | |
35 | s = "%.2f " % (to_f / 1024**e) | |
36 | s.sub(/\.?0*$/, units[e]) | |
37 | else | |
38 | "0 B" | |
39 | end | |
40 | end | |
41 | end | |
42 | ||
43 | def load_report(path) | |
44 | YAML.load_file(path) | |
45 | end | |
46 | ||
47 | def report_resources(report) | |
48 | report.resource_statuses | |
49 | end | |
50 | ||
51 | def resource_with_evaluation_time(report) | |
52 | report_resources(report).select{|r_name, r| !r.evaluation_time.nil? } | |
53 | end | |
54 | ||
55 | def resource_by_eval_time(report) | |
56 | report_resources(report).reject{|r_name, r| r.evaluation_time.nil? }.sort_by{|r_name, r| r.evaluation_time rescue 0} | |
57 | end | |
58 | ||
59 | def resources_of_type(report, type) | |
60 | report_resources(report).select{|r_name, r| r.resource_type == type} | |
61 | end | |
62 | ||
63 | def color(code, msg, reset=false) | |
64 | colors = { | |
65 | :red => "\e[31m", | |
66 | :green => "\e[32m", | |
67 | :yellow => "\e[33m", | |
68 | :cyan => "\e[36m", | |
69 | :bold => "\e[1m", | |
70 | :underline => "\e[4m", | |
71 | :reset => "\e[0m", | |
72 | } | |
73 | ||
74 | colors.merge!( | |
75 | :changed => colors[:yellow], | |
76 | :unchanged => colors[:green], | |
77 | :failed => colors[:red] | |
78 | ) | |
79 | ||
80 | return "%s%s%s%s" % [colors.fetch(code, ""), msg, colors[:reset], reset ? colors.fetch(reset, "") : ""] if @options[:color] | |
81 | ||
82 | msg | |
83 | end | |
84 | ||
85 | def print_report_summary(report) | |
86 | puts color(:bold, "Report for %s in environment %s at %s" % [color(:underline, report.host, :bold), color(:underline, report.environment, :bold), color(:underline, report.time, :bold)]) | |
87 | puts | |
88 | puts " Report File: %s" % @options[:report] | |
89 | puts " Report Status: %s" % report.status | |
90 | puts " Puppet Version: %s" % report.puppet_version | |
91 | puts " Report Format: %s" % report.report_format | |
92 | puts " Configuration Version: %s" % report.configuration_version | |
93 | puts " UUID: %s" % report.transaction_uuid rescue nil | |
94 | puts " Log Lines: %s %s" % [report.logs.size, @options[:logs] ? "" : "(show with --log)"] | |
95 | ||
96 | puts | |
97 | end | |
98 | ||
99 | def print_report_motd(report, motd_path) | |
100 | motd = [] | |
101 | header = "# #{report.host} #" | |
102 | headline = "#" * header.size | |
103 | motd << headline << header << headline << '' | |
104 | ||
105 | motd << "Last puppet run happened at %s in environment %s." % [report.time, report.environment] | |
106 | ||
107 | motd << "The result of this puppet run was %s." % color(report.status.to_sym, report.status) | |
108 | ||
109 | if report.metrics.empty? or report.metrics["events"].nil? | |
110 | motd << 'No Report Metrics.' | |
111 | else | |
112 | motd << 'Events:' | |
113 | report.metrics["events"].values.each do |metric| | |
114 | i, m, v = metric | |
115 | motd.last << ' ' << [m, v].join(': ') << '.' | |
116 | end | |
117 | end | |
118 | ||
119 | motd << '' << '' | |
120 | ||
121 | File.write(motd_path, motd.join("\n")) | |
122 | end | |
123 | ||
124 | def print_report_metrics(report) | |
125 | if report.metrics.empty? | |
126 | puts color(:bold, "No Report Metrics") | |
127 | puts | |
128 | return | |
129 | end | |
130 | ||
131 | puts color(:bold, "Report Metrics:") | |
132 | puts | |
133 | ||
134 | padding = report.metrics.map{|i, m| m.values}.flatten(1).map{|i, m, v| m.size}.sort[-1] + 6 | |
135 | ||
136 | report.metrics.sort_by{|i, m| m.label}.each do |i, metric| | |
137 | puts " %s:" % metric.label | |
138 | ||
139 | metric.values.sort_by{|j, m, v| v}.reverse.each do |j, m, v| | |
140 | puts "%#{padding}s: %s" % [m, v] | |
141 | end | |
142 | ||
143 | puts | |
144 | end | |
145 | ||
146 | puts | |
147 | end | |
148 | ||
149 | def print_summary_by_type(report) | |
150 | summary = {} | |
151 | ||
152 | report_resources(report).each do |resource| | |
153 | if resource[0] =~ /^(.+?)\[/ | |
154 | name = $1 | |
155 | ||
156 | summary[name] ||= 0 | |
157 | summary[name] += 1 | |
158 | else | |
159 | STDERR.puts "ERROR: Cannot parse type %s" % resource[0] | |
160 | end | |
161 | end | |
162 | ||
163 | puts color(:bold, "Resources by resource type:") | |
164 | puts | |
165 | ||
166 | summary.sort_by{|k, v| v}.reverse.each do |type, count| | |
167 | puts " %4d %s" % [count, type] | |
168 | end | |
169 | ||
170 | puts | |
171 | end | |
172 | ||
173 | def print_slow_resources(report, number=20) | |
174 | if report.report_format < 4 | |
175 | puts color(:red, " Cannot print slow resources for report versions %d" % report.report_format) | |
176 | puts | |
177 | return | |
178 | end | |
179 | ||
180 | resources = resource_by_eval_time(report) | |
181 | ||
182 | number = resources.size if resources.size < number | |
183 | ||
184 | puts color(:bold, "Slowest %d resources by evaluation time:" % number) | |
185 | puts | |
186 | ||
187 | resources[(0-number)..-1].reverse.each do |r_name, r| | |
188 | puts " %7.2f %s" % [r.evaluation_time, r_name] | |
189 | end | |
190 | ||
191 | puts | |
192 | end | |
193 | ||
194 | def print_logs(report) | |
195 | puts color(:bold, "%d Log lines:" % report.logs.size) | |
196 | puts | |
197 | ||
198 | report.logs.each do |log| | |
199 | puts " %s" % log.to_report | |
200 | end | |
201 | ||
202 | puts | |
203 | end | |
204 | ||
205 | def print_summary_by_containment_path(report, number=20) | |
206 | resources = resource_with_evaluation_time(report) | |
207 | ||
208 | containment = Hash.new(0) | |
209 | ||
210 | resources.each do |r_name, r| | |
211 | r.containment_path.each do |containment_path| | |
212 | #if containment_path !~ /\[/ | |
213 | containment[containment_path] += r.evaluation_time | |
214 | #end | |
215 | end | |
216 | end | |
217 | ||
218 | number = containment.size if containment.size < number | |
219 | ||
220 | puts color(:bold, "%d most time consuming containment" % number) | |
221 | puts | |
222 | ||
223 | containment.sort_by{|c, s| s}[(0-number)..-1].reverse.each do |c_name, evaluation_time| | |
224 | puts " %7.2f %s" % [evaluation_time, c_name] | |
225 | end | |
226 | ||
227 | puts | |
228 | end | |
229 | ||
230 | def print_files(report, number=20) | |
231 | resources = resources_of_type(report, "File") | |
232 | ||
233 | files = {} | |
234 | ||
235 | resources.each do |r_name, r| | |
236 | if r_name =~ /^File\[(.+)\]$/ | |
237 | file = $1 | |
238 | ||
239 | if File.exist?(file) && File.readable?(file) && File.file?(file) && !File.symlink?(file) | |
240 | files[file] = File.size?(file) || 0 | |
241 | end | |
242 | end | |
243 | end | |
244 | ||
245 | number = files.size if files.size < number | |
246 | ||
247 | puts color(:bold, "%d largest managed files" % number) + " (only those with full path as resource name that are readable)" | |
248 | puts | |
249 | ||
250 | files.sort_by{|f, s| s}[(0-number)..-1].reverse.each do |f_name, size| | |
251 | puts " %9s %s" % [size.bytes_to_human, f_name] | |
252 | end | |
253 | ||
254 | puts | |
255 | end | |
256 | ||
257 | def get_reports_for_node(nodename) | |
258 | Dir.glob("%s/%s/*.yaml" % [get_server_reports_dir, nodename]).sort_by{|p|File.basename(p, ".*")} | |
259 | end | |
260 | ||
261 | def load_report_for_node(nodename, report) | |
262 | report_path = "%s/%s/%s.yaml" % [get_server_reports_dir, nodename, report] | |
263 | puts report_path | |
264 | load_report(report_path) unless report_path.nil? | |
265 | end | |
266 | ||
267 | def load_report_by_id(report) | |
268 | report_glob = "%s/*/%s.yaml" % [get_server_reports_dir, report] | |
269 | Dir.glob(report_glob).map do |report_path| | |
270 | puts report_path | |
271 | load_report(report_path) unless report_path.nil? | |
272 | end.first | |
273 | end | |
274 | ||
275 | def load_last_report_for_node(nodename) | |
276 | report_path = get_reports_for_node(nodename).last | |
277 | load_report(report_path) unless report_path.nil? | |
278 | end | |
279 | ||
280 | def print_reports_for_node(nodename) | |
281 | puts color(:bold, "Reports for %s" % nodename) | |
282 | get_reports_for_node(nodename).each do |report_path| | |
283 | prefix = File.basename(report_path, ".*") | |
284 | report = load_report(report_path) | |
285 | print_report_oneliner(report, prefix) | |
286 | end | |
287 | end | |
288 | ||
289 | def print_report_oneliner(report, prefix) | |
290 | puts "%s: %s" % [prefix, color(report.status.to_sym, report.status)] | |
291 | end | |
292 | ||
293 | def print_node_oneliner(nodename) | |
294 | report = load_last_report_for_node(nodename) | |
295 | print_report_oneliner(report, report.name) unless report.nil? | |
296 | end | |
297 | ||
298 | def print_server_nodes_status | |
299 | puts color(:bold, 'Nodes list') | |
300 | dir = get_server_reports_dir | |
301 | puts color(:bold, 'No nodes found!') unless Puppet::FileSystem.exist?(dir) | |
302 | Dir.glob("%s/*/" % dir).each do |node_path| | |
303 | print_node_oneliner(File.basename(node_path)) | |
304 | end | |
305 | end | |
306 | ||
307 | def initialize_puppet | |
308 | require 'puppet/util/run_mode' | |
309 | Puppet.settings.preferred_run_mode = :agent | |
310 | Puppet.settings.initialize_global_settings([]) | |
311 | Puppet.settings.initialize_app_defaults(Puppet::Settings.app_defaults_for_run_mode(Puppet.run_mode)) | |
312 | end | |
313 | ||
314 | initialize_puppet | |
315 | ||
316 | opt = OptionParser.new | |
317 | ||
318 | @options = { | |
319 | :logs => false, | |
320 | :history => false, | |
321 | :server => false, | |
322 | :node => nil, | |
323 | :motd => false, | |
324 | :motd_path => '/etc/motd', | |
325 | :count => 20, | |
326 | :report => Puppet[:lastrunreport], | |
327 | :reportid => nil, | |
328 | :color => STDOUT.tty?} | |
329 | ||
330 | opt.on("--logs", "Show logs") do |val| | |
331 | @options[:logs] = val | |
332 | end | |
333 | ||
334 | opt.on("--nodelist", "(Puppet Server) List Puppet nodes and the status of their last report") do |val| | |
335 | @options[:server] = val | |
336 | end | |
337 | ||
338 | opt.on("--node [NODE]", "(Puppet Server) Use last report of a node") do |val| | |
339 | @options[:node] = val | |
340 | end | |
341 | ||
342 | opt.on("--history", "(with --node) Print the reports history for a node") do |val| | |
343 | @options[:history] = val | |
344 | end | |
345 | ||
346 | opt.on("--motd", "Produce an output suitable for MOTD") do |val| | |
347 | @options[:motd] = val | |
348 | end | |
349 | ||
350 | opt.on("--motd-path [PATH]", "Path to the MOTD file to overwrite with the --motd option") do |val| | |
351 | @options[:motd_path] = val | |
352 | end | |
353 | ||
354 | opt.on("--count [RESOURCES]", Integer, "Number of resources to show evaluation times for") do |val| | |
355 | @options[:count] = val | |
356 | end | |
357 | ||
358 | opt.on("--report [REPORT]", "Path to the Puppet last run report") do |val| | |
359 | abort("Could not find report %s" % val) unless File.readable?(val) | |
360 | @options[:report] = val | |
361 | end | |
362 | ||
363 | opt.on("--report-id [REPORTID]", "(with --node) ID of the report to load") do |val| | |
364 | @options[:reportid] = val | |
365 | end | |
366 | ||
367 | opt.on("--[no-]color", "Colorize the report") do |val| | |
368 | @options[:color] = val | |
369 | end | |
370 | ||
371 | opt.parse! | |
372 | ||
373 | report = load_report(@options[:report]) unless @options[:server] or @options[:node] | |
374 | if @options[:node] and not @options[:history] and not @options[:reportid] | |
375 | report = load_last_report_for_node(@options[:node]) | |
376 | elsif @options[:node] and @options[:reportid] | |
377 | report = load_report_for_node(@options[:node], @options[:reportid]) | |
378 | elsif @options[:reportid] | |
379 | report = load_report_by_id(@options[:reportid]) | |
380 | end | |
381 | ||
382 | if @options[:server] | |
383 | print_server_nodes_status | |
384 | elsif @options[:node] and @options[:history] | |
385 | print_reports_for_node(@options[:node]) | |
386 | elsif @options[:motd] | |
387 | print_report_motd(report, @options[:motd_path]) | |
388 | else | |
389 | print_report_summary(report) | |
390 | print_report_metrics(report) | |
391 | print_summary_by_type(report) | |
392 | print_slow_resources(report, @options[:count]) | |
393 | print_files(report, @options[:count]) | |
394 | print_summary_by_containment_path(report, @options[:count]) | |
395 | print_logs(report) if @options[:logs] | |
396 | end |