aboutsummaryrefslogblamecommitdiff
path: root/modules/base_installation/files/scripts/report_print.rb
blob: 632374c11efba6dfa3ee704c02f3cd1bcd77b95e (plain) (tree)











































































































































































































































































































































































































                                                                                                                                                                                           
#!/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       => "",
    :green     => "",
    :yellow    => "",
    :cyan      => "",
    :bold      => "",
    :underline => "",
    :reset     => "",
  }

  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