diff --git a/lib/puppet/reports/tagmail.rb b/lib/puppet/reports/tagmail.rb index 8df669b88..9982678cc 100644 --- a/lib/puppet/reports/tagmail.rb +++ b/lib/puppet/reports/tagmail.rb @@ -1,179 +1,179 @@ require 'puppet' require 'pp' require 'net/smtp' require 'time' Puppet::Reports.register_report(:tagmail) do desc "This report sends specific log messages to specific email addresses based on the tags in the log messages. See the [documentation on tags](http://projects.puppetlabs.com/projects/puppet/wiki/Using_Tags) for more information. To use this report, you must create a `tagmail.conf` file in the location specified by the `tagmap` setting. This is a simple file that maps tags to email addresses: Any log messages in the report that match the specified tags will be sent to the specified email addresses. Lines in the `tagmail.conf` file consist of a comma-separated list of tags, a colon, and a comma-separated list of email addresses. Tags can be !negated with a leading exclamation mark, which will subtract any messages with that tag from the set of events handled by that line. Puppet's log levels (`debug`, `info`, `notice`, `warning`, `err`, `alert`, `emerg`, `crit`, and `verbose`) can also be used as tags, and there is an `all` tag that will always match all log messages. An example `tagmail.conf`: all: me@domain.com webserver, !mailserver: httpadmins@domain.com This will send all messages to `me@domain.com`, and all messages from webservers that are not also from mailservers to `httpadmins@domain.com`. If you are using anti-spam controls such as grey-listing on your mail server, you should whitelist the sending email address (controlled by `reportform` configuration option) to ensure your email is not discarded as spam. " # Find all matching messages. def match(taglists) matching_logs = [] taglists.each do |emails, pos, neg| # First find all of the messages matched by our positive tags messages = nil if pos.include?("all") messages = self.logs else # Find all of the messages that are tagged with any of our # tags. messages = self.logs.find_all do |log| pos.detect { |tag| log.tagged?(tag) } end end # Now go through and remove any messages that match our negative tags messages = messages.reject do |log| true if neg.detect do |tag| log.tagged?(tag) end end if messages.empty? Puppet.info "No messages to report to #{emails.join(",")}" next else matching_logs << [emails, messages.collect { |m| m.to_report }.join("\n")] end end matching_logs end # Load the config file def parse(text) taglists = [] text.split("\n").each do |line| taglist = emails = nil case line.chomp when /^\s*#/; next when /^\s*$/; next when /^\s*(.+)\s*:\s*(.+)\s*$/ taglist = $1 emails = $2.sub(/#.*$/,'') else raise ArgumentError, "Invalid tagmail config file" end pos = [] neg = [] taglist.sub(/\s+$/,'').split(/\s*,\s*/).each do |tag| unless tag =~ /^!?[-\w\.]+$/ raise ArgumentError, "Invalid tag #{tag.inspect}" end case tag when /^\w+/; pos << tag when /^!\w+/; neg << tag.sub("!", '') else raise Puppet::Error, "Invalid tag '#{tag}'" end end # Now split the emails emails = emails.sub(/\s+$/,'').split(/\s*,\s*/) taglists << [emails, pos, neg] end taglists end # Process the report. This just calls the other associated messages. def process unless FileTest.exists?(Puppet[:tagmap]) Puppet.notice "Cannot send tagmail report; no tagmap file #{Puppet[:tagmap]}" return end metrics = raw_summary['resources'] || {} rescue {} if metrics['out_of_sync'] == 0 && metrics['changed'] == 0 Puppet.notice "Not sending tagmail report; no changes" return end taglists = parse(File.read(Puppet[:tagmap])) # Now find any appropriately tagged messages. reports = match(taglists) - send(reports) + send(reports) unless reports.empty? end # Send the email reports. def send(reports) pid = Puppet::Util.safe_posix_fork do if Puppet[:smtpserver] != "none" begin Net::SMTP.start(Puppet[:smtpserver]) do |smtp| reports.each do |emails, messages| smtp.open_message_stream(Puppet[:reportfrom], *emails) do |p| p.puts "From: #{Puppet[:reportfrom]}" p.puts "Subject: Puppet Report for #{self.host}" p.puts "To: " + emails.join(", ") p.puts "Date: #{Time.now.rfc2822}" p.puts p.puts messages end end end rescue => detail puts detail.backtrace if Puppet[:debug] raise Puppet::Error, "Could not send report emails through smtp: #{detail}" end elsif Puppet[:sendmail] != "" begin reports.each do |emails, messages| # We need to open a separate process for every set of email addresses IO.popen(Puppet[:sendmail] + " " + emails.join(" "), "w") do |p| p.puts "From: #{Puppet[:reportfrom]}" p.puts "Subject: Puppet Report for #{self.host}" p.puts "To: " + emails.join(", ") p.puts messages end end rescue => detail puts detail.backtrace if Puppet[:debug] raise Puppet::Error, "Could not send report emails via sendmail: #{detail}" end else raise Puppet::Error, "SMTP server is unset and could not find sendmail" end end # Don't bother waiting for the pid to return. Process.detach(pid) end end diff --git a/spec/unit/reports/tagmail_spec.rb b/spec/unit/reports/tagmail_spec.rb index 00f78c932..7fd209828 100755 --- a/spec/unit/reports/tagmail_spec.rb +++ b/spec/unit/reports/tagmail_spec.rb @@ -1,168 +1,218 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/reports' tagmail = Puppet::Reports.report(:tagmail) describe tagmail do before do @processor = Puppet::Transaction::Report.new("apply") @processor.extend(Puppet::Reports.report(:tagmail)) end passers = my_fixture "tagmail_passers.conf" File.readlines(passers).each do |line| it "should be able to parse '#{line.inspect}'" do @processor.parse(line) end end failers = my_fixture "tagmail_failers.conf" File.readlines(failers).each do |line| it "should not be able to parse '#{line.inspect}'" do lambda { @processor.parse(line) }.should raise_error(ArgumentError) end end { "tag: abuse@domain.com" => [%w{abuse@domain.com}, %w{tag}, []], "tag.localhost: abuse@domain.com" => [%w{abuse@domain.com}, %w{tag.localhost}, []], "tag, other: abuse@domain.com" => [%w{abuse@domain.com}, %w{tag other}, []], "tag-other: abuse@domain.com" => [%w{abuse@domain.com}, %w{tag-other}, []], "tag, !other: abuse@domain.com" => [%w{abuse@domain.com}, %w{tag}, %w{other}], "tag, !other, one, !two: abuse@domain.com" => [%w{abuse@domain.com}, %w{tag one}, %w{other two}], "tag: abuse@domain.com, other@domain.com" => [%w{abuse@domain.com other@domain.com}, %w{tag}, []] }.each do |line, results| it "should parse '#{line}' as #{results.inspect}" do @processor.parse(line).shift.should == results end end describe "when matching logs" do before do @processor << Puppet::Util::Log.new(:level => :notice, :message => "first", :tags => %w{one}) @processor << Puppet::Util::Log.new(:level => :notice, :message => "second", :tags => %w{one two}) @processor << Puppet::Util::Log.new(:level => :notice, :message => "third", :tags => %w{one two three}) end def match(pos = [], neg = []) pos = Array(pos) neg = Array(neg) result = @processor.match([[%w{abuse@domain.com}, pos, neg]]) actual_result = result.shift if actual_result actual_result[1] else nil end end it "should match all messages when provided the 'all' tag as a positive matcher" do results = match("all") %w{first second third}.each do |str| results.should be_include(str) end end it "should remove messages that match a negated tag" do match("all", "three").should_not be_include("third") end it "should find any messages tagged with a provided tag" do results = match("two") results.should be_include("second") results.should be_include("third") results.should_not be_include("first") end it "should allow negation of specific tags from a specific tag list" do results = match("two", "three") results.should be_include("second") results.should_not be_include("third") end it "should allow a tag to negate all matches" do results = match([], "one") results.should be_nil end end describe "the behavior of tagmail.process" do before do Puppet[:tagmap] = my_fixture "tagmail_email.conf" end let(:processor) do processor = Puppet::Transaction::Report.new("apply") processor.extend(Puppet::Reports.report(:tagmail)) processor end context "when any messages match a positive tag" do before do processor << log_entry end let(:log_entry) do Puppet::Util::Log.new( :level => :notice, :message => "Secure change", :tags => %w{secure}) end let(:message) do "#{log_entry.time} Puppet (notice): Secure change" end it "should send email if there are changes" do processor.expects(:send).with([[['user@domain.com'], message]]) processor.expects(:raw_summary).returns({ "resources" => { "changed" => 1, "out_of_sync" => 0 } }) processor.process end it "should send email if there are resources out of sync" do processor.expects(:send).with([[['user@domain.com'], message]]) processor.expects(:raw_summary).returns({ "resources" => { "changed" => 0, "out_of_sync" => 1 } }) processor.process end it "should not send email if no changes or resources out of sync" do processor.expects(:send).never processor.expects(:raw_summary).returns({ "resources" => { "changed" => 0, "out_of_sync" => 0 } }) processor.process end it "should log a message if no changes or resources out of sync" do processor.expects(:send).never processor.expects(:raw_summary).returns({ "resources" => { "changed" => 0, "out_of_sync" => 0 } }) Puppet.expects(:notice).with("Not sending tagmail report; no changes") processor.process end it "should send email if raw_summary is not defined" do processor.expects(:send).with([[['user@domain.com'], message]]) processor.expects(:raw_summary).returns(nil) processor.process end it "should send email if there are no resource metrics" do processor.expects(:send).with([[['user@domain.com'], message]]) processor.expects(:raw_summary).returns({'resources' => nil}) processor.process end end + + context "when no message match a positive tag" do + before do + processor << log_entry + end + + let(:log_entry) do + Puppet::Util::Log.new( + :level => :notice, + :message => 'Unnotices change', + :tags => %w{not_present_in_tagmail.conf} + ) + end + + it "should send no email if there are changes" do + processor.expects(:send).never + processor.expects(:raw_summary).returns({ + "resources" => { "changed" => 1, "out_of_sync" => 0 } + }) + processor.process + end + + it "should send no email if there are resources out of sync" do + processor.expects(:send).never + processor.expects(:raw_summary).returns({ + "resources" => { "changed" => 0, "out_of_sync" => 1 } + }) + processor.process + end + + it "should send no email if no changes or resources out of sync" do + processor.expects(:send).never + processor.expects(:raw_summary).returns({ + "resources" => { "changed" => 0, "out_of_sync" => 0 } + }) + processor.process + end + + it "should send no email if raw_summary is not defined" do + processor.expects(:send).never + processor.expects(:raw_summary).returns(nil) + processor.process + end + + it "should send no email if there are no resource metrics" do + processor.expects(:send).never + processor.expects(:raw_summary).returns({'resources' => nil}) + processor.process + end + end end end