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/puppet/network/handler | |
| 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/puppet/network/handler')
| -rw-r--r-- | lib/puppet/network/handler/ca.rb | 152 | ||||
| -rwxr-xr-x | lib/puppet/network/handler/filebucket.rb | 150 | ||||
| -rwxr-xr-x | lib/puppet/network/handler/fileserver.rb | 590 | ||||
| -rwxr-xr-x | lib/puppet/network/handler/logger.rb | 52 | ||||
| -rw-r--r-- | lib/puppet/network/handler/master.rb | 214 | ||||
| -rwxr-xr-x | lib/puppet/network/handler/report.rb | 158 | ||||
| -rwxr-xr-x | lib/puppet/network/handler/resource.rb | 190 | ||||
| -rwxr-xr-x | lib/puppet/network/handler/runner.rb | 61 | ||||
| -rw-r--r-- | lib/puppet/network/handler/status.rb | 13 |
9 files changed, 1580 insertions, 0 deletions
diff --git a/lib/puppet/network/handler/ca.rb b/lib/puppet/network/handler/ca.rb new file mode 100644 index 000000000..06e0486bf --- /dev/null +++ b/lib/puppet/network/handler/ca.rb @@ -0,0 +1,152 @@ +require 'openssl' +require 'puppet' +require 'puppet/sslcertificates' +require 'xmlrpc/server' + +# Much of this was taken from QuickCert: +# http://segment7.net/projects/ruby/QuickCert/ + +class Puppet::Network::Handler + class CA < Handler + attr_reader :ca + + @interface = XMLRPC::Service::Interface.new("puppetca") { |iface| + iface.add_method("array getcert(csr)") + } + + def autosign + if defined? @autosign + @autosign + else + Puppet[:autosign] + end + end + + # FIXME autosign? should probably accept both hostnames and IP addresses + def autosign?(hostname) + # simple values are easy + if autosign == true or autosign == false + return autosign + end + + # we only otherwise know how to handle files + unless autosign =~ /^\// + raise Puppet::Error, "Invalid autosign value %s" % + autosign.inspect + end + + unless FileTest.exists?(autosign) + unless defined? @@warnedonautosign + @@warnedonautosign = true + Puppet.info "Autosign is enabled but %s is missing" % autosign + end + return false + end + auth = Puppet::Network::AuthStore.new + File.open(autosign) { |f| + f.each { |line| + next if line =~ /^\s*#/ + next if line =~ /^\s*$/ + auth.allow(line.chomp) + } + } + + # for now, just cheat and pass a fake IP address to allowed? + return auth.allowed?(hostname, "127.1.1.1") + end + + def initialize(hash = {}) + Puppet.config.use(:puppet, :certificates, :ca) + if hash.include? :autosign + @autosign = hash[:autosign] + end + + @ca = Puppet::SSLCertificates::CA.new(hash) + end + + # our client sends us a csr, and we either store it for later signing, + # or we sign it right away + def getcert(csrtext, client = nil, clientip = nil) + csr = OpenSSL::X509::Request.new(csrtext) + + # Use the hostname from the CSR, not from the network. + subject = csr.subject + + nameary = subject.to_a.find { |ary| + ary[0] == "CN" + } + + if nameary.nil? + Puppet.err( + "Invalid certificate request: could not retrieve server name" + ) + return "invalid" + end + + hostname = nameary[1] + + unless @ca + Puppet.notice "Host %s asked for signing from non-CA master" % hostname + return "" + end + + # We used to save the public key, but it's basically unnecessary + # and it mucks with the permissions requirements. + # save_pk(hostname, csr.public_key) + + certfile = File.join(Puppet[:certdir], [hostname, "pem"].join(".")) + + # first check to see if we already have a signed cert for the host + cert, cacert = ca.getclientcert(hostname) + if cert and cacert + Puppet.info "Retrieving existing certificate for %s" % hostname + #Puppet.info "Cert: %s; Cacert: %s" % [cert.class, cacert.class] + return [cert.to_pem, cacert.to_pem] + elsif @ca + if self.autosign?(hostname) or client.nil? + if client.nil? + Puppet.info "Signing certificate for CA server" + end + # okay, we don't have a signed cert + # if we're a CA and autosign is turned on, then go ahead and sign + # the csr and return the results + Puppet.info "Signing certificate for %s" % hostname + cert, cacert = @ca.sign(csr) + #Puppet.info "Cert: %s; Cacert: %s" % [cert.class, cacert.class] + return [cert.to_pem, cacert.to_pem] + else # just write out the csr for later signing + if @ca.getclientcsr(hostname) + Puppet.info "Not replacing existing request from %s" % hostname + else + Puppet.notice "Host %s has a waiting certificate request" % + hostname + @ca.storeclientcsr(csr) + end + return ["", ""] + end + else + raise "huh?" + end + end + + private + + # Save the public key. + def save_pk(hostname, public_key) + pkeyfile = File.join(Puppet[:publickeydir], [hostname, "pem"].join('.')) + + if FileTest.exists?(pkeyfile) + currentkey = File.open(pkeyfile) { |k| k.read } + unless currentkey == public_key.to_s + raise Puppet::Error, "public keys for %s differ" % hostname + end + else + File.open(pkeyfile, "w", 0644) { |f| + f.print public_key.to_s + } + end + end + end +end + +# $Id$ diff --git a/lib/puppet/network/handler/filebucket.rb b/lib/puppet/network/handler/filebucket.rb new file mode 100755 index 000000000..653d566b4 --- /dev/null +++ b/lib/puppet/network/handler/filebucket.rb @@ -0,0 +1,150 @@ +#-------------------- +# accept and serve files + + +require 'webrick' +require 'xmlrpc/server' +require 'xmlrpc/client' +require 'facter' +require 'digest/md5' +require 'puppet/external/base64' + +class Puppet::Network::Handler + class BucketError < RuntimeError; end + class FileBucket < Handler + @interface = XMLRPC::Service::Interface.new("puppetbucket") { |iface| + iface.add_method("string addfile(string, string)") + iface.add_method("string getfile(string)") + } + + Puppet::Util.logmethods(self, true) + attr_reader :name, :path + + # this doesn't work for relative paths + def self.paths(base,md5) + return [ + File.join(base, md5), + File.join(base, md5, "contents"), + File.join(base, md5, "paths") + ] + end + + def initialize(hash) + if hash.include?(:ConflictCheck) + @conflictchk = hash[:ConflictCheck] + hash.delete(:ConflictCheck) + else + @conflictchk = true + end + + if hash.include?(:Path) + @path = hash[:Path] + hash.delete(:Path) + else + if defined? Puppet + @path = Puppet[:bucketdir] + else + @path = File.expand_path("~/.filebucket") + end + end + + Puppet.config.use(:filebucket) + + @name = "Filebucket[#{@path}]" + end + + # accept a file from a client + def addfile(contents, path, client = nil, clientip = nil) + if client + contents = Base64.decode64(contents) + end + md5 = Digest::MD5.hexdigest(contents) + + bpath, bfile, pathpath = FileBucket.paths(@path,md5) + + # if it's a new directory... + if Puppet.recmkdir(bpath) + msg = "Adding %s(%s)" % [path, md5] + msg += " from #{client}" if client + self.info msg + # ...then just create the file + File.open(bfile, File::WRONLY|File::CREAT, 0440) { |of| + of.print contents + } + else # if the dir already existed... + # ...we need to verify that the contents match the existing file + if @conflictchk + unless FileTest.exists?(bfile) + raise(BucketError, + "No file at %s for sum %s" % [bfile,md5], caller) + end + + curfile = File.read(bfile) + + # If the contents don't match, then we've found a conflict. + # Unlikely, but quite bad. + if curfile != contents + raise(BucketError, + "Got passed new contents for sum %s" % md5, caller) + else + msg = "Got duplicate %s(%s)" % [path, md5] + msg += " from #{client}" if client + self.info msg + end + end + end + + contents = "" + + # in either case, add the passed path to the list of paths + paths = nil + addpath = false + if FileTest.exists?(pathpath) + File.open(pathpath) { |of| + paths = of.readlines.collect { |l| l.chomp } + } + + # unless our path is already there... + unless paths.include?(path) + addpath = true + end + else + addpath = true + end + + # if it's a new file, or if our path isn't in the file yet, add it + if addpath + File.open(pathpath, File::WRONLY|File::CREAT|File::APPEND) { |of| + of.puts path + } + end + + return md5 + end + + def getfile(md5, client = nil, clientip = nil) + bpath, bfile, bpaths = FileBucket.paths(@path,md5) + + unless FileTest.exists?(bfile) + return false + end + + contents = nil + File.open(bfile) { |of| + contents = of.read + } + + if client + return Base64.encode64(contents) + else + return contents + end + end + + def to_s + self.name + end + end +end + +# $Id$ diff --git a/lib/puppet/network/handler/fileserver.rb b/lib/puppet/network/handler/fileserver.rb new file mode 100755 index 000000000..6def09837 --- /dev/null +++ b/lib/puppet/network/handler/fileserver.rb @@ -0,0 +1,590 @@ +require 'puppet' +require 'puppet/network/authstore' +require 'webrick/httpstatus' +require 'cgi' +require 'delegate' + +class Puppet::Network::Handler + class FileServerError < Puppet::Error; end + class FileServer < Handler + attr_accessor :local + + CHECKPARAMS = [:mode, :type, :owner, :group, :checksum] + + @interface = XMLRPC::Service::Interface.new("fileserver") { |iface| + iface.add_method("string describe(string, string)") + iface.add_method("string list(string, string, boolean, array)") + iface.add_method("string retrieve(string, string)") + } + + 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::Handler::FileServerError, "Cannot currently copy links" + end + + mount, path = convert(url, client, clientip) + + if client + mount.debug "Describing %s for %s" % [url, client] + end + + obj = nil + unless obj = mount.check(path, links) + return "" + end + + desc = [] + CHECKPARAMS.each { |check| + if property = obj.property(check) + unless property.is + mount.debug "Manually retrieving info for %s" % check + property.retrieve + end + desc << property.is + else + if check == "checksum" and obj.property(:type).is == "file" + mount.notice "File %s does not have data for %s" % + [obj.name, check] + end + desc << nil + end + } + + return desc.join("\t") + end + + # Create a new fileserving module. + def initialize(hash = {}) + @mounts = {} + @files = {} + + if hash[:Local] + @local = hash[:Local] + else + @local = false + end + + if hash[:Config] == false + @noreadconfig = true + else + @config = Puppet::Util::LoadedFile.new( + hash[:Config] || Puppet[:fileserverconfig] + ) + @noreadconfig = false + end + + if hash.include?(:Mount) + @passedconfig = true + unless hash[:Mount].is_a?(Hash) + raise Puppet::DevError, "Invalid mount hash %s" % + hash[:Mount].inspect + end + + hash[:Mount].each { |dir, name| + if FileTest.exists?(dir) + self.mount(dir, name) + end + } + else + @passedconfig = false + readconfig(false) # don't check the file the first time. + end + end + + # List a specific directory's contents. + def list(url, links = :ignore, recurse = false, ignore = false, client = nil, clientip = nil) + mount, path = convert(url, client, clientip) + + if client + mount.debug "Listing %s for %s" % [url, client] + end + + obj = nil + unless FileTest.exists?(path) + return "" + end + + # We pass two paths here, but reclist internally changes one + # of the arguments when called internally. + desc = reclist(mount, path, path, recurse, ignore) + + if desc.length == 0 + mount.notice "Got no information on //%s/%s" % + [mount, path] + return "" + end + + desc.collect { |sub| + sub.join("\t") + }.join("\n") + end + + def local? + self.local + end + + # Mount a new directory with a name. + def mount(path, name) + if @mounts.include?(name) + if @mounts[name] != path + raise FileServerError, "%s is already mounted at %s" % + [@mounts[name].path, name] + else + # it's already mounted; no problem + return + end + end + + # Let the mounts do their own error-checking. + @mounts[name] = Mount.new(name, path) + @mounts[name].info "Mounted %s" % path + + return @mounts[name] + end + + # Retrieve a file from the local disk and pass it to the remote + # client. + def retrieve(url, links = :ignore, client = nil, clientip = nil) + links = links.intern if links.is_a? String + + mount, path = convert(url, client, clientip) + + if client + mount.info "Sending %s to %s" % [url, client] + end + + unless FileTest.exists?(path) + return "" + end + + links = links.intern if links.is_a? String + + if links == :ignore and FileTest.symlink?(path) + return "" + end + + str = nil + if links == :manage + raise Puppet::Error, "Cannot copy links yet." + else + str = File.read(path) + end + + if @local + return str + else + return CGI.escape(str) + end + end + + def umount(name) + @mounts.delete(name) if @mounts.include? name + end + + private + + def authcheck(file, mount, client, clientip) + # If we're local, don't bother passing in information. + if local? + client = nil + clientip = nil + end + unless mount.allowed?(client, clientip) + mount.warning "%s cannot access %s" % + [client, file] + raise Puppet::AuthorizationError, "Cannot access %s" % mount + end + end + + def convert(url, client, clientip) + readconfig + + url = URI.unescape(url) + + mount, stub = splitpath(url, client) + + authcheck(url, mount, client, clientip) + + path = nil + unless path = mount.subdir(stub, client) + mount.notice "Could not find subdirectory %s" % + "//%s/%s" % [mount, stub] + return "" + end + + return mount, path + end + + # Deal with ignore parameters. + def handleignore(children, path, ignore) + ignore.each { |ignore| + Dir.glob(File.join(path,ignore), File::FNM_DOTMATCH) { |match| + children.delete(File.basename(match)) + } + } + return children + end + + # Read the configuration file. + def readconfig(check = true) + return if @noreadconfig + + if check and ! @config.changed? + return + end + + newmounts = {} + begin + File.open(@config.file) { |f| + mount = nil + count = 1 + f.each { |line| + case line + when /^\s*#/: next # skip comments + when /^\s*$/: next # skip blank lines + when /\[(\w+)\]/: + name = $1 + if newmounts.include?(name) + raise FileServerError, "%s is already mounted at %s" % + [newmounts[name], name], count, @config.file + end + mount = Mount.new(name) + newmounts[name] = mount + when /^\s*(\w+)\s+(.+)$/: + var = $1 + value = $2 + case var + when "path": + begin + mount.path = value + rescue FileServerError => detail + Puppet.err "Removing mount %s: %s" % + [mount.name, detail] + newmounts.delete(mount.name) + end + when "allow": + value.split(/\s*,\s*/).each { |val| + begin + mount.info "allowing %s access" % val + mount.allow(val) + rescue AuthStoreError => detail + raise FileServerError.new(detail.to_s, + count, @config.file) + end + } + when "deny": + value.split(/\s*,\s*/).each { |val| + begin + mount.info "denying %s access" % val + mount.deny(val) + rescue AuthStoreError => detail + raise FileServerError.new(detail.to_s, + count, @config.file) + end + } + else + raise FileServerError.new("Invalid argument '%s'" % var, + count, @config.file) + end + else + raise FileServerError.new("Invalid line '%s'" % line.chomp, + count, @config.file) + end + count += 1 + } + } + rescue Errno::EACCES => detail + Puppet.err "FileServer error: Cannot read %s; cannot serve" % @config + #raise Puppet::Error, "Cannot read %s" % @config + rescue Errno::ENOENT => detail + Puppet.err "FileServer error: '%s' does not exist; cannot serve" % + @config + #raise Puppet::Error, "%s does not exit" % @config + #rescue FileServerError => detail + # Puppet.err "FileServer error: %s" % detail + end + + # Verify each of the mounts are valid. + # We let the check raise an error, so that it can raise an error + # pointing to the specific problem. + newmounts.each { |name, mount| + unless mount.valid? + raise FileServerError, "No path specified for mount %s" % + name + end + } + @mounts = newmounts + end + + # Recursively list the directory. FIXME This should be using + # puppet objects, not directly listing. + def reclist(mount, root, path, recurse, ignore) + # Take out the root of the path. + name = path.sub(root, '') + if name == "" + name = "/" + end + + if name == path + raise FileServerError, "Could not match %s in %s" % + [root, path] + end + + desc = [name] + ftype = File.stat(path).ftype + + desc << ftype + if recurse.is_a?(Integer) + recurse -= 1 + end + + ary = [desc] + if recurse == true or (recurse.is_a?(Integer) and recurse > -1) + if ftype == "directory" + children = Dir.entries(path) + if ignore + children = handleignore(children, path, ignore) + end + children.each { |child| + next if child =~ /^\.\.?$/ + reclist(mount, root, File.join(path, child), recurse, ignore).each { |cobj| + ary << cobj + } + } + end + end + + return ary.reject { |c| c.nil? } + end + + # Split the path into the separate mount point and path. + def splitpath(dir, client) + # the dir is based on one of the mounts + # so first retrieve the mount path + mount = nil + path = nil + if dir =~ %r{/(\w+)/?} + tmp = $1 + path = dir.sub(%r{/#{tmp}/?}, '') + + unless mount = @mounts[tmp] + raise FileServerError, "Fileserver module '%s' not mounted" % tmp + end + else + raise FileServerError, "Fileserver error: Invalid path '%s'" % dir + end + + if path == "" + path = nil + else + # Remove any double slashes that might have occurred + path = URI.unescape(path.gsub(/\/\//, "/")) + end + + return mount, path + end + + def to_s + "fileserver" + end + + # A simple class for wrapping mount points. Instances of this class + # don't know about the enclosing object; they're mainly just used for + # authorization. + class Mount < Puppet::Network::AuthStore + attr_reader :name + + Puppet::Util.logmethods(self, true) + + # Run 'retrieve' on a file. This gets the actual parameters, so + # we can pass them to the client. + def check(dir, links) + unless FileTest.exists?(dir) + self.notice "File source %s does not exist" % dir + return nil + end + + obj = fileobj(dir, links) + + # FIXME we should really have a timeout here -- we don't + # want to actually check on every connection, maybe no more + # than every 60 seconds or something. It'd be nice if we + # could use the builtin scheduling to do this. + + # Retrieval is enough here, because we don't want to cache + # any information in the state file, and we don't want to generate + # any state changes or anything. We don't even need to sync + # the checksum, because we're always going to hit the disk + # directly. + obj.retrieve + + return obj + end + + # Create a map for a specific client. + def clientmap(client) + { + "h" => client.sub(/\..*$/, ""), + "H" => client, + "d" => client.sub(/[^.]+\./, "") # domain name + } + end + + # Replace % patterns as appropriate. + def expand(path, client = nil) + # This map should probably be moved into a method. + map = nil + + if client + map = clientmap(client) + else + Puppet.notice "No client; expanding '%s' with local host" % + path + # Else, use the local information + map = localmap() + end + path.gsub(/%(.)/) do |v| + key = $1 + if key == "%" + "%" + else + map[key] || v + end + end + end + + # Do we have any patterns in our path, yo? + def expandable? + if defined? @expandable + @expandable + else + false + end + end + + # Create out object. It must have a name. + def initialize(name, path = nil) + unless name =~ %r{^\w+$} + raise FileServerError, "Invalid name format '%s'" % name + end + @name = name + + if path + self.path = path + else + @path = nil + end + + super() + end + + def fileobj(path, links) + obj = nil + if obj = Puppet.type(:file)[path] + # This can only happen in local fileserving, but it's an + # important one. It'd be nice if we didn't just set + # the check params every time, but I'm not sure it's worth + # the effort. + obj[:check] = CHECKPARAMS + else + obj = Puppet.type(:file).create( + :name => path, + :check => CHECKPARAMS + ) + end + + if links == :manage + links = :follow + end + + # This, ah, might be completely redundant + unless obj[:links] == links + obj[:links] = links + end + + return obj + end + + # Cache this manufactured map, since if it's used it's likely + # to get used a lot. + def localmap + unless defined? @@localmap + @@localmap = { + "h" => Facter.value("hostname"), + "H" => [Facter.value("hostname"), + Facter.value("domain")].join("."), + "d" => Facter.value("domain") + } + end + @@localmap + end + + # Return the path as appropriate, expanding as necessary. + def path(client = nil) + if expandable? + return expand(@path, client) + else + return @path + end + end + + # Set the path. + def path=(path) + # FIXME: For now, just don't validate paths with replacement + # patterns in them. + if path =~ /%./ + # Mark that we're expandable. + @expandable = true + else + unless FileTest.exists?(path) + raise FileServerError, "%s does not exist" % path + end + unless FileTest.directory?(path) + raise FileServerError, "%s is not a directory" % path + end + unless FileTest.readable?(path) + raise FileServerError, "%s is not readable" % path + end + @expandable = false + end + @path = path + end + + # Retrieve a specific directory relative to a mount point. + # If they pass in a client, then expand as necessary. + def subdir(dir = nil, client = nil) + basedir = self.path(client) + + dirname = if dir + File.join(basedir, dir.split("/").join(File::SEPARATOR)) + else + basedir + end + + dirname + end + + def to_s + "mount[#{@name}]" + end + + # Verify our configuration is valid. This should really check to + # make sure at least someone will be allowed, but, eh. + def valid? + return false unless @path + + return true + end + end + end +end + +# $Id$ diff --git a/lib/puppet/network/handler/logger.rb b/lib/puppet/network/handler/logger.rb new file mode 100755 index 000000000..f01b48325 --- /dev/null +++ b/lib/puppet/network/handler/logger.rb @@ -0,0 +1,52 @@ +require 'yaml' + +class Puppet::Network::Handler + class LoggerError < RuntimeError; end + + # Receive logs from remote hosts. + class Logger < Handler + @interface = XMLRPC::Service::Interface.new("puppetlogger") { |iface| + iface.add_method("void addlog(string)") + } + + # accept a log message from a client, and route it accordingly + def addlog(message, client = nil, clientip = nil) + unless message + raise Puppet::DevError, "Did not receive message" + end + + Puppet.info message.inspect + # if the client is set, then we're not local + if client + begin + message = YAML.load(CGI.unescape(message)) + #message = message + rescue => detail + raise XMLRPC::FaultException.new( + 1, "Could not unYAML log message from %s" % client + ) + end + end + + unless message + raise Puppet::DevError, "Could not resurrect message" + end + + # Mark it as remote, so it's not sent to syslog + message.remote = true + + if client + if ! message.source or message.source == "Puppet" + message.source = client + end + end + + Puppet::Util::Log.newmessage(message) + + # This is necessary or XMLRPC gets all pukey + return "" + end + end +end + +# $Id$ diff --git a/lib/puppet/network/handler/master.rb b/lib/puppet/network/handler/master.rb new file mode 100644 index 000000000..2b0a215d0 --- /dev/null +++ b/lib/puppet/network/handler/master.rb @@ -0,0 +1,214 @@ +require 'openssl' +require 'puppet' +require 'puppet/parser/interpreter' +require 'puppet/sslcertificates' +require 'xmlrpc/server' +require 'yaml' + +class Puppet::Network::Handler + class MasterError < Puppet::Error; end + class Master < Handler + include Puppet::Util + + attr_accessor :ast, :local + attr_reader :ca + + @interface = XMLRPC::Service::Interface.new("puppetmaster") { |iface| + iface.add_method("string getconfig(string)") + iface.add_method("int freshness()") + } + + # FIXME At some point, this should be autodocumenting. + def addfacts(facts) + # Add our server version to the fact list + facts["serverversion"] = Puppet.version.to_s + + # And then add the server name and IP + {"servername" => "hostname", + "serverip" => "ipaddress" + }.each do |var, fact| + if obj = Facter[fact] + facts[var] = obj.value + else + Puppet.warning "Could not retrieve fact %s" % fact + end + end + end + + # Manipulate the client name as appropriate. + def clientname(name, ip, facts) + # Always use the hostname from Facter. + client = facts["hostname"] + clientip = facts["ipaddress"] + if Puppet[:node_name] == 'cert' + if name + client = name + end + if ip + clientip = ip + end + end + + return client, clientip + end + + # Tell a client whether there's a fresh config for it + def freshness(client = nil, clientip = nil) + if Puppet.features.rails? and Puppet[:storeconfigs] + 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 == "") + host.ip = clientip + end + host.save + end + if defined? @interpreter + return @interpreter.parsedate + else + return 0 + end + end + + def initialize(hash = {}) + args = {} + + # Allow specification of a code snippet or of a file + if code = hash[:Code] + args[:Code] = code + else + args[:Manifest] = hash[:Manifest] || Puppet[:manifest] + end + + if hash[:Local] + @local = hash[:Local] + else + @local = false + end + + args[:Local] = @local + + if hash.include?(:CA) and hash[:CA] + @ca = Puppet::SSLCertificates::CA.new() + else + @ca = nil + end + + Puppet.debug("Creating interpreter") + + if hash.include?(:UseNodes) + args[:UseNodes] = hash[:UseNodes] + elsif @local + args[:UseNodes] = false + end + + # This is only used by the cfengine module, or if --loadclasses was + # specified in +puppet+. + if hash.include?(:Classes) + args[:Classes] = hash[:Classes] + end + + @interpreter = Puppet::Parser::Interpreter.new(args) + end + + def getconfig(facts, format = "marshal", client = nil, clientip = nil) + if @local + # we don't need to do anything, since we should already + # have raw objects + Puppet.debug "Our client is local" + else + Puppet.debug "Our client is remote" + + # XXX this should definitely be done in the protocol, somehow + case format + when "marshal": + Puppet.warning "You should upgrade your client. 'Marshal' will not be supported much longer." + begin + facts = Marshal::load(CGI.unescape(facts)) + rescue => detail + raise XMLRPC::FaultException.new( + 1, "Could not rebuild facts" + ) + end + when "yaml": + begin + facts = YAML.load(CGI.unescape(facts)) + rescue => detail + raise XMLRPC::FaultException.new( + 1, "Could not rebuild facts" + ) + end + else + raise XMLRPC::FaultException.new( + 1, "Unavailable config format %s" % format + ) + end + end + + client, clientip = clientname(client, clientip, facts) + + # Add any server-side facts to our server. + addfacts(facts) + + retobjects = nil + + # This is hackish, but there's no "silence" option for benchmarks + # right now + if @local + #begin + retobjects = @interpreter.run(client, facts) + #rescue Puppet::Error => detail + # Puppet.err detail + # raise XMLRPC::FaultException.new( + # 1, detail.to_s + # ) + #rescue => detail + # Puppet.err detail.to_s + # return "" + #end + else + benchmark(:notice, "Compiled configuration for %s" % client) do + begin + retobjects = @interpreter.run(client, facts) + rescue Puppet::Error => detail + Puppet.err detail + raise XMLRPC::FaultException.new( + 1, detail.to_s + ) + rescue => detail + Puppet.err detail.to_s + return "" + end + end + end + + if @local + return retobjects + else + str = nil + case format + when "marshal": + str = Marshal::dump(retobjects) + when "yaml": + str = YAML.dump(retobjects) + else + raise XMLRPC::FaultException.new( + 1, "Unavailable config format %s" % format + ) + end + return CGI.escape(str) + end + end + + def local? + if defined? @local and @local + return true + else + return false + end + end + end +end + +# $Id$ diff --git a/lib/puppet/network/handler/report.rb b/lib/puppet/network/handler/report.rb new file mode 100755 index 000000000..77e31f04a --- /dev/null +++ b/lib/puppet/network/handler/report.rb @@ -0,0 +1,158 @@ +# A simple server for triggering a new run on a Puppet client. +class Puppet::Network::Handler + class Report < Handler + extend Puppet::Util::ClassGen + + module ReportBase + include Puppet::Util::Docs + attr_writer :useyaml + + def useyaml? + if defined? @useyaml + @useyaml + else + false + end + end + end + + @interface = XMLRPC::Service::Interface.new("puppetreports") { |iface| + iface.add_method("string report(array)") + } + + @reports = {} + @reportloader = Puppet::Util::Autoload.new(self, "puppet/reports") + + class << self + attr_reader :hooks + end + + # Add a new report type. + def self.newreport(name, options = {}, &block) + name = symbolize(name) + + mod = genmodule(name, :extend => ReportBase, :hash => @reports, :block => block) + + if options[:useyaml] + mod.useyaml = true + end + + mod.send(:define_method, :report_name) do + name + end + end + + # Load a report. + def self.report(name) + name = name.intern if name.is_a? String + unless @reports.include? name + if @reportloader.load(name) + unless @reports.include? name + Puppet.warning( + "Loaded report file for %s but report was not defined" % + name + ) + return nil + end + else + return nil + end + end + @reports[symbolize(name)] + end + + # Collect the docs for all of our reports. + def self.reportdocs + docs = "" + + # Use this method so they all get loaded + reports.sort { |a,b| a.to_s <=> b.to_s }.each do |name| + mod = self.report(name) + docs += "## %s\n\n" % name + + docs += Puppet::Util::Docs.scrub(mod.doc) + "\n\n" + end + + docs + end + + # List each of the reports. + def self.reports + @reportloader.loadall + @reports.keys + end + + def initialize(*args) + super + Puppet.config.use(:reporting) + Puppet.config.use(:metrics) + end + + # Accept a report from a client. + def report(report, client = nil, clientip = nil) + # Unescape the report + unless @local + report = CGI.unescape(report) + end + + Puppet.info "Processing reports %s for %s" % [reports().join(", "), client] + begin + process(report) + rescue => detail + Puppet.err "Could not process report for %s: %s" % [client, detail] + if Puppet[:trace] + puts detail.backtrace + end + end + end + + private + + # Process the report using all of the existing hooks. + def process(yaml) + return if Puppet[:reports] == "none" + + # First convert the report to real objects + begin + report = YAML.load(yaml) + rescue => detail + Puppet.warning "Could not load report: %s" % detail + return + end + + # Used for those reports that accept yaml + client = report.host + + reports().each do |name| + if mod = self.class.report(name) + # We have to use a dup because we're including a module in the + # report. + newrep = report.dup + begin + newrep.extend(mod) + if mod.useyaml? + newrep.process(yaml) + else + newrep.process + end + rescue => detail + if Puppet[:trace] + puts detail.backtrace + end + Puppet.err "Report %s failed: %s" % + [name, detail] + end + else + Puppet.warning "No report named '%s'" % name + end + end + end + + # Handle the parsing of the reports attribute. + def reports + Puppet[:reports].gsub(/(^\s+)|(\s+$)/, '').split(/\s*,\s*/) + end + end +end + +# $Id$ diff --git a/lib/puppet/network/handler/resource.rb b/lib/puppet/network/handler/resource.rb new file mode 100755 index 000000000..92533dd2a --- /dev/null +++ b/lib/puppet/network/handler/resource.rb @@ -0,0 +1,190 @@ +require 'puppet' +require 'puppet/network/handler' + +# Serve Puppet elements. Useful for querying, copying, and, um, other stuff. +class Puppet::Network::Handler + class Resource < Handler + attr_accessor :local + + @interface = XMLRPC::Service::Interface.new("resource") { |iface| + iface.add_method("string apply(string, string)") + iface.add_method("string describe(string, string, array, array)") + iface.add_method("string list(string, array, string)") + } + + # Apply a TransBucket as a transaction. + def apply(bucket, format = "yaml", client = nil, clientip = nil) + unless @local + begin + case format + when "yaml": + bucket = YAML::load(Base64.decode64(bucket)) + else + raise Puppet::Error, "Unsupported format '%s'" % format + end + rescue => detail + raise Puppet::Error, "Could not load YAML TransBucket: %s" % detail + end + end + + component = bucket.to_type + + # Create a client, but specify the remote machine as the server + # because the class requires it, even though it's unused + client = Puppet::Network::Client.client(:Master).new(:Server => client||"localhost") + + # Set the objects + client.objects = component + + # And then apply the configuration. This way we're reusing all + # the code in there. It should probably just be separated out, though. + transaction = client.apply + + # And then clean up + component.remove + + # It'd be nice to return some kind of report, but... at this point + # we have no such facility. + return "success" + end + + # Describe a given object. This returns the 'is' values for every property + # available on the object type. + def describe(type, name, retrieve = nil, ignore = [], format = "yaml", client = nil, clientip = nil) + Puppet.info "Describing %s[%s]" % [type.to_s.capitalize, name] + @local = true unless client + typeklass = nil + unless typeklass = Puppet.type(type) + raise Puppet::Error, "Puppet type %s is unsupported" % type + end + + obj = nil + + retrieve ||= :all + ignore ||= [] + + if obj = typeklass[name] + obj[:check] = retrieve + else + begin + obj = typeklass.create(:name => name, :check => retrieve) + rescue Puppet::Error => detail + raise Puppet::Error, "%s[%s] could not be created: %s" % + [type, name, detail] + end + end + + unless obj + raise XMLRPC::FaultException.new( + 1, "Could not create %s[%s]" % [type, name] + ) + end + + trans = obj.to_trans + + # Now get rid of any attributes they specifically don't want + ignore.each do |st| + if trans.include? st + trans.delete(st) + end + end + + # And get rid of any attributes that are nil + trans.each do |attr, value| + if value.nil? + trans.delete(attr) + end + end + + unless @local + case format + when "yaml": + trans = Base64.encode64(YAML::dump(trans)) + else + raise XMLRPC::FaultException.new( + 1, "Unavailable config format %s" % format + ) + end + end + + return trans + end + + # Create a new fileserving module. + def initialize(hash = {}) + if hash[:Local] + @local = hash[:Local] + else + @local = false + end + end + + # List all of the elements of a given type. + def list(type, ignore = [], base = nil, format = "yaml", client = nil, clientip = nil) + @local = true unless client + typeklass = nil + unless typeklass = Puppet.type(type) + raise Puppet::Error, "Puppet type %s is unsupported" % type + end + + # They can pass in false + ignore ||= [] + ignore = [ignore] unless ignore.is_a? Array + bucket = Puppet::TransBucket.new + bucket.type = typeklass.name + + typeklass.list.each do |obj| + next if ignore.include? obj.name + + object = Puppet::TransObject.new(obj.name, typeklass.name) + bucket << object + end + + unless @local + case format + when "yaml": + begin + bucket = Base64.encode64(YAML::dump(bucket)) + rescue => detail + Puppet.err detail + raise XMLRPC::FaultException.new( + 1, detail.to_s + ) + end + else + raise XMLRPC::FaultException.new( + 1, "Unavailable config format %s" % format + ) + end + end + + return bucket + end + + private + + def authcheck(file, mount, client, clientip) + unless mount.allowed?(client, clientip) + mount.warning "%s cannot access %s" % + [client, file] + raise Puppet::AuthorizationError, "Cannot access %s" % mount + end + end + + # Deal with ignore parameters. + def handleignore(children, path, ignore) + ignore.each { |ignore| + Dir.glob(File.join(path,ignore), File::FNM_DOTMATCH) { |match| + children.delete(File.basename(match)) + } + } + return children + end + + def to_s + "resource" + end + end +end + +# $Id$ diff --git a/lib/puppet/network/handler/runner.rb b/lib/puppet/network/handler/runner.rb new file mode 100755 index 000000000..79084f847 --- /dev/null +++ b/lib/puppet/network/handler/runner.rb @@ -0,0 +1,61 @@ +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 + @interface = XMLRPC::Service::Interface.new("puppetrunner") { |iface| + iface.add_method("string run(string, string)") + } + + # Run the client configuration right now, optionally specifying + # tags and whether to ignore schedules + def run(tags = nil, ignoreschedules = false, fg = true, client = nil, clientip = nil) + # We need to retrieve the client + master = Puppet::Network::Client.client(:Master).instance + + unless master + raise MissingMasterError, "Could not find the master client" + end + + if Puppet::Util::Pidlock.new(Puppet[:puppetdlockfile]).locked? + Puppet.notice "Could not trigger run; already running" + return "running" + end + + if tags == "" + tags = nil + end + + if ignoreschedules == "" + ignoreschedules == nil + end + + msg = "" + if client + msg = "%s(%s) " % [client, clientip] + end + msg += "triggered run" % + if tags + msg += " with tags %s" % tags + end + + if ignoreschedules + msg += " without schedules" + end + + Puppet.notice msg + + # And then we need to tell it to run, with this extra info. + if fg + master.run(tags, ignoreschedules) + else + Puppet.newthread do + master.run(tags, ignoreschedules) + end + end + + return "success" + end + end +end + +# $Id$ diff --git a/lib/puppet/network/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$ |
