--- /dev/null
+#!/usr/bin/env ruby
+# This file was modified from its original version at
+# https://github.com/ripienaar/puppet-reportprint/
+#
+# Copyright 2013-2016 R.I.Pienaar and contributors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require 'puppet'
+require 'pp'
+require 'optparse'
+
+def get_server_reports_dir
+ Puppet.settings[:reportdir]
+end
+
+class ::Numeric
+ def bytes_to_human
+ # Prevent nonsense values being returned for fractions
+ if self >= 1
+ units = ['B', 'KB', 'MB' ,'GB' ,'TB']
+ e = (Math.log(self)/Math.log(1024)).floor
+ # Cap at TB
+ e = 4 if e > 4
+ s = "%.2f " % (to_f / 1024**e)
+ s.sub(/\.?0*$/, units[e])
+ else
+ "0 B"
+ end
+ end
+end
+
+def load_report(path)
+ YAML.load_file(path)
+end
+
+def report_resources(report)
+ report.resource_statuses
+end
+
+def resource_with_evaluation_time(report)
+ report_resources(report).select{|r_name, r| !r.evaluation_time.nil? }
+end
+
+def resource_by_eval_time(report)
+ report_resources(report).reject{|r_name, r| r.evaluation_time.nil? }.sort_by{|r_name, r| r.evaluation_time rescue 0}
+end
+
+def resources_of_type(report, type)
+ report_resources(report).select{|r_name, r| r.resource_type == type}
+end
+
+def color(code, msg, reset=false)
+ colors = {
+ :red => "\e[31m",
+ :green => "\e[32m",
+ :yellow => "\e[33m",
+ :cyan => "\e[36m",
+ :bold => "\e[1m",
+ :underline => "\e[4m",
+ :reset => "\e[0m",
+ }
+
+ colors.merge!(
+ :changed => colors[:yellow],
+ :unchanged => colors[:green],
+ :failed => colors[:red]
+ )
+
+ return "%s%s%s%s" % [colors.fetch(code, ""), msg, colors[:reset], reset ? colors.fetch(reset, "") : ""] if @options[:color]
+
+ msg
+end
+
+def print_report_summary(report)
+ 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)])
+ puts
+ puts " Report File: %s" % @options[:report]
+ puts " Report Status: %s" % report.status
+ puts " Puppet Version: %s" % report.puppet_version
+ puts " Report Format: %s" % report.report_format
+ puts " Configuration Version: %s" % report.configuration_version
+ puts " UUID: %s" % report.transaction_uuid rescue nil
+ puts " Log Lines: %s %s" % [report.logs.size, @options[:logs] ? "" : "(show with --log)"]
+
+ puts
+end
+
+def print_report_motd(report, motd_path)
+ motd = []
+ header = "# #{report.host} #"
+ headline = "#" * header.size
+ motd << headline << header << headline << ''
+
+ motd << "Last puppet run happened at %s in environment %s." % [report.time, report.environment]
+
+ motd << "The result of this puppet run was %s." % color(report.status.to_sym, report.status)
+
+ if report.metrics.empty? or report.metrics["events"].nil?
+ motd << 'No Report Metrics.'
+ else
+ motd << 'Events:'
+ report.metrics["events"].values.each do |metric|
+ i, m, v = metric
+ motd.last << ' ' << [m, v].join(': ') << '.'
+ end
+ end
+
+ motd << '' << ''
+
+ File.write(motd_path, motd.join("\n"))
+end
+
+def print_report_metrics(report)
+ if report.metrics.empty?
+ puts color(:bold, "No Report Metrics")
+ puts
+ return
+ end
+
+ puts color(:bold, "Report Metrics:")
+ puts
+
+ padding = report.metrics.map{|i, m| m.values}.flatten(1).map{|i, m, v| m.size}.sort[-1] + 6
+
+ report.metrics.sort_by{|i, m| m.label}.each do |i, metric|
+ puts " %s:" % metric.label
+
+ metric.values.sort_by{|j, m, v| v}.reverse.each do |j, m, v|
+ puts "%#{padding}s: %s" % [m, v]
+ end
+
+ puts
+ end
+
+ puts
+end
+
+def print_summary_by_type(report)
+ summary = {}
+
+ report_resources(report).each do |resource|
+ if resource[0] =~ /^(.+?)\[/
+ name = $1
+
+ summary[name] ||= 0
+ summary[name] += 1
+ else
+ STDERR.puts "ERROR: Cannot parse type %s" % resource[0]
+ end
+ end
+
+ puts color(:bold, "Resources by resource type:")
+ puts
+
+ summary.sort_by{|k, v| v}.reverse.each do |type, count|
+ puts " %4d %s" % [count, type]
+ end
+
+ puts
+end
+
+def print_slow_resources(report, number=20)
+ if report.report_format < 4
+ puts color(:red, " Cannot print slow resources for report versions %d" % report.report_format)
+ puts
+ return
+ end
+
+ resources = resource_by_eval_time(report)
+
+ number = resources.size if resources.size < number
+
+ puts color(:bold, "Slowest %d resources by evaluation time:" % number)
+ puts
+
+ resources[(0-number)..-1].reverse.each do |r_name, r|
+ puts " %7.2f %s" % [r.evaluation_time, r_name]
+ end
+
+ puts
+end
+
+def print_logs(report)
+ puts color(:bold, "%d Log lines:" % report.logs.size)
+ puts
+
+ report.logs.each do |log|
+ puts " %s" % log.to_report
+ end
+
+ puts
+end
+
+def print_summary_by_containment_path(report, number=20)
+ resources = resource_with_evaluation_time(report)
+
+ containment = Hash.new(0)
+
+ resources.each do |r_name, r|
+ r.containment_path.each do |containment_path|
+ #if containment_path !~ /\[/
+ containment[containment_path] += r.evaluation_time
+ #end
+ end
+ end
+
+ number = containment.size if containment.size < number
+
+ puts color(:bold, "%d most time consuming containment" % number)
+ puts
+
+ containment.sort_by{|c, s| s}[(0-number)..-1].reverse.each do |c_name, evaluation_time|
+ puts " %7.2f %s" % [evaluation_time, c_name]
+ end
+
+ puts
+end
+
+def print_files(report, number=20)
+ resources = resources_of_type(report, "File")
+
+ files = {}
+
+ resources.each do |r_name, r|
+ if r_name =~ /^File\[(.+)\]$/
+ file = $1
+
+ if File.exist?(file) && File.readable?(file) && File.file?(file) && !File.symlink?(file)
+ files[file] = File.size?(file) || 0
+ end
+ end
+ end
+
+ number = files.size if files.size < number
+
+ puts color(:bold, "%d largest managed files" % number) + " (only those with full path as resource name that are readable)"
+ puts
+
+ files.sort_by{|f, s| s}[(0-number)..-1].reverse.each do |f_name, size|
+ puts " %9s %s" % [size.bytes_to_human, f_name]
+ end
+
+ puts
+end
+
+def get_reports_for_node(nodename)
+ Dir.glob("%s/%s/*.yaml" % [get_server_reports_dir, nodename]).sort_by{|p|File.basename(p, ".*")}
+end
+
+def load_report_for_node(nodename, report)
+ report_path = "%s/%s/%s.yaml" % [get_server_reports_dir, nodename, report]
+ puts report_path
+ load_report(report_path) unless report_path.nil?
+end
+
+def load_report_by_id(report)
+ report_glob = "%s/*/%s.yaml" % [get_server_reports_dir, report]
+ Dir.glob(report_glob).map do |report_path|
+ puts report_path
+ load_report(report_path) unless report_path.nil?
+ end.first
+end
+
+def load_last_report_for_node(nodename)
+ report_path = get_reports_for_node(nodename).last
+ load_report(report_path) unless report_path.nil?
+end
+
+def print_reports_for_node(nodename)
+ puts color(:bold, "Reports for %s" % nodename)
+ get_reports_for_node(nodename).each do |report_path|
+ prefix = File.basename(report_path, ".*")
+ report = load_report(report_path)
+ print_report_oneliner(report, prefix)
+ end
+end
+
+def print_report_oneliner(report, prefix)
+ puts "%s: %s" % [prefix, color(report.status.to_sym, report.status)]
+end
+
+def print_node_oneliner(nodename)
+ report = load_last_report_for_node(nodename)
+ print_report_oneliner(report, report.name) unless report.nil?
+end
+
+def print_server_nodes_status
+ puts color(:bold, 'Nodes list')
+ dir = get_server_reports_dir
+ puts color(:bold, 'No nodes found!') unless Puppet::FileSystem.exist?(dir)
+ Dir.glob("%s/*/" % dir).each do |node_path|
+ print_node_oneliner(File.basename(node_path))
+ end
+end
+
+def initialize_puppet
+ require 'puppet/util/run_mode'
+ Puppet.settings.preferred_run_mode = :agent
+ Puppet.settings.initialize_global_settings([])
+ Puppet.settings.initialize_app_defaults(Puppet::Settings.app_defaults_for_run_mode(Puppet.run_mode))
+end
+
+initialize_puppet
+
+opt = OptionParser.new
+
+@options = {
+ :logs => false,
+ :history => false,
+ :server => false,
+ :node => nil,
+ :motd => false,
+ :motd_path => '/etc/motd',
+ :count => 20,
+ :report => Puppet[:lastrunreport],
+ :reportid => nil,
+ :color => STDOUT.tty?}
+
+opt.on("--logs", "Show logs") do |val|
+ @options[:logs] = val
+end
+
+opt.on("--nodelist", "(Puppet Server) List Puppet nodes and the status of their last report") do |val|
+ @options[:server] = val
+end
+
+opt.on("--node [NODE]", "(Puppet Server) Use last report of a node") do |val|
+ @options[:node] = val
+end
+
+opt.on("--history", "(with --node) Print the reports history for a node") do |val|
+ @options[:history] = val
+end
+
+opt.on("--motd", "Produce an output suitable for MOTD") do |val|
+ @options[:motd] = val
+end
+
+opt.on("--motd-path [PATH]", "Path to the MOTD file to overwrite with the --motd option") do |val|
+ @options[:motd_path] = val
+end
+
+opt.on("--count [RESOURCES]", Integer, "Number of resources to show evaluation times for") do |val|
+ @options[:count] = val
+end
+
+opt.on("--report [REPORT]", "Path to the Puppet last run report") do |val|
+ abort("Could not find report %s" % val) unless File.readable?(val)
+ @options[:report] = val
+end
+
+opt.on("--report-id [REPORTID]", "(with --node) ID of the report to load") do |val|
+ @options[:reportid] = val
+end
+
+opt.on("--[no-]color", "Colorize the report") do |val|
+ @options[:color] = val
+end
+
+opt.parse!
+
+report = load_report(@options[:report]) unless @options[:server] or @options[:node]
+if @options[:node] and not @options[:history] and not @options[:reportid]
+ report = load_last_report_for_node(@options[:node])
+elsif @options[:node] and @options[:reportid]
+ report = load_report_for_node(@options[:node], @options[:reportid])
+elsif @options[:reportid]
+ report = load_report_by_id(@options[:reportid])
+end
+
+if @options[:server]
+ print_server_nodes_status
+elsif @options[:node] and @options[:history]
+ print_reports_for_node(@options[:node])
+elsif @options[:motd]
+ print_report_motd(report, @options[:motd_path])
+else
+ print_report_summary(report)
+ print_report_metrics(report)
+ print_summary_by_type(report)
+ print_slow_resources(report, @options[:count])
+ print_files(report, @options[:count])
+ print_summary_by_containment_path(report, @options[:count])
+ print_logs(report) if @options[:logs]
+end