diff options
author | luke <luke@980ebf18-57e1-0310-9a29-db15c13687c0> | 2007-03-06 19:03:05 +0000 |
---|---|---|
committer | luke <luke@980ebf18-57e1-0310-9a29-db15c13687c0> | 2007-03-06 19:03:05 +0000 |
commit | 46d344b9daa24047b60183cc94509d306b6b562a (patch) | |
tree | 3c11eaad696ba3d6e6dd40bd7b9e7d1a4a71af85 /lib | |
parent | 68233706a9ff05be8fa8ab3ab7198cd0918517d6 (diff) | |
download | puppet-46d344b9daa24047b60183cc94509d306b6b562a.tar.gz puppet-46d344b9daa24047b60183cc94509d306b6b562a.tar.xz puppet-46d344b9daa24047b60183cc94509d306b6b562a.zip |
Merging the webserver_portability branch from version 2182 to version 2258.
git-svn-id: https://reductivelabs.com/svn/puppet/trunk@2259 980ebf18-57e1-0310-9a29-db15c13687c0
Diffstat (limited to 'lib')
55 files changed, 1494 insertions, 1365 deletions
diff --git a/lib/puppet.rb b/lib/puppet.rb index 6498d74a8..b0c6ee9f8 100644 --- a/lib/puppet.rb +++ b/lib/puppet.rb @@ -35,15 +35,7 @@ module Puppet # lazy. attr_accessor :args attr_reader :features - end - - - def Puppet.execname - unless defined? @name - @name = $0.gsub(/.+#{File::SEPARATOR}/,'').sub(/\.rb$/, '') - end - - return @name + attr_writer :name end # the hash that determines how our system behaves @@ -78,9 +70,6 @@ module Puppet @@config.setdefaults(section, hash) end - # Load all of the configuration parameters. - require 'puppet/configuration' - # configuration parameter access and stuff def self.[](param) case param @@ -116,6 +105,9 @@ module Puppet @@config end + # Load all of the configuration parameters. + require 'puppet/configuration' + def self.genconfig if Puppet[:configprint] != "" val = Puppet[:configprint] @@ -268,6 +260,7 @@ module Puppet # Stop our services defined? @services and @services.each do |svc| + next unless svc.respond_to?(:shutdown) begin timeout(20) do svc.shutdown @@ -389,9 +382,9 @@ module Puppet end end -require 'puppet/network/server' require 'puppet/type' require 'puppet/util/storage' +require 'puppet/parser/interpreter' if Puppet[:storeconfigs] require 'puppet/rails' end diff --git a/lib/puppet/configuration.rb b/lib/puppet/configuration.rb index 5b2d90af8..5764a2139 100644 --- a/lib/puppet/configuration.rb +++ b/lib/puppet/configuration.rb @@ -4,7 +4,8 @@ module Puppet # use basedirs that are in the user's home directory. conf = nil var = nil - if self.name != "puppetmasterd" and Puppet::Util::SUIDManager.uid != 0 + name = $0.gsub(/.+#{File::SEPARATOR}/,'').sub(/\.rb$/, '') + if name != "puppetmasterd" and Puppet::Util::SUIDManager.uid != 0 conf = File.expand_path("~/.puppet") var = File.expand_path("~/.puppet/var") else @@ -15,10 +16,12 @@ module Puppet self.setdefaults(:puppet, :confdir => [conf, "The main Puppet configuration directory."], - :vardir => [var, "Where Puppet stores dynamic and growing data."] + :vardir => [var, "Where Puppet stores dynamic and growing data."], + :name => [name, "The name of the service, if we are running as one. The + default is essentially $0 without the path or '.rb'."] ) - if self.name == "puppetmasterd" + if name == "puppetmasterd" logopts = {:default => "$vardir/log", :mode => 0750, :owner => "$user", @@ -114,7 +117,14 @@ module Puppet # Define the config default. self.setdefaults(self.name, :config => ["$confdir/#{self.name}.conf", - "The configuration file for #{self.name}."] + "The configuration file for #{self.name}."], + :pidfile => ["", "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 + options are webrick and mongrel. If you use mongrel, you will need + a proxy in front of the process or processes, since Mongrel cannot + speak SSL."] ) self.setdefaults("puppetmasterd", @@ -145,7 +155,15 @@ module Puppet for determining which 'node' statement applies to the client. Possible values are 'cert' (use the subject's CN in the client's certificate) and 'facter' (use the hostname that the client - reported in its facts)"] + reported in its facts)"], + :bucketdir => { + :default => "$vardir/bucket", + :mode => 0750, + :owner => "$user", + :group => "$group", + :desc => "Where FileBucket files are stored." + }, + :ca => [true, "Wether the master should function as a certificate authority."] ) self.setdefaults("puppetd", @@ -179,7 +197,121 @@ module Puppet :puppetport => [8139, "Which port puppetd listens on."], :noop => [false, "Whether puppetd should be run in noop mode."], :runinterval => [1800, # 30 minutes - "How often puppetd applies the client configuration; in seconds"] + "How often puppetd applies the client configuration; in seconds"], + :listen => [false, "Whether puppetd should listen for + connections. If this is true, then by default only the + ``runner`` server is started, which allows remote authorized + and authenticated nodes to connect and trigger ``puppetd`` + runs."], + :ca_server => ["$server", "The server to use for certificate + authority requests. It's a separate server because it cannot + and does not need to horizontally scale."], + :ca_port => ["$master_port", "The port to use for the certificate authority."] + ) + + self.setdefaults("filebucket", + :clientbucketdir => { + :default => "$vardir/clientbucket", + :mode => 0750, + :desc => "Where FileBucket files are stored locally." + } + ) + self.setdefaults("fileserver", + :fileserverconfig => ["$confdir/fileserver.conf", + "Where the fileserver configuration is stored."] + ) + self.setdefaults(:reporting, + :reports => ["store", + "The list of reports to generate. All reports are looked for + in puppet/reports/<name>.rb, and multiple report names should be + comma-separated (whitespace is okay)." + ], + :reportdir => {:default => "$vardir/reports", + :mode => 0750, + :owner => "$user", + :group => "$group", + :desc => "The directory in which to store reports + received from the client. Each client gets a separate + subdirectory."} + ) + self.setdefaults("puppetd", + :puppetdlockfile => [ "$statedir/puppetdlock", + "A lock file to temporarily stop puppetd from doing anything."], + :usecacheonfailure => [true, + "Whether to use the cached configuration when the remote + configuration will not compile. This option is useful for testing + new configurations, where you want to fix the broken configuration + rather than reverting to a known-good one." + ], + :ignorecache => [false, + "Ignore cache and always recompile the configuration. This is + useful for testing new configurations, where the local cache may in + fact be stale even if the timestamps are up to date - if the facts + change or if the server changes." + ], + :downcasefacts => [false, + "Whether facts should be made all lowercase when sent to the server."] + ) + + self.setdefaults(:puppetd, + :configtimeout => [120, + "How long the client should wait for the configuration to be retrieved + before considering it a failure. This can help reduce flapping if too + many clients contact the server at one time." + ], + :reportserver => ["$server", + "The server to which to send transaction reports." + ], + :report => [false, + "Whether to send reports after every transaction." + ] + ) + + # Plugin information. + self.setdefaults("puppet", + :pluginpath => ["$vardir/plugins", + "Where Puppet should look for plugins. Multiple directories should + be colon-separated, like normal PATH variables."], + :plugindest => ["$vardir/plugins", + "Where Puppet should store plugins that it pulls down from the central + server."], + :pluginsource => ["puppet://$server/plugins", + "From where to retrieve plugins. The standard Puppet ``file`` type + is used for retrieval, so anything that is a valid file source can + be used here."], + :pluginsync => [false, + "Whether plugins should be synced with the central server."], + :pluginsignore => [".svn CVS", + "What files to ignore when pulling down plugins."] + ) + + # Central fact information. + self.setdefaults("puppet", + :factpath => ["$vardir/facts", + "Where Puppet should look for facts. Multiple directories should + be colon-separated, like normal PATH variables."], + :factdest => ["$vardir/facts", + "Where Puppet should store facts that it pulls down from the central + server."], + :factsource => ["puppet://$server/facts", + "From where to retrieve facts. The standard Puppet ``file`` type + is used for retrieval, so anything that is a valid file source can + be used here."], + :factsync => [false, + "Whether facts should be synced with the central server."], + :factsignore => [".svn CVS", + "What files to ignore when pulling down facts."] + ) + + self.setdefaults(:reporting, + :tagmap => ["$confdir/tagmail.conf", + "The mapping between reporting tags and email addresses."], + :sendmail => [%x{which sendmail 2>/dev/null}.chomp, + "Where to find the sendmail binary with which to send email."], + :reportfrom => ["report@" + [Facter["hostname"].value, Facter["domain"].value].join("."), + "The 'from' email address for the reports."], + :smtpserver => ["none", + "The server through which to send email reports."] ) end diff --git a/lib/puppet/daemon.rb b/lib/puppet/daemon.rb index 13caf4541..52197f562 100755 --- a/lib/puppet/daemon.rb +++ b/lib/puppet/daemon.rb @@ -1,284 +1,83 @@ -# helper functions for daemons - require 'puppet' require 'puppet/util/pidlock' -module Puppet - # A module that handles operations common to all daemons. This is included - # into the Server and Client base classes. - module Daemon - include Puppet::Util - - Puppet.config.setdefaults(:puppet, :setpidfile => [true, - "Whether to store a PID file for the daemon."]) - def daemonname - #$0.sub(/.+#{File::SEPARATOR}/,'') - Puppet.execname - end - - # The path to the pid file for this server - def pidfile - File.join(Puppet[:rundir], daemonname() + ".pid") - end - - # Put the daemon into the background. - def daemonize - if pid = fork() - Process.detach(pid) - exit(0) - end +# A module that handles operations common to all daemons. This is included +# into the Server and Client base classes. +module Puppet::Daemon + include Puppet::Util - # Get rid of console logging - Puppet::Util::Log.close(:console) + def daemonname + Puppet[:name] + end - 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.execname, detail] - } - Puppet.err "Could not start %s: %s" % [Puppet.execname, detail] - exit(12) - end + # Put the daemon into the background. + def daemonize + if pid = fork() + Process.detach(pid) + exit(0) end - - def fqdn - unless defined? @fqdn and @fqdn - hostname = Facter.value("hostname") - domain = Facter.value("domain") - if !domain || domain.empty? then - @fqdn = hostname - else - @fqdn = [hostname, domain].join(".") - end - end - return @fqdn + + setpidfile() + + # 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] + } + Puppet.err "Could not start %s: %s" % [Puppet[:name], detail] + exit(12) end + end - def httplog - args = [] - - # yuck; separate http logs - file = nil - Puppet.config.use(:puppet, :certificates, Puppet.execname) - if Puppet.execname == "puppetmasterd" - file = Puppet[:masterhttplog] - else - file = Puppet[:httplog] - end -# -# unless FileTest.exists?(File.dirname(file)) -# Puppet.recmkdir(File.dirname(file)) -# end - - args << file - if Puppet[:debug] - args << WEBrick::Log::DEBUG - end - - log = WEBrick::Log.new(*args) - - - return log - end - - # Read in an existing certificate. - def readcert - return unless @secureinit - Puppet.config.use(:puppet, :certificates) - # verify we've got all of the certs set up and such - - if defined? @cert and defined? @key and @cert and @key - return true - end - - unless defined? @fqdn - self.fqdn - end - - # we are not going to encrypt our key, but we need at a minimum - # a keyfile and a certfile - #@certfile = File.join(Puppet[:certdir], [@fqdn, "pem"].join(".")) - #@cacertfile = File.join(Puppet[:certdir], ["ca", "pem"].join(".")) - #@keyfile = File.join(Puppet[:privatekeydir], [@fqdn, "pem"].join(".")) - #@publickeyfile = File.join(Puppet[:publickeydir], [@fqdn, "pem"].join(".")) - @certfile = Puppet[:hostcert] - @cacertfile = Puppet[:localcacert] - @keyfile = Puppet[:hostprivkey] - @publickeyfile = Puppet[:hostpubkey] - - if File.exists?(@keyfile) - # load the key - @key = OpenSSL::PKey::RSA.new(File.read(@keyfile)) - else - return false - end - - if File.exists?(@certfile) - if File.exists?(@cacertfile) - @cacert = OpenSSL::X509::Certificate.new(File.read(@cacertfile)) - else - raise Puppet::Error, "Found cert file with no ca cert file" - end - @cert = OpenSSL::X509::Certificate.new(File.read(@certfile)) - else - return false - end - return true - end - - # Request a certificate from the remote system. This does all of the work - # of creating the cert request, contacting the remote system, and - # storing the cert locally. - def requestcert - unless @secureinit - raise Puppet::DevError, - "Tried to request cert without initialized security" - end - retrieved = false - Puppet.config.use(:puppet, :certificates) - # create the directories involved - # FIXME it's a stupid hack that i have to do this -# [Puppet[:certdir], Puppet[:privatekeydir], Puppet[:csrdir], -# Puppet[:publickeydir]].each { |dir| -# unless FileTest.exists?(dir) -# Puppet.recmkdir(dir, 0770) -# end -# } - - if self.readcert - Puppet.info "Certificate already exists; not requesting" - return true - end - - unless defined? @key and @key - # create a new one and store it - Puppet.info "Creating a new SSL key at %s" % @keyfile - @key = OpenSSL::PKey::RSA.new(Puppet[:keylength]) - Puppet.config.write(:hostprivkey) do |f| f.print @key.to_pem end - Puppet.config.write(:hostpubkey) do |f| - f.print @key.public_key.to_pem - end - #File.open(@keyfile, "w", 0660) { |f| f.print @key.to_pem } - #File.open(@publickeyfile, "w", 0660) { |f| - # f.print @key.public_key.to_pem - #} - end - - - unless defined? @driver - Puppet.err "Cannot request a certificate without a defined target" - return false - end - - unless defined? @csr - Puppet.info "Creating a new certificate request for %s" % @fqdn - name = OpenSSL::X509::Name.new([["CN", @fqdn]]) - - @csr = OpenSSL::X509::Request.new - @csr.version = 0 - @csr.subject = name - @csr.public_key = @key.public_key - @csr.sign(@key, OpenSSL::Digest::MD5.new) - end - - Puppet.info "Requesting certificate" - - # We can only request a client with a CA client, so we need - # to create one if we don't already have one (or if we're not a CA - # server). - caclient = nil - if @driver.is_a? Puppet::Network::Client::CA or @driver.is_a? Puppet::Network::Server::CA - caclient = @driver - else - # Create a CA client with which to request the cert. - if @driver.local? - raise Puppet::DevError, - "Incorrect setup for a local CA request" - end - caclient = Puppet::Network::Client::CA.new( - :Port => @driver.puppet_port, - :Server => @driver.puppet_server - ) - end - - begin - cert, cacert = caclient.getcert(@csr.to_pem) - rescue => detail - if Puppet[:trace] - puts detail.backtrace - end - raise Puppet::Error.new("Certificate retrieval failed: %s" % - detail) - end - - if cert.nil? or cert == "" - return nil - end - Puppet.config.write(:hostcert) do |f| f.print cert end - Puppet.config.write(:localcacert) do |f| f.print cacert end - #File.open(@certfile, "w", 0644) { |f| f.print cert } - #File.open(@cacertfile, "w", 0644) { |f| f.print cacert } - begin - @cert = OpenSSL::X509::Certificate.new(cert) - @cacert = OpenSSL::X509::Certificate.new(cacert) - retrieved = true - rescue => detail - raise Puppet::Error.new( - "Invalid certificate: %s" % detail - ) - end - - unless @cert.check_private_key(@key) - raise Puppet::DevError, "Received invalid certificate" - end - return retrieved + # The path to the pid file for this server + def pidfile + if Puppet[:pidfile] != "" + Puppet[:pidfile] + else + File.join(Puppet[:rundir], daemonname() + ".pid") end + end - # Remove the pid file - def rmpidfile - threadlock(:pidfile) do - locker = Puppet::Util::Pidlock.new(pidfile) - if locker.locked? - locker.unlock or Puppet.err "Could not remove PID file %s" % [pidfile] - end + # Remove the pid file + def rmpidfile + threadlock(:pidfile) 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 - # Create the pid file. - def setpidfile - return unless Puppet[:setpidfile] - - threadlock(:pidfile) do - unless Puppet::Util::Pidlock.new(pidfile).lock - Puppet.err("Could not create PID file: %s" % [pidfile]) - exit(74) - end + # Create the pid file. + def setpidfile + threadlock(:pidfile) do + unless Puppet::Util::Pidlock.new(pidfile).lock + Puppet.err("Could not create PID file: %s" % [pidfile]) + exit(74) end end + end - # Shut down our server - def shutdown - # Remove our pid file - rmpidfile() - - # And close all logs except the console. - Puppet::Util::Log.destinations.reject { |d| d == :console }.each do |dest| - Puppet::Util::Log.close(dest) - end + # Shut down our server + def shutdown + # Remove our pid file + rmpidfile() - super + # And close all logs except the console. + Puppet::Util::Log.destinations.reject { |d| d == :console }.each do |dest| + Puppet::Util::Log.close(dest) end - def start - setpidfile() - super - end + super end end diff --git a/lib/puppet/dsl.rb b/lib/puppet/dsl.rb index 09a4e2b61..9c652f082 100644 --- a/lib/puppet/dsl.rb +++ b/lib/puppet/dsl.rb @@ -68,7 +68,7 @@ module Puppet def apply bucket = export() objects = bucket.to_type - master = Puppet::Network::Client::MasterClient.new :Master => "whatever" + master = Puppet::Network::Client.master.new :Master => "whatever" master.objects = objects master.apply diff --git a/lib/puppet/network/authconfig.rb b/lib/puppet/network/authconfig.rb index bf21fb5ce..19fbc6cf3 100644 --- a/lib/puppet/network/authconfig.rb +++ b/lib/puppet/network/authconfig.rb @@ -12,6 +12,13 @@ module Puppet ] ) + def self.main + unless defined? @main + @main = self.new() + end + @main + end + # Just proxy the setting methods to our rights stuff [:allow, :deny].each do |method| define_method(method) do |*args| @@ -19,24 +26,19 @@ module Puppet end end - # Here we add a little bit of semantics. They can set auth on a whole namespace - # or on just a single method in the namespace. - def allowed?(name, host, ip) - namespace, method = name.to_s.split(".") - unless namespace and method - raise ArgumentError, "Invalid method name %s" % name - end - - name = name.intern if name.is_a? String - namespace = namespace.intern - method = method.intern + # Here we add a little bit of semantics. They can set auth on a whole + # namespace or on just a single method in the namespace. + def allowed?(request) + name = request.call.intern + namespace = request.handler.intern + method = request.method.intern read() if @rights.include?(name) - return @rights[name].allowed?(host, ip) + return @rights[name].allowed?(request.name, request.ip) elsif @rights.include?(namespace) - return @rights[namespace].allowed?(host, ip) + return @rights[namespace].allowed?(request.name, request.ip) else return false end diff --git a/lib/puppet/network/authorization.rb b/lib/puppet/network/authorization.rb new file mode 100644 index 000000000..a3c1d50e1 --- /dev/null +++ b/lib/puppet/network/authorization.rb @@ -0,0 +1,84 @@ +require 'puppet/network/client_request' +require 'puppet/network/authconfig' + +module Puppet::Network + # Most of our subclassing is just so that we can get + # access to information from the request object, like + # the client name and IP address. + class InvalidClientRequest < Puppet::Error; end + module Authorization + # Create our config object if necessary. This works even if + # there's no configuration file. + def authconfig + unless defined? @authconfig + @authconfig = Puppet::Network::AuthConfig.main() + end + + @authconfig + end + + # Verify that our client has access. We allow untrusted access to + # puppetca methods but no others. + def authorized?(request) + msg = "%s client %s access to %s" % + [request.authenticated? ? "authenticated" : "unauthenticated", + request, request.call] + + if request.authenticated? + if authconfig.exists? + if authconfig.allowed?(request) + Puppet.debug "Allowing " + msg + return true + else + Puppet.notice "Denying " + msg + return false + end + else + # This is a hack way of seeing if we're a config master. + if Puppet[:name] == "puppetmasterd" + Puppet.debug "Allowing " + msg + return true + else + Puppet.notice "Denying " + msg + return false + end + end + else + if request.handler == "puppetca" + Puppet.notice "Allowing " + msg + else + Puppet.notice "Denying " + msg + return false + end + end + end + + # Is this functionality available? + def available?(request) + if handler_loaded?(request.handler) + return true + else + Puppet.warning "Client %s requested unavailable functionality %s" % + [request, request.handler] + return false + end + end + + # Make sure that this method is available and authorized. + def verify(request) + unless available?(request) + raise InvalidClientRequest.new( + "Functionality %s not available" % request.handler + ) + end + unless authorized?(request) + raise InvalidClientRequest.new( + "Host %s not authorized to call %s" % + [request, request.call] + ) + end + end + end +end + +# $Id$ diff --git a/lib/puppet/network/client.rb b/lib/puppet/network/client.rb index 604912025..c08acabb1 100644 --- a/lib/puppet/network/client.rb +++ b/lib/puppet/network/client.rb @@ -1,60 +1,73 @@ # the available clients require 'puppet' -require 'puppet/network/networkclient' +require 'puppet/daemon' +require 'puppet/network/xmlrpc/client' +require 'puppet/util/subclass_loader' +require 'puppet/util/methodhelper' +require 'puppet/sslcertificates/support' + +require 'net/http' + +# Some versions of ruby don't have this method defined, which basically causes +# us to never use ssl. Yay. +class Net::HTTP + def use_ssl? + if defined? @use_ssl + @use_ssl + else + false + end + end +end -# FIXME this still isn't a good design, because none of the handlers overlap -# so i could just as easily include them all in the main module -# but at least it's better organized for now +# The base class for all of the clients. Many clients just directly +# call methods, but some of them need to do some extra work or +# provide a different interface. class Puppet::Network::Client + Client = self include Puppet::Daemon include Puppet::Util + extend Puppet::Util::SubclassLoader + include Puppet::Util::MethodHelper + + # This handles reading in the key and such-like. + include Puppet::SSLCertificates::Support - # FIXME The cert stuff should only come up with networking, so it - # should be in the network client, not the normal client. But if i do - # that, it's hard to tell whether the certs have been initialized. - include Puppet::Daemon - attr_reader :secureinit attr_accessor :schedule, :lastrun, :local, :stopping - class << self - attr_reader :drivername, :handler - attr_accessor :netclient - end + # Set up subclass loading + handle_subclasses :client, "puppet/network/client" - def initcerts - unless self.readcert - #if self.is_a? Puppet::Network::Client::CA - unless self.requestcert - return nil - end - #else - # return nil - #end - #unless self.requestcert - #end + # Determine what clients look for when being passed an object for local + # client/server stuff. E.g., you could call Client::CA.new(:CA => ca). + def self.drivername + unless defined? @drivername + @drivername = self.name end + @drivername + end - # unless we have a driver, we're a local client and we can't add - # certs anyway, so it doesn't matter - unless @driver - return true + # Figure out the handler for our client. + def self.handler + unless defined? @handler + @handler = Puppet::Network::Handler.handler(self.name) end + @handler + end - self.setcerts + # The class that handles xmlrpc interaction for us. + def self.xmlrpc_client + unless defined? @xmlrpc_client + @xmlrpc_client = Puppet::Network::XMLRPCClient.handler_class(self.handler) + end + @xmlrpc_client end + # Create our client. def initialize(hash) # to whom do we connect? @server = nil - @nil = nil - @secureinit = hash[:NoSecureInit] || true - - if hash.include?(:FQDN) - @fqdn = hash[:FQDN] - else - self.fqdn - end if hash.include?(:Cache) @cache = hash[:Cache] @@ -64,37 +77,24 @@ class Puppet::Network::Client driverparam = self.class.drivername if hash.include?(:Server) - if $noclientnetworking - raise NetworkClientError.new("Networking not available: %s" % - $nonetworking) - end - args = {:Server => hash[:Server]} args[:Port] = hash[:Port] || Puppet[:masterport] - if self.readcert - args[:Certificate] = @cert - args[:Key] = @key - args[:CAFile] = @cacertfile - end + @driver = self.class.xmlrpc_client.new(args) - netclient = nil - unless netclient = self.class.netclient - unless handler = self.class.handler - raise Puppet::DevError, - "Class %s has no handler defined" % self.class - end - namespace = self.class.handler.interface.prefix - netclient = Puppet::Network::NetworkClient.netclient(namespace) - self.class.netclient = netclient + if self.read_cert + @driver.cert_setup(self) end - @driver = netclient.new(args) + @local = false elsif hash.include?(driverparam) @driver = hash[driverparam] + if @driver == true + @driver = self.class.handler.new + end @local = true else - raise ClientError, "%s must be passed a Server or %s" % + raise Puppet::Network::ClientError, "%s must be passed a Server or %s" % [self.class, driverparam] end end @@ -138,12 +138,6 @@ class Puppet::Network::Client end end - def setcerts - @driver.cert = @cert - @driver.key = @key - @driver.ca_file = @cacertfile - end - def shutdown if self.stopping Puppet.notice "Already in shutdown" @@ -159,7 +153,6 @@ class Puppet::Network::Client # Start listening for events. We're pretty much just listening for # timer events here. def start - setpidfile() # Create our timer. Puppet will handle observing it and such. timer = Puppet.newtimer( :interval => Puppet[:runinterval], @@ -176,15 +169,6 @@ class Puppet::Network::Client end require 'puppet/network/client/proxy' - require 'puppet/network/client/ca' - require 'puppet/network/client/dipper' - require 'puppet/network/client/file' - require 'puppet/network/client/log' - require 'puppet/network/client/master' - require 'puppet/network/client/runner' - require 'puppet/network/client/status' - require 'puppet/network/client/reporter' - require 'puppet/network/client/resource' end # $Id$ diff --git a/lib/puppet/network/client/ca.rb b/lib/puppet/network/client/ca.rb index 9a99c1145..fe3cb060a 100644 --- a/lib/puppet/network/client/ca.rb +++ b/lib/puppet/network/client/ca.rb @@ -1,22 +1,55 @@ -require 'puppet/network/client/proxy' +require 'puppet/network/client' -class Puppet::Network::Client::CA < Puppet::Network::Client::ProxyClient - @drivername = :CA +# Request a certificate from the remote system. +class Puppet::Network::Client::CA < Puppet::Network::Client + class InvalidCertificate < Puppet::Error; end - # set up the appropriate interface methods - @handler = Puppet::Network::Server::CA - self.mkmethods + def initialize(options = {}) + options = symbolize_options(options) + unless options.include?(:Server) or options.include?(:CA) + options[:Server] = Puppet[:ca_server] + options[:Port] = Puppet[:ca_port] + end + super(options) + end + + # This client is really only able to request certificates for the + # current host. It uses the Puppet.config settings to figure everything out. + def request_cert + Puppet.config.use(:puppet, :certificates) + + if cert = read_cert + return cert + end - def initialize(hash = {}) - if hash.include?(:CA) - if hash[:CA].is_a? Hash - hash[:CA] = Puppet::Network::Server::CA.new(hash[:CA]) - else - hash[:CA] = Puppet::Network::Server::CA.new() + begin + cert, cacert = @driver.getcert(csr.to_pem) + rescue => detail + if Puppet[:trace] + puts detail.backtrace end + raise Puppet::Error.new("Certificate retrieval failed: %s" % detail) end - super(hash) + if cert.nil? or cert == "" + return nil + end + Puppet.config.write(:hostcert) do |f| f.print cert end + Puppet.config.write(:localcacert) do |f| f.print cacert end + + begin + @cert = OpenSSL::X509::Certificate.new(cert) + @cacert = OpenSSL::X509::Certificate.new(cacert) + rescue => detail + raise InvalidCertificate.new( + "Invalid certificate: %s" % detail + ) + end + + unless @cert.check_private_key(key) + raise InvalidCertificate, "Certificate does not match private key" + end + return @cert end end diff --git a/lib/puppet/network/client/dipper.rb b/lib/puppet/network/client/dipper.rb index 8eaffc1a0..2084e09f6 100644 --- a/lib/puppet/network/client/dipper.rb +++ b/lib/puppet/network/client/dipper.rb @@ -1,17 +1,14 @@ # The client class for filebuckets. class Puppet::Network::Client::Dipper < Puppet::Network::Client + @handler = Puppet::Network::Handler.handler(:filebucket) @drivername = :Bucket - - @handler = Puppet::Network::Server::FileBucket attr_accessor :name # Create our bucket client def initialize(hash = {}) if hash.include?(:Path) - bucket = Puppet::Network::Server::FileBucket.new( - :Path => hash[:Path] - ) + bucket = self.class.handler.new(:Path => hash[:Path]) hash.delete(:Path) hash[:Bucket] = bucket end @@ -24,7 +21,7 @@ class Puppet::Network::Client::Dipper < Puppet::Network::Client unless FileTest.exists?(file) raise(BucketError, "File %s does not exist" % file) end - contents = File.read(file) + contents = ::File.read(file) unless local? contents = Base64.encode64(contents) end @@ -35,7 +32,7 @@ class Puppet::Network::Client::Dipper < Puppet::Network::Client def restore(file,sum) restore = true if FileTest.exists?(file) - cursum = Digest::MD5.hexdigest(File.read(file)) + cursum = Digest::MD5.hexdigest(::File.read(file)) # if the checksum has changed... # this might be extra effort @@ -53,14 +50,14 @@ class Puppet::Network::Client::Dipper < Puppet::Network::Client newsum = Digest::MD5.hexdigest(newcontents) changed = nil unless FileTest.writable?(file) - changed = File.stat(file).mode - File.chmod(changed | 0200, file) + changed = ::File.stat(file).mode + ::File.chmod(changed | 0200, file) end - File.open(file,File::WRONLY|File::TRUNC) { |of| + ::File.open(file,::File::WRONLY|::File::TRUNC) { |of| of.print(newcontents) } if changed - File.chmod(changed, file) + ::File.chmod(changed, file) end else Puppet.err "Could not find file with checksum %s" % sum diff --git a/lib/puppet/network/client/file.rb b/lib/puppet/network/client/file.rb index 7596aec1f..381a10cdb 100644 --- a/lib/puppet/network/client/file.rb +++ b/lib/puppet/network/client/file.rb @@ -1,20 +1,7 @@ -class Puppet::Network::Client::FileClient < Puppet::Network::Client::ProxyClient +class Puppet::Network::Client::File < Puppet::Network::Client::ProxyClient + @handler = Puppet::Network::Handler.handler(:fileserver) @drivername = :FileServer - - # set up the appropriate interface methods - @handler = Puppet::Network::Server::FileServer - self.mkmethods - - def initialize(hash = {}) - if hash.include?(:FileServer) - unless hash[:FileServer].is_a?(Puppet::Network::Server::FileServer) - raise Puppet::DevError, "Must pass an actual FS object" - end - end - - super(hash) - end end # $Id$ diff --git a/lib/puppet/network/client/log.rb b/lib/puppet/network/client/log.rb deleted file mode 100644 index eddb8e0ca..000000000 --- a/lib/puppet/network/client/log.rb +++ /dev/null @@ -1,17 +0,0 @@ -class Puppet::Network::Client::LogClient < Puppet::Network::Client::ProxyClient - @drivername = :Logger - - # set up the appropriate interface methods - @handler = Puppet::Network::Server::Logger - self.mkmethods - - def initialize(hash = {}) - if hash.include?(:Logger) - hash[:Logger] = Puppet::Network::Server::Logger.new() - end - - super(hash) - end -end - -# $Id$ diff --git a/lib/puppet/network/client/logger.rb b/lib/puppet/network/client/logger.rb new file mode 100644 index 000000000..9c18efcd9 --- /dev/null +++ b/lib/puppet/network/client/logger.rb @@ -0,0 +1,6 @@ +class Puppet::Network::Client::Logger < Puppet::Network::Client::ProxyClient + @handler = Puppet::Network::Handler.handler(:logger) + self.mkmethods +end + +# $Id$ diff --git a/lib/puppet/network/client/master.rb b/lib/puppet/network/client/master.rb index ebeddd281..dd8a883bd 100644 --- a/lib/puppet/network/client/master.rb +++ b/lib/puppet/network/client/master.rb @@ -2,84 +2,11 @@ require 'sync' require 'timeout' -class Puppet::Network::Client::MasterClient < Puppet::Network::Client +class Puppet::Network::Client::Master < Puppet::Network::Client unless defined? @@sync @@sync = Sync.new end - @handler = Puppet::Network::Server::Master - - Puppet.setdefaults("puppetd", - :puppetdlockfile => [ "$statedir/puppetdlock", - "A lock file to temporarily stop puppetd from doing anything."], - :usecacheonfailure => [true, - "Whether to use the cached configuration when the remote - configuration will not compile. This option is useful for testing - new configurations, where you want to fix the broken configuration - rather than reverting to a known-good one." - ], - :ignorecache => [false, - "Ignore cache and always recompile the configuration. This is - useful for testing new configurations, where the local cache may in - fact be stale even if the timestamps are up to date - if the facts - change or if the server changes." - ], - :downcasefacts => [false, - "Whether facts should be made all lowercase when sent to the server."] - ) - - Puppet.setdefaults(:puppetd, - :configtimeout => [30, - "How long the client should wait for the configuration to be retrieved - before considering it a failure. This can help reduce flapping if too - many clients contact the server at one time." - ], - :reportserver => ["$server", - "The server to which to send transaction reports." - ], - :report => [false, - "Whether to send reports after every transaction." - ] - ) - - # Plugin information. - Puppet.setdefaults("puppet", - :pluginpath => ["$vardir/plugins", - "Where Puppet should look for plugins. Multiple directories should - be colon-separated, like normal PATH variables."], - :plugindest => ["$vardir/plugins", - "Where Puppet should store plugins that it pulls down from the central - server."], - :pluginsource => ["puppet://$server/plugins", - "From where to retrieve plugins. The standard Puppet ``file`` type - is used for retrieval, so anything that is a valid file source can - be used here."], - :pluginsync => [false, - "Whether plugins should be synced with the central server."], - :pluginsignore => [".svn CVS", - "What files to ignore when pulling down plugins."] - ) - - # Central fact information. - Puppet.setdefaults("puppet", - :factpath => ["$vardir/facts", - "Where Puppet should look for facts. Multiple directories should - be colon-separated, like normal PATH variables."], - :factdest => ["$vardir/facts", - "Where Puppet should store facts that it pulls down from the central - server."], - :factsource => ["puppet://$server/facts", - "From where to retrieve facts. The standard Puppet ``file`` type - is used for retrieval, so anything that is a valid file source can - be used here."], - :factsync => [false, - "Whether facts should be synced with the central server."], - :factsignore => [".svn CVS", - "What files to ignore when pulling down facts."] - ) - - @drivername = :Master - attr_accessor :objects attr_reader :compile_time @@ -160,11 +87,11 @@ class Puppet::Network::Client::MasterClient < Puppet::Network::Client # Cache the config def cache(text) Puppet.info "Caching configuration at %s" % self.cachefile - confdir = File.dirname(Puppet[:localconfig]) - File.open(self.cachefile + ".tmp", "w", 0660) { |f| + confdir = ::File.dirname(Puppet[:localconfig]) + ::File.open(self.cachefile + ".tmp", "w", 0660) { |f| f.print text } - File.rename(self.cachefile + ".tmp", self.cachefile) + ::File.rename(self.cachefile + ".tmp", self.cachefile) end def cachefile @@ -191,7 +118,7 @@ class Puppet::Network::Client::MasterClient < Puppet::Network::Client end Puppet.err "Corrupt state file %s: %s" % [Puppet[:statefile], detail] begin - File.unlink(Puppet[:statefile]) + ::File.unlink(Puppet[:statefile]) retry rescue => detail raise Puppet::Error.new("Cannot remove %s: %s" % @@ -336,7 +263,7 @@ class Puppet::Network::Client::MasterClient < Puppet::Network::Client # Retrieve the cached config def retrievecache if FileTest.exists?(self.cachefile) - return File.read(self.cachefile) + return ::File.read(self.cachefile) else return "" end @@ -395,7 +322,7 @@ class Puppet::Network::Client::MasterClient < Puppet::Network::Client return end begin - File.open(Puppet[:classfile], "w") { |f| + ::File.open(Puppet[:classfile], "w") { |f| f.puts ary.join("\n") } rescue => detail @@ -467,7 +394,7 @@ class Puppet::Network::Client::MasterClient < Puppet::Network::Client download(:dest => Puppet[:factdest], :source => Puppet[:factsource], :ignore => Puppet[:factsignore], :name => "fact") do |object| - next unless path.include?(File.dirname(object[:path])) + next unless path.include?(::File.dirname(object[:path])) files << object[:path] @@ -495,11 +422,11 @@ class Puppet::Network::Client::MasterClient < Puppet::Network::Client download(:dest => Puppet[:plugindest], :source => Puppet[:pluginsource], :ignore => Puppet[:pluginsignore], :name => "plugin") do |object| - next unless path.include?(File.dirname(object[:path])) + next unless path.include?(::File.dirname(object[:path])) begin Puppet.info "Reloading plugin %s" % - File.basename(File.basename(object[:path])).sub(".rb",'') + ::File.basename(::File.basename(object[:path])).sub(".rb",'') load object[:path] rescue => detail Puppet.warning "Could not reload plugin %s: %s" % @@ -512,9 +439,9 @@ class Puppet::Network::Client::MasterClient < Puppet::Network::Client return unless FileTest.directory?(dir) Dir.entries(dir).find_all { |e| e =~ /\.rb$/ }.each do |file| - fqfile = File.join(dir, file) + fqfile = ::File.join(dir, file) begin - Puppet.info "Loading #{type} %s" % File.basename(file.sub(".rb",'')) + Puppet.info "Loading #{type} %s" % ::File.basename(file.sub(".rb",'')) Timeout::timeout(self.timeout) do load fqfile end @@ -560,7 +487,7 @@ class Puppet::Network::Client::MasterClient < Puppet::Network::Client def reportclient unless defined? @reportclient - @reportclient = Puppet::Network::Client::Reporter.new( + @reportclient = Puppet::Network::Client.report.new( :Server => Puppet[:reportserver] ) end @@ -572,7 +499,8 @@ class Puppet::Network::Client::MasterClient < Puppet::Network::Client private - # Actually retrieve the configuration, either from the server or from a local master. + # Actually retrieve the configuration, either from the server or from a + # local master. def get_actual_config(facts) if @local return get_local_config(facts) diff --git a/lib/puppet/network/client/proxy.rb b/lib/puppet/network/client/proxy.rb index e1295a96f..4cd37bb5d 100644 --- a/lib/puppet/network/client/proxy.rb +++ b/lib/puppet/network/client/proxy.rb @@ -3,7 +3,7 @@ # and that's about it class Puppet::Network::Client::ProxyClient < Puppet::Network::Client def self.mkmethods - interface = @handler.interface + interface = self.handler.interface namespace = interface.prefix diff --git a/lib/puppet/network/client/reporter.rb b/lib/puppet/network/client/report.rb index dd340da02..cb4711afe 100644 --- a/lib/puppet/network/client/reporter.rb +++ b/lib/puppet/network/client/report.rb @@ -1,12 +1,9 @@ -class Puppet::Network::Client::Reporter < Puppet::Network::Client - @drivername = :Report - - # set up the appropriate interface methods - @handler = Puppet::Network::Server::Report +class Puppet::Network::Client::Report < Puppet::Network::Client + @handler = Puppet::Network::Handler.handler(:report) def initialize(hash = {}) if hash.include?(:Report) - hash[:Report] = Puppet::Network::Server::Report.new() + hash[:Report] = self.class.handler.new end super(hash) diff --git a/lib/puppet/network/client/resource.rb b/lib/puppet/network/client/resource.rb index 71a19bf91..bf026dff0 100644 --- a/lib/puppet/network/client/resource.rb +++ b/lib/puppet/network/client/resource.rb @@ -1,10 +1,7 @@ +# The client for interacting with remote Puppet agents to query and modify +# remote system state. class Puppet::Network::Client::Resource < Puppet::Network::Client - @drivername = :ResourceServer - - @handler = Puppet::Network::Server::Resource - def apply(bucket) - case bucket when Puppet::TransObject tmp = Puppet::TransBucket.new @@ -41,16 +38,6 @@ class Puppet::Network::Client::Resource < Puppet::Network::Client return object end - def initialize(hash = {}) - if hash.include?(:ResourceServer) - unless hash[:ResourceServer].is_a?(Puppet::Network::Server::Resource) - raise Puppet::DevError, "Must pass an actual PElement server object" - end - end - - super(hash) - end - def list(type, ignore = false, base = false) bucket = @driver.list(type, ignore, base, "yaml") diff --git a/lib/puppet/network/client/runner.rb b/lib/puppet/network/client/runner.rb index 40d13ac86..12097c52f 100644 --- a/lib/puppet/network/client/runner.rb +++ b/lib/puppet/network/client/runner.rb @@ -1,13 +1,9 @@ class Puppet::Network::Client::Runner < Puppet::Network::Client::ProxyClient - @drivername = :Runner - - # set up the appropriate interface methods - @handler = Puppet::Network::Server::Runner self.mkmethods def initialize(hash = {}) if hash.include?(:Runner) - hash[:Runner] = Puppet::Network::Server::Runner.new() + hash[:Runner] = self.class.handler.new() end super(hash) diff --git a/lib/puppet/network/client/status.rb b/lib/puppet/network/client/status.rb index 6c1a96e85..d35e44c1a 100644 --- a/lib/puppet/network/client/status.rb +++ b/lib/puppet/network/client/status.rb @@ -1,6 +1,4 @@ -class Puppet::Network::Client::StatusClient < Puppet::Network::Client::ProxyClient - # set up the appropriate interface methods - @handler = Puppet::Network::Server::ServerStatus +class Puppet::Network::Client::Status < Puppet::Network::Client::ProxyClient self.mkmethods end diff --git a/lib/puppet/network/client_request.rb b/lib/puppet/network/client_request.rb new file mode 100644 index 000000000..16a9d9993 --- /dev/null +++ b/lib/puppet/network/client_request.rb @@ -0,0 +1,32 @@ +module Puppet::Network # :nodoc: + # A struct-like class for passing around a client request. It's mostly + # just used for validation and authorization. + class ClientRequest + attr_accessor :name, :ip, :authenticated, :handler, :method + + def authenticated? + self.authenticated + end + + # A common way of talking about the full call. Individual servers + # are responsible for setting the values correctly, but this common + # format makes it possible to check rights. + def call + unless handler and method + raise ArgumentError, "Request is not set up; cannot build call" + end + + [handler, method].join(".") + end + + def initialize(name, ip, authenticated) + @name, @ip, @authenticated = name, ip, authenticated + end + + def to_s + "%s(%s)" % [self.name, self.ip] + end + end +end + +# $Id$ diff --git a/lib/puppet/network/handler.rb b/lib/puppet/network/handler.rb new file mode 100644 index 000000000..080997e98 --- /dev/null +++ b/lib/puppet/network/handler.rb @@ -0,0 +1,33 @@ +require 'puppet/util/subclass_loader' + +module Puppet::Network + # The base class for the different handlers. The handlers are each responsible + # for separate xmlrpc namespaces. + class Handler + # This is so that the handlers can subclass just 'Handler', rather + # then having to specify the full class path. + Handler = self + attr_accessor :server + + extend Puppet::Util::SubclassLoader + extend Puppet::Util + + handle_subclasses :handler, "puppet/network/handler" + + # Return the xmlrpc interface. + def self.interface + if defined? @interface + return @interface + else + raise Puppet::DevError, "Handler %s has no defined interface" % + self + end + end + + # Create an empty init method with the same signature. + def initialize(hash = {}) + end + end +end + +# $Id$ diff --git a/lib/puppet/network/server/ca.rb b/lib/puppet/network/handler/ca.rb index 8a61399ba..06e0486bf 100644 --- a/lib/puppet/network/server/ca.rb +++ b/lib/puppet/network/handler/ca.rb @@ -6,7 +6,7 @@ require 'xmlrpc/server' # Much of this was taken from QuickCert: # http://segment7.net/projects/ruby/QuickCert/ -class Puppet::Network::Server +class Puppet::Network::Handler class CA < Handler attr_reader :ca diff --git a/lib/puppet/network/server/filebucket.rb b/lib/puppet/network/handler/filebucket.rb index 77dbbde5e..653d566b4 100755 --- a/lib/puppet/network/server/filebucket.rb +++ b/lib/puppet/network/handler/filebucket.rb @@ -9,26 +9,9 @@ require 'facter' require 'digest/md5' require 'puppet/external/base64' -class Puppet::Network::Server +class Puppet::Network::Handler class BucketError < RuntimeError; end class FileBucket < Handler - Puppet.config.setdefaults("puppetmasterd", - :bucketdir => { - :default => "$vardir/bucket", - :mode => 0750, - :owner => "$user", - :group => "$group", - :desc => "Where FileBucket files are stored." - } - ) - - Puppet.config.setdefaults("filebucket", - :clientbucketdir => { - :default => "$vardir/clientbucket", - :mode => 0750, - :desc => "Where FileBucket files are stored locally." - } - ) @interface = XMLRPC::Service::Interface.new("puppetbucket") { |iface| iface.add_method("string addfile(string, string)") iface.add_method("string getfile(string)") @@ -38,7 +21,7 @@ class Puppet::Network::Server attr_reader :name, :path # this doesn't work for relative paths - def FileBucket.paths(base,md5) + def self.paths(base,md5) return [ File.join(base, md5), File.join(base, md5, "contents"), diff --git a/lib/puppet/network/server/fileserver.rb b/lib/puppet/network/handler/fileserver.rb index 904d497ca..6def09837 100755 --- a/lib/puppet/network/server/fileserver.rb +++ b/lib/puppet/network/handler/fileserver.rb @@ -1,17 +1,14 @@ require 'puppet' +require 'puppet/network/authstore' require 'webrick/httpstatus' require 'cgi' require 'delegate' -class Puppet::Network::Server +class Puppet::Network::Handler class FileServerError < Puppet::Error; end class FileServer < Handler attr_accessor :local - Puppet.setdefaults("fileserver", - :fileserverconfig => ["$confdir/fileserver.conf", - "Where the fileserver configuration is stored."]) - CHECKPARAMS = [:mode, :type, :owner, :group, :checksum] @interface = XMLRPC::Service::Interface.new("fileserver") { |iface| @@ -20,13 +17,17 @@ class Puppet::Network::Server iface.add_method("string retrieve(string, string)") } + def self.params + CHECKPARAMS.dup + end + # Describe a given file. This returns all of the manageable aspects # of that file. def describe(url, links = :ignore, client = nil, clientip = nil) links = links.intern if links.is_a? String if links == :manage - raise Puppet::Network::Server::FileServerError, "Cannot currently copy links" + raise Puppet::Network::Handler::FileServerError, "Cannot currently copy links" end mount, path = convert(url, client, clientip) diff --git a/lib/puppet/network/server/logger.rb b/lib/puppet/network/handler/logger.rb index f6bf9ba88..f01b48325 100755 --- a/lib/puppet/network/server/logger.rb +++ b/lib/puppet/network/handler/logger.rb @@ -1,6 +1,6 @@ require 'yaml' -class Puppet::Network::Server +class Puppet::Network::Handler class LoggerError < RuntimeError; end # Receive logs from remote hosts. diff --git a/lib/puppet/network/server/master.rb b/lib/puppet/network/handler/master.rb index b9b796d96..2b0a215d0 100644 --- a/lib/puppet/network/server/master.rb +++ b/lib/puppet/network/handler/master.rb @@ -5,7 +5,7 @@ require 'puppet/sslcertificates' require 'xmlrpc/server' require 'yaml' -class Puppet::Network::Server +class Puppet::Network::Handler class MasterError < Puppet::Error; end class Master < Handler include Puppet::Util @@ -24,7 +24,7 @@ class Puppet::Network::Server facts["serverversion"] = Puppet.version.to_s # And then add the server name and IP - {"servername" => "fqdn", + {"servername" => "hostname", "serverip" => "ipaddress" }.each do |var, fact| if obj = Facter[fact] @@ -55,6 +55,8 @@ class Puppet::Network::Server # Tell a client whether there's a fresh config for it def freshness(client = nil, clientip = nil) if Puppet.features.rails? and Puppet[:storeconfigs] + Puppet::Rails.connect + host = Puppet::Rails::Host.find_or_create_by_name(client) host.last_freshcheck = Time.now if clientip and (! host.ip or host.ip == "") diff --git a/lib/puppet/network/server/report.rb b/lib/puppet/network/handler/report.rb index 781a392ed..77e31f04a 100755 --- a/lib/puppet/network/server/report.rb +++ b/lib/puppet/network/handler/report.rb @@ -1,9 +1,7 @@ # A simple server for triggering a new run on a Puppet client. -class Puppet::Network::Server +class Puppet::Network::Handler class Report < Handler - class << self - include Puppet::Util::ClassGen - end + extend Puppet::Util::ClassGen module ReportBase include Puppet::Util::Docs @@ -22,21 +20,6 @@ class Puppet::Network::Server iface.add_method("string report(array)") } - Puppet.setdefaults(:reporting, - :reports => ["store", - "The list of reports to generate. All reports are looked for - in puppet/reports/<name>.rb, and multiple report names should be - comma-separated (whitespace is okay)." - ], - :reportdir => {:default => "$vardir/reports", - :mode => 0750, - :owner => "$user", - :group => "$group", - :desc => "The directory in which to store reports - received from the client. Each client gets a separate - subdirectory."} - ) - @reports = {} @reportloader = Puppet::Util::Autoload.new(self, "puppet/reports") @@ -78,6 +61,7 @@ class Puppet::Network::Server @reports[symbolize(name)] end + # Collect the docs for all of our reports. def self.reportdocs docs = "" @@ -92,6 +76,7 @@ class Puppet::Network::Server docs end + # List each of the reports. def self.reports @reportloader.loadall @reports.keys @@ -110,16 +95,15 @@ class Puppet::Network::Server report = CGI.unescape(report) end + Puppet.info "Processing reports %s for %s" % [reports().join(", "), client] begin process(report) rescue => detail - Puppet.err "Could not process report %s: %s" % [$1, detail] + Puppet.err "Could not process report for %s: %s" % [client, detail] if Puppet[:trace] puts detail.backtrace end end - - update_timestamp(client) end private @@ -141,8 +125,6 @@ class Puppet::Network::Server reports().each do |name| if mod = self.class.report(name) - Puppet.info "Processing report %s for %s" % [name, client] - # We have to use a dup because we're including a module in the # report. newrep = report.dup @@ -170,17 +152,6 @@ class Puppet::Network::Server def reports Puppet[:reports].gsub(/(^\s+)|(\s+$)/, '').split(/\s*,\s*/) end - - def update_timestamp(client) - return unless Puppet[:storeconfigs] - - if host = Puppet::Rails::Host.find_by_name(client) - host.last_report = Time.now - host.save - else - Puppet.warning "Could not find Rails host for %s" % client - end - end end end diff --git a/lib/puppet/network/server/resource.rb b/lib/puppet/network/handler/resource.rb index 37e331a13..92533dd2a 100755 --- a/lib/puppet/network/server/resource.rb +++ b/lib/puppet/network/handler/resource.rb @@ -1,8 +1,8 @@ require 'puppet' -require 'puppet/network/server' +require 'puppet/network/handler' # Serve Puppet elements. Useful for querying, copying, and, um, other stuff. -class Puppet::Network::Server +class Puppet::Network::Handler class Resource < Handler attr_accessor :local @@ -31,7 +31,7 @@ class Puppet::Network::Server # Create a client, but specify the remote machine as the server # because the class requires it, even though it's unused - client = Puppet::Network::Client::MasterClient.new(:Server => client||"localhost") + client = Puppet::Network::Client.client(:Master).new(:Server => client||"localhost") # Set the objects client.objects = component diff --git a/lib/puppet/network/server/runner.rb b/lib/puppet/network/handler/runner.rb index c0ec8fb9d..79084f847 100755 --- a/lib/puppet/network/server/runner.rb +++ b/lib/puppet/network/handler/runner.rb @@ -1,4 +1,4 @@ -class Puppet::Network::Server +class Puppet::Network::Handler class MissingMasterError < RuntimeError; end # Cannot find the master client # A simple server for triggering a new run on a Puppet client. class Runner < Handler @@ -10,7 +10,7 @@ class Puppet::Network::Server # tags and whether to ignore schedules def run(tags = nil, ignoreschedules = false, fg = true, client = nil, clientip = nil) # We need to retrieve the client - master = Puppet::Network::Client::MasterClient.instance + master = Puppet::Network::Client.client(:Master).instance unless master raise MissingMasterError, "Could not find the master client" diff --git a/lib/puppet/network/handler/status.rb b/lib/puppet/network/handler/status.rb new file mode 100644 index 000000000..774c49f6d --- /dev/null +++ b/lib/puppet/network/handler/status.rb @@ -0,0 +1,13 @@ +class Puppet::Network::Handler + class Status < Handler + @interface = XMLRPC::Service::Interface.new("status") { |iface| + iface.add_method("int status()") + } + + def status(client = nil, clientip = nil) + return 1 + end + end +end + +# $Id$ diff --git a/lib/puppet/network/networkclient.rb b/lib/puppet/network/networkclient.rb deleted file mode 100644 index 62d8906e0..000000000 --- a/lib/puppet/network/networkclient.rb +++ /dev/null @@ -1,167 +0,0 @@ -require 'puppet/sslcertificates' -require 'openssl' -require 'puppet/daemon' -require 'puppet/network/server' -require 'puppet/external/base64' - -require 'webrick' -require 'cgi' -require 'xmlrpc/client' -require 'xmlrpc/server' -require 'yaml' - -module Puppet - module Network - class ClientError < Puppet::Error; end - class NetworkClientError < Puppet::Error; end - class NetworkClient < XMLRPC::Client - attr_accessor :puppet_server, :puppet_port - @clients = {} - - class << self - include Puppet::Util - include Puppet::Util::ClassGen - end - - # Create a netclient for each handler - def self.mkclients - # add the methods associated with each namespace - Puppet::Network::Server::Handler.each { |handler| - interface = handler.interface - namespace = interface.prefix - - # Create a subclass for every client type. This is - # so that all of the methods are on their own class, - # so that they namespaces can define the same methods if - # they want. - constant = handler.to_s.sub(/^.+::/, '') - name = namespace.downcase - newclient = genclass(name, :hash => @clients, - :constant => constant) - - interface.methods.each { |ary| - method = ary[0] - if public_method_defined?(method) - raise Puppet::DevError, "Method %s is already defined" % - method - end - newclient.send(:define_method,method) { |*args| - Puppet.debug "Calling %s.%s" % [namespace, method] - #Puppet.info "peer cert is %s" % @http.peer_cert - #Puppet.info "cert is %s" % @http.cert - begin - call("%s.%s" % [namespace, method.to_s],*args) - rescue OpenSSL::SSL::SSLError => detail - raise NetworkClientError, - "Certificates were not trusted: %s" % detail - rescue XMLRPC::FaultException => detail - #Puppet.err "Could not call %s.%s: %s" % - # [namespace, method, detail.faultString] - #raise NetworkClientError, - # "XMLRPC Error: %s" % detail.faultString - raise NetworkClientError, detail.faultString - rescue Errno::ECONNREFUSED => detail - msg = "Could not connect to %s on port %s" % - [@host, @port] - raise NetworkClientError, msg - rescue SocketError => detail - error = NetworkClientError.new( - "Could not find server %s" % @puppetserver - ) - error.set_backtrace detail.backtrace - raise error - rescue => detail - Puppet.err "Could not call %s.%s: %s" % - [namespace, method, detail.inspect] - error = NetworkClientError.new(detail.to_s) - error.set_backtrace detail.backtrace - raise error - end - } - } - } - end - - def self.netclient(namespace) - if @clients.empty? - self.mkclients() - end - - namespace = symbolize(namespace) - - @clients[namespace] - end - - def ca_file=(cafile) - @http.ca_file = cafile - store = OpenSSL::X509::Store.new - store.add_file(cafile) - store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT - @http.cert_store = store - end - - def cert=(cert) - #Puppet.debug "Adding certificate" - @http.cert = cert - @http.verify_mode = OpenSSL::SSL::VERIFY_PEER - end - - def key=(key) - @http.key = key - end - - def initialize(hash) - hash[:Path] ||= "/RPC2" - hash[:Server] ||= "localhost" - hash[:Port] ||= Puppet[:masterport] - - @puppet_server = hash[:Server] - @puppet_port = hash[:Port] - - @puppetserver = hash[:Server] - - super( - hash[:Server], - hash[:Path], - hash[:Port], - nil, # proxy_host - nil, # proxy_port - nil, # user - nil, # password - true, # use_ssl - 120 # a two minute timeout, instead of 30 seconds - ) - - if hash[:Certificate] - self.cert = hash[:Certificate] - else - unless defined? $nocertwarned - Puppet.err "No certificate; running with reduced functionality." - $nocertwarned = true - end - end - - if hash[:Key] - self.key = hash[:Key] - end - - if hash[:CAFile] - self.ca_file = hash[:CAFile] - end - - # from here, i need to add the key, cert, and ca cert - # and reorgize how i start the client - end - - def local - false - end - - def local? - false - end - end - end -end - -# $Id$ diff --git a/lib/puppet/network/server.rb b/lib/puppet/network/server.rb index 3f19a88ff..1bf3f5f63 100644 --- a/lib/puppet/network/server.rb +++ b/lib/puppet/network/server.rb @@ -1,208 +1,5 @@ -# the server -# -# allow things to connect to us and communicate, and stuff - -require 'puppet' -require 'puppet/daemon' -require 'webrick' -require 'webrick/https' -require 'cgi' -require 'xmlrpc/server' -require 'xmlrpc/client' - -module Puppet - class ServerError < RuntimeError; end - module Network - class Server < WEBrick::HTTPServer - include Puppet::Daemon - - Puppet.config.setdefaults(:puppetd, - :listen => [false, "Whether puppetd should listen for - connections. If this is true, then by default only the - ``runner`` server is started, which allows remote authorized - and authenticated nodes to connect and trigger ``puppetd`` - runs."] - ) - - # Create our config object if necessary. This works even if - # there's no configuration file. - def authconfig - unless defined? @authconfig - @authconfig = Puppet::Network::AuthConfig.new() - end - - @authconfig - end - - # Read the CA cert and CRL and populate an OpenSSL::X509::Store - # with them, with flags appropriate for checking client - # certificates for revocation - def x509store - if Puppet[:cacrl] == 'none' - # No CRL, no store needed - return nil - end - unless File.exist?(Puppet[:cacrl]) - raise Puppet::Error, "Could not find CRL" - end - crl = OpenSSL::X509::CRL.new(File.read(Puppet[:cacrl])) - store = OpenSSL::X509::Store.new - store.purpose = OpenSSL::X509::PURPOSE_ANY - store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK_ALL|OpenSSL::X509::V_FLAG_CRL_CHECK - store.add_file(@cacertfile) - store.add_crl(crl) - return store - end - - def initialize(hash = {}) - Puppet.info "Starting server for Puppet version %s" % Puppet.version - daemonize = nil - if hash.include?(:Daemonize) - daemonize = hash[:Daemonize] - end - - # FIXME we should have some kind of access control here, using - # :RequestHandler - hash[:Port] ||= Puppet[:masterport] - hash[:Logger] ||= self.httplog - hash[:AccessLog] ||= [ - [ self.httplog, WEBrick::AccessLog::COMMON_LOG_FORMAT ], - [ self.httplog, WEBrick::AccessLog::REFERER_LOG_FORMAT ] - ] - - if hash.include?(:Handlers) - unless hash[:Handlers].is_a?(Hash) - raise ServerError, "Handlers must have arguments" - end - - @handlers = hash[:Handlers].collect { |handler, args| - hclass = nil - unless hclass = Handler.handler(handler) - raise ServerError, "Invalid handler %s" % handler - end - hclass.new(args) - } - else - raise ServerError, "A server must have handlers" - end - - # okay, i need to retrieve my cert and set it up, somehow - # the default case will be that i'm also the ca - if ca = @handlers.find { |handler| handler.is_a?(Puppet::Network::Server::CA) } - @driver = ca - @secureinit = true - self.fqdn - else - if hash.include?(:NoSecureInit) - @secureinit = false - else - @secureinit = true - end - end - - unless self.readcert - unless self.requestcert - raise Puppet::Error, "Cannot start without certificates" - end - end - - hash[:SSLCertificateStore] = x509store - hash[:SSLCertificate] = @cert - hash[:SSLPrivateKey] = @key - hash[:SSLStartImmediately] = true - hash[:SSLEnable] = true - hash[:SSLCACertificateFile] = @cacertfile - hash[:SSLVerifyClient] = OpenSSL::SSL::VERIFY_PEER - hash[:SSLCertName] = nil - - super(hash) - - Puppet.info "Listening on port %s" % hash[:Port] - - # this creates a new servlet for every connection, - # but all servlets have the same list of handlers - # thus, the servlets can have their own state -- passing - # around the requests and such -- but the handlers - # have a global state - - # mount has to be called after the server is initialized - self.mount("/RPC2", Puppet::Network::Server::Servlet, @handlers) - end - - # the base class for the different handlers - class Handler - attr_accessor :server - class << self - include Puppet::Util - end - - @subclasses = [] - - def self.each - @subclasses.each { |c| yield c } - end - - def self.handler(name) - name = name.to_s.downcase - @subclasses.find { |h| - h.name.to_s.downcase == name - } - end - - def self.inherited(sub) - @subclasses << sub - end - - def self.interface - if defined? @interface - return @interface - else - raise Puppet::DevError, "Handler %s has no defined interface" % - self - end - end - - def self.name - unless defined? @name - @name = self.to_s.sub(/.+::/, '').intern - end - - return @name - end - - def initialize(hash = {}) - end - end - - - class ServerStatus < Handler - - @interface = XMLRPC::Service::Interface.new("status") { |iface| - iface.add_method("int status()") - } - - @name = :Status - - def status(status = nil, client = nil, clientip = nil) - return 1 - end - end - end - end +# Just a stub, so we can correctly scope other classes. +module Puppet::Network::Server # :nodoc: end -require 'puppet/network/authstore' -require 'puppet/network/authconfig' -require 'puppet/network/rights' -require 'puppet/network/server/servlet' -require 'puppet/network/server/master' -require 'puppet/network/server/ca' -require 'puppet/network/server/fileserver' -require 'puppet/network/server/filebucket' -require 'puppet/network/server/resource' -require 'puppet/network/server/runner' -require 'puppet/network/server/logger' -require 'puppet/network/server/report' -require 'puppet/network/client' - # $Id$ diff --git a/lib/puppet/network/server/mongrel.rb b/lib/puppet/network/server/mongrel.rb new file mode 100644 index 000000000..a85ed1314 --- /dev/null +++ b/lib/puppet/network/server/mongrel.rb @@ -0,0 +1,138 @@ +#!/usr/bin/env ruby +# File: 06-11-14-mongrel_xmlrpc.rb +# Author: Manuel Holtgrewe <purestorm at ggnore.net> +# +# Copyright (c) 2006 Manuel Holtgrewe, 2007 Luke Kanies +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# This file is based heavily on a file retrieved from +# http://ttt.ggnore.net/2006/11/15/xmlrpc-with-mongrel-and-ruby-off-rails/ + +require 'rubygems' +require 'mongrel' +require 'xmlrpc/server' +require 'puppet/network/server' +require 'puppet/network/xmlrpc/server' +require 'puppet/network/client_request' + +require 'resolv' + +# This handler can be hooked into Mongrel to accept HTTP requests. After +# checking whether the request itself is sane, the handler forwards it +# to an internal instance of XMLRPC::BasicServer to process it. +# +# You can access the server by calling the Handler's "xmlrpc_server" +# attribute accessor method and add XMLRPC handlers there. For example: +# +# <pre> +# handler = XmlRpcHandler.new +# handler.xmlrpc_server.add_handler("my.add") { |a, b| a.to_i + b.to_i } +# </pre> +class Puppet::Network::Server + class MongrelHandler < Mongrel::HttpHandler + attr_reader :xmlrpc_server + + def initialize(handlers) + # Create a new instance of BasicServer. We are supposed to subclass it + # but that does not make sense since we would not introduce any new + # 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| + unless handler = Puppet::Network::Handler.handler(name) + raise ArgumentError, "Invalid handler %s" % name + end + @xmlrpc_server.add_handler(handler.interface, handler.new(args)) + end + end + + # This method produces the same results as XMLRPC::CGIServer.serve + # from Ruby's stdlib XMLRPC implementation. + def process(request, response) + # Make sure this has been a POST as required for XMLRPC. + request_method = request.params[Mongrel::Const::REQUEST_METHOD] || Mongrel::Const::GET + if request_method != "POST" then + response.start(405) { |head, out| out.write("Method Not Allowed") } + return + end + + # Make sure the user has sent text/xml data. + request_mime = request.params["CONTENT_TYPE"] || "text/plain" + if parse_content_type(request_mime).first != "text/xml" then + response.start(400) { |head, out| out.write("Bad Request") } + return + end + + # Make sure there is data in the body at all. + length = request.params[Mongrel::Const::CONTENT_LENGTH].to_i + if length <= 0 then + response.start(411) { |head, out| out.write("Length Required") } + return + end + + # Check the body to be valid. + if request.body.nil? or request.body.size != length then + response.start(400) { |head, out| out.write("Bad Request") } + return + end + + info = client_info(request) + + # All checks above passed through + response.start(200) do |head, out| + head["Content-Type"] = "text/xml; charset=utf-8" + begin + out.write(@xmlrpc_server.process(request.body, info)) + rescue => detail + puts detail.backtrace + raise + end + end + end + + private + + def client_info(request) + params = request.params + ip = params["REMOTE_ADDR"] + if dn = params["HTTP_X_CLIENT_DN"] + client = dn.sub("/CN=", '') + valid = true + else + client = Resolv.getname(ip) + valid = false + end + + info = Puppet::Network::ClientRequest.new(client, ip, valid) + + return info + end + + # Taken from XMLRPC::ParseContentType + def parse_content_type(str) + a, *b = str.split(";") + return a.strip, *b + end + end +end + +# $Id$ diff --git a/lib/puppet/network/server/servlet.rb b/lib/puppet/network/server/servlet.rb deleted file mode 100644 index 325956d8c..000000000 --- a/lib/puppet/network/server/servlet.rb +++ /dev/null @@ -1,277 +0,0 @@ -require 'xmlrpc/server' - -class Puppet::Network::Server - class ServletError < RuntimeError; end - class Servlet < XMLRPC::WEBrickServlet - ERR_UNAUTHORIZED = 30 - - attr_accessor :request - - # this is just a duplicate of the normal method; it's here for - # debugging when i need it - def self.get_instance(server, *options) - self.new(server, *options) - end - - # This is a hackish way to avoid an auth message every time we have a - # normal operation - def self.log(msg) - unless defined? @logs - @logs = {} - end - if @logs.include?(msg) - @logs[msg] += 1 - else - Puppet.info msg - @logs[msg] = 1 - end - end - - def add_handler(interface, handler) - @loadedhandlers << interface.prefix - super - end - - # Verify that our client has access. We allow untrusted access to - # puppetca methods but no others. - def authorize(request, method) - namespace = method.sub(/\..+/, '') - client = request.peeraddr[2] - if defined? @client and @client - client = @client - end - ip = request.peeraddr[3] - if request.client_cert - if @puppetserver.authconfig.exists? - allowed = @puppetserver.authconfig.allowed?(method, client, ip) - - if allowed - Puppet.debug "Allowing %s(%s) trusted access to %s" % - [client, ip, method] - return true - else - Puppet.debug "Denying %s(%s) trusted access to %s" % - [client, ip, method] - return false - end - else - # This is pretty hackish, but... - # This means we can't actually test this method at this point. - # The next release of Puppet will almost definitely require - # this file to exist or will default to denying all access. - if Puppet.execname == "puppetmasterd" or defined? Test::Unit::TestCase - Puppet.debug "Allowing %s(%s) trusted access to %s" % - [client, ip, method] - return true - else - Puppet.debug "Denying %s(%s) trusted access to %s on %s" % - [client, ip, method, Puppet.execname] - return false - end - end - else - if method =~ /^puppetca\./ - Puppet.notice "Allowing %s(%s) untrusted access to CA methods" % - [client, ip] - else - Puppet.err "Unauthenticated client %s(%s) cannot call %s" % - [client, ip, method] - return false - end - end - end - - def available?(method) - namespace = method.sub(/\..+/, '') - client = request.peeraddr[2] - ip = request.peeraddr[3] - if @loadedhandlers.include?(namespace) - return true - else - Puppet.warning "Client %s(%s) requested unavailable functionality %s" % - [client, ip, namespace] - return false - end - end - - def initialize(server, handlers) - @puppetserver = server - @notified = {} - # the servlet base class does not consume any arguments - # and its BasicServer base class only accepts a 'class_delim' - # option which won't change in Puppet at all - # thus, we don't need to pass any args to our base class, - # and we can consume them all ourselves - super() - - @loadedhandlers = [] - handlers.each { |handler| - #Puppet.debug "adding handler for %s" % handler.class - self.add_handler(handler.class.interface, handler) - } - - # Initialize these to nil, but they will get set to values - # by the 'service' method. These have to instance variables - # because I don't have a clear line from the service method to - # the service hook. - @request = nil - @client = nil - @clientip = nil - - self.set_service_hook { |obj, *args| - if @client and @clientip - args.push(@client, @clientip) - end - begin - obj.call(*args) - rescue XMLRPC::FaultException - raise - rescue Puppet::AuthorizationError => detail - #Puppet.warning obj.inspect - #Puppet.warning args.inspect - Puppet.err "Permission denied: %s" % detail.to_s - raise XMLRPC::FaultException.new( - 1, detail.to_s - ) - rescue Puppet::Error => detail - #Puppet.warning obj.inspect - #Puppet.warning args.inspect - if Puppet[:trace] - puts detail.backtrace - end - Puppet.err detail.to_s - error = XMLRPC::FaultException.new( - 1, detail.to_s - ) - error.set_backtrace detail.backtrace - raise error - rescue => detail - #Puppet.warning obj.inspect - #Puppet.warning args.inspect - if Puppet[:trace] - puts detail.backtrace - end - Puppet.err "Could not call: %s" % detail.to_s - error = XMLRPC::FaultException.new(1, detail.to_s) - error.set_backtrace detail.backtrace - raise error - end - } - end - - # Handle the actual request. This does some basic collection of - # data, and then just calls the parent method. - def service(request, response) - @request = request - - # The only way that @client can be nil is if the request is local. - if peer = request.peeraddr - @client = peer[2] - @clientip = peer[3] - else - raise XMLRPC::FaultException.new( - ERR_UNCAUGHT_EXCEPTION, - "Could not retrieve client information" - ) - end - - # If they have a certificate (which will almost always be true) - # then we get the hostname from the cert, instead of via IP - # info - if cert = request.client_cert - nameary = cert.subject.to_a.find { |ary| - ary[0] == "CN" - } - - if nameary.nil? - Puppet.warning "Could not retrieve server name from cert" - else - unless @client == nameary[1] - Puppet.debug "Overriding %s with cert name %s" % - [@client, nameary[1]] - @client = nameary[1] - end - end - end - begin - super - rescue => detail - Puppet.err "Could not service request: %s: %s" % - [detail.class, detail] - end - @client = nil - @clientip = nil - @request = nil - end - - private - - # this is pretty much just a copy of the original method but with more - # feedback - # here's where we have our authorization hooks - def dispatch(methodname, *args) - - if defined? @request and @request - unless self.available?(methodname) - raise XMLRPC::FaultException.new( - ERR_UNAUTHORIZED, - "Functionality %s not available" % - methodname.sub(/\..+/, '') - ) - end - unless self.authorize(@request, methodname) - raise XMLRPC::FaultException.new( - ERR_UNAUTHORIZED, - "Host %s not authorized to call %s" % - [@request.host, methodname] - ) - end - else - raise Puppet::DevError, "Did not get request in dispatch" - end - - #Puppet.warning "dispatch on %s called with %s" % - # [methodname, args.inspect] - for name, obj in @handler - if obj.kind_of? Proc - unless methodname == name - #Puppet.debug "obj is proc but %s != %s" % - # [methodname, name] - next - end - else - unless methodname =~ /^#{name}(.+)$/ - #Puppet.debug "methodname did not match" - next - end - unless obj.respond_to? $1 - #Puppet.debug "methodname does not respond to %s" % $1 - next - end - obj = obj.method($1) - end - - if check_arity(obj, args.size) - if @service_hook.nil? - return obj.call(*args) - else - return @service_hook.call(obj, *args) - end - else - Puppet.debug "arity is incorrect" - end - end - - if @default_handler.nil? - raise XMLRPC::FaultException.new( - ERR_METHOD_MISSING, - "Method #{methodname} missing or wrong number of parameters!" - ) - else - @default_handler.call(methodname, *args) - end - end - end -end - -# $Id$ diff --git a/lib/puppet/network/server/webrick.rb b/lib/puppet/network/server/webrick.rb new file mode 100644 index 000000000..7b9f0f0c6 --- /dev/null +++ b/lib/puppet/network/server/webrick.rb @@ -0,0 +1,153 @@ +require 'puppet' +require 'puppet/daemon' +require 'webrick' +require 'webrick/https' + +require 'puppet/sslcertificates/support' +require 'puppet/network/xmlrpc/webrick_servlet' +require 'puppet/network/server' +require 'puppet/network/client' + +module Puppet + class ServerError < RuntimeError; end + module Network + # The old-school, pure ruby webrick server, which is the default serving + # mechanism. + class Server::WEBrick < WEBrick::HTTPServer + include Puppet::Daemon + include Puppet::SSLCertificates::Support + + # Read the CA cert and CRL and populate an OpenSSL::X509::Store + # with them, with flags appropriate for checking client + # certificates for revocation + def x509store + if Puppet[:cacrl] == 'none' + # No CRL, no store needed + return nil + end + unless File.exist?(Puppet[:cacrl]) + raise Puppet::Error, "Could not find CRL" + end + crl = OpenSSL::X509::CRL.new(File.read(Puppet[:cacrl])) + store = OpenSSL::X509::Store.new + store.purpose = OpenSSL::X509::PURPOSE_ANY + store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK_ALL|OpenSSL::X509::V_FLAG_CRL_CHECK + unless self.ca_cert + raise Puppet::Error, "No CA certificate" + end + + store.add_file(Puppet[:localcacert]) + store.add_crl(crl) + return store + end + + # Set up the http log. + def httplog + args = [] + + # yuck; separate http logs + file = nil + Puppet.config.use(:puppet, :certificates, Puppet.name) + if Puppet[:name] == "puppetmasterd" + file = Puppet[:masterhttplog] + else + file = Puppet[:httplog] + end + + args << file + if Puppet[:debug] + args << WEBrick::Log::DEBUG + end + + log = WEBrick::Log.new(*args) + + + return log + end + + # Create our server, yo. + def initialize(hash = {}) + Puppet.info "Starting server for Puppet version %s" % Puppet.version + + if handlers = hash[:Handlers] + handler_instances = setup_handlers(handlers) + else + raise ServerError, "A server must have handlers" + end + + unless self.read_cert + if ca = handler_instances.find { |handler| handler.is_a?(Puppet::Network::Handler.ca) } + request_cert(ca) + else + raise Puppet::Error, "No certificate and no CA; cannot get cert" + end + end + + setup_webrick(hash) + + super(hash) + + Puppet.info "Listening on port %s" % hash[:Port] + + # this creates a new servlet for every connection, + # but all servlets have the same list of handlers + # thus, the servlets can have their own state -- passing + # around the requests and such -- but the handlers + # have a global state + + # mount has to be called after the server is initialized + servlet = Puppet::Network::XMLRPC::WEBrickServlet.new( + handler_instances) + self.mount("/RPC2", servlet) + end + + # Create a ca client to set up our cert for us. + def request_cert(ca) + client = Puppet::Network::Client.ca.new(:CA => ca) + unless client.request_cert + raise Puppet::Error, "Could get certificate" + end + end + + # Create all of our handler instances. + def setup_handlers(handlers) + unless handlers.is_a?(Hash) + raise ServerError, "Handlers must have arguments" + end + + handlers.collect { |handler, args| + hclass = nil + unless hclass = Handler.handler(handler) + raise ServerError, "Invalid handler %s" % handler + end + hclass.new(args) + } + end + + # Handle all of the many webrick arguments. + def setup_webrick(hash) + hash[:Port] ||= Puppet[:masterport] + hash[:Logger] ||= self.httplog + hash[:AccessLog] ||= [ + [ self.httplog, WEBrick::AccessLog::COMMON_LOG_FORMAT ], + [ self.httplog, WEBrick::AccessLog::REFERER_LOG_FORMAT ] + ] + + hash[:SSLCertificateStore] = x509store + hash[:SSLCertificate] = self.cert + hash[:SSLPrivateKey] = self.key + hash[:SSLStartImmediately] = true + hash[:SSLEnable] = true + hash[:SSLCACertificateFile] = Puppet[:localcacert] + hash[:SSLVerifyClient] = OpenSSL::SSL::VERIFY_PEER + hash[:SSLCertName] = nil + + if addr = Puppet[:bindaddress] and addr != "" + hash[:BindAddress] = addr + end + end + end + end +end + +# $Id$ diff --git a/lib/puppet/network/xmlrpc/client.rb b/lib/puppet/network/xmlrpc/client.rb new file mode 100644 index 000000000..38f40a827 --- /dev/null +++ b/lib/puppet/network/xmlrpc/client.rb @@ -0,0 +1,129 @@ +require 'puppet/sslcertificates' +require 'openssl' +require 'puppet/external/base64' + +require 'xmlrpc/client' +require 'yaml' + +module Puppet::Network + class ClientError < Puppet::Error; end + class XMLRPCClientError < Puppet::Error; end + class XMLRPCClient < ::XMLRPC::Client + attr_accessor :puppet_server, :puppet_port + @clients = {} + + class << self + include Puppet::Util + include Puppet::Util::ClassGen + end + + # Create a netclient for each handler + def self.mkclient(handler) + interface = handler.interface + namespace = interface.prefix + + # Create a subclass for every client type. This is + # so that all of the methods are on their own class, + # so that they namespaces can define the same methods if + # they want. + constant = handler.name.to_s.capitalize + name = namespace.downcase + newclient = genclass(name, :hash => @clients, + :constant => constant) + + interface.methods.each { |ary| + method = ary[0] + if public_method_defined?(method) + raise Puppet::DevError, "Method %s is already defined" % + method + end + newclient.send(:define_method,method) { |*args| + Puppet.debug "Calling %s.%s" % [namespace, method] + begin + call("%s.%s" % [namespace, method.to_s],*args) + rescue OpenSSL::SSL::SSLError => detail + raise XMLRPCClientError, + "Certificates were not trusted: %s" % detail + rescue ::XMLRPC::FaultException => detail + #Puppet.err "Could not call %s.%s: %s" % + # [namespace, method, detail.faultString] + #raise XMLRPCClientError, + # "XMLRPC Error: %s" % detail.faultString + raise XMLRPCClientError, detail.faultString + rescue Errno::ECONNREFUSED => detail + msg = "Could not connect to %s on port %s" % + [@host, @port] + raise XMLRPCClientError, msg + rescue SocketError => detail + Puppet.err "Could not find server %s: %s" % + [@puppet_server, detail.to_s] + error = XMLRPCClientError.new( + "Could not find server %s" % @puppet_server + ) + error.set_backtrace detail.backtrace + raise error + rescue => detail + Puppet.err "Could not call %s.%s: %s" % + [namespace, method, detail.inspect] + error = XMLRPCClientError.new(detail.to_s) + error.set_backtrace detail.backtrace + raise error + end + } + } + + return newclient + end + + def self.handler_class(handler) + @clients[handler] || self.mkclient(handler) + end + + # Use cert information from a Puppet client to set up the http object. + def cert_setup(client) + unless FileTest.exists?(Puppet[:localcacert]) + raise Puppet::SSLCertificates::Support::MissingCertificate, + "Could not find ca certificate %s" % Puppet[:localcacert] + end + @http.ca_file = Puppet[:localcacert] + store = OpenSSL::X509::Store.new + store.add_file Puppet[:localcacert] + store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT + @http.cert_store = store + @http.cert = client.cert + @http.verify_mode = OpenSSL::SSL::VERIFY_PEER + @http.key = client.key + end + + def initialize(hash = {}) + hash[:Path] ||= "/RPC2" + hash[:Server] ||= Puppet[:server] + hash[:Port] ||= Puppet[:masterport] + + @puppet_server = hash[:Server] + @puppet_port = hash[:Port] + + super( + hash[:Server], + hash[:Path], + hash[:Port], + nil, # proxy_host + nil, # proxy_port + nil, # user + nil, # password + true, # use_ssl + 120 # a two minute timeout, instead of 30 seconds + ) + end + + def local + false + end + + def local? + false + end + end +end + +# $Id$ diff --git a/lib/puppet/network/xmlrpc/processor.rb b/lib/puppet/network/xmlrpc/processor.rb new file mode 100644 index 000000000..248354696 --- /dev/null +++ b/lib/puppet/network/xmlrpc/processor.rb @@ -0,0 +1,91 @@ +require 'puppet/network/authorization' +require 'xmlrpc/server' + +# Just silly. +class ::XMLRPC::FaultException + def to_s + self.message + end +end + +module Puppet::Network + # Most of our subclassing is just so that we can get + # access to information from the request object, like + # the client name and IP address. + module XMLRPCProcessor + include Puppet::Network::Authorization + + ERR_UNAUTHORIZED = 30 + + def add_handler(interface, handler) + @loadedhandlers << interface.prefix + super(interface, handler) + end + + def handler_loaded?(handler) + @loadedhandlers.include?(handler.to_s) + end + + # Convert our data and client request into xmlrpc calls, and verify + # they're authorized and such-like. This method differs from the + # default in that it expects a ClientRequest object in addition to the + # data. + def process(data, request) + call, params = parser().parseMethodCall(data) + params << request.name << request.ip + handler, method = call.split(".") + request.handler = handler + request.method = method + begin + verify(request) + rescue InvalidClientRequest => detail + raise ::XMLRPC::FaultException.new(ERR_UNAUTHORIZED, detail.to_s) + end + handle(request.call, *params) + end + + private + + # Provide error handling for method calls. + def protect_service(obj, *args) + begin + obj.call(*args) + rescue ::XMLRPC::FaultException + raise + rescue Puppet::AuthorizationError => detail + Puppet.err "Permission denied: %s" % detail.to_s + raise ::XMLRPC::FaultException.new( + 1, detail.to_s + ) + rescue Puppet::Error => detail + if Puppet[:trace] + puts detail.backtrace + end + Puppet.err detail.to_s + error = ::XMLRPC::FaultException.new( + 1, detail.to_s + ) + error.set_backtrace detail.backtrace + raise error + rescue => detail + if Puppet[:trace] + puts detail.backtrace + end + Puppet.err "Could not call: %s" % detail.to_s + error = ::XMLRPC::FaultException.new(1, detail.to_s) + error.set_backtrace detail.backtrace + raise error + end + end + + # Set up our service hook and init our handler list. + def setup_processor + @loadedhandlers = [] + self.set_service_hook do |obj, *args| + protect_service(obj, *args) + end + end + end +end + +# $Id$ diff --git a/lib/puppet/network/xmlrpc/server.rb b/lib/puppet/network/xmlrpc/server.rb new file mode 100644 index 000000000..bd05703ba --- /dev/null +++ b/lib/puppet/network/xmlrpc/server.rb @@ -0,0 +1,20 @@ +require 'xmlrpc/server' +require 'puppet/network/authorization' +require 'puppet/network/xmlrpc/processor' + +module Puppet::Network + # Most of our subclassing is just so that we can get + # access to information from the request object, like + # the client name and IP address. + class XMLRPCServer < ::XMLRPC::BasicServer + include Puppet::Util + include Puppet::Network::XMLRPCProcessor + + def initialize + super() + setup_processor() + end + end +end + +# $Id$ diff --git a/lib/puppet/network/xmlrpc/webrick_servlet.rb b/lib/puppet/network/xmlrpc/webrick_servlet.rb new file mode 100644 index 000000000..0ddb056dc --- /dev/null +++ b/lib/puppet/network/xmlrpc/webrick_servlet.rb @@ -0,0 +1,121 @@ +require 'xmlrpc/server' +require 'puppet/network/authorization' +require 'puppet/network/xmlrpc/processor' + +module Puppet::Network::XMLRPC + class ServletError < RuntimeError; end + class WEBrickServlet < ::XMLRPC::WEBrickServlet + include Puppet::Network::XMLRPCProcessor + + # This is a hackish way to avoid an auth message every time we have a + # normal operation + def self.log(msg) + unless defined? @logs + @logs = {} + end + if @logs.include?(msg) + @logs[msg] += 1 + else + Puppet.info msg + @logs[msg] = 1 + end + end + + # Accept a list of handlers and register them all. + def initialize(handlers) + # the servlet base class does not consume any arguments + # and its BasicServer base class only accepts a 'class_delim' + # option which won't change in Puppet at all + # thus, we don't need to pass any args to our base class, + # and we can consume them all ourselves + super() + + setup_processor() + + # Set up each of the passed handlers. + handlers.each do |handler| + add_handler(handler.class.interface, handler) + end + end + + # Handle the actual request. We can't use the super() method, because + # we need to pass a ClientRequest object to process() so we can do + # authorization. It's the only way to stay thread-safe. + def service(request, response) + if @valid_ip + raise WEBrick::HTTPStatus::Forbidden unless @valid_ip.any? { |ip| request.peeraddr[3] =~ ip } + end + + if request.request_method != "POST" + raise WEBrick::HTTPStatus::MethodNotAllowed, + "unsupported method `#{request.request_method}'." + end + + if parse_content_type(request['Content-type']).first != "text/xml" + raise WEBrick::HTTPStatus::BadRequest + end + + length = (request['Content-length'] || 0).to_i + + raise WEBrick::HTTPStatus::LengthRequired unless length > 0 + + data = request.body + + if data.nil? or data.size != length + raise WEBrick::HTTPStatus::BadRequest + end + + resp = process(data, client_request(request)) + if resp.nil? or resp.size <= 0 + raise WEBrick::HTTPStatus::InternalServerError + end + + response.status = 200 + response['Content-Length'] = resp.size + response['Content-Type'] = "text/xml; charset=utf-8" + response.body = resp + end + + private + + # Generate a ClientRequest object for later validation. + def client_request(request) + if peer = request.peeraddr + client = peer[2] + clientip = peer[3] + else + raise ::XMLRPC::FaultException.new( + ERR_UNCAUGHT_EXCEPTION, + "Could not retrieve client information" + ) + end + + # If they have a certificate (which will almost always be true) + # then we get the hostname from the cert, instead of via IP + # info + valid = false + if cert = request.client_cert + nameary = cert.subject.to_a.find { |ary| + ary[0] == "CN" + } + + if nameary.nil? + Puppet.warning "Could not retrieve server name from cert" + else + unless client == nameary[1] + Puppet.debug "Overriding %s with cert name %s" % + [client, nameary[1]] + client = nameary[1] + end + valid = true + end + end + + info = Puppet::Network::ClientRequest.new(client, clientip, valid) + + return info + end + end +end + +# $Id$ diff --git a/lib/puppet/parser/ast/caseopt.rb b/lib/puppet/parser/ast/caseopt.rb index 8d578a10b..11483d082 100644 --- a/lib/puppet/parser/ast/caseopt.rb +++ b/lib/puppet/parser/ast/caseopt.rb @@ -54,8 +54,7 @@ class Puppet::Parser::AST # Evaluate the actual statements; this only gets called if # our option matched. def evaluate(hash) - scope = hash[:scope] - return @statements.safeevaluate(:scope => scope) + return @statements.safeevaluate(hash) end def tree(indent = 0) diff --git a/lib/puppet/parser/ast/casestatement.rb b/lib/puppet/parser/ast/casestatement.rb index 760a1095c..c56e33bc9 100644 --- a/lib/puppet/parser/ast/casestatement.rb +++ b/lib/puppet/parser/ast/casestatement.rb @@ -18,6 +18,7 @@ class Puppet::Parser::AST found = false # Iterate across the options looking for a match. + default = nil @options.each { |option| option.eachvalue(scope) { |opval| opval = opval.downcase if ! sensitive and opval.respond_to?(:downcase) @@ -32,12 +33,16 @@ class Puppet::Parser::AST retvalue = option.safeevaluate(:scope => scope) break end + + if option.default? + default = option + end } # Unless we found something, look for the default. unless found - if defined? @default - retvalue = @default.safeevaluate(:scope => scope) + if default + retvalue = default.safeevaluate(:scope => scope) else Puppet.debug "No true answers and no default" retvalue = nil @@ -46,33 +51,6 @@ class Puppet::Parser::AST return retvalue end - # Do some input validation on our options. - def initialize(hash) - values = {} - - super - - # This won't work if we move away from only allowing - # constants here, but for now, it's fine and useful. - @options.each { |option| - unless option.is_a?(CaseOpt) - raise Puppet::DevError, "Option is not a CaseOpt" - end - if option.default? - @default = option - end - option.eachvalue(nil) { |val| - if values.include?(val) - raise Puppet::ParseError, - "Value %s appears twice in case statement" % - val - else - values[val] = true - end - } - } - end - def tree(indent = 0) rettree = [ @test.tree(indent + 1), @@ -87,5 +65,6 @@ class Puppet::Parser::AST [@test,@options].each { |child| yield child } end end - end + +# $Id$ diff --git a/lib/puppet/parser/scope.rb b/lib/puppet/parser/scope.rb index e81862dad..e5d7df7ca 100644 --- a/lib/puppet/parser/scope.rb +++ b/lib/puppet/parser/scope.rb @@ -552,7 +552,7 @@ class Puppet::Parser::Scope if ss.matched == '\\$' out << '$' else # look the variable up - out << lookupvar(ss[1] || ss[2]) || "" + out << lookupvar(ss[1] || ss[2]).to_s || "" end elsif ss.scan(/^\\(.)/) # Puppet.debug("Got escape: pos:%d; m:%s" % [ss.pos, ss.matched]) diff --git a/lib/puppet/rails.rb b/lib/puppet/rails.rb index bb4781c0e..274861b8b 100644 --- a/lib/puppet/rails.rb +++ b/lib/puppet/rails.rb @@ -29,6 +29,27 @@ module Puppet::Rails } ) + def self.connect + # This global init does not work for testing, because we remove + # the state dir on every test. + unless ActiveRecord::Base.connected? + Puppet.config.use(:puppet) + + ActiveRecord::Base.logger = Logger.new(Puppet[:railslog]) + ActiveRecord::Base.allow_concurrency = true + ActiveRecord::Base.verify_active_connections! + + begin + ActiveRecord::Base.establish_connection(database_arguments()) + rescue => detail + if Puppet[:trace] + puts detail.backtrace + end + raise Puppet::Error, "Could not connect to database: %s" % detail + end + end + end + # The arguments for initializing the database connection. def self.database_arguments args = {:adapter => Puppet[:dbadapter]} @@ -54,24 +75,7 @@ module Puppet::Rails raise Puppet::DevError, "No activerecord, cannot init Puppet::Rails" end - # This global init does not work for testing, because we remove - # the state dir on every test. - unless ActiveRecord::Base.connected? - Puppet.config.use(:puppet) - - ActiveRecord::Base.logger = Logger.new(Puppet[:railslog]) - ActiveRecord::Base.allow_concurrency = true - ActiveRecord::Base.verify_active_connections! - - begin - ActiveRecord::Base.establish_connection(database_arguments()) - rescue => detail - if Puppet[:trace] - puts detail.backtrace - end - raise Puppet::Error, "Could not connect to database: %s" % detail - end - end + connect() unless ActiveRecord::Base.connection.tables.include?("resources") require 'puppet/rails/database/schema' diff --git a/lib/puppet/reports/log.rb b/lib/puppet/reports/log.rb index c33bf0a67..e6ae7e3d1 100644 --- a/lib/puppet/reports/log.rb +++ b/lib/puppet/reports/log.rb @@ -1,6 +1,6 @@ require 'puppet' -Puppet::Network::Server::Report.newreport(:log) do +Puppet::Network::Handler.report.newreport(:log) do desc "Send all received logs to the local log destinations." def process diff --git a/lib/puppet/reports/rrdgraph.rb b/lib/puppet/reports/rrdgraph.rb index 0fbe6e5ca..34162ac25 100644 --- a/lib/puppet/reports/rrdgraph.rb +++ b/lib/puppet/reports/rrdgraph.rb @@ -1,6 +1,6 @@ require 'puppet' -Puppet::Network::Server::Report.newreport(:rrdgraph) do +Puppet::Network::Handler.report.newreport(:rrdgraph) do desc "Graph all available data about hosts using the RRD library. You must have the RRD binary library installed to use this report, which you can get from [Tobias Oetiker's site](http://people.ee.ethz.ch/~oetiker/webtools/rrdtool/pub/contrib/). diff --git a/lib/puppet/reports/store.rb b/lib/puppet/reports/store.rb index ed4f08a9e..8d6e11379 100644 --- a/lib/puppet/reports/store.rb +++ b/lib/puppet/reports/store.rb @@ -1,6 +1,6 @@ require 'puppet' -Puppet::Network::Server::Report.newreport(:store, :useyaml => true) do +Puppet::Network::Handler.report.newreport(:store, :useyaml => true) do Puppet.config.use(:reporting) desc "Store the yaml report on disk. Each host sends its report as a YAML dump diff --git a/lib/puppet/reports/tagmail.rb b/lib/puppet/reports/tagmail.rb index 93e494d49..ba327bc85 100644 --- a/lib/puppet/reports/tagmail.rb +++ b/lib/puppet/reports/tagmail.rb @@ -1,20 +1,9 @@ require 'puppet' require 'pp' -Puppet.config.setdefaults(:reporting, - :tagmap => ["$confdir/tagmail.conf", - "The mapping between reporting tags and email addresses."], - :sendmail => [%x{which sendmail 2>/dev/null}.chomp, - "Where to find the sendmail binary with which to send email."], - :reportfrom => ["report@" + [Facter["hostname"].value, Facter["domain"].value].join("."), - "The 'from' email address for the reports."], - :smtpserver => ["none", - "The server through which to send email reports."] -) - require 'net/smtp' -Puppet::Network::Server::Report.newreport(:tagmail) do +Puppet::Network::Handler.report.newreport(:tagmail) do desc "This report sends specific log messages to specific email addresses based on the tags in the log messages. See the [tag documentation](/trac/puppet/wiki/UsingTags) for more information diff --git a/lib/puppet/sslcertificates.rb b/lib/puppet/sslcertificates.rb index eaee75d6c..503de7ae9 100755 --- a/lib/puppet/sslcertificates.rb +++ b/lib/puppet/sslcertificates.rb @@ -11,13 +11,15 @@ end module Puppet::SSLCertificates hostname = Facter["hostname"].value domain = Facter["domain"].value - if !domain || domain.empty? then - fqdn = hostname - else + if domain and domain != "" fqdn = [hostname, domain].join(".") + else + fqdn = hostname end Puppet.setdefaults("certificates", + :certname => [fqdn, "The name to use when handling certificates. Defaults + to the fully qualified domain name."], :certdir => ["$ssldir/certs", "The certificate directory."], :publickeydir => ["$ssldir/public_keys", "The public key directory."], :privatekeydir => { :default => "$ssldir/private_keys", @@ -33,15 +35,19 @@ module Puppet::SSLCertificates :desc => "Where puppetd stores the password for its private key. Generally unused." }, - :hostcert => { :default => "$certdir/#{fqdn}.pem", + :hostcsr => { :default => "$ssldir/csr_$certname.pem", + :mode => 0644, + :desc => "Where individual hosts store and look for their certificates." + }, + :hostcert => { :default => "$certdir/$certname.pem", :mode => 0644, :desc => "Where individual hosts store and look for their certificates." }, - :hostprivkey => { :default => "$privatekeydir/#{fqdn}.pem", + :hostprivkey => { :default => "$privatekeydir/$certname.pem", :mode => 0600, :desc => "Where individual hosts store and look for their private key." }, - :hostpubkey => { :default => "$publickeydir/#{fqdn}.pem", + :hostpubkey => { :default => "$publickeydir/$certname.pem", :mode => 0644, :desc => "Where individual hosts store and look for their public key." }, diff --git a/lib/puppet/sslcertificates/ca.rb b/lib/puppet/sslcertificates/ca.rb index 42ad57e48..723a0444b 100644 --- a/lib/puppet/sslcertificates/ca.rb +++ b/lib/puppet/sslcertificates/ca.rb @@ -5,8 +5,6 @@ class Puppet::SSLCertificates::CA attr_accessor :keyfile, :file, :config, :dir, :cert, :crl Puppet.setdefaults(:ca, - :ca => [true, - "Whether a CA should be started in puppetmasterd."], :cadir => { :default => "$ssldir/ca", :owner => "$user", :group => "$group", @@ -97,7 +95,7 @@ class Puppet::SSLCertificates::CA if FileTest.exists?(file) begin - if Puppet.execname == "puppetca" + if Puppet[:name] == "puppetca" puts "Removing %s" % file else Puppet.info "Removing %s" % file diff --git a/lib/puppet/sslcertificates/support.rb b/lib/puppet/sslcertificates/support.rb new file mode 100644 index 000000000..e37a52038 --- /dev/null +++ b/lib/puppet/sslcertificates/support.rb @@ -0,0 +1,128 @@ +require 'puppet/sslcertificates' + +# A module to handle reading of certificates. +module Puppet::SSLCertificates::Support + class MissingCertificate < Puppet::Error; end + class InvalidCertificate < Puppet::Error; end + + attr_reader :cacert + + # Some metaprogramming to create methods for retrieving and creating keys. + # This probably isn't fewer lines than defining each separately... + def self.keytype(name, options, &block) + var = "@%s" % name + + maker = "mk_%s" % name + reader = "read_%s" % name + + unless param = options[:param] + raise ArgumentError, "You must specify the parameter for the key" + end + + unless klass = options[:class] + raise ArgumentError, "You must specify the class for the key" + end + + # Define the method that creates it. + define_method(maker, &block) + + # Define the reading method. + define_method(reader) do + return nil unless FileTest.exists?(Puppet[param]) + begin + instance_variable_set(var, + klass.new(File.read(Puppet[param]))) + rescue => detail + raise InvalidCertificate, "Could not read %s: %s" % + [param, detail] + end + end + + # Define the overall method, which just calls the reader and maker + # as appropriate. + define_method(name) do + unless instance_variable_get(var) + unless cert = send(reader) + cert = send(maker) + Puppet.config.write(param) { |f| f.puts cert.to_pem } + end + instance_variable_set(var, cert) + end + instance_variable_get(var) + end + end + + # The key pair. + keytype :key, :param => :hostprivkey, :class => OpenSSL::PKey::RSA do + Puppet.info "Creating a new SSL key at %s" % Puppet[:hostprivkey] + key = OpenSSL::PKey::RSA.new(Puppet[:keylength]) + + # Our key meta programming can only handle one file, so we have + # to separately write out the public key. + Puppet.config.write(:hostpubkey) do |f| + f.print key.public_key.to_pem + end + return key + end + + # Our certificate request + keytype :csr, :param => :hostcsr, :class => OpenSSL::X509::Request do + Puppet.info "Creating a new certificate request for %s" % + Puppet[:certname] + + csr = OpenSSL::X509::Request.new + csr.version = 0 + csr.subject = OpenSSL::X509::Name.new([["CN", Puppet[:certname]]]) + csr.public_key = key.public_key + csr.sign(key, OpenSSL::Digest::MD5.new) + + return csr + end + + keytype :cert, :param => :hostcert, :class => OpenSSL::X509::Certificate do + raise MissingCertificate, "No host certificate" + end + + keytype :ca_cert, :param => :localcacert, :class => OpenSSL::X509::Certificate do + raise MissingCertificate, "No CA certificate" + end + + # Request a certificate from the remote system. This does all of the work + # of creating the cert request, contacting the remote system, and + # storing the cert locally. + def requestcert + begin + cert, cacert = caclient.getcert(@csr.to_pem) + rescue => detail + if Puppet[:trace] + puts detail.backtrace + end + raise Puppet::Error.new("Certificate retrieval failed: %s" % + detail) + end + + if cert.nil? or cert == "" + return nil + end + Puppet.config.write(:hostcert) do |f| f.print cert end + Puppet.config.write(:localcacert) do |f| f.print cacert end + #File.open(@certfile, "w", 0644) { |f| f.print cert } + #File.open(@cacertfile, "w", 0644) { |f| f.print cacert } + begin + @cert = OpenSSL::X509::Certificate.new(cert) + @cacert = OpenSSL::X509::Certificate.new(cacert) + retrieved = true + rescue => detail + raise Puppet::Error.new( + "Invalid certificate: %s" % detail + ) + end + + unless @cert.check_private_key(@key) + raise Puppet::DevError, "Received invalid certificate" + end + return retrieved + end +end + +# $Id$ diff --git a/lib/puppet/type/pfile.rb b/lib/puppet/type/pfile.rb index 22e7073c8..ffeba9f0e 100644 --- a/lib/puppet/type/pfile.rb +++ b/lib/puppet/type/pfile.rb @@ -4,7 +4,7 @@ require 'etc' require 'uri' require 'fileutils' require 'puppet/type/property' -require 'puppet/network/server/fileserver' +require 'puppet/network/handler' module Puppet newtype(:file) do @@ -104,7 +104,7 @@ module Puppet @parent.bucket = value value end - when Puppet::Network::Client::Dipper: + when Puppet::Network::Client.client(:Dipper): @parent.bucket = value value.name else @@ -311,7 +311,7 @@ module Puppet # This sets the @value on :backup, too self.bucket = obj elsif bucket == "puppet" - obj = Puppet::Network::Client::Dipper.new( + obj = Puppet::Network::Client.client(:Dipper).new( :Path => Puppet[:clientbucketdir] ) self.bucket = obj @@ -322,7 +322,7 @@ module Puppet else self.fail "Could not find filebucket %s" % bucket end - when Puppet::Network::Client::Dipper: # things are hunky-dorey + when Puppet::Network::Client.client(:Dipper): # things are hunky-dorey else self.fail "Invalid bucket type %s" % bucket.class end @@ -357,7 +357,7 @@ module Puppet else backup = self.bucket || self[:backup] case backup - when Puppet::Network::Client::Dipper: + when Puppet::Network::Client.client(:Dipper): notice "Recursively backing up to filebucket" require 'find' Find.find(self[:path]) do |f| @@ -396,7 +396,7 @@ module Puppet when "file": backup = self.bucket || self[:backup] case backup - when Puppet::Network::Client::Dipper: + when Puppet::Network::Client.client(:Dipper): sum = backup.backup(file) self.info "Filebucketed to %s with sum %s" % [backup.name, sum] @@ -975,7 +975,7 @@ module Puppet case uri.scheme when "file": unless defined? @@localfileserver - @@localfileserver = Puppet::Network::Server::FileServer.new( + @@localfileserver = Puppet::Network::Handler.handler(:fileserver).new( :Local => true, :Mount => { "/" => "localhost" }, :Config => false @@ -992,7 +992,7 @@ module Puppet # FIXME We should cache a copy of this server #sourceobj.server = Puppet::Network::NetworkClient.new(args) unless @clients.include?(source) - @clients[source] = Puppet::Network::Client::FileClient.new(args) + @clients[source] = Puppet::Network::Client.file.new(args) end sourceobj.server = @clients[source] diff --git a/lib/puppet/type/pfile/source.rb b/lib/puppet/type/pfile/source.rb index 4622e966d..2d569f2ed 100755 --- a/lib/puppet/type/pfile/source.rb +++ b/lib/puppet/type/pfile/source.rb @@ -1,5 +1,3 @@ -require 'puppet/network/server/fileserver' - module Puppet # Copy files from a local or remote source. This state *only* does any work # when the remote file is an actual file; in that case, this state copies @@ -7,7 +5,6 @@ module Puppet # this state, during retrieval, modifies the appropriate other states # so that things get taken care of appropriately. Puppet.type(:file).newproperty(:source) do - PINPARAMS = Puppet::Network::Server::FileServer::CHECKPARAMS attr_accessor :source, :local desc "Copy a file over the current file. Uses ``checksum`` to @@ -86,14 +83,14 @@ module Puppet begin desc = server.describe(path, @parent[:links]) - rescue Puppet::Network::NetworkClientError => detail + rescue Puppet::Network::XMLRPCClientError => detail self.err "Could not describe %s: %s" % [path, detail] return nil end args = {} - PINPARAMS.zip( + pinparams.zip( desc.split("\t") ).each { |param, value| if value =~ /^[0-9]+$/ @@ -144,6 +141,10 @@ module Puppet # Now, we just check to see if the checksums are the same return @parent.is(:checksum) == @stats[:checksum] end + + def pinparams + Puppet::Network::Handler.handler(:fileserver).params + end # This basically calls describe() on our file, and then sets all # of the local states appropriately. If the remote file is a normal @@ -209,7 +210,7 @@ module Puppet def should=(value) super - checks = (PINPARAMS + [:ensure]) + checks = (pinparams + [:ensure]) checks.delete(:checksum) @parent[:check] = checks @@ -231,7 +232,7 @@ module Puppet begin contents = sourceobj.server.retrieve(path, @parent[:links]) - rescue Puppet::Network::NetworkClientError => detail + rescue Puppet::Network::XMLRPCClientError => detail self.err "Could not retrieve %s: %s" % [path, detail] return nil diff --git a/lib/puppet/type/pfilebucket.rb b/lib/puppet/type/pfilebucket.rb index 5ec7e790f..9ed2bdb59 100755 --- a/lib/puppet/type/pfilebucket.rb +++ b/lib/puppet/type/pfilebucket.rb @@ -1,5 +1,3 @@ -require 'puppet/network/server/filebucket' - module Puppet newtype(:filebucket) do @doc = "A repository for backing up files. If no filebucket is @@ -86,7 +84,7 @@ module Puppet def mkbucket if self[:server] begin - @bucket = Puppet::Network::Client::Dipper.new( + @bucket = Puppet::Network::Client.client(:Dipper).new( :Server => self[:server], :Port => self[:port] ) @@ -97,7 +95,7 @@ module Puppet end else begin - @bucket = Puppet::Network::Client::Dipper.new( + @bucket = Puppet::Network::Client.client(:Dipper).new( :Path => self[:path] ) rescue => detail diff --git a/lib/puppet/util/config.rb b/lib/puppet/util/config.rb index 117f87f92..e139d2217 100644 --- a/lib/puppet/util/config.rb +++ b/lib/puppet/util/config.rb @@ -301,7 +301,7 @@ class Puppet::Util::Config # the group can be set in the config file. The problem # is that we're using the word 'group' twice, which is # confusing. - if var == :group and section == Puppet.execname and @config.include?(:group) + if var == :group and section == Puppet[:name] and @config.include?(:group) @config[:group].value = value end next @@ -519,7 +519,7 @@ class Puppet::Util::Config # Convert our list of objects into a configuration file. def to_config - str = %{The configuration file for #{Puppet.execname}. Note that this file + str = %{The configuration file for #{Puppet[:name]}. Note that this file is likely to have unused configuration parameters in it; any parameter that's valid anywhere in Puppet can be in any config file, even if it's not used. @@ -719,18 +719,16 @@ Generated on #{Time.now}. def convert(value) return value unless value return value unless value.is_a? String - if value =~ /\$(\w+)/ - parent = $1 - if pval = @parent[parent] - newval = value.to_s.sub(/\$#{parent.to_s}/, pval.to_s) - #return File.join(newval.split("/")) - return newval + newval = value.gsub(/\$(\w+)|\$\{(\w+)\}/) do |value| + varname = $2 || $1 + if pval = @parent[varname] + pval else raise Puppet::DevError, "Could not find value for %s" % parent end - else - return value end + + return newval end def desc=(value) diff --git a/lib/puppet/util/log.rb b/lib/puppet/util/log.rb index 38f9d8de1..b427f7b61 100644 --- a/lib/puppet/util/log.rb +++ b/lib/puppet/util/log.rb @@ -167,7 +167,7 @@ class Puppet::Util::Log if Syslog.opened? Syslog.close end - name = Puppet.execname + name = Puppet[:name] name = "puppet-#{name}" unless name =~ /puppet/ options = Syslog::LOG_PID | Syslog::LOG_NDELAY diff --git a/lib/puppet/util/subclass_loader.rb b/lib/puppet/util/subclass_loader.rb new file mode 100644 index 000000000..4f24d5544 --- /dev/null +++ b/lib/puppet/util/subclass_loader.rb @@ -0,0 +1,83 @@ +# A module for loading subclasses into an array and retrieving +# them by name. Also sets up a method for each class so +# that you can just do Klass.subclass, rather than Klass.subclass(:subclass). +module Puppet::Util::SubclassLoader + attr_accessor :loader, :classloader + + # Iterate over each of the subclasses. + def each + @subclasses ||= [] + @subclasses.each { |c| yield c } + end + + # The hook method that sets up subclass loading. We need the name + # of the method to create and the path in which to look for them. + def handle_subclasses(name, path) + unless self.is_a?(Class) + raise ArgumentError, "Must be a class to use SubclassLoader" + end + @subclasses = [] + @loader = Puppet::Util::Autoload.new(self, + path, :wrap => false + ) + + @subclassname = name + + @classloader = self + + # Now create a method for retrieving these subclasses by name. Note + # that we're defining a class method here, not an instance. + meta_def(name) do |subname| + subname = subname.to_s.downcase + + unless c = @subclasses.find { |c| c.name.to_s.downcase == subname } + loader.load(subname) + c = @subclasses.find { |c| c.name.to_s.downcase == subname } + + # Now make the method that returns this subclass. This way we + # normally avoid the method_missing method. + if c and ! respond_to?(subname) + define_method(subname) { c } + end + end + return c + end + end + + # Add a new class to our list. Note that this has to handle subclasses of + # subclasses, thus the reason we're keeping track of the @@classloader. + def inherited(sub) + @subclasses ||= [] + @subclasses << sub + sub.classloader = self.classloader + if self.classloader == self + @subclasses << sub + else + @classloader.inherited(sub) + end + end + + # See if we can load a class. + def method_missing(method, *args) + unless self == self.classloader + super + end + return nil unless defined? @subclassname + if c = self.send(@subclassname, method) + return c + else + return nil + end + end + + # Retrieve or calculate a name. + def name + unless defined? @name + @name = self.to_s.sub(/.+::/, '').intern + end + + return @name + end +end + +# $Id$ |