diff options
Diffstat (limited to 'lib/puppet')
61 files changed, 1764 insertions, 426 deletions
diff --git a/lib/puppet/defaults.rb b/lib/puppet/defaults.rb index 57299b7e7..d77ec0486 100644 --- a/lib/puppet/defaults.rb +++ b/lib/puppet/defaults.rb @@ -60,12 +60,6 @@ module Puppet this directory can be removed without causing harm (although it might result in spurious service restarts)." }, - :ssldir => { - :default => "$confdir/ssl", - :mode => 0771, - :owner => "root", - :desc => "Where SSL certificates are kept." - }, :rundir => { :default => rundir, :mode => 01777, @@ -148,7 +142,20 @@ module Puppet but then ship with tools that do not know how to handle signed ints, so the UIDs show up as huge numbers that can then not be fed back into the system. This is a hackish way to fail in a slightly more useful way when that happens."], - :node_terminus => ["plain", "Where to find information about nodes."] + :node_terminus => ["plain", "Where to find information about nodes."], + :httplog => { :default => "$logdir/http.log", + :owner => "root", + :mode => 0640, + :desc => "Where the puppetd web server logs." + }, + :http_proxy_host => ["none", + "The HTTP proxy host to use for outgoing connections. Note: You + may need to use a FQDN for the server hostname when using a proxy."], + :http_proxy_port => [3128, + "The HTTP proxy port to use for outgoing connections"], + :http_enable_post_connection_check => [true, + "Boolean; wheter or not puppetd should validate the server + SSL certificate against the request hostname."] ) hostname = Facter["hostname"].value @@ -159,14 +166,21 @@ module Puppet fqdn = hostname end - Puppet.setdefaults(:ssl, + Puppet.setdefaults(:main, :certname => [fqdn, "The name to use when handling certificates. Defaults to the fully qualified domain name."], :certdnsnames => ['', "The DNS names on the Server certificate as a colon-separated list. If it's anything other than an empty string, it will be used as an alias in the created certificate. By default, only the server gets an alias set up, and only for 'puppet'."], :certdir => ["$ssldir/certs", "The certificate directory."], + :ssldir => { + :default => "$confdir/ssl", + :mode => 0771, + :owner => "root", + :desc => "Where SSL certificates are kept." + }, :publickeydir => ["$ssldir/public_keys", "The public key directory."], + :requestdir => ["$ssldir/certificate_requests", "Where host certificate requests are stored."], :privatekeydir => { :default => "$ssldir/private_keys", :mode => 0750, :desc => "The private key directory." @@ -182,7 +196,7 @@ module Puppet }, :hostcsr => { :default => "$ssldir/csr_$certname.pem", :mode => 0644, - :desc => "Where individual hosts store and look for their certificates." + :desc => "Where individual hosts store and look for their certificate requests." }, :hostcert => { :default => "$certdir/$certname.pem", :mode => 0644, @@ -199,6 +213,11 @@ module Puppet :localcacert => { :default => "$certdir/ca.pem", :mode => 0644, :desc => "Where each client stores the CA certificate." + }, + :hostcrl => { :default => "$ssldir/crl.pem", + :mode => 0644, + :desc => "Where the host's certificate revocation list can be found. + This is distinct from the certificate authority's CRL." } ) @@ -230,7 +249,12 @@ module Puppet :owner => "$user", :group => "$group", :mode => 0664, - :desc => "The certificate revocation list (CRL) for the CA. Set this to 'false' if you do not want to use a CRL." + :desc => "The certificate revocation list (CRL) for the CA. Will be used if present but otherwise ignored.", + :hook => proc do |value| + if value == 'false' + Puppet.warning "Setting the :cacrl to 'false' is deprecated; Puppet will just ignore the crl if yours is missing" + end + end }, :caprivatedir => { :default => "$cadir/private", :owner => "$user", @@ -258,7 +282,7 @@ module Puppet :serial => { :default => "$cadir/serial", :owner => "$user", :group => "$group", - :mode => 0600, + :mode => 0644, :desc => "Where the serial number for certificates is stored." }, :autosign => { :default => "$confdir/autosign.conf", @@ -291,7 +315,7 @@ module Puppet self.setdefaults(self.settings[:name], :config => ["$confdir/puppet.conf", "The configuration file for #{Puppet[:name]}."], - :pidfile => ["", "The pid file"], + :pidfile => ["$rundir/$name.pid", "The pid file"], :bindaddress => ["", "The address to bind to. Mongrel servers default to 127.0.0.1 and WEBrick defaults to 0.0.0.0."], :servertype => ["webrick", "The type of server to use. Currently supported @@ -382,19 +406,6 @@ module Puppet :mode => 0640, :desc => "The log file for puppetd. This is generally not used." }, - :httplog => { :default => "$logdir/http.log", - :owner => "root", - :mode => 0640, - :desc => "Where the puppetd web server logs." - }, - :http_proxy_host => ["none", - "The HTTP proxy host to use for outgoing connections. Note: You - may need to use a FQDN for the server hostname when using a proxy."], - :http_proxy_port => [3128, - "The HTTP proxy port to use for outgoing connections"], - :http_enable_post_connection_check => [true, - "Boolean; wheter or not puppetd should validate the server - SSL certificate against the request hostname."], :server => ["puppet", "The server to which server puppetd should connect"], :ignoreschedules => [false, diff --git a/lib/puppet/executables/client/certhandler.rb b/lib/puppet/executables/client/certhandler.rb index b041397ae..b73642941 100644 --- a/lib/puppet/executables/client/certhandler.rb +++ b/lib/puppet/executables/client/certhandler.rb @@ -4,12 +4,14 @@ module Puppet module Client class CertHandler attr_writer :wait_for_cert, :one_time - attr_reader :new_cert + attr_reader :caclient, :new_cert def initialize(wait_time, is_one_time) @wait_for_cert = wait_time @one_time = is_one_time @new_cert = false + + @caclient = Puppet::Network::Client.ca.new() end # Did we just read a cert? @@ -31,8 +33,6 @@ module Puppet end def retrieve_cert - caclient = Puppet::Network::Client.ca.new() - while true do begin if caclient.request_cert @@ -54,11 +54,11 @@ module Puppet end def read_cert - Puppet::Network::HttpPool.read_cert + caclient.read_cert end def read_new_cert - if Puppet::Network::HttpPool.read_cert + if caclient.read_cert # If we read it in, then we need to get rid of our existing http connection. # The @new_cert flag will help us do that, in that it provides a way # to notify that the cert status has changed. diff --git a/lib/puppet/file_serving/configuration.rb b/lib/puppet/file_serving/configuration.rb index ccf0957d1..9c38aaa19 100644 --- a/lib/puppet/file_serving/configuration.rb +++ b/lib/puppet/file_serving/configuration.rb @@ -5,25 +5,20 @@ require 'puppet' require 'puppet/file_serving' require 'puppet/file_serving/mount' +require 'puppet/util/cacher' class Puppet::FileServing::Configuration require 'puppet/file_serving/configuration/parser' + extend Puppet::Util::Cacher + @config_fileuration = nil Mount = Puppet::FileServing::Mount - # Remove our singleton instance. - def self.clear_cache - @config_fileuration = nil - end - # Create our singleton configuration. def self.create - unless @config_fileuration - @config_fileuration = new() - end - @config_fileuration + attr_cache(:configuration) { new() } end private_class_method :new diff --git a/lib/puppet/file_serving/mount.rb b/lib/puppet/file_serving/mount.rb index 8e5bd03e8..c52cedbfb 100644 --- a/lib/puppet/file_serving/mount.rb +++ b/lib/puppet/file_serving/mount.rb @@ -4,6 +4,7 @@ require 'puppet/network/authstore' require 'puppet/util/logging' +require 'puppet/util/cacher' require 'puppet/file_serving' require 'puppet/file_serving/metadata' require 'puppet/file_serving/content' @@ -12,12 +13,16 @@ require 'puppet/file_serving/content' # or content objects. class Puppet::FileServing::Mount < Puppet::Network::AuthStore include Puppet::Util::Logging + extend Puppet::Util::Cacher - @@localmap = nil - - # Clear the cache. This is only ever used for testing. - def self.clear_cache - @@localmap = nil + def self.localmap + attr_cache(:localmap) { + { "h" => Facter.value("hostname"), + "H" => [Facter.value("hostname"), + Facter.value("domain")].join("."), + "d" => Facter.value("domain") + } + } end attr_reader :name @@ -173,14 +178,6 @@ class Puppet::FileServing::Mount < Puppet::Network::AuthStore # Cache this manufactured map, since if it's used it's likely # to get used a lot. def localmap - unless @@localmap - @@localmap = { - "h" => Facter.value("hostname"), - "H" => [Facter.value("hostname"), - Facter.value("domain")].join("."), - "d" => Facter.value("domain") - } - end - @@localmap + self.class.localmap end end diff --git a/lib/puppet/indirector/certificate/ca.rb b/lib/puppet/indirector/certificate/ca.rb new file mode 100644 index 000000000..b64080e16 --- /dev/null +++ b/lib/puppet/indirector/certificate/ca.rb @@ -0,0 +1,9 @@ +require 'puppet/indirector/ssl_file' +require 'puppet/ssl/certificate' + +class Puppet::SSL::Certificate::Ca < Puppet::Indirector::SslFile + desc "Manage the CA collection of signed SSL certificates on disk." + + store_in :signeddir + store_ca_at :cacert +end diff --git a/lib/puppet/indirector/certificate/file.rb b/lib/puppet/indirector/certificate/file.rb new file mode 100644 index 000000000..c19d001f4 --- /dev/null +++ b/lib/puppet/indirector/certificate/file.rb @@ -0,0 +1,9 @@ +require 'puppet/indirector/ssl_file' +require 'puppet/ssl/certificate' + +class Puppet::SSL::Certificate::File < Puppet::Indirector::SslFile + desc "Manage SSL certificates on disk." + + store_in :certdir + store_ca_at :localcacert +end diff --git a/lib/puppet/indirector/certificate/rest.rb b/lib/puppet/indirector/certificate/rest.rb new file mode 100644 index 000000000..f88d60d40 --- /dev/null +++ b/lib/puppet/indirector/certificate/rest.rb @@ -0,0 +1,6 @@ +require 'puppet/ssl/certificate' +require 'puppet/indirector/rest' + +class Puppet::SSL::Certificate::Rest < Puppet::Indirector::REST + desc "Find and save certificates over HTTP via REST." +end diff --git a/lib/puppet/indirector/certificate_request/ca.rb b/lib/puppet/indirector/certificate_request/ca.rb new file mode 100644 index 000000000..e90f43a03 --- /dev/null +++ b/lib/puppet/indirector/certificate_request/ca.rb @@ -0,0 +1,14 @@ +require 'puppet/indirector/ssl_file' +require 'puppet/ssl/certificate_request' + +class Puppet::SSL::CertificateRequest::Ca < Puppet::Indirector::SslFile + desc "Manage the CA collection of certificate requests on disk." + + store_in :csrdir + + def save(request) + result = super + Puppet.notice "%s has a waiting certificate request" % request.key + result + end +end diff --git a/lib/puppet/indirector/certificate_request/file.rb b/lib/puppet/indirector/certificate_request/file.rb new file mode 100644 index 000000000..274311e2c --- /dev/null +++ b/lib/puppet/indirector/certificate_request/file.rb @@ -0,0 +1,8 @@ +require 'puppet/indirector/ssl_file' +require 'puppet/ssl/certificate_request' + +class Puppet::SSL::CertificateRequest::File < Puppet::Indirector::SslFile + desc "Manage the collection of certificate requests on disk." + + store_in :requestdir +end diff --git a/lib/puppet/indirector/certificate_request/rest.rb b/lib/puppet/indirector/certificate_request/rest.rb new file mode 100644 index 000000000..6df014583 --- /dev/null +++ b/lib/puppet/indirector/certificate_request/rest.rb @@ -0,0 +1,6 @@ +require 'puppet/ssl/certificate_request' +require 'puppet/indirector/rest' + +class Puppet::SSL::CertificateRequest::Rest < Puppet::Indirector::REST + desc "Find and save certificate requests over HTTP via REST." +end diff --git a/lib/puppet/indirector/certificate_revocation_list/ca.rb b/lib/puppet/indirector/certificate_revocation_list/ca.rb new file mode 100644 index 000000000..66cc23e50 --- /dev/null +++ b/lib/puppet/indirector/certificate_revocation_list/ca.rb @@ -0,0 +1,8 @@ +require 'puppet/indirector/ssl_file' +require 'puppet/ssl/certificate_revocation_list' + +class Puppet::SSL::CertificateRevocationList::Ca < Puppet::Indirector::SslFile + desc "Manage the CA collection of certificate requests on disk." + + store_at :cacrl +end diff --git a/lib/puppet/indirector/certificate_revocation_list/file.rb b/lib/puppet/indirector/certificate_revocation_list/file.rb new file mode 100644 index 000000000..037aa6b8c --- /dev/null +++ b/lib/puppet/indirector/certificate_revocation_list/file.rb @@ -0,0 +1,8 @@ +require 'puppet/indirector/ssl_file' +require 'puppet/ssl/certificate_revocation_list' + +class Puppet::SSL::CertificateRevocationList::File < Puppet::Indirector::SslFile + desc "Manage the global certificate revocation list." + + store_at :hostcrl +end diff --git a/lib/puppet/indirector/certificate_revocation_list/rest.rb b/lib/puppet/indirector/certificate_revocation_list/rest.rb new file mode 100644 index 000000000..13cc95c87 --- /dev/null +++ b/lib/puppet/indirector/certificate_revocation_list/rest.rb @@ -0,0 +1,6 @@ +require 'puppet/ssl/certificate_revocation_list' +require 'puppet/indirector/rest' + +class Puppet::SSL::CertificateRevocationList::Rest < Puppet::Indirector::REST + desc "Find and save certificate revocation lists over HTTP via REST." +end diff --git a/lib/puppet/indirector/indirection.rb b/lib/puppet/indirector/indirection.rb index 4841ec532..a9fff75b8 100644 --- a/lib/puppet/indirector/indirection.rb +++ b/lib/puppet/indirector/indirection.rb @@ -1,20 +1,17 @@ require 'puppet/util/docs' require 'puppet/indirector/envelope' require 'puppet/indirector/request' +require 'puppet/util/cacher' # The class that connects functional classes with their different collection # back-ends. Each indirection has a set of associated terminus classes, # each of which is a subclass of Puppet::Indirector::Terminus. class Puppet::Indirector::Indirection + include Puppet::Util::Cacher include Puppet::Util::Docs @@indirections = [] - # Clear all cached termini from all indirections. - def self.clear_cache - @@indirections.each { |ind| ind.clear_cache } - end - # Find an indirection by name. This is provided so that Terminus classes # can specifically hook up with the indirections they are associated with. def self.instance(name) @@ -54,13 +51,6 @@ class Puppet::Indirector::Indirection @cache_class = class_name end - # Clear our cached list of termini, and reset the cache name - # so it's looked up again. - # This is only used for testing. - def clear_cache - @termini.clear - end - # This is only used for testing. def delete @@indirections.delete(self) if @@indirections.include?(self) @@ -104,7 +94,6 @@ class Puppet::Indirector::Indirection @model = model @name = name - @termini = {} @cache_class = nil @terminus_class = nil @@ -138,7 +127,7 @@ class Puppet::Indirector::Indirection raise Puppet::DevError, "No terminus specified for %s; cannot redirect" % self.name end - return @termini[terminus_name] ||= make_terminus(terminus_name) + return termini[terminus_name] ||= make_terminus(terminus_name) end # This can be used to select the terminus class. @@ -299,4 +288,9 @@ class Puppet::Indirector::Indirection end return klass.new end + + # Use cached termini. + def termini + attr_cache(:termini) { Hash.new } + end end diff --git a/lib/puppet/indirector/key/ca.rb b/lib/puppet/indirector/key/ca.rb new file mode 100644 index 000000000..3604de22b --- /dev/null +++ b/lib/puppet/indirector/key/ca.rb @@ -0,0 +1,20 @@ +require 'puppet/indirector/ssl_file' +require 'puppet/ssl/key' + +class Puppet::SSL::Key::Ca < Puppet::Indirector::SslFile + desc "Manage the CA's private on disk. This terminus *only* works + with the CA key, because that's the only key that the CA ever interacts + with." + + # This is just to pass the validation in the base class. Eh. + store_at :cakey + + store_ca_at :cakey + + def path(name) + unless ca?(name) + raise ArgumentError, "The :ca terminus can only handle the CA private key" + end + super + end +end diff --git a/lib/puppet/indirector/key/file.rb b/lib/puppet/indirector/key/file.rb new file mode 100644 index 000000000..4536f8aa7 --- /dev/null +++ b/lib/puppet/indirector/key/file.rb @@ -0,0 +1,42 @@ +require 'puppet/indirector/ssl_file' +require 'puppet/ssl/key' + +class Puppet::SSL::Key::File < Puppet::Indirector::SslFile + desc "Manage SSL private and public keys on disk." + + store_in :privatekeydir + store_ca_at :cakey + + # Where should we store the public key? + def public_key_path(name) + if ca?(name) + Puppet[:capub] + else + File.join(Puppet[:publickeydir], name.to_s + ".pem") + end + end + + # Remove the public key, in addition to the private key + def destroy(request) + super + + return unless FileTest.exist?(public_key_path(request.key)) + + begin + File.unlink(public_key_path(request.key)) + rescue => detail + raise Puppet::Error, "Could not remove %s public key: %s" % [request.key, detail] + end + end + + # Save the public key, in addition to the private key. + def save(request) + super + + begin + File.open(public_key_path(request.key), "w") { |f| f.print request.instance.content.public_key.to_pem } + rescue => detail + raise Puppet::Error, "Could not write %s: %s" % [key, detail] + end + end +end diff --git a/lib/puppet/indirector/rest.rb b/lib/puppet/indirector/rest.rb index d33150fc2..77dd0538b 100644 --- a/lib/puppet/indirector/rest.rb +++ b/lib/puppet/indirector/rest.rb @@ -3,21 +3,20 @@ require 'uri' # Access objects via REST class Puppet::Indirector::REST < Puppet::Indirector::Terminus - def rest_connection_details { :host => Puppet[:server], :port => Puppet[:masterport].to_i } end def network_fetch(path) - network {|conn| conn.get("/#{path}").body } + network.get("/#{path}").body end def network_delete(path) - network {|conn| conn.delete("/#{path}").body } + network.delete("/#{path}").body end def network_put(path, data) - network {|conn| conn.put("/#{path}", data).body } + network.put("/#{path}", data).body end def find(request) @@ -46,8 +45,8 @@ class Puppet::Indirector::REST < Puppet::Indirector::Terminus private - def network(&block) - Net::HTTP.start(rest_connection_details[:host], rest_connection_details[:port]) {|conn| yield(conn) } + def network + Puppet::Network::HttpPool.http_instance(rest_connection_details[:host], rest_connection_details[:port]) end def exception?(yaml_string) diff --git a/lib/puppet/indirector/ssl_file.rb b/lib/puppet/indirector/ssl_file.rb new file mode 100644 index 000000000..44a66fab2 --- /dev/null +++ b/lib/puppet/indirector/ssl_file.rb @@ -0,0 +1,147 @@ +require 'puppet/ssl' + +class Puppet::Indirector::SslFile < Puppet::Indirector::Terminus + # Specify the directory in which multiple files are stored. + def self.store_in(setting) + @directory_setting = setting + end + + # Specify a single file location for storing just one file. + # This is used for things like the CRL. + def self.store_at(setting) + @file_setting = setting + end + + # Specify where a specific ca file should be stored. + def self.store_ca_at(setting) + @ca_setting = setting + end + + class << self + attr_reader :directory_setting, :file_setting, :ca_setting + end + + # The full path to where we should store our files. + def self.collection_directory + return nil unless directory_setting + Puppet.settings[directory_setting] + end + + # The full path to an individual file we would be managing. + def self.file_location + return nil unless file_setting + Puppet.settings[file_setting] + end + + # The full path to a ca file we would be managing. + def self.ca_location + return nil unless ca_setting + Puppet.settings[ca_setting] + end + + # We assume that all files named 'ca' are pointing to individual ca files, + # rather than normal host files. It's a bit hackish, but all the other + # solutions seemed even more hackish. + def ca?(name) + name == Puppet::SSL::Host.ca_name + end + + def initialize + Puppet.settings.use(:main, :ssl) + + (collection_directory || file_location) or raise Puppet::DevError, "No file or directory setting provided; terminus %s cannot function" % self.class.name + end + + # Use a setting to determine our path. + def path(name) + if ca?(name) and ca_location + ca_location + elsif collection_directory + File.join(collection_directory, name.to_s + ".pem") + else + file_location + end + end + + # Remove our file. + def destroy(request) + path = path(request.key) + return false unless FileTest.exist?(path) + + begin + File.unlink(path) + rescue => detail + raise Puppet::Error, "Could not remove %s: %s" % [request.key, detail] + end + end + + # Find the file on disk, returning an instance of the model. + def find(request) + path = path(request.key) + + return nil unless FileTest.exist?(path) + + result = model.new(request.key) + result.read(path) + result + end + + # Save our file to disk. + def save(request) + path = path(request.key) + dir = File.dirname(path) + + raise Puppet::Error.new("Cannot save %s; parent directory %s does not exist" % [request.key, dir]) unless FileTest.directory?(dir) + raise Puppet::Error.new("Cannot save %s; parent directory %s is not writable" % [request.key, dir]) unless FileTest.writable?(dir) + + write(request.key, path) { |f| f.print request.instance.to_s } + end + + # Search for more than one file. At this point, it just returns + # an instance for every file in the directory. + def search(request) + dir = collection_directory + Dir.entries(dir).reject { |file| file !~ /\.pem$/ }.collect do |file| + name = file.sub(/\.pem$/, '') + result = model.new(name) + result.read(File.join(dir, file)) + result + end + end + + private + + # Demeterish pointers to class info. + def collection_directory + self.class.collection_directory + end + + def file_location + self.class.file_location + end + + def ca_location + self.class.ca_location + end + + # Yield a filehandle set up appropriately, either with our settings doing + # the work or opening a filehandle manually. + def write(name, path) + if ca?(name) and ca_location + Puppet.settings.write(self.class.ca_setting) { |f| yield f } + elsif file_location + Puppet.settings.write(self.class.file_setting) { |f| yield f } + else + begin + File.open(path, "w") { |f| yield f } + rescue => detail + raise Puppet::Error, "Could not write %s: %s" % [path, detail] + end + end + end +end + +# LAK:NOTE This has to be at the end, because classes like SSL::Key use this +# class, and this require statement loads those, which results in a load loop +# and lots of failures. +require 'puppet/ssl/host' diff --git a/lib/puppet/indirector/ssl_rsa.rb b/lib/puppet/indirector/ssl_rsa.rb deleted file mode 100644 index 162d8200a..000000000 --- a/lib/puppet/indirector/ssl_rsa.rb +++ /dev/null @@ -1,5 +0,0 @@ -# This is a stub class - -class Puppet::Indirector::SslRsa #:nodoc: -end - diff --git a/lib/puppet/indirector/ssl_rsa/file.rb b/lib/puppet/indirector/ssl_rsa/file.rb deleted file mode 100644 index 435aa8f86..000000000 --- a/lib/puppet/indirector/ssl_rsa/file.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'puppet/indirector/file' -require 'puppet/indirector/ssl_rsa' - -class Puppet::Indirector::SslRsa::File < Puppet::Indirector::File - desc "Store SSL keys on disk." - - def initialize - Puppet.settings.use(:ssl) - end - - def path(name) - if name == :ca - File.join Puppet.settings[:cadir], "ca_key.pem" - else - File.join Puppet.settings[:publickeydir], name.to_s + ".pem" - end - end - - def save(key) - File.open(path(key.name), "w") { |f| f.print key.to_pem } - end - - def find(name) - return nil unless FileTest.exists?(path(name)) - OpenSSL::PKey::RSA.new(File.read(path(name))) - end - - def destroy(name) - return nil unless FileTest.exists?(path(name)) - File.unlink(path(name)) and true - end - -end diff --git a/lib/puppet/metatype/container.rb b/lib/puppet/metatype/container.rb index 2bbe3f546..dbc8a3dee 100644 --- a/lib/puppet/metatype/container.rb +++ b/lib/puppet/metatype/container.rb @@ -36,7 +36,6 @@ class Puppet::Type obj.remove end @parameters.clear - self.class.delete(self) @parent = nil diff --git a/lib/puppet/metatype/instances.rb b/lib/puppet/metatype/instances.rb index 3f44413f8..34d542c5d 100644 --- a/lib/puppet/metatype/instances.rb +++ b/lib/puppet/metatype/instances.rb @@ -8,11 +8,13 @@ class Puppet::Type # retrieve a named instance of the current type def self.[](name) + raise "Global resource access is deprecated" @objects[name] || @aliases[name] end # add an instance by name to the class list of instances def self.[]=(name,object) + raise "Global resource storage is deprecated" newobj = nil if object.is_a?(Puppet::Type) newobj = object @@ -44,6 +46,7 @@ class Puppet::Type # Create an alias. We keep these in a separate hash so that we don't encounter # the objects multiple times when iterating over them. def self.alias(name, obj) + raise "Global resource aliasing is deprecated" if @objects.include?(name) unless @objects[name] == obj raise Puppet::Error.new( @@ -67,6 +70,7 @@ class Puppet::Type # remove all of the instances of a single type def self.clear + raise "Global resource removal is deprecated" if defined? @objects @objects.each do |name, obj| obj.remove(true) @@ -126,24 +130,6 @@ class Puppet::Type # XXX This will have to change when transobjects change to using titles title = hash.name - # if the object already exists - if self.isomorphic? and retobj = self[title] - # if only one of our objects is implicit, then it's easy to see - # who wins -- the non-implicit one. - if retobj.implicit? and ! implicit - Puppet.notice "Removing implicit %s" % retobj.title - # Remove all of the objects, but do not remove their subscriptions. - retobj.remove(false) - - # now pass through and create the new object - elsif implicit - Puppet.debug "Ignoring implicit %s[%s]" % [self.name, title] - return nil - else - raise Puppet::Error, "%s is already being managed" % retobj.ref - end - end - # create it anew # if there's a failure, destroy the object if it got that far, but raise # the error. @@ -153,8 +139,6 @@ class Puppet::Type Puppet.err "Could not create %s: %s" % [title, detail.to_s] if obj obj.remove(true) - elsif obj = self[title] - obj.remove(true) end raise end @@ -163,14 +147,12 @@ class Puppet::Type obj.implicit = true end - # Store the object by title - self[obj.title] = obj - return obj end # remove a specified object def self.delete(resource) + raise "Global resource removal is deprecated" return unless defined? @objects if @objects.include?(resource.title) @objects.delete(resource.title) @@ -191,6 +173,7 @@ class Puppet::Type # iterate across each of the type's instances def self.each + raise "Global resource iteration is deprecated" return unless defined? @objects @objects.each { |name,instance| yield instance @@ -199,6 +182,7 @@ class Puppet::Type # does the type have an object with the given name? def self.has_key?(name) + raise "Global resource access is deprecated" return @objects.has_key?(name) end @@ -258,10 +242,6 @@ class Puppet::Type provider_instances = {} providers_by_source.collect do |provider| provider.instances.collect do |instance| - # First try to get the resource if it already exists - # Skip instances that map to a managed resource with a different provider - next if resource = self[instance.name] and resource.provider.class != instance.class - # We always want to use the "first" provider instance we find, unless the resource # is already managed and has a different provider set if other = provider_instances[instance.name] @@ -271,12 +251,7 @@ class Puppet::Type end provider_instances[instance.name] = instance - if resource - resource.provider = instance - resource - else - create(:name => instance.name, :provider => instance, :check => :all) - end + create(:name => instance.name, :provider => instance, :check => :all) end end.flatten.compact end diff --git a/lib/puppet/metatype/metaparams.rb b/lib/puppet/metatype/metaparams.rb index edd594114..bf64d3a93 100644 --- a/lib/puppet/metatype/metaparams.rb +++ b/lib/puppet/metatype/metaparams.rb @@ -207,9 +207,6 @@ class Puppet::Type next end - # LAK:FIXME Old-school, add the alias to the class. - @resource.class.alias(other, @resource) - # Newschool, add it to the catalog. @resource.catalog.alias(@resource, other) end @@ -277,6 +274,7 @@ class Puppet::Type # to an object... tname, name = value reference = Puppet::ResourceReference.new(tname, name) + reference.catalog = resource.catalog # Either of the two retrieval attempts could have returned # nil. diff --git a/lib/puppet/metatype/relationships.rb b/lib/puppet/metatype/relationships.rb index 4fb78ae56..0070c6efb 100644 --- a/lib/puppet/metatype/relationships.rb +++ b/lib/puppet/metatype/relationships.rb @@ -17,7 +17,10 @@ class Puppet::Type # Figure out of there are any objects we can automatically add as # dependencies. - def autorequire + def autorequire(rel_catalog = nil) + rel_catalog ||= catalog + raise(Puppet::DevError, "You cannot add relationships without a catalog") unless rel_catalog + reqs = [] self.class.eachautorequire { |type, block| # Ignore any types we can't find, although that would be a bit odd. @@ -35,7 +38,7 @@ class Puppet::Type # Support them passing objects directly, to save some effort. unless dep.is_a? Puppet::Type # Skip autorequires that we aren't managing - unless dep = typeobj[dep] + unless dep = rel_catalog.resource(type, dep) next end end diff --git a/lib/puppet/metatype/schedules.rb b/lib/puppet/metatype/schedules.rb index 96ebce0ab..b4782f852 100644 --- a/lib/puppet/metatype/schedules.rb +++ b/lib/puppet/metatype/schedules.rb @@ -3,9 +3,13 @@ class Puppet::Type # the instantiation phase, so that the schedule can be anywhere in the # file. def schedule + unless catalog + warning "Cannot schedule without a schedule-containing catalog" + return nil + end unless defined? @schedule if name = self[:schedule] - if sched = Puppet.type(:schedule)[name] + if sched = catalog.resource(:schedule, name) @schedule = sched else self.fail "Could not find schedule %s" % name diff --git a/lib/puppet/network/client/ca.rb b/lib/puppet/network/client/ca.rb index a2704e451..5fbdfe9e3 100644 --- a/lib/puppet/network/client/ca.rb +++ b/lib/puppet/network/client/ca.rb @@ -45,7 +45,7 @@ class Puppet::Network::Client::CA < Puppet::Network::Client end unless @cert.check_private_key(key) - raise InvalidCertificate, "Certificate does not match private key. Try 'puppetca --clean %s' on the server." % Facter.value(:fqdn) + raise InvalidCertificate, "Certificate does not match private key. Try 'puppetca --clean %s' on the server." % Puppet[:certname] end # Only write the cert out if it passes validating. diff --git a/lib/puppet/network/client/master.rb b/lib/puppet/network/client/master.rb index 22bb3fa4e..a2b6499bb 100644 --- a/lib/puppet/network/client/master.rb +++ b/lib/puppet/network/client/master.rb @@ -75,7 +75,6 @@ class Puppet::Network::Client::Master < Puppet::Network::Client def clear @catalog.clear(true) if @catalog - Puppet::Type.allclear @catalog = nil end diff --git a/lib/puppet/network/handler/fileserver.rb b/lib/puppet/network/handler/fileserver.rb index 3e62cdbd9..183979429 100755 --- a/lib/puppet/network/handler/fileserver.rb +++ b/lib/puppet/network/handler/fileserver.rb @@ -496,12 +496,14 @@ class Puppet::Network::Handler @path = nil end + @files = {} + super() end def fileobj(path, links, client) obj = nil - if obj = Puppet.type(:file)[file_path(path, client)] + if obj = @files[file_path(path, client)] # 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 @@ -512,6 +514,7 @@ class Puppet::Network::Handler :name => file_path(path, client), :check => CHECKPARAMS ) + @files[file_path(path, client)] = obj end if links == :manage @@ -528,7 +531,7 @@ class Puppet::Network::Handler # Read the contents of the file at the relative path given. def read_file(relpath, client) - File.read(file_path(relpath, client)) + File.read(file_path(relpath, client)) end # Cache this manufactured map, since if it's used it's likely diff --git a/lib/puppet/network/handler/resource.rb b/lib/puppet/network/handler/resource.rb index f2a339751..7ec27b4dc 100755 --- a/lib/puppet/network/handler/resource.rb +++ b/lib/puppet/network/handler/resource.rb @@ -68,15 +68,11 @@ class Puppet::Network::Handler 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 + 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 unless obj diff --git a/lib/puppet/network/http/mongrel.rb b/lib/puppet/network/http/mongrel.rb index 9a4531c7a..847781cf2 100644 --- a/lib/puppet/network/http/mongrel.rb +++ b/lib/puppet/network/http/mongrel.rb @@ -16,6 +16,7 @@ class Puppet::Network::HTTP::Mongrel @protocols = args[:protocols] @handlers = args[:handlers] + @xmlrpc_handlers = args[:xmlrpc_handlers] @server = Mongrel::HttpServer.new(args[:address], args[:port]) setup_handlers @@ -38,12 +39,22 @@ class Puppet::Network::HTTP::Mongrel def setup_handlers @protocols.each do |protocol| + next if protocol == :xmlrpc klass = class_for_protocol(protocol) @handlers.each do |handler| @server.register('/' + handler.to_s, klass.new(:server => @server, :handler => handler)) @server.register('/' + handler.to_s + 's', klass.new(:server => @server, :handler => handler)) end end + + if @protocols.include?(:xmlrpc) and ! @xmlrpc_handlers.empty? + setup_xmlrpc_handlers + end + end + + # Use our existing code to provide the xmlrpc backward compatibility. + def setup_xmlrpc_handlers + @server.register('/RPC2', Puppet::Network::HTTPServer::Mongrel.new(@xmlrpc_handlers)) end def class_for_protocol(protocol) diff --git a/lib/puppet/network/http/webrick.rb b/lib/puppet/network/http/webrick.rb index 3a37e2071..eacf81ec2 100644 --- a/lib/puppet/network/http/webrick.rb +++ b/lib/puppet/network/http/webrick.rb @@ -1,8 +1,12 @@ require 'webrick' require 'webrick/https' require 'puppet/network/http/webrick/rest' +require 'puppet/network/xmlrpc/webrick_servlet' require 'thread' +require 'puppet/ssl/certificate' +require 'puppet/ssl/certificate_revocation_list' + class Puppet::Network::HTTP::WEBrick def initialize(args = {}) @listening = false @@ -22,7 +26,14 @@ class Puppet::Network::HTTP::WEBrick @protocols = args[:protocols] @handlers = args[:handlers] - @server = WEBrick::HTTPServer.new(:BindAddress => args[:address], :Port => args[:port]) + @xmlrpc_handlers = args[:xmlrpc_handlers] + + arguments = {:BindAddress => args[:address], :Port => args[:port]} + arguments.merge!(setup_logger) + arguments.merge!(setup_ssl) + + @server = WEBrick::HTTPServer.new(arguments) + setup_handlers @mutex.synchronize do @@ -48,15 +59,86 @@ class Puppet::Network::HTTP::WEBrick end end + # Configure our http log file. + def setup_logger + # Make sure the settings are all ready for us. + Puppet.settings.use(:main, :ssl, Puppet[:name]) + + if Puppet[:name] == "puppetmasterd" + file = Puppet[:masterhttplog] + else + file = Puppet[:httplog] + end + + # open the log manually to prevent file descriptor leak + file_io = ::File.open(file, "a+") + file_io.sync + file_io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) + + args = [file_io] + args << WEBrick::Log::DEBUG if Puppet::Util::Log.level == :debug + + logger = WEBrick::Log.new(*args) + return :Logger => logger, :AccessLog => [ + [logger, WEBrick::AccessLog::COMMON_LOG_FORMAT ], + [logger, WEBrick::AccessLog::REFERER_LOG_FORMAT ] + ] + end + + # Add all of the ssl cert information. + def setup_ssl + results = {} + + host = Puppet::SSL::Host.new + + host.generate unless host.certificate + + raise Puppet::Error, "Could not retrieve certificate for %s and not running on a valid certificate authority" % host.name unless host.certificate + + results[:SSLPrivateKey] = host.key.content + results[:SSLCertificate] = host.certificate.content + results[:SSLStartImmediately] = true + results[:SSLEnable] = true + + unless Puppet::SSL::Certificate.find("ca") + raise Puppet::Error, "Could not find CA certificate" + end + + results[:SSLCACertificateFile] = Puppet[:localcacert] + results[:SSLVerifyClient] = OpenSSL::SSL::VERIFY_PEER + + results[:SSLCertificateStore] = host.ssl_store + + results + end + private def setup_handlers + # Set up the new-style protocols. @protocols.each do |protocol| + next if protocol == :xmlrpc klass = self.class.class_for_protocol(protocol) @handlers.each do |handler| @server.mount('/' + handler.to_s, klass, handler) @server.mount('/' + handler.to_s + 's', klass, handler) end end + + # And then set up xmlrpc, if configured. + if @protocols.include?(:xmlrpc) and ! @xmlrpc_handlers.empty? + @server.mount("/RPC2", xmlrpc_servlet) + end + end + + # Create our xmlrpc servlet, which provides backward compatibility. + def xmlrpc_servlet + handlers = @xmlrpc_handlers.collect { |handler| + unless hclass = Puppet::Network::Handler.handler(handler) + raise "Invalid xmlrpc handler %s" % handler + end + hclass.new({}) + } + Puppet::Network::XMLRPC::WEBrickServlet.new handlers end end diff --git a/lib/puppet/network/http_pool.rb b/lib/puppet/network/http_pool.rb index 1227f78dc..78a35cc15 100644 --- a/lib/puppet/network/http_pool.rb +++ b/lib/puppet/network/http_pool.rb @@ -1,11 +1,13 @@ -require 'puppet/sslcertificates/support' +require 'puppet/ssl/host' require 'net/https' +require 'puppet/util/cacher' -module Puppet::Network -end +module Puppet::Network; end # Manage Net::HTTP instances for keep-alive. module Puppet::Network::HttpPool + extend Puppet::Util::Cacher + # 2008/03/23 # LAK:WARNING: Enabling this has a high propability of # causing corrupt files and who knows what else. See #1010. @@ -15,18 +17,18 @@ module Puppet::Network::HttpPool HTTP_KEEP_ALIVE end - # This handles reading in the key and such-like. - extend Puppet::SSLCertificates::Support - @http_cache = {} + # Create an ssl host instance for getting certificate + # information. + def self.ssl_host + attr_cache(:ssl_host) { Puppet::SSL::Host.new } + end # Clear our http cache, closing all connections. def self.clear_http_instances - @http_cache.each do |name, connection| + http_cache.each do |name, connection| connection.finish if connection.started? end - @http_cache.clear - @cert = nil - @key = nil + Puppet::Util::Cacher.invalidate end # Make sure we set the driver up when we read the cert in. @@ -44,17 +46,13 @@ module Puppet::Network::HttpPool # Use cert information from a Puppet client to set up the http object. def self.cert_setup(http) # Just no-op if we don't have certs. - return false unless (defined?(@cert) and @cert) or self.read_cert + return false unless FileTest.exist?(Puppet[:hostcert]) # ssl_host.certificate - store = OpenSSL::X509::Store.new - store.add_file Puppet[:localcacert] - store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT - - http.cert_store = store + http.cert_store = ssl_host.ssl_store http.ca_file = Puppet[:localcacert] - http.cert = self.cert + http.cert = ssl_host.certificate.content http.verify_mode = OpenSSL::SSL::VERIFY_PEER - http.key = self.key + http.key = ssl_host.key.content end # Retrieve a cached http instance of caching is enabled, else return @@ -66,11 +64,11 @@ module Puppet::Network::HttpPool # Return our cached instance if we've got a cache, as long as we're not # resetting the instance. if keep_alive? - return @http_cache[key] if ! reset and @http_cache[key] + return http_cache[key] if ! reset and http_cache[key] # Clean up old connections if we have them. - if http = @http_cache[key] - @http_cache.delete(key) + if http = http_cache[key] + http_cache.delete(key) http.finish if http.started? end end @@ -100,8 +98,15 @@ module Puppet::Network::HttpPool cert_setup(http) - @http_cache[key] = http if keep_alive? + http_cache[key] = http if keep_alive? return http end + + private + + def self.http_cache + # Default to an empty hash. + attr_cache(:http) { Hash.new } + end end diff --git a/lib/puppet/network/http_server/mongrel.rb b/lib/puppet/network/http_server/mongrel.rb index 6b2325d29..e9421c781 100644 --- a/lib/puppet/network/http_server/mongrel.rb +++ b/lib/puppet/network/http_server/mongrel.rb @@ -64,11 +64,11 @@ module Puppet::Network # behaviour and we have to subclass Mongrel::HttpHandler so our handler # works for Mongrel. @xmlrpc_server = Puppet::Network::XMLRPCServer.new - handlers.each do |name, args| + handlers.each do |name| unless handler = Puppet::Network::Handler.handler(name) raise ArgumentError, "Invalid handler %s" % name end - @xmlrpc_server.add_handler(handler.interface, handler.new(args)) + @xmlrpc_server.add_handler(handler.interface, handler.new({})) end end diff --git a/lib/puppet/network/server.rb b/lib/puppet/network/server.rb index cab14519b..de32db02f 100644 --- a/lib/puppet/network/server.rb +++ b/lib/puppet/network/server.rb @@ -1,59 +1,153 @@ require 'puppet/network/http' +require 'puppet/util/pidlock' class Puppet::Network::Server - attr_reader :server_type, :protocols, :address, :port + attr_reader :server_type, :protocols, :address, :port + + # Put the daemon into the background. + def daemonize + if pid = fork() + Process.detach(pid) + exit(0) + end + + # Get rid of console logging + Puppet::Util::Log.close(:console) + + Process.setsid + Dir.chdir("/") + begin + $stdin.reopen "/dev/null" + $stdout.reopen "/dev/null", "a" + $stderr.reopen $stdout + Puppet::Util::Log.reopen + rescue => detail + File.open("/tmp/daemonout", "w") { |f| + f.puts "Could not start %s: %s" % [Puppet[:name], detail] + } + raise "Could not start %s: %s" % [Puppet[:name], detail] + end + end + + # Create a pidfile for our daemon, so we can be stopped and others + # don't try to start. + def create_pidfile + Puppet::Util.sync(Puppet[:name]).synchronize(Sync::EX) do + unless Puppet::Util::Pidlock.new(pidfile).lock + raise "Could not create PID file: %s" % [pidfile] + end + end + end + + # Remove the pid file for our daemon. + def remove_pidfile + Puppet::Util.sync(Puppet[:name]).synchronize(Sync::EX) do + locker = Puppet::Util::Pidlock.new(pidfile) + if locker.locked? + locker.unlock or Puppet.err "Could not remove PID file %s" % [pidfile] + end + end + end + + # Provide the path to our pidfile. + def pidfile + Puppet[:pidfile] + end def initialize(args = {}) @server_type = Puppet[:servertype] or raise "No servertype configuration found." # e.g., WEBrick, Mongrel, etc. http_server_class || raise(ArgumentError, "Could not determine HTTP Server class for server type [#{@server_type}]") - @address = args[:address] || Puppet[:bindaddress] || - raise(ArgumentError, "Must specify :address or configure Puppet :bindaddress.") - @port = args[:port] || Puppet[:masterport] || - raise(ArgumentError, "Must specify :port or configure Puppet :masterport") - @protocols = [ :rest ] + + @address = args[:address] || Puppet[:bindaddress] || raise(ArgumentError, "Must specify :address or configure Puppet :bindaddress.") + @port = args[:port] || Puppet[:masterport] || raise(ArgumentError, "Must specify :port or configure Puppet :masterport") + + @protocols = [ :rest, :xmlrpc ] @listening = false @routes = {} + @xmlrpc_routes = {} self.register(args[:handlers]) if args[:handlers] + self.register_xmlrpc(args[:xmlrpc_handlers]) if args[:xmlrpc_handlers] + + # Make sure we have all of the directories we need to function. + Puppet.settings.use(:main, :ssl, Puppet[:name]) end + # Register handlers for REST networking, based on the Indirector. def register(*indirections) - raise ArgumentError, "Indirection names are required." if indirections.empty? - indirections.flatten.each { |i| @routes[i.to_sym] = true } + raise ArgumentError, "Indirection names are required." if indirections.empty? + indirections.flatten.each do |name| + Puppet::Indirector::Indirection.model(name) || raise(ArgumentError, "Cannot locate indirection '#{name}'.") + @routes[name.to_sym] = true + end end + # Unregister Indirector handlers. def unregister(*indirections) - raise "Cannot unregister indirections while server is listening." if listening? - indirections = @routes.keys if indirections.empty? + raise "Cannot unregister indirections while server is listening." if listening? + indirections = @routes.keys if indirections.empty? + + indirections.flatten.each do |i| + raise(ArgumentError, "Indirection [%s] is unknown." % i) unless @routes[i.to_sym] + end + + indirections.flatten.each do |i| + @routes.delete(i.to_sym) + end + end + + # Register xmlrpc handlers for backward compatibility. + def register_xmlrpc(*namespaces) + raise ArgumentError, "XMLRPC namespaces are required." if namespaces.empty? + namespaces.flatten.each do |name| + Puppet::Network::Handler.handler(name) || raise(ArgumentError, "Cannot locate XMLRPC handler for namespace '#{name}'.") + @xmlrpc_routes[name.to_sym] = true + end + end + + # Unregister xmlrpc handlers. + def unregister_xmlrpc(*namespaces) + raise "Cannot unregister xmlrpc handlers while server is listening." if listening? + namespaces = @xmlrpc_routes.keys if namespaces.empty? - indirections.flatten.each do |i| - raise(ArgumentError, "Indirection [%s] is unknown." % i) unless @routes[i.to_sym] - end + namespaces.flatten.each do |i| + raise(ArgumentError, "XMLRPC handler '%s' is unknown." % i) unless @xmlrpc_routes[i.to_sym] + end - indirections.flatten.each do |i| - @routes.delete(i.to_sym) - end + namespaces.flatten.each do |i| + @xmlrpc_routes.delete(i.to_sym) + end end def listening? - @listening + @listening end def listen - raise "Cannot listen -- already listening." if listening? - @listening = true - http_server.listen(:address => address, :port => port, :handlers => @routes.keys, :protocols => protocols) + raise "Cannot listen -- already listening." if listening? + @listening = true + http_server.listen(:address => address, :port => port, :handlers => @routes.keys, :xmlrpc_handlers => @xmlrpc_routes.keys, :protocols => protocols) end def unlisten - raise "Cannot unlisten -- not currently listening." unless listening? - http_server.unlisten - @listening = false + raise "Cannot unlisten -- not currently listening." unless listening? + http_server.unlisten + @listening = false end def http_server_class http_server_class_by_type(@server_type) end + def start + create_pidfile + listen + end + + def stop + unlisten + remove_pidfile + end + private def http_server diff --git a/lib/puppet/network/xmlrpc/client.rb b/lib/puppet/network/xmlrpc/client.rb index e0fb5a0ab..dfd4a95a7 100644 --- a/lib/puppet/network/xmlrpc/client.rb +++ b/lib/puppet/network/xmlrpc/client.rb @@ -51,7 +51,8 @@ module Puppet::Network end ["certificate verify failed", "hostname was not match", "hostname not match"].each do |str| if detail.message.include?(str) - Puppet.warning "Certificate validation failed; considering using the certname configuration option" + Puppet.warning "Certificate validation failed; consider using the certname configuration option" + break end end raise XMLRPCClientError, diff --git a/lib/puppet/node.rb b/lib/puppet/node.rb index 576e2265d..1d2041201 100644 --- a/lib/puppet/node.rb +++ b/lib/puppet/node.rb @@ -164,5 +164,7 @@ class Puppet::Node params.each do |name, value| @parameters[name] = value unless @parameters.include?(name) end + + @parameters["environment"] ||= self.environment if self.environment end end diff --git a/lib/puppet/node/catalog.rb b/lib/puppet/node/catalog.rb index 7d9a83795..3ac11a66d 100644 --- a/lib/puppet/node/catalog.rb +++ b/lib/puppet/node/catalog.rb @@ -61,7 +61,7 @@ class Puppet::Node::Catalog < Puppet::PGraph def add_resource(*resources) resources.each do |resource| unless resource.respond_to?(:ref) - raise ArgumentError, "Can only add objects that respond to :ref" + raise ArgumentError, "Can only add objects that respond to :ref, not instances of %s" % resource.class end fail_unless_unique(resource) @@ -309,7 +309,7 @@ class Puppet::Node::Catalog < Puppet::PGraph # And filebuckets if bucket = Puppet::Type.type(:filebucket).mkdefaultbucket - add_resource(bucket) + add_resource(bucket) unless resource(bucket.ref) end end @@ -337,7 +337,7 @@ class Puppet::Node::Catalog < Puppet::PGraph # Lastly, add in any autorequires @relationship_graph.vertices.each do |vertex| - vertex.autorequire.each do |edge| + vertex.autorequire(self).each do |edge| unless @relationship_graph.edge?(edge.source, edge.target) # don't let automatic relationships conflict with manual ones. unless @relationship_graph.edge?(edge.target, edge.source) vertex.debug "Autorequiring %s" % [edge.source] diff --git a/lib/puppet/resource_reference.rb b/lib/puppet/resource_reference.rb index 44b518816..12b9f54a9 100644 --- a/lib/puppet/resource_reference.rb +++ b/lib/puppet/resource_reference.rb @@ -22,15 +22,8 @@ class Puppet::ResourceReference # Find our resource. def resolve - if catalog - return catalog.resource(to_s) - end - # If it's builtin, then just ask for it directly from the type. - if t = builtin_type - t[@title] - else # Else, look for a component with the full reference as the name. - Puppet::Type::Component[to_s] - end + return catalog.resource(to_s) if catalog + return nil end # If the title has square brackets, treat it like a reference and diff --git a/lib/puppet/ssl.rb b/lib/puppet/ssl.rb new file mode 100644 index 000000000..68c65ca80 --- /dev/null +++ b/lib/puppet/ssl.rb @@ -0,0 +1,6 @@ +# Just to make the constants work out. +require 'puppet' +require 'openssl' + +module Puppet::SSL # :nodoc: +end diff --git a/lib/puppet/ssl/base.rb b/lib/puppet/ssl/base.rb new file mode 100644 index 000000000..80bfcae84 --- /dev/null +++ b/lib/puppet/ssl/base.rb @@ -0,0 +1,51 @@ +require 'puppet/ssl' + +# The base class for wrapping SSL instances. +class Puppet::SSL::Base + def self.wraps(klass) + @wrapped_class = klass + end + + def self.wrapped_class + raise(Puppet::DevError, "%s has not declared what class it wraps" % self) unless defined?(@wrapped_class) + @wrapped_class + end + + attr_accessor :name, :content + + # Is this file for the CA? + def ca? + name == Puppet::SSL::Host.ca_name + end + + def generate + raise Puppet::DevError, "%s did not override 'generate'" % self.class + end + + def initialize(name) + @name = name + end + + # Read content from disk appropriately. + def read(path) + @content = wrapped_class.new(File.read(path)) + end + + # Convert our thing to pem. + def to_s + return "" unless content + content.to_pem + end + + # Provide the full text of the thing we're dealing with. + def to_text + return "" unless content + content.to_text + end + + private + + def wrapped_class + self.class.wrapped_class + end +end diff --git a/lib/puppet/ssl/certificate.rb b/lib/puppet/ssl/certificate.rb new file mode 100644 index 000000000..16af85d06 --- /dev/null +++ b/lib/puppet/ssl/certificate.rb @@ -0,0 +1,19 @@ +require 'puppet/ssl/base' + +# Manage certificates themselves. This class has no +# 'generate' method because the CA is responsible +# for turning CSRs into certificates; we can only +# retrieve them from the CA (or not, as is often +# the case). +class Puppet::SSL::Certificate < Puppet::SSL::Base + # This is defined from the base class + wraps OpenSSL::X509::Certificate + + extend Puppet::Indirector + indirects :certificate, :terminus_class => :file + + def expiration + return nil unless content + return content.not_after + end +end diff --git a/lib/puppet/ssl/certificate_authority.rb b/lib/puppet/ssl/certificate_authority.rb new file mode 100644 index 000000000..6947af11c --- /dev/null +++ b/lib/puppet/ssl/certificate_authority.rb @@ -0,0 +1,285 @@ +require 'puppet/ssl/host' +require 'puppet/ssl/certificate_request' +require 'puppet/util/cacher' + +# The class that knows how to sign certificates. It creates +# a 'special' SSL::Host whose name is 'ca', thus indicating +# that, well, it's the CA. There's some magic in the +# indirector/ssl_file terminus base class that does that +# for us. +# This class mostly just signs certs for us, but +# it can also be seen as a general interface into all of the +# SSL stuff. +class Puppet::SSL::CertificateAuthority + require 'puppet/ssl/certificate_factory' + require 'puppet/ssl/inventory' + require 'puppet/ssl/certificate_revocation_list' + + require 'puppet/ssl/certificate_authority/interface' + + extend Puppet::Util::Cacher + + def self.ca? + return false unless Puppet[:ca] + return false unless Puppet[:name] == "puppetmasterd" + return true + end + + # If this process can function as a CA, then return a singleton + # instance. + def self.instance + return nil unless ca? + + attr_cache(:instance) { new } + end + + attr_reader :name, :host + + # Create and run an applicator. I wanted to build an interface where you could do + # something like 'ca.apply(:generate).to(:all) but I don't think it's really possible. + def apply(method, options) + unless options[:to] + raise ArgumentError, "You must specify the hosts to apply to; valid values are an array or the symbol :all" + end + applier = Interface.new(method, options[:to]) + + applier.apply(self) + end + + # If autosign is configured, then autosign all CSRs that match our configuration. + def autosign + return unless auto = autosign? + + store = nil + if auto != true + store = autosign_store(auto) + end + + Puppet::SSL::CertificateRequest.search("*").each do |csr| + sign(csr.name) if auto == true or store.allowed?(csr.name, "127.1.1.1") + end + end + + # Do we autosign? This returns true, false, or a filename. + def autosign? + auto = Puppet[:autosign] + return false if ['false', false].include?(auto) + return true if ['true', true].include?(auto) + + raise ArgumentError, "The autosign configuration '%s' must be a fully qualified file" % auto unless auto =~ /^\// + if FileTest.exist?(auto) + return auto + else + return false + end + end + + # Create an AuthStore for autosigning. + def autosign_store(file) + auth = Puppet::Network::AuthStore.new + File.readlines(file).each do |line| + next if line =~ /^\s*#/ + next if line =~ /^\s*$/ + auth.allow(line.chomp) + end + + auth + end + + # Retrieve (or create, if necessary) the certificate revocation list. + def crl + unless defined?(@crl) + unless @crl = Puppet::SSL::CertificateRevocationList.find("ca") + @crl = Puppet::SSL::CertificateRevocationList.new("ca") + @crl.generate(host.certificate.content, host.key.content) + @crl.save + end + end + @crl + end + + # Delegate this to our Host class. + def destroy(name) + Puppet::SSL::Host.destroy(name) + end + + # Generate a new certificate. + def generate(name) + raise ArgumentError, "A Certificate already exists for %s" % name if Puppet::SSL::Certificate.find(name) + host = Puppet::SSL::Host.new(name) + + host.generate_certificate_request + + sign(name) + end + + # Generate our CA certificate. + def generate_ca_certificate + generate_password unless password? + + host.generate_key unless host.key + + # Create a new cert request. We do this + # specially, because we don't want to actually + # save the request anywhere. + request = Puppet::SSL::CertificateRequest.new(host.name) + request.generate(host.key) + + # Create a self-signed certificate. + @certificate = sign(host.name, :ca, request) + + # And make sure we initialize our CRL. + crl() + end + + def initialize + Puppet.settings.use :main, :ssl, :ca + + @name = Puppet[:certname] + + @host = Puppet::SSL::Host.new(Puppet::SSL::Host.ca_name) + + setup() + end + + # Retrieve (or create, if necessary) our inventory manager. + def inventory + unless defined?(@inventory) + @inventory = Puppet::SSL::Inventory.new + end + @inventory + end + + # Generate a new password for the CA. + def generate_password + pass = "" + 20.times { pass += (rand(74) + 48).chr } + + begin + Puppet.settings.write(:capass) { |f| f.print pass } + rescue Errno::EACCES => detail + raise Puppet::Error, "Could not write CA password: %s" % detail.to_s + end + + @password = pass + + return pass + end + + # List all signed certificates. + def list + Puppet::SSL::Certificate.search("*").collect { |c| c.name } + end + + # Read the next serial from the serial file, and increment the + # file so this one is considered used. + def next_serial + serial = nil + + # This is slightly odd. If the file doesn't exist, our readwritelock creates + # it, but with a mode we can't actually read in some cases. So, use + # a default before the lock. + unless FileTest.exist?(Puppet[:serial]) + serial = 0x0 + end + + Puppet.settings.readwritelock(:serial) { |f| + if FileTest.exist?(Puppet[:serial]) + serial ||= File.read(Puppet.settings[:serial]).chomp.hex + end + + # We store the next valid serial, not the one we just used. + f << "%04X" % (serial + 1) + } + + return serial + end + + # Does the password file exist? + def password? + FileTest.exist? Puppet[:capass] + end + + # Print a given host's certificate as text. + def print(name) + if cert = Puppet::SSL::Certificate.find(name) + return cert.to_text + else + return nil + end + end + + # Revoke a given certificate. + def revoke(name) + raise ArgumentError, "Cannot revoke certificates when the CRL is disabled" unless crl + + if cert = Puppet::SSL::Certificate.find(name) + serial = cert.content.serial + elsif ! serial = inventory.serial(name) + raise ArgumentError, "Could not find a serial number for %s" % name + end + crl.revoke(serial, host.key.content) + end + + # This initializes our CA so it actually works. This should be a private + # method, except that you can't any-instance stub private methods, which is + # *awesome*. This method only really exists to provide a stub-point during + # testing. + def setup + generate_ca_certificate unless @host.certificate + end + + # Sign a given certificate request. + def sign(hostname, cert_type = :server, self_signing_csr = nil) + # This is a self-signed certificate + if self_signing_csr + csr = self_signing_csr + issuer = csr.content + else + unless csr = Puppet::SSL::CertificateRequest.find(hostname) + raise ArgumentError, "Could not find certificate request for %s" % hostname + end + issuer = host.certificate.content + end + + cert = Puppet::SSL::Certificate.new(hostname) + cert.content = Puppet::SSL::CertificateFactory.new(cert_type, csr.content, issuer, next_serial).result + cert.content.sign(host.key.content, OpenSSL::Digest::SHA1.new) + + Puppet.notice "Signed certificate request for %s" % hostname + + # Add the cert to the inventory before we save it, since + # otherwise we could end up with it being duplicated, if + # this is the first time we build the inventory file. + inventory.add(cert) + + # Save the now-signed cert. This should get routed correctly depending + # on the certificate type. + cert.save + + # And remove the CSR if this wasn't self signed. + Puppet::SSL::CertificateRequest.destroy(csr.name) unless self_signing_csr + + return cert + end + + # Verify a given host's certificate. + def verify(name) + unless cert = Puppet::SSL::Certificate.find(name) + raise ArgumentError, "Could not find a certificate for %s" % name + end + store = OpenSSL::X509::Store.new + store.add_file Puppet[:cacert] + store.add_crl crl.content if self.crl + store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT + + unless store.verify(cert.content) + raise "Certificate for %s failed verification" % name + end + end + + # List the waiting certificate requests. + def waiting? + Puppet::SSL::CertificateRequest.search("*").collect { |r| r.name } + end +end diff --git a/lib/puppet/ssl/certificate_authority/interface.rb b/lib/puppet/ssl/certificate_authority/interface.rb new file mode 100644 index 000000000..b355e21f0 --- /dev/null +++ b/lib/puppet/ssl/certificate_authority/interface.rb @@ -0,0 +1,110 @@ +# This class is basically a hidden class that knows how to act +# on the CA. It's only used by the 'puppetca' executable, and its +# job is to provide a CLI-like interface to the CA class. +class Puppet::SSL::CertificateAuthority::Interface + INTERFACE_METHODS = [:destroy, :list, :revoke, :generate, :sign, :print, :verify] + + class InterfaceError < ArgumentError; end + + attr_reader :method, :subjects + + # Actually perform the work. + def apply(ca) + unless subjects or method == :list + raise ArgumentError, "You must provide hosts or :all when using %s" % method + end + + begin + if respond_to?(method) + return send(method, ca) + end + + (subjects == :all ? ca.list : subjects).each do |host| + ca.send(method, host) + end + rescue InterfaceError + raise + rescue => detail + puts detail.backtrace if Puppet[:trace] + Puppet.err "Could not call %s: %s" % [method, detail] + end + end + + def generate(ca) + raise InterfaceError, "It makes no sense to generate all hosts; you must specify a list" if subjects == :all + + subjects.each do |host| + ca.generate(host) + end + end + + def initialize(method, subjects) + self.method = method + self.subjects = subjects + end + + # List the hosts. + def list(ca) + unless subjects + puts ca.waiting?.join("\n") + return nil + end + + signed = ca.list + requests = ca.waiting? + + if subjects == :all + hosts = [signed, requests].flatten + else + hosts = subjects + end + + hosts.uniq.sort.each do |host| + if signed.include?(host) + puts "+ " + host + else + puts host + end + end + end + + # Set the method to apply. + def method=(method) + raise ArgumentError, "Invalid method %s to apply" % method unless INTERFACE_METHODS.include?(method) + @method = method + end + + # Print certificate information. + def print(ca) + (subjects == :all ? ca.list : subjects).each do |host| + if value = ca.print(host) + puts value + else + Puppet.err "Could not find certificate for %s" % host + end + end + end + + # Sign a given certificate. + def sign(ca) + list = subjects == :all ? ca.waiting? : subjects + raise InterfaceError, "No waiting certificate requests to sign" if list.empty? + list.each do |host| + ca.sign(host) + end + end + + # Set the list of hosts we're operating on. Also supports keywords. + def subjects=(value) + unless value == :all or value.is_a?(Array) + raise ArgumentError, "Subjects must be an array or :all; not %s" % value + end + + if value.is_a?(Array) and value.empty? + value = nil + end + + @subjects = value + end +end + diff --git a/lib/puppet/ssl/certificate_factory.rb b/lib/puppet/ssl/certificate_factory.rb new file mode 100644 index 000000000..41155fd41 --- /dev/null +++ b/lib/puppet/ssl/certificate_factory.rb @@ -0,0 +1,145 @@ +require 'puppet/ssl' + +# The tedious class that does all the manipulations to the +# certificate to correctly sign it. Yay. +class Puppet::SSL::CertificateFactory + # How we convert from various units to the required seconds. + UNITMAP = { + "y" => 365 * 24 * 60 * 60, + "d" => 24 * 60 * 60, + "h" => 60 * 60, + "s" => 1 + } + + attr_reader :name, :cert_type, :csr, :issuer, :serial + + def initialize(cert_type, csr, issuer, serial) + @cert_type, @csr, @issuer, @serial = cert_type, csr, issuer, serial + + @name = @csr.subject + end + + # Actually generate our certificate. + def result + @cert = OpenSSL::X509::Certificate.new + + @cert.version = 2 # X509v3 + @cert.subject = @csr.subject + @cert.issuer = @issuer.subject + @cert.public_key = @csr.public_key + @cert.serial = @serial + + build_extensions() + + set_ttl + + @cert + end + + private + + # This is pretty ugly, but I'm not really sure it's even possible to do + # it any other way. + def build_extensions + @ef = OpenSSL::X509::ExtensionFactory.new + + @ef.subject_certificate = @cert + + if @issuer.is_a?(OpenSSL::X509::Request) # It's a self-signed cert + @ef.issuer_certificate = @cert + else + @ef.issuer_certificate = @issuer + end + + @subject_alt_name = [] + @key_usage = nil + @ext_key_usage = nil + @extensions = [] + + method = "add_#{@cert_type.to_s}_extensions" + + begin + send(method) + rescue NoMethodError + raise ArgumentError, "%s is an invalid certificate type" % @cert_type + end + + @extensions << @ef.create_extension("nsComment", "Puppet Ruby/OpenSSL Generated Certificate") + @extensions << @ef.create_extension("basicConstraints", @basic_constraint, true) + @extensions << @ef.create_extension("subjectKeyIdentifier", "hash") + @extensions << @ef.create_extension("keyUsage", @key_usage.join(",")) if @key_usage + @extensions << @ef.create_extension("extendedKeyUsage", @ext_key_usage.join(",")) if @ext_key_usage + @extensions << @ef.create_extension("subjectAltName", @subject_alt_name.join(",")) if ! @subject_alt_name.empty? + + @cert.extensions = @extensions + + # for some reason this _must_ be the last extension added + @extensions << @ef.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always") if @cert_type == :ca + end + + # TTL for new certificates in seconds. If config param :ca_ttl is set, + # use that, otherwise use :ca_days for backwards compatibility + def ttl + ttl = Puppet.settings[:ca_ttl] + + return ttl unless ttl.is_a?(String) + + raise ArgumentError, "Invalid ca_ttl #{ttl}" unless ttl =~ /^(\d+)(y|d|h|s)$/ + + return $1.to_i * UNITMAP[$2] + end + + def set_ttl + # Make the certificate valid as of yesterday, because + # so many people's clocks are out of sync. + from = Time.now - (60*60*24) + @cert.not_before = from + @cert.not_after = from + ttl + end + + # Woot! We're a CA. + def add_ca_extensions + @basic_constraint = "CA:TRUE" + @key_usage = %w{cRLSign keyCertSign} + end + + # We're a terminal CA, probably not self-signed. + def add_terminalsubca_extensions + @basic_constraint = "CA:TRUE,pathlen:0" + @key_usage = %w{cRLSign keyCertSign} + end + + # We're a normal server. + def add_server_extensions + @basic_constraint = "CA:FALSE" + dnsnames = Puppet[:certdnsnames] + name = @name.to_s.sub(%r{/CN=},'') + if dnsnames != "" + dnsnames.split(':').each { |d| @subject_alt_name << 'DNS:' + d } + @subject_alt_name << 'DNS:' + name # Add the fqdn as an alias + elsif name == Facter.value(:fqdn) # we're a CA server, and thus probably the server + @subject_alt_name << 'DNS:' + "puppet" # Add 'puppet' as an alias + @subject_alt_name << 'DNS:' + name # Add the fqdn as an alias + @subject_alt_name << 'DNS:' + name.sub(/^[^.]+./, "puppet.") # add puppet.domain as an alias + end + @key_usage = %w{digitalSignature keyEncipherment} + @ext_key_usage = %w{serverAuth clientAuth emailProtection} + end + + # Um, no idea. + def add_ocsp_extensions + @basic_constraint = "CA:FALSE" + @key_usage = %w{nonRepudiation digitalSignature} + @ext_key_usage = %w{serverAuth OCSPSigning} + end + + # Normal client. + def add_client_extensions + @basic_constraint = "CA:FALSE" + @key_usage = %w{nonRepudiation digitalSignature keyEncipherment} + @ext_key_usage = %w{clientAuth emailProtection} + + @extensions << @ef.create_extension("nsCertType", "client,email") + end +end + diff --git a/lib/puppet/ssl/certificate_request.rb b/lib/puppet/ssl/certificate_request.rb new file mode 100644 index 000000000..34cae5a3e --- /dev/null +++ b/lib/puppet/ssl/certificate_request.rb @@ -0,0 +1,36 @@ +require 'puppet/ssl/base' + +# Manage certificate requests. +class Puppet::SSL::CertificateRequest < Puppet::SSL::Base + wraps OpenSSL::X509::Request + + extend Puppet::Indirector + indirects :certificate_request, :terminus_class => :file + + # How to create a certificate request with our system defaults. + def generate(key) + Puppet.info "Creating a new SSL certificate request for %s" % name + + # Support either an actual SSL key, or a Puppet key. + key = key.content if key.is_a?(Puppet::SSL::Key) + + csr = OpenSSL::X509::Request.new + csr.version = 0 + csr.subject = OpenSSL::X509::Name.new([["CN", name]]) + csr.public_key = key.public_key + csr.sign(key, OpenSSL::Digest::MD5.new) + + raise Puppet::Error, "CSR sign verification failed; you need to clean the certificate request for %s on the server" % name unless csr.verify(key.public_key) + + @content = csr + end + + def save + super() + + # Try to autosign the CSR. + if ca = Puppet::SSL::CertificateAuthority.instance + ca.autosign + end + end +end diff --git a/lib/puppet/ssl/certificate_revocation_list.rb b/lib/puppet/ssl/certificate_revocation_list.rb new file mode 100644 index 000000000..3029c14a4 --- /dev/null +++ b/lib/puppet/ssl/certificate_revocation_list.rb @@ -0,0 +1,72 @@ +require 'puppet/ssl/base' +require 'puppet/indirector' + +# Manage the CRL. +class Puppet::SSL::CertificateRevocationList < Puppet::SSL::Base + wraps OpenSSL::X509::CRL + + extend Puppet::Indirector + indirects :certificate_revocation_list, :terminus_class => :file + + # Knows how to create a CRL with our system defaults. + def generate(cert, cakey) + Puppet.info "Creating a new certificate revocation list" + @content = wrapped_class.new + @content.issuer = cert.subject + @content.version = 1 + + # Init the CRL number. + crlNum = OpenSSL::ASN1::Integer(0) + @content.extensions = [OpenSSL::X509::Extension.new("crlNumber", crlNum)] + + # Set last/next update + @content.last_update = Time.now + # Keep CRL valid for 5 years + @content.next_update = Time.now + 5 * 365*24*60*60 + + @content.sign(cakey, OpenSSL::Digest::SHA1.new) + + @content + end + + # The name doesn't actually matter; there's only one CRL. + # We just need the name so our Indirector stuff all works more easily. + def initialize(fakename) + raise Puppet::Error, "Cannot manage the CRL when :cacrl is set to false" if [false, "false"].include?(Puppet[:cacrl]) + + @name = "crl" + end + + # Revoke the certificate with serial number SERIAL issued by this + # CA, then write the CRL back to disk. The REASON must be one of the + # OpenSSL::OCSP::REVOKED_* reasons + def revoke(serial, cakey, reason = OpenSSL::OCSP::REVOKED_STATUS_KEYCOMPROMISE) + Puppet.notice "Revoked certificate with serial %s" % serial + time = Time.now + + # Add our revocation to the CRL. + revoked = OpenSSL::X509::Revoked.new + revoked.serial = serial + revoked.time = time + enum = OpenSSL::ASN1::Enumerated(reason) + ext = OpenSSL::X509::Extension.new("CRLReason", enum) + revoked.add_extension(ext) + @content.add_revoked(revoked) + + # Increment the crlNumber + e = @content.extensions.find { |e| e.oid == 'crlNumber' } + ext = @content.extensions.reject { |e| e.oid == 'crlNumber' } + crlNum = OpenSSL::ASN1::Integer(e ? e.value.to_i + 1 : 0) + ext << OpenSSL::X509::Extension.new("crlNumber", crlNum) + @content.extensions = ext + + # Set last/next update + @content.last_update = time + # Keep CRL valid for 5 years + @content.next_update = time + 5 * 365*24*60*60 + + @content.sign(cakey, OpenSSL::Digest::SHA1.new) + + save + end +end diff --git a/lib/puppet/ssl/host.rb b/lib/puppet/ssl/host.rb new file mode 100644 index 000000000..e366bfbdd --- /dev/null +++ b/lib/puppet/ssl/host.rb @@ -0,0 +1,185 @@ +require 'puppet/ssl' +require 'puppet/ssl/key' +require 'puppet/ssl/certificate' +require 'puppet/ssl/certificate_request' +require 'puppet/ssl/certificate_revocation_list' +require 'puppet/util/constant_inflector' + +# The class that manages all aspects of our SSL certificates -- +# private keys, public keys, requests, etc. +class Puppet::SSL::Host + # Yay, ruby's strange constant lookups. + Key = Puppet::SSL::Key + Certificate = Puppet::SSL::Certificate + CertificateRequest = Puppet::SSL::CertificateRequest + CertificateRevocationList = Puppet::SSL::CertificateRevocationList + + extend Puppet::Util::ConstantInflector + + attr_reader :name + attr_accessor :ca + + attr_writer :key, :certificate, :certificate_request + + CA_NAME = "ca" + + # This is the constant that people will use to mark that a given host is + # a certificate authority. + def self.ca_name + CA_NAME + end + + class << self + attr_reader :ca_location + end + + # Configure how our various classes interact with their various terminuses. + def self.configure_indirection(terminus, cache = nil) + Certificate.terminus_class = terminus + CertificateRequest.terminus_class = terminus + CertificateRevocationList.terminus_class = terminus + + if cache + # This is weird; we don't actually cache our keys or CRL, we + # use what would otherwise be the cache as our normal + # terminus. + Key.terminus_class = cache + else + Key.terminus_class = terminus + end + + if cache + Certificate.cache_class = cache + CertificateRequest.cache_class = cache + CertificateRevocationList.cache_class = cache + end + end + + # Specify how we expect to interact with our certificate authority. + def self.ca_location=(mode) + raise ArgumentError, "CA Mode can only be :local, :remote, or :none" unless [:local, :remote, :none].include?(mode) + + @ca_mode = mode + + case @ca_mode + when :local: + # Our ca is local, so we use it as the ultimate source of information + # And we cache files locally. + configure_indirection :ca, :file + when :remote: + configure_indirection :rest, :file + when :none: + # We have no CA, so we just look in the local file store. + configure_indirection :file + end + end + + # Remove all traces of a given host + def self.destroy(name) + [Key, Certificate, CertificateRequest].inject(false) do |result, klass| + if klass.destroy(name) + result = true + end + result + end + end + + # Search for more than one host, optionally only specifying + # an interest in hosts with a given file type. + # This just allows our non-indirected class to have one of + # indirection methods. + def self.search(options = {}) + classes = [Key, CertificateRequest, Certificate] + if klass = options[:for] + classlist = [klass].flatten + else + classlist = [Key, CertificateRequest, Certificate] + end + + # Collect the results from each class, flatten them, collect all of the names, make the name list unique, + # then create a Host instance for each one. + classlist.collect { |klass| klass.search }.flatten.collect { |r| r.name }.uniq.collect do |name| + new(name) + end + end + + # Is this a ca host, meaning that all of its files go in the CA location? + def ca? + ca + end + + def key + return nil unless @key ||= Key.find(name) + @key + end + + # This is the private key; we can create it from scratch + # with no inputs. + def generate_key + @key = Key.new(name) + @key.generate + @key.save + true + end + + def certificate_request + return nil unless @certificate_request ||= CertificateRequest.find(name) + @certificate_request + end + + # Our certificate request requires the key but that's all. + def generate_certificate_request + generate_key unless key + @certificate_request = CertificateRequest.new(name) + @certificate_request.generate(key.content) + @certificate_request.save + return true + end + + def certificate + return nil unless @certificate ||= Certificate.find(name) + @certificate + end + + # Generate all necessary parts of our ssl host. + def generate + generate_key unless key + generate_certificate_request unless certificate_request + + # If we can get a CA instance, then we're a valid CA, and we + # should use it to sign our request; else, just try to read + # the cert. + if ! certificate() and ca = Puppet::SSL::CertificateAuthority.instance + ca.sign(self.name) + end + end + + def initialize(name = nil) + @name = name || Puppet[:certname] + @key = @certificate = @certificate_request = nil + @ca = (name == self.class.ca_name) + end + + # Extract the public key from the private key. + def public_key + key.content.public_key + end + + # Create/return a store that uses our SSL info to validate + # connections. + def ssl_store(purpose = OpenSSL::X509::PURPOSE_ANY) + store = OpenSSL::X509::Store.new + store.purpose = purpose + + store.add_file(Puppet[:localcacert]) + + # If there's a CRL, add it to our store. + if crl = Puppet::SSL::CertificateRevocationList.find("ca") + store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK_ALL|OpenSSL::X509::V_FLAG_CRL_CHECK + store.add_crl(crl.content) + end + return store + end +end + +require 'puppet/ssl/certificate_authority' diff --git a/lib/puppet/ssl/inventory.rb b/lib/puppet/ssl/inventory.rb new file mode 100644 index 000000000..38cbf46e9 --- /dev/null +++ b/lib/puppet/ssl/inventory.rb @@ -0,0 +1,52 @@ +require 'puppet/ssl' +require 'puppet/ssl/certificate' + +# Keep track of all of our known certificates. +class Puppet::SSL::Inventory + attr_reader :path + + # Add a certificate to our inventory. + def add(cert) + cert = cert.content if cert.is_a?(Puppet::SSL::Certificate) + + # Create our file, if one does not already exist. + rebuild unless FileTest.exist?(@path) + + Puppet.settings.write(:cert_inventory, "a") do |f| + f.print format(cert) + end + end + + # Format our certificate for output. + def format(cert) + iso = '%Y-%m-%dT%H:%M:%S%Z' + return "0x%04x %s %s %s\n" % [cert.serial, cert.not_before.strftime(iso), cert.not_after.strftime(iso), cert.subject] + end + + def initialize + @path = Puppet[:cert_inventory] + end + + # Rebuild the inventory from scratch. This should happen if + # the file is entirely missing or if it's somehow corrupted. + def rebuild + Puppet.notice "Rebuilding inventory file" + + Puppet.settings.write(:cert_inventory) do |f| + f.print "# Inventory of signed certificates\n# SERIAL NOT_BEFORE NOT_AFTER SUBJECT\n" + end + + Puppet::SSL::Certificate.search("*").each { |cert| add(cert) } + end + + # Find the serial number for a given certificate. + def serial(name) + return nil unless FileTest.exist?(@path) + + File.readlines(@path).each do |line| + next unless line =~ /^(\S+).+\/CN=#{name}$/ + + return Integer($1) + end + end +end diff --git a/lib/puppet/ssl/key.rb b/lib/puppet/ssl/key.rb new file mode 100644 index 000000000..a1d436090 --- /dev/null +++ b/lib/puppet/ssl/key.rb @@ -0,0 +1,50 @@ +require 'puppet/ssl/base' +require 'puppet/indirector' + +# Manage private and public keys as a pair. +class Puppet::SSL::Key < Puppet::SSL::Base + wraps OpenSSL::PKey::RSA + + extend Puppet::Indirector + indirects :key, :terminus_class => :file + + attr_accessor :password_file + + # Knows how to create keys with our system defaults. + def generate + Puppet.info "Creating a new SSL key for %s" % name + @content = OpenSSL::PKey::RSA.new(Puppet[:keylength].to_i) + end + + def initialize(name) + super + + if ca? + @password_file = Puppet[:capass] + else + @password_file = Puppet[:passfile] + end + end + + def password + return nil unless password_file and FileTest.exist?(password_file) + + ::File.read(password_file) + end + + # Optionally support specifying a password file. + def read(path) + return super unless password_file + + #@content = wrapped_class.new(::File.read(path), password) + @content = wrapped_class.new(::File.read(path), password) + end + + def to_s + if pass = password + @content.export(OpenSSL::Cipher::DES.new(:EDE3, :CBC), pass) + else + return super + end + end +end diff --git a/lib/puppet/transaction.rb b/lib/puppet/transaction.rb index b191f8219..fb00a592d 100644 --- a/lib/puppet/transaction.rb +++ b/lib/puppet/transaction.rb @@ -153,7 +153,7 @@ class Transaction # contained resources might never get cleaned up. def cleanup if defined? @generated - relationship_graph.remove_resource(*@generated) + catalog.remove_resource(*@generated) end end diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb index f8949ec90..7deb25fff 100644 --- a/lib/puppet/type.rb +++ b/lib/puppet/type.rb @@ -229,14 +229,14 @@ class Type end # If the name and title differ, set up an alias - if self.name != self.title - if obj = self.class[self.name] + if self.name != self.title and self.catalog + if obj = catalog.resource(self.class.name, self.name) if self.class.isomorphic? raise Puppet::Error, "%s already exists with name %s" % [obj.title, self.name] end else - self.class.alias(self.name, self) + catalog.alias(self, self.name) end end diff --git a/lib/puppet/type/component.rb b/lib/puppet/type/component.rb index 356205089..1d1bc9ee8 100644 --- a/lib/puppet/type/component.rb +++ b/lib/puppet/type/component.rb @@ -88,8 +88,8 @@ Puppet::Type.newtype(:component) do @reference = Puppet::ResourceReference.new(:component, @title) - unless self.class[@reference.to_s] - self.class.alias(@reference.to_s, self) + if catalog and ! catalog.resource[@reference.to_s] + catalog.alias(self, @reference.to_s) end end diff --git a/lib/puppet/type/file.rb b/lib/puppet/type/file.rb index 3518e8bb3..ef010efda 100644 --- a/lib/puppet/type/file.rb +++ b/lib/puppet/type/file.rb @@ -78,9 +78,8 @@ module Puppet munge do |value| # I don't really know how this is happening. - if value.is_a?(Array) - value = value.shift - end + value = value.shift if value.is_a?(Array) + case value when false, "false", :false: false @@ -92,7 +91,7 @@ module Puppet # We can't depend on looking this up right now, # we have to do it after all of the objects # have been instantiated. - if bucketobj = Puppet::Type.type(:filebucket)[value] + if resource.catalog and bucketobj = resource.catalog.resource(:filebucket, value) @resource.bucket = bucketobj.bucket bucketobj.title else @@ -304,30 +303,23 @@ module Puppet # We have to do some extra finishing, to retrieve our bucket if # there is one. def finish - # Let's cache these values, since there should really only be - # a couple of these buckets - @@filebuckets ||= {} - # Look up our bucket, if there is one if bucket = self.bucket case bucket when String: - if obj = @@filebuckets[bucket] - # This sets the @value on :backup, too - self.bucket = obj + if catalog and obj = catalog.resource(:filebucket, bucket) + self.bucket = obj.bucket elsif bucket == "puppet" obj = Puppet::Network::Client.client(:Dipper).new( :Path => Puppet[:clientbucketdir] ) self.bucket = obj - @@filebuckets[bucket] = obj - elsif obj = Puppet::Type.type(:filebucket).bucket(bucket) - @@filebuckets[bucket] = obj - self.bucket = obj else - self.fail "Could not find filebucket %s" % bucket + self.fail "Could not find filebucket '%s'" % bucket end when Puppet::Network::Client.client(:Dipper): # things are hunky-dorey + when Puppet::Type::Filebucket # things are hunky-dorey + self.bucket = bucket.bucket else self.fail "Invalid bucket type %s" % bucket.class end @@ -432,8 +424,7 @@ module Puppet end when "link": return true else - self.notice "Cannot backup files of type %s" % - File.stat(file).ftype + self.notice "Cannot backup files of type %s" % File.stat(file).ftype return false end end @@ -805,7 +796,6 @@ module Puppet # a wrapper method to make sure the file exists before doing anything def retrieve unless stat = self.stat(true) - self.debug "File does not exist" # If the file doesn't exist but we have a source, then call # retrieve on that property diff --git a/lib/puppet/type/filebucket.rb b/lib/puppet/type/filebucket.rb index b268610e9..872bded4e 100755 --- a/lib/puppet/type/filebucket.rb +++ b/lib/puppet/type/filebucket.rb @@ -54,21 +54,9 @@ module Puppet defaultto { Puppet[:clientbucketdir] } end - # get the actual filebucket object - def self.bucket(name) - if object = self[name] - return object.bucket - else - return nil - end - end - # Create a default filebucket. def self.mkdefaultbucket - unless default = self["puppet"] - return self.create(:name => "puppet", :path => Puppet[:clientbucketdir]) - end - return nil + self.create(:name => "puppet", :path => Puppet[:clientbucketdir]) end def self.instances diff --git a/lib/puppet/type/schedule.rb b/lib/puppet/type/schedule.rb index b8479e01d..ecbf7d49a 100755 --- a/lib/puppet/type/schedule.rb +++ b/lib/puppet/type/schedule.rb @@ -241,7 +241,9 @@ module Puppet :daily => :day, :monthly => :month, :weekly => proc do |prev, now| - prev.strftime("%U") != now.strftime("%U") + # Run the resource if the previous day was after this weekday (e.g., prev is wed, current is tue) + # or if it's been more than a week since we ran + prev.wday > now.wday or (now - prev) > (24 * 3600 * 7) end } @@ -314,8 +316,6 @@ module Puppet end def self.mkdefaultschedules - return [] if self["puppet"] - result = [] Puppet.debug "Creating default schedules" result << self.create( @@ -326,12 +326,10 @@ module Puppet # And then one for every period @parameters.find { |p| p.name == :period }.values.each { |value| - unless self[value.to_s] - result << self.create( - :name => value.to_s, - :period => value - ) - end + result << self.create( + :name => value.to_s, + :period => value + ) } result diff --git a/lib/puppet/type/user.rb b/lib/puppet/type/user.rb index 71507d172..0b668395d 100755 --- a/lib/puppet/type/user.rb +++ b/lib/puppet/type/user.rb @@ -302,19 +302,7 @@ module Puppet group end } - groups.each { |group| - case group - when Integer: - if obj = Puppet.type(:group).find { |gobj| - gobj.should(:gid) == group - } - autos << obj - - end - else - autos << group - end - } + autos = groups.reject { |g| g.is_a?(Integer) } end if obj = @parameters[:groups] and groups = obj.should diff --git a/lib/puppet/type/yumrepo.rb b/lib/puppet/type/yumrepo.rb index acb3b9b83..8800dd71d 100644 --- a/lib/puppet/type/yumrepo.rb +++ b/lib/puppet/type/yumrepo.rb @@ -67,8 +67,9 @@ module Puppet class << self attr_accessor :filetype # The writer is only used for testing, there should be no need - # to change yumconf in any other context + # to change yumconf or inifile in any other context attr_accessor :yumconf + attr_writer :inifile end self.filetype = Puppet::Util::FileType.filetype(:flat) @@ -181,11 +182,11 @@ module Puppet inifile.store end + # This is only used during testing. def self.clear @inifile = nil @yumconf = "/etc/yum.conf" @defaultrepodir = nil - super end # Return the Puppet::Util::IniConfig::Section for this yumrepo resource diff --git a/lib/puppet/util/cacher.rb b/lib/puppet/util/cacher.rb new file mode 100644 index 000000000..7b352c72a --- /dev/null +++ b/lib/puppet/util/cacher.rb @@ -0,0 +1,69 @@ +module Puppet::Util::Cacher + # Cause all cached values to be considered invalid. + def self.invalidate + @timestamp = Time.now + end + + def self.valid?(timestamp) + unless defined?(@timestamp) and @timestamp + @timestamp = Time.now + return true + end + return timestamp >= @timestamp + end + + def self.extended(other) + other.extend(InstanceMethods) + end + + def self.included(other) + other.extend(ClassMethods) + other.send(:include, InstanceMethods) + end + + module ClassMethods + private + + def cached_attr(name, &block) + define_method(name) do + attr_cache(name, &block) + end + end + end + + module InstanceMethods + private + + def attr_cache(name, &block) + unless defined?(@cacher_caches) and @cacher_caches + @cacher_caches = Cache.new + end + + @cacher_caches.value(name, &block) + end + end + + class Cache + attr_accessor :caches, :timestamp + + def initialize + @caches = {} + end + + def value(name) + raise ArgumentError, "You must provide a block when using the cache" unless block_given? + + if timestamp.nil? or ! Puppet::Util::Cacher.valid?(timestamp) + caches.clear + self.timestamp = Time.now + end + + # Use 'include?' here rather than testing for truth, so we + # can cache false values. + unless caches.include?(name) + caches[name] = yield + end + caches[name] + end + end +end diff --git a/lib/puppet/util/filetype.rb b/lib/puppet/util/filetype.rb index 8dcb67286..ae078fff4 100755 --- a/lib/puppet/util/filetype.rb +++ b/lib/puppet/util/filetype.rb @@ -74,8 +74,10 @@ class Puppet::Util::FileType # Pick or create a filebucket to use. def bucket - filebucket = Puppet::Type.type(:filebucket) - (filebucket["puppet"] || filebucket.mkdefaultbucket).bucket + unless defined?(@bucket) + @bucket = Puppet::Type.type(:filebucket).mkdefaultbucket.bucket + end + @bucket end def initialize(path) @@ -104,6 +106,9 @@ class Puppet::Util::FileType # Overwrite the file. def write(text) backup() + + raise("Cannot create file %s in absent directory" % @path) unless FileTest.exist?(File.dirname(@path)) + require "tempfile" tf = Tempfile.new("puppet") tf.print text; tf.flush diff --git a/lib/puppet/util/posix.rb b/lib/puppet/util/posix.rb index cc0340ef7..8228734ef 100755 --- a/lib/puppet/util/posix.rb +++ b/lib/puppet/util/posix.rb @@ -63,45 +63,6 @@ module Puppet::Util::POSIX return nil end - # Look in memory for an already-managed type and use its info if available. - # Currently unused. - def get_provider_value(type, field, id) - unless typeklass = Puppet::Type.type(type) - raise ArgumentError, "Invalid type %s" % type - end - - id = id.to_s - - chkfield = idfield(type) - obj = typeklass.find { |obj| - if id =~ /^\d+$/ - obj.should(chkfield).to_s == id || - obj.provider.send(chkfield) == id - else - obj[:name] == id - end - } - - return nil unless obj - - if obj.provider - begin - val = obj.provider.send(field) - if val == :absent - return nil - else - return val - end - rescue => detail - if Puppet[:trace] - puts detail.backtrace - Puppet.err detail - return nil - end - end - end - end - # Determine what the field name is for users and groups. def idfield(space) case Puppet::Util.symbolize(space) diff --git a/lib/puppet/util/settings.rb b/lib/puppet/util/settings.rb index 0e6f91e48..eec86625d 100644 --- a/lib/puppet/util/settings.rb +++ b/lib/puppet/util/settings.rb @@ -39,6 +39,8 @@ class Puppet::Util::Settings end @values[:memory][param] = value @cache.clear + + clearused end return value @@ -65,7 +67,6 @@ class Puppet::Util::Settings config = trans.to_catalog config.store_state = false config.apply - config.clear rescue => detail if Puppet[:trace] puts detail.backtrace @@ -312,94 +313,6 @@ class Puppet::Util::Settings end end - # Parse the configuration file. As of May 2007, this is a backward-compatibility method and - # will be deprecated soon. - def old_parse(file) - text = nil - - if file.is_a? Puppet::Util::LoadedFile - @file = file - else - @file = Puppet::Util::LoadedFile.new(file) - end - - # Don't create a timer for the old style parsing. - # settimer() - - begin - text = File.read(@file.file) - rescue Errno::ENOENT - raise Puppet::Error, "No such file %s" % file - rescue Errno::EACCES - raise Puppet::Error, "Permission denied to file %s" % file - end - - @values = Hash.new { |names, name| - names[name] = {} - } - - # Get rid of the values set by the file, keeping cli values. - self.clear(true) - - section = "puppet" - metas = %w{owner group mode} - values = Hash.new { |hash, key| hash[key] = {} } - text.split(/\n/).each { |line| - case line - when /^\[(\w+)\]$/: section = $1 # Section names - when /^\s*#/: next # Skip comments - when /^\s*$/: next # Skip blanks - when /^\s*(\w+)\s*=\s*(.+)$/: # settings - var = $1.intern - if var == :mode - value = $2 - else - value = munge_value($2) - end - - # Only warn if we don't know what this config var is. This - # prevents exceptions later on. - unless @config.include?(var) or metas.include?(var.to_s) - Puppet.warning "Discarded unknown configuration parameter %s" % var.inspect - next # Skip this line. - end - - # Mmm, "special" attributes - if metas.include?(var.to_s) - unless values.include?(section) - values[section] = {} - end - values[section][var.to_s] = value - - # If the parameter is valid, then set it. - if section == Puppet[:name] and @config.include?(var) - #@config[var].value = value - @values[:main][var] = value - end - next - end - - # Don't override set parameters, since the file is parsed - # after cli arguments are handled. - unless @config.include?(var) and @config[var].setbycli - Puppet.debug "%s: Setting %s to '%s'" % [section, var, value] - @values[:main][var] = value - end - @config[var].section = symbolize(section) - - metas.each { |meta| - if values[section][meta] - if @config[var].respond_to?(meta + "=") - @config[var].send(meta + "=", values[section][meta]) - end - end - } - else - raise Puppet::Error, "Could not match line %s" % line - end - } - end - # Create a new element. The value is passed in because it's used to determine # what kind of element we're creating, but the value itself might be either # a default or a value, so we can't actually assign it. @@ -663,7 +576,7 @@ Generated on #{Time.now}. catalog = bucket.to_catalog rescue => detail puts detail.backtrace if Puppet[:trace] - Puppet.err "Could not create resources for managing Puppet's files and directories: %s" % detail + Puppet.err "Could not create resources for managing Puppet's files and directories in sections %s: %s" % [sections.inspect, detail] # We need some way to get rid of any resources created during the catalog creation # but not cleaned up. @@ -674,11 +587,14 @@ Generated on #{Time.now}. catalog.host_config = false catalog.apply do |transaction| if failures = transaction.any_failed? - raise "Could not configure for running; got %s failure(s)" % failures + # LAK:NOTE We should do something like this for some cases, + # since it can otherwise be hard to know what failed. + #transaction.report.logs.find_all { |log| log.level == :err }.each do |log| + # puts log.message + #end + raise "Could not configure myself; got %s failure(s)" % failures end end - ensure - catalog.clear end sections.each { |s| @used << s } @@ -776,20 +692,26 @@ Generated on #{Time.now}. end sync.synchronize(Sync::EX) do - File.open(file, "r+", 0600) do |rf| + File.open(file, ::File::CREAT|::File::RDWR, 0600) do |rf| rf.lock_exclusive do if File.exist?(tmpfile) raise Puppet::Error, ".tmp file already exists for %s; Aborting locked write. Check the .tmp file and delete if appropriate" % [file] end - writesub(default, tmpfile, *args, &bloc) + # If there's a failure, remove our tmpfile + begin + writesub(default, tmpfile, *args, &bloc) + rescue + File.unlink(tmpfile) if FileTest.exist?(tmpfile) + raise + end begin File.rename(tmpfile, file) rescue => detail - Puppet.err "Could not rename %s to %s: %s" % - [file, tmpfile, detail] + Puppet.err "Could not rename %s to %s: %s" % [file, tmpfile, detail] + File.unlink(tmpfile) if FileTest.exist?(tmpfile) end end end @@ -1163,7 +1085,6 @@ Generated on #{Time.now}. return nil unless path.is_a?(String) return nil if path =~ /^\/dev/ - return nil if Puppet::Type.type(:file)[path] # skip files that are in our global resource list. objects = [] |