diff options
author | Luke Kanies <luke@madstop.com> | 2007-10-17 11:44:03 -0500 |
---|---|---|
committer | Luke Kanies <luke@madstop.com> | 2007-10-17 11:44:03 -0500 |
commit | d0bd48cc50cf90440429569e748877ab6e23491f (patch) | |
tree | f7dadaa96bb2fce07bf9efaef2b610d890c59b89 /lib/puppet/file_serving | |
parent | a815f7888b021a46332c23450795f057533d0093 (diff) | |
download | puppet-d0bd48cc50cf90440429569e748877ab6e23491f.tar.gz puppet-d0bd48cc50cf90440429569e748877ab6e23491f.tar.xz puppet-d0bd48cc50cf90440429569e748877ab6e23491f.zip |
Adding the first pass at modifying file serving
to work with indirection. I've split the
fileserver handler into four pieces: Mount (which so
far I've just copied wholesale), Configuration (responsible
for reading the configuration file and determining what's allowed),
Metadata (retrieves information about the files), and Content
(retrieves the actual file content).
I haven't added the indirection tests yet, and the configuration
tests are still all stubs.
Diffstat (limited to 'lib/puppet/file_serving')
-rw-r--r-- | lib/puppet/file_serving/configuration.rb | 261 | ||||
-rw-r--r-- | lib/puppet/file_serving/content.rb | 35 | ||||
-rw-r--r-- | lib/puppet/file_serving/metadata.rb | 48 | ||||
-rw-r--r-- | lib/puppet/file_serving/mount.rb | 180 |
4 files changed, 524 insertions, 0 deletions
diff --git a/lib/puppet/file_serving/configuration.rb b/lib/puppet/file_serving/configuration.rb new file mode 100644 index 000000000..234f66962 --- /dev/null +++ b/lib/puppet/file_serving/configuration.rb @@ -0,0 +1,261 @@ +# +# Created by Luke Kanies on 2007-10-16. +# Copyright (c) 2007. All rights reserved. + +require 'puppet' +require 'puppet/file_serving' + +class Puppet::FileServing::Configuration + + def self.create(options = {}) + unless defined?(@configuration) + @configuration = new(options) + end + @configuration + end + + def initialize(options = {}) + if options.include?(:Mount) + @passedconfig = true + unless options[:Mount].is_a?(Hash) + raise Puppet::DevError, "Invalid mount options %s" % + options[:Mount].inspect + end + + options[:Mount].each { |dir, name| + if FileTest.exists?(dir) + mount(dir, name) + end + } + mount(nil, MODULES) + else + @passedconfig = false + readconfig(false) # don't check the file the first time. + end + end + + private :initialize + + # 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 + + 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 + + # Return the mount for the Puppet modules; allows file copying from + # the modules. + def modules_mount(module_name, client) + # Find our environment, if we have one. + unless hostname = (client || Facter.value("hostname")) + raise ArgumentError, "Could not find hostname" + end + if node = Puppet::Node.find(hostname) + env = node.environment + else + env = nil + end + + # And use the environment to look up the module. + mod = Puppet::Module::find(module_name, env) + if mod + return @mounts[MODULES].copy(mod.name, mod.files) + else + return nil + end + 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 + + # 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]+)/?} + # Strip off the mount name. + mount_name, path = dir.sub(%r{^/}, '').split(File::Separator, 2) + + unless mount = modules_mount(mount_name, client) + unless mount = @mounts[mount_name] + raise FileServerError, "Fileserver module '%s' not mounted" % mount_name + end + end + else + raise FileServerError, "Fileserver error: Invalid path '%s'" % dir + end + + if path == "" + path = nil + elsif path + # Remove any double slashes that might have occurred + path = URI.unescape(path.gsub(/\/\//, "/")) + end + + return mount, path + end + + def to_s + "fileserver" + end +end diff --git a/lib/puppet/file_serving/content.rb b/lib/puppet/file_serving/content.rb new file mode 100644 index 000000000..773ae89a5 --- /dev/null +++ b/lib/puppet/file_serving/content.rb @@ -0,0 +1,35 @@ +# +# Created by Luke Kanies on 2007-10-16. +# Copyright (c) 2007. All rights reserved. + +require 'puppet/indirector' +require 'puppet/file_serving' + +# A class that handles retrieving file contents. +# It only reads the file when its content is specifically +# asked for. +class Puppet::FileServing::Content + extend Puppet::Indirector + indirects :content, :terminus_class => :file + + attr_reader :path + + def content + ::File.read(@path) + end + + def initialize(path) + raise ArgumentError.new("Files must be fully qualified") unless path =~ /^#{::File::SEPARATOR}/ + raise ArgumentError.new("Files must exist") unless FileTest.exists?(path) + + @path = path + end + + # Just return the file contents as the yaml. This allows us to + # avoid escaping or any such thing. LAK:FIXME Not really sure how + # this will behave if the file contains yaml... I think the far + # side needs to understand that it's a plain string. + def to_yaml + content + end +end diff --git a/lib/puppet/file_serving/metadata.rb b/lib/puppet/file_serving/metadata.rb new file mode 100644 index 000000000..69211b2d0 --- /dev/null +++ b/lib/puppet/file_serving/metadata.rb @@ -0,0 +1,48 @@ +# +# Created by Luke Kanies on 2007-10-16. +# Copyright (c) 2007. All rights reserved. + +require 'puppet' +require 'puppet/indirector' +require 'puppet/file_serving' +require 'puppet/util/checksums' + +# A class that handles retrieving file metadata. +class Puppet::FileServing::Metadata + include Puppet::Util::Checksums + + extend Puppet::Indirector + indirects :metadata, :terminus_class => :ral + + attr_reader :path, :owner, :group, :mode, :checksum_type, :checksum + + def checksum_type=(type) + raise(ArgumentError, "Unsupported checksum type %s" % type) unless respond_to?("%s_file" % type) + + @checksum_type = type + end + + def initialize(path, checksum_type = "md5") + raise ArgumentError.new("Files must be fully qualified") unless path =~ /^#{::File::SEPARATOR}/ + raise ArgumentError.new("Files must exist") unless FileTest.exists?(path) + + @path = path + + stat = File.stat(path) + @owner = stat.uid + @group = stat.gid + + # Set the octal mode, but as a string. + @mode = "%o" % (stat.mode & 007777) + + @checksum_type = checksum_type + @checksum = get_checksum + end + + private + + # Retrieve our checksum. + def get_checksum + ("{%s}" % @checksum_type) + send("%s_file" % @checksum_type, @path) + end +end diff --git a/lib/puppet/file_serving/mount.rb b/lib/puppet/file_serving/mount.rb new file mode 100644 index 000000000..719c30b16 --- /dev/null +++ b/lib/puppet/file_serving/mount.rb @@ -0,0 +1,180 @@ +# +# Created by Luke Kanies on 2007-10-16. +# Copyright (c) 2007. All rights reserved. + +require 'puppet/network/authstore' +require 'puppet/util/logging' +require 'puppet/file_serving' +require 'puppet/file_serving/metadata' +require 'puppet/file_serving/content' + +# Broker access to the filesystem, converting local URIs into metadata +# or content objects. +class Puppet::FileServing::Mount < Puppet::Network::AuthStore + include Puppet::Util::Logging + + attr_reader :name + + @@syncs = {} + + @@files = {} + + # 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 + + # Return a content instance for a given file. + def content(short_file, client = nil) + file_instance(Puppet::FileServing::Content, short_file, client) + end + + # Return a fully qualified path, given a short path and + # possibly a client name. + def file_path(short, client = nil) + p = path(client) + raise ArgumentError.new("Mounts without paths are not usable") unless p + File.join(p, short) + end + + # Create out object. It must have a name. + def initialize(name, path = nil) + unless name =~ %r{^[-\w]+$} + raise ArgumentError, "Invalid mount name format '%s'" % name + end + @name = name + + if path + self.path = path + else + @path = nil + end + + super() + end + + # Return a metadata instance with the appropriate information provided. + def metadata(short_file, client = nil) + file_instance(Puppet::FileServing::Metadata, short_file, client) + 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 ArgumentError, "%s does not exist" % path + end + unless FileTest.directory?(path) + raise ArgumentError, "%s is not a directory" % path + end + unless FileTest.readable?(path) + raise ArgumentError, "%s is not readable" % path + end + @expandable = false + end + @path = path + 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 + + private + + # 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 + + # Return an instance of the appropriate class. + def file_instance(klass, short_file, client = nil) + file = file_path(short_file, client) + + return nil unless FileTest.exists?(file) + + return klass.new(file) + 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 +end |