1
# The client for interacting with the puppetmaster config server.
4
require 'puppet/network/http_pool'
6
class Puppet::Network::Client::Master < Puppet::Network::Client
11
attr_accessor :catalog
12
attr_reader :compile_time
15
# Puppetd should only have one instance running, and we need a way
17
attr_accessor :instance
22
# Retrieve the facts from the central server.
27
down = Puppet[:downcasefacts]
32
if Facter.respond_to? :loadfacts
34
elsif Facter.respond_to? :load
37
Puppet.warning "You should upgrade your version of Facter to at least 1.3.8"
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.
43
facts = Facter.to_hash.inject({}) do |newhash, array|
46
newhash[name] = fact.to_s.downcase
48
newhash[name] = fact.to_s
53
# Add our client version to the list of facts, so people can use it
55
facts["clientversion"] = Puppet.version.to_s
57
# And add our environment as a fact.
58
unless facts.include?("environment")
59
facts["environment"] = Puppet[:environment]
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
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 }
75
Puppet.info "Caching catalog at %s" % self.cachefile
76
confdir = ::File.dirname(Puppet[:localconfig])
77
::File.open(self.cachefile + ".tmp", "w", 0660) { |f|
80
::File.rename(self.cachefile + ".tmp", self.cachefile)
84
unless defined? @cachefile
85
@cachefile = Puppet[:localconfig] + ".yaml"
91
@catalog.clear(true) if @catalog
96
# Initialize and load storage
99
Puppet::Util::Storage.load
100
@compile_time ||= Puppet::Util::Storage.cache(:configuration)[:compile_time]
103
puts detail.backtrace
105
Puppet.err "Corrupt state file %s: %s" % [Puppet[:statefile], detail]
107
::File.unlink(Puppet[:statefile])
110
raise Puppet::Error.new("Cannot remove %s: %s" %
111
[Puppet[:statefile], detail])
116
# Let the daemon run again, freely in the filesystem. Frolick, little
119
lockfile.unlock(:anonymous => true)
122
# Stop the daemon from making any catalog runs.
124
lockfile.lock(:anonymous => true)
127
# Retrieve the config from a remote server. If this fails, then
128
# use the cached copy.
132
# Retrieve the plugins.
133
getplugins() if Puppet[:pluginsync]
136
Puppet::Util.benchmark(:debug, "Retrieved facts") do
137
facts = self.class.facts
140
raise Puppet::Network::ClientError.new("Could not retrieve any facts") unless facts.length > 0
142
Puppet.debug("Retrieving catalog")
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)
152
case Puppet[:catalog_format]
153
when "marshal": objects = Marshal.load(marshalled_objects)
154
when "yaml": objects = YAML.load(marshalled_objects)
156
raise "Invalid catalog format '%s'" % Puppet[:catalog_format]
159
msg = "Configuration could not be translated from %s" % Puppet[:catalog_format]
160
msg += "; using cached catalog" if use_cached_config(true)
165
self.setclasses(objects.classes)
167
# Clear all existing objects, so we can recreate our stack.
168
clear() if self.catalog
170
# Now convert the objects to a puppet catalog graph.
172
@catalog = objects.to_catalog
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)
182
if ! @catalog.from_cache
183
self.cache(marshalled_objects)
186
# Keep the state database up to date.
187
@catalog.host_config = true
190
# A simple proxy method, so it's easy to test.
192
self.class.getplugins
195
# Just so we can specify that we are "the" instance.
196
def initialize(*args)
197
Puppet.settings.use(:main, :ssl, :puppetd)
200
self.class.instance = self
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.
208
# If we're currently running, then just mark for later
209
Puppet.notice "Received signal to restart; waiting until run is complete"
222
# Retrieve the cached config
224
if FileTest.exists?(self.cachefile)
225
return ::File.read(self.cachefile)
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 = {})
237
Puppet::Util.sync(:puppetrun).synchronize(Sync::EX) do
239
Puppet.notice "Lock file %s exists; skipping catalog run" %
244
duration = thinmark do
248
puts detail.backtrace if Puppet[:trace]
249
Puppet.err "Could not retrieve catalog: %s" % detail
253
@catalog.retrieval_duration = duration
254
Puppet.notice "Starting catalog run" unless @local
255
benchmark(:notice, "Finished catalog run") do
256
@catalog.apply(options)
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
267
# Did we get HUPped during the run? If so, then restart now that we're
270
Process.kill(:HUP, $$)
274
# Just make sure we remove the lock file if we set it.
275
lockfile.unlock if got_lock and lockfile.locked?
283
# Store the classes in the classfile, but only if we're not local.
288
unless ary and ary.length > 0
289
Puppet.info "No classes to store"
293
::File.open(Puppet[:classfile], "w") { |f|
294
f.puts ary.join("\n")
297
Puppet.err "Could not create class file %s: %s" %
298
[Puppet[:classfile], detail]
304
# Download files from the remote server, returning a list of all
306
def self.download(args)
308
:path => args[:dest],
310
:source => args[:source],
311
:tag => "#{args[:name]}s",
312
:owner => Process.uid,
313
:group => Process.gid,
321
hash[:ignore] = args[:ignore].split(/\s+/)
323
downconfig = Puppet::Node::Catalog.new("downloading")
324
downconfig.add_resource Puppet::Type.type(:file).create(hash)
326
Puppet.info "Retrieving #{args[:name]}s"
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]
338
rescue Puppet::Error, Timeout::Error => detail
340
puts detail.backtrace
342
Puppet.err "Could not retrieve %ss: %s" % [args[:name], detail]
345
# Now clean up after ourselves
351
# Retrieve facts from the central server.
353
# Download the new facts
354
path = Puppet[:factpath].split(":")
356
download(:dest => Puppet[:factdest], :source => Puppet[:factsource],
357
:ignore => Puppet[:factsignore], :name => "fact") do |resource|
359
next unless path.include?(::File.dirname(resource[:path]))
361
files << resource[:path]
365
# Retrieve the plugins from the central server. We only have to load the
366
# changed plugins, because Puppet::Type loads plugins on demand.
368
download(:dest => Puppet[:plugindest], :source => Puppet[:pluginsource],
369
:ignore => Puppet[:pluginsignore], :name => "plugin") do |resource|
371
next if FileTest.directory?(resource[:path])
372
path = resource[:path].sub(Puppet[:plugindest], '').sub(/^\/+/, '')
373
unless Puppet::Util::Autoload.loaded?(path)
378
Puppet.info "Reloading downloaded file %s" % path
381
Puppet.warning "Could not reload downloaded file %s: %s" %
382
[resource[:path], detail]
387
def self.loaddir(dir, type)
388
return unless FileTest.directory?(dir)
390
Dir.entries(dir).find_all { |e| e =~ /\.rb$/ }.each do |file|
391
fqfile = ::File.join(dir, file)
393
Puppet.info "Loading %s %s" %
394
[type, ::File.basename(file.sub(".rb",''))]
395
Timeout::timeout(self.timeout) do
399
Puppet.warning "Could not load %s %s: %s" % [type, fqfile, detail]
405
# LAK:NOTE See http://snurl.com/21zf8 [groups_google_com]
406
x = Puppet[:factpath].split(":").each do |dir|
412
timeout = Puppet[:configtimeout]
415
if timeout =~ /^\d+$/
416
timeout = Integer(timeout)
418
raise ArgumentError, "Configuration timeout must be an integer"
420
when Integer: # nothing
422
raise ArgumentError, "Configuration timeout must be an integer"
430
# Actually retrieve the catalog, either from the server or from a
432
def get_actual_config(facts)
434
Timeout::timeout(self.class.timeout) do
435
return get_remote_config(facts)
437
rescue Timeout::Error
438
Puppet.err "Configuration retrieval timed out"
443
# Retrieve a config from a remote master.
444
def get_remote_config(facts)
447
textfacts = CGI.escape(YAML.dump(facts))
449
benchmark(:debug, "Retrieved catalog") do
450
# error handling for this is done in the network client
452
textobjects = @driver.getconfig(textfacts, Puppet[:catalog_format])
454
textobjects = CGI.unescape(textobjects)
456
raise Puppet::Error, "Could not CGI.unescape catalog"
460
Puppet.err "Could not retrieve catalog: %s" % detail
465
return nil if textobjects == ""
467
@compile_time = Time.now
468
Puppet::Util::Storage.cache(:configuration)[:facts] = facts
469
Puppet::Util::Storage.cache(:configuration)[:compile_time] = @compile_time
475
unless defined?(@lockfile)
476
@lockfile = Puppet::Util::Pidlock.new(Puppet[:puppetdlockfile])
486
# Sleep when splay is enabled; else just return.
488
return unless Puppet[:splay]
491
time = rand(Integer(Puppet[:splaylimit]) + 1)
492
Puppet.info "Sleeping for %s seconds (splay is enabled)" % time
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
504
if because_of_failure and ! Puppet[:usecacheonfailure]
506
Puppet.warning "Not using cache on failed catalog"
510
return false unless oldtext = self.retrievecache
513
@catalog = YAML.load(oldtext).to_catalog
514
@catalog.from_cache = true
515
@catalog.host_config = true
517
puts detail.backtrace if Puppet[:trace]
518
Puppet.warning "Could not load cached catalog: %s" % detail