diff --git a/lib/puppet/transaction/report.rb b/lib/puppet/transaction/report.rb index 6315973ba..8e04759ad 100644 --- a/lib/puppet/transaction/report.rb +++ b/lib/puppet/transaction/report.rb @@ -1,169 +1,169 @@ require 'puppet' require 'puppet/indirector' # A class for reporting what happens on each client. Reports consist of # two types of data: Logs and Metrics. Logs are the output that each # change produces, and Metrics are all of the numerical data involved # in the transaction. class Puppet::Transaction::Report extend Puppet::Indirector indirects :report, :terminus_class => :processor attr_accessor :configuration_version attr_reader :resource_statuses, :logs, :metrics, :host, :time, :kind, :status # This is necessary since Marshall doesn't know how to # dump hash with default proc (see below @records) def self.default_format :yaml end def <<(msg) @logs << msg self end def add_times(name, value) @external_times[name] = value end def add_metric(name, hash) metric = Puppet::Util::Metric.new(name) hash.each do |name, value| metric.newvalue(name, value) end @metrics[metric.name] = metric metric end def add_resource_status(status) @resource_statuses[status.resource] = status end def compute_status(resource_metrics, change_metric) - if (resource_metrics[:failed] || 0) > 0 + if (resource_metrics["failed"] || 0) > 0 'failed' elsif change_metric > 0 'changed' else 'unchanged' end end def finalize_report resource_metrics = add_metric(:resources, calculate_resource_metrics) add_metric(:time, calculate_time_metrics) change_metric = calculate_change_metric - add_metric(:changes, {:total => change_metric}) + add_metric(:changes, {"total" => change_metric}) add_metric(:events, calculate_event_metrics) @status = compute_status(resource_metrics, change_metric) end def initialize(kind, configuration_version=nil) @metrics = {} @logs = [] @resource_statuses = {} @external_times ||= {} @host = Puppet[:certname] @time = Time.now @kind = kind @report_format = 2 @puppet_version = Puppet.version @configuration_version = configuration_version @status = 'failed' # assume failed until the report is finalized end def name host end # Provide a summary of this report. def summary ret = "" @metrics.sort { |a,b| a[1].label <=> b[1].label }.each do |name, metric| ret += "#{metric.label}:\n" metric.values.sort { |a,b| # sort by label if a[0] == :total 1 elsif b[0] == :total -1 else a[1] <=> b[1] end }.each do |name, label, value| next if value == 0 value = "%0.2f" % value if value.is_a?(Float) ret += " %15s %s\n" % [label + ":", value] end end ret end # Based on the contents of this report's metrics, compute a single number # that represents the report. The resulting number is a bitmask where # individual bits represent the presence of different metrics. def exit_status status = 0 - status |= 2 if @metrics["changes"][:total] > 0 - status |= 4 if @metrics["resources"][:failed] > 0 + status |= 2 if @metrics["changes"]["total"] > 0 + status |= 4 if @metrics["resources"]["failed"] > 0 status end def to_yaml_properties (instance_variables - ["@external_times"]).sort end private def calculate_change_metric resource_statuses.map { |name, status| status.change_count || 0 }.inject(0) { |a,b| a+b } end def calculate_event_metrics metrics = Hash.new(0) - metrics[:total] = 0 + metrics["total"] = 0 resource_statuses.each do |name, status| - metrics[:total] += status.events.length + metrics["total"] += status.events.length status.events.each do |event| metrics[event.status] += 1 end end metrics end def calculate_resource_metrics metrics = Hash.new(0) - metrics[:total] = resource_statuses.length + metrics["total"] = resource_statuses.length resource_statuses.each do |name, status| Puppet::Resource::Status::STATES.each do |state| - metrics[state] += 1 if status.send(state) + metrics[state.to_s] += 1 if status.send(state) end end metrics end def calculate_time_metrics metrics = Hash.new(0) resource_statuses.each do |name, status| type = Puppet::Resource.new(name).type metrics[type.to_s.downcase] += status.evaluation_time if status.evaluation_time end @external_times.each do |name, value| metrics[name.to_s.downcase] = value end metrics["total"] = metrics.values.inject(0) { |a,b| a+b } metrics end end diff --git a/lib/puppet/util/metric.rb b/lib/puppet/util/metric.rb index 8f55e7b44..09bbb6137 100644 --- a/lib/puppet/util/metric.rb +++ b/lib/puppet/util/metric.rb @@ -1,187 +1,188 @@ # included so we can test object types require 'puppet' # A class for handling metrics. This is currently ridiculously hackish. class Puppet::Util::Metric attr_accessor :type, :name, :value, :label attr_writer :values attr_writer :basedir # Return a specific value def [](name) if value = @values.find { |v| v[0] == name } return value[2] else return 0 end end def basedir if defined?(@basedir) @basedir else Puppet[:rrddir] end end def create(start = nil) Puppet.settings.use(:main, :metrics) start ||= Time.now.to_i - 5 args = [] if Puppet.features.rrd_legacy? && ! Puppet.features.rrd? @rrd = RRDtool.new(self.path) end values.each { |value| # the 7200 is the heartbeat -- this means that any data that isn't # more frequently than every two hours gets thrown away args.push "DS:#{value[0]}:GAUGE:7200:U:U" } args.push "RRA:AVERAGE:0.5:1:300" begin if Puppet.features.rrd_legacy? && ! Puppet.features.rrd? @rrd.create( Puppet[:rrdinterval].to_i, start, args) else RRD.create( self.path, '-s', Puppet[:rrdinterval].to_i.to_s, '-b', start.to_i.to_s, *args) end rescue => detail raise "Could not create RRD file #{path}: #{detail}" end end def dump if Puppet.features.rrd_legacy? && ! Puppet.features.rrd? puts @rrd.info else puts RRD.info(self.path) end end def graph(range = nil) unless Puppet.features.rrd? || Puppet.features.rrd_legacy? Puppet.warning "RRD library is missing; cannot graph metrics" return end unit = 60 * 60 * 24 colorstack = %w{#00ff00 #ff0000 #0000ff #ffff00 #ff99ff #ff9966 #66ffff #990000 #099000 #000990 #f00990 #0f0f0f #555555 #333333 #ffffff} {:daily => unit, :weekly => unit * 7, :monthly => unit * 30, :yearly => unit * 365}.each do |name, time| file = self.path.sub(/\.rrd$/, "-#{name}.png") args = [file] args.push("--title",self.label) args.push("--imgformat","PNG") args.push("--interlace") i = 0 defs = [] lines = [] #p @values.collect { |s,l| s } values.zip(colorstack).each { |value,color| next if value.nil? # this actually uses the data label defs.push("DEF:#{value[0]}=#{self.path}:#{value[0]}:AVERAGE") lines.push("LINE2:#{value[0]}#{color}:#{value[1]}") } args << defs args << lines args.flatten! if range if Puppet.features.rrd_legacy? && ! Puppet.features.rrd? args.push("--start",range[0],"--end",range[1]) else args.push("--start",range[0].to_i.to_s,"--end",range[1].to_i.to_s) end else if Puppet.features.rrd_legacy? && ! Puppet.features.rrd? args.push("--start", Time.now.to_i - time, "--end", Time.now.to_i) else args.push("--start", (Time.now.to_i - time).to_s, "--end", Time.now.to_i.to_s) end end begin #Puppet.warning "args = #{args}" if Puppet.features.rrd_legacy? && ! Puppet.features.rrd? RRDtool.graph( args ) else RRD.graph( *args ) end rescue => detail Puppet.err "Failed to graph #{self.name}: #{detail}" end end end def initialize(name,label = nil) @name = name.to_s @label = label || labelize(name) @values = [] end def path File.join(self.basedir, @name + ".rrd") end def newvalue(name,value,label = nil) + raise ArgumentError.new("metric name #{name.inspect} is not a string") unless name.is_a? String label ||= labelize(name) @values.push [name,label,value] end def store(time) unless Puppet.features.rrd? || Puppet.features.rrd_legacy? Puppet.warning "RRD library is missing; cannot store metrics" return end self.create(time - 5) unless FileTest.exists?(self.path) if Puppet.features.rrd_legacy? && ! Puppet.features.rrd? @rrd ||= RRDtool.new(self.path) end # XXX this is not terribly error-resistant args = [time] temps = [] values.each { |value| #Puppet.warning "value[0]: #{value[0]}; value[1]: #{value[1]}; value[2]: #{value[2]}; " args.push value[2] temps.push value[0] } arg = args.join(":") template = temps.join(":") begin if Puppet.features.rrd_legacy? && ! Puppet.features.rrd? @rrd.update( template, [ arg ] ) else RRD.update( self.path, '-t', template, arg ) end #system("rrdtool updatev #{self.path} '#{arg}'") rescue => detail raise Puppet::Error, "Failed to update #{self.name}: #{detail}" end end def values @values.sort { |a, b| a[1] <=> b[1] } end private # Convert a name into a label. def labelize(name) name.to_s.capitalize.gsub("_", " ") end end # This is necessary because we changed the class path in early 2007, # and reports directly yaml-dump these metrics, so both client and server # have to agree on the class name. Puppet::Metric = Puppet::Util::Metric diff --git a/spec/integration/indirector/report/rest_spec.rb b/spec/integration/indirector/report/rest_spec.rb index 7fa026b73..5b5b2ddd8 100644 --- a/spec/integration/indirector/report/rest_spec.rb +++ b/spec/integration/indirector/report/rest_spec.rb @@ -1,97 +1,93 @@ #!/usr/bin/env ruby Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } require 'puppet/transaction/report' require 'puppet/network/server' require 'puppet/network/http/webrick/rest' describe "Report REST Terminus" do before do Puppet[:masterport] = 34343 Puppet[:server] = "localhost" # Get a safe temporary file @tmpfile = Tempfile.new("webrick_integration_testing") @dir = @tmpfile.path + "_dir" Puppet.settings[:confdir] = @dir Puppet.settings[:vardir] = @dir Puppet.settings[:group] = Process.gid Puppet.settings[:server] = "127.0.0.1" Puppet.settings[:masterport] = "34343" Puppet::Util::Cacher.expire Puppet[:servertype] = 'webrick' Puppet[:server] = '127.0.0.1' Puppet[:certname] = '127.0.0.1' # Generate the certificate with a local CA Puppet::SSL::Host.ca_location = :local ca = Puppet::SSL::CertificateAuthority.new ca.generate(Puppet[:certname]) unless Puppet::SSL::Certificate.find(Puppet[:certname]) ca.generate("foo.madstop.com") unless Puppet::SSL::Certificate.find(Puppet[:certname]) @host = Puppet::SSL::Host.new(Puppet[:certname]) @params = { :port => 34343, :handlers => [ :report ] } @server = Puppet::Network::Server.new(@params) @server.listen # Let's use REST for our reports :-) @old_terminus = Puppet::Transaction::Report.indirection.terminus_class Puppet::Transaction::Report.terminus_class = :rest # LAK:NOTE We need to have a fake model here so that our indirected methods get # passed through REST; otherwise we'd be stubbing 'save', which would cause an immediate # return. @report = stub_everything 'report' @mock_model = stub_everything 'faked model', :name => "report", :convert_from => @report Puppet::Indirector::Request.any_instance.stubs(:model).returns(@mock_model) Puppet::Network::HTTP::WEBrickREST.any_instance.stubs(:check_authorization) end after do Puppet::Network::HttpPool.expire Puppet::SSL::Host.ca_location = :none Puppet.settings.clear @server.unlisten Puppet::Transaction::Report.terminus_class = @old_terminus end it "should be able to send a report to the server" do @report.expects(:save) report = Puppet::Transaction::Report.new("apply") resourcemetrics = { - :total => 12, - :out_of_sync => 20, - :applied => 45, - :skipped => 1, - :restarted => 23, - :failed_restarts => 1, - :scheduled => 10 + "total" => 12, + "out_of_sync" => 20, + "applied" => 45, + "skipped" => 1, + "restarted" => 23, + "failed_restarts" => 1, + "scheduled" => 10 } report.add_metric(:resources, resourcemetrics) timemetrics = { - :resource1 => 10, - :resource2 => 50, - :resource3 => 40, - :resource4 => 20, + "resource1" => 10, + "resource2" => 50, + "resource3" => 40, + "resource4" => 20, } report.add_metric(:times, timemetrics) - report.add_metric( - :changes, - - :total => 20 - ) + report.add_metric(:changes, "total" => 20) report.save end end diff --git a/spec/unit/transaction/report_spec.rb b/spec/unit/transaction/report_spec.rb index 96d464b7a..766d4f14d 100755 --- a/spec/unit/transaction/report_spec.rb +++ b/spec/unit/transaction/report_spec.rb @@ -1,290 +1,290 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/transaction/report' describe Puppet::Transaction::Report do before do Puppet::Util::Storage.stubs(:store) end it "should set its host name to the certname" do Puppet.settings.expects(:value).with(:certname).returns "myhost" Puppet::Transaction::Report.new("apply").host.should == "myhost" end it "should return its host name as its name" do r = Puppet::Transaction::Report.new("apply") r.name.should == r.host end it "should create an initialization timestamp" do Time.expects(:now).returns "mytime" Puppet::Transaction::Report.new("apply").time.should == "mytime" end it "should take a 'kind' as an argument" do Puppet::Transaction::Report.new("inspect").kind.should == "inspect" end it "should take a 'configuration_version' as an argument" do Puppet::Transaction::Report.new("inspect", "some configuration version").configuration_version.should == "some configuration version" end it "should be able to set configuration_version" do report = Puppet::Transaction::Report.new("inspect") report.configuration_version = "some version" report.configuration_version.should == "some version" end describe "when accepting logs" do before do @report = Puppet::Transaction::Report.new("apply") end it "should add new logs to the log list" do @report << "log" @report.logs[-1].should == "log" end it "should return self" do r = @report << "log" r.should equal(@report) end end describe "when accepting resource statuses" do before do @report = Puppet::Transaction::Report.new("apply") end it "should add each status to its status list" do status = stub 'status', :resource => "foo" @report.add_resource_status status @report.resource_statuses["foo"].should equal(status) end end describe "when using the indirector" do it "should redirect :find to the indirection" do @indirection = stub 'indirection', :name => :report Puppet::Transaction::Report.stubs(:indirection).returns(@indirection) @indirection.expects(:find) Puppet::Transaction::Report.find(:report) end it "should redirect :save to the indirection" do Facter.stubs(:value).returns("eh") @indirection = stub 'indirection', :name => :report Puppet::Transaction::Report.stubs(:indirection).returns(@indirection) report = Puppet::Transaction::Report.new("apply") @indirection.expects(:save) report.save end it "should default to the 'processor' terminus" do Puppet::Transaction::Report.indirection.terminus_class.should == :processor end it "should delegate its name attribute to its host method" do report = Puppet::Transaction::Report.new("apply") report.expects(:host).returns "me" report.name.should == "me" end after do Puppet::Util::Cacher.expire end end describe "when computing exit status" do it "should produce 2 if changes are present" do report = Puppet::Transaction::Report.new("apply") - report.add_metric("changes", {:total => 1}) - report.add_metric("resources", {:failed => 0}) + report.add_metric("changes", {"total" => 1}) + report.add_metric("resources", {"failed" => 0}) report.exit_status.should == 2 end it "should produce 4 if failures are present" do report = Puppet::Transaction::Report.new("apply") - report.add_metric("changes", {:total => 0}) - report.add_metric("resources", {:failed => 1}) + report.add_metric("changes", {"total" => 0}) + report.add_metric("resources", {"failed" => 1}) report.exit_status.should == 4 end it "should produce 6 if both changes and failures are present" do report = Puppet::Transaction::Report.new("apply") - report.add_metric("changes", {:total => 1}) - report.add_metric("resources", {:failed => 1}) + report.add_metric("changes", {"total" => 1}) + report.add_metric("resources", {"failed" => 1}) report.exit_status.should == 6 end end describe "before finalizing the report" do it "should have a status of 'failed'" do report = Puppet::Transaction::Report.new("apply") report.status.should == 'failed' end end describe "when finalizing the report" do before do @report = Puppet::Transaction::Report.new("apply") end def metric(name, value) if metric = @report.metrics[name.to_s] metric[value] else nil end end def add_statuses(count, type = :file) count.times do |i| status = Puppet::Resource::Status.new(Puppet::Type.type(type).new(:title => "/my/path#{i}")) yield status if block_given? @report.add_resource_status status end end [:time, :resources, :changes, :events].each do |type| it "should add #{type} metrics" do @report.finalize_report @report.metrics[type.to_s].should be_instance_of(Puppet::Transaction::Metric) end end describe "for resources" do it "should provide the total number of resources" do add_statuses(3) @report.finalize_report - metric(:resources, :total).should == 3 + metric(:resources, "total").should == 3 end Puppet::Resource::Status::STATES.each do |state| it "should provide the number of #{state} resources as determined by the status objects" do add_statuses(3) { |status| status.send(state.to_s + "=", true) } @report.finalize_report - metric(:resources, state).should == 3 + metric(:resources, state.to_s).should == 3 end end it "should mark the report as 'failed' if there are failing resources" do add_statuses(1) { |status| status.failed = true } @report.finalize_report @report.status.should == 'failed' end end describe "for changes" do it "should provide the number of changes from the resource statuses and mark the report as 'changed'" do add_statuses(3) { |status| 3.times { status << Puppet::Transaction::Event.new(:status => 'success') } } @report.finalize_report - metric(:changes, :total).should == 9 + metric(:changes, "total").should == 9 @report.status.should == 'changed' end it "should provide a total even if there are no changes, and mark the report as 'unchanged'" do @report.finalize_report - metric(:changes, :total).should == 0 + metric(:changes, "total").should == 0 @report.status.should == 'unchanged' end end describe "for times" do it "should provide the total amount of time for each resource type" do add_statuses(3, :file) do |status| status.evaluation_time = 1 end add_statuses(3, :exec) do |status| status.evaluation_time = 2 end add_statuses(3, :mount) do |status| status.evaluation_time = 3 end @report.finalize_report metric(:time, "file").should == 3 metric(:time, "exec").should == 6 metric(:time, "mount").should == 9 end it "should add any provided times from external sources" do @report.add_times :foobar, 50 @report.finalize_report metric(:time, "foobar").should == 50 end it "should have a total time" do add_statuses(3, :file) do |status| status.evaluation_time = 1.25 end @report.add_times :config_retrieval, 0.5 @report.finalize_report metric(:time, "total").should == 4.25 end end describe "for events" do it "should provide the total number of events" do add_statuses(3) do |status| - 3.times { |i| status.add_event(Puppet::Transaction::Event.new) } + 3.times { |i| status.add_event(Puppet::Transaction::Event.new :status => 'success') } end @report.finalize_report - metric(:events, :total).should == 9 + metric(:events, "total").should == 9 end it "should provide the total even if there are no events" do @report.finalize_report - metric(:events, :total).should == 0 + metric(:events, "total").should == 0 end Puppet::Transaction::Event::EVENT_STATUSES.each do |status_name| it "should provide the number of #{status_name} events" do add_statuses(3) do |status| 3.times do |i| event = Puppet::Transaction::Event.new event.status = status_name status.add_event(event) end end @report.finalize_report metric(:events, status_name).should == 9 end end end end describe "when producing a summary" do before do resource = Puppet::Type.type(:notify).new(:name => "testing") catalog = Puppet::Resource::Catalog.new catalog.add_resource resource trans = catalog.apply @report = trans.report @report.finalize_report end %w{Changes Total Resources}.each do |main| it "should include information on #{main} in the summary" do @report.summary.should be_include(main) end end end describe "when outputting yaml" do it "should not include @external_times" do report = Puppet::Transaction::Report.new('apply') report.add_times('config_retrieval', 1.0) report.to_yaml_properties.should_not include('@external_times') end end end diff --git a/spec/unit/util/metric_spec.rb b/spec/unit/util/metric_spec.rb index 72571ee4a..600b88f85 100755 --- a/spec/unit/util/metric_spec.rb +++ b/spec/unit/util/metric_spec.rb @@ -1,95 +1,95 @@ #!/usr/bin/env ruby Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } require 'puppet/util/metric' describe Puppet::Util::Metric do before do @metric = Puppet::Util::Metric.new("foo") end it "should be aliased to Puppet::Metric" do Puppet::Util::Metric.should equal(Puppet::Metric) end [:type, :name, :value, :label, :basedir].each do |name| it "should have a #{name} attribute" do @metric.should respond_to(name) @metric.should respond_to(name.to_s + "=") end end it "should default to the :rrdir as the basedir "do Puppet.settings.expects(:value).with(:rrddir).returns "myrrd" @metric.basedir.should == "myrrd" end it "should use any provided basedir" do @metric.basedir = "foo" @metric.basedir.should == "foo" end it "should require a name at initialization" do lambda { Puppet::Util::Metric.new }.should raise_error(ArgumentError) end it "should always convert its name to a string" do Puppet::Util::Metric.new(:foo).name.should == "foo" end it "should support a label" do Puppet::Util::Metric.new("foo", "mylabel").label.should == "mylabel" end it "should autogenerate a label if none is provided" do Puppet::Util::Metric.new("foo_bar").label.should == "Foo bar" end it "should have a method for adding values" do @metric.should respond_to(:newvalue) end it "should have a method for returning values" do @metric.should respond_to(:values) end it "should require a name and value for its values" do lambda { @metric.newvalue }.should raise_error(ArgumentError) end it "should support a label for values" do - @metric.newvalue(:foo, 10, "label") + @metric.newvalue("foo", 10, "label") @metric.values[0][1].should == "label" end it "should autogenerate value labels if none is provided" do @metric.newvalue("foo_bar", 10) @metric.values[0][1].should == "Foo bar" end it "should return its values sorted by label" do - @metric.newvalue(:foo, 10, "b") - @metric.newvalue(:bar, 10, "a") + @metric.newvalue("foo", 10, "b") + @metric.newvalue("bar", 10, "a") - @metric.values.should == [[:bar, "a", 10], [:foo, "b", 10]] + @metric.values.should == [["bar", "a", 10], ["foo", "b", 10]] end it "should use an array indexer method to retrieve individual values" do - @metric.newvalue(:foo, 10) - @metric[:foo].should == 10 + @metric.newvalue("foo", 10) + @metric["foo"].should == 10 end it "should return nil if the named value cannot be found" do - @metric[:foo].should == 0 + @metric["foo"].should == 0 end # LAK: I'm not taking the time to develop these tests right now. # I expect they should actually be extracted into a separate class # anyway. it "should be able to graph metrics using RRDTool" it "should be able to create a new RRDTool database" it "should be able to store metrics into an RRDTool database" end