require 'puppet'
require 'puppet/network/authstore'
require 'webrick/httpstatus'
require 'cgi'
require 'delegate'
require 'sync'
class Puppet::Network::Handler
AuthStoreError = Puppet::AuthStoreError
class FileServerError < Puppet::Error; end
class FileServer < Handler
desc "The interface to Puppet's fileserving abilities."
attr_accessor :local
CHECKPARAMS = [:mode, :type, :owner, :group, :checksum]
# Special filserver module for puppet's module system
MODULES = "modules"
@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.getfileobject(path, links)
return ""
end
currentvalues = mount.check(obj)
desc = []
CHECKPARAMS.each { |check|
if value = currentvalues[check]
desc << value
else
if check == "checksum" and currentvalues[:type] == "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
}
self.mount(nil, MODULES)
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":
if mount.name == MODULES
Puppet.warning "The '#{MODULES}' module can not have a path. Ignoring attempt to set it"
else
begin
mount.path = value
rescue FileServerError => detail
Puppet.err "Removing mount %s: %s" %
[mount.name, detail]
newmounts.delete(mount.name)
end
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
unless newmounts[MODULES]
mount = Mount.new(MODULES)
mount.allow("*")
newmounts[MODULES] = mount
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}/?}, '')
mod = Puppet::Module::find(tmp)
if mod
mount = @mounts[MODULES].copy(mod.name, mod.files)
else
unless mount = @mounts[tmp]
raise FileServerError, "Fileserver module '%s' not mounted" % tmp
end
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
@@syncs = {}
@@files = {}
Puppet::Util.logmethods(self, true)
def getfileobject(dir, links)
unless FileTest.exists?(dir)
self.notice "File source %s does not exist" % dir
return nil
end
return fileobj(dir, links)
end
# Run 'retrieve' on a file. This gets the actual parameters, so
# we can pass them to the client.
def check(obj)
# 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.
# We're now caching file data, using the LoadedFile to check the
# disk no more frequently than the :filetimeout.
path = obj[:path]
sync = sync(path)
unless data = @@files[path]
data = {}
sync.synchronize(Sync::EX) do
@@files[path] = data
data[:loaded_obj] = Puppet::Util::LoadedFile.new(path)
data[:values] = properties(obj)
return data[:values]
end
end
changed = nil
sync.synchronize(Sync::SH) do
changed = data[:loaded_obj].changed?
end
if changed
sync.synchronize(Sync::EX) do
data[:values] = properties(obj)
return data[:values]
end
else
sync.synchronize(Sync::SH) do
return data[:values]
end
end
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
# Return the current values for the object.
def properties(obj)
obj.retrieve.inject({}) { |props, ary| props[ary[0].name] = ary[1]; props }
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 sync(path)
@@syncs[path] ||= Sync.new
@@syncs[path]
end
def to_s
"mount[%s]" % @name
end
# Verify our configuration is valid. This should really check to
# make sure at least someone will be allowed, but, eh.
def valid?
if name == MODULES
return @path.nil?
else
return ! @path.nil?
end
end
# Return a new mount with the same properties as +self+, except
# with a different name and path.
def copy(name, path)
result = self.clone
result.path = path
result.instance_variable_set(:@name, name)
return result
end
end
end
end
# $Id: fileserver.rb 2653 2007-07-06 00:54:51Z luke $
syntax highlighted by Code2HTML, v. 0.9.1