summaryrefslogtreecommitdiffstats
path: root/lib/puppet/network
diff options
context:
space:
mode:
authorluke <luke@980ebf18-57e1-0310-9a29-db15c13687c0>2007-02-08 01:39:39 +0000
committerluke <luke@980ebf18-57e1-0310-9a29-db15c13687c0>2007-02-08 01:39:39 +0000
commit7e07e3dc843798bdbc7a03428ca054adaff2fb72 (patch)
tree34d0f9f8c2ee11bdc281e6e4d18cad444253fe36 /lib/puppet/network
parent6d8068eddd0d29ec53f62557eb53f6ebb8e40591 (diff)
downloadpuppet-7e07e3dc843798bdbc7a03428ca054adaff2fb72.tar.gz
puppet-7e07e3dc843798bdbc7a03428ca054adaff2fb72.tar.xz
puppet-7e07e3dc843798bdbc7a03428ca054adaff2fb72.zip
Moving all of the client and server code into a single network/ directory. In other words, more code structure cleanup.
git-svn-id: https://reductivelabs.com/svn/puppet/trunk@2179 980ebf18-57e1-0310-9a29-db15c13687c0
Diffstat (limited to 'lib/puppet/network')
-rw-r--r--lib/puppet/network/client.rb190
-rw-r--r--lib/puppet/network/client/ca.rb23
-rw-r--r--lib/puppet/network/client/dipper.rb76
-rw-r--r--lib/puppet/network/client/file.rb20
-rw-r--r--lib/puppet/network/client/log.rb17
-rw-r--r--lib/puppet/network/client/master.rb654
-rw-r--r--lib/puppet/network/client/proxy.rb28
-rw-r--r--lib/puppet/network/client/reporter.rb34
-rw-r--r--lib/puppet/network/client/resource.rb65
-rw-r--r--lib/puppet/network/client/runner.rb17
-rw-r--r--lib/puppet/network/client/status.rb7
-rw-r--r--lib/puppet/network/networkclient.rb167
-rw-r--r--lib/puppet/network/server.rb207
-rw-r--r--lib/puppet/network/server/authconfig.rb173
-rwxr-xr-xlib/puppet/network/server/authstore.rb227
-rw-r--r--lib/puppet/network/server/ca.rb152
-rwxr-xr-xlib/puppet/network/server/filebucket.rb167
-rwxr-xr-xlib/puppet/network/server/fileserver.rb589
-rwxr-xr-xlib/puppet/network/server/logger.rb52
-rw-r--r--lib/puppet/network/server/master.rb212
-rwxr-xr-xlib/puppet/network/server/report.rb174
-rwxr-xr-xlib/puppet/network/server/resource.rb190
-rwxr-xr-xlib/puppet/network/server/rights.rb74
-rwxr-xr-xlib/puppet/network/server/runner.rb61
-rw-r--r--lib/puppet/network/server/servlet.rb277
25 files changed, 3853 insertions, 0 deletions
diff --git a/lib/puppet/network/client.rb b/lib/puppet/network/client.rb
new file mode 100644
index 000000000..604912025
--- /dev/null
+++ b/lib/puppet/network/client.rb
@@ -0,0 +1,190 @@
+# the available clients
+
+require 'puppet'
+require 'puppet/network/networkclient'
+
+# FIXME this still isn't a good design, because none of the handlers overlap
+# so i could just as easily include them all in the main module
+# but at least it's better organized for now
+class Puppet::Network::Client
+ include Puppet::Daemon
+ include Puppet::Util
+
+ # FIXME The cert stuff should only come up with networking, so it
+ # should be in the network client, not the normal client. But if i do
+ # that, it's hard to tell whether the certs have been initialized.
+ include Puppet::Daemon
+ attr_reader :secureinit
+ attr_accessor :schedule, :lastrun, :local, :stopping
+
+ class << self
+ attr_reader :drivername, :handler
+ attr_accessor :netclient
+ end
+
+ def initcerts
+ unless self.readcert
+ #if self.is_a? Puppet::Network::Client::CA
+ unless self.requestcert
+ return nil
+ end
+ #else
+ # return nil
+ #end
+ #unless self.requestcert
+ #end
+ end
+
+ # unless we have a driver, we're a local client and we can't add
+ # certs anyway, so it doesn't matter
+ unless @driver
+ return true
+ end
+
+ self.setcerts
+ end
+
+ def initialize(hash)
+ # to whom do we connect?
+ @server = nil
+ @nil = nil
+ @secureinit = hash[:NoSecureInit] || true
+
+ if hash.include?(:FQDN)
+ @fqdn = hash[:FQDN]
+ else
+ self.fqdn
+ end
+
+ if hash.include?(:Cache)
+ @cache = hash[:Cache]
+ else
+ @cache = true
+ end
+
+ driverparam = self.class.drivername
+ if hash.include?(:Server)
+ if $noclientnetworking
+ raise NetworkClientError.new("Networking not available: %s" %
+ $nonetworking)
+ end
+
+ args = {:Server => hash[:Server]}
+ args[:Port] = hash[:Port] || Puppet[:masterport]
+
+ if self.readcert
+ args[:Certificate] = @cert
+ args[:Key] = @key
+ args[:CAFile] = @cacertfile
+ end
+
+ netclient = nil
+ unless netclient = self.class.netclient
+ unless handler = self.class.handler
+ raise Puppet::DevError,
+ "Class %s has no handler defined" % self.class
+ end
+ namespace = self.class.handler.interface.prefix
+ netclient = Puppet::Network::NetworkClient.netclient(namespace)
+ self.class.netclient = netclient
+ end
+ @driver = netclient.new(args)
+ @local = false
+ elsif hash.include?(driverparam)
+ @driver = hash[driverparam]
+ @local = true
+ else
+ raise ClientError, "%s must be passed a Server or %s" %
+ [self.class, driverparam]
+ end
+ end
+
+ # Are we a local client?
+ def local?
+ if defined? @local and @local
+ true
+ else
+ false
+ end
+ end
+
+ # A wrapper method to run and then store the last run time
+ def runnow
+ if self.stopping
+ Puppet.notice "In shutdown progress; skipping run"
+ return
+ end
+ begin
+ self.run
+ self.lastrun = Time.now.to_i
+ rescue => detail
+ if Puppet[:trace]
+ puts detail.backtrace
+ end
+ Puppet.err "Could not run %s: %s" % [self.class, detail]
+ end
+ end
+
+ def run
+ raise Puppet::DevError, "Client type %s did not override run" %
+ self.class
+ end
+
+ def scheduled?
+ if sched = self.schedule
+ return sched.match?(self.lastrun)
+ else
+ return true
+ end
+ end
+
+ def setcerts
+ @driver.cert = @cert
+ @driver.key = @key
+ @driver.ca_file = @cacertfile
+ end
+
+ def shutdown
+ if self.stopping
+ Puppet.notice "Already in shutdown"
+ else
+ self.stopping = true
+ if self.respond_to? :running? and self.running?
+ Puppet::Util::Storage.store
+ end
+ rmpidfile()
+ end
+ end
+
+ # Start listening for events. We're pretty much just listening for
+ # timer events here.
+ def start
+ setpidfile()
+ # Create our timer. Puppet will handle observing it and such.
+ timer = Puppet.newtimer(
+ :interval => Puppet[:runinterval],
+ :tolerance => 1,
+ :start? => true
+ ) do
+ if self.scheduled?
+ self.runnow
+ end
+ end
+
+ # Run once before we start following the timer
+ self.runnow
+ end
+
+ require 'puppet/network/client/proxy'
+ require 'puppet/network/client/ca'
+ require 'puppet/network/client/dipper'
+ require 'puppet/network/client/file'
+ require 'puppet/network/client/log'
+ require 'puppet/network/client/master'
+ require 'puppet/network/client/runner'
+ require 'puppet/network/client/status'
+ require 'puppet/network/client/reporter'
+ require 'puppet/network/client/resource'
+end
+
+# $Id$
diff --git a/lib/puppet/network/client/ca.rb b/lib/puppet/network/client/ca.rb
new file mode 100644
index 000000000..9a99c1145
--- /dev/null
+++ b/lib/puppet/network/client/ca.rb
@@ -0,0 +1,23 @@
+require 'puppet/network/client/proxy'
+
+class Puppet::Network::Client::CA < Puppet::Network::Client::ProxyClient
+ @drivername = :CA
+
+ # set up the appropriate interface methods
+ @handler = Puppet::Network::Server::CA
+ self.mkmethods
+
+ def initialize(hash = {})
+ if hash.include?(:CA)
+ if hash[:CA].is_a? Hash
+ hash[:CA] = Puppet::Network::Server::CA.new(hash[:CA])
+ else
+ hash[:CA] = Puppet::Network::Server::CA.new()
+ end
+ end
+
+ super(hash)
+ end
+end
+
+# $Id$
diff --git a/lib/puppet/network/client/dipper.rb b/lib/puppet/network/client/dipper.rb
new file mode 100644
index 000000000..8eaffc1a0
--- /dev/null
+++ b/lib/puppet/network/client/dipper.rb
@@ -0,0 +1,76 @@
+# The client class for filebuckets.
+class Puppet::Network::Client::Dipper < Puppet::Network::Client
+ @drivername = :Bucket
+
+ @handler = Puppet::Network::Server::FileBucket
+
+ attr_accessor :name
+
+ # Create our bucket client
+ def initialize(hash = {})
+ if hash.include?(:Path)
+ bucket = Puppet::Network::Server::FileBucket.new(
+ :Path => hash[:Path]
+ )
+ hash.delete(:Path)
+ hash[:Bucket] = bucket
+ end
+
+ super(hash)
+ end
+
+ # Back up a file to our bucket
+ def backup(file)
+ unless FileTest.exists?(file)
+ raise(BucketError, "File %s does not exist" % file)
+ end
+ contents = File.read(file)
+ unless local?
+ contents = Base64.encode64(contents)
+ end
+ return @driver.addfile(contents,file)
+ end
+
+ # Restore the file
+ def restore(file,sum)
+ restore = true
+ if FileTest.exists?(file)
+ cursum = Digest::MD5.hexdigest(File.read(file))
+
+ # if the checksum has changed...
+ # this might be extra effort
+ if cursum == sum
+ restore = false
+ end
+ end
+
+ if restore
+ if newcontents = @driver.getfile(sum)
+ unless local?
+ newcontents = Base64.decode64(newcontents)
+ end
+ tmp = ""
+ newsum = Digest::MD5.hexdigest(newcontents)
+ changed = nil
+ unless FileTest.writable?(file)
+ changed = File.stat(file).mode
+ File.chmod(changed | 0200, file)
+ end
+ File.open(file,File::WRONLY|File::TRUNC) { |of|
+ of.print(newcontents)
+ }
+ if changed
+ File.chmod(changed, file)
+ end
+ else
+ Puppet.err "Could not find file with checksum %s" % sum
+ return nil
+ end
+ return newsum
+ else
+ return nil
+ end
+ end
+end
+
+# $Id$
diff --git a/lib/puppet/network/client/file.rb b/lib/puppet/network/client/file.rb
new file mode 100644
index 000000000..7596aec1f
--- /dev/null
+++ b/lib/puppet/network/client/file.rb
@@ -0,0 +1,20 @@
+class Puppet::Network::Client::FileClient < Puppet::Network::Client::ProxyClient
+ @drivername = :FileServer
+
+ # set up the appropriate interface methods
+ @handler = Puppet::Network::Server::FileServer
+
+ self.mkmethods
+
+ def initialize(hash = {})
+ if hash.include?(:FileServer)
+ unless hash[:FileServer].is_a?(Puppet::Network::Server::FileServer)
+ raise Puppet::DevError, "Must pass an actual FS object"
+ end
+ end
+
+ super(hash)
+ end
+end
+
+# $Id$
diff --git a/lib/puppet/network/client/log.rb b/lib/puppet/network/client/log.rb
new file mode 100644
index 000000000..eddb8e0ca
--- /dev/null
+++ b/lib/puppet/network/client/log.rb
@@ -0,0 +1,17 @@
+class Puppet::Network::Client::LogClient < Puppet::Network::Client::ProxyClient
+ @drivername = :Logger
+
+ # set up the appropriate interface methods
+ @handler = Puppet::Network::Server::Logger
+ self.mkmethods
+
+ def initialize(hash = {})
+ if hash.include?(:Logger)
+ hash[:Logger] = Puppet::Network::Server::Logger.new()
+ end
+
+ super(hash)
+ end
+end
+
+# $Id$
diff --git a/lib/puppet/network/client/master.rb b/lib/puppet/network/client/master.rb
new file mode 100644
index 000000000..9f07f48ef
--- /dev/null
+++ b/lib/puppet/network/client/master.rb
@@ -0,0 +1,654 @@
+# The client for interacting with the puppetmaster config server.
+require 'sync'
+require 'timeout'
+
+class Puppet::Network::Client::MasterClient < Puppet::Network::Client
+ unless defined? @@sync
+ @@sync = Sync.new
+ end
+
+ @handler = Puppet::Network::Server::Master
+
+ Puppet.setdefaults("puppetd",
+ :puppetdlockfile => [ "$statedir/puppetdlock",
+ "A lock file to temporarily stop puppetd from doing anything."],
+ :usecacheonfailure => [true,
+ "Whether to use the cached configuration when the remote
+ configuration will not compile. This option is useful for testing
+ new configurations, where you want to fix the broken configuration
+ rather than reverting to a known-good one."
+ ],
+ :downcasefacts => [false,
+ "Whether facts should be made all lowercase when sent to the server."]
+ )
+
+ Puppet.setdefaults(:puppetd,
+ :configtimeout => [30,
+ "How long the client should wait for the configuration to be retrieved
+ before considering it a failure. This can help reduce flapping if too
+ many clients contact the server at one time."
+ ],
+ :reportserver => ["$server",
+ "The server to which to send transaction reports."
+ ],
+ :report => [false,
+ "Whether to send reports after every transaction."
+ ]
+ )
+
+ # Plugin information.
+ Puppet.setdefaults("puppet",
+ :pluginpath => ["$vardir/plugins",
+ "Where Puppet should look for plugins. Multiple directories should
+ be colon-separated, like normal PATH variables."],
+ :plugindest => ["$vardir/plugins",
+ "Where Puppet should store plugins that it pulls down from the central
+ server."],
+ :pluginsource => ["puppet://$server/plugins",
+ "From where to retrieve plugins. The standard Puppet ``file`` type
+ is used for retrieval, so anything that is a valid file source can
+ be used here."],
+ :pluginsync => [false,
+ "Whether plugins should be synced with the central server."],
+ :pluginsignore => [".svn CVS",
+ "What files to ignore when pulling down plugins."]
+ )
+
+ # Central fact information.
+ Puppet.setdefaults("puppet",
+ :factpath => ["$vardir/facts",
+ "Where Puppet should look for facts. Multiple directories should
+ be colon-separated, like normal PATH variables."],
+ :factdest => ["$vardir/facts",
+ "Where Puppet should store facts that it pulls down from the central
+ server."],
+ :factsource => ["puppet://$server/facts",
+ "From where to retrieve facts. The standard Puppet ``file`` type
+ is used for retrieval, so anything that is a valid file source can
+ be used here."],
+ :factsync => [false,
+ "Whether facts should be synced with the central server."],
+ :factsignore => [".svn CVS",
+ "What files to ignore when pulling down facts."]
+ )
+
+ @drivername = :Master
+
+ attr_accessor :objects
+ attr_reader :compile_time
+
+ class << self
+ # Puppetd should only have one instance running, and we need a way
+ # to retrieve it.
+ attr_accessor :instance
+ include Puppet::Util
+ end
+
+ def self.facts
+ # Retrieve the facts from the central server.
+ if Puppet[:factsync]
+ self.getfacts()
+ end
+
+ down = Puppet[:downcasefacts]
+
+ facts = {}
+ Facter.each { |name,fact|
+ if down
+ facts[name] = fact.to_s.downcase
+ else
+ facts[name] = fact.to_s
+ end
+ }
+
+ # Add our client version to the list of facts, so people can use it
+ # in their manifests
+ facts["clientversion"] = Puppet.version.to_s
+
+ facts
+ end
+
+ # This method actually applies the configuration.
+ def apply(tags = nil, ignoreschedules = false)
+ unless defined? @objects
+ raise Puppet::Error, "Cannot apply; objects not defined"
+ end
+
+ transaction = @objects.evaluate
+
+ if tags
+ transaction.tags = tags
+ end
+
+ if ignoreschedules
+ transaction.ignoreschedules = true
+ end
+
+ transaction.addtimes :config_retrieval => @configtime
+
+ begin
+ transaction.evaluate
+ rescue Puppet::Error => detail
+ Puppet.err "Could not apply complete configuration: %s" %
+ detail
+ rescue => detail
+ Puppet.err "Found a bug: %s" % detail
+ if Puppet[:trace]
+ puts detail.backtrace
+ end
+ ensure
+ Puppet::Util::Storage.store
+ end
+
+ if Puppet[:report]
+ report(transaction)
+ end
+
+ return transaction
+ ensure
+ if defined? transaction and transaction
+ transaction.cleanup
+ end
+ end
+
+ # Cache the config
+ def cache(text)
+ Puppet.info "Caching configuration at %s" % self.cachefile
+ confdir = File.dirname(Puppet[:localconfig])
+ File.open(self.cachefile + ".tmp", "w", 0660) { |f|
+ f.print text
+ }
+ File.rename(self.cachefile + ".tmp", self.cachefile)
+ end
+
+ def cachefile
+ unless defined? @cachefile
+ @cachefile = Puppet[:localconfig] + ".yaml"
+ end
+ @cachefile
+ end
+
+ def clear
+ #@objects = nil
+ @objects.remove(true)
+ Puppet::Type.allclear
+ end
+
+ # Initialize and load storage
+ def dostorage
+ begin
+ Puppet::Util::Storage.load
+ @compile_time ||= Puppet::Util::Storage.cache(:configuration)[:compile_time]
+ rescue => detail
+ if Puppet[:trace]
+ puts detail.backtrace
+ end
+ Puppet.err "Corrupt state file %s: %s" % [Puppet[:statefile], detail]
+ begin
+ File.unlink(Puppet[:statefile])
+ retry
+ rescue => detail
+ raise Puppet::Error.new("Cannot remove %s: %s" %
+ [Puppet[:statefile], detail])
+ end
+ end
+ end
+
+ # Check whether our configuration is up to date
+ def fresh?
+ unless self.compile_time
+ return false
+ end
+
+ # We're willing to give a 2 second drift
+ if @driver.freshness - @compile_time.to_i < 1
+ return true
+ else
+ return false
+ end
+ end
+
+ # Let the daemon run again, freely in the filesystem. Frolick, little
+ # daemon!
+ def enable
+ Puppet::Util::Pidlock.new(Puppet[:puppetdlockfile]).unlock(:anonymous => true)
+ end
+
+ # Stop the daemon from making any configuration runs.
+ def disable
+ Puppet::Util::Pidlock.new(Puppet[:puppetdlockfile]).lock(:anonymous => true)
+ end
+
+ # Retrieve the config from a remote server. If this fails, then
+ # use the cached copy.
+ def getconfig
+ dostorage()
+ if self.fresh?
+ Puppet.info "Config is up to date"
+ unless defined? @objects
+ begin
+ @objects = YAML.load(self.retrievecache).to_type
+ rescue => detail
+ Puppet.warning "Could not load cached configuration: %s" % detail
+ end
+ end
+ return
+ end
+ Puppet.debug("getting config")
+
+ # Retrieve the plugins.
+ if Puppet[:pluginsync]
+ getplugins()
+ end
+
+ facts = self.class.facts
+
+ unless facts.length > 0
+ raise Puppet::Network::ClientError.new(
+ "Could not retrieve any facts"
+ )
+ end
+
+ unless objects = get_actual_config(facts)
+ @objects = nil
+ return
+ end
+
+ unless objects.is_a?(Puppet::TransBucket)
+ raise NetworkClientError,
+ "Invalid returned objects of type %s" % objects.class
+ end
+
+ self.setclasses(objects.classes)
+
+ # Clear all existing objects, so we can recreate our stack.
+ if defined? @objects
+ Puppet::Type.allclear
+
+ # Make sure all of the objects are really gone.
+ @objects.remove(true)
+ end
+ @objects = nil
+
+ # Now convert the objects to real Puppet objects
+ @objects = objects.to_type
+
+ if @objects.nil?
+ raise Puppet::Error, "Configuration could not be processed"
+ end
+
+ # and perform any necessary final actions before we evaluate.
+ @objects.finalize
+
+ return @objects
+ end
+
+ # A simple proxy method, so it's easy to test.
+ def getplugins
+ self.class.getplugins
+ end
+
+ # Just so we can specify that we are "the" instance.
+ def initialize(*args)
+ Puppet.config.use(:puppet, :sslcertificates, :puppetd)
+ super
+
+ # This might be nil
+ @configtime = 0
+
+ self.class.instance = self
+ @running = false
+
+ mkdefault_objects
+ end
+
+ # Make the default objects necessary for function.
+ def mkdefault_objects
+ # First create the default scheduling objects
+ Puppet::Type.type(:schedule).mkdefaultschedules
+
+ # And filebuckets
+ Puppet::Type.type(:filebucket).mkdefaultbucket
+ end
+
+ # Mark that we should restart. The Puppet module checks whether we're running,
+ # so this only gets called if we're in the middle of a run.
+ def restart
+ # If we're currently running, then just mark for later
+ Puppet.notice "Received signal to restart; waiting until run is complete"
+ @restart = true
+ end
+
+ # Should we restart?
+ def restart?
+ if defined? @restart
+ @restart
+ else
+ false
+ end
+ end
+
+ # Retrieve the cached config
+ def retrievecache
+ if FileTest.exists?(self.cachefile)
+ return File.read(self.cachefile)
+ else
+ return ""
+ end
+ end
+
+ # The code that actually runs the configuration.
+ def run(tags = nil, ignoreschedules = false)
+ lockfile = Puppet::Util::Pidlock.new(Puppet[:puppetdlockfile])
+
+ Puppet::Util.sync(:puppetrun).synchronize(Sync::EX) do
+ if !lockfile.lock
+ Puppet.notice "Lock file %s exists; skipping configuration run" %
+ lockfile.lockfile
+ else
+ @running = true
+ @configtime = thinmark do
+ self.getconfig
+ end
+
+ if defined? @objects and @objects
+ unless @local
+ Puppet.notice "Starting configuration run"
+ end
+ benchmark(:notice, "Finished configuration run") do
+ self.apply(tags, ignoreschedules)
+ end
+ end
+ @running = false
+ end
+
+ lockfile.unlock
+
+ # Did we get HUPped during the run? If so, then restart now that we're
+ # done with the run.
+ if self.restart?
+ Process.kill(:HUP, $$)
+ end
+ end
+ end
+
+ def running?
+ @running
+ end
+
+ # Store the classes in the classfile, but only if we're not local.
+ def setclasses(ary)
+ if @local
+ return
+ end
+ unless ary and ary.length > 0
+ Puppet.info "No classes to store"
+ return
+ end
+ begin
+ File.open(Puppet[:classfile], "w") { |f|
+ f.puts ary.join("\n")
+ }
+ rescue => detail
+ Puppet.err "Could not create class file %s: %s" %
+ [Puppet[:classfile], detail]
+ end
+ end
+
+ private
+
+ # Download files from the remote server, returning a list of all
+ # changed files.
+ def self.download(args)
+ objects = Puppet::Type.type(:component).create(
+ :name => "#{args[:name]}_collector"
+ )
+ hash = {
+ :path => args[:dest],
+ :recurse => true,
+ :source => args[:source],
+ :tag => "#{args[:name]}s",
+ :owner => Process.uid,
+ :group => Process.gid,
+ :backup => false
+ }
+
+ if args[:ignore]
+ hash[:ignore] = args[:ignore].split(/\s+/)
+ end
+ objects.push Puppet::Type.type(:file).create(hash)
+
+ Puppet.info "Retrieving #{args[:name]}s"
+
+ begin
+ trans = objects.evaluate
+ trans.ignoretags = true
+ Timeout::timeout(self.timeout) do
+ trans.evaluate
+ end
+ rescue Puppet::Error, Timeout::Error => detail
+ if Puppet[:debug]
+ puts detail.backtrace
+ end
+ Puppet.err "Could not retrieve #{args[:name]}s: %s" % detail
+ end
+
+ # Now source all of the changed objects, but only source those
+ # that are top-level.
+ files = []
+ trans.changed?.find_all do |object|
+ yield object if block_given?
+ files << object[:path]
+ end
+ trans.cleanup
+
+ # Now clean up after ourselves
+ objects.remove
+ files
+ end
+
+ # Retrieve facts from the central server.
+ def self.getfacts
+ # Clear all existing definitions.
+ Facter.clear
+
+ # Download the new facts
+ path = Puppet[:factpath].split(":")
+ files = []
+ download(:dest => Puppet[:factdest], :source => Puppet[:factsource],
+ :ignore => Puppet[:factsignore], :name => "fact") do |object|
+
+ next unless path.include?(File.dirname(object[:path]))
+
+ files << object[:path]
+
+ end
+ ensure
+ # Reload everything.
+ if Facter.respond_to? :loadfacts
+ Facter.loadfacts
+ elsif Facter.respond_to? :load
+ Facter.load
+ else
+ raise Puppet::Error,
+ "You must upgrade your version of Facter to use centralized facts"
+ end
+
+ # This loads all existing facts and any new ones. We have to remove and
+ # reload because there's no way to unload specific facts.
+ loadfacts()
+ end
+
+ # Retrieve the plugins from the central server. We only have to load the
+ # changed plugins, because Puppet::Type loads plugins on demand.
+ def self.getplugins
+ path = Puppet[:pluginpath].split(":")
+ download(:dest => Puppet[:plugindest], :source => Puppet[:pluginsource],
+ :ignore => Puppet[:pluginsignore], :name => "plugin") do |object|
+
+ next unless path.include?(File.dirname(object[:path]))
+
+ begin
+ Puppet.info "Reloading plugin %s" %
+ File.basename(File.basename(object[:path])).sub(".rb",'')
+ load object[:path]
+ rescue => detail
+ Puppet.warning "Could not reload plugin %s: %s" %
+ [object[:path], detail]
+ end
+ end
+ end
+
+ def self.loaddir(dir, type)
+ return unless FileTest.directory?(dir)
+
+ Dir.entries(dir).find_all { |e| e =~ /\.rb$/ }.each do |file|
+ fqfile = File.join(dir, file)
+ begin
+ Puppet.info "Loading #{type} %s" % File.basename(file.sub(".rb",''))
+ Timeout::timeout(self.timeout) do
+ load fqfile
+ end
+ rescue => detail
+ Puppet.warning "Could not load #{type} %s: %s" % [fqfile, detail]
+ end
+ end
+ end
+
+ def self.loadfacts
+ Puppet[:factpath].split(":").each do |dir|
+ loaddir(dir, "fact")
+ end
+ end
+
+ def self.timeout
+ @timeout = Puppet[:configtimeout]
+ case @timeout
+ when String:
+ if @timeout =~ /^\d+$/
+ @timeout = Integer(@timeout)
+ else
+ raise ArgumentError, "Configuration timeout must be an integer"
+ end
+ when Integer: # nothing
+ else
+ raise ArgumentError, "Configuration timeout must be an integer"
+ end
+ end
+
+ # Send off the transaction report.
+ def report(transaction)
+ begin
+ report = transaction.report()
+ if Puppet[:rrdgraph] == true
+ report.graph()
+ end
+ reportclient().report(report)
+ rescue => detail
+ Puppet.err "Reporting failed: %s" % detail
+ end
+ end
+
+ def reportclient
+ unless defined? @reportclient
+ @reportclient = Puppet::Network::Client::Reporter.new(
+ :Server => Puppet[:reportserver]
+ )
+ end
+
+ @reportclient
+ end
+
+ loadfacts()
+
+ private
+
+ # Actually retrieve the configuration, either from the server or from a local master.
+ def get_actual_config(facts)
+ if @local
+ return get_local_config(facts)
+ else
+ begin
+ Timeout::timeout(self.class.timeout) do
+ return get_remote_config(facts)
+ end
+ rescue Timeout::Error
+ Puppet.err "Configuration retrieval timed out"
+ return nil
+ end
+ end
+ end
+
+ # Retrieve a configuration from a local master.
+ def get_local_config(facts)
+ # If we're local, we don't have to do any of the conversion
+ # stuff.
+ objects = @driver.getconfig(facts, "yaml")
+ @compile_time = Time.now
+
+ if objects == ""
+ raise Puppet::Error, "Could not retrieve configuration"
+ end
+
+ return objects
+ end
+
+ # Retrieve a config from a remote master.
+ def get_remote_config(facts)
+ textobjects = ""
+
+ textfacts = CGI.escape(YAML.dump(facts))
+
+ benchmark(:debug, "Retrieved configuration") do
+ # error handling for this is done in the network client
+ begin
+ textobjects = @driver.getconfig(textfacts, "yaml")
+ rescue => detail
+ Puppet.err "Could not retrieve configuration: %s" % detail
+
+ unless Puppet[:usecacheonfailure]
+ @objects = nil
+ Puppet.warning "Not using cache on failed configuration"
+ return
+ end
+ end
+ end
+
+ fromcache = false
+ if textobjects == ""
+ textobjects = self.retrievecache
+ if textobjects == ""
+ raise Puppet::Error.new(
+ "Cannot connect to server and there is no cached configuration"
+ )
+ end
+ Puppet.warning "Could not get config; using cached copy"
+ fromcache = true
+ else
+ @compile_time = Time.now
+ Puppet::Util::Storage.cache(:configuration)[:compile_time] = @compile_time
+ end
+
+ begin
+ textobjects = CGI.unescape(textobjects)
+ rescue => detail
+ raise Puppet::Error, "Could not CGI.unescape configuration"
+ end
+
+ if @cache and ! fromcache
+ self.cache(textobjects)
+ end
+
+ begin
+ objects = YAML.load(textobjects)
+ rescue => detail
+ raise Puppet::Error,
+ "Could not understand configuration: %s" %
+ detail.to_s
+ end
+
+ return objects
+ end
+end
+
+# $Id$
diff --git a/lib/puppet/network/client/proxy.rb b/lib/puppet/network/client/proxy.rb
new file mode 100644
index 000000000..e1295a96f
--- /dev/null
+++ b/lib/puppet/network/client/proxy.rb
@@ -0,0 +1,28 @@
+# unlike the other client classes (again, this design sucks) this class
+# is basically just a proxy class -- it calls its methods on the driver
+# and that's about it
+class Puppet::Network::Client::ProxyClient < Puppet::Network::Client
+ def self.mkmethods
+ interface = @handler.interface
+ namespace = interface.prefix
+
+
+ interface.methods.each { |ary|
+ method = ary[0]
+ Puppet.debug "%s: defining %s.%s" % [self, namespace, method]
+ define_method(method) { |*args|
+ begin
+ @driver.send(method, *args)
+ rescue XMLRPC::FaultException => detail
+ #Puppet.err "Could not call %s.%s: %s" %
+ # [namespace, method, detail.faultString]
+ #raise NetworkClientError,
+ # "XMLRPC Error: %s" % detail.faultString
+ raise NetworkClientError, detail.faultString
+ end
+ }
+ }
+ end
+end
+
+# $Id$
diff --git a/lib/puppet/network/client/reporter.rb b/lib/puppet/network/client/reporter.rb
new file mode 100644
index 000000000..dd340da02
--- /dev/null
+++ b/lib/puppet/network/client/reporter.rb
@@ -0,0 +1,34 @@
+class Puppet::Network::Client::Reporter < Puppet::Network::Client
+ @drivername = :Report
+
+ # set up the appropriate interface methods
+ @handler = Puppet::Network::Server::Report
+
+ def initialize(hash = {})
+ if hash.include?(:Report)
+ hash[:Report] = Puppet::Network::Server::Report.new()
+ end
+
+ super(hash)
+ end
+
+ # Send our report. We get the transaction report and convert it to YAML
+ # as appropriate.
+ def report(transreport)
+ report = YAML.dump(transreport)
+
+ unless self.local
+ report = CGI.escape(report)
+ end
+
+ # Now send the report
+ file = nil
+ benchmark(:info, "Sent transaction report") do
+ file = @driver.report(report)
+ end
+
+ file
+ end
+end
+
+# $Id$
diff --git a/lib/puppet/network/client/resource.rb b/lib/puppet/network/client/resource.rb
new file mode 100644
index 000000000..71a19bf91
--- /dev/null
+++ b/lib/puppet/network/client/resource.rb
@@ -0,0 +1,65 @@
+class Puppet::Network::Client::Resource < Puppet::Network::Client
+ @drivername = :ResourceServer
+
+ @handler = Puppet::Network::Server::Resource
+
+ def apply(bucket)
+
+ case bucket
+ when Puppet::TransObject
+ tmp = Puppet::TransBucket.new
+ tmp.push bucket
+ bucket = tmp
+ bucket.name = Facter["hostname"].value
+ bucket.type = "resource"
+ when Puppet::TransBucket
+ # nothing
+ else
+ raise Puppet::DevError, "You must pass a transportable object, not a %s" %
+ bucket.class
+ end
+
+ unless @local
+ bucket = Base64.encode64(YAML::dump(bucket))
+ end
+ report = @driver.apply(bucket, "yaml")
+
+ return report
+ end
+
+ def describe(type, name, retrieve = false, ignore = false)
+ Puppet.info "Describing %s[%s]" % [type.to_s.capitalize, name]
+ text = @driver.describe(type, name, retrieve, ignore, "yaml")
+
+ object = nil
+ if @local
+ object = text
+ else
+ object = YAML::load(Base64.decode64(text))
+ end
+
+ return object
+ end
+
+ def initialize(hash = {})
+ if hash.include?(:ResourceServer)
+ unless hash[:ResourceServer].is_a?(Puppet::Network::Server::Resource)
+ raise Puppet::DevError, "Must pass an actual PElement server object"
+ end
+ end
+
+ super(hash)
+ end
+
+ def list(type, ignore = false, base = false)
+ bucket = @driver.list(type, ignore, base, "yaml")
+
+ unless @local
+ bucket = YAML::load(Base64.decode64(bucket))
+ end
+
+ return bucket
+ end
+end
+
+# $Id$
diff --git a/lib/puppet/network/client/runner.rb b/lib/puppet/network/client/runner.rb
new file mode 100644
index 000000000..40d13ac86
--- /dev/null
+++ b/lib/puppet/network/client/runner.rb
@@ -0,0 +1,17 @@
+class Puppet::Network::Client::Runner < Puppet::Network::Client::ProxyClient
+ @drivername = :Runner
+
+ # set up the appropriate interface methods
+ @handler = Puppet::Network::Server::Runner
+ self.mkmethods
+
+ def initialize(hash = {})
+ if hash.include?(:Runner)
+ hash[:Runner] = Puppet::Network::Server::Runner.new()
+ end
+
+ super(hash)
+ end
+end
+
+# $Id$
diff --git a/lib/puppet/network/client/status.rb b/lib/puppet/network/client/status.rb
new file mode 100644
index 000000000..6c1a96e85
--- /dev/null
+++ b/lib/puppet/network/client/status.rb
@@ -0,0 +1,7 @@
+class Puppet::Network::Client::StatusClient < Puppet::Network::Client::ProxyClient
+ # set up the appropriate interface methods
+ @handler = Puppet::Network::Server::ServerStatus
+ self.mkmethods
+end
+
+# $Id$
diff --git a/lib/puppet/network/networkclient.rb b/lib/puppet/network/networkclient.rb
new file mode 100644
index 000000000..62d8906e0
--- /dev/null
+++ b/lib/puppet/network/networkclient.rb
@@ -0,0 +1,167 @@
+require 'puppet/sslcertificates'
+require 'openssl'
+require 'puppet/daemon'
+require 'puppet/network/server'
+require 'puppet/external/base64'
+
+require 'webrick'
+require 'cgi'
+require 'xmlrpc/client'
+require 'xmlrpc/server'
+require 'yaml'
+
+module Puppet
+ module Network
+ class ClientError < Puppet::Error; end
+ class NetworkClientError < Puppet::Error; end
+ class NetworkClient < XMLRPC::Client
+ attr_accessor :puppet_server, :puppet_port
+ @clients = {}
+
+ class << self
+ include Puppet::Util
+ include Puppet::Util::ClassGen
+ end
+
+ # Create a netclient for each handler
+ def self.mkclients
+ # add the methods associated with each namespace
+ Puppet::Network::Server::Handler.each { |handler|
+ interface = handler.interface
+ namespace = interface.prefix
+
+ # Create a subclass for every client type. This is
+ # so that all of the methods are on their own class,
+ # so that they namespaces can define the same methods if
+ # they want.
+ constant = handler.to_s.sub(/^.+::/, '')
+ name = namespace.downcase
+ newclient = genclass(name, :hash => @clients,
+ :constant => constant)
+
+ interface.methods.each { |ary|
+ method = ary[0]
+ if public_method_defined?(method)
+ raise Puppet::DevError, "Method %s is already defined" %
+ method
+ end
+ newclient.send(:define_method,method) { |*args|
+ Puppet.debug "Calling %s.%s" % [namespace, method]
+ #Puppet.info "peer cert is %s" % @http.peer_cert
+ #Puppet.info "cert is %s" % @http.cert
+ begin
+ call("%s.%s" % [namespace, method.to_s],*args)
+ rescue OpenSSL::SSL::SSLError => detail
+ raise NetworkClientError,
+ "Certificates were not trusted: %s" % detail
+ rescue XMLRPC::FaultException => detail
+ #Puppet.err "Could not call %s.%s: %s" %
+ # [namespace, method, detail.faultString]
+ #raise NetworkClientError,
+ # "XMLRPC Error: %s" % detail.faultString
+ raise NetworkClientError, detail.faultString
+ rescue Errno::ECONNREFUSED => detail
+ msg = "Could not connect to %s on port %s" %
+ [@host, @port]
+ raise NetworkClientError, msg
+ rescue SocketError => detail
+ error = NetworkClientError.new(
+ "Could not find server %s" % @puppetserver
+ )
+ error.set_backtrace detail.backtrace
+ raise error
+ rescue => detail
+ Puppet.err "Could not call %s.%s: %s" %
+ [namespace, method, detail.inspect]
+ error = NetworkClientError.new(detail.to_s)
+ error.set_backtrace detail.backtrace
+ raise error
+ end
+ }
+ }
+ }
+ end
+
+ def self.netclient(namespace)
+ if @clients.empty?
+ self.mkclients()
+ end
+
+ namespace = symbolize(namespace)
+
+ @clients[namespace]
+ end
+
+ def ca_file=(cafile)
+ @http.ca_file = cafile
+ store = OpenSSL::X509::Store.new
+ store.add_file(cafile)
+ store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT
+ @http.cert_store = store
+ end
+
+ def cert=(cert)
+ #Puppet.debug "Adding certificate"
+ @http.cert = cert
+ @http.verify_mode = OpenSSL::SSL::VERIFY_PEER
+ end
+
+ def key=(key)
+ @http.key = key
+ end
+
+ def initialize(hash)
+ hash[:Path] ||= "/RPC2"
+ hash[:Server] ||= "localhost"
+ hash[:Port] ||= Puppet[:masterport]
+
+ @puppet_server = hash[:Server]
+ @puppet_port = hash[:Port]
+
+ @puppetserver = hash[:Server]
+
+ super(
+ hash[:Server],
+ hash[:Path],
+ hash[:Port],
+ nil, # proxy_host
+ nil, # proxy_port
+ nil, # user
+ nil, # password
+ true, # use_ssl
+ 120 # a two minute timeout, instead of 30 seconds
+ )
+
+ if hash[:Certificate]
+ self.cert = hash[:Certificate]
+ else
+ unless defined? $nocertwarned
+ Puppet.err "No certificate; running with reduced functionality."
+ $nocertwarned = true
+ end
+ end
+
+ if hash[:Key]
+ self.key = hash[:Key]
+ end
+
+ if hash[:CAFile]
+ self.ca_file = hash[:CAFile]
+ end
+
+ # from here, i need to add the key, cert, and ca cert
+ # and reorgize how i start the client
+ end
+
+ def local
+ false
+ end
+
+ def local?
+ false
+ end
+ end
+ end
+end
+
+# $Id$
diff --git a/lib/puppet/network/server.rb b/lib/puppet/network/server.rb
new file mode 100644
index 000000000..e9205d48b
--- /dev/null
+++ b/lib/puppet/network/server.rb
@@ -0,0 +1,207 @@
+# the server
+#
+# allow things to connect to us and communicate, and stuff
+
+require 'puppet'
+require 'puppet/daemon'
+require 'webrick'
+require 'webrick/https'
+require 'cgi'
+require 'xmlrpc/server'
+require 'xmlrpc/client'
+
+module Puppet
+ class ServerError < RuntimeError; end
+ module Network
+ class Server < WEBrick::HTTPServer
+ include Puppet::Daemon
+
+ Puppet.config.setdefaults(:puppetd,
+ :listen => [false, "Whether puppetd should listen for
+ connections. If this is true, then by default only the
+ ``runner`` server is started, which allows remote authorized
+ and authenticated nodes to connect and trigger ``puppetd``
+ runs."]
+ )
+
+ # Create our config object if necessary. This works even if
+ # there's no configuration file.
+ def authconfig
+ unless defined? @authconfig
+ @authconfig = Puppet::Network::AuthConfig.new()
+ end
+
+ @authconfig
+ end
+
+ # Read the CA cert and CRL and populate an OpenSSL::X509::Store
+ # with them, with flags appropriate for checking client
+ # certificates for revocation
+ def x509store
+ if Puppet[:cacrl] == 'none'
+ # No CRL, no store needed
+ return nil
+ end
+ unless File.exist?(Puppet[:cacrl])
+ raise Puppet::Error, "Could not find CRL"
+ end
+ crl = OpenSSL::X509::CRL.new(File.read(Puppet[:cacrl]))
+ store = OpenSSL::X509::Store.new
+ store.purpose = OpenSSL::X509::PURPOSE_ANY
+ store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK_ALL|OpenSSL::X509::V_FLAG_CRL_CHECK
+ store.add_file(@cacertfile)
+ store.add_crl(crl)
+ return store
+ end
+
+ def initialize(hash = {})
+ Puppet.info "Starting server for Puppet version %s" % Puppet.version
+ daemonize = nil
+ if hash.include?(:Daemonize)
+ daemonize = hash[:Daemonize]
+ end
+
+ # FIXME we should have some kind of access control here, using
+ # :RequestHandler
+ hash[:Port] ||= Puppet[:masterport]
+ hash[:Logger] ||= self.httplog
+ hash[:AccessLog] ||= [
+ [ self.httplog, WEBrick::AccessLog::COMMON_LOG_FORMAT ],
+ [ self.httplog, WEBrick::AccessLog::REFERER_LOG_FORMAT ]
+ ]
+
+ if hash.include?(:Handlers)
+ unless hash[:Handlers].is_a?(Hash)
+ raise ServerError, "Handlers must have arguments"
+ end
+
+ @handlers = hash[:Handlers].collect { |handler, args|
+ hclass = nil
+ unless hclass = Handler.handler(handler)
+ raise ServerError, "Invalid handler %s" % handler
+ end
+ hclass.new(args)
+ }
+ else
+ raise ServerError, "A server must have handlers"
+ end
+
+ # okay, i need to retrieve my cert and set it up, somehow
+ # the default case will be that i'm also the ca
+ if ca = @handlers.find { |handler| handler.is_a?(Puppet::Network::Server::CA) }
+ @driver = ca
+ @secureinit = true
+ self.fqdn
+ else
+ if hash.include?(:NoSecureInit)
+ @secureinit = false
+ else
+ @secureinit = true
+ end
+ end
+
+ unless self.readcert
+ unless self.requestcert
+ raise Puppet::Error, "Cannot start without certificates"
+ end
+ end
+
+ hash[:SSLCertificateStore] = x509store
+ hash[:SSLCertificate] = @cert
+ hash[:SSLPrivateKey] = @key
+ hash[:SSLStartImmediately] = true
+ hash[:SSLEnable] = true
+ hash[:SSLCACertificateFile] = @cacertfile
+ hash[:SSLVerifyClient] = OpenSSL::SSL::VERIFY_PEER
+ hash[:SSLCertName] = nil
+
+ super(hash)
+
+ Puppet.info "Listening on port %s" % hash[:Port]
+
+ # this creates a new servlet for every connection,
+ # but all servlets have the same list of handlers
+ # thus, the servlets can have their own state -- passing
+ # around the requests and such -- but the handlers
+ # have a global state
+
+ # mount has to be called after the server is initialized
+ self.mount("/RPC2", Puppet::Network::Server::Servlet, @handlers)
+ end
+
+ # the base class for the different handlers
+ class Handler
+ attr_accessor :server
+ class << self
+ include Puppet::Util
+ end
+
+ @subclasses = []
+
+ def self.each
+ @subclasses.each { |c| yield c }
+ end
+
+ def self.handler(name)
+ name = name.to_s.downcase
+ @subclasses.find { |h|
+ h.name.to_s.downcase == name
+ }
+ end
+
+ def self.inherited(sub)
+ @subclasses << sub
+ end
+
+ def self.interface
+ if defined? @interface
+ return @interface
+ else
+ raise Puppet::DevError, "Handler %s has no defined interface" %
+ self
+ end
+ end
+
+ def self.name
+ unless defined? @name
+ @name = self.to_s.sub(/.+::/, '').intern
+ end
+
+ return @name
+ end
+
+ def initialize(hash = {})
+ end
+ end
+
+
+ class ServerStatus < Handler
+
+ @interface = XMLRPC::Service::Interface.new("status") { |iface|
+ iface.add_method("int status()")
+ }
+
+ @name = :Status
+
+ def status(status = nil, client = nil, clientip = nil)
+ return 1
+ end
+ end
+ end
+ end
+end
+
+require 'puppet/network/server/authstore'
+require 'puppet/network/server/authconfig'
+require 'puppet/network/server/servlet'
+require 'puppet/network/server/master'
+require 'puppet/network/server/ca'
+require 'puppet/network/server/fileserver'
+require 'puppet/network/server/filebucket'
+require 'puppet/network/server/resource'
+require 'puppet/network/server/runner'
+require 'puppet/network/server/logger'
+require 'puppet/network/server/report'
+require 'puppet/network/client'
+
+# $Id$
diff --git a/lib/puppet/network/server/authconfig.rb b/lib/puppet/network/server/authconfig.rb
new file mode 100644
index 000000000..e4d31d8d8
--- /dev/null
+++ b/lib/puppet/network/server/authconfig.rb
@@ -0,0 +1,173 @@
+require 'puppet/util/loadedfile'
+require 'puppet/network/server/rights'
+
+module Puppet
+ class ConfigurationError < Puppet::Error; end
+ class Network::AuthConfig < Puppet::Util::LoadedFile
+ Puppet.config.setdefaults(:puppet,
+ :authconfig => [ "$confdir/namespaceauth.conf",
+ "The configuration file that defines the rights to the different
+ namespaces and methods. This can be used as a coarse-grained
+ authorization system for both ``puppetd`` and ``puppetmasterd``."
+ ]
+ )
+
+ # Just proxy the setting methods to our rights stuff
+ [:allow, :deny].each do |method|
+ define_method(method) do |*args|
+ @rights.send(method, *args)
+ end
+ end
+
+ # Here we add a little bit of semantics. They can set auth on a whole namespace
+ # or on just a single method in the namespace.
+ def allowed?(name, host, ip)
+ namespace, method = name.to_s.split(".")
+ unless namespace and method
+ raise ArgumentError, "Invalid method name %s" % name
+ end
+
+ name = name.intern if name.is_a? String
+ namespace = namespace.intern
+ method = method.intern
+
+ read()
+
+ if @rights.include?(name)
+ return @rights[name].allowed?(host, ip)
+ elsif @rights.include?(namespace)
+ return @rights[namespace].allowed?(host, ip)
+ else
+ return false
+ end
+ end
+
+ # Does the file exist? Puppetmasterd does not require it, but
+ # puppetd does.
+ def exists?
+ FileTest.exists?(@file)
+ end
+
+ def initialize(file = nil, parsenow = true)
+ @file ||= Puppet[:authconfig]
+
+ unless @file
+ raise Puppet::DevError, "No authconfig file defined"
+ end
+ return unless self.exists?
+ super(@file)
+ @rights = Puppet::Network::Rights.new
+ @configstamp = @configstatted = nil
+ @configtimeout = 60
+
+ if parsenow
+ read()
+ end
+ end
+
+ # Read the configuration file.
+ def read
+ return unless FileTest.exists?(@file)
+
+ if @configstamp
+ if @configtimeout and @configstatted
+ if Time.now - @configstatted > @configtimeout
+ @configstatted = Time.now
+ tmp = File.stat(@file).ctime
+
+ if tmp == @configstamp
+ return
+ else
+ Puppet.notice "%s vs %s" % [tmp, @configstamp]
+ end
+ else
+ return
+ end
+ else
+ Puppet.notice "%s and %s" % [@configtimeout, @configstatted]
+ end
+ end
+
+ parse()
+
+ @configstamp = File.stat(@file).ctime
+ @configstatted = Time.now
+ end
+
+ private
+
+ def parse
+ newrights = Puppet::Network::Rights.new
+ begin
+ File.open(@file) { |f|
+ right = nil
+ count = 1
+ f.each { |line|
+ case line
+ when /^\s*#/: next # skip comments
+ when /^\s*$/: next # skip blank lines
+ when /\[([\w.]+)\]/: # "namespace" or "namespace.method"
+ name = $1
+ if newrights.include?(name)
+ raise FileServerError, "%s is already set at %s" %
+ [newrights[name], name]
+ end
+ newrights.newright(name)
+ right = newrights[name]
+ when /^\s*(\w+)\s+(.+)$/:
+ var = $1
+ value = $2
+ case var
+ when "allow":
+ value.split(/\s*,\s*/).each { |val|
+ begin
+ right.info "allowing %s access" % val
+ right.allow(val)
+ rescue AuthStoreError => detail
+ raise ConfigurationError, "%s at line %s of %s" %
+ [detail.to_s, count, @config]
+ end
+ }
+ when "deny":
+ value.split(/\s*,\s*/).each { |val|
+ begin
+ right.info "denying %s access" % val
+ right.deny(val)
+ rescue AuthStoreError => detail
+ raise ConfigurationError, "%s at line %s of %s" %
+ [detail.to_s, count, @config]
+ end
+ }
+ else
+ raise ConfigurationError,
+ "Invalid argument '%s' at line %s" % [var, count]
+ end
+ else
+ raise ConfigurationError, "Invalid line %s: %s" % [count, line]
+ end
+ count += 1
+ }
+ }
+ rescue Errno::EACCES => detail
+ Puppet.err "Configuration error: Cannot read %s; cannot serve" % @file
+ #raise Puppet::Error, "Cannot read %s" % @config
+ rescue Errno::ENOENT => detail
+ Puppet.err "Configuration error: '%s' does not exit; cannot serve" %
+ @file
+ #raise Puppet::Error, "%s does not exit" % @config
+ #rescue FileServerError => detail
+ # Puppet.err "FileServer error: %s" % detail
+ end
+
+ # Verify each of the rights are valid.
+ # We let the check raise an error, so that it can raise an error
+ # pointing to the specific problem.
+ newrights.each { |name, right|
+ right.valid?
+ }
+ @rights = newrights
+ end
+ end
+end
+
+# $Id$
diff --git a/lib/puppet/network/server/authstore.rb b/lib/puppet/network/server/authstore.rb
new file mode 100755
index 000000000..51ce93d46
--- /dev/null
+++ b/lib/puppet/network/server/authstore.rb
@@ -0,0 +1,227 @@
+# standard module for determining whether a given hostname or IP has access to
+# the requested resource
+
+require 'ipaddr'
+
+module Puppet
+ class AuthStoreError < Puppet::Error; end
+ class AuthorizationError < Puppet::Error; end
+
+ class Network::AuthStore
+ # This has to be an array, not a hash, else it loses its ordering.
+ ORDER = [
+ [:ip, [:ip]],
+ [:name, [:hostname, :domain]]
+ ]
+
+ Puppet::Util.logmethods(self, true)
+
+ def allow(pattern)
+ # a simple way to allow anyone at all to connect
+ if pattern == "*"
+ @globalallow = true
+ else
+ store(pattern, @allow)
+ end
+ end
+
+ def allowed?(name, ip)
+ if name or ip
+ # This is probably unnecessary, and can cause some weirdnesses in
+ # cases where we're operating over localhost but don't have a real
+ # IP defined.
+ unless name and ip
+ raise Puppet::DevError, "Name and IP must be passed to 'allowed?'"
+ end
+ # else, we're networked and such
+ else
+ # we're local
+ return true
+ end
+
+ # yay insecure overrides
+ if @globalallow
+ return true
+ end
+
+ value = nil
+ ORDER.each { |nametype, array|
+ if nametype == :ip
+ value = IPAddr.new(ip)
+ else
+ value = name.split(".").reverse
+ end
+
+
+ array.each { |type|
+ [[@deny, false], [@allow, true]].each { |ary|
+ hash, retval = ary
+ if hash.include?(type)
+ hash[type].each { |pattern|
+ if match?(nametype, value, pattern)
+ return retval
+ end
+ }
+ end
+ }
+ }
+ }
+
+ self.info "defaulting to no access for %s" % name
+ # default to false
+ return false
+ end
+
+ def deny(pattern)
+ store(pattern, @deny)
+ end
+
+ def initialize
+ @globalallow = nil
+ @allow = Hash.new { |hash, key|
+ hash[key] = []
+ }
+ @deny = Hash.new { |hash, key|
+ hash[key] = []
+ }
+ end
+
+ private
+
+ def match?(nametype, value, pattern)
+ if value == pattern # simplest shortcut
+ return true
+ end
+
+ case nametype
+ when :ip: matchip?(value, pattern)
+ when :name: matchname?(value, pattern)
+ else
+ raise Puppet::DevError, "Invalid match type %s" % nametype
+ end
+ end
+
+ def matchip?(value, pattern)
+ # we're just using builtin stuff for this, thankfully
+ if pattern.include?(value)
+ return true
+ else
+ return false
+ end
+ end
+
+ def matchname?(value, pattern)
+ # yay, horribly inefficient
+ if pattern[-1] != '*' # the pattern has no metachars and is not equal
+ # thus, no match
+ #Puppet.info "%s is not equal with no * in %s" % [value, pattern]
+ return false
+ else
+ # we know the last field of the pattern is '*'
+ # if everything up to that doesn't match, we're definitely false
+ if pattern[0..-2] != value[0..pattern.length-2]
+ #Puppet.notice "subpatterns didn't match; %s vs %s" %
+ # [pattern[0..-2], value[0..pattern.length-2]]
+ return false
+ end
+
+ case value.length <=> pattern.length
+ when -1: # value is shorter than pattern
+ if pattern.length - value.length == 1
+ # only ever allowed when the value is the domain of a
+ # splatted pattern
+ #Puppet.info "allowing splatted domain %s" % [value]
+ return true
+ else
+ return false
+ end
+ when 0: # value is the same length as pattern
+ if pattern[-1] == "*"
+ #Puppet.notice "same length with *"
+ return true
+ else
+ return false
+ end
+ when 1: # value is longer than pattern
+ # at this point we've already verified that everything up to
+ # the '*' in the pattern matches, so we are true
+ return true
+ end
+ end
+ end
+
+ def store(pattern, hash)
+ type, value = type(pattern)
+
+ if type and value
+ # this won't work once we get beyond simple stuff...
+ hash[type] << value
+ else
+ raise AuthStoreError, "Invalid pattern %s" % pattern
+ end
+ end
+
+ def type(pattern)
+ type = value = nil
+ case pattern
+ when /^(\d+\.){3}\d+$/:
+ type = :ip
+ begin
+ value = IPAddr.new(pattern)
+ rescue ArgumentError => detail
+ raise AuthStoreError, "Invalid IP address pattern %s" % pattern
+ end
+ when /^(\d+\.){3}\d+\/(\d+)$/:
+ mask = Integer($2)
+ if mask < 1 or mask > 32
+ raise AuthStoreError, "Invalid IP mask %s" % mask
+ end
+ type = :ip
+ begin
+ value = IPAddr.new(pattern)
+ rescue ArgumentError => detail
+ raise AuthStoreError, "Invalid IP address pattern %s" % pattern
+ end
+ when /^(\d+\.){1,3}\*$/: # an ip address with a '*' at the end
+ type = :ip
+ match = $1
+ match.sub!(".", '')
+ ary = pattern.split(".")
+
+ mask = case ary.index(match)
+ when 0: 8
+ when 1: 16
+ when 2: 24
+ else
+ raise AuthStoreError, "Invalid IP pattern %s" % pattern
+ end
+
+ ary.pop
+ while ary.length < 4
+ ary.push("0")
+ end
+
+ begin
+ value = IPAddr.new(ary.join(".") + "/" + mask.to_s)
+ rescue ArgumentError => detail
+ raise AuthStoreError, "Invalid IP address pattern %s" % pattern
+ end
+ when /^[\d.]+$/: # necessary so incomplete IP addresses can't look
+ # like hostnames
+ raise AuthStoreError, "Invalid IP address pattern %s" % pattern
+ when /^([a-zA-Z][-\w]*\.)+[-\w]+$/: # a full hostname
+ type = :hostname
+ value = pattern.split(".").reverse
+ when /^\*(\.([a-zA-Z][-\w]*)){1,}$/:
+ type = :domain
+ value = pattern.split(".").reverse
+ else
+ raise AuthStoreError, "Invalid pattern %s" % pattern
+ end
+
+ return [type, value]
+ end
+ end
+end
+
+# $Id$
diff --git a/lib/puppet/network/server/ca.rb b/lib/puppet/network/server/ca.rb
new file mode 100644
index 000000000..8a61399ba
--- /dev/null
+++ b/lib/puppet/network/server/ca.rb
@@ -0,0 +1,152 @@
+require 'openssl'
+require 'puppet'
+require 'puppet/sslcertificates'
+require 'xmlrpc/server'
+
+# Much of this was taken from QuickCert:
+# http://segment7.net/projects/ruby/QuickCert/
+
+class Puppet::Network::Server
+ class CA < Handler
+ attr_reader :ca
+
+ @interface = XMLRPC::Service::Interface.new("puppetca") { |iface|
+ iface.add_method("array getcert(csr)")
+ }
+
+ def autosign
+ if defined? @autosign
+ @autosign
+ else
+ Puppet[:autosign]
+ end
+ end
+
+ # FIXME autosign? should probably accept both hostnames and IP addresses
+ def autosign?(hostname)
+ # simple values are easy
+ if autosign == true or autosign == false
+ return autosign
+ end
+
+ # we only otherwise know how to handle files
+ unless autosign =~ /^\//
+ raise Puppet::Error, "Invalid autosign value %s" %
+ autosign.inspect
+ end
+
+ unless FileTest.exists?(autosign)
+ unless defined? @@warnedonautosign
+ @@warnedonautosign = true
+ Puppet.info "Autosign is enabled but %s is missing" % autosign
+ end
+ return false
+ end
+ auth = Puppet::Network::AuthStore.new
+ File.open(autosign) { |f|
+ f.each { |line|
+ next if line =~ /^\s*#/
+ next if line =~ /^\s*$/
+ auth.allow(line.chomp)
+ }
+ }
+
+ # for now, just cheat and pass a fake IP address to allowed?
+ return auth.allowed?(hostname, "127.1.1.1")
+ end
+
+ def initialize(hash = {})
+ Puppet.config.use(:puppet, :certificates, :ca)
+ if hash.include? :autosign
+ @autosign = hash[:autosign]
+ end
+
+ @ca = Puppet::SSLCertificates::CA.new(hash)
+ end
+
+ # our client sends us a csr, and we either store it for later signing,
+ # or we sign it right away
+ def getcert(csrtext, client = nil, clientip = nil)
+ csr = OpenSSL::X509::Request.new(csrtext)
+
+ # Use the hostname from the CSR, not from the network.
+ subject = csr.subject
+
+ nameary = subject.to_a.find { |ary|
+ ary[0] == "CN"
+ }
+
+ if nameary.nil?
+ Puppet.err(
+ "Invalid certificate request: could not retrieve server name"
+ )
+ return "invalid"
+ end
+
+ hostname = nameary[1]
+
+ unless @ca
+ Puppet.notice "Host %s asked for signing from non-CA master" % hostname
+ return ""
+ end
+
+ # We used to save the public key, but it's basically unnecessary
+ # and it mucks with the permissions requirements.
+ # save_pk(hostname, csr.public_key)
+
+ certfile = File.join(Puppet[:certdir], [hostname, "pem"].join("."))
+
+ # first check to see if we already have a signed cert for the host
+ cert, cacert = ca.getclientcert(hostname)
+ if cert and cacert
+ Puppet.info "Retrieving existing certificate for %s" % hostname
+ #Puppet.info "Cert: %s; Cacert: %s" % [cert.class, cacert.class]
+ return [cert.to_pem, cacert.to_pem]
+ elsif @ca
+ if self.autosign?(hostname) or client.nil?
+ if client.nil?
+ Puppet.info "Signing certificate for CA server"
+ end
+ # okay, we don't have a signed cert
+ # if we're a CA and autosign is turned on, then go ahead and sign
+ # the csr and return the results
+ Puppet.info "Signing certificate for %s" % hostname
+ cert, cacert = @ca.sign(csr)
+ #Puppet.info "Cert: %s; Cacert: %s" % [cert.class, cacert.class]
+ return [cert.to_pem, cacert.to_pem]
+ else # just write out the csr for later signing
+ if @ca.getclientcsr(hostname)
+ Puppet.info "Not replacing existing request from %s" % hostname
+ else
+ Puppet.notice "Host %s has a waiting certificate request" %
+ hostname
+ @ca.storeclientcsr(csr)
+ end
+ return ["", ""]
+ end
+ else
+ raise "huh?"
+ end
+ end
+
+ private
+
+ # Save the public key.
+ def save_pk(hostname, public_key)
+ pkeyfile = File.join(Puppet[:publickeydir], [hostname, "pem"].join('.'))
+
+ if FileTest.exists?(pkeyfile)
+ currentkey = File.open(pkeyfile) { |k| k.read }
+ unless currentkey == public_key.to_s
+ raise Puppet::Error, "public keys for %s differ" % hostname
+ end
+ else
+ File.open(pkeyfile, "w", 0644) { |f|
+ f.print public_key.to_s
+ }
+ end
+ end
+ end
+end
+
+# $Id$
diff --git a/lib/puppet/network/server/filebucket.rb b/lib/puppet/network/server/filebucket.rb
new file mode 100755
index 000000000..77dbbde5e
--- /dev/null
+++ b/lib/puppet/network/server/filebucket.rb
@@ -0,0 +1,167 @@
+#--------------------
+# accept and serve files
+
+
+require 'webrick'
+require 'xmlrpc/server'
+require 'xmlrpc/client'
+require 'facter'
+require 'digest/md5'
+require 'puppet/external/base64'
+
+class Puppet::Network::Server
+ class BucketError < RuntimeError; end
+ class FileBucket < Handler
+ Puppet.config.setdefaults("puppetmasterd",
+ :bucketdir => {
+ :default => "$vardir/bucket",
+ :mode => 0750,
+ :owner => "$user",
+ :group => "$group",
+ :desc => "Where FileBucket files are stored."
+ }
+ )
+
+ Puppet.config.setdefaults("filebucket",
+ :clientbucketdir => {
+ :default => "$vardir/clientbucket",
+ :mode => 0750,
+ :desc => "Where FileBucket files are stored locally."
+ }
+ )
+ @interface = XMLRPC::Service::Interface.new("puppetbucket") { |iface|
+ iface.add_method("string addfile(string, string)")
+ iface.add_method("string getfile(string)")
+ }
+
+ Puppet::Util.logmethods(self, true)
+ attr_reader :name, :path
+
+ # this doesn't work for relative paths
+ def FileBucket.paths(base,md5)
+ return [
+ File.join(base, md5),
+ File.join(base, md5, "contents"),
+ File.join(base, md5, "paths")
+ ]
+ end
+
+ def initialize(hash)
+ if hash.include?(:ConflictCheck)
+ @conflictchk = hash[:ConflictCheck]
+ hash.delete(:ConflictCheck)
+ else
+ @conflictchk = true
+ end
+
+ if hash.include?(:Path)
+ @path = hash[:Path]
+ hash.delete(:Path)
+ else
+ if defined? Puppet
+ @path = Puppet[:bucketdir]
+ else
+ @path = File.expand_path("~/.filebucket")
+ end
+ end
+
+ Puppet.config.use(:filebucket)
+
+ @name = "Filebucket[#{@path}]"
+ end
+
+ # accept a file from a client
+ def addfile(contents, path, client = nil, clientip = nil)
+ if client
+ contents = Base64.decode64(contents)
+ end
+ md5 = Digest::MD5.hexdigest(contents)
+
+ bpath, bfile, pathpath = FileBucket.paths(@path,md5)
+
+ # if it's a new directory...
+ if Puppet.recmkdir(bpath)
+ msg = "Adding %s(%s)" % [path, md5]
+ msg += " from #{client}" if client
+ self.info msg
+ # ...then just create the file
+ File.open(bfile, File::WRONLY|File::CREAT, 0440) { |of|
+ of.print contents
+ }
+ else # if the dir already existed...
+ # ...we need to verify that the contents match the existing file
+ if @conflictchk
+ unless FileTest.exists?(bfile)
+ raise(BucketError,
+ "No file at %s for sum %s" % [bfile,md5], caller)
+ end
+
+ curfile = File.read(bfile)
+
+ # If the contents don't match, then we've found a conflict.
+ # Unlikely, but quite bad.
+ if curfile != contents
+ raise(BucketError,
+ "Got passed new contents for sum %s" % md5, caller)
+ else
+ msg = "Got duplicate %s(%s)" % [path, md5]
+ msg += " from #{client}" if client
+ self.info msg
+ end
+ end
+ end
+
+ contents = ""
+
+ # in either case, add the passed path to the list of paths
+ paths = nil
+ addpath = false
+ if FileTest.exists?(pathpath)
+ File.open(pathpath) { |of|
+ paths = of.readlines.collect { |l| l.chomp }
+ }
+
+ # unless our path is already there...
+ unless paths.include?(path)
+ addpath = true
+ end
+ else
+ addpath = true
+ end
+
+ # if it's a new file, or if our path isn't in the file yet, add it
+ if addpath
+ File.open(pathpath, File::WRONLY|File::CREAT|File::APPEND) { |of|
+ of.puts path
+ }
+ end
+
+ return md5
+ end
+
+ def getfile(md5, client = nil, clientip = nil)
+ bpath, bfile, bpaths = FileBucket.paths(@path,md5)
+
+ unless FileTest.exists?(bfile)
+ return false
+ end
+
+ contents = nil
+ File.open(bfile) { |of|
+ contents = of.read
+ }
+
+ if client
+ return Base64.encode64(contents)
+ else
+ return contents
+ end
+ end
+
+ def to_s
+ self.name
+ end
+ end
+end
+
+# $Id$
diff --git a/lib/puppet/network/server/fileserver.rb b/lib/puppet/network/server/fileserver.rb
new file mode 100755
index 000000000..904d497ca
--- /dev/null
+++ b/lib/puppet/network/server/fileserver.rb
@@ -0,0 +1,589 @@
+require 'puppet'
+require 'webrick/httpstatus'
+require 'cgi'
+require 'delegate'
+
+class Puppet::Network::Server
+ class FileServerError < Puppet::Error; end
+ class FileServer < Handler
+ attr_accessor :local
+
+ Puppet.setdefaults("fileserver",
+ :fileserverconfig => ["$confdir/fileserver.conf",
+ "Where the fileserver configuration is stored."])
+
+ CHECKPARAMS = [:mode, :type, :owner, :group, :checksum]
+
+ @interface = XMLRPC::Service::Interface.new("fileserver") { |iface|
+ iface.add_method("string describe(string, string)")
+ iface.add_method("string list(string, string, boolean, array)")
+ iface.add_method("string retrieve(string, string)")
+ }
+
+ # Describe a given file. This returns all of the manageable aspects
+ # of that file.
+ def describe(url, links = :ignore, client = nil, clientip = nil)
+ links = links.intern if links.is_a? String
+
+ if links == :manage
+ raise Puppet::Network::Server::FileServerError, "Cannot currently copy links"
+ end
+
+ mount, path = convert(url, client, clientip)
+
+ if client
+ mount.debug "Describing %s for %s" % [url, client]
+ end
+
+ obj = nil
+ unless obj = mount.check(path, links)
+ return ""
+ end
+
+ desc = []
+ CHECKPARAMS.each { |check|
+ if property = obj.property(check)
+ unless property.is
+ mount.debug "Manually retrieving info for %s" % check
+ property.retrieve
+ end
+ desc << property.is
+ else
+ if check == "checksum" and obj.property(:type).is == "file"
+ mount.notice "File %s does not have data for %s" %
+ [obj.name, check]
+ end
+ desc << nil
+ end
+ }
+
+ return desc.join("\t")
+ end
+
+ # Create a new fileserving module.
+ def initialize(hash = {})
+ @mounts = {}
+ @files = {}
+
+ if hash[:Local]
+ @local = hash[:Local]
+ else
+ @local = false
+ end
+
+ if hash[:Config] == false
+ @noreadconfig = true
+ else
+ @config = Puppet::Util::LoadedFile.new(
+ hash[:Config] || Puppet[:fileserverconfig]
+ )
+ @noreadconfig = false
+ end
+
+ if hash.include?(:Mount)
+ @passedconfig = true
+ unless hash[:Mount].is_a?(Hash)
+ raise Puppet::DevError, "Invalid mount hash %s" %
+ hash[:Mount].inspect
+ end
+
+ hash[:Mount].each { |dir, name|
+ if FileTest.exists?(dir)
+ self.mount(dir, name)
+ end
+ }
+ else
+ @passedconfig = false
+ readconfig(false) # don't check the file the first time.
+ end
+ end
+
+ # List a specific directory's contents.
+ def list(url, links = :ignore, recurse = false, ignore = false, client = nil, clientip = nil)
+ mount, path = convert(url, client, clientip)
+
+ if client
+ mount.debug "Listing %s for %s" % [url, client]
+ end
+
+ obj = nil
+ unless FileTest.exists?(path)
+ return ""
+ end
+
+ # We pass two paths here, but reclist internally changes one
+ # of the arguments when called internally.
+ desc = reclist(mount, path, path, recurse, ignore)
+
+ if desc.length == 0
+ mount.notice "Got no information on //%s/%s" %
+ [mount, path]
+ return ""
+ end
+
+ desc.collect { |sub|
+ sub.join("\t")
+ }.join("\n")
+ end
+
+ def local?
+ self.local
+ end
+
+ # Mount a new directory with a name.
+ def mount(path, name)
+ if @mounts.include?(name)
+ if @mounts[name] != path
+ raise FileServerError, "%s is already mounted at %s" %
+ [@mounts[name].path, name]
+ else
+ # it's already mounted; no problem
+ return
+ end
+ end
+
+ # Let the mounts do their own error-checking.
+ @mounts[name] = Mount.new(name, path)
+ @mounts[name].info "Mounted %s" % path
+
+ return @mounts[name]
+ end
+
+ # Retrieve a file from the local disk and pass it to the remote
+ # client.
+ def retrieve(url, links = :ignore, client = nil, clientip = nil)
+ links = links.intern if links.is_a? String
+
+ mount, path = convert(url, client, clientip)
+
+ if client
+ mount.info "Sending %s to %s" % [url, client]
+ end
+
+ unless FileTest.exists?(path)
+ return ""
+ end
+
+ links = links.intern if links.is_a? String
+
+ if links == :ignore and FileTest.symlink?(path)
+ return ""
+ end
+
+ str = nil
+ if links == :manage
+ raise Puppet::Error, "Cannot copy links yet."
+ else
+ str = File.read(path)
+ end
+
+ if @local
+ return str
+ else
+ return CGI.escape(str)
+ end
+ end
+
+ def umount(name)
+ @mounts.delete(name) if @mounts.include? name
+ end
+
+ private
+
+ def authcheck(file, mount, client, clientip)
+ # If we're local, don't bother passing in information.
+ if local?
+ client = nil
+ clientip = nil
+ end
+ unless mount.allowed?(client, clientip)
+ mount.warning "%s cannot access %s" %
+ [client, file]
+ raise Puppet::AuthorizationError, "Cannot access %s" % mount
+ end
+ end
+
+ def convert(url, client, clientip)
+ readconfig
+
+ url = URI.unescape(url)
+
+ mount, stub = splitpath(url, client)
+
+ authcheck(url, mount, client, clientip)
+
+ path = nil
+ unless path = mount.subdir(stub, client)
+ mount.notice "Could not find subdirectory %s" %
+ "//%s/%s" % [mount, stub]
+ return ""
+ end
+
+ return mount, path
+ end
+
+ # Deal with ignore parameters.
+ def handleignore(children, path, ignore)
+ ignore.each { |ignore|
+ Dir.glob(File.join(path,ignore), File::FNM_DOTMATCH) { |match|
+ children.delete(File.basename(match))
+ }
+ }
+ return children
+ end
+
+ # Read the configuration file.
+ def readconfig(check = true)
+ return if @noreadconfig
+
+ if check and ! @config.changed?
+ return
+ end
+
+ newmounts = {}
+ begin
+ File.open(@config.file) { |f|
+ mount = nil
+ count = 1
+ f.each { |line|
+ case line
+ when /^\s*#/: next # skip comments
+ when /^\s*$/: next # skip blank lines
+ when /\[(\w+)\]/:
+ name = $1
+ if newmounts.include?(name)
+ raise FileServerError, "%s is already mounted at %s" %
+ [newmounts[name], name], count, @config.file
+ end
+ mount = Mount.new(name)
+ newmounts[name] = mount
+ when /^\s*(\w+)\s+(.+)$/:
+ var = $1
+ value = $2
+ case var
+ when "path":
+ begin
+ mount.path = value
+ rescue FileServerError => detail
+ Puppet.err "Removing mount %s: %s" %
+ [mount.name, detail]
+ newmounts.delete(mount.name)
+ end
+ when "allow":
+ value.split(/\s*,\s*/).each { |val|
+ begin
+ mount.info "allowing %s access" % val
+ mount.allow(val)
+ rescue AuthStoreError => detail
+ raise FileServerError.new(detail.to_s,
+ count, @config.file)
+ end
+ }
+ when "deny":
+ value.split(/\s*,\s*/).each { |val|
+ begin
+ mount.info "denying %s access" % val
+ mount.deny(val)
+ rescue AuthStoreError => detail
+ raise FileServerError.new(detail.to_s,
+ count, @config.file)
+ end
+ }
+ else
+ raise FileServerError.new("Invalid argument '%s'" % var,
+ count, @config.file)
+ end
+ else
+ raise FileServerError.new("Invalid line '%s'" % line.chomp,
+ count, @config.file)
+ end
+ count += 1
+ }
+ }
+ rescue Errno::EACCES => detail
+ Puppet.err "FileServer error: Cannot read %s; cannot serve" % @config
+ #raise Puppet::Error, "Cannot read %s" % @config
+ rescue Errno::ENOENT => detail
+ Puppet.err "FileServer error: '%s' does not exist; cannot serve" %
+ @config
+ #raise Puppet::Error, "%s does not exit" % @config
+ #rescue FileServerError => detail
+ # Puppet.err "FileServer error: %s" % detail
+ end
+
+ # Verify each of the mounts are valid.
+ # We let the check raise an error, so that it can raise an error
+ # pointing to the specific problem.
+ newmounts.each { |name, mount|
+ unless mount.valid?
+ raise FileServerError, "No path specified for mount %s" %
+ name
+ end
+ }
+ @mounts = newmounts
+ end
+
+ # Recursively list the directory. FIXME This should be using
+ # puppet objects, not directly listing.
+ def reclist(mount, root, path, recurse, ignore)
+ # Take out the root of the path.
+ name = path.sub(root, '')
+ if name == ""
+ name = "/"
+ end
+
+ if name == path
+ raise FileServerError, "Could not match %s in %s" %
+ [root, path]
+ end
+
+ desc = [name]
+ ftype = File.stat(path).ftype
+
+ desc << ftype
+ if recurse.is_a?(Integer)
+ recurse -= 1
+ end
+
+ ary = [desc]
+ if recurse == true or (recurse.is_a?(Integer) and recurse > -1)
+ if ftype == "directory"
+ children = Dir.entries(path)
+ if ignore
+ children = handleignore(children, path, ignore)
+ end
+ children.each { |child|
+ next if child =~ /^\.\.?$/
+ reclist(mount, root, File.join(path, child), recurse, ignore).each { |cobj|
+ ary << cobj
+ }
+ }
+ end
+ end
+
+ return ary.reject { |c| c.nil? }
+ end
+
+ # Split the path into the separate mount point and path.
+ def splitpath(dir, client)
+ # the dir is based on one of the mounts
+ # so first retrieve the mount path
+ mount = nil
+ path = nil
+ if dir =~ %r{/(\w+)/?}
+ tmp = $1
+ path = dir.sub(%r{/#{tmp}/?}, '')
+
+ unless mount = @mounts[tmp]
+ raise FileServerError, "Fileserver module '%s' not mounted" % tmp
+ end
+ else
+ raise FileServerError, "Fileserver error: Invalid path '%s'" % dir
+ end
+
+ if path == ""
+ path = nil
+ else
+ # Remove any double slashes that might have occurred
+ path = URI.unescape(path.gsub(/\/\//, "/"))
+ end
+
+ return mount, path
+ end
+
+ def to_s
+ "fileserver"
+ end
+
+ # A simple class for wrapping mount points. Instances of this class
+ # don't know about the enclosing object; they're mainly just used for
+ # authorization.
+ class Mount < Puppet::Network::AuthStore
+ attr_reader :name
+
+ Puppet::Util.logmethods(self, true)
+
+ # Run 'retrieve' on a file. This gets the actual parameters, so
+ # we can pass them to the client.
+ def check(dir, links)
+ unless FileTest.exists?(dir)
+ self.notice "File source %s does not exist" % dir
+ return nil
+ end
+
+ obj = fileobj(dir, links)
+
+ # FIXME we should really have a timeout here -- we don't
+ # want to actually check on every connection, maybe no more
+ # than every 60 seconds or something. It'd be nice if we
+ # could use the builtin scheduling to do this.
+
+ # Retrieval is enough here, because we don't want to cache
+ # any information in the state file, and we don't want to generate
+ # any state changes or anything. We don't even need to sync
+ # the checksum, because we're always going to hit the disk
+ # directly.
+ obj.retrieve
+
+ return obj
+ end
+
+ # Create a map for a specific client.
+ def clientmap(client)
+ {
+ "h" => client.sub(/\..*$/, ""),
+ "H" => client,
+ "d" => client.sub(/[^.]+\./, "") # domain name
+ }
+ end
+
+ # Replace % patterns as appropriate.
+ def expand(path, client = nil)
+ # This map should probably be moved into a method.
+ map = nil
+
+ if client
+ map = clientmap(client)
+ else
+ Puppet.notice "No client; expanding '%s' with local host" %
+ path
+ # Else, use the local information
+ map = localmap()
+ end
+ path.gsub(/%(.)/) do |v|
+ key = $1
+ if key == "%"
+ "%"
+ else
+ map[key] || v
+ end
+ end
+ end
+
+ # Do we have any patterns in our path, yo?
+ def expandable?
+ if defined? @expandable
+ @expandable
+ else
+ false
+ end
+ end
+
+ # Create out object. It must have a name.
+ def initialize(name, path = nil)
+ unless name =~ %r{^\w+$}
+ raise FileServerError, "Invalid name format '%s'" % name
+ end
+ @name = name
+
+ if path
+ self.path = path
+ else
+ @path = nil
+ end
+
+ super()
+ end
+
+ def fileobj(path, links)
+ obj = nil
+ if obj = Puppet.type(:file)[path]
+ # This can only happen in local fileserving, but it's an
+ # important one. It'd be nice if we didn't just set
+ # the check params every time, but I'm not sure it's worth
+ # the effort.
+ obj[:check] = CHECKPARAMS
+ else
+ obj = Puppet.type(:file).create(
+ :name => path,
+ :check => CHECKPARAMS
+ )
+ end
+
+ if links == :manage
+ links = :follow
+ end
+
+ # This, ah, might be completely redundant
+ unless obj[:links] == links
+ obj[:links] = links
+ end
+
+ return obj
+ end
+
+ # Cache this manufactured map, since if it's used it's likely
+ # to get used a lot.
+ def localmap
+ unless defined? @@localmap
+ @@localmap = {
+ "h" => Facter.value("hostname"),
+ "H" => [Facter.value("hostname"),
+ Facter.value("domain")].join("."),
+ "d" => Facter.value("domain")
+ }
+ end
+ @@localmap
+ end
+
+ # Return the path as appropriate, expanding as necessary.
+ def path(client = nil)
+ if expandable?
+ return expand(@path, client)
+ else
+ return @path
+ end
+ end
+
+ # Set the path.
+ def path=(path)
+ # FIXME: For now, just don't validate paths with replacement
+ # patterns in them.
+ if path =~ /%./
+ # Mark that we're expandable.
+ @expandable = true
+ else
+ unless FileTest.exists?(path)
+ raise FileServerError, "%s does not exist" % path
+ end
+ unless FileTest.directory?(path)
+ raise FileServerError, "%s is not a directory" % path
+ end
+ unless FileTest.readable?(path)
+ raise FileServerError, "%s is not readable" % path
+ end
+ @expandable = false
+ end
+ @path = path
+ end
+
+ # Retrieve a specific directory relative to a mount point.
+ # If they pass in a client, then expand as necessary.
+ def subdir(dir = nil, client = nil)
+ basedir = self.path(client)
+
+ dirname = if dir
+ File.join(basedir, dir.split("/").join(File::SEPARATOR))
+ else
+ basedir
+ end
+
+ dirname
+ end
+
+ def to_s
+ "mount[#{@name}]"
+ end
+
+ # Verify our configuration is valid. This should really check to
+ # make sure at least someone will be allowed, but, eh.
+ def valid?
+ return false unless @path
+
+ return true
+ end
+ end
+ end
+end
+
+# $Id$
diff --git a/lib/puppet/network/server/logger.rb b/lib/puppet/network/server/logger.rb
new file mode 100755
index 000000000..f6bf9ba88
--- /dev/null
+++ b/lib/puppet/network/server/logger.rb
@@ -0,0 +1,52 @@
+require 'yaml'
+
+class Puppet::Network::Server
+ class LoggerError < RuntimeError; end
+
+ # Receive logs from remote hosts.
+ class Logger < Handler
+ @interface = XMLRPC::Service::Interface.new("puppetlogger") { |iface|
+ iface.add_method("void addlog(string)")
+ }
+
+ # accept a log message from a client, and route it accordingly
+ def addlog(message, client = nil, clientip = nil)
+ unless message
+ raise Puppet::DevError, "Did not receive message"
+ end
+
+ Puppet.info message.inspect
+ # if the client is set, then we're not local
+ if client
+ begin
+ message = YAML.load(CGI.unescape(message))
+ #message = message
+ rescue => detail
+ raise XMLRPC::FaultException.new(
+ 1, "Could not unYAML log message from %s" % client
+ )
+ end
+ end
+
+ unless message
+ raise Puppet::DevError, "Could not resurrect message"
+ end
+
+ # Mark it as remote, so it's not sent to syslog
+ message.remote = true
+
+ if client
+ if ! message.source or message.source == "Puppet"
+ message.source = client
+ end
+ end
+
+ Puppet::Util::Log.newmessage(message)
+
+ # This is necessary or XMLRPC gets all pukey
+ return ""
+ end
+ end
+end
+
+# $Id$
diff --git a/lib/puppet/network/server/master.rb b/lib/puppet/network/server/master.rb
new file mode 100644
index 000000000..b7096cd6d
--- /dev/null
+++ b/lib/puppet/network/server/master.rb
@@ -0,0 +1,212 @@
+require 'openssl'
+require 'puppet'
+require 'puppet/parser/interpreter'
+require 'puppet/sslcertificates'
+require 'xmlrpc/server'
+require 'yaml'
+
+class Puppet::Network::Server
+ class MasterError < Puppet::Error; end
+ class Master < Handler
+ include Puppet::Util
+
+ attr_accessor :ast, :local
+ attr_reader :ca
+
+ @interface = XMLRPC::Service::Interface.new("puppetmaster") { |iface|
+ iface.add_method("string getconfig(string)")
+ iface.add_method("int freshness()")
+ }
+
+ # FIXME At some point, this should be autodocumenting.
+ def addfacts(facts)
+ # Add our server version to the fact list
+ facts["serverversion"] = Puppet.version.to_s
+
+ # And then add the server name and IP
+ {"servername" => "hostname",
+ "serverip" => "ipaddress"
+ }.each do |var, fact|
+ if obj = Facter[fact]
+ facts[var] = obj.value
+ else
+ Puppet.warning "Could not retrieve fact %s" % fact
+ end
+ end
+ end
+
+ # Manipulate the client name as appropriate.
+ def clientname(name, ip, facts)
+ # Always use the hostname from Facter.
+ client = facts["hostname"]
+ clientip = facts["ipaddress"]
+ if Puppet[:node_name] == 'cert'
+ if name
+ client = name
+ end
+ if ip
+ clientip = ip
+ end
+ end
+
+ return client, clientip
+ end
+
+ # Tell a client whether there's a fresh config for it
+ def freshness(client = nil, clientip = nil)
+ if Puppet.features.rails? and Puppet[:storeconfigs]
+ host = Puppet::Rails::Host.find_or_create_by_name(client)
+ host.last_freshcheck = Time.now
+ if clientip and (! host.ip or host.ip == "")
+ host.ip = clientip
+ end
+ host.save
+ end
+ if defined? @interpreter
+ return @interpreter.parsedate
+ else
+ return 0
+ end
+ end
+
+ def initialize(hash = {})
+ args = {}
+
+ # Allow specification of a code snippet or of a file
+ if code = hash[:Code]
+ args[:Code] = code
+ else
+ args[:Manifest] = hash[:Manifest] || Puppet[:manifest]
+ end
+
+ if hash[:Local]
+ @local = hash[:Local]
+ else
+ @local = false
+ end
+
+ args[:Local] = @local
+
+ if hash.include?(:CA) and hash[:CA]
+ @ca = Puppet::SSLCertificates::CA.new()
+ else
+ @ca = nil
+ end
+
+ Puppet.debug("Creating interpreter")
+
+ if hash.include?(:UseNodes)
+ args[:UseNodes] = hash[:UseNodes]
+ elsif @local
+ args[:UseNodes] = false
+ end
+
+ # This is only used by the cfengine module, or if --loadclasses was
+ # specified in +puppet+.
+ if hash.include?(:Classes)
+ args[:Classes] = hash[:Classes]
+ end
+
+ @interpreter = Puppet::Parser::Interpreter.new(args)
+ end
+
+ def getconfig(facts, format = "marshal", client = nil, clientip = nil)
+ if @local
+ # we don't need to do anything, since we should already
+ # have raw objects
+ Puppet.debug "Our client is local"
+ else
+ Puppet.debug "Our client is remote"
+
+ # XXX this should definitely be done in the protocol, somehow
+ case format
+ when "marshal":
+ Puppet.warning "You should upgrade your client. 'Marshal' will not be supported much longer."
+ begin
+ facts = Marshal::load(CGI.unescape(facts))
+ rescue => detail
+ raise XMLRPC::FaultException.new(
+ 1, "Could not rebuild facts"
+ )
+ end
+ when "yaml":
+ begin
+ facts = YAML.load(CGI.unescape(facts))
+ rescue => detail
+ raise XMLRPC::FaultException.new(
+ 1, "Could not rebuild facts"
+ )
+ end
+ else
+ raise XMLRPC::FaultException.new(
+ 1, "Unavailable config format %s" % format
+ )
+ end
+ end
+
+ client, clientip = clientname(client, clientip, facts)
+
+ # Add any server-side facts to our server.
+ addfacts(facts)
+
+ retobjects = nil
+
+ # This is hackish, but there's no "silence" option for benchmarks
+ # right now
+ if @local
+ #begin
+ retobjects = @interpreter.run(client, facts)
+ #rescue Puppet::Error => detail
+ # Puppet.err detail
+ # raise XMLRPC::FaultException.new(
+ # 1, detail.to_s
+ # )
+ #rescue => detail
+ # Puppet.err detail.to_s
+ # return ""
+ #end
+ else
+ benchmark(:notice, "Compiled configuration for %s" % client) do
+ begin
+ retobjects = @interpreter.run(client, facts)
+ rescue Puppet::Error => detail
+ Puppet.err detail
+ raise XMLRPC::FaultException.new(
+ 1, detail.to_s
+ )
+ rescue => detail
+ Puppet.err detail.to_s
+ return ""
+ end
+ end
+ end
+
+ if @local
+ return retobjects
+ else
+ str = nil
+ case format
+ when "marshal":
+ str = Marshal::dump(retobjects)
+ when "yaml":
+ str = YAML.dump(retobjects)
+ else
+ raise XMLRPC::FaultException.new(
+ 1, "Unavailable config format %s" % format
+ )
+ end
+ return CGI.escape(str)
+ end
+ end
+
+ def local?
+ if defined? @local and @local
+ return true
+ else
+ return false
+ end
+ end
+ end
+end
+
+# $Id$
diff --git a/lib/puppet/network/server/report.rb b/lib/puppet/network/server/report.rb
new file mode 100755
index 000000000..cd0214e30
--- /dev/null
+++ b/lib/puppet/network/server/report.rb
@@ -0,0 +1,174 @@
+# A simple server for triggering a new run on a Puppet client.
+class Puppet::Network::Server
+ class Report < Handler
+ class << self
+ include Puppet::Util::ClassGen
+ end
+
+ module ReportBase
+ include Puppet::Util::Docs
+ attr_writer :useyaml
+
+ def useyaml?
+ if defined? @useyaml
+ @useyaml
+ else
+ false
+ end
+ end
+ end
+
+ @interface = XMLRPC::Service::Interface.new("puppetreports") { |iface|
+ iface.add_method("string report(array)")
+ }
+
+ Puppet.setdefaults(:reporting,
+ :reports => ["store",
+ "The list of reports to generate. All reports are looked for
+ in puppet/reports/<name>.rb, and multiple report names should be
+ comma-separated (whitespace is okay)."
+ ],
+ :reportdir => {:default => "$vardir/reports",
+ :mode => 0750,
+ :owner => "$user",
+ :group => "$group",
+ :desc => "The directory in which to store reports
+ received from the client. Each client gets a separate
+ subdirectory."}
+ )
+
+ @reports = {}
+ @reportloader = Puppet::Util::Autoload.new(self, "puppet/reports")
+
+ class << self
+ attr_reader :hooks
+ end
+
+ # Add a new report type.
+ def self.newreport(name, options = {}, &block)
+ name = symbolize(name)
+
+ mod = genmodule(name, :extend => ReportBase, :hash => @reports, :block => block)
+
+ if options[:useyaml]
+ mod.useyaml = true
+ end
+
+ mod.send(:define_method, :report_name) do
+ name
+ end
+ end
+
+ # Load a report.
+ def self.report(name)
+ name = name.intern if name.is_a? String
+ unless @reports.include? name
+ if @reportloader.load(name)
+ unless @reports.include? name
+ Puppet.warning(
+ "Loaded report file for %s but report was not defined" %
+ name
+ )
+ return nil
+ end
+ else
+ return nil
+ end
+ end
+ @reports[symbolize(name)]
+ end
+
+ def self.reportdocs
+ docs = ""
+
+ # Use this method so they all get loaded
+ reports.sort { |a,b| a.to_s <=> b.to_s }.each do |name|
+ mod = self.report(name)
+ docs += "## %s\n\n" % name
+
+ docs += Puppet::Util::Docs.scrub(mod.doc) + "\n\n"
+ end
+
+ docs
+ end
+
+ def self.reports
+ @reportloader.loadall
+ @reports.keys
+ end
+
+ def initialize(*args)
+ super
+ Puppet.config.use(:reporting)
+ Puppet.config.use(:metrics)
+ end
+
+ # Accept a report from a client.
+ def report(report, client = nil, clientip = nil)
+ # Unescape the report
+ unless @local
+ report = CGI.unescape(report)
+ end
+
+ begin
+ process(report)
+ rescue => detail
+ Puppet.err "Could not process report %s: %s" % [$1, detail]
+ if Puppet[:trace]
+ puts detail.backtrace
+ end
+ end
+ end
+
+ private
+
+ # Process the report using all of the existing hooks.
+ def process(yaml)
+ return if Puppet[:reports] == "none"
+
+ # First convert the report to real objects
+ begin
+ report = YAML.load(yaml)
+ rescue => detail
+ Puppet.warning "Could not load report: %s" % detail
+ return
+ end
+
+ # Used for those reports that accept yaml
+ client = report.host
+
+ reports().each do |name|
+ if mod = self.class.report(name)
+ Puppet.info "Processing report %s for %s" % [name, client]
+
+ # We have to use a dup because we're including a module in the
+ # report.
+ newrep = report.dup
+ begin
+ newrep.extend(mod)
+ if mod.useyaml?
+ newrep.process(yaml)
+ else
+ newrep.process
+ end
+ rescue => detail
+ if Puppet[:trace]
+ puts detail.backtrace
+ end
+ Puppet.err "Report %s failed: %s" %
+ [name, detail]
+ end
+ else
+ Puppet.warning "No report named '%s'" % name
+ end
+ end
+ end
+
+ # Handle the parsing of the reports attribute.
+ def reports
+ Puppet[:reports].gsub(/(^\s+)|(\s+$)/, '').split(/\s*,\s*/)
+ end
+ end
+end
+
+# $Id$
diff --git a/lib/puppet/network/server/resource.rb b/lib/puppet/network/server/resource.rb
new file mode 100755
index 000000000..37e331a13
--- /dev/null
+++ b/lib/puppet/network/server/resource.rb
@@ -0,0 +1,190 @@
+require 'puppet'
+require 'puppet/network/server'
+
+# Serve Puppet elements. Useful for querying, copying, and, um, other stuff.
+class Puppet::Network::Server
+ class Resource < Handler
+ attr_accessor :local
+
+ @interface = XMLRPC::Service::Interface.new("resource") { |iface|
+ iface.add_method("string apply(string, string)")
+ iface.add_method("string describe(string, string, array, array)")
+ iface.add_method("string list(string, array, string)")
+ }
+
+ # Apply a TransBucket as a transaction.
+ def apply(bucket, format = "yaml", client = nil, clientip = nil)
+ unless @local
+ begin
+ case format
+ when "yaml":
+ bucket = YAML::load(Base64.decode64(bucket))
+ else
+ raise Puppet::Error, "Unsupported format '%s'" % format
+ end
+ rescue => detail
+ raise Puppet::Error, "Could not load YAML TransBucket: %s" % detail
+ end
+ end
+
+ component = bucket.to_type
+
+ # Create a client, but specify the remote machine as the server
+ # because the class requires it, even though it's unused
+ client = Puppet::Network::Client::MasterClient.new(:Server => client||"localhost")
+
+ # Set the objects
+ client.objects = component
+
+ # And then apply the configuration. This way we're reusing all
+ # the code in there. It should probably just be separated out, though.
+ transaction = client.apply
+
+ # And then clean up
+ component.remove
+
+ # It'd be nice to return some kind of report, but... at this point
+ # we have no such facility.
+ return "success"
+ end
+
+ # Describe a given object. This returns the 'is' values for every property
+ # available on the object type.
+ def describe(type, name, retrieve = nil, ignore = [], format = "yaml", client = nil, clientip = nil)
+ Puppet.info "Describing %s[%s]" % [type.to_s.capitalize, name]
+ @local = true unless client
+ typeklass = nil
+ unless typeklass = Puppet.type(type)
+ raise Puppet::Error, "Puppet type %s is unsupported" % type
+ end
+
+ obj = nil
+
+ retrieve ||= :all
+ ignore ||= []
+
+ if obj = typeklass[name]
+ obj[:check] = retrieve
+ else
+ begin
+ obj = typeklass.create(:name => name, :check => retrieve)
+ rescue Puppet::Error => detail
+ raise Puppet::Error, "%s[%s] could not be created: %s" %
+ [type, name, detail]
+ end
+ end
+
+ unless obj
+ raise XMLRPC::FaultException.new(
+ 1, "Could not create %s[%s]" % [type, name]
+ )
+ end
+
+ trans = obj.to_trans
+
+ # Now get rid of any attributes they specifically don't want
+ ignore.each do |st|
+ if trans.include? st
+ trans.delete(st)
+ end
+ end
+
+ # And get rid of any attributes that are nil
+ trans.each do |attr, value|
+ if value.nil?
+ trans.delete(attr)
+ end
+ end
+
+ unless @local
+ case format
+ when "yaml":
+ trans = Base64.encode64(YAML::dump(trans))
+ else
+ raise XMLRPC::FaultException.new(
+ 1, "Unavailable config format %s" % format
+ )
+ end
+ end
+
+ return trans
+ end
+
+ # Create a new fileserving module.
+ def initialize(hash = {})
+ if hash[:Local]
+ @local = hash[:Local]
+ else
+ @local = false
+ end
+ end
+
+ # List all of the elements of a given type.
+ def list(type, ignore = [], base = nil, format = "yaml", client = nil, clientip = nil)
+ @local = true unless client
+ typeklass = nil
+ unless typeklass = Puppet.type(type)
+ raise Puppet::Error, "Puppet type %s is unsupported" % type
+ end
+
+ # They can pass in false
+ ignore ||= []
+ ignore = [ignore] unless ignore.is_a? Array
+ bucket = Puppet::TransBucket.new
+ bucket.type = typeklass.name
+
+ typeklass.list.each do |obj|
+ next if ignore.include? obj.name
+
+ object = Puppet::TransObject.new(obj.name, typeklass.name)
+ bucket << object
+ end
+
+ unless @local
+ case format
+ when "yaml":
+ begin
+ bucket = Base64.encode64(YAML::dump(bucket))
+ rescue => detail
+ Puppet.err detail
+ raise XMLRPC::FaultException.new(
+ 1, detail.to_s
+ )
+ end
+ else
+ raise XMLRPC::FaultException.new(
+ 1, "Unavailable config format %s" % format
+ )
+ end
+ end
+
+ return bucket
+ end
+
+ private
+
+ def authcheck(file, mount, client, clientip)
+ unless mount.allowed?(client, clientip)
+ mount.warning "%s cannot access %s" %
+ [client, file]
+ raise Puppet::AuthorizationError, "Cannot access %s" % mount
+ end
+ end
+
+ # Deal with ignore parameters.
+ def handleignore(children, path, ignore)
+ ignore.each { |ignore|
+ Dir.glob(File.join(path,ignore), File::FNM_DOTMATCH) { |match|
+ children.delete(File.basename(match))
+ }
+ }
+ return children
+ end
+
+ def to_s
+ "resource"
+ end
+ end
+end
+
+# $Id$
diff --git a/lib/puppet/network/server/rights.rb b/lib/puppet/network/server/rights.rb
new file mode 100755
index 000000000..11da3b705
--- /dev/null
+++ b/lib/puppet/network/server/rights.rb
@@ -0,0 +1,74 @@
+require 'ipaddr'
+require 'puppet/network/server/authstore'
+
+# Define a set of rights and who has access to them.
+class Puppet::Network::Rights < Hash
+ # We basically just proxy directly to our rights. Each Right stores
+ # its own auth abilities.
+ [:allow, :allowed?, :deny].each do |method|
+ define_method(method) do |name, *args|
+ name = name.intern if name.is_a? String
+
+ if obj = right(name)
+ obj.send(method, *args)
+ else
+ raise ArgumentError, "Unknown right '%s'" % name
+ end
+ end
+ end
+
+ def [](name)
+ name = name.intern if name.is_a? String
+ super(name)
+ end
+
+ # Define a new right to which access can be provided.
+ def newright(name)
+ name = name.intern if name.is_a? String
+ shortname = Right.shortname(name)
+ if self.include? name
+ raise ArgumentError, "Right '%s' is already defined" % name
+ else
+ self[name] = Right.new(name, shortname)
+ end
+ end
+
+ private
+
+ # Retrieve a right by name.
+ def right(name)
+ name = name.intern if name.is_a? String
+ self[name]
+ end
+
+ # A right.
+ class Right < Puppet::Network::AuthStore
+ attr_accessor :name, :shortname
+
+ Puppet::Util.logmethods(self, true)
+
+ def self.shortname(name)
+ name.to_s[0..0]
+ end
+
+ def initialize(name, shortname = nil)
+ @name = name
+ @shortname = shortname
+ unless @shortname
+ @shortname = Right.shortname(name)
+ end
+ super()
+ end
+
+ def to_s
+ "access[%s]" % @name
+ end
+
+ # There's no real check to do at this point
+ def valid?
+ true
+ end
+ end
+end
+
+# $Id$
diff --git a/lib/puppet/network/server/runner.rb b/lib/puppet/network/server/runner.rb
new file mode 100755
index 000000000..c0ec8fb9d
--- /dev/null
+++ b/lib/puppet/network/server/runner.rb
@@ -0,0 +1,61 @@
+class Puppet::Network::Server
+ class MissingMasterError < RuntimeError; end # Cannot find the master client
+ # A simple server for triggering a new run on a Puppet client.
+ class Runner < Handler
+ @interface = XMLRPC::Service::Interface.new("puppetrunner") { |iface|
+ iface.add_method("string run(string, string)")
+ }
+
+ # Run the client configuration right now, optionally specifying
+ # tags and whether to ignore schedules
+ def run(tags = nil, ignoreschedules = false, fg = true, client = nil, clientip = nil)
+ # We need to retrieve the client
+ master = Puppet::Network::Client::MasterClient.instance
+
+ unless master
+ raise MissingMasterError, "Could not find the master client"
+ end
+
+ if Puppet::Util::Pidlock.new(Puppet[:puppetdlockfile]).locked?
+ Puppet.notice "Could not trigger run; already running"
+ return "running"
+ end
+
+ if tags == ""
+ tags = nil
+ end
+
+ if ignoreschedules == ""
+ ignoreschedules == nil
+ end
+
+ msg = ""
+ if client
+ msg = "%s(%s) " % [client, clientip]
+ end
+ msg += "triggered run" %
+ if tags
+ msg += " with tags %s" % tags
+ end
+
+ if ignoreschedules
+ msg += " without schedules"
+ end
+
+ Puppet.notice msg
+
+ # And then we need to tell it to run, with this extra info.
+ if fg
+ master.run(tags, ignoreschedules)
+ else
+ Puppet.newthread do
+ master.run(tags, ignoreschedules)
+ end
+ end
+
+ return "success"
+ end
+ end
+end
+
+# $Id$
diff --git a/lib/puppet/network/server/servlet.rb b/lib/puppet/network/server/servlet.rb
new file mode 100644
index 000000000..0a7253eff
--- /dev/null
+++ b/lib/puppet/network/server/servlet.rb
@@ -0,0 +1,277 @@
+require 'xmlrpc/server'
+
+class Puppet::Network::Server
+ class ServletError < RuntimeError; end
+ class Servlet < XMLRPC::WEBrickServlet
+ ERR_UNAUTHORIZED = 30
+
+ attr_accessor :request
+
+ # this is just a duplicate of the normal method; it's here for
+ # debugging when i need it
+ def self.get_instance(server, *options)
+ self.new(server, *options)
+ end
+
+ # This is a hackish way to avoid an auth message every time we have a
+ # normal operation
+ def self.log(msg)
+ unless defined? @logs
+ @logs = {}
+ end
+ if @logs.include?(msg)
+ @logs[msg] += 1
+ else
+ Puppet.info msg
+ @logs[msg] = 1
+ end
+ end
+
+ def add_handler(interface, handler)
+ @loadedhandlers << interface.prefix
+ super
+ end
+
+ # Verify that our client has access. We allow untrusted access to
+ # puppetca methods but no others.
+ def authorize(request, method)
+ namespace = method.sub(/\..+/, '')
+ client = request.peeraddr[2]
+ if defined? @client and @client
+ client = @client
+ end
+ ip = request.peeraddr[3]
+ if request.client_cert
+ if @puppetserver.authconfig.exists?
+ allowed = @puppetserver.authconfig.allowed?(method, client, ip)
+
+ if allowed
+ Puppet.debug "Allowing %s(%s) trusted access to %s" %
+ [client, ip, method]
+ return true
+ else
+ Puppet.debug "Denying %s(%s) trusted access to %s" %
+ [client, ip, method]
+ return false
+ end
+ else
+ # This is pretty hackish, but...
+ # This means we can't actually test this method at this point.
+ # The next release of Puppet will almost definitely require
+ # this file to exist or will default to denying all access.
+ if Puppet.name == "puppetmasterd" or defined? Test::Unit::TestCase
+ Puppet.debug "Allowing %s(%s) trusted access to %s" %
+ [client, ip, method]
+ return true
+ else
+ Puppet.debug "Denying %s(%s) trusted access to %s on %s" %
+ [client, ip, method, Puppet.name]
+ return false
+ end
+ end
+ else
+ if method =~ /^puppetca\./
+ Puppet.notice "Allowing %s(%s) untrusted access to CA methods" %
+ [client, ip]
+ else
+ Puppet.err "Unauthenticated client %s(%s) cannot call %s" %
+ [client, ip, method]
+ return false
+ end
+ end
+ end
+
+ def available?(method)
+ namespace = method.sub(/\..+/, '')
+ client = request.peeraddr[2]
+ ip = request.peeraddr[3]
+ if @loadedhandlers.include?(namespace)
+ return true
+ else
+ Puppet.warning "Client %s(%s) requested unavailable functionality %s" %
+ [client, ip, namespace]
+ return false
+ end
+ end
+
+ def initialize(server, handlers)
+ @puppetserver = server
+ @notified = {}
+ # the servlet base class does not consume any arguments
+ # and its BasicServer base class only accepts a 'class_delim'
+ # option which won't change in Puppet at all
+ # thus, we don't need to pass any args to our base class,
+ # and we can consume them all ourselves
+ super()
+
+ @loadedhandlers = []
+ handlers.each { |handler|
+ #Puppet.debug "adding handler for %s" % handler.class
+ self.add_handler(handler.class.interface, handler)
+ }
+
+ # Initialize these to nil, but they will get set to values
+ # by the 'service' method. These have to instance variables
+ # because I don't have a clear line from the service method to
+ # the service hook.
+ @request = nil
+ @client = nil
+ @clientip = nil
+
+ self.set_service_hook { |obj, *args|
+ if @client and @clientip
+ args.push(@client, @clientip)
+ end
+ begin
+ obj.call(*args)
+ rescue XMLRPC::FaultException
+ raise
+ rescue Puppet::AuthorizationError => detail
+ #Puppet.warning obj.inspect
+ #Puppet.warning args.inspect
+ Puppet.err "Permission denied: %s" % detail.to_s
+ raise XMLRPC::FaultException.new(
+ 1, detail.to_s
+ )
+ rescue Puppet::Error => detail
+ #Puppet.warning obj.inspect
+ #Puppet.warning args.inspect
+ if Puppet[:trace]
+ puts detail.backtrace
+ end
+ Puppet.err detail.to_s
+ error = XMLRPC::FaultException.new(
+ 1, detail.to_s
+ )
+ error.set_backtrace detail.backtrace
+ raise error
+ rescue => detail
+ #Puppet.warning obj.inspect
+ #Puppet.warning args.inspect
+ if Puppet[:trace]
+ puts detail.backtrace
+ end
+ Puppet.err "Could not call: %s" % detail.to_s
+ error = XMLRPC::FaultException.new(1, detail.to_s)
+ error.set_backtrace detail.backtrace
+ raise error
+ end
+ }
+ end
+
+ # Handle the actual request. This does some basic collection of
+ # data, and then just calls the parent method.
+ def service(request, response)
+ @request = request
+
+ # The only way that @client can be nil is if the request is local.
+ if peer = request.peeraddr
+ @client = peer[2]
+ @clientip = peer[3]
+ else
+ raise XMLRPC::FaultException.new(
+ ERR_UNCAUGHT_EXCEPTION,
+ "Could not retrieve client information"
+ )
+ end
+
+ # If they have a certificate (which will almost always be true)
+ # then we get the hostname from the cert, instead of via IP
+ # info
+ if cert = request.client_cert
+ nameary = cert.subject.to_a.find { |ary|
+ ary[0] == "CN"
+ }
+
+ if nameary.nil?
+ Puppet.warning "Could not retrieve server name from cert"
+ else
+ unless @client == nameary[1]
+ Puppet.debug "Overriding %s with cert name %s" %
+ [@client, nameary[1]]
+ @client = nameary[1]
+ end
+ end
+ end
+ begin
+ super
+ rescue => detail
+ Puppet.err "Could not service request: %s: %s" %
+ [detail.class, detail]
+ end
+ @client = nil
+ @clientip = nil
+ @request = nil
+ end
+
+ private
+
+ # this is pretty much just a copy of the original method but with more
+ # feedback
+ # here's where we have our authorization hooks
+ def dispatch(methodname, *args)
+
+ if defined? @request and @request
+ unless self.available?(methodname)
+ raise XMLRPC::FaultException.new(
+ ERR_UNAUTHORIZED,
+ "Functionality %s not available" %
+ methodname.sub(/\..+/, '')
+ )
+ end
+ unless self.authorize(@request, methodname)
+ raise XMLRPC::FaultException.new(
+ ERR_UNAUTHORIZED,
+ "Host %s not authorized to call %s" %
+ [@request.host, methodname]
+ )
+ end
+ else
+ raise Puppet::DevError, "Did not get request in dispatch"
+ end
+
+ #Puppet.warning "dispatch on %s called with %s" %
+ # [methodname, args.inspect]
+ for name, obj in @handler
+ if obj.kind_of? Proc
+ unless methodname == name
+ #Puppet.debug "obj is proc but %s != %s" %
+ # [methodname, name]
+ next
+ end
+ else
+ unless methodname =~ /^#{name}(.+)$/
+ #Puppet.debug "methodname did not match"
+ next
+ end
+ unless obj.respond_to? $1
+ #Puppet.debug "methodname does not respond to %s" % $1
+ next
+ end
+ obj = obj.method($1)
+ end
+
+ if check_arity(obj, args.size)
+ if @service_hook.nil?
+ return obj.call(*args)
+ else
+ return @service_hook.call(obj, *args)
+ end
+ else
+ Puppet.debug "arity is incorrect"
+ end
+ end
+
+ if @default_handler.nil?
+ raise XMLRPC::FaultException.new(
+ ERR_METHOD_MISSING,
+ "Method #{methodname} missing or wrong number of parameters!"
+ )
+ else
+ @default_handler.call(methodname, *args)
+ end
+ end
+ end
+end
+
+# $Id$