~ubuntu-branches/ubuntu/oneiric/puppet/oneiric-security

« back to all changes in this revision

Viewing changes to lib/puppet/util/log.rb

  • Committer: Bazaar Package Importer
  • Author(s): Micah Anderson
  • Date: 2008-07-26 15:43:45 UTC
  • mto: (3.1.1 lenny) (1.3.1 upstream)
  • mto: This revision was merged to the branch mainline in revision 16.
  • Revision ID: james.westby@ubuntu.com-20080726154345-1fmgo76b4l72ulvc
ImportĀ upstreamĀ versionĀ 0.24.5

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
require 'syslog'
 
2
 
 
3
# Pass feedback to the user.  Log levels are modeled after syslog's, and it is
 
4
# expected that that will be the most common log destination.  Supports
 
5
# multiple destinations, one of which is a remote server.
 
6
class Puppet::Util::Log
 
7
    include Puppet::Util
 
8
 
 
9
    @levels = [:debug,:info,:notice,:warning,:err,:alert,:emerg,:crit]
 
10
    @loglevel = 2
 
11
 
 
12
    @desttypes = {}
 
13
 
 
14
    # A type of log destination.
 
15
    class Destination
 
16
        class << self
 
17
            attr_accessor :name
 
18
        end
 
19
 
 
20
        def self.initvars
 
21
            @matches = []
 
22
        end
 
23
 
 
24
        # Mark the things we're supposed to match.
 
25
        def self.match(obj)
 
26
            @matches ||= []
 
27
            @matches << obj
 
28
        end
 
29
 
 
30
        # See whether we match a given thing.
 
31
        def self.match?(obj)
 
32
            # Convert single-word strings into symbols like :console and :syslog
 
33
            if obj.is_a? String and obj =~ /^\w+$/
 
34
                obj = obj.downcase.intern
 
35
            end
 
36
 
 
37
            @matches.each do |thing|
 
38
                # Search for direct matches or class matches
 
39
                return true if thing === obj or thing == obj.class.to_s
 
40
            end
 
41
            return false
 
42
        end
 
43
 
 
44
        def name
 
45
            if defined? @name
 
46
                return @name
 
47
            else
 
48
                return self.class.name
 
49
            end
 
50
        end
 
51
 
 
52
        # Set how to handle a message.
 
53
        def self.sethandler(&block)
 
54
            define_method(:handle, &block)
 
55
        end
 
56
 
 
57
        # Mark how to initialize our object.
 
58
        def self.setinit(&block)
 
59
            define_method(:initialize, &block)
 
60
        end
 
61
    end
 
62
 
 
63
    # Create a new destination type.
 
64
    def self.newdesttype(name, options = {}, &block)
 
65
        dest = genclass(name, :parent => Destination, :prefix => "Dest",
 
66
            :block => block,
 
67
            :hash => @desttypes,
 
68
            :attributes => options
 
69
        )
 
70
        dest.match(dest.name)
 
71
 
 
72
        return dest
 
73
    end
 
74
 
 
75
    @destinations = {}
 
76
 
 
77
    class << self
 
78
        include Puppet::Util
 
79
        include Puppet::Util::ClassGen
 
80
    end
 
81
 
 
82
    # Reset all logs to basics.  Basically just closes all files and undefs
 
83
    # all of the other objects.
 
84
    def Log.close(dest = nil)
 
85
        if dest
 
86
            if @destinations.include?(dest)
 
87
                if @destinations.respond_to?(:close)
 
88
                    @destinations[dest].close
 
89
                end
 
90
                @destinations.delete(dest)
 
91
            end
 
92
        else
 
93
            @destinations.each { |name, dest|
 
94
                if dest.respond_to?(:flush)
 
95
                    dest.flush
 
96
                end
 
97
                if dest.respond_to?(:close)
 
98
                    dest.close
 
99
                end
 
100
            }
 
101
            @destinations = {}
 
102
        end
 
103
    end
 
104
 
 
105
    # Flush any log destinations that support such operations.
 
106
    def Log.flush
 
107
        @destinations.each { |type, dest|
 
108
            if dest.respond_to?(:flush)
 
109
                dest.flush
 
110
            end
 
111
        }
 
112
    end
 
113
 
 
114
    # Create a new log message.  The primary role of this method is to
 
115
    # avoid creating log messages below the loglevel.
 
116
    def Log.create(hash)
 
117
        unless hash.include?(:level)
 
118
            raise Puppet::DevError, "Logs require a level"
 
119
        end
 
120
        unless @levels.index(hash[:level])
 
121
            raise Puppet::DevError, "Invalid log level %s" % hash[:level]
 
122
        end
 
123
        if @levels.index(hash[:level]) >= @loglevel 
 
124
            return Puppet::Util::Log.new(hash)
 
125
        else
 
126
            return nil
 
127
        end
 
128
    end
 
129
 
 
130
    def Log.destinations
 
131
        return @destinations.keys
 
132
    end
 
133
 
 
134
    # Yield each valid level in turn
 
135
    def Log.eachlevel
 
136
        @levels.each { |level| yield level }
 
137
    end
 
138
 
 
139
    # Return the current log level.
 
140
    def Log.level
 
141
        return @levels[@loglevel]
 
142
    end
 
143
 
 
144
    # Set the current log level.
 
145
    def Log.level=(level)
 
146
        unless level.is_a?(Symbol)
 
147
            level = level.intern
 
148
        end
 
149
 
 
150
        unless @levels.include?(level)
 
151
            raise Puppet::DevError, "Invalid loglevel %s" % level
 
152
        end
 
153
 
 
154
        @loglevel = @levels.index(level)
 
155
    end
 
156
 
 
157
    def Log.levels
 
158
        @levels.dup
 
159
    end
 
160
 
 
161
    newdesttype :syslog do
 
162
        def close
 
163
            Syslog.close
 
164
        end
 
165
 
 
166
        def initialize
 
167
            if Syslog.opened?
 
168
                Syslog.close
 
169
            end
 
170
            name = Puppet[:name]
 
171
            name = "puppet-#{name}" unless name =~ /puppet/
 
172
 
 
173
            options = Syslog::LOG_PID | Syslog::LOG_NDELAY
 
174
 
 
175
            # XXX This should really be configurable.
 
176
            str = Puppet[:syslogfacility]
 
177
            begin
 
178
                facility = Syslog.const_get("LOG_#{str.upcase}")
 
179
            rescue NameError
 
180
                raise Puppet::Error, "Invalid syslog facility %s" % str
 
181
            end
 
182
 
 
183
            @syslog = Syslog.open(name, options, facility)
 
184
        end
 
185
 
 
186
        def handle(msg)
 
187
            # XXX Syslog currently has a bug that makes it so you
 
188
            # cannot log a message with a '%' in it.  So, we get rid
 
189
            # of them.
 
190
            if msg.source == "Puppet"
 
191
                @syslog.send(msg.level, msg.to_s.gsub("%", '%%'))
 
192
            else
 
193
                @syslog.send(msg.level, "(%s) %s" %
 
194
                    [msg.source.to_s.gsub("%", ""),
 
195
                        msg.to_s.gsub("%", '%%')
 
196
                    ]
 
197
                )
 
198
            end
 
199
        end
 
200
    end
 
201
 
 
202
    newdesttype :file do
 
203
        match(/^\//)
 
204
 
 
205
        def close
 
206
            if defined? @file
 
207
                @file.close
 
208
                @file = nil
 
209
            end
 
210
        end
 
211
 
 
212
        def flush
 
213
            if defined? @file
 
214
                @file.flush
 
215
            end
 
216
        end
 
217
 
 
218
        def initialize(path)
 
219
            @name = path
 
220
            # first make sure the directory exists
 
221
            # We can't just use 'Config.use' here, because they've
 
222
            # specified a "special" destination.
 
223
            unless FileTest.exist?(File.dirname(path))
 
224
                Puppet.recmkdir(File.dirname(path))
 
225
                Puppet.info "Creating log directory %s" % File.dirname(path)
 
226
            end
 
227
 
 
228
            # create the log file, if it doesn't already exist
 
229
            file = File.open(path, File::WRONLY|File::CREAT|File::APPEND)
 
230
 
 
231
            @file = file
 
232
 
 
233
            @autoflush = Puppet[:autoflush]
 
234
        end
 
235
 
 
236
        def handle(msg)
 
237
            @file.puts("%s %s (%s): %s" %
 
238
                [msg.time, msg.source, msg.level, msg.to_s])
 
239
 
 
240
            @file.flush if @autoflush
 
241
        end
 
242
    end
 
243
 
 
244
    newdesttype :console do
 
245
                
 
246
        
 
247
        PINK = {:console => "", :html => "FFA0A0"}
 
248
        GREEN = {:console => "", :html => "00CD00"}
 
249
        YELLOW = {:console => "", :html => "FFFF60"}
 
250
        SLATE = {:console => "", :html => "80A0FF"}
 
251
        ORANGE = {:console => "", :html => "FFA500"}
 
252
        BLUE = {:console => "", :html => "40FFFF"}
 
253
        RESET = {:console => "", :html => ""}
 
254
 
 
255
        @@colormap = {
 
256
            :debug => SLATE,
 
257
            :info => GREEN,
 
258
            :notice => PINK,
 
259
            :warning => ORANGE,
 
260
            :err => YELLOW,
 
261
            :alert => BLUE,
 
262
            :emerg => RESET,
 
263
            :crit => RESET
 
264
        }
 
265
        
 
266
        def colorize(level, str)
 
267
            case Puppet[:color]
 
268
            when false: str
 
269
            when true, :ansi, "ansi": console_color(level, str)
 
270
            when :html, "html": html_color(level, str)
 
271
            end
 
272
        end
 
273
        
 
274
        def console_color(level, str)
 
275
            @@colormap[level][:console] + str + RESET[:console]
 
276
        end
 
277
        
 
278
        def html_color(level, str)
 
279
            %{<span style="color: %s">%s</span>} % [@@colormap[level][:html], str]
 
280
        end
 
281
 
 
282
        def initialize
 
283
            # Flush output immediately.
 
284
            $stdout.sync = true
 
285
        end
 
286
 
 
287
        def handle(msg)
 
288
            if msg.source == "Puppet"
 
289
                puts colorize(msg.level, "%s: %s" % [msg.level, msg.to_s])
 
290
            else
 
291
                puts colorize(msg.level, "%s: %s: %s" % [msg.level, msg.source, msg.to_s])
 
292
            end
 
293
        end
 
294
    end
 
295
 
 
296
    newdesttype :host do
 
297
        def initialize(host)
 
298
            Puppet.info "Treating %s as a hostname" % host
 
299
            args = {}
 
300
            if host =~ /:(\d+)/
 
301
                args[:Port] = $1
 
302
                args[:Server] = host.sub(/:\d+/, '')
 
303
            else
 
304
                args[:Server] = host
 
305
            end
 
306
 
 
307
            @name = host
 
308
 
 
309
            @driver = Puppet::Network::Client::LogClient.new(args)
 
310
        end
 
311
 
 
312
        def handle(msg)
 
313
            unless msg.is_a?(String) or msg.remote
 
314
                unless defined? @hostname
 
315
                    @hostname = Facter["hostname"].value
 
316
                end
 
317
                unless defined? @domain
 
318
                    @domain = Facter["domain"].value
 
319
                    if @domain
 
320
                        @hostname += "." + @domain
 
321
                    end
 
322
                end
 
323
                if msg.source =~ /^\//
 
324
                    msg.source = @hostname + ":" + msg.source
 
325
                elsif msg.source == "Puppet"
 
326
                    msg.source = @hostname + " " + msg.source
 
327
                else
 
328
                    msg.source = @hostname + " " + msg.source
 
329
                end
 
330
                begin
 
331
                    #puts "would have sent %s" % msg
 
332
                    #puts "would have sent %s" %
 
333
                    #    CGI.escape(YAML.dump(msg))
 
334
                    begin
 
335
                        tmp = CGI.escape(YAML.dump(msg))
 
336
                    rescue => detail
 
337
                        puts "Could not dump: %s" % detail.to_s
 
338
                        return
 
339
                    end
 
340
                    # Add the hostname to the source
 
341
                    @driver.addlog(tmp)
 
342
                rescue => detail
 
343
                    if Puppet[:trace]
 
344
                        puts detail.backtrace
 
345
                    end
 
346
                    Puppet.err detail
 
347
                    Puppet::Util::Log.close(self)
 
348
                end
 
349
            end
 
350
        end
 
351
    end
 
352
 
 
353
    # Log to a transaction report.
 
354
    newdesttype :report do
 
355
        match "Puppet::Transaction::Report"
 
356
 
 
357
        def initialize(report)
 
358
            @report = report
 
359
        end
 
360
 
 
361
        def handle(msg)
 
362
            # Only add messages from objects, since anything else is
 
363
            # probably unrelated to this run.
 
364
            if msg.objectsource?
 
365
                @report.newlog(msg)
 
366
            end
 
367
        end
 
368
    end
 
369
 
 
370
    # Log to an array, just for testing.
 
371
    newdesttype :array do
 
372
        match "Array"
 
373
 
 
374
        def initialize(array)
 
375
            @array = array
 
376
        end
 
377
 
 
378
        def handle(msg)
 
379
            @array << msg
 
380
        end
 
381
    end
 
382
 
 
383
    # Create a new log destination.
 
384
    def Log.newdestination(dest)
 
385
        # Each destination can only occur once.
 
386
        if @destinations.find { |name, obj| obj.name == dest }
 
387
            return
 
388
        end
 
389
 
 
390
        name, type = @desttypes.find do |name, klass|
 
391
            klass.match?(dest)
 
392
        end
 
393
 
 
394
        unless type
 
395
            raise Puppet::DevError, "Unknown destination type %s" % dest
 
396
        end
 
397
 
 
398
        begin
 
399
            if type.instance_method(:initialize).arity == 1
 
400
                @destinations[dest] = type.new(dest)
 
401
            else
 
402
                @destinations[dest] = type.new()
 
403
            end
 
404
        rescue => detail
 
405
            if Puppet[:debug]
 
406
                puts detail.backtrace
 
407
            end
 
408
 
 
409
            # If this was our only destination, then add the console back in.
 
410
            if @destinations.empty? and (dest != :console and dest != "console")
 
411
                newdestination(:console)
 
412
            end
 
413
        end
 
414
    end
 
415
 
 
416
    # Route the actual message. FIXME There are lots of things this method
 
417
    # should do, like caching, storing messages when there are not yet
 
418
    # destinations, a bit more.  It's worth noting that there's a potential
 
419
    # for a loop here, if the machine somehow gets the destination set as
 
420
    # itself.
 
421
    def Log.newmessage(msg)
 
422
        if @levels.index(msg.level) < @loglevel 
 
423
            return
 
424
        end
 
425
 
 
426
        @destinations.each do |name, dest|
 
427
            threadlock(dest) do
 
428
                dest.handle(msg)
 
429
            end
 
430
        end
 
431
    end
 
432
 
 
433
    def Log.sendlevel?(level)
 
434
        @levels.index(level) >= @loglevel 
 
435
    end
 
436
 
 
437
    # Reopen all of our logs.
 
438
    def Log.reopen
 
439
        Puppet.notice "Reopening log files"
 
440
        types = @destinations.keys
 
441
        @destinations.each { |type, dest|
 
442
            if dest.respond_to?(:close)
 
443
                dest.close
 
444
            end
 
445
        }
 
446
        @destinations.clear
 
447
        # We need to make sure we always end up with some kind of destination
 
448
        begin
 
449
            types.each { |type|
 
450
                Log.newdestination(type)
 
451
            }
 
452
        rescue => detail
 
453
            if @destinations.empty?
 
454
                Log.newdestination(:syslog)
 
455
                Puppet.err detail.to_s
 
456
            end
 
457
        end
 
458
    end
 
459
 
 
460
    # Is the passed level a valid log level?
 
461
    def self.validlevel?(level)
 
462
        @levels.include?(level)
 
463
    end
 
464
 
 
465
    attr_accessor :level, :message, :time, :tags, :remote
 
466
    attr_reader :source
 
467
 
 
468
    def initialize(args)
 
469
        unless args.include?(:level) && args.include?(:message)
 
470
            raise Puppet::DevError, "Puppet::Util::Log called incorrectly"
 
471
        end
 
472
 
 
473
        if args[:level].class == String
 
474
            @level = args[:level].intern
 
475
        elsif args[:level].class == Symbol
 
476
            @level = args[:level]
 
477
        else
 
478
            raise Puppet::DevError,
 
479
                "Level is not a string or symbol: #{args[:level].class}"
 
480
        end
 
481
 
 
482
        # Just return unless we're actually at a level we should send
 
483
        #return unless self.class.sendlevel?(@level)
 
484
 
 
485
        @message = args[:message].to_s
 
486
        @time = Time.now
 
487
        # this should include the host name, and probly lots of other
 
488
        # stuff, at some point
 
489
        unless self.class.validlevel?(level)
 
490
            raise Puppet::DevError, "Invalid message level #{level}"
 
491
        end
 
492
 
 
493
        if args.include?(:tags)
 
494
            @tags = args[:tags]
 
495
        end
 
496
 
 
497
        if args.include?(:source)
 
498
            self.source = args[:source]
 
499
        else
 
500
            @source = "Puppet"
 
501
        end
 
502
 
 
503
        Log.newmessage(self)
 
504
    end
 
505
 
 
506
    # Was the source of this log an object?
 
507
    def objectsource?
 
508
        if defined? @objectsource and @objectsource
 
509
            @objectsource
 
510
        else
 
511
            false
 
512
        end
 
513
    end
 
514
 
 
515
    # If they pass a source in to us, we make sure it is a string, and
 
516
    # we retrieve any tags we can.
 
517
    def source=(source)
 
518
        # We can't store the actual source, we just store the path.
 
519
        # We can't just check for whether it responds to :path, because
 
520
        # plenty of providers respond to that in their normal function.
 
521
        if (source.is_a?(Puppet::Type) or source.is_a?(Puppet::Parameter)) and source.respond_to?(:path)
 
522
            @objectsource = true
 
523
            @source = source.path
 
524
        else
 
525
            @objectsource = false
 
526
            @source = source.to_s
 
527
        end
 
528
        unless defined? @tags and @tags
 
529
            if source.respond_to?(:tags)
 
530
                @tags = source.tags
 
531
            end
 
532
        end
 
533
    end
 
534
 
 
535
    def tagged?(tag)
 
536
        @tags.detect { |t| t.to_s == tag.to_s }
 
537
    end
 
538
 
 
539
    def to_report
 
540
        "%s %s (%s): %s" % [self.time, self.source, self.level, self.to_s]
 
541
    end
 
542
 
 
543
    def to_s
 
544
        return @message
 
545
    end
 
546
end
 
547
Puppet::Log = Puppet::Util::Log
 
548