~ubuntu-branches/ubuntu/lucid/puppet/lucid-security

« back to all changes in this revision

Viewing changes to lib/puppet/network/client/master.rb

  • Committer: Bazaar Package Importer
  • Author(s): Chuck Short
  • Date: 2009-12-23 00:48:10 UTC
  • mfrom: (1.1.10 upstream) (3.1.7 squeeze)
  • Revision ID: james.westby@ubuntu.com-20091223004810-3i4oryds922g5n59
Tags: 0.25.1-3ubuntu1
* Merge from debian testing.  Remaining changes:
  - debian/rules:
    + Don't start puppet when first installing puppet.
  - debian/puppet.conf, lib/puppet/defaults.rb:
    + Move templates to /etc/puppet
  - lib/puppet/defaults.rb:
    + Fix /var/lib/puppet/state ownership.
  - man/man8/puppet.conf.8: 
    + Fix broken URL in manpage.
  - debian/control:
    + Update maintainer accordint to spec.
    + Puppetmaster Recommends -> Suggests
    + Created puppet-testsuite as a seperate. Allow the users to run puppet's 
      testsuite.
  - tests/Rakefile: Fix rakefile so that the testsuite can acutally be ran.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# The client for interacting with the puppetmaster config server.
2
 
require 'sync'
3
 
require 'timeout'
4
 
require 'puppet/network/http_pool'
5
 
 
6
 
class Puppet::Network::Client::Master < Puppet::Network::Client
7
 
    unless defined? @@sync
8
 
        @@sync = Sync.new
9
 
    end
10
 
 
11
 
    attr_accessor :catalog
12
 
    attr_reader :compile_time
13
 
 
14
 
    class << self
15
 
        # Puppetd should only have one instance running, and we need a way
16
 
        # to retrieve it.
17
 
        attr_accessor :instance
18
 
        include Puppet::Util
19
 
    end
20
 
 
21
 
    def self.facts
22
 
        # Retrieve the facts from the central server.
23
 
        if Puppet[:factsync]
24
 
            self.getfacts()
25
 
        end
26
 
        
27
 
        down = Puppet[:downcasefacts]
28
 
 
29
 
        Facter.clear
30
 
 
31
 
        # Reload everything.
32
 
        if Facter.respond_to? :loadfacts
33
 
            Facter.loadfacts
34
 
        elsif Facter.respond_to? :load
35
 
            Facter.load
36
 
        else
37
 
            Puppet.warning "You should upgrade your version of Facter to at least 1.3.8"
38
 
        end
39
 
 
40
 
        # This loads all existing facts and any new ones.  We have to remove and
41
 
        # reload because there's no way to unload specific facts.
42
 
        loadfacts()
43
 
        facts = Facter.to_hash.inject({}) do |newhash, array|
44
 
            name, fact = array
45
 
            if down
46
 
                newhash[name] = fact.to_s.downcase
47
 
            else
48
 
                newhash[name] = fact.to_s
49
 
            end
50
 
            newhash
51
 
        end
52
 
 
53
 
        # Add our client version to the list of facts, so people can use it
54
 
        # in their manifests
55
 
        facts["clientversion"] = Puppet.version.to_s
56
 
 
57
 
        # And add our environment as a fact.
58
 
        unless facts.include?("environment")
59
 
            facts["environment"] = Puppet[:environment]
60
 
        end
61
 
 
62
 
        facts
63
 
    end
64
 
 
65
 
    # Return the list of dynamic facts as an array of symbols
66
 
    # NOTE:LAK(2008/04/10): This code is currently unused, since we now always
67
 
    # recompile.
68
 
    def self.dynamic_facts
69
 
        # LAK:NOTE See http://snurl.com/21zf8  [groups_google_com] 
70
 
        x = Puppet.settings[:dynamicfacts].split(/\s*,\s*/).collect { |fact| fact.downcase }
71
 
    end
72
 
 
73
 
    # Cache the config
74
 
    def cache(text)
75
 
        Puppet.info "Caching catalog at %s" % self.cachefile
76
 
        confdir = ::File.dirname(Puppet[:localconfig])
77
 
        ::File.open(self.cachefile + ".tmp", "w", 0660) { |f|
78
 
            f.print text
79
 
        }
80
 
        ::File.rename(self.cachefile + ".tmp", self.cachefile)
81
 
    end
82
 
 
83
 
    def cachefile
84
 
        unless defined? @cachefile
85
 
            @cachefile = Puppet[:localconfig] + ".yaml"
86
 
        end
87
 
        @cachefile
88
 
    end
89
 
 
90
 
    def clear
91
 
        @catalog.clear(true) if @catalog
92
 
        Puppet::Type.allclear
93
 
        @catalog = nil
94
 
    end
95
 
 
96
 
    # Initialize and load storage
97
 
    def dostorage
98
 
        begin
99
 
            Puppet::Util::Storage.load
100
 
            @compile_time ||= Puppet::Util::Storage.cache(:configuration)[:compile_time]
101
 
        rescue => detail
102
 
            if Puppet[:trace]
103
 
                puts detail.backtrace
104
 
            end
105
 
            Puppet.err "Corrupt state file %s: %s" % [Puppet[:statefile], detail]
106
 
            begin
107
 
                ::File.unlink(Puppet[:statefile])
108
 
                retry
109
 
            rescue => detail
110
 
                raise Puppet::Error.new("Cannot remove %s: %s" %
111
 
                    [Puppet[:statefile], detail])
112
 
            end
113
 
        end
114
 
    end
115
 
 
116
 
    # Let the daemon run again, freely in the filesystem.  Frolick, little
117
 
    # daemon!
118
 
    def enable
119
 
        lockfile.unlock(:anonymous => true)
120
 
    end
121
 
 
122
 
    # Stop the daemon from making any catalog runs.
123
 
    def disable
124
 
        lockfile.lock(:anonymous => true)
125
 
    end
126
 
 
127
 
    # Retrieve the config from a remote server.  If this fails, then
128
 
    # use the cached copy.
129
 
    def getconfig
130
 
        dostorage()
131
 
 
132
 
        # Retrieve the plugins.
133
 
        getplugins() if Puppet[:pluginsync]
134
 
 
135
 
        facts = nil
136
 
        Puppet::Util.benchmark(:debug, "Retrieved facts") do
137
 
            facts = self.class.facts
138
 
        end
139
 
 
140
 
        raise Puppet::Network::ClientError.new("Could not retrieve any facts") unless facts.length > 0
141
 
 
142
 
        Puppet.debug("Retrieving catalog")
143
 
 
144
 
        # If we can't retrieve the catalog, just return, which will either
145
 
        # fail, or use the in-memory catalog.
146
 
        unless marshalled_objects = get_actual_config(facts)
147
 
            use_cached_config(true)
148
 
            return
149
 
        end
150
 
 
151
 
        begin
152
 
            case Puppet[:catalog_format]
153
 
            when "marshal": objects = Marshal.load(marshalled_objects)
154
 
            when "yaml": objects = YAML.load(marshalled_objects)
155
 
            else
156
 
                raise "Invalid catalog format '%s'" % Puppet[:catalog_format]
157
 
            end
158
 
        rescue => detail
159
 
            msg = "Configuration could not be translated from %s" % Puppet[:catalog_format]
160
 
            msg += "; using cached catalog" if use_cached_config(true)
161
 
            Puppet.warning msg
162
 
            return
163
 
        end
164
 
 
165
 
        self.setclasses(objects.classes)
166
 
 
167
 
        # Clear all existing objects, so we can recreate our stack.
168
 
        clear() if self.catalog
169
 
 
170
 
        # Now convert the objects to a puppet catalog graph.
171
 
        begin
172
 
            @catalog = objects.to_catalog
173
 
        rescue => detail
174
 
            clear()
175
 
            puts detail.backtrace if Puppet[:trace]
176
 
            msg = "Configuration could not be instantiated: %s" % detail
177
 
            msg += "; using cached catalog" if use_cached_config(true)
178
 
            Puppet.warning msg
179
 
            return
180
 
        end
181
 
 
182
 
        if ! @catalog.from_cache
183
 
            self.cache(marshalled_objects)
184
 
        end
185
 
 
186
 
        # Keep the state database up to date.
187
 
        @catalog.host_config = true
188
 
    end
189
 
    
190
 
    # A simple proxy method, so it's easy to test.
191
 
    def getplugins
192
 
        self.class.getplugins
193
 
    end
194
 
    
195
 
    # Just so we can specify that we are "the" instance.
196
 
    def initialize(*args)
197
 
        Puppet.settings.use(:main, :ssl, :puppetd)
198
 
        super
199
 
 
200
 
        self.class.instance = self
201
 
        @running = false
202
 
        @splayed = false
203
 
    end
204
 
 
205
 
    # Mark that we should restart.  The Puppet module checks whether we're running,
206
 
    # so this only gets called if we're in the middle of a run.
207
 
    def restart
208
 
        # If we're currently running, then just mark for later
209
 
        Puppet.notice "Received signal to restart; waiting until run is complete"
210
 
        @restart = true
211
 
    end
212
 
 
213
 
    # Should we restart?
214
 
    def restart?
215
 
        if defined? @restart
216
 
            @restart
217
 
        else
218
 
            false
219
 
        end
220
 
    end
221
 
 
222
 
    # Retrieve the cached config
223
 
    def retrievecache
224
 
        if FileTest.exists?(self.cachefile)
225
 
            return ::File.read(self.cachefile)
226
 
        else
227
 
            return nil
228
 
        end
229
 
    end
230
 
 
231
 
    # The code that actually runs the catalog.  
232
 
    # This just passes any options on to the catalog,
233
 
    # which accepts :tags and :ignoreschedules.
234
 
    def run(options = {})
235
 
        got_lock = false
236
 
        splay
237
 
        Puppet::Util.sync(:puppetrun).synchronize(Sync::EX) do
238
 
            if !lockfile.lock
239
 
                Puppet.notice "Lock file %s exists; skipping catalog run" %
240
 
                    lockfile.lockfile
241
 
            else
242
 
                got_lock = true
243
 
                begin
244
 
                    duration = thinmark do
245
 
                        self.getconfig
246
 
                    end
247
 
                rescue => detail
248
 
                    puts detail.backtrace if Puppet[:trace]
249
 
                    Puppet.err "Could not retrieve catalog: %s" % detail
250
 
                end
251
 
 
252
 
                if self.catalog
253
 
                    @catalog.retrieval_duration = duration
254
 
                    Puppet.notice "Starting catalog run" unless @local
255
 
                    benchmark(:notice, "Finished catalog run") do
256
 
                        @catalog.apply(options)
257
 
                    end
258
 
                end
259
 
 
260
 
                # Now close all of our existing http connections, since there's no
261
 
                # reason to leave them lying open.
262
 
                Puppet::Network::HttpPool.clear_http_instances
263
 
            end
264
 
            
265
 
            lockfile.unlock
266
 
 
267
 
            # Did we get HUPped during the run?  If so, then restart now that we're
268
 
            # done with the run.
269
 
            if self.restart?
270
 
                Process.kill(:HUP, $$)
271
 
            end
272
 
        end
273
 
    ensure
274
 
        # Just make sure we remove the lock file if we set it.
275
 
        lockfile.unlock if got_lock and lockfile.locked?
276
 
        clear()
277
 
    end
278
 
 
279
 
    def running?
280
 
        lockfile.locked?
281
 
    end
282
 
 
283
 
    # Store the classes in the classfile, but only if we're not local.
284
 
    def setclasses(ary)
285
 
        if @local
286
 
            return
287
 
        end
288
 
        unless ary and ary.length > 0
289
 
            Puppet.info "No classes to store"
290
 
            return
291
 
        end
292
 
        begin
293
 
            ::File.open(Puppet[:classfile], "w") { |f|
294
 
                f.puts ary.join("\n")
295
 
            }
296
 
        rescue => detail
297
 
            Puppet.err "Could not create class file %s: %s" %
298
 
                [Puppet[:classfile], detail]
299
 
        end
300
 
    end
301
 
 
302
 
    private
303
 
 
304
 
    # Download files from the remote server, returning a list of all
305
 
    # changed files.
306
 
    def self.download(args)
307
 
        hash = {
308
 
            :path => args[:dest],
309
 
            :recurse => true,
310
 
            :source => args[:source],
311
 
            :tag => "#{args[:name]}s",
312
 
            :owner => Process.uid,
313
 
            :group => Process.gid,
314
 
            :purge => true,
315
 
            :force => true,
316
 
            :backup => false,
317
 
            :noop => false
318
 
        }
319
 
 
320
 
        if args[:ignore]
321
 
            hash[:ignore] = args[:ignore].split(/\s+/)
322
 
        end
323
 
        downconfig = Puppet::Node::Catalog.new("downloading")
324
 
        downconfig.add_resource Puppet::Type.type(:file).create(hash)
325
 
        
326
 
        Puppet.info "Retrieving #{args[:name]}s"
327
 
 
328
 
        files = []
329
 
        begin
330
 
            Timeout::timeout(self.timeout) do
331
 
                downconfig.apply do |trans|
332
 
                    trans.changed?.find_all do |resource|
333
 
                        yield resource if block_given?
334
 
                        files << resource[:path]
335
 
                    end
336
 
                end
337
 
            end
338
 
        rescue Puppet::Error, Timeout::Error => detail
339
 
            if Puppet[:debug]
340
 
                puts detail.backtrace
341
 
            end
342
 
            Puppet.err "Could not retrieve %ss: %s" % [args[:name], detail]
343
 
        end
344
 
 
345
 
        # Now clean up after ourselves
346
 
        downconfig.clear
347
 
 
348
 
        return files
349
 
    end
350
 
 
351
 
    # Retrieve facts from the central server.
352
 
    def self.getfacts
353
 
        # Download the new facts
354
 
        path = Puppet[:factpath].split(":")
355
 
        files = []
356
 
        download(:dest => Puppet[:factdest], :source => Puppet[:factsource],
357
 
            :ignore => Puppet[:factsignore], :name => "fact") do |resource|
358
 
 
359
 
            next unless path.include?(::File.dirname(resource[:path]))
360
 
 
361
 
            files << resource[:path]
362
 
        end
363
 
    end
364
 
 
365
 
    # Retrieve the plugins from the central server.  We only have to load the
366
 
    # changed plugins, because Puppet::Type loads plugins on demand.
367
 
    def self.getplugins
368
 
        download(:dest => Puppet[:plugindest], :source => Puppet[:pluginsource],
369
 
            :ignore => Puppet[:pluginsignore], :name => "plugin") do |resource|
370
 
 
371
 
            next if FileTest.directory?(resource[:path])
372
 
            path = resource[:path].sub(Puppet[:plugindest], '').sub(/^\/+/, '')
373
 
            unless Puppet::Util::Autoload.loaded?(path)
374
 
                next
375
 
            end
376
 
 
377
 
            begin
378
 
                Puppet.info "Reloading downloaded file %s" % path
379
 
                load resource[:path]
380
 
            rescue => detail
381
 
                Puppet.warning "Could not reload downloaded file %s: %s" %
382
 
                    [resource[:path], detail]
383
 
            end
384
 
        end
385
 
    end
386
 
 
387
 
    def self.loaddir(dir, type)
388
 
        return unless FileTest.directory?(dir)
389
 
 
390
 
        Dir.entries(dir).find_all { |e| e =~ /\.rb$/ }.each do |file|
391
 
            fqfile = ::File.join(dir, file)
392
 
            begin
393
 
                Puppet.info "Loading %s %s" % 
394
 
                    [type, ::File.basename(file.sub(".rb",''))]
395
 
                Timeout::timeout(self.timeout) do
396
 
                    load fqfile
397
 
                end
398
 
            rescue => detail
399
 
                Puppet.warning "Could not load %s %s: %s" % [type, fqfile, detail]
400
 
            end
401
 
        end
402
 
    end
403
 
 
404
 
    def self.loadfacts
405
 
        # LAK:NOTE See http://snurl.com/21zf8  [groups_google_com] 
406
 
        x = Puppet[:factpath].split(":").each do |dir|
407
 
            loaddir(dir, "fact")
408
 
        end
409
 
    end
410
 
    
411
 
    def self.timeout
412
 
        timeout = Puppet[:configtimeout]
413
 
        case timeout
414
 
        when String:
415
 
            if timeout =~ /^\d+$/
416
 
                timeout = Integer(timeout)
417
 
            else
418
 
                raise ArgumentError, "Configuration timeout must be an integer"
419
 
            end
420
 
        when Integer: # nothing
421
 
        else
422
 
            raise ArgumentError, "Configuration timeout must be an integer"
423
 
        end
424
 
 
425
 
        return timeout
426
 
    end
427
 
 
428
 
    loadfacts()
429
 
 
430
 
    # Actually retrieve the catalog, either from the server or from a
431
 
    # local master.
432
 
    def get_actual_config(facts)
433
 
        begin
434
 
            Timeout::timeout(self.class.timeout) do
435
 
                return get_remote_config(facts)
436
 
            end
437
 
        rescue Timeout::Error
438
 
            Puppet.err "Configuration retrieval timed out"
439
 
            return nil
440
 
        end
441
 
    end
442
 
    
443
 
    # Retrieve a config from a remote master.
444
 
    def get_remote_config(facts)
445
 
        textobjects = ""
446
 
 
447
 
        textfacts = CGI.escape(YAML.dump(facts))
448
 
 
449
 
        benchmark(:debug, "Retrieved catalog") do
450
 
            # error handling for this is done in the network client
451
 
            begin
452
 
                textobjects = @driver.getconfig(textfacts, Puppet[:catalog_format])
453
 
                begin
454
 
                    textobjects = CGI.unescape(textobjects)
455
 
                rescue => detail
456
 
                    raise Puppet::Error, "Could not CGI.unescape catalog"
457
 
                end
458
 
 
459
 
            rescue => detail
460
 
                Puppet.err "Could not retrieve catalog: %s" % detail
461
 
                return nil
462
 
            end
463
 
        end
464
 
 
465
 
        return nil if textobjects == ""
466
 
 
467
 
        @compile_time = Time.now
468
 
        Puppet::Util::Storage.cache(:configuration)[:facts] = facts
469
 
        Puppet::Util::Storage.cache(:configuration)[:compile_time] = @compile_time
470
 
 
471
 
        return textobjects
472
 
    end
473
 
 
474
 
    def lockfile
475
 
        unless defined?(@lockfile)
476
 
            @lockfile = Puppet::Util::Pidlock.new(Puppet[:puppetdlockfile])
477
 
        end
478
 
 
479
 
        @lockfile
480
 
    end
481
 
 
482
 
    def splayed?
483
 
        @splayed
484
 
    end
485
 
 
486
 
    # Sleep when splay is enabled; else just return.
487
 
    def splay
488
 
        return unless Puppet[:splay]
489
 
        return if splayed?
490
 
 
491
 
        time = rand(Integer(Puppet[:splaylimit]) + 1)
492
 
        Puppet.info "Sleeping for %s seconds (splay is enabled)" % time
493
 
        sleep(time)
494
 
        @splayed = true
495
 
    end
496
 
 
497
 
    private
498
 
 
499
 
    # Use our cached config, optionally specifying whether this is
500
 
    # necessary because of a failure.
501
 
    def use_cached_config(because_of_failure = false)
502
 
        return true if self.catalog
503
 
 
504
 
        if because_of_failure and ! Puppet[:usecacheonfailure]
505
 
            @catalog = nil
506
 
            Puppet.warning "Not using cache on failed catalog"
507
 
            return false
508
 
        end
509
 
 
510
 
        return false unless oldtext = self.retrievecache
511
 
 
512
 
        begin
513
 
            @catalog = YAML.load(oldtext).to_catalog
514
 
            @catalog.from_cache = true
515
 
            @catalog.host_config = true
516
 
        rescue => detail
517
 
            puts detail.backtrace if Puppet[:trace]
518
 
            Puppet.warning "Could not load cached catalog: %s" % detail
519
 
            clear
520
 
            return false
521
 
        end
522
 
        return true
523
 
    end
524
 
end