diff options
Diffstat (limited to 'lib/puppet/server')
-rw-r--r-- | lib/puppet/server/ca.rb | 159 | ||||
-rwxr-xr-x | lib/puppet/server/filebucket.rb | 277 | ||||
-rwxr-xr-x | lib/puppet/server/fileserver.rb | 264 | ||||
-rw-r--r-- | lib/puppet/server/master.rb | 92 | ||||
-rw-r--r-- | lib/puppet/server/servlet.rb | 112 |
5 files changed, 904 insertions, 0 deletions
diff --git a/lib/puppet/server/ca.rb b/lib/puppet/server/ca.rb new file mode 100644 index 000000000..65074c3f6 --- /dev/null +++ b/lib/puppet/server/ca.rb @@ -0,0 +1,159 @@ +require 'openssl' +require 'puppet' +require 'puppet/sslcertificates' +require 'xmlrpc/server' + +# Much of this was taken from QuickCert: +# http://segment7.net/projects/ruby/QuickCert/ + +module Puppet +class Server + class CAError < Puppet::Error; end + class CA + attr_reader :ca + + def self.interface + XMLRPC::Service::Interface.new("puppetca") { |iface| + iface.add_method("array getcert(csr)") + } + end + + Puppet::Server.addhandler(:CA, self) + + def autosign?(hostname) + # simple values are easy + asign = Puppet[:autosign] + if asign == true or asign == false + return asign + end + + # we only otherwise know how to handle files + unless asign =~ /^\// + raise Puppet::Error, "Invalid autosign value %s" % + asign + end + + unless FileTest.exists?(asign) + Puppet.warning "Autosign is enabled but %s is missing" % asign + return false + end + File.open(asign) { |f| + f.each { |line| + line.chomp! + if line =~ /^[.\w-]+$/ and line == hostname + Puppet.info "%s exactly matched %s" % [hostname, line] + return true + else + begin + rx = Regexp.new(line) + rescue => detail + Puppet.err( + "Could not create regexp out of autosign line %s: %s" % + [line, detail] + ) + next + end + + if hostname =~ rx + Puppet.info "%s matched %s" % [hostname, line] + return true + end + end + } + } + + return false + end + + def initialize(hash = {}) + @ca = Puppet::SSLCertificates::CA.new() + end + + # our client sends us a csr, and we either store it for later signing, + # or we sign it right away + def getcert(csrtext, request = nil) + # okay, i need to retrieve the hostname from the csr, and then + # verify that i get the same hostname through reverse lookup or + # something + + Puppet.info "Someone's trying for a cert" + csr = OpenSSL::X509::Request.new(csrtext) + + subject = csr.subject + + nameary = subject.to_a.find { |ary| + ary[0] == "CN" + } + + if nameary.nil? + Puppet.err "Invalid certificate request" + return "invalid" + end + + hostname = nameary[1] + + unless @ca + Puppet.notice "Host %s asked for signing from non-CA master" % hostname + return "" + end + + # okay, we're now going to store the public key if we don't already + # have it + public_key = csr.public_key + unless FileTest.directory?(Puppet[:publickeydir]) + Puppet.recmkdir(Puppet[:publickeydir]) + end + 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 + unless FileTest.directory?(Puppet[:certdir]) + Puppet.recmkdir(Puppet[:certdir], 0770) + end + certfile = File.join(Puppet[:certdir], [hostname, "pem"].join(".")) + + #puts hostname + #puts certfile + + unless FileTest.directory?(Puppet[:csrdir]) + Puppet.recmkdir(Puppet[:csrdir], 0770) + end + # 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) + # 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.info "Storing certificate request for %s" % hostname + @ca.storeclientcsr(csr) + end + return ["", ""] + end + else + raise "huh?" + end + end + end +end +end diff --git a/lib/puppet/server/filebucket.rb b/lib/puppet/server/filebucket.rb new file mode 100755 index 000000000..f0e717146 --- /dev/null +++ b/lib/puppet/server/filebucket.rb @@ -0,0 +1,277 @@ +#!/usr/bin/ruby -w + +#-------------------- +# accept and serve files +# +# $Id$ + + +require 'webrick' +#require 'webrick/https' +require 'xmlrpc/server' +require 'xmlrpc/client' +#require 'webrick/httpstatus' +require 'facter' +require 'digest/md5' +require 'base64' + +class BucketError < RuntimeError; end + +module FileBucket + DEFAULTPORT = 8139 + # this doesn't work for relative paths + def FileBucket.mkdir(dir) + if FileTest.exist?(dir) + return false + else + tmp = dir.sub(/^\//,'') + path = [File::SEPARATOR] + tmp.split(File::SEPARATOR).each { |dir| + path.push dir + unless FileTest.exist?(File.join(path)) + Dir.mkdir(File.join(path)) + end + } + return true + end + end + + def FileBucket.paths(base,md5) + return [ + File.join(base, md5), + File.join(base, md5, "contents"), + File.join(base, md5, "paths") + ] + end + + #--------------------------------------------------------------- + class Bucket + def initialize(hash) + # build our AST + + if hash.include?(:ConflictCheck) + @conflictchk = hash[:ConflictCheck] + hash.delete(:ConflictCheck) + else + @conflictchk = true + end + + if hash.include?(:Path) + @bucket = hash[:Path] + hash.delete(:Path) + else + if defined? Puppet + @bucket = Puppet[:bucketdir] + else + @bucket = File.expand_path("~/.filebucket") + end + end + + # XXX this should really be done using Puppet::Type instances... + FileBucket.mkdir(@bucket) + end + + # accept a file from a client + def addfile(string,path) + #puts "entering addfile" + contents = Base64.decode64(string) + #puts "string is decoded" + + md5 = Digest::MD5.hexdigest(contents) + #puts "md5 is made" + + bpath, bfile, pathpath = FileBucket.paths(@bucket,md5) + + # if it's a new directory... + if FileBucket.mkdir(bpath) + # ...then just create the file + #puts "creating file" + File.open(bfile, File::WRONLY|File::CREAT) { |of| + of.print contents + } + #puts "File is created" + 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.open(bfile) { |of| + curfile = of.read + } + + # 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) + end + end + #puts "Conflict check is done" + end + + # 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 + } + + # unless our path is already there... + unless paths.include?(path) + addpath = true + end + else + addpath = true + end + #puts "Path is checked" + + # 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 + } + #puts "Path is added" + end + + return md5 + end + + def getfile(md5) + bpath, bfile, bpaths = FileBucket.paths(@bucket,md5) + + unless FileTest.exists?(bfile) + return false + end + + contents = nil + File.open(bfile) { |of| + contents = of.read + } + + return Base64.encode64(contents) + end + + private + + def on_init + @default_namespace = 'urn:filebucket-server' + add_method(self, 'addfile', 'string', 'path') + add_method(self, 'getfile', 'md5') + end + + def cert(filename) + OpenSSL::X509::Certificate.new(File.open(File.join(@dir, filename)) { |f| + f.read + }) + end + + def key(filename) + OpenSSL::PKey::RSA.new(File.open(File.join(@dir, filename)) { |f| + f.read + }) + end + + end + #--------------------------------------------------------------- + + class BucketWebserver < WEBrick::HTTPServer + def initialize(hash) + unless hash.include?(:Port) + hash[:Port] = FileBucket::DEFAULTPORT + end + servlet = XMLRPC::WEBrickServlet.new + @bucket = FileBucket::Bucket.new(hash) + #puts @bucket + servlet.add_handler("bucket",@bucket) + super + + self.mount("/RPC2", servlet) + end + end + + class BucketClient < XMLRPC::Client + @@methods = [ :addfile, :getfile ] + + @@methods.each { |method| + self.send(:define_method,method) { |*args| + begin + call("bucket.%s" % method.to_s,*args) + rescue => detail + #puts detail + end + } + } + + def initialize(hash) + hash[:Path] ||= "/RPC2" + hash[:Server] ||= "localhost" + hash[:Port] ||= FileBucket::DEFAULTPORT + super(hash[:Server],hash[:Path],hash[:Port]) + end + end + + class Dipper + def initialize(hash) + if hash.include?(:Server) + @bucket = FileBucket::BucketClient.new( + :Server => hash[:Server] + ) + elsif hash.include?(:Bucket) + @bucket = hash[:Bucket] + elsif hash.include?(:Path) + @bucket = FileBucket::Bucket.new( + :Path => hash[:Path] + ) + end + end + + def backup(file) + unless FileTest.exists?(file) + raise(BucketError, "File %s does not exist" % file, caller) + end + contents = File.open(file) { |of| of.read } + + string = Base64.encode64(contents) + #puts "string is created" + + sum = @bucket.addfile(string,file) + #puts "file %s is added" % file + return sum + end + + def restore(file,sum) + restore = true + if FileTest.exists?(file) + contents = File.open(file) { |of| of.read } + + cursum = Digest::MD5.hexdigest(contents) + + # if the checksum has changed... + # this might be extra effort + if cursum == sum + restore = false + end + end + + if restore + #puts "Restoring %s" % file + newcontents = Base64.decode64(@bucket.getfile(sum)) + newsum = Digest::MD5.hexdigest(newcontents) + File.open(file,File::WRONLY|File::TRUNC) { |of| + of.print(newcontents) + } + #puts "Done" + return newsum + else + return nil + end + + end + end + #--------------------------------------------------------------- +end diff --git a/lib/puppet/server/fileserver.rb b/lib/puppet/server/fileserver.rb new file mode 100755 index 000000000..43e08655f --- /dev/null +++ b/lib/puppet/server/fileserver.rb @@ -0,0 +1,264 @@ +require 'puppet' +require 'cgi' + +module Puppet +class Server + class FileServerError < Puppet::Error; end + class FileServer + attr_accessor :local + + #CHECKPARAMS = %w{checksum type mode owner group} + CHECKPARAMS = [:mode, :type, :owner, :group, :checksum] + + def self.interface + XMLRPC::Service::Interface.new("fileserver") { |iface| + iface.add_method("string describe(string)") + iface.add_method("string list(string, boolean)") + iface.add_method("string retrieve(string)") + } + end + + Puppet::Server.addhandler(:FileServer, self) + + def check(dir) + unless FileTest.exists?(dir) + Puppet.notice "File source %s does not exist" % dir + return nil + end + + obj = nil + unless obj = Puppet::Type::PFile[dir] + obj = Puppet::Type::PFile.new( + :name => dir, + :check => CHECKPARAMS + ) + end + # 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 + #@files[mount].evaluate + obj.evaluate + + return obj + end + + def describe(file) + mount, path = splitpath(file) + + subdir = nil + unless subdir = subdir(mount, path) + Puppet.notice "Could not find subdirectory %s" % + "//%s/%s" % [mount, path] + return "" + end + + obj = nil + unless obj = self.check(subdir) + return "" + end + + desc = [] + CHECKPARAMS.each { |check| + if state = obj.state(check) + unless state.is + Puppet.notice "Manually retrieving info for %s" % check + state.retrieve + end + desc << state.is + else + if check == "checksum" and obj.state(:type).is == "file" + Puppet.notice "File %s does not have data for %s" % + [obj.name, check] + end + desc << nil + end + } + + return desc.join("\t") + end + + def initialize(hash = {}) + @mounts = {} + @files = {} + + if hash[:Local] + @local = hash[:Local] + else + @local = false + end + end + + def list(dir, recurse = false, sum = "md5") + mount, path = splitpath(dir) + + subdir = nil + unless subdir = subdir(mount, path) + Puppet.notice "Could not find subdirectory %s" % + "//%s/%s" % [mount, path] + return "" + end + + obj = nil + unless FileTest.exists?(subdir) + return "" + end + + #rmdir = File.dirname(File.join(@mounts[mount], path)) + rmdir = nameswap(dir, mount) + desc = self.reclist(rmdir, subdir, recurse) + + if desc.length == 0 + Puppet.notice "Got no information on //%s/%s" % + [mount, path] + return "" + end + + desc.collect { |sub| + sub.join("\t") + }.join("\n") + end + + def mount(dir, name) + if @mounts.include?(name) + if @mounts[name] != dir + raise FileServerError, "%s is already mounted at %s" % + [@mounts[name], name] + else + # it's already mounted; no problem + return + end + end + + unless name =~ %r{^\w+$} + raise FileServerError, "Invalid name format '%s'" % name + end + + unless FileTest.exists?(dir) + raise FileServerError, "%s does not exist" % dir + end + + if FileTest.directory?(dir) + if FileTest.readable?(dir) + Puppet.info "Mounting %s at %s" % [dir, name] + @mounts[name] = dir + else + raise FileServerError, "%s is not readable" % dir + end + else + raise FileServerError, "%s is not a directory" % dir + end + end + + # recursive listing function + def reclist(root, path, recurse) + #desc = [obj.name.sub(%r{#{root}/?}, '')] + name = path.sub(root, '') + if name == "" + name = "/" + end + + if name == path + raise Puppet::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" + Dir.entries(path).each { |child| + next if child =~ /^\.\.?$/ + self.reclist(root, File.join(path, child), recurse).each { |cobj| + ary << cobj + } + } + end + end + + return ary.reject { |c| c.nil? } + end + + def retrieve(file) + mount, path = splitpath(file) + + unless (@mounts.include?(mount)) + # FIXME I really need some better way to pass and handle xmlrpc errors + raise FileServerError, "%s not mounted" % mount + end + + fpath = nil + if path + fpath = File.join(@mounts[mount], path) + else + fpath = @mounts[mount] + end + + unless FileTest.exists?(fpath) + return "" + end + + str = File.read(fpath) + + if @local + return str + else + return CGI.escape(str) + end + end + + private + + def nameswap(name, mount) + name.sub(/\/#{mount}/, @mounts[mount]).gsub(%r{//}, '/').sub( + %r{/$}, '' + ) + #Puppet.info "Swapped %s to %s" % [name, newname] + #newname + end + + def splitpath(dir) + # the dir is based on one of the mounts + # so first retrieve the mount path + mount = nil + path = nil + if dir =~ %r{/(\w+)/?} + mount = $1 + path = dir.sub(%r{/#{mount}/?}, '') + + unless @mounts.include?(mount) + raise FileServerError, "%s not mounted" % mount + end + else + raise FileServerError, "Invalid path '%s'" % dir + end + + if path == "" + path = nil + end + return mount, path + end + + def subdir(mount, dir) + basedir = @mounts[mount] + + dirname = nil + if dir + dirname = File.join(basedir, dir.split("/").join(File::SEPARATOR)) + else + dirname = basedir + end + + return dirname + end + end +end +end + +# $Id$ diff --git a/lib/puppet/server/master.rb b/lib/puppet/server/master.rb new file mode 100644 index 000000000..f3f0411e9 --- /dev/null +++ b/lib/puppet/server/master.rb @@ -0,0 +1,92 @@ +require 'openssl' +require 'puppet' +require 'puppet/parser/interpreter' +require 'puppet/sslcertificates' +require 'xmlrpc/server' + +module Puppet +class Server + class MasterError < Puppet::Error; end + class Master + attr_accessor :ast, :local + attr_reader :ca + + def self.interface + XMLRPC::Service::Interface.new("puppetmaster") { |iface| + iface.add_method("string getconfig(string)") + } + end + + Puppet::Server.addhandler(:Master, self) + + def initialize(hash = {}) + + # build our AST + @file = hash[:File] || Puppet[:manifest] + @parser = Puppet::Parser::Parser.new() + @parser.file = @file + @ast = @parser.parse + hash.delete(:File) + + if hash[:Local] + @local = hash[:Local] + else + @local = false + end + + if hash.include?(:CA) and hash[:CA] + @ca = Puppet::SSLCertificates::CA.new() + else + @ca = nil + end + end + + def getconfig(facts, request = nil) + if request + #Puppet.warning request.inspect + end + 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 + begin + facts = Marshal::load(CGI.unescape(facts)) + rescue => detail + puts "AAAAA" + puts detail + exit + end + end + + Puppet.debug("Creating interpreter") + + begin + interpreter = Puppet::Parser::Interpreter.new( + :ast => @ast, + :facts => facts + ) + rescue => detail + return detail.to_s + end + + Puppet.debug("Running interpreter") + begin + retobjects = interpreter.run() + rescue => detail + Puppet.err detail.to_s + return "" + end + + if @local + return retobjects + else + return CGI.escape(Marshal::dump(retobjects)) + end + end + end +end +end diff --git a/lib/puppet/server/servlet.rb b/lib/puppet/server/servlet.rb new file mode 100644 index 000000000..4db9f6c0e --- /dev/null +++ b/lib/puppet/server/servlet.rb @@ -0,0 +1,112 @@ +require 'xmlrpc/server' + +module Puppet +class Server + class ServletError < RuntimeError; end + class Servlet < XMLRPC::WEBrickServlet + 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 + + def initialize(server, handlers) + #Puppet.info server.inspect + + # 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() + + handlers.each { |handler| + Puppet.debug "adding handler for %s" % handler.class + self.add_handler(handler.class.interface, handler) + } + + @request = nil + self.set_service_hook { |obj, *args| + #raise "crap!" + if @request + args.push @request + #obj.call(args, @request) + end + begin + obj.call(*args) + rescue => detail + Puppet.warning obj.inspect + Puppet.err "Could not call: %s" % detail.to_s + end + } + end + + def service(request, response) + @request = request + if @request.client_cert + Puppet.info "client cert is %s" % @request.client_cert + end + if @request.server_cert + Puppet.info "server cert is %s" % @request.server_cert + end + #p @request + begin + super + rescue => detail + Puppet.err "Could not service request: %s: %s" % + [detail.class, detail] + end + @request = nil + end + + private + + # this is pretty much just a copy of the original method but with more + # feedback + def dispatch(methodname, *args) + #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 +end |