diff options
Diffstat (limited to 'lib/puppet')
-rw-r--r-- | lib/puppet/file_serving.rb | 7 | ||||
-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 | ||||
-rw-r--r-- | lib/puppet/indirector/content/file.rb | 10 | ||||
-rw-r--r-- | lib/puppet/indirector/metadata/ral.rb | 15 | ||||
-rw-r--r-- | lib/puppet/util/checksums.rb | 37 |
8 files changed, 593 insertions, 0 deletions
diff --git a/lib/puppet/file_serving.rb b/lib/puppet/file_serving.rb new file mode 100644 index 000000000..e7e2b898e --- /dev/null +++ b/lib/puppet/file_serving.rb @@ -0,0 +1,7 @@ +# +# Created by Luke Kanies on 2007-10-16. +# Copyright (c) 2007. All rights reserved. + +# Just a stub class. +class Puppet::FileServing # :nodoc: +end 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 diff --git a/lib/puppet/indirector/content/file.rb b/lib/puppet/indirector/content/file.rb new file mode 100644 index 000000000..1e0af4f12 --- /dev/null +++ b/lib/puppet/indirector/content/file.rb @@ -0,0 +1,10 @@ +# +# Created by Luke Kanies on 2007-10-16. +# Copyright (c) 2007. All rights reserved. + +require 'puppet/file_serving/content' +require 'puppet/indirector/file' + +class Puppet::FileServing::Content::File < Puppet::Indirector::File + desc "Retrieve file contents from disk." +end diff --git a/lib/puppet/indirector/metadata/ral.rb b/lib/puppet/indirector/metadata/ral.rb new file mode 100644 index 000000000..a06253315 --- /dev/null +++ b/lib/puppet/indirector/metadata/ral.rb @@ -0,0 +1,15 @@ +# +# Created by Luke Kanies on 2007-10-16. +# Copyright (c) 2007. All rights reserved. + +require 'puppet/file_serving/metadata' +require 'puppet/indirector/code' + +class Puppet::FileServing::Metadata::Ral < Puppet::Indirector::Code + desc "Retrieve file metadata using Puppet's Resource Abstraction Layer. + Returns everything about the file except its content." + + def find(file) + Puppet::Node::Facts.new(key, Facter.to_hash) + end +end diff --git a/lib/puppet/util/checksums.rb b/lib/puppet/util/checksums.rb new file mode 100644 index 000000000..6f6ea59b5 --- /dev/null +++ b/lib/puppet/util/checksums.rb @@ -0,0 +1,37 @@ +module Puppet::Util::Checksums + def md5(content) + require 'digest/md5' + Digest::MD5.hexdigest(content) + end + + def md5_file(filename) + require 'digest/md5' + + incr_digest = Digest::MD5.new() + File.open(filename, 'r') do |file| + file.each_line do |line| + incr_digest << line + end + end + + return incr_digest.hexdigest + end + + def sha1(content) + require 'digest/sha1' + Digest::SHA1.hexdigest(content) + end + + def sha1_file(filename) + require 'digest/sha1' + + incr_digest = Digest::SHA1.new() + File.open(filename, 'r') do |file| + file.each_line do |line| + incr_digest << line + end + end + + return incr_digest.hexdigest + end +end |