diff options
author | luke <luke@980ebf18-57e1-0310-9a29-db15c13687c0> | 2006-01-24 06:01:58 +0000 |
---|---|---|
committer | luke <luke@980ebf18-57e1-0310-9a29-db15c13687c0> | 2006-01-24 06:01:58 +0000 |
commit | ae2575b45de1e8f4c0ec956cebe0eed2bafbcf57 (patch) | |
tree | 9c2b7c839087c285c228374f525315e55c392a34 | |
parent | 18e8e74a2e3b4c5d092fc0aae38bbc5455d4db48 (diff) | |
download | puppet-ae2575b45de1e8f4c0ec956cebe0eed2bafbcf57.tar.gz puppet-ae2575b45de1e8f4c0ec956cebe0eed2bafbcf57.tar.xz puppet-ae2575b45de1e8f4c0ec956cebe0eed2bafbcf57.zip |
Adding the event-loop stuff to the repository and switching to using it. Also, breaking many classes out into their own class files.
git-svn-id: https://reductivelabs.com/svn/puppet/trunk@848 980ebf18-57e1-0310-9a29-db15c13687c0
31 files changed, 2232 insertions, 1203 deletions
diff --git a/bin/puppetd b/bin/puppetd index 6d2b804b0..c082e2a80 100755 --- a/bin/puppetd +++ b/bin/puppetd @@ -259,11 +259,23 @@ end client.setcerts trap(:INT) { - exit(1) + client.shutdown } -client.run(onetime) +if onetime + begin + client.run + rescue => detail + Puppet.err detail.to_s + if Puppet[:debug] + puts detail.backtrace + end + end +else + client.start -#Puppet.join + # Mmm, hackish + Puppet.start +end # $Id$ diff --git a/lib/puppet.rb b/lib/puppet.rb index b092dd7e2..2e1a88e66 100644 --- a/lib/puppet.rb +++ b/lib/puppet.rb @@ -1,4 +1,5 @@ require 'singleton' +require 'puppet/event-loop' require 'puppet/log' require 'puppet/util' @@ -85,7 +86,7 @@ PUPPETVERSION = '0.11.2' :statedir => [:puppetvar, "state"], :rundir => [:puppetvar, "run"], - # then the files}, + # then the files, :manifestdir => [:puppetconf, "manifests"], :manifest => [:manifestdir, "site.pp"], :localconfig => [:puppetconf, "localconfig"], @@ -109,6 +110,7 @@ PUPPETVERSION = '0.11.2' :parseonly => false, :puppetport => 8139, :masterport => 8140, + :runinterval => 60, } # If we're running the standalone puppet process as a non-root user, @@ -181,47 +183,30 @@ PUPPETVERSION = '0.11.2' end end - def self.asuser(user) - # FIXME this should use our user object, since it already knows how - # to find users and such - require 'etc' - - begin - obj = Etc.getpwnam(user) - rescue ArgumentError - raise Puppet::Error, "User %s not found" - end - - uid = obj.uid - - olduid = nil - if Process.uid != uid - olduid = Process.uid - Process.euid = uid - end - - retval = yield - - - if olduid - Process.euid = olduid - end - - return retval + # Start our event loop. This blocks, waiting for someone, somewhere, + # to generate events of some kind. + def self.start + #Puppet.info "Starting loop" + EventLoop.current.run end - def self.join - return unless defined? @threads - @threads.each { |th| th.join } - end - - def self.newthread - @threads ||= [] - @threads << Thread.new { - yield - } + # Create the timer that our different objects (uh, mostly the client) + # check. + def self.timer + unless defined? @timer + #Puppet.info "Interval is %s" % Puppet[:runinterval] + #@timer = EventLoop::Timer.new(:interval => Puppet[:runinterval]) + @timer = EventLoop::Timer.new( + :interval => Puppet[:runinterval], + :tolerance => 1, + :start? => true + ) + EventLoop.current.monitor_timer @timer + end + @timer end + # Store a new default value. def self.setdefault(param,value) if value.is_a?(Array) if value[0].is_a?(Symbol) diff --git a/lib/puppet/client.rb b/lib/puppet/client.rb index 27c7cef72..c6061e0ef 100644 --- a/lib/puppet/client.rb +++ b/lib/puppet/client.rb @@ -1,155 +1,22 @@ # the available clients require 'puppet' -require 'puppet/sslcertificates' -require 'puppet/type' -require 'facter' -require 'openssl' -require 'puppet/transaction' -require 'puppet/transportable' -require 'puppet/metric' -require 'puppet/daemon' -require 'puppet/server' -require 'puppet/base64' - -$noclientnetworking = false -begin - require 'webrick' - require 'cgi' - require 'xmlrpc/client' - require 'xmlrpc/server' - require 'yaml' -rescue LoadError => detail - $noclientnetworking = detail - raise Puppet::Error, "You must have the Ruby XMLRPC, CGI, and Webrick libraries installed" -end +require 'puppet/networkclient' module Puppet - class NetworkClientError < RuntimeError; end - class ClientError < RuntimeError; end - #--------------------------------------------------------------- - if $noclientnetworking - Puppet.err "Could not load client network libs: %s" % $noclientnetworking - else - class NetworkClient < XMLRPC::Client - #include Puppet::Daemon - - # add the methods associated with each namespace - Puppet::Server::Handler.each { |handler| - interface = handler.interface - namespace = interface.prefix - - interface.methods.each { |ary| - method = ary[0] - Puppet.info "Defining %s.%s" % [namespace, method] - self.send(:define_method,method) { |*args| - #Puppet.info "Calling %s" % 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 - #Puppet.err "Could not call %s.%s: Untrusted certificates" % - # [namespace, method] - raise NetworkClientError, - "Certificates were not trusted" - 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] - #Puppet.err msg - raise NetworkClientError, msg - rescue SocketError => detail - Puppet.err "Could not find server %s" % @puppetserver - exit(12) - rescue => detail - Puppet.err "Could not call %s.%s: %s" % - [namespace, method, detail.inspect] - #raise NetworkClientError.new(detail.to_s) - raise - end - } - } - } - - def ca_file=(cafile) - @http.ca_file = cafile - store = OpenSSL::X509::Store.new - cacert = OpenSSL::X509::Certificate.new( - File.read(cafile) - ) - store.add_cert(cacert) - 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] - - @puppetserver = hash[:Server] - - super( - hash[:Server], - hash[:Path], - hash[:Port], - nil, # proxy_host - nil, # proxy_port - nil, # user - nil, # password - true # use_ssl - ) - - if hash[:Certificate] - self.cert = hash[:Certificate] - else - Puppet.err "No certificate; running with reduced functionality." - 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 - end - end - # 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 Client include Puppet + include SignalObserver # 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 :local, :secureinit + attr_accessor :schedule, :lastrun class << self attr_reader :drivername @@ -216,436 +83,69 @@ module Puppet end end - def setcerts - @driver.cert = @cert - @driver.key = @key - @driver.ca_file = @cacertfile - end - - class MasterClient < Puppet::Client - @drivername = :Master - - def self.facts - facts = {} - Facter.each { |name,fact| - facts[name] = fact.downcase - } - - facts - end - - # This method is how the client receives the tree of Transportable - # objects. For now, just descend into the tree and perform and - # necessary manipulations. - def apply - dostorage() - unless defined? @objects - raise Puppet::Error, "Cannot apply; objects not defined" - end - - #Puppet.err :yay - #p @objects - #Puppet.err :mark - #@objects = @objects.to_type - # this is a gross hack... but i don't see a good way around it - # set all of the variables to empty - Puppet::Transaction.init - - # For now we just evaluate the top-level object, but eventually - # there will be schedules and such associated with each object, - # and probably with the container itself. - transaction = @objects.evaluate - #transaction = Puppet::Transaction.new(objects) - transaction.toplevel = true - 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[:debug] - puts detail.backtrace - end - ensure - Puppet::Storage.store - end - Puppet::Metric.gather - Puppet::Metric.tally - if Puppet[:rrdgraph] == true - Metric.store - Metric.graph - end - - return transaction - end - - # Cache the config - def cache(text) - Puppet.info "Caching configuration at %s" % self.cachefile - confdir = File.dirname(Puppet[:localconfig]) - unless FileTest.exists?(confdir) - Puppet.recmkdir(confdir, 0770) - end - 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 - - # Initialize and load storage - def dostorage - begin - Puppet::Storage.init - Puppet::Storage.load - rescue => detail - Puppet.err "Corrupt state file %s" % Puppet[:checksumfile] - begin - File.unlink(Puppet[:checksumfile]) - 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 defined? @configstamp - return false - end - - # We're willing to give a 2 second drift - if @driver.freshness - @configstamp < 1 - return true - else - return false - end - end - - # Retrieve the config from a remote server. If this fails, then - # use the cached copy. - def getconfig - if self.fresh? - Puppet.info "Config is up to date" - return - end - Puppet.debug("getting config") - dostorage() - - facts = self.class.facts - - unless facts.length > 0 - raise Puppet::ClientError.new( - "Could not retrieve any facts" - ) - end - - objects = nil - if @local - # If we're local, we don't have to do any of the conversion - # stuff. - objects = @driver.getconfig(facts, "yaml") - @configstamp = Time.now.to_i - - if objects == "" - raise Puppet::Error, "Could not retrieve configuration" - end - else - textobjects = "" - - textfacts = CGI.escape(YAML.dump(facts)) - - # 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 - 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.notice "Could not get config; using cached copy" - fromcache = true - end - - begin - textobjects = CGI.unescape(textobjects) - @configstamp = Time.now.to_i - 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 - end - - unless objects.is_a?(Puppet::TransBucket) - raise NetworkClientError, - "Invalid returned objects of type %s" % objects.class - end - - if classes = objects.classes - self.setclasses(classes) - else - Puppet.info "No classes to store" - end - - # Clear all existing objects, so we can recreate our stack. - if defined? @objects - Puppet::Type.allclear - end - @objects = nil - - # First create the default scheduling objects - Puppet.type(:schedule).mkdefaultschedules - - # Now convert the objects to real Puppet objects - @objects = objects.to_type - - if @objects.nil? - raise Puppet::Error, "Configuration could not be processed" - end - #@objects = objects - - # and perform any necessary final actions before we evaluate. - Puppet::Type.finalize - - return @objects - 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. For now, just - # ignore the onetime thing. - def run(onetime = false) - #if onetime - begin - self.getconfig - self.apply - rescue => detail - Puppet.err detail.to_s - if Puppet[:debug] - puts detail.backtrace - end - exit(13) - end - return - #end - -# Puppet.newthread do -# begin -# self.getconfig -# self.apply -# rescue => detail -# Puppet.err detail.to_s -# if Puppet[:debug] -# puts detail.backtrace -# end -# exit(13) -# end -# end - end - - def setclasses(ary) - 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 + # A wrapper method to run and then store the last run time + def runnow + begin + self.run + self.lastrun = Time.now.to_i + rescue => detail + Puppet.err "Could not run %s: %s" % [self.class, detail] end end - class Dipper < Puppet::Client - @drivername = :Bucket - - def initialize(hash = {}) - if hash.include?(:Path) - bucket = Puppet::Server::FileBucket.new( - :Bucket => hash[:Path] - ) - hash.delete(:Path) - hash[:Bucket] = bucket - end - - super(hash) - end - - def backup(file) - unless FileTest.exists?(file) - raise(BucketError, "File %s does not exist" % file, caller) - end - contents = File.open(file) { |of| of.read } - - string = Base64.encode64(contents) - #puts "string is created" - - sum = @driver.addfile(string,file) - #puts "file %s is added" % file - return sum - end - - def restore(file,sum) - restore = true - if FileTest.exists?(file) - contents = File.open(file) { |of| of.read } - - cursum = Digest::MD5.hexdigest(contents) - - # if the checksum has changed... - # this might be extra effort - if cursum == sum - restore = false - end - end - - if restore - #puts "Restoring %s" % file - if tmp = @driver.getfile(sum) - newcontents = Base64.decode64(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 - #puts "Done" - return newsum - else - return nil - end - - end + def run + raise Puppet::DevError, "Client type %s did not override run" % + self.class end - # 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 ProxyClient < Puppet::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] - self.send(: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 - } - } + def scheduled? + if sched = self.schedule + return sched.match?(self.lastrun) + else + return true end end - class FileClient < Puppet::Client::ProxyClient - @drivername = :FileServer - - # set up the appropriate interface methods - @handler = Puppet::Server::FileServer - - self.mkmethods - - def initialize(hash = {}) - if hash.include?(:FileServer) - unless hash[:FileServer].is_a?(Puppet::Server::FileServer) - raise Puppet::DevError, "Must pass an actual FS object" - end - end - - super(hash) - end + def setcerts + @driver.cert = @cert + @driver.key = @key + @driver.ca_file = @cacertfile end - class CAClient < Puppet::Client::ProxyClient - @drivername = :CA - - # set up the appropriate interface methods - @handler = Puppet::Server::CA - self.mkmethods - - def initialize(hash = {}) - if hash.include?(:CA) - hash[:CA] = Puppet::Server::CA.new() - end - - super(hash) - end + def shutdown + Puppet::Storage.store + exit end - class LogClient < Puppet::Client::ProxyClient - @drivername = :Logger + # Start listening for events. We're pretty much just listening for + # timer events here. + def start + # Create our timer + timer = EventLoop::Timer.new( + :interval => Puppet[:runinterval], + :tolerance => 1, + :start? => true + ) - # set up the appropriate interface methods - @handler = Puppet::Server::Logger - self.mkmethods + # Stick it in the loop + EventLoop.current.monitor_timer timer - def initialize(hash = {}) - if hash.include?(:Logger) - hash[:Logger] = Puppet::Server::Logger.new() + # And run indefinitely + observe_signal timer, :alarm do + if self.scheduled? + self.runnow end - - super(hash) end end - class StatusClient < Puppet::Client::ProxyClient - # set up the appropriate interface methods - @handler = Puppet::Server::ServerStatus - self.mkmethods - end - + require 'puppet/client/proxy' + require 'puppet/client/ca' + require 'puppet/client/dipper' + require 'puppet/client/file' + require 'puppet/client/log' + require 'puppet/client/master' + require 'puppet/client/status' end -#--------------------------------------------------------------- end # $Id$ diff --git a/lib/puppet/client/ca.rb b/lib/puppet/client/ca.rb new file mode 100644 index 000000000..11989230c --- /dev/null +++ b/lib/puppet/client/ca.rb @@ -0,0 +1,17 @@ +class Puppet::Client::CAClient < Puppet::Client::ProxyClient + @drivername = :CA + + # set up the appropriate interface methods + @handler = Puppet::Server::CA + self.mkmethods + + def initialize(hash = {}) + if hash.include?(:CA) + hash[:CA] = Puppet::Server::CA.new() + end + + super(hash) + end +end + +# $Id$ diff --git a/lib/puppet/client/dipper.rb b/lib/puppet/client/dipper.rb new file mode 100644 index 000000000..281d0bab7 --- /dev/null +++ b/lib/puppet/client/dipper.rb @@ -0,0 +1,78 @@ +module Puppet + class Client + # The client class for filebuckets. + class Dipper < Puppet::Client + @drivername = :Bucket + + def initialize(hash = {}) + if hash.include?(:Path) + bucket = Puppet::Server::FileBucket.new( + :Bucket => hash[:Path] + ) + hash.delete(:Path) + hash[:Bucket] = bucket + end + + super(hash) + end + + def backup(file) + unless FileTest.exists?(file) + raise(BucketError, "File %s does not exist" % file, caller) + end + contents = File.open(file) { |of| of.read } + + string = Base64.encode64(contents) + #puts "string is created" + + sum = @driver.addfile(string,file) + #puts "file %s is added" % file + return sum + end + + def restore(file,sum) + restore = true + if FileTest.exists?(file) + contents = File.open(file) { |of| of.read } + + cursum = Digest::MD5.hexdigest(contents) + + # if the checksum has changed... + # this might be extra effort + if cursum == sum + restore = false + end + end + + if restore + #puts "Restoring %s" % file + if tmp = @driver.getfile(sum) + newcontents = Base64.decode64(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 + #puts "Done" + return newsum + else + return nil + end + + end + end + end +end + +# $Id$ diff --git a/lib/puppet/client/file.rb b/lib/puppet/client/file.rb new file mode 100644 index 000000000..116624003 --- /dev/null +++ b/lib/puppet/client/file.rb @@ -0,0 +1,20 @@ +class Puppet::Client::FileClient < Puppet::Client::ProxyClient + @drivername = :FileServer + + # set up the appropriate interface methods + @handler = Puppet::Server::FileServer + + self.mkmethods + + def initialize(hash = {}) + if hash.include?(:FileServer) + unless hash[:FileServer].is_a?(Puppet::Server::FileServer) + raise Puppet::DevError, "Must pass an actual FS object" + end + end + + super(hash) + end +end + +# $Id$ diff --git a/lib/puppet/client/log.rb b/lib/puppet/client/log.rb new file mode 100644 index 000000000..e20c0532c --- /dev/null +++ b/lib/puppet/client/log.rb @@ -0,0 +1,17 @@ +class Puppet::Client::LogClient < Puppet::Client::ProxyClient + @drivername = :Logger + + # set up the appropriate interface methods + @handler = Puppet::Server::Logger + self.mkmethods + + def initialize(hash = {}) + if hash.include?(:Logger) + hash[:Logger] = Puppet::Server::Logger.new() + end + + super(hash) + end +end + +# $Id$ diff --git a/lib/puppet/client/master.rb b/lib/puppet/client/master.rb new file mode 100644 index 000000000..f0204c418 --- /dev/null +++ b/lib/puppet/client/master.rb @@ -0,0 +1,244 @@ +# The client for interacting with the puppetmaster config server. +class Puppet::Client::MasterClient < Puppet::Client + @drivername = :Master + + def self.facts + facts = {} + Facter.each { |name,fact| + facts[name] = fact.downcase + } + + facts + end + + # This method is how the client receives the tree of Transportable + # objects. For now, just descend into the tree and perform and + # necessary manipulations. + def apply + dostorage() + unless defined? @objects + raise Puppet::Error, "Cannot apply; objects not defined" + end + + #Puppet.err :yay + #p @objects + #Puppet.err :mark + #@objects = @objects.to_type + # this is a gross hack... but i don't see a good way around it + # set all of the variables to empty + Puppet::Transaction.init + + # For now we just evaluate the top-level object, but eventually + # there will be schedules and such associated with each object, + # and probably with the container itself. + transaction = @objects.evaluate + #transaction = Puppet::Transaction.new(objects) + transaction.toplevel = true + 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[:debug] + puts detail.backtrace + end + ensure + Puppet::Storage.store + end + Puppet::Metric.gather + Puppet::Metric.tally + if Puppet[:rrdgraph] == true + Metric.store + Metric.graph + end + + return transaction + end + + # Cache the config + def cache(text) + Puppet.info "Caching configuration at %s" % self.cachefile + confdir = File.dirname(Puppet[:localconfig]) + unless FileTest.exists?(confdir) + Puppet.recmkdir(confdir, 0770) + end + 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 + + # Initialize and load storage + def dostorage + begin + Puppet::Storage.init + Puppet::Storage.load + rescue => detail + Puppet.err "Corrupt state file %s: %s" % [Puppet[:checksumfile], detail] + begin + File.unlink(Puppet[:checksumfile]) + 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 defined? @configstamp + return false + end + + # We're willing to give a 2 second drift + if @driver.freshness - @configstamp < 1 + return true + else + return false + end + end + + # Retrieve the config from a remote server. If this fails, then + # use the cached copy. + def getconfig + if self.fresh? + Puppet.info "Config is up to date" + return + end + Puppet.debug("getting config") + dostorage() + + facts = self.class.facts + + unless facts.length > 0 + raise Puppet::ClientError.new( + "Could not retrieve any facts" + ) + end + + objects = nil + if @local + # If we're local, we don't have to do any of the conversion + # stuff. + objects = @driver.getconfig(facts, "yaml") + @configstamp = Time.now.to_i + + if objects == "" + raise Puppet::Error, "Could not retrieve configuration" + end + else + textobjects = "" + + textfacts = CGI.escape(YAML.dump(facts)) + + # 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 + 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.notice "Could not get config; using cached copy" + fromcache = true + end + + begin + textobjects = CGI.unescape(textobjects) + @configstamp = Time.now.to_i + 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 + end + + unless objects.is_a?(Puppet::TransBucket) + raise NetworkClientError, + "Invalid returned objects of type %s" % objects.class + end + + if classes = objects.classes + self.setclasses(classes) + else + Puppet.info "No classes to store" + end + + # Clear all existing objects, so we can recreate our stack. + if defined? @objects + Puppet::Type.allclear + end + @objects = nil + + # First create the default scheduling objects + Puppet.type(:schedule).mkdefaultschedules + + # Now convert the objects to real Puppet objects + @objects = objects.to_type + + if @objects.nil? + raise Puppet::Error, "Configuration could not be processed" + end + #@objects = objects + + # and perform any necessary final actions before we evaluate. + Puppet::Type.finalize + + return @objects + 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 + self.getconfig + self.apply + end + + def setclasses(ary) + 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 +end + +# $Id$ diff --git a/lib/puppet/client/proxy.rb b/lib/puppet/client/proxy.rb new file mode 100644 index 000000000..2ea0a87f9 --- /dev/null +++ b/lib/puppet/client/proxy.rb @@ -0,0 +1,27 @@ +# 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::Client::ProxyClient < Puppet::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] + self.send(: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/client/status.rb b/lib/puppet/client/status.rb new file mode 100644 index 000000000..ed1445e04 --- /dev/null +++ b/lib/puppet/client/status.rb @@ -0,0 +1,7 @@ +class Puppet::Client::StatusClient < Puppet::Client::ProxyClient + # set up the appropriate interface methods + @handler = Puppet::Server::ServerStatus + self.mkmethods +end + +# $Id$ diff --git a/lib/puppet/daemon.rb b/lib/puppet/daemon.rb index a3ced6c47..a8ef0a87a 100755 --- a/lib/puppet/daemon.rb +++ b/lib/puppet/daemon.rb @@ -153,6 +153,9 @@ module Puppet begin cert, cacert = @driver.getcert(@csr.to_pem) rescue => detail + if Puppet[:debug] + puts detail.backtrace + end raise Puppet::Error.new("Certificate retrieval failed: %s" % detail) end diff --git a/lib/puppet/event-loop.rb b/lib/puppet/event-loop.rb new file mode 100644 index 000000000..9d98cf0ee --- /dev/null +++ b/lib/puppet/event-loop.rb @@ -0,0 +1 @@ +require "puppet/event-loop/event-loop" diff --git a/lib/puppet/event-loop/better-definers.rb b/lib/puppet/event-loop/better-definers.rb new file mode 100644 index 000000000..0af37da62 --- /dev/null +++ b/lib/puppet/event-loop/better-definers.rb @@ -0,0 +1,367 @@ +## better-definers.rb --- better attribute and method definers +# Copyright (C) 2005 Daniel Brockman + +# This program is free software; you can redistribute it +# and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation; +# either version 2 of the License, or (at your option) any +# later version. + +# This file is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty +# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public +# License along with this program; if not, write to the Free +# Software Foundation, 51 Franklin Street, Fifth Floor, +# Boston, MA 02110-1301, USA. + +class Symbol + def predicate? + to_s.include? "?" end + def imperative? + to_s.include? "!" end + def writer? + to_s.include? "=" end + + def punctuated? + predicate? or imperative? or writer? end + def without_punctuation + to_s.delete("?!=").to_sym end + + def predicate + without_punctuation.to_s + "?" end + def imperative + without_punctuation.to_s + "!" end + def writer + without_punctuation.to_s + "=" end +end + +class Hash + def collect! (&block) + replace Hash[*collect(&block).flatten] + end + + def flatten + to_a.flatten + end +end + +module Kernel + def returning (value) + yield value ; value + end +end + +class Module + def define_hard_aliases (name_pairs) + for new_aliases, existing_name in name_pairs do + new_aliases.kind_of? Array or new_aliases = [new_aliases] + for new_alias in new_aliases do + alias_method(new_alias, existing_name) + end + end + end + + def define_soft_aliases (name_pairs) + for new_aliases, existing_name in name_pairs do + new_aliases.kind_of? Array or new_aliases = [new_aliases] + for new_alias in new_aliases do + class_eval %{def #{new_alias}(*args, &block) + #{existing_name}(*args, &block) end} + end + end + end + + define_soft_aliases \ + :define_hard_alias => :define_hard_aliases, + :define_soft_alias => :define_soft_aliases + + # This method lets you define predicates like :foo?, + # which will be defined to return the value of @foo. + def define_readers (*names) + for name in names.map { |x| x.to_sym } do + if name.punctuated? + # There's no way to define an efficient reader whose + # name is different from the instance variable. + class_eval %{def #{name} ; @#{name.without_punctuation} end} + else + # Use `attr_reader' to define an efficient method. + attr_reader(name) + end + end + end + + def writer_defined? (name) + method_defined? name.to_sym.writer + end + + # If you pass a predicate symbol :foo? to this method, it'll first + # define a regular writer method :foo, without a question mark. + # Then it'll define an imperative writer method :foo! as a shorthand + # for setting the property to true. + def define_writers (*names, &body) + for name in names.map { |x| x.to_sym } do + if block_given? + define_method(name.writer, &body) + else + attr_writer(name.without_punctuation) + end + if name.predicate? + class_eval %{def #{name.imperative} + self.#{name.writer} true end} + end + end + end + + define_soft_aliases \ + :define_reader => :define_readers, + :define_writer => :define_writers + + # We don't need a singular alias for `define_accessors', + # because it always defines at least two methods. + + def define_accessors (*names) + define_readers(*names) + define_writers(*names) + end + + def define_opposite_readers (name_pairs) + name_pairs.collect! { |k, v| [k.to_sym, v.to_sym] } + for opposite_name, name in name_pairs do + define_reader(name) unless method_defined? name + class_eval %{def #{opposite_name} ; not #{name} end} + end + end + + def define_opposite_writers (name_pairs) + name_pairs.collect! { |k, v| [k.to_sym, v.to_sym] } + for opposite_name, name in name_pairs do + define_writer(name) unless writer_defined? name + class_eval %{def #{opposite_name.writer} x + self.#{name.writer} !x end} + class_eval %{def #{opposite_name.imperative} + self.#{name.writer} false end} + end + end + + define_soft_aliases \ + :define_opposite_reader => :define_opposite_readers, + :define_opposite_writer => :define_opposite_writers + + def define_opposite_accessors (name_pairs) + define_opposite_readers name_pairs + define_opposite_writers name_pairs + end + + def define_reader_with_opposite (name_pair, &body) + name, opposite_name = name_pair.flatten.collect { |x| x.to_sym } + define_method(name, &body) + define_opposite_reader(opposite_name => name) + end + + def define_writer_with_opposite (name_pair, &body) + name, opposite_name = name_pair.flatten.collect { |x| x.to_sym } + define_writer(name, &body) + define_opposite_writer(opposite_name => name) + end + + public :define_method + + def define_methods (*names, &body) + names.each { |name| define_method(name, &body) } + end + + def define_private_methods (*names, &body) + define_methods(*names, &body) + names.each { |name| private name } + end + + def define_protected_methods (*names, &body) + define_methods(*names, &body) + names.each { |name| protected name } + end + + def define_private_method (name, &body) + define_method(name, &body) + private name + end + + def define_protected_method (name, &body) + define_method(name, &body) + protected name + end +end + +class ImmutableAttributeError < StandardError + def initialize (attribute=nil, message=nil) + super message + @attribute = attribute + end + + define_accessors :attribute + + def to_s + if @attribute and @message + "cannot change the value of `#@attribute': #@message" + elsif @attribute + "cannot change the value of `#@attribute'" + elsif @message + "cannot change the value of attribute: #@message" + else + "cannot change the value of attribute" + end + end +end + +class Module + # Guard each of the specified attributes by replacing the writer + # method with a proxy that asks the supplied block before proceeding + # with the change. + # + # If it's okay to change the attribute, the block should return + # either nil or the symbol :mutable. If it isn't okay, the block + # should return a string saying why the attribute can't be changed. + # If you don't want to provide a reason, you can have the block + # return just the symbol :immutable. + def guard_writers(*names, &predicate) + for name in names.map { |x| x.to_sym } do + define_hard_alias("__unguarded_#{name.writer}" => name.writer) + define_method(name.writer) do |new_value| + case result = predicate.call + when :mutable, nil + __send__("__unguarded_#{name.writer}", new_value) + when :immutable + raise ImmutableAttributeError.new(name) + else + raise ImmutableAttributeError.new(name, result) + end + end + end + end + + def define_guarded_writers (*names, &block) + define_writers(*names) + guard_writers(*names, &block) + end + + define_soft_alias :guard_writer => :guard_writers + define_soft_alias :define_guarded_writer => :define_guarded_writers +end + +if __FILE__ == $0 + require "test/unit" + + class DefineAccessorsTest < Test::Unit::TestCase + def setup + @X = Class.new + @Y = Class.new @X + @x = @X.new + @y = @Y.new + end + + def test_define_hard_aliases + @X.define_method(:foo) { 123 } + @X.define_method(:baz) { 321 } + @X.define_hard_aliases :bar => :foo, :quux => :baz + assert_equal @x.foo, 123 + assert_equal @x.bar, 123 + assert_equal @y.foo, 123 + assert_equal @y.bar, 123 + assert_equal @x.baz, 321 + assert_equal @x.quux, 321 + assert_equal @y.baz, 321 + assert_equal @y.quux, 321 + @Y.define_method(:foo) { 456 } + assert_equal @y.foo, 456 + assert_equal @y.bar, 123 + @Y.define_method(:quux) { 654 } + assert_equal @y.baz, 321 + assert_equal @y.quux, 654 + end + + def test_define_soft_aliases + @X.define_method(:foo) { 123 } + @X.define_method(:baz) { 321 } + @X.define_soft_aliases :bar => :foo, :quux => :baz + assert_equal @x.foo, 123 + assert_equal @x.bar, 123 + assert_equal @y.foo, 123 + assert_equal @y.bar, 123 + assert_equal @x.baz, 321 + assert_equal @x.quux, 321 + assert_equal @y.baz, 321 + assert_equal @y.quux, 321 + @Y.define_method(:foo) { 456 } + assert_equal @y.foo, @y.bar, 456 + @Y.define_method(:quux) { 654 } + assert_equal @y.baz, 321 + assert_equal @y.quux, 654 + end + + def test_define_readers + @X.define_readers :foo, :bar + assert !@x.respond_to?(:foo=) + assert !@x.respond_to?(:bar=) + @x.instance_eval { @foo = 123 ; @bar = 456 } + assert_equal @x.foo, 123 + assert_equal @x.bar, 456 + @X.define_readers :baz?, :quux? + assert !@x.respond_to?(:baz=) + assert !@x.respond_to?(:quux=) + @x.instance_eval { @baz = false ; @quux = true } + assert !@x.baz? + assert @x.quux? + end + + def test_define_writers + assert !@X.writer_defined?(:foo) + assert !@X.writer_defined?(:bar) + @X.define_writers :foo, :bar + assert @X.writer_defined?(:foo) + assert @X.writer_defined?(:bar) + assert @X.writer_defined?(:foo=) + assert @X.writer_defined?(:bar=) + assert @X.writer_defined?(:foo?) + assert @X.writer_defined?(:bar?) + assert !@x.respond_to?(:foo) + assert !@x.respond_to?(:bar) + @x.foo = 123 + @x.bar = 456 + assert_equal @x.instance_eval { @foo }, 123 + assert_equal @x.instance_eval { @bar }, 456 + @X.define_writers :baz?, :quux? + assert !@x.respond_to?(:baz?) + assert !@x.respond_to?(:quux?) + @x.baz = true + @x.quux = false + assert_equal @x.instance_eval { @baz }, true + assert_equal @x.instance_eval { @quux }, false + end + + def test_define_accessors + @X.define_accessors :foo, :bar + @x.foo = 123 ; @x.bar = 456 + assert_equal @x.foo, 123 + assert_equal @x.bar, 456 + end + + def test_define_opposite_readers + @X.define_opposite_readers :foo? => :bar?, :baz? => :quux? + assert !@x.respond_to?(:foo=) + assert !@x.respond_to?(:bar=) + assert !@x.respond_to?(:baz=) + assert !@x.respond_to?(:quux=) + @x.instance_eval { @bar = true ; @quux = false } + assert !@x.foo? + assert @x.bar? + assert @x.baz? + assert !@x.quux? + end + + def test_define_opposite_writers + @X.define_opposite_writers :foo? => :bar?, :baz => :quux + end + end +end diff --git a/lib/puppet/event-loop/event-loop.rb b/lib/puppet/event-loop/event-loop.rb new file mode 100644 index 000000000..5d78844ef --- /dev/null +++ b/lib/puppet/event-loop/event-loop.rb @@ -0,0 +1,355 @@ +## event-loop.rb --- high-level IO multiplexer +# Copyright (C) 2005 Daniel Brockman + +# This program is free software; you can redistribute it +# and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation; +# either version 2 of the License, or (at your option) any +# later version. + +# This file is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty +# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public +# License along with this program; if not, write to the Free +# Software Foundation, 51 Franklin Street, Fifth Floor, +# Boston, MA 02110-1301, USA. + +require "puppet/event-loop/better-definers" +require "puppet/event-loop/signal-system" + +require "fcntl" + +class EventLoop + include SignalEmitter + + IO_STATES = [:readable, :writable, :exceptional] + + class << self + def default ; @default ||= new end + def default= x ; @default = x end + + def current + Thread.current["event-loop::current"] || default end + def current= x + Thread.current["event-loop::current"] = x end + + def with_current (new) + if current == new + yield + else + begin + old = self.current + self.current = new + yield + ensure + self.current = old + end + end + end + + def method_missing (name, *args, &block) + if current.respond_to? name + current.__send__(name, *args, &block) + else + super + end + end + end + + define_signals :before_sleep, :after_sleep + + def initialize + @running = false + @awake = false + @wakeup_time = nil + @timers = [] + + @io_arrays = [[], [], []] + @ios = Hash.new do |h, k| raise ArgumentError, + "invalid IO event: #{k}", caller(2) end + IO_STATES.each_with_index { |x, i| @ios[x] = @io_arrays[i] } + + @notify_src, @notify_snk = IO.pipe + + @notify_src.will_block = false + @notify_snk.will_block = false + + # Each time a byte is sent through the notification pipe + # we need to read it, or IO.select will keep returning. + monitor_io(@notify_src, :readable) + @notify_src.extend(Watchable) + @notify_src.on_readable do + begin + @notify_src.sysread(256) + rescue Errno::EAGAIN + # The pipe wasn't readable after all. + end + end + end + + define_opposite_accessors \ + :stopped? => :running?, + :sleeping? => :awake? + + def run + if block_given? + thread = Thread.new { run } + yield ; quit ; thread.join + else + running! + iterate while running? + end + ensure + quit + end + + def iterate (user_timeout=nil) + t1, t2 = user_timeout, max_timeout + timeout = t1 && t2 ? [t1, t2].min : t1 || t2 + select(timeout).zip(IO_STATES) do |ios, state| + ios.each { |x| x.signal(state) } if ios + end + end + + private + + def select (timeout) + @wakeup_time = timeout ? Time.now + timeout : nil + # puts "waiting: #{timeout} seconds" + signal :before_sleep ; sleeping! + IO.select(*@io_arrays + [timeout]) || [] + ensure + awake! ; signal :after_sleep + @timers.each { |x| x.sound_alarm if x.ready? } + end + + public + + def quit ; stopped! ; wake_up ; self end + + def monitoring_io? (io, event) + @ios[event].include? io end + def monitoring_timer? (timer) + @timers.include? timer end + + def monitor_io (io, *events) + for event in events do + unless monitoring_io?(io, event) + @ios[event] << io ; wake_up + end + end + end + + def monitor_timer (timer) + unless monitoring_timer? timer + @timers << timer + end + end + + def check_timer (timer) + wake_up if timer.end_time < @wakeup_time + end + + def ignore_io (io, *events) + events = IO_STATES if events.empty? + for event in events do + wake_up if @ios[event].delete(io) + end + end + + def ignore_timer (timer) + # Don't need to wake up for this. + @timers.delete(timer) + end + + def max_timeout + return nil if @timers.empty? + [@timers.collect { |x| x.time_left }.min, 0].max + end + + def wake_up + @notify_snk.write('.') if sleeping? + end +end + +class Symbol + def io_state? + EventLoop::IO_STATES.include? self + end +end + +module EventLoop::Watchable + include SignalEmitter + + define_signals :readable, :writable, :exceptional + + def monitor_events (*events) + EventLoop.monitor_io(self, *events) end + def ignore_events (*events) + EventLoop.ignore_io(self, *events) end + + define_soft_aliases \ + :monitor_event => :monitor_events, + :ignore_event => :ignore_events + + def close ; super + ignore_events end + def close_read ; super + ignore_event :readable end + def close_write ; super + ignore_event :writable end + + module Automatic + include EventLoop::Watchable + + def add_signal_handler (name, &handler) super + monitor_event(name) if name.io_state? + end + + def remove_signal_handler (name, handler) super + if @signal_handlers[name].empty? + ignore_event(name) if name.io_state? + end + end + end +end + +class IO + def on_readable &block + extend EventLoop::Watchable::Automatic + on_readable(&block) + end + + def on_writable &block + extend EventLoop::Watchable::Automatic + on_writable(&block) + end + + def on_exceptional &block + extend EventLoop::Watchable::Automatic + on_exceptional(&block) + end + + def will_block? + require "fcntl" + fcntl(Fcntl::F_GETFL, 0) & Fcntl::O_NONBLOCK == 0 + end + + def will_block= (wants_blocking) + require "fcntl" + flags = fcntl(Fcntl::F_GETFL, 0) + if wants_blocking + flags &= ~Fcntl::O_NONBLOCK + else + flags |= Fcntl::O_NONBLOCK + end + fcntl(Fcntl::F_SETFL, flags) + end +end + +class EventLoop::Timer + include SignalEmitter + + DEFAULT_INTERVAL = 0.0 + DEFAULT_TOLERANCE = 0.001 + + def initialize (options={}, &handler) + @running = false + @start_time = nil + + if options.kind_of? Numeric + options = { :interval => options } + end + + if options[:interval] + @interval = options[:interval].to_f + else + @interval = DEFAULT_INTERVAL + end + + if options[:tolerance] + @tolerance = options[:tolerance].to_f + elsif DEFAULT_TOLERANCE < @interval + @tolerance = DEFAULT_TOLERANCE + else + @tolerance = 0.0 + end + + @event_loop = options[:event_loop] || EventLoop.current + + if block_given? + add_signal_handler(:alarm, &handler) + start unless options[:start?] == false + else + start if options[:start?] + end + end + + define_readers :interval, :tolerance + define_signal :alarm + + def stopped? ; @start_time == nil end + def running? ; @start_time != nil end + + def interval= (new_interval) + old_interval = @interval + @interval = new_interval + if new_interval < old_interval + @event_loop.check_timer(self) + end + end + + def end_time + @start_time + @interval end + def time_left + end_time - Time.now end + def ready? + time_left <= @tolerance end + + def restart + @start_time = Time.now + end + + def sound_alarm + signal :alarm + restart if running? + end + + def start + @start_time = Time.now + @event_loop.monitor_timer(self) + end + + def stop + @start_time = nil + @event_loop.ignore_timer(self) + end +end + +if __FILE__ == $0 + require "test/unit" + + class TimerTest < Test::Unit::TestCase + def setup + @timer = EventLoop::Timer.new(:interval => 0.001) + end + + def test_timer + @timer.on_alarm do + puts "[#{@timer.time_left} seconds left after alarm]" + EventLoop.quit + end + 8.times do + t0 = Time.now + @timer.start ; EventLoop.run + t1 = Time.now + assert(t1 - t0 > @timer.interval - @timer.tolerance) + end + end + end +end + +## event-loop.rb ends here. diff --git a/lib/puppet/event-loop/signal-system.rb b/lib/puppet/event-loop/signal-system.rb new file mode 100644 index 000000000..f7fe9b52c --- /dev/null +++ b/lib/puppet/event-loop/signal-system.rb @@ -0,0 +1,220 @@ +## signal-system.rb --- simple intra-process signal system +# Copyright (C) 2005 Daniel Brockman + +# This program is free software; you can redistribute it +# and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation; +# either version 2 of the License, or (at your option) any +# later version. + +# This file is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty +# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public +# License along with this program; if not, write to the Free +# Software Foundation, 51 Franklin Street, Fifth Floor, +# Boston, MA 02110-1301, USA. + +require "puppet/event-loop/better-definers" + +module SignalEmitterModule + def self.extended (object) + if object.kind_of? Module and not object < SignalEmitter + if object.respond_to? :fcall + # This is the way to call private methods + # in Ruby 1.9 as of November 16. + object.fcall :include, SignalEmitter + else + object.__send__ :include, SignalEmitter + end + end + end + + def define_signal (name, slot=:before, &body) + # Can't use `define_method' and take a block pre-1.9. + class_eval %{ def on_#{name} &block + add_signal_handler(:#{name}, &block) end } + define_signal_handler(name, :before, &lambda {|*a|}) + define_signal_handler(name, :after, &lambda {|*a|}) + define_signal_handler(name, slot, &body) if block_given? + end + + def define_signals (*names, &body) + names.each { |x| define_signal(x, &body) } + end + + def define_signal_handler (name, slot=:before, &body) + case slot + when :before + define_protected_method "handle_#{name}", &body + when :after + define_protected_method "after_handle_#{name}", &body + else + raise ArgumentError, "invalid slot `#{slot.inspect}'; " + + "should be `:before' or `:after'", caller(1) + end + end +end + +# This is an old name for the same thing. +SignalEmitterClass = SignalEmitterModule + +module SignalEmitter + def self.included (includer) + if not includer.kind_of? SignalEmitterClass + includer.extend SignalEmitterClass + end + end + + def __maybe_initialize_signal_emitter + @signal_handlers ||= Hash.new { |h, k| h[k] = Array.new } + @allow_dynamic_signals ||= false + end + + define_accessors :allow_dynamic_signals? + + def add_signal_handler (name, &handler) + __maybe_initialize_signal_emitter + @signal_handlers[name] << handler + return handler + end + + define_soft_aliases [:on, :on_signal] => :add_signal_handler + + def remove_signal_handler (name, handler) + __maybe_initialize_signal_emitter + @signal_handlers[name].delete(handler) + end + + def __signal__ (name, *args, &block) + __maybe_initialize_signal_emitter + respond_to? "on_#{name}" or allow_dynamic_signals? or + fail "undefined signal `#{name}' for #{self}:#{self.class}" + __send__("handle_#{name}", *args, &block) if + respond_to? "handle_#{name}" + @signal_handlers[name].each { |x| x.call(*args, &block) } + __send__("after_handle_#{name}", *args, &block) if + respond_to? "after_handle_#{name}" + end + + define_soft_alias :signal => :__signal__ +end + +# This module is indended to be a convenience mixin to be used by +# classes whose objects need to observe foreign signals. That is, +# if you want to observe some signals coming from an object, *you* +# should mix in this module. +# +# You cannot use this module at two different places of the same +# inheritance chain to observe signals coming from the same object. +# +# XXX: This has not seen much use, and I'd like to provide a +# better solution for the problem in the future. +module SignalObserver + def __maybe_initialize_signal_observer + @observed_signals ||= Hash.new do |signals, object| + signals[object] = Hash.new do |handlers, name| + handlers[name] = Array.new + end + end + end + + def observe_signal (subject, name, &handler) + __maybe_initialize_signal_observer + @observed_signals[subject][name] << handler + subject.add_signal_handler(name, &handler) + end + + def map_signals (source, pairs={}) + pairs.each do |src_name, dst_name| + observe_signal(source, src_name) do |*args| + __signal__(dst_name, *args) + end + end + end + + def absorb_signals (subject, *names) + names.each do |name| + observe_signal(subject, name) do |*args| + __signal__(name, *args) + end + end + end + + define_soft_aliases \ + :map_signal => :map_signals, + :absorb_signal => :absorb_signals + + def ignore_signal (subject, name) + __maybe_initialize_signal_observer + __ignore_signal_1(subject, name) + @observed_signals.delete(subject) if + @observed_signals[subject].empty? + end + + def ignore_signals (subject, *names) + __maybe_initialize_signal_observer + names = @observed_signals[subject] if names.empty? + names.each { |x| __ignore_signal_1(subject, x) } + end + + private + + def __ignore_signal_1(subject, name) + @observed_signals[subject][name].each do |handler| + subject.remove_signal_handler(name, handler) end + @observed_signals[subject].delete(name) + end +end + +if __FILE__ == $0 + require "test/unit" + class SignalEmitterTest < Test::Unit::TestCase + class X + include SignalEmitter + define_signal :foo + end + + def setup + @x = X.new + end + + def test_on_signal + moomin = 0 + @x.on_signal(:foo) { moomin = 1 } + @x.signal :foo + assert moomin == 1 + end + + def test_on_foo + moomin = 0 + @x.on_foo { moomin = 1 } + @x.signal :foo + assert moomin == 1 + end + + def test_multiple_on_signal + moomin = 0 + @x.on_signal(:foo) { moomin += 1 } + @x.on_signal(:foo) { moomin += 2 } + @x.on_signal(:foo) { moomin += 4 } + @x.on_signal(:foo) { moomin += 8 } + @x.signal :foo + assert moomin == 15 + end + + def test_multiple_on_foo + moomin = 0 + @x.on_foo { moomin += 1 } + @x.on_foo { moomin += 2 } + @x.on_foo { moomin += 4 } + @x.on_foo { moomin += 8 } + @x.signal :foo + assert moomin == 15 + end + end +end + +## application-signals.rb ends here. diff --git a/lib/puppet/filetype.rb b/lib/puppet/filetype.rb index dfca58511..c86ed9c07 100755 --- a/lib/puppet/filetype.rb +++ b/lib/puppet/filetype.rb @@ -174,7 +174,7 @@ module Puppet # Remove a specific @path's cron tab. def remove - Puppet.asuser(@path) { + Puppet::Util.asuser(@path) { %x{crontab -r 2>/dev/null} } end @@ -182,7 +182,7 @@ module Puppet # Overwrite a specific @path's cron tab; must be passed the @path name # and the text with which to create the cron tab. def write(text) - Puppet.asuser(@path) { + Puppet::Util.asuser(@path) { IO.popen("crontab", "w") { |p| p.print text } diff --git a/lib/puppet/networkclient.rb b/lib/puppet/networkclient.rb new file mode 100644 index 000000000..31bc9a9dd --- /dev/null +++ b/lib/puppet/networkclient.rb @@ -0,0 +1,141 @@ +require 'puppet' +require 'puppet/sslcertificates' +require 'puppet/type' +require 'facter' +require 'openssl' +require 'puppet/transaction' +require 'puppet/transportable' +require 'puppet/metric' +require 'puppet/daemon' +require 'puppet/server' +require 'puppet/base64' + +$noclientnetworking = false +begin + require 'webrick' + require 'cgi' + require 'xmlrpc/client' + require 'xmlrpc/server' + require 'yaml' +rescue LoadError => detail + $noclientnetworking = detail + raise Puppet::Error, "You must have the Ruby XMLRPC, CGI, and Webrick libraries installed" +end + +module Puppet + class NetworkClientError < RuntimeError; end + class ClientError < RuntimeError; end + #--------------------------------------------------------------- + if $noclientnetworking + Puppet.err "Could not load client network libs: %s" % $noclientnetworking + else + class NetworkClient < XMLRPC::Client + #include Puppet::Daemon + + # add the methods associated with each namespace + Puppet::Server::Handler.each { |handler| + interface = handler.interface + namespace = interface.prefix + + interface.methods.each { |ary| + method = ary[0] + Puppet.info "Defining %s.%s" % [namespace, method] + self.send(:define_method,method) { |*args| + #Puppet.info "Calling %s" % 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 + #Puppet.err "Could not call %s.%s: Untrusted certificates" % + # [namespace, method] + raise NetworkClientError, + "Certificates were not trusted" + 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] + #Puppet.err msg + raise NetworkClientError, msg + rescue SocketError => detail + Puppet.err "Could not find server %s" % @puppetserver + exit(12) + rescue => detail + Puppet.err "Could not call %s.%s: %s" % + [namespace, method, detail.inspect] + #raise NetworkClientError.new(detail.to_s) + raise + end + } + } + } + + def ca_file=(cafile) + @http.ca_file = cafile + store = OpenSSL::X509::Store.new + cacert = OpenSSL::X509::Certificate.new( + File.read(cafile) + ) + store.add_cert(cacert) + 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] + + @puppetserver = hash[:Server] + + super( + hash[:Server], + hash[:Path], + hash[:Port], + nil, # proxy_host + nil, # proxy_port + nil, # user + nil, # password + true # use_ssl + ) + + if hash[:Certificate] + self.cert = hash[:Certificate] + else + Puppet.err "No certificate; running with reduced functionality." + 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 + end + end +end + +# $Id$ diff --git a/lib/puppet/parameter.rb b/lib/puppet/parameter.rb index dfb259909..df6fa8b4d 100644 --- a/lib/puppet/parameter.rb +++ b/lib/puppet/parameter.rb @@ -226,10 +226,10 @@ module Puppet ) end - # each parameter class must define the name() method, and parameter instances - # do not change that name - # this implicitly means that a given object can only have one parameter - # instance of a given parameter class + # each parameter class must define the name() method, and parameter + # instances do not change that name this implicitly means that a given + # object can only have one parameter instance of a given parameter + # class def name return self.class.name end @@ -325,10 +325,6 @@ module Puppet end end - def name - self.class.name - end - def to_s s = "Parameter(%s)" % self.name end diff --git a/lib/puppet/sslcertificates.rb b/lib/puppet/sslcertificates.rb index 6c21f7c49..0c6322bcf 100755 --- a/lib/puppet/sslcertificates.rb +++ b/lib/puppet/sslcertificates.rb @@ -8,8 +8,7 @@ rescue LoadError raise Puppet::Error, "You must have the Ruby openssl library installed" end -module Puppet -module SSLCertificates +module Puppet::SSLCertificates def self.mkdir(dir) # this is all a bunch of stupid hackery unless FileTest.exists?(dir) @@ -165,575 +164,8 @@ module SSLCertificates return hashpath end - - - - class CA - attr_accessor :keyfile, :file, :config, :dir, :cert - - @@params = [ - :certdir, - :publickeydir, - :privatekeydir, - :cadir, - :cakey, - :cacert, - :capass, - :capub, - :csrdir, - :signeddir, - :serial, - :privatedir, - :ca_crl_days, - :ca_days, - :ca_md, - :req_bits, - :keylength, - :autosign - ] - - @@defaults = { - :certdir => [:ssldir, "certs"], - :publickeydir => [:ssldir, "public_keys"], - :privatekeydir => [:ssldir, "private_keys"], - :cadir => [:ssldir, "ca"], - :cacert => [:cadir, "ca_crt.pem"], - :cakey => [:cadir, "ca_key.pem"], - :capub => [:cadir, "ca_pub.pem"], - :csrdir => [:cadir, "requests"], - :signeddir => [:cadir, "signed"], - :capass => [:cadir, "ca.pass"], - :serial => [:cadir, "serial"], - :privatedir => [:ssldir, "private"], - :passfile => [:privatedir, "password"], - :autosign => [:puppetconf, "autosign.conf"], - :ca_crl_days => 365, - :ca_days => 1825, - :ca_md => "md5", - :req_bits => 2048, - :keylength => 1024, - } - - @@params.each { |param| - Puppet.setdefault(param,@@defaults[param]) - } - - def certfile - @config[:cacert] - end - - def host2csrfile(hostname) - File.join(Puppet[:csrdir], [hostname, "pem"].join(".")) - end - - # this stores signed certs in a directory unrelated to - # normal client certs - def host2certfile(hostname) - File.join(Puppet[:signeddir], [hostname, "pem"].join(".")) - end - - def thing2name(thing) - thing.subject.to_a.find { |ary| - ary[0] == "CN" - }[1] - end - - def initialize(hash = {}) - self.setconfig(hash) - - self.getcert - unless FileTest.exists?(@config[:serial]) - File.open(@config[:serial], "w") { |f| - f << "%04X" % 1 - } - end - - if Puppet[:capass] and ! FileTest.exists?(Puppet[:capass]) - self.genpass - end - end - - def genpass - pass = "" - 20.times { pass += (rand(74) + 48).chr } - - unless @config[:capass] - raise "No passfile" - end - Puppet::SSLCertificates.mkdir(File.dirname(@config[:capass])) - File.open(@config[:capass], "w", 0600) { |f| f.print pass } - return pass - end - - def getcert - if FileTest.exists?(@config[:cacert]) - @cert = OpenSSL::X509::Certificate.new( - File.read(@config[:cacert]) - ) - else - self.mkrootcert - end - end - - def getclientcsr(host) - csrfile = host2csrfile(host) - unless File.exists?(csrfile) - return nil - end - - return OpenSSL::X509::Request.new(File.read(csrfile)) - end - - def getclientcert(host) - certfile = host2certfile(host) - unless File.exists?(certfile) - return [nil, nil] - end - - return [OpenSSL::X509::Certificate.new(File.read(certfile)), @cert] - end - - def list - return Dir.entries(Puppet[:csrdir]).reject { |file| - file =~ /^\.+$/ - }.collect { |file| - file.sub(/\.pem$/, '') - } - end - - def mkrootcert - cert = Certificate.new( - :name => "CAcert", - :cert => @config[:cacert], - :encrypt => @config[:passfile], - :key => @config[:cakey], - :selfsign => true, - :length => 1825, - :type => :ca - ) - @cert = cert.mkselfsigned - File.open(@config[:cacert], "w", 0660) { |f| - f.puts @cert.to_pem - } - @key = cert.key - return cert - end - - def removeclientcsr(host) - csrfile = host2csrfile(host) - unless File.exists?(csrfile) - raise Puppet::Error, "No certificate request for %s" % host - end - - File.unlink(csrfile) - end - - def setconfig(hash) - @config = {} - @@params.each { |param| - if hash.include?(param) - begin - @config[param] = hash[param] - Puppet[param] = hash[param] - hash.delete(param) - rescue => detail - puts detail - exit - end - else - begin - @config[param] = Puppet[param] - rescue => detail - puts detail - exit - end - end - } - - if hash.include?(:password) - @config[:password] = hash[:password] - hash.delete(:password) - end - - if hash.length > 0 - raise ArgumentError, "Unknown parameters %s" % hash.keys.join(",") - end - - [:cadir, :csrdir, :signeddir].each { |dir| - unless @config[dir] - raise "%s is undefined" % dir - end - unless FileTest.exists?(@config[dir]) - Puppet::SSLCertificates.mkdir(@config[dir]) - end - } - end - - def sign(csr) - unless csr.is_a?(OpenSSL::X509::Request) - raise Puppet::Error, - "CA#sign only accepts OpenSSL::X509::Request objects, not %s" % - csr.class - end - - unless csr.verify(csr.public_key) - raise Puppet::Error, "CSR sign verification failed" - end - - # i should probably check key length... - - # read the ca cert in - cacert = OpenSSL::X509::Certificate.new( - File.read(@config[:cacert]) - ) - - cakey = nil - if @config[:password] - cakey = OpenSSL::PKey::RSA.new( - File.read(@config[:cakey]), @config[:password] - ) - else - cakey = OpenSSL::PKey::RSA.new( - File.read(@config[:cakey]) - ) - end - - unless cacert.check_private_key(cakey) - raise Puppet::Error, "CA Certificate is invalid" - end - - serial = File.read(@config[:serial]).chomp.hex - newcert = SSLCertificates.mkcert( - :type => :server, - :name => csr.subject, - :days => @config[:ca_days], - :issuer => cacert, - :serial => serial, - :publickey => csr.public_key - ) - - # increment the serial - File.open(@config[:serial], "w") { |f| - f << "%04X" % (serial + 1) - } - - newcert.sign(cakey, OpenSSL::Digest::SHA1.new) - - self.storeclientcert(newcert) - - return [newcert, cacert] - end - - def storeclientcsr(csr) - host = thing2name(csr) - - csrfile = host2csrfile(host) - if File.exists?(csrfile) - raise Puppet::Error, "Certificate request for %s already exists" % host - end - - File.open(csrfile, "w", 0660) { |f| - f.print csr.to_pem - } - end - - def storeclientcert(cert) - host = thing2name(cert) - - certfile = host2certfile(host) - if File.exists?(certfile) - Puppet.notice "Overwriting signed certificate %s for %s" % - [certfile, host] - end - - File.open(certfile, "w", 0660) { |f| - f.print cert.to_pem - } - end - - end - - class Certificate - attr_accessor :certfile, :keyfile, :name, :dir, :hash, :type - attr_accessor :key, :cert, :csr, :cacert - - @@params2names = { - :name => "CN", - :state => "ST", - :country => "C", - :email => "emailAddress", - :org => "O", - :city => "L", - :ou => "OU" - } - - def certname - OpenSSL::X509::Name.new self.subject - end - - def delete - [@certfile,@keyfile].each { |file| - if FileTest.exists?(file) - File.unlink(file) - end - } - - if defined? @hash and @hash - if FileTest.symlink?(@hash) - File.unlink(@hash) - end - end - end - - def exists? - return FileTest.exists?(@certfile) - end - - def getkey - unless FileTest.exists?(@keyfile) - self.mkkey() - end - if @password - @key = OpenSSL::PKey::RSA.new( - File.read(@keyfile), - @password - ) - else - @key = OpenSSL::PKey::RSA.new( - File.read(@keyfile) - ) - end - end - - def initialize(hash) - unless hash.include?(:name) - raise "You must specify the common name for the certificate" - end - @name = hash[:name] - - # init a few variables - @cert = @key = @csr = nil - - if hash.include?(:cert) - @certfile = hash[:cert] - @dir = File.dirname(@certfile) - else - @dir = hash[:dir] || Puppet[:certdir] - @certfile = File.join(@dir, @name) - end - - @cacertfile ||= File.join(Puppet[:certdir], "ca.pem") - - unless FileTest.directory?(@dir) - Puppet::SSLCertificates.mkdir(@dir) - end - - unless @certfile =~ /\.pem$/ - @certfile += ".pem" - end - @keyfile = hash[:key] || File.join( - Puppet[:privatekeydir], [@name,"pem"].join(".") - ) - unless FileTest.directory?(@dir) - Puppet::SSLCertificates.mkdir(@dir) - end - - [@keyfile].each { |file| - dir = File.dirname(file) - - unless FileTest.directory?(dir) - Puppet::SSLCertificates.mkdir(dir) - end - } - - @days = hash[:length] || 365 - @selfsign = hash[:selfsign] || false - @encrypt = hash[:encrypt] || false - @replace = hash[:replace] || false - @issuer = hash[:issuer] || nil - - if hash.include?(:type) - case hash[:type] - when :ca, :client, :server: @type = hash[:type] - else - raise "Invalid Cert type %s" % hash[:type] - end - else - @type = :client - end - - @params = {:name => @name} - [:state, :country, :email, :org, :ou].each { |param| - if hash.include?(param) - @params[param] = hash[param] - end - } - - if @encrypt - if @encrypt =~ /^\// - File.open(@encrypt) { |f| - @password = f.read.chomp - } - else - raise ":encrypt must be a path to a pass phrase file" - end - else - @password = nil - end - - if hash.include?(:selfsign) - @selfsign = hash[:selfsign] - else - @selfsign = false - end - end - - # this only works for servers, not for users - def mkcsr - unless defined? @key and @key - self.getkey - end - - name = OpenSSL::X509::Name.new self.subject - - @csr = OpenSSL::X509::Request.new - @csr.version = 0 - @csr.subject = name - @csr.public_key = @key.public_key - @csr.sign(@key, OpenSSL::Digest::SHA1.new) - - #File.open(@csrfile, "w") { |f| - # f << @csr.to_pem - #} - - unless @csr.verify(@key.public_key) - raise Puppet::Error, "CSR sign verification failed" - end - - return @csr - end - - def mkkey - # @key is the file - - @key = OpenSSL::PKey::RSA.new(1024) -# { |p,n| -# case p -# when 0; Puppet.info "key info: ." # BN_generate_prime -# when 1; Puppet.info "key info: +" # BN_generate_prime -# when 2; Puppet.info "key info: *" # searching good prime, -# # n = #of try, -# # but also data from BN_generate_prime -# when 3; Puppet.info "key info: \n" # found good prime, n==0 - p, n==1 - q, -# # but also data from BN_generate_prime -# else; Puppet.info "key info: *" # BN_generate_prime -# end -# } - - if @password - #passwdproc = proc { @password } - keytext = @key.export( - OpenSSL::Cipher::DES.new(:EDE3, :CBC), - @password - ) - File.open(@keyfile, "w", 0400) { |f| - f << keytext - } - else - File.open(@keyfile, "w", 0400) { |f| - f << @key.to_pem - } - end - - #cmd = "#{ossl} genrsa -out #{@key} 1024" - end - - def mkselfsigned - unless defined? @key and @key - self.getkey - end - - if defined? @cert and @cert - raise Puppet::Error, "Cannot replace existing certificate" - end - - args = { - :name => self.certname, - :days => @days, - :issuer => nil, - :serial => 0x0, - :publickey => @key.public_key - } - if @type - args[:type] = @type - else - args[:type] = :server - end - @cert = SSLCertificates.mkcert(args) - - @cert.sign(@key, OpenSSL::Digest::SHA1.new) if @selfsign - - return @cert - end - - def subject(string = false) - subj = @@params2names.collect { |param, name| - if @params.include?(param) - [name, @params[param]] - end - }.reject { |ary| ary.nil? } - - if string - return "/" + subj.collect { |ary| - "%s=%s" % ary - }.join("/") + "/" - else - return subj - end - end - - # verify that we can track down the cert chain or whatever - def verify - "openssl verify -verbose -CAfile /home/luke/.puppet/ssl/certs/ca.pem -purpose sslserver culain.madstop.com.pem" - end - - def write - files = { - @certfile => @cert, - @keyfile => @key, - } - if defined? @cacert - files[@cacertfile] = @cacert - end - - files.each { |file,thing| - if defined? thing and thing - if FileTest.exists?(file) - next - end - - text = nil - - if thing.is_a?(OpenSSL::PKey::RSA) and @password - text = thing.export( - OpenSSL::Cipher::DES.new(:EDE3, :CBC), - @password - ) - else - text = thing.to_pem - end - - File.open(file, "w", 0660) { |f| f.print text } - end - } - - if defined? @cacert - SSLCertificates.mkhash(Puppet[:certdir], @cacert, @cacertfile) - end - end - end -end + require 'puppet/sslcertificates/certificate' + require 'puppet/sslcertificates/ca' end -# # $Id$ diff --git a/lib/puppet/sslcertificates/ca.rb b/lib/puppet/sslcertificates/ca.rb new file mode 100644 index 000000000..0137e15eb --- /dev/null +++ b/lib/puppet/sslcertificates/ca.rb @@ -0,0 +1,286 @@ +class Puppet::SSLCertificates::CA + Certificate = Puppet::SSLCertificates::Certificate + attr_accessor :keyfile, :file, :config, :dir, :cert + + @@params = [ + :certdir, + :publickeydir, + :privatekeydir, + :cadir, + :cakey, + :cacert, + :capass, + :capub, + :csrdir, + :signeddir, + :serial, + :privatedir, + :ca_crl_days, + :ca_days, + :ca_md, + :req_bits, + :keylength, + :autosign + ] + + @@defaults = { + :certdir => [:ssldir, "certs"], + :publickeydir => [:ssldir, "public_keys"], + :privatekeydir => [:ssldir, "private_keys"], + :cadir => [:ssldir, "ca"], + :cacert => [:cadir, "ca_crt.pem"], + :cakey => [:cadir, "ca_key.pem"], + :capub => [:cadir, "ca_pub.pem"], + :csrdir => [:cadir, "requests"], + :signeddir => [:cadir, "signed"], + :capass => [:cadir, "ca.pass"], + :serial => [:cadir, "serial"], + :privatedir => [:ssldir, "private"], + :passfile => [:privatedir, "password"], + :autosign => [:puppetconf, "autosign.conf"], + :ca_crl_days => 365, + :ca_days => 1825, + :ca_md => "md5", + :req_bits => 2048, + :keylength => 1024, + } + + @@params.each { |param| + Puppet.setdefault(param,@@defaults[param]) + } + + def certfile + @config[:cacert] + end + + def host2csrfile(hostname) + File.join(Puppet[:csrdir], [hostname, "pem"].join(".")) + end + + # this stores signed certs in a directory unrelated to + # normal client certs + def host2certfile(hostname) + File.join(Puppet[:signeddir], [hostname, "pem"].join(".")) + end + + def thing2name(thing) + thing.subject.to_a.find { |ary| + ary[0] == "CN" + }[1] + end + + def initialize(hash = {}) + self.setconfig(hash) + + self.getcert + unless FileTest.exists?(@config[:serial]) + File.open(@config[:serial], "w") { |f| + f << "%04X" % 1 + } + end + + if Puppet[:capass] and ! FileTest.exists?(Puppet[:capass]) + self.genpass + end + end + + def genpass + pass = "" + 20.times { pass += (rand(74) + 48).chr } + + unless @config[:capass] + raise "No passfile" + end + Puppet::SSLCertificates.mkdir(File.dirname(@config[:capass])) + File.open(@config[:capass], "w", 0600) { |f| f.print pass } + return pass + end + + def getcert + if FileTest.exists?(@config[:cacert]) + @cert = OpenSSL::X509::Certificate.new( + File.read(@config[:cacert]) + ) + else + self.mkrootcert + end + end + + def getclientcsr(host) + csrfile = host2csrfile(host) + unless File.exists?(csrfile) + return nil + end + + return OpenSSL::X509::Request.new(File.read(csrfile)) + end + + def getclientcert(host) + certfile = host2certfile(host) + unless File.exists?(certfile) + return [nil, nil] + end + + return [OpenSSL::X509::Certificate.new(File.read(certfile)), @cert] + end + + def list + return Dir.entries(Puppet[:csrdir]).reject { |file| + file =~ /^\.+$/ + }.collect { |file| + file.sub(/\.pem$/, '') + } + end + + def mkrootcert + cert = Certificate.new( + :name => "CAcert", + :cert => @config[:cacert], + :encrypt => @config[:passfile], + :key => @config[:cakey], + :selfsign => true, + :length => 1825, + :type => :ca + ) + @cert = cert.mkselfsigned + File.open(@config[:cacert], "w", 0660) { |f| + f.puts @cert.to_pem + } + @key = cert.key + return cert + end + + def removeclientcsr(host) + csrfile = host2csrfile(host) + unless File.exists?(csrfile) + raise Puppet::Error, "No certificate request for %s" % host + end + + File.unlink(csrfile) + end + + def setconfig(hash) + @config = {} + @@params.each { |param| + if hash.include?(param) + begin + @config[param] = hash[param] + Puppet[param] = hash[param] + hash.delete(param) + rescue => detail + puts detail + exit + end + else + begin + @config[param] = Puppet[param] + rescue => detail + puts detail + exit + end + end + } + + if hash.include?(:password) + @config[:password] = hash[:password] + hash.delete(:password) + end + + if hash.length > 0 + raise ArgumentError, "Unknown parameters %s" % hash.keys.join(",") + end + + [:cadir, :csrdir, :signeddir].each { |dir| + unless @config[dir] + raise "%s is undefined" % dir + end + unless FileTest.exists?(@config[dir]) + Puppet::SSLCertificates.mkdir(@config[dir]) + end + } + end + + def sign(csr) + unless csr.is_a?(OpenSSL::X509::Request) + raise Puppet::Error, + "CA#sign only accepts OpenSSL::X509::Request objects, not %s" % + csr.class + end + + unless csr.verify(csr.public_key) + raise Puppet::Error, "CSR sign verification failed" + end + + # i should probably check key length... + + # read the ca cert in + cacert = OpenSSL::X509::Certificate.new( + File.read(@config[:cacert]) + ) + + cakey = nil + if @config[:password] + cakey = OpenSSL::PKey::RSA.new( + File.read(@config[:cakey]), @config[:password] + ) + else + cakey = OpenSSL::PKey::RSA.new( + File.read(@config[:cakey]) + ) + end + + unless cacert.check_private_key(cakey) + raise Puppet::Error, "CA Certificate is invalid" + end + + serial = File.read(@config[:serial]).chomp.hex + newcert = Puppet::SSLCertificates.mkcert( + :type => :server, + :name => csr.subject, + :days => @config[:ca_days], + :issuer => cacert, + :serial => serial, + :publickey => csr.public_key + ) + + # increment the serial + File.open(@config[:serial], "w") { |f| + f << "%04X" % (serial + 1) + } + + newcert.sign(cakey, OpenSSL::Digest::SHA1.new) + + self.storeclientcert(newcert) + + return [newcert, cacert] + end + + def storeclientcsr(csr) + host = thing2name(csr) + + csrfile = host2csrfile(host) + if File.exists?(csrfile) + raise Puppet::Error, "Certificate request for %s already exists" % host + end + + File.open(csrfile, "w", 0660) { |f| + f.print csr.to_pem + } + end + + def storeclientcert(cert) + host = thing2name(cert) + + certfile = host2certfile(host) + if File.exists?(certfile) + Puppet.notice "Overwriting signed certificate %s for %s" % + [certfile, host] + end + + File.open(certfile, "w", 0660) { |f| + f.print cert.to_pem + } + end +end + +# $Id$ diff --git a/lib/puppet/sslcertificates/certificate.rb b/lib/puppet/sslcertificates/certificate.rb new file mode 100644 index 000000000..65ceb44b9 --- /dev/null +++ b/lib/puppet/sslcertificates/certificate.rb @@ -0,0 +1,283 @@ +class Puppet::SSLCertificates::Certificate + SSLCertificates = Puppet::SSLCertificates + + attr_accessor :certfile, :keyfile, :name, :dir, :hash, :type + attr_accessor :key, :cert, :csr, :cacert + + @@params2names = { + :name => "CN", + :state => "ST", + :country => "C", + :email => "emailAddress", + :org => "O", + :city => "L", + :ou => "OU" + } + + def certname + OpenSSL::X509::Name.new self.subject + end + + def delete + [@certfile,@keyfile].each { |file| + if FileTest.exists?(file) + File.unlink(file) + end + } + + if defined? @hash and @hash + if FileTest.symlink?(@hash) + File.unlink(@hash) + end + end + end + + def exists? + return FileTest.exists?(@certfile) + end + + def getkey + unless FileTest.exists?(@keyfile) + self.mkkey() + end + if @password + @key = OpenSSL::PKey::RSA.new( + File.read(@keyfile), + @password + ) + else + @key = OpenSSL::PKey::RSA.new( + File.read(@keyfile) + ) + end + end + + def initialize(hash) + unless hash.include?(:name) + raise "You must specify the common name for the certificate" + end + @name = hash[:name] + + # init a few variables + @cert = @key = @csr = nil + + if hash.include?(:cert) + @certfile = hash[:cert] + @dir = File.dirname(@certfile) + else + @dir = hash[:dir] || Puppet[:certdir] + @certfile = File.join(@dir, @name) + end + + @cacertfile ||= File.join(Puppet[:certdir], "ca.pem") + + unless FileTest.directory?(@dir) + Puppet::SSLCertificates.mkdir(@dir) + end + + unless @certfile =~ /\.pem$/ + @certfile += ".pem" + end + @keyfile = hash[:key] || File.join( + Puppet[:privatekeydir], [@name,"pem"].join(".") + ) + unless FileTest.directory?(@dir) + Puppet::SSLCertificates.mkdir(@dir) + end + + [@keyfile].each { |file| + dir = File.dirname(file) + + unless FileTest.directory?(dir) + Puppet::SSLCertificates.mkdir(dir) + end + } + + @days = hash[:length] || 365 + @selfsign = hash[:selfsign] || false + @encrypt = hash[:encrypt] || false + @replace = hash[:replace] || false + @issuer = hash[:issuer] || nil + + if hash.include?(:type) + case hash[:type] + when :ca, :client, :server: @type = hash[:type] + else + raise "Invalid Cert type %s" % hash[:type] + end + else + @type = :client + end + + @params = {:name => @name} + [:state, :country, :email, :org, :ou].each { |param| + if hash.include?(param) + @params[param] = hash[param] + end + } + + if @encrypt + if @encrypt =~ /^\// + File.open(@encrypt) { |f| + @password = f.read.chomp + } + else + raise ":encrypt must be a path to a pass phrase file" + end + else + @password = nil + end + + if hash.include?(:selfsign) + @selfsign = hash[:selfsign] + else + @selfsign = false + end + end + + # this only works for servers, not for users + def mkcsr + unless defined? @key and @key + self.getkey + end + + name = OpenSSL::X509::Name.new self.subject + + @csr = OpenSSL::X509::Request.new + @csr.version = 0 + @csr.subject = name + @csr.public_key = @key.public_key + @csr.sign(@key, OpenSSL::Digest::SHA1.new) + + #File.open(@csrfile, "w") { |f| + # f << @csr.to_pem + #} + + unless @csr.verify(@key.public_key) + raise Puppet::Error, "CSR sign verification failed" + end + + return @csr + end + + def mkkey + # @key is the file + + @key = OpenSSL::PKey::RSA.new(1024) +# { |p,n| +# case p +# when 0; Puppet.info "key info: ." # BN_generate_prime +# when 1; Puppet.info "key info: +" # BN_generate_prime +# when 2; Puppet.info "key info: *" # searching good prime, +# # n = #of try, +# # but also data from BN_generate_prime +# when 3; Puppet.info "key info: \n" # found good prime, n==0 - p, n==1 - q, +# # but also data from BN_generate_prime +# else; Puppet.info "key info: *" # BN_generate_prime +# end +# } + + if @password + #passwdproc = proc { @password } + keytext = @key.export( + OpenSSL::Cipher::DES.new(:EDE3, :CBC), + @password + ) + File.open(@keyfile, "w", 0400) { |f| + f << keytext + } + else + File.open(@keyfile, "w", 0400) { |f| + f << @key.to_pem + } + end + + #cmd = "#{ossl} genrsa -out #{@key} 1024" + end + + def mkselfsigned + unless defined? @key and @key + self.getkey + end + + if defined? @cert and @cert + raise Puppet::Error, "Cannot replace existing certificate" + end + + args = { + :name => self.certname, + :days => @days, + :issuer => nil, + :serial => 0x0, + :publickey => @key.public_key + } + if @type + args[:type] = @type + else + args[:type] = :server + end + @cert = SSLCertificates.mkcert(args) + + @cert.sign(@key, OpenSSL::Digest::SHA1.new) if @selfsign + + return @cert + end + + def subject(string = false) + subj = @@params2names.collect { |param, name| + if @params.include?(param) + [name, @params[param]] + end + }.reject { |ary| ary.nil? } + + if string + return "/" + subj.collect { |ary| + "%s=%s" % ary + }.join("/") + "/" + else + return subj + end + end + + # verify that we can track down the cert chain or whatever + def verify + "openssl verify -verbose -CAfile /home/luke/.puppet/ssl/certs/ca.pem -purpose sslserver culain.madstop.com.pem" + end + + def write + files = { + @certfile => @cert, + @keyfile => @key, + } + if defined? @cacert + files[@cacertfile] = @cacert + end + + files.each { |file,thing| + if defined? thing and thing + if FileTest.exists?(file) + next + end + + text = nil + + if thing.is_a?(OpenSSL::PKey::RSA) and @password + text = thing.export( + OpenSSL::Cipher::DES.new(:EDE3, :CBC), + @password + ) + else + text = thing.to_pem + end + + File.open(file, "w", 0660) { |f| f.print text } + end + } + + if defined? @cacert + SSLCertificates.mkhash(Puppet[:certdir], @cacert, @cacertfile) + end + end +end + +# $Id$ diff --git a/lib/puppet/storage.rb b/lib/puppet/storage.rb index a12a94178..0c06f5173 100644 --- a/lib/puppet/storage.rb +++ b/lib/puppet/storage.rb @@ -52,8 +52,8 @@ module Puppet begin @@state = YAML.load(file) rescue => detail - Puppet.err "Checksumfile %s is corrupt; replacing" % - Puppet[:statefile] + Puppet.err "Checksumfile %s is corrupt (%s); replacing" % + [Puppet[:statefile], detail] begin File.rename(Puppet[:statefile], Puppet[:statefile] + ".bad") @@ -65,6 +65,11 @@ module Puppet end } + unless @@state.is_a?(Hash) + Puppet.err "State got corrupted" + self.init + end + #Puppet.debug "Loaded state is %s" % @@state.inspect end @@ -73,6 +78,7 @@ module Puppet end def self.store + Puppet.debug "Storing state" unless FileTest.directory?(File.dirname(Puppet[:statefile])) begin Puppet.recmkdir(File.dirname(Puppet[:statefile])) @@ -89,7 +95,7 @@ module Puppet end Puppet::Util.lock( - Puppet[:statefile], File::CREAT|File::WRONLY, 0600 + Puppet[:statefile], "w", 0600 ) { |file| file.print YAML.dump(@@state) } diff --git a/lib/puppet/transaction.rb b/lib/puppet/transaction.rb index 39aa27858..8caa28588 100644 --- a/lib/puppet/transaction.rb +++ b/lib/puppet/transaction.rb @@ -121,9 +121,9 @@ class Transaction # these children are all Puppet::Type instances # not all of the children will return a change, and Containers # return transactions - ary = child.evaluate - child.cache(:checked, now) - ary + #ary = child.evaluate + #ary + child.evaluate }.flatten.reject { |child| child.nil? # remove empties } diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb index 5c9bcf027..424646742 100644 --- a/lib/puppet/type.rb +++ b/lib/puppet/type.rb @@ -1289,31 +1289,33 @@ class Type < Puppet::Element # If we've already set the schedule, then just move on return if self[:schedule].is_a?(Puppet.type(:schedule)) + return unless self[:schedule] + # Schedules don't need to be scheduled - return if self.is_a?(Puppet.type(:schedule)) + #return if self.is_a?(Puppet.type(:schedule)) # Nor do components - return if self.is_a?(Puppet.type(:component)) + #return if self.is_a?(Puppet.type(:component)) - if self[:schedule] - if sched = Puppet.type(:schedule)[self[:schedule]] - self[:schedule] = sched - else - self.fail "Could not find schedule %s" % self[:schedule] - end - elsif Puppet[:schedule] and ! Puppet[:ignoreschedules] - # We handle schedule defaults here mostly because otherwise things - # will behave very very erratically during testing. - if sched = Puppet.type(:schedule)[Puppet[:schedule]] - self[:schedule] = sched - else - self.fail "Could not find default schedule %s" % Puppet[:schedule] - end + if sched = Puppet.type(:schedule)[self[:schedule]] + self[:schedule] = sched else - # While it's unlikely we won't have any schedule (since there's a - # default), it's at least possible during testing - return true - end + self.fail "Could not find schedule %s" % self[:schedule] + end +# if self[:schedule] +# elsif Puppet[:schedule] and ! Puppet[:ignoreschedules] +# # We handle schedule defaults here mostly because otherwise things +# # will behave very very erratically during testing. +# if sched = Puppet.type(:schedule)[Puppet[:schedule]] +# self[:schedule] = sched +# else +# self.fail "Could not find default schedule %s" % Puppet[:schedule] +# end +# else +# # While it's unlikely we won't have any schedule (since there's a +# # default), it's at least possible during testing +# return true +# end end # Check whether we are scheduled to run right now or not. @@ -1533,6 +1535,8 @@ class Type < Puppet::Element # this returns any changes resulting from testing, thus 'collect' # rather than 'each' def evaluate + now = Time.now.to_i + #Puppet.err "Evaluating %s" % self.path.join(":") unless defined? @evalcount self.err "No evalcount defined on '%s' of type '%s'" % @@ -1576,7 +1580,9 @@ class Type < Puppet::Element #end changes << @children.collect { |child| - child.evaluate + ch = child.evaluate + child.cache(:checked, now) + ch } #unless self.class.depthfirst? # changes << self.collect { |child| @@ -1604,6 +1610,7 @@ class Type < Puppet::Element # self.debug "change: %s" % change.state.name #} end + self.cache(:checked, now) return changes.flatten end @@ -2057,6 +2064,7 @@ require 'puppet/type/group' require 'puppet/type/package' require 'puppet/type/pfile' require 'puppet/type/pfilebucket' +require 'puppet/type/schedule' require 'puppet/type/service' require 'puppet/type/symlink' require 'puppet/type/user' diff --git a/lib/puppet/type/package/rpm.rb b/lib/puppet/type/package/rpm.rb index cf84c3536..11906558e 100755 --- a/lib/puppet/type/package/rpm.rb +++ b/lib/puppet/type/package/rpm.rb @@ -3,13 +3,14 @@ module Puppet def query fields = { :name => "NAME", - :ensure => "VERSION", + :version => "VERSION", :description => "DESCRIPTION" } cmd = "rpm -q #{self.name} --qf '%s\n'" % "%{NAME} %{VERSION}-%{RELEASE}" + self.debug "Executing %s" % cmd.inspect # list out all of the packages output = %x{#{cmd} 2>/dev/null}.chomp @@ -22,7 +23,7 @@ module Puppet regex = %r{^(\S+)\s+(\S+)} #fields = [:name, :ensure, :description] - fields = [:name, :ensure] + fields = [:name, :version] hash = {} if match = regex.match(output) fields.zip(match.captures) { |field,value| @@ -34,6 +35,8 @@ module Puppet output end + hash[:ensure] = :present + return hash end diff --git a/lib/puppet/type/pfile.rb b/lib/puppet/type/pfile.rb index 12b162845..10ca20976 100644 --- a/lib/puppet/type/pfile.rb +++ b/lib/puppet/type/pfile.rb @@ -254,7 +254,7 @@ module Puppet # than this last bit, so it doesn't really make sense. if child = klass[path] unless @children.include?(child) - self.notice "Not managing more explicit file %s" % + self.debug "Not managing more explicit file %s" % path return nil end diff --git a/lib/puppet/type/schedule.rb b/lib/puppet/type/schedule.rb index 515ae0194..b3bcb9d1b 100755 --- a/lib/puppet/type/schedule.rb +++ b/lib/puppet/type/schedule.rb @@ -33,7 +33,7 @@ module Puppet Puppet automatically creates a schedule for each valid period with the same name as that period (e.g., hourly and daily). Additionally, a schedule named *puppet* is created and used as the default, - with the following attributes: + with the following attributes:: schedule { puppet: period => hourly, diff --git a/lib/puppet/type/service.rb b/lib/puppet/type/service.rb index fca9ff4d3..13aad3a00 100644 --- a/lib/puppet/type/service.rb +++ b/lib/puppet/type/service.rb @@ -77,7 +77,6 @@ module Puppet [self.class,should.inspect] should = 0 end - self.debug "Service should is %s" % should return should end diff --git a/lib/puppet/util.rb b/lib/puppet/util.rb index 650eb484a..9974d55e3 100644 --- a/lib/puppet/util.rb +++ b/lib/puppet/util.rb @@ -89,13 +89,29 @@ module Util def self.lock(*opts) lock = opts[0] + ".lock" while File.exists?(lock) + stamp = File.stat(lock).mtime.to_i + if Time.now.to_i - stamp > 5 + Puppet.notice "Lock file %s is %s seconds old; removing" + File.delete(lock) + end #Puppet.debug "%s is locked" % opts[0] sleep 0.1 end File.open(lock, "w") { |f| f.print " "; f.flush } + writing = false + if opts[1] == "w" + writing = true + tmp = opts[0] + ".tmp" + orig = opts[0] + opts[0] = tmp + end begin File.open(*opts) { |file| yield file } - rescue + if writing + File.rename(tmp, orig) + end + rescue => detail + Puppet.err "Storage error: %s" % detail raise ensure # I don't really understand how the lock file could disappear, diff --git a/test/executables/puppetd.rb b/test/executables/puppetd.rb index 68b9b5f08..90733ed19 100755 --- a/test/executables/puppetd.rb +++ b/test/executables/puppetd.rb @@ -25,6 +25,7 @@ class TestPuppetDExe < Test::Unit::TestCase cmd = "puppetd" cmd += " --verbose" + cmd += " --onetime" #cmd += " --fqdn %s" % fqdn cmd += " --port %s" % @@port cmd += " --confdir %s" % Puppet[:puppetconf] diff --git a/test/language/snippets.rb b/test/language/snippets.rb index 413fd1035..0e217bbbe 100755 --- a/test/language/snippets.rb +++ b/test/language/snippets.rb @@ -442,10 +442,15 @@ class TestSnippets < Test::Unit::TestCase Puppet::Type.eachtype { |type| type.each { |obj| - unless obj.name == "puppet[top]" + unless obj.name == "puppet[top]" or + obj.is_a?(Puppet.type(:schedule)) assert(obj.parent, "%s has no parent" % obj.name) end assert(obj.name) + + if obj.is_a?(Puppet.type(:file)) + @@tmpfiles << obj.name + end } } assert_nothing_raised { |