diff options
| author | luke <luke@980ebf18-57e1-0310-9a29-db15c13687c0> | 2007-02-08 01:39:39 +0000 |
|---|---|---|
| committer | luke <luke@980ebf18-57e1-0310-9a29-db15c13687c0> | 2007-02-08 01:39:39 +0000 |
| commit | 7e07e3dc843798bdbc7a03428ca054adaff2fb72 (patch) | |
| tree | 34d0f9f8c2ee11bdc281e6e4d18cad444253fe36 /lib/puppet/network/server/fileserver.rb | |
| parent | 6d8068eddd0d29ec53f62557eb53f6ebb8e40591 (diff) | |
| download | puppet-7e07e3dc843798bdbc7a03428ca054adaff2fb72.tar.gz puppet-7e07e3dc843798bdbc7a03428ca054adaff2fb72.tar.xz puppet-7e07e3dc843798bdbc7a03428ca054adaff2fb72.zip | |
Moving all of the client and server code into a single network/ directory. In other words, more code structure cleanup.
git-svn-id: https://reductivelabs.com/svn/puppet/trunk@2179 980ebf18-57e1-0310-9a29-db15c13687c0
Diffstat (limited to 'lib/puppet/network/server/fileserver.rb')
| -rwxr-xr-x | lib/puppet/network/server/fileserver.rb | 589 |
1 files changed, 589 insertions, 0 deletions
diff --git a/lib/puppet/network/server/fileserver.rb b/lib/puppet/network/server/fileserver.rb new file mode 100755 index 000000000..904d497ca --- /dev/null +++ b/lib/puppet/network/server/fileserver.rb @@ -0,0 +1,589 @@ +require 'puppet' +require 'webrick/httpstatus' +require 'cgi' +require 'delegate' + +class Puppet::Network::Server + 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| + iface.add_method("string describe(string, string)") + iface.add_method("string list(string, string, boolean, array)") + iface.add_method("string retrieve(string, string)") + } + + # 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" + 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$ |
