diff --git a/ext/nagios/naggen b/ext/nagios/naggen index 6ff09e260..a9e04c47a 100755 --- a/ext/nagios/naggen +++ b/ext/nagios/naggen @@ -1,302 +1,302 @@ #!/usr/bin/env ruby # # = Synopsis # # Generate Nagios configurations from Puppet Resources in an ActiveRecord database # # = Usage # # naggen [-h|--help] [-d|--debug] [-v|--verbose] [--compare] # # = Description # # This executable is a means of short-circuiting the process of generating nagios # configurations on your server using Puppet Exported Resources. It skips any possible # naming conflicts resulting from Puppet's resource uniqueness requirements, and it # also skips the inefficiencies involved in converting and transporting large numbers # of Puppet resources. # # At the least, the machine that runs this will need ActiveRecord (2.0.2) installed, # along with any database libraries you use. # # = Options # # Note that any configuration parameter that's valid in the configuration file # is also a valid long argument. For example, 'ssldir' is a valid configuration # parameter, so you can specify '--ssldir ' as an argument. # # You can add naggen-specific settings to your puppet.conf in a '[naggen]' section, # just like any other executable. # # See the configuration file documentation at # http://reductivelabs.com/projects/puppet/reference/configref.html for # the full list of acceptable parameters. A commented list of all # configuration options can also be generated by running puppet with # '--genconfig'. # # compare:: # Compare new and old files and only backup and write if the files are different. # Potentially expensive computationally, but idempotent. Will exit with 0 if # no changes were made and 1 if there were. # # debug:: # Enable full debugging. # # detailed-exitcodes:: # Provide transaction information via exit codes. If this is enabled, an exit # code of '2' means there were changes, and an exit code of '4' means that there # were failures during the transaction. # # help:: # Print this help message # # verbose:: # Print extra information. # # = Example # # naggen --storeconfigs --confdir /foo --compare # # = Author # # Luke Kanies # # = Copyright # # Copyright (c) 2009 Reductive Labs, LLC # Licensed under the GPL 2 require 'puppet' require 'puppet/rails' require 'puppet/rails/resource' require 'puppet/rails/param_value' require 'puppet/network/client' require 'puppet/parser/collector' require 'puppet/provider/naginator' require 'getoptlong' # Monkey-patch the rails resources so we can # easily convert them to nagios instances. class Puppet::Rails::Resource def to_param_hash values = @params_hash || Puppet::Rails::ParamValue.find_all_params_from_resource(self) if values.size == 0 return {} end values.inject({}) do |hash, value| hash[value['name']] ||= [] hash[value['name']] << value["value"] hash end end def to_nagios unless nagios_type = Nagios::Base.type(restype.sub("Nagios_", '').to_sym) raise Puppet::DevError, "Could not find nagios type '%s'" % restype end result = nagios_type.new to_param_hash.each do |param, value| next unless nagios_type.parameter?(param) result[param] = value end result[:name] = self.title result end end class NagiosWriter class FakeScope def debug(string) Puppet.debug string end def host "this host doesn't exist" end end attr_accessor :nagios_type, :bucket def backup(target) return unless FileTest.exist?(target) and File.stat(target).size > 0 Puppet.info "Backing up %s" % target bucket.backup(target) end def collector collector = Puppet::Parser::Collector.new(FakeScope.new, "nagios_" + @nagios_type.to_s, nil, nil, :exported) # We don't have a scope, so we're stubbing everything out that would interact # with the scope. class << collector def collect_virtual(*args) [] end def exported_resource(res) res end end collector end def default_target "/etc/nagios/nagios_#{nagios_type.to_s}.cfg" end def evaluate return unless resources = rails_resources() resources_by_target = resources.inject({}) do |hash, resource| target = resource["target"] || default_target hash[target] ||= [] hash[target] << resource hash end changed = false resources_by_target.each do |target, resources| begin result = write(target, resources) rescue => detail $stderr.puts detail.backtrace Puppet.err "Could not write to %s: %s" % [target, detail] end changed = true if result end changed end def initialize(nagios_type) @nagios_type = nagios_type - @bucket = Puppet::Network::Client.client(:Dipper).new(:Path => Puppet[:clientbucketdir]) + @bucket = Puppet::FileBucket::Dipper.new(:Path => Puppet[:clientbucketdir]) end def rails_resources collector.send(:collect_exported) end def write(target, resources) # Skip the nagios type when we have no resources and no existing # file. return if resources.empty? and ! FileTest.exist?(target) dir = File.dirname(target) unless FileTest.exist?(dir) FileUtils.mkdir_p(dir) end count = 0 tempfile = target + ".tmp" File.open(tempfile, "w") do |file| resources.each do |resource| count += 1 file.puts resource.to_nagios.to_s.gsub("_naginator_name", Puppet::Provider::Naginator::NAME_STRING) end end if $options[:compare] if FileTest.exist?(target) and File.read(tempfile) == File.read(target) return false end end backup(target) # Atomic rename File.rename(tempfile, target) Puppet.notice "Wrote %s resources to %s" % [count, target] return true ensure File.unlink(tempfile) if tempfile and FileTest.exist?(tempfile) end end arguments = [ [ "--compare", "-c", GetoptLong::NO_ARGUMENT ], [ "--debug", "-d", GetoptLong::NO_ARGUMENT ], [ "--verbose", "-v", GetoptLong::NO_ARGUMENT ], [ "--help", "-h", GetoptLong::NO_ARGUMENT ] ] Puppet.settings.addargs(arguments) result = GetoptLong.new(*arguments) $options = {} result.each { |opt,arg| case opt when "--help" begin require 'rdoc/usage' RDoc::usage && exit rescue LoadError docs = [] File.readlines(__FILE__).each do |line| next if line =~ /^#\!/ unless line =~ /^#/ next if docs.length == 0 # skip the first line or so break # else, we've passed the docs, so just break end docs << line.sub(/^# ?/, '') end print docs exit end when "--compare" $options[:compare] = true when "--verbose" $options[:verbose] = true when "--debug" $options[:debug] = true when "--debug" $options[:debug] = true else Puppet.settings.handlearg(opt, arg) end } # Read in Puppet settings, so we know how Puppet's configured. Puppet.parse_config Puppet::Util::Log.newdestination(:console) if $options[:debug] Puppet::Util::Log.level = :debug elsif $options[:verbose] Puppet::Util::Log.level = :info end # See if Naginator is installed directly, else load Puppet's version. begin require 'nagios' rescue LoadError require 'puppet/external/nagios' end changed = false Nagios::Base.eachtype do |name, type| writer = NagiosWriter.new(name) changed = true if writer.evaluate end if $options[:compare] and changed exit(1) else exit(0) end diff --git a/ext/puppet-test b/ext/puppet-test index dbbde40c6..53333076f 100755 --- a/ext/puppet-test +++ b/ext/puppet-test @@ -1,536 +1,536 @@ #!/usr/bin/env ruby # == Synopsis # # Test individual client performance. Can compile configurations, describe # files, or retrieve files. # # = Usage # # puppet-test [-c|--compile] [-D|--describe ] [-d|--debug] # [--fork ] [-h|--help] [-H|--hostname ] [-l|--list] [-r|--repeat ] # [-R|--retrieve ] [-t|--test ] [-V|--version] [-v|--verbose] # # = Description # # This is a simple script meant for doing performance tests with Puppet. By # default it pulls down a compiled configuration, but it supports multiple # other tests. # # = Options # # Note that any configuration parameter that's valid in the configuration file # is also a valid long argument. For example, 'server' is a valid configuration # parameter, so you can specify '--server ' as an argument. # # See the configuration file documentation at # http://reductivelabs.com/projects/puppet/reference/configref.html for # the full list of acceptable parameters. A commented list of all # configuration $options can also be generated by running puppetd with # '--genconfig'. # # compile:: # Compile the client's configuration. The default. # # debug:: # Enable full debugging. # # describe:: # Describe the file being tested. This is a query to get information about # the file from the server, to determine if it should be copied, and is the # first half of every file transfer. # # fork:: # Fork the specified number of times, thus acting as multiple clients. # # fqdn:: # Set the fully-qualified domain name of the client. This is only used for # certificate purposes, but can be used to override the discovered hostname. # If you need to use this flag, it is generally an indication of a setup problem. # # help:: # Print this help message # # list:: # List all available tests. # # node:: # Specify the node to use. This is useful for looking up cached yaml data # in your :clientyaml directory, and forcing a specific host's configuration to # get compiled. # # pause:: # Pause before starting test (useful for testing with dtrace). # # repeat:: # How many times to perform the test. # # retrieve:: # Test file retrieval performance. Retrieves the specified file from the # remote system. Note that the server should be specified via --server, # so the argument to this option is just the remote module name and path, # e.g., "/dist/apps/myapp/myfile", where "dist" is the module and # "apps/myapp/myfile" is the path to the file relative to the module. # # test:: # Specify the test to run. You can see the list of tests by running this command with --list. # # verbose:: # Turn on verbose reporting. # # version:: # Print the puppet version number and exit. # # = Example # # puppet-test --retrieve /module/path/to/file # # = Author # # Luke Kanies # # = Copyright # # Copyright (c) 2005, 2006 Reductive Labs, LLC # Licensed under the GNU Public License # Do an initial trap, so that cancels don't get a stack trace. trap(:INT) do $stderr.puts "Cancelling startup" exit(1) end require 'puppet' require 'puppet/network/client' require 'getoptlong' class Suite attr_reader :name, :doc @@suites = {} @@tests = {} def self.[](name) @@suites[name] end # Run a test by first finding the suite then running the appropriate test. def self.run(test) unless suite_name = @@tests[test] raise "Unknown test %s" % test end unless suite = @@suites[suite_name] raise "Unknown test suite %s from test %s" % [suite_name, test] end suite.run(test) end # What suites are available? def self.suites @@suites.keys end def forked? defined? @forking end # Create a new test suite. def initialize(name, doc, &block) @name = name @doc = doc @tests = {} @@suites[name] = self raise "You must pass a block to the Test" unless block_given? instance_eval(&block) end # Define a new type of test on this suite. def newtest(name, doc, &block) @tests[name] = doc if @@tests[name] raise "Test names must be unique; cannot redefine %s" % name end @@tests[name] = @name meta_def(name, &block) end # Run the actual test. def run(test) unless doc = @tests[test] raise "Suite %s only supports tests %s; not %s" % [@name, @tests.keys.collect { |k| k.to_s }.join(","), test] end puts "Running %s %s test" % [@name, test] prepare() if respond_to?(:prepare) if $options[:pause] puts "Hit any key to continue" $stdin.readline puts "Continuing with test" end if $options[:fork] > 0 @forking = true $options[:fork].times { if pid = fork $pids << pid else break end } end $options[:repeat].times do |i| @count = i if forked? msg = doc + " in PID %s" % Process.pid else msg = doc end Puppet::Util.benchmark(:notice, msg) do begin send(test) rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "%s failed: %s" % [@name, detail.to_s] end end end end # What tests are available on this suite? def tests @tests.keys end end Suite.new :parser, "Manifest parsing" do newtest :parse, "Parsed files" do @parser = Puppet::Parser::Parser.new(:environment => Puppet[:environment]) @parser.file = Puppet[:manifest] @parser.parse end end Suite.new :local_catalog, "Local catalog handling" do def prepare @node = Puppet::Node.find($options[:nodes][0]) end newtest :compile, "Compiled catalog" do Puppet::Resource::Catalog.find(@node) end end Suite.new :remote_catalog, "Remote catalog handling" do def prepare $args[:cache] = false # Create a config client and pull the config down @client = Puppet::Network::Client.master.new($args) unless @client.read_cert fail "Could not read client certificate" end if tmp = Puppet::Node::Facts.find($options[:nodes][0]) @facts = tmp.values else raise "Could not find facts for %s" % $optins[:nodes][0] end if host = $options[:fqdn] @facts["fqdn"] = host @facts["hostname"] = host.sub(/\..+/, '') @facts["domain"] = host.sub(/^[^.]+\./, '') end @facts = YAML.dump(@facts) end newtest :getconfig, "Compiled catalog" do @client.driver.getconfig(@facts, "yaml") end # This test will always force a false answer. newtest :fresh, "Checked freshness" do @client.driver.freshness end end Suite.new :file, "File interactions" do def prepare unless $options[:file] fail "You must specify a file (using --file ) to interact with on the server" end @client = Puppet::Network::Client.file.new($args) unless @client.read_cert fail "Could not read client certificate" end end newtest :describe, "Described file" do @client.describe($options[:file], :ignore) end newtest :retrieve, "Retrieved file" do @client.retrieve($options[:file], :ignore) end end Suite.new :filebucket, "Filebucket interactions" do def prepare require 'tempfile' - @client = Puppet::Network::Client.dipper.new($args) + @client = Puppet::FileBucket::Dipper.new($args) end newtest :backup, "Backed up file" do Tempfile.open("bucket_testing") do |f| f.print rand(1024) f.close @client.backup(f.path) end end end # Note that this uses an env variable to determine how many resources per # host to create (with a default of 10). 'repeat' determines how # many hosts to create. You probably will get mad failures if you # use forking and sqlite. # Here's an example run of this test, using sqlite: # RESOURCE_COUNT=50 ext/puppet-test --suite rails --test storage --confdir /tmp/storagetesting --vardir /tmp/storagetesting --repeat 10 Suite.new :rails, "Rails Interactions" do def prepare Puppet::Rails.init @facts = Facter.to_hash if num = ENV["RESOURCECOUNT"] @resources = Integer(num) else @resources = 10 end end Resource = Puppet::Parser::Resource def execute(test, msg) begin send(test) rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "%s failed: %s" % [@name, detail.to_s] end end def mkresource(type, title, parameters) source = "fakesource" res = Resource.new(:type => type, :title => title, :source => source, :scope => "fakescope") parameters.each do |param, value| res.set(param, value, source) end res end def ref(type, title) Resource::Reference.new(:type => type, :title => title) end newtest :storage, "Stored resources" do hostname = "host%s" % @count @facts["hostname"] = hostname args = {:facts => @facts, :name => hostname} # Make all of the resources we want. Make them at least slightly complex, # so we model real resources as close as we can. resources = [] args[:resources] = resources @resources.times do |resnum| exec = mkresource("exec", "exec%s" % resnum, :command => "/bin/echo do something %s" % resnum) exec.tags = %w{exec one} << "exec%s" % resnum user = mkresource("user", "user%s" % resnum, :uid => resnum.to_s, :require => ref("exec", "exec%s" % resnum)) user.tags = %w{user one two} << "user%s" % resnum file = mkresource("file", "/tmp/file%s" % resnum, :owner => resnum.to_s, :require => [ref("exec", "exec%s" % resnum), ref("user", "user%s" % resnum)]) file.tags = %w{file one three} << "file%s" % resnum file.exported = true resources << exec << user << file end Puppet::Rails::Host.store(args) end end Suite.new :report, "Reports interactions" do def prepare Puppet::Transaction::Report.terminus_class = :rest end newtest :empty, "send empty report" do report = Puppet::Transaction::Report.new report.time = Time.now report.save end newtest :fake, "send fake report" do report = Puppet::Transaction::Report.new resourcemetrics = { :total => 12, :out_of_sync => 20, :applied => 45, :skipped => 1, :restarted => 23, :failed_restarts => 1, :scheduled => 10 } report.newmetric(:resources, resourcemetrics) timemetrics = { :resource1 => 10, :resource2 => 50, :resource3 => 40, :resource4 => 20, } report.newmetric(:times, timemetrics) report.newmetric(:changes, :total => 20 ) report.time = Time.now report.save end end $cmdargs = [ [ "--compile", "-c", GetoptLong::NO_ARGUMENT ], [ "--describe", GetoptLong::REQUIRED_ARGUMENT ], [ "--retrieve", "-R", GetoptLong::REQUIRED_ARGUMENT ], [ "--fork", GetoptLong::REQUIRED_ARGUMENT ], [ "--fqdn", "-F", GetoptLong::REQUIRED_ARGUMENT ], [ "--suite", "-s", GetoptLong::REQUIRED_ARGUMENT ], [ "--test", "-t", GetoptLong::REQUIRED_ARGUMENT ], [ "--pause", "-p", GetoptLong::NO_ARGUMENT ], [ "--repeat", "-r", GetoptLong::REQUIRED_ARGUMENT ], [ "--node", "-n", GetoptLong::REQUIRED_ARGUMENT ], [ "--debug", "-d", GetoptLong::NO_ARGUMENT ], [ "--help", "-h", GetoptLong::NO_ARGUMENT ], [ "--list", "-l", GetoptLong::NO_ARGUMENT ], [ "--verbose", "-v", GetoptLong::NO_ARGUMENT ], [ "--version", "-V", GetoptLong::NO_ARGUMENT ], ] # Add all of the config parameters as valid $options. Puppet.settings.addargs($cmdargs) Puppet::Util::Log.newdestination(:console) Puppet::Node.terminus_class = :plain Puppet::Node.cache_class = :yaml Puppet::Node::Facts.terminus_class = :facter Puppet::Node::Facts.cache_class = :yaml result = GetoptLong.new(*$cmdargs) $args = {} $options = {:repeat => 1, :fork => 0, :pause => false, :nodes => []} begin explicit_waitforcert = false result.each { |opt,arg| case opt # First check to see if the argument is a valid configuration parameter; # if so, set it. when "--compile" $options[:suite] = :configuration $options[:test] = :compile when "--retrieve" $options[:suite] = :file $options[:test] = :retrieve $options[:file] = arg when "--fork" begin $options[:fork] = Integer(arg) rescue => detail $stderr.puts "The argument to 'fork' must be an integer" exit(14) end when "--describe" $options[:suite] = :file $options[:test] = :describe $options[:file] = arg when "--fqdn" $options[:fqdn] = arg when "--repeat" $options[:repeat] = Integer(arg) when "--help" if Puppet.features.usage? RDoc::usage && exit else puts "No help available unless you have RDoc::usage installed" exit end when "--version" puts "%s" % Puppet.version exit when "--verbose" Puppet::Util::Log.level = :info Puppet::Util::Log.newdestination(:console) when "--debug" Puppet::Util::Log.level = :debug Puppet::Util::Log.newdestination(:console) when "--suite" $options[:suite] = arg.intern when "--test" $options[:test] = arg.intern when "--file" $options[:file] = arg when "--pause" $options[:pause] = true when "--node" $options[:nodes] << arg when "--list" Suite.suites.sort { |a,b| a.to_s <=> b.to_s }.each do |suite_name| suite = Suite[suite_name] tests = suite.tests.sort { |a,b| a.to_s <=> b.to_s }.join(", ") puts "%20s: %s" % [suite_name, tests] end exit(0) else Puppet.settings.handlearg(opt, arg) end } rescue GetoptLong::InvalidOption => detail $stderr.puts detail $stderr.puts "Try '#{$0} --help'" exit(1) end # Now parse the config Puppet.parse_config $options[:nodes] << Puppet.settings[:certname] if $options[:nodes].empty? $args[:Server] = Puppet[:server] unless $options[:test] $options[:suite] = :remote_catalog $options[:test] = :getconfig end unless $options[:test] raise "A suite was specified without a test" end $pids = [] Suite.run($options[:test]) if $options[:fork] > 0 Process.waitall end diff --git a/lib/puppet.rb b/lib/puppet.rb index 1fa51c3b4..0904f044a 100644 --- a/lib/puppet.rb +++ b/lib/puppet.rb @@ -1,164 +1,165 @@ # Try to load rubygems. Hey rubygems, I hate you. begin require 'rubygems' rescue LoadError end # see the bottom of the file for further inclusions require 'singleton' require 'facter' require 'puppet/error' require 'puppet/util' require 'puppet/util/autoload' require 'puppet/util/settings' require 'puppet/util/feature' require 'puppet/util/suidmanager' #------------------------------------------------------------ # the top-level module # # all this really does is dictate how the whole system behaves, through # preferences for things like debugging # # it's also a place to find top-level commands like 'debug' module Puppet PUPPETVERSION = '0.25.4' def Puppet.version return PUPPETVERSION end class << self include Puppet::Util attr_reader :features attr_writer :name end # the hash that determines how our system behaves @@settings = Puppet::Util::Settings.new # The services running in this process. @services ||= [] require 'puppet/util/logging' extend Puppet::Util::Logging # The feature collection @features = Puppet::Util::Feature.new('puppet/feature') # Load the base features. require 'puppet/feature/base' # Store a new default value. def self.setdefaults(section, hash) @@settings.setdefaults(section, hash) end # configuration parameter access and stuff def self.[](param) case param when :debug if Puppet::Util::Log.level == :debug return true else return false end else return @@settings[param] end end # configuration parameter access and stuff def self.[]=(param,value) @@settings[param] = value end def self.clear @@settings.clear end def self.debug=(value) if value Puppet::Util::Log.level=(:debug) else Puppet::Util::Log.level=(:notice) end end def self.settings @@settings end # Load all of the configuration parameters. require 'puppet/defaults' def self.genmanifest if Puppet[:genmanifest] puts Puppet.settings.to_manifest exit(0) end end # Parse the config file for this process. def self.parse_config Puppet.settings.parse end # XXX this should all be done using puppet objects, not using # normal mkdir def self.recmkdir(dir,mode = 0755) if FileTest.exist?(dir) return false else tmp = dir.sub(/^\//,'') path = [File::SEPARATOR] tmp.split(File::SEPARATOR).each { |dir| path.push dir if ! FileTest.exist?(File.join(path)) begin Dir.mkdir(File.join(path), mode) rescue Errno::EACCES => detail Puppet.err detail.to_s return false rescue => detail Puppet.err "Could not create %s: %s" % [path, detail.to_s] return false end elsif FileTest.directory?(File.join(path)) next else FileTest.exist?(File.join(path)) raise Puppet::Error, "Cannot create %s: basedir %s is a file" % [dir, File.join(path)] end } return true end end # Create a new type. Just proxy to the Type class. def self.newtype(name, options = {}, &block) Puppet::Type.newtype(name, options, &block) end # Retrieve a type by name. Just proxy to the Type class. def self.type(name) # LAK:DEP Deprecation notice added 12/17/2008 Puppet.warning "Puppet.type is deprecated; use Puppet::Type.type" Puppet::Type.type(name) end end require 'puppet/type' require 'puppet/parser' require 'puppet/network' require 'puppet/ssl' require 'puppet/module' require 'puppet/util/storage' require 'puppet/status' +require 'puppet/file_bucket/file' if Puppet[:storeconfigs] require 'puppet/rails' end diff --git a/lib/puppet/application/filebucket.rb b/lib/puppet/application/filebucket.rb index 0723054df..09aaf2b5d 100644 --- a/lib/puppet/application/filebucket.rb +++ b/lib/puppet/application/filebucket.rb @@ -1,87 +1,88 @@ require 'puppet' require 'puppet/application' -require 'puppet/network/client' +require 'puppet/file_bucket/dipper' Puppet::Application.new(:filebucket) do should_not_parse_config option("--bucket BUCKET","-b") option("--debug","-d") option("--local","-l") option("--remote","-r") option("--verbose","-v") dispatch do ARGV.shift end command(:get) do md5 = ARGV.shift out = @client.getfile(md5) print out end command(:backup) do ARGV.each do |file| unless FileTest.exists?(file) $stderr.puts "%s: no such file" % file next end unless FileTest.readable?(file) $stderr.puts "%s: cannot read file" % file next end md5 = @client.backup(file) puts "%s: %s" % [file, md5] end end command(:restore) do file = ARGV.shift md5 = ARGV.shift @client.restore(file, md5) end setup do Puppet::Log.newdestination(:console) @client = nil @server = nil trap(:INT) do $stderr.puts "Cancelling" exit(1) end if options[:debug] Puppet::Log.level = :debug elsif options[:verbose] Puppet::Log.level = :info end # Now parse the config Puppet.parse_config if Puppet.settings.print_configs? exit(Puppet.settings.print_configs ? 0 : 1) end begin if options[:local] or options[:bucket] path = options[:bucket] || Puppet[:bucketdir] - @client = Puppet::Network::Client.dipper.new(:Path => path) + @client = Puppet::FileBucket::Dipper.new(:Path => path) else require 'puppet/network/handler' - @client = Puppet::Network::Client.dipper.new(:Server => Puppet[:server]) + @client = Puppet::FileBucket::Dipper.new(:Server => Puppet[:server]) end rescue => detail $stderr.puts detail if Puppet[:trace] puts detail.backtrace end exit(1) end end -end \ No newline at end of file +end + diff --git a/lib/puppet/application/server.rb b/lib/puppet/application/server.rb index afdad54fb..7aeb6ad5d 100644 --- a/lib/puppet/application/server.rb +++ b/lib/puppet/application/server.rb @@ -1,168 +1,167 @@ require 'puppet' require 'puppet/application' require 'puppet/daemon' require 'puppet/network/server' require 'puppet/network/http/rack' if Puppet.features.rack? Puppet::Application.new(:server) do should_parse_config option("--debug", "-d") option("--verbose", "-v") # internal option, only to be used by ext/rack/config.ru option("--rack") option("--compile host", "-c host") do |arg| options[:node] = arg end option("--logdest DEST", "-l DEST") do |arg| begin Puppet::Util::Log.newdestination(arg) options[:setdest] = true rescue => detail if Puppet[:debug] puts detail.backtrace end $stderr.puts detail.to_s end end preinit do trap(:INT) do $stderr.puts "Cancelling startup" exit(0) end # Create this first-off, so we have ARGV @daemon = Puppet::Daemon.new @daemon.argv = ARGV.dup end dispatch do if options[:node] :compile elsif Puppet[:parseonly] :parseonly else :main end end command(:compile) do Puppet::Util::Log.newdestination :console raise ArgumentError, "Cannot render compiled catalogs without pson support" unless Puppet.features.pson? begin unless catalog = Puppet::Resource::Catalog.find(options[:node]) raise "Could not compile catalog for %s" % options[:node] end $stdout.puts catalog.render(:pson) rescue => detail $stderr.puts detail exit(30) end exit(0) end command(:parseonly) do begin Puppet::Resource::TypeCollection.new(Puppet[:environment]).perform_initial_import rescue => detail Puppet.err detail exit 1 end exit(0) end command(:main) do require 'etc' require 'puppet/file_serving/content' require 'puppet/file_serving/metadata' - require 'puppet/checksum' xmlrpc_handlers = [:Status, :FileServer, :Master, :Report, :Filebucket] if Puppet[:ca] xmlrpc_handlers << :CA end # Make sure we've got a localhost ssl cert Puppet::SSL::Host.localhost # And now configure our server to *only* hit the CA for data, because that's # all it will have write access to. if Puppet::SSL::CertificateAuthority.ca? Puppet::SSL::Host.ca_location = :only end if Process.uid == 0 begin Puppet::Util.chuser rescue => detail puts detail.backtrace if Puppet[:trace] $stderr.puts "Could not change user to %s: %s" % [Puppet[:user], detail] exit(39) end end unless options[:rack] @daemon.server = Puppet::Network::Server.new(:xmlrpc_handlers => xmlrpc_handlers) @daemon.daemonize if Puppet[:daemonize] else require 'puppet/network/http/rack' @app = Puppet::Network::HTTP::Rack.new(:xmlrpc_handlers => xmlrpc_handlers, :protocols => [:rest, :xmlrpc]) end Puppet.notice "Starting Puppet server version %s" % [Puppet.version] unless options[:rack] @daemon.start else return @app end end setup do # Handle the logging settings. if options[:debug] or options[:verbose] if options[:debug] Puppet::Util::Log.level = :debug else Puppet::Util::Log.level = :info end unless Puppet[:daemonize] or options[:rack] Puppet::Util::Log.newdestination(:console) options[:setdest] = true end end unless options[:setdest] Puppet::Util::Log.newdestination(:syslog) end if Puppet.settings.print_configs? exit(Puppet.settings.print_configs ? 0 : 1) end Puppet.settings.use :main, :puppetmasterd, :ssl # A temporary solution, to at least make the master work for now. Puppet::Node::Facts.terminus_class = :yaml # Cache our nodes in yaml. Currently not configurable. Puppet::Node.cache_class = :yaml # Configure all of the SSL stuff. if Puppet::SSL::CertificateAuthority.ca? Puppet::SSL::Host.ca_location = :local Puppet.settings.use :ca Puppet::SSL::CertificateAuthority.instance else Puppet::SSL::Host.ca_location = :none end end end diff --git a/lib/puppet/checksum.rb b/lib/puppet/checksum.rb deleted file mode 100644 index 27f08aa99..000000000 --- a/lib/puppet/checksum.rb +++ /dev/null @@ -1,57 +0,0 @@ -# -# Created by Luke Kanies on 2007-9-22. -# Copyright (c) 2007. All rights reserved. - -require 'puppet' -require 'puppet/util/checksums' -require 'puppet/indirector' - -# A checksum class to model translating checksums to file paths. This -# is the new filebucket. -class Puppet::Checksum - include Puppet::Util::Checksums - - extend Puppet::Indirector - - indirects :checksum - - attr_reader :algorithm, :content - - def algorithm=(value) - unless respond_to?(value) - raise ArgumentError, "Checksum algorithm %s is not supported" % value - end - value = value.intern if value.is_a?(String) - @algorithm = value - # Reset the checksum so it's forced to be recalculated. - @checksum = nil - end - - # Calculate (if necessary) and return the checksum - def checksum - unless @checksum - @checksum = send(algorithm, content) - end - @checksum - end - - def initialize(content, algorithm = "md5") - raise ArgumentError.new("You must specify the content") unless content - - @content = content - - # Init to avoid warnings. - @checksum = nil - - self.algorithm = algorithm - end - - # This is here so the Indirector::File terminus works correctly. - def name - checksum - end - - def to_s - "Checksum<{%s}%s>" % [algorithm, checksum] - end -end diff --git a/lib/puppet/file_bucket.rb b/lib/puppet/file_bucket.rb new file mode 100644 index 000000000..881c81e58 --- /dev/null +++ b/lib/puppet/file_bucket.rb @@ -0,0 +1,4 @@ +# stub +module Puppet::FileBucket + class BucketError < RuntimeError; end +end diff --git a/lib/puppet/network/client/dipper.rb b/lib/puppet/file_bucket/dipper.rb similarity index 59% rename from lib/puppet/network/client/dipper.rb rename to lib/puppet/file_bucket/dipper.rb index 0e2dc1425..c73d76345 100644 --- a/lib/puppet/network/client/dipper.rb +++ b/lib/puppet/file_bucket/dipper.rb @@ -1,85 +1,97 @@ -# The client class for filebuckets. -class Puppet::Network::Client::Dipper < Puppet::Network::Client - @handler = Puppet::Network::Handler.handler(:filebucket) - @drivername = :Bucket +require 'puppet/file_bucket' +require 'puppet/file_bucket/file' +require 'puppet/indirector/request' + +class Puppet::FileBucket::Dipper + # This is a transitional implementation that uses REST + # to access remote filebucket files. attr_accessor :name # Create our bucket client def initialize(hash = {}) + # Emulate the XMLRPC client + server = hash[:Server] + port = hash[:Port] || Puppet[:masterport] + environment = Puppet[:environment] + if hash.include?(:Path) - bucket = self.class.handler.new(:Path => hash[:Path]) - hash.delete(:Path) - hash[:Bucket] = bucket + @local_path = hash[:Path] + @rest_path = nil + else + @local_path = nil + @rest_path = "https://#{server}:#{port}/#{environment}/file_bucket_file/" end + end - super(hash) + def local? + !! @local_path end # Back up a file to our bucket def backup(file) - unless FileTest.exists?(file) + unless ::File.exist?(file) raise(ArgumentError, "File %s does not exist" % file) end contents = ::File.read(file) - unless local? - contents = Base64.encode64(contents) - end begin - return @driver.addfile(contents,file) + file_bucket_file = Puppet::FileBucket::File.new(contents, :bucket_path => @local_path, :path => file) + dest_path = "#{@rest_path}#{file_bucket_file.name}" + + request = Puppet::Indirector::Request.new(:file_bucket_file, :save, dest_path) + + file_bucket_file.save(request) + return file_bucket_file.checksum_data rescue => detail puts detail.backtrace if Puppet[:trace] raise Puppet::Error, "Could not back up %s: %s" % [file, detail] end end # Retrieve a file by sum. def getfile(sum) - if newcontents = @driver.getfile(sum) - unless local? - newcontents = Base64.decode64(newcontents) - end - return newcontents - end - return nil + source_path = "#{@rest_path}md5/#{sum}" + file_bucket_file = Puppet::FileBucket::File.find(source_path) + + return file_bucket_file.to_s end # Restore the file def restore(file,sum) restore = true if FileTest.exists?(file) cursum = Digest::MD5.hexdigest(::File.read(file)) # if the checksum has changed... # this might be extra effort if cursum == sum restore = false end end if restore if newcontents = getfile(sum) tmp = "" newsum = Digest::MD5.hexdigest(newcontents) changed = nil if FileTest.exists?(file) and ! FileTest.writable?(file) changed = ::File.stat(file).mode ::File.chmod(changed | 0200, file) end ::File.open(file, ::File::WRONLY|::File::TRUNC|::File::CREAT) { |of| of.print(newcontents) } if changed ::File.chmod(changed, file) end else Puppet.err "Could not find file with checksum %s" % sum return nil end return newsum else return nil end end end diff --git a/lib/puppet/file_bucket/file.rb b/lib/puppet/file_bucket/file.rb new file mode 100644 index 000000000..7122bfbbb --- /dev/null +++ b/lib/puppet/file_bucket/file.rb @@ -0,0 +1,123 @@ +require 'puppet/file_bucket' +require 'puppet/indirector' + +class Puppet::FileBucket::File + # This class handles the abstract notion of a file in a filebucket. + # There are mechanisms to save and load this file locally and remotely in puppet/indirector/filebucketfile/* + # There is a compatibility class that emulates pre-indirector filebuckets in Puppet::FileBucket::Dipper + extend Puppet::Indirector + require 'puppet/file_bucket/file/indirection_hooks' + indirects :file_bucket_file, :terminus_class => :file, :extend => Puppet::FileBucket::File::IndirectionHooks + + attr :path, true + attr :paths, true + attr :contents, true + attr :checksum_type + attr :bucket_path + + def self.default_checksum_type + :md5 + end + + def initialize( contents, options = {} ) + @contents = contents + @bucket_path = options[:bucket_path] + @path = options[:path] + @paths = options[:paths] || [] + @checksum = options[:checksum] + @checksum_type = options[:checksum_type] || self.class.default_checksum_type + + yield(self) if block_given? + + validate! + end + + def validate! + digest_class( @checksum_type ) # raises error on bad types + raise ArgumentError, 'contents must be a string' unless @contents.is_a?(String) + validate_checksum(@checksum) if @checksum + end + + def contents=(contents) + raise "You may not change the contents of a FileBucket File" if @contents + @contents = contents + end + + def checksum=(checksum) + validate_checksum(checksum) + self.checksum_type = checksum # this grabs the prefix only + @checksum = checksum + end + + def validate_checksum(new_checksum) + unless new_checksum == checksum_of_type(new_checksum) + raise Puppet::Error, "checksum does not match contents" + end + end + + def checksum + @checksum ||= checksum_of_type(checksum_type) + end + + def checksum_of_type( type ) + type = checksum_type( type ) # strip out data segment if there is one + type.to_s + ":" + digest_class(type).hexdigest(@contents) + end + + def checksum_type=( new_checksum_type ) + @checksum = nil + @checksum_type = checksum_type(new_checksum_type) + end + + def checksum_type(checksum = @checksum_type) + checksum.to_s.split(':',2)[0].to_sym + end + + def checksum_data(new_checksum = self.checksum) + new_checksum.split(':',2)[1] + end + + def checksum_data=(new_data) + self.checksum = "#{checksum_type}:#{new_data}" + end + + def digest_class(type = nil) + case checksum_type(type) + when :md5 : require 'digest/md5' ; Digest::MD5 + when :sha1 : require 'digest/sha1' ; Digest::SHA1 + else + raise ArgumentError, "not a known checksum type: #{checksum_type(type)}" + end + end + + def to_s + contents + end + + def name + [checksum_type, checksum_data, path].compact.join('/') + end + + def name=(name) + self.checksum_type, self.checksum_data, self.path = name.split('/',3) + end + + def conflict_check? + true + end + + def self.from_s( contents ) + self.new( contents ) + end + + def to_pson + hash = { "contents" => contents } + hash["path"] = @path if @path + hash.to_pson + end + + def self.from_pson( pson ) + self.new( pson["contents"], :path => pson["path"] ) + end + +end diff --git a/lib/puppet/file_bucket/file/indirection_hooks.rb b/lib/puppet/file_bucket/file/indirection_hooks.rb new file mode 100644 index 000000000..ec2bb3469 --- /dev/null +++ b/lib/puppet/file_bucket/file/indirection_hooks.rb @@ -0,0 +1,10 @@ +require 'puppet/file_bucket/file' + +# This module is used to pick the appropriate terminus +# in filebucket indirections. +module Puppet::FileBucket::File::IndirectionHooks + def select_terminus(request) + return :rest if request.protocol == 'https' + return Puppet::FileBucket::File.indirection.terminus_class + end +end diff --git a/lib/puppet/indirector/file_bucket_file/file.rb b/lib/puppet/indirector/file_bucket_file/file.rb new file mode 100644 index 000000000..ec02ca0bd --- /dev/null +++ b/lib/puppet/indirector/file_bucket_file/file.rb @@ -0,0 +1,142 @@ +require 'puppet/indirector/code' +require 'puppet/file_bucket/file' + +module Puppet::FileBucketFile + class File < Puppet::Indirector::Code + desc "Store files in a directory set based on their checksums." + + def initialize + Puppet.settings.use(:filebucket) + end + + def find( request ) + checksum, path = request_to_checksum_and_path( request ) + return find_by_checksum( checksum ) + end + + def save( request ) + checksum, path = request_to_checksum_and_path( request ) + + instance = request.instance + instance.checksum = checksum if checksum + instance.path = path if path + + save_to_disk(instance) + instance.to_s + end + + private + + def find_by_checksum( checksum ) + model.new( nil, :checksum => checksum ) do |bucket_file| + filename = contents_path_for bucket_file + + if ! ::File.exist? filename + return nil + end + + begin + contents = ::File.read filename + Puppet.info "FileBucket read #{bucket_file.checksum}" + rescue RuntimeError => e + raise Puppet::Error, "file could not be read: #{e.message}" + end + + if ::File.exist?(paths_path_for bucket_file) + ::File.open(paths_path_for bucket_file) do |f| + bucket_file.paths = f.readlines.map { |l| l.chomp } + end + end + + bucket_file.contents = contents + end + end + + def save_to_disk( bucket_file ) + # If the file already exists, just return the md5 sum. + if ::File.exist?(contents_path_for bucket_file) + verify_identical_file!(bucket_file) + else + # Make the directories if necessary. + unless ::File.directory?(path_for bucket_file) + Puppet::Util.withumask(0007) do + ::FileUtils.mkdir_p(path_for bucket_file) + end + end + + Puppet.info "FileBucket adding #{bucket_file.path} (#{bucket_file.checksum_data})" + + # Write the file to disk. + Puppet::Util.withumask(0007) do + ::File.open(contents_path_for(bucket_file), ::File::WRONLY|::File::CREAT, 0440) do |of| + of.print bucket_file.contents + end + end + end + + save_path_to_paths_file(bucket_file) + return bucket_file.checksum_data + end + + def request_to_checksum_and_path( request ) + checksum_type, checksum, path = request.key.split(/[:\/]/, 3) + return nil if checksum_type.to_s == "" + return [ checksum_type + ":" + checksum, path ] + end + + def path_for(bucket_file, subfile = nil) + bucket_path = bucket_file.bucket_path || Puppet[:bucketdir] + digest = bucket_file.checksum_data + + dir = ::File.join(digest[0..7].split("")) + basedir = ::File.join(bucket_path, dir, digest) + + return basedir unless subfile + return ::File.join(basedir, subfile) + end + + def contents_path_for(bucket_file) + path_for(bucket_file, "contents") + end + + def paths_path_for(bucket_file) + path_for(bucket_file, "paths") + end + + def content_check? + true + end + + # If conflict_check is enabled, verify that the passed text is + # the same as the text in our file. + def verify_identical_file!(bucket_file) + return unless content_check? + disk_contents = ::File.read(contents_path_for(bucket_file)) + + # If the contents don't match, then we've found a conflict. + # Unlikely, but quite bad. + if disk_contents != bucket_file.contents + raise Puppet::FileBucket::BucketError, "Got passed new contents for sum #{bucket_file.checksum}", caller + else + Puppet.info "FileBucket got a duplicate file #{bucket_file.path} (#{bucket_file.checksum})" + end + end + + def save_path_to_paths_file(bucket_file) + return unless bucket_file.path + + # check for dupes + if ::File.exist?(paths_path_for bucket_file) + ::File.open(paths_path_for bucket_file) do |f| + return if f.readlines.collect { |l| l.chomp }.include?(bucket_file.path) + end + end + + # if it's a new file, or if our path isn't in the file yet, add it + ::File.open(paths_path_for(bucket_file), ::File::WRONLY|::File::CREAT|::File::APPEND) do |of| + of.puts bucket_file.path + end + end + + end +end diff --git a/lib/puppet/indirector/file_bucket_file/rest.rb b/lib/puppet/indirector/file_bucket_file/rest.rb new file mode 100644 index 000000000..15e4f331d --- /dev/null +++ b/lib/puppet/indirector/file_bucket_file/rest.rb @@ -0,0 +1,8 @@ +require 'puppet/indirector/rest' +require 'puppet/file_bucket/file' + +module Puppet::FileBucketFile + class Rest < Puppet::Indirector::REST + desc "This is a REST based mechanism to send/retrieve file to/from the filebucket" + end +end diff --git a/lib/puppet/indirector/indirection.rb b/lib/puppet/indirector/indirection.rb index d762701f5..3c6414624 100644 --- a/lib/puppet/indirector/indirection.rb +++ b/lib/puppet/indirector/indirection.rb @@ -1,312 +1,318 @@ require 'puppet/util/docs' require 'puppet/indirector/envelope' require 'puppet/indirector/request' require 'puppet/util/cacher' # The class that connects functional classes with their different collection # back-ends. Each indirection has a set of associated terminus classes, # each of which is a subclass of Puppet::Indirector::Terminus. class Puppet::Indirector::Indirection include Puppet::Util::Cacher include Puppet::Util::Docs @@indirections = [] # Find an indirection by name. This is provided so that Terminus classes # can specifically hook up with the indirections they are associated with. def self.instance(name) @@indirections.find { |i| i.name == name } end # Return a list of all known indirections. Used to generate the # reference. def self.instances @@indirections.collect { |i| i.name } end # Find an indirected model by name. This is provided so that Terminus classes # can specifically hook up with the indirections they are associated with. def self.model(name) return nil unless match = @@indirections.find { |i| i.name == name } match.model end attr_accessor :name, :model # Create and return our cache terminus. def cache raise(Puppet::DevError, "Tried to cache when no cache class was set") unless cache_class terminus(cache_class) end # Should we use a cache? def cache? cache_class ? true : false end attr_reader :cache_class # Define a terminus class to be used for caching. def cache_class=(class_name) validate_terminus_class(class_name) if class_name @cache_class = class_name end # This is only used for testing. def delete @@indirections.delete(self) if @@indirections.include?(self) end # Set the time-to-live for instances created through this indirection. def ttl=(value) raise ArgumentError, "Indirection TTL must be an integer" unless value.is_a?(Fixnum) @ttl = value end # Default to the runinterval for the ttl. def ttl unless defined?(@ttl) @ttl = Puppet[:runinterval].to_i end @ttl end # Calculate the expiration date for a returned instance. def expiration Time.now + ttl end # Generate the full doc string. def doc text = "" if defined? @doc and @doc text += scrub(@doc) + "\n\n" end if s = terminus_setting() text += "* **Terminus Setting**: %s" % terminus_setting end text end def initialize(model, name, options = {}) @model = model @name = name @cache_class = nil @terminus_class = nil raise(ArgumentError, "Indirection %s is already defined" % @name) if @@indirections.find { |i| i.name == @name } @@indirections << self if mod = options[:extend] extend(mod) options.delete(:extend) end # This is currently only used for cache_class and terminus_class. options.each do |name, value| begin send(name.to_s + "=", value) rescue NoMethodError raise ArgumentError, "%s is not a valid Indirection parameter" % name end end end # Set up our request object. - def request(method, key, arguments = nil) - Puppet::Indirector::Request.new(self.name, method, key, arguments) + def request(method, instance_or_key, request_or_options = {}) + if request_or_options.is_a? Puppet::Indirector::Request + request = request_or_options + request.instance = instance_or_key + request.method = method + return request + end + Puppet::Indirector::Request.new(self.name, method, instance_or_key, request_or_options) end # Return the singleton terminus for this indirection. def terminus(terminus_name = nil) # Get the name of the terminus. unless terminus_name ||= terminus_class raise Puppet::DevError, "No terminus specified for %s; cannot redirect" % self.name end return termini[terminus_name] ||= make_terminus(terminus_name) end # This can be used to select the terminus class. attr_accessor :terminus_setting # Determine the terminus class. def terminus_class unless @terminus_class if setting = self.terminus_setting self.terminus_class = Puppet.settings[setting].to_sym else raise Puppet::DevError, "No terminus class nor terminus setting was provided for indirection %s" % self.name end end @terminus_class end # Specify the terminus class to use. def terminus_class=(klass) validate_terminus_class(klass) @terminus_class = klass end # This is used by terminus_class= and cache=. def validate_terminus_class(terminus_class) unless terminus_class and terminus_class.to_s != "" raise ArgumentError, "Invalid terminus name %s" % terminus_class.inspect end unless Puppet::Indirector::Terminus.terminus_class(self.name, terminus_class) raise ArgumentError, "Could not find terminus %s for indirection %s" % [terminus_class, self.name] end end # Expire a cached object, if one is cached. Note that we now actually # remove it if possible, and only mark it as expired if destroy isn't # supported. def expire(key, *args) if cache? and instance = cache.find(request(:find, key, *args)) Puppet.info "Expiring the #{name} cache of #{instance.name}" if cache.respond_to? :destroy cache.destroy(request(:destroy, instance, *args)) else instance.expiration = Time.now - 1 cache.save(request(:save,instance,*args)) end end end # Search for an instance in the appropriate terminus, caching the # results if caching is configured.. def find(key, *args) request = request(:find, key, *args) terminus = prepare(request) begin if result = find_in_cache(request) return result end rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Cached %s for %s failed: %s" % [self.name, request.key, detail] end # Otherwise, return the result from the terminus, caching if appropriate. if ! request.ignore_terminus? and result = terminus.find(request) result.expiration ||= self.expiration if cache? and request.use_cache? Puppet.info "Caching %s for %s" % [self.name, request.key] cache.save request(:save, result, *args) end return terminus.respond_to?(:filter) ? terminus.filter(result) : result end return nil end def find_in_cache(request) # See if our instance is in the cache and up to date. return nil unless cache? and ! request.ignore_cache? and cached = cache.find(request) if cached.expired? Puppet.info "Not using expired %s for %s from cache; expired at %s" % [self.name, request.key, cached.expiration] return nil end Puppet.debug "Using cached #{name} for #{request.key}, good until #{cached.expiration}" return cached end # Remove something via the terminus. def destroy(key, *args) request = request(:destroy, key, *args) terminus = prepare(request) result = terminus.destroy(request) if cache? and cached = cache.find(request(:find, key, *args)) # Reuse the existing request, since it's equivalent. cache.destroy(request) end result end # Search for more than one instance. Should always return an array. def search(key, *args) request = request(:search, key, *args) terminus = prepare(request) if result = terminus.search(request) raise Puppet::DevError, "Search results from terminus %s are not an array" % terminus.name unless result.is_a?(Array) result.each do |instance| instance.expiration ||= self.expiration end return result end end # Save the instance in the appropriate terminus. This method is # normally an instance method on the indirected class. - def save(instance, *args) - request = request(:save, instance, *args) + def save(instance, request_or_options = nil) + request = request(:save, instance, request_or_options) terminus = prepare(request) result = terminus.save(request) # If caching is enabled, save our document there cache.save(request) if cache? result end private # Check authorization if there's a hook available; fail if there is one # and it returns false. def check_authorization(request, terminus) # At this point, we're assuming authorization makes no sense without # client information. return unless request.node # This is only to authorize via a terminus-specific authorization hook. return unless terminus.respond_to?(:authorized?) unless terminus.authorized?(request) msg = "Not authorized to call %s on %s" % [request.method, request.to_s] unless request.options.empty? msg += " with %s" % request.options.inspect end raise ArgumentError, msg end end # Setup a request, pick the appropriate terminus, check the request's authorization, and return it. def prepare(request) # Pick our terminus. if respond_to?(:select_terminus) unless terminus_name = select_terminus(request) raise ArgumentError, "Could not determine appropriate terminus for %s" % request end else terminus_name = terminus_class end dest_terminus = terminus(terminus_name) check_authorization(request, dest_terminus) return dest_terminus end # Create a new terminus instance. def make_terminus(terminus_class) # Load our terminus class. unless klass = Puppet::Indirector::Terminus.terminus_class(self.name, terminus_class) raise ArgumentError, "Could not find terminus %s for indirection %s" % [terminus_class, self.name] end return klass.new end # Cache our terminus instances indefinitely, but make it easy to clean them up. cached_attr(:termini) { Hash.new } end diff --git a/lib/puppet/indirector/request.rb b/lib/puppet/indirector/request.rb index d9e66cb5b..14608d0dc 100644 --- a/lib/puppet/indirector/request.rb +++ b/lib/puppet/indirector/request.rb @@ -1,185 +1,193 @@ require 'cgi' require 'uri' require 'puppet/indirector' # This class encapsulates all of the information you need to make an # Indirection call, and as a a result also handles REST calls. It's somewhat # analogous to an HTTP Request object, except tuned for our Indirector. class Puppet::Indirector::Request attr_accessor :key, :method, :options, :instance, :node, :ip, :authenticated, :ignore_cache, :ignore_terminus attr_accessor :server, :port, :uri, :protocol attr_reader :indirection_name OPTION_ATTRIBUTES = [:ip, :node, :authenticated, :ignore_terminus, :ignore_cache, :instance, :environment] # Is this an authenticated request? def authenticated? # Double negative, so we just get true or false ! ! authenticated end def environment unless defined?(@environment) and @environment @environment = Puppet::Node::Environment.new() end @environment end def environment=(env) @environment = if env.is_a?(Puppet::Node::Environment) env else Puppet::Node::Environment.new(env) end end def escaped_key URI.escape(key) end # LAK:NOTE This is a messy interface to the cache, and it's only # used by the Configurer class. I decided it was better to implement # it now and refactor later, when we have a better design, than # to spend another month coming up with a design now that might # not be any better. def ignore_cache? ignore_cache end def ignore_terminus? ignore_terminus end def initialize(indirection_name, method, key, options = {}) options ||= {} raise ArgumentError, "Request options must be a hash, not %s" % options.class unless options.is_a?(Hash) self.indirection_name = indirection_name self.method = method set_attributes(options) @options = options.inject({}) { |hash, ary| hash[ary[0].to_sym] = ary[1]; hash } if key.is_a?(String) or key.is_a?(Symbol) # If the request key is a URI, then we need to treat it specially, # because it rewrites the key. We could otherwise strip server/port/etc # info out in the REST class, but it seemed bad design for the REST # class to rewrite the key. if key.to_s =~ /^\w+:\/\// # it's a URI set_uri_key(key) else @key = key end else @instance = key @key = @instance.name end end # Look up the indirection based on the name provided. def indirection Puppet::Indirector::Indirection.instance(indirection_name) end def indirection_name=(name) @indirection_name = name.to_sym end def model raise ArgumentError, "Could not find indirection '%s'" % indirection_name unless i = indirection i.model end # Should we allow use of the cached object? def use_cache? if defined?(@use_cache) ! ! use_cache else true end end # Are we trying to interact with multiple resources, or just one? def plural? method == :search end # Create the query string, if options are present. def query_string return "" unless options and ! options.empty? "?" + options.collect do |key, value| case value when nil; next when true, false; value = value.to_s when Fixnum, Bignum, Float; value = value # nothing when String; value = CGI.escape(value) when Symbol; value = CGI.escape(value.to_s) when Array; value = CGI.escape(YAML.dump(value)) else raise ArgumentError, "HTTP REST queries cannot handle values of type '%s'" % value.class end "%s=%s" % [key, value] end.join("&") end def to_hash result = options.dup OPTION_ATTRIBUTES.each do |attribute| if value = send(attribute) result[attribute] = value end end result end def to_s return uri if uri return "/%s/%s" % [indirection_name, key] end private def set_attributes(options) OPTION_ATTRIBUTES.each do |attribute| if options.include?(attribute) send(attribute.to_s + "=", options[attribute]) options.delete(attribute) end end end # Parse the key as a URI, setting attributes appropriately. def set_uri_key(key) @uri = key begin uri = URI.parse(URI.escape(key)) rescue => detail raise ArgumentError, "Could not understand URL %s: %s" % [key, detail.to_s] end # Just short-circuit these to full paths if uri.scheme == "file" @key = URI.unescape(uri.path) return end @server = uri.host if uri.host # If the URI class can look up the scheme, it will provide a port, # otherwise it will default to '0'. if uri.port.to_i == 0 and uri.scheme == "puppet" @port = Puppet.settings[:masterport].to_i else @port = uri.port.to_i end @protocol = uri.scheme - @key = URI.unescape(uri.path.sub(/^\//, '')) + + if uri.scheme == 'puppet' + @key = URI.unescape(uri.path.sub(/^\//, '')) + return + end + + env, indirector, @key = URI.unescape(uri.path.sub(/^\//, '')).split('/',3) + @key ||= '' + self.environment = env unless env == '' end end diff --git a/lib/puppet/network/handler/filebucket.rb b/lib/puppet/network/handler/filebucket.rb index 4973886f7..bea1c85f5 100755 --- a/lib/puppet/network/handler/filebucket.rb +++ b/lib/puppet/network/handler/filebucket.rb @@ -1,182 +1,53 @@ require 'fileutils' require 'digest/md5' require 'puppet/external/base64' class Puppet::Network::Handler # :nodoc: - class BucketError < RuntimeError; end # Accept files and store them by md5 sum, returning the md5 sum back # to the client. Alternatively, accept an md5 sum and return the # associated content. class FileBucket < Handler desc "The interface to Puppet's FileBucket system. Can be used to store files in and retrieve files from a filebucket." @interface = XMLRPC::Service::Interface.new("puppetbucket") { |iface| iface.add_method("string addfile(string, string)") iface.add_method("string getfile(string)") } Puppet::Util.logmethods(self, true) attr_reader :name, :path - # this doesn't work for relative paths - def self.oldpaths(base,md5) - return [ - File.join(base, md5), - File.join(base, md5, "contents"), - File.join(base, md5, "paths") - ] - end - - # this doesn't work for relative paths - def self.paths(base,md5) - dir = File.join(md5[0..7].split("")) - basedir = File.join(base, dir, md5) - return [ - basedir, - File.join(basedir, "contents"), - File.join(basedir, "paths") - ] - end - - # Should we check each file as it comes in to make sure the md5 - # sums match? Defaults to false. - def conflict_check? - @confictchk - end - def initialize(hash) - if hash.include?(:ConflictCheck) - @conflictchk = hash[:ConflictCheck] - hash.delete(:ConflictCheck) - else - @conflictchk = false - end - - if hash.include?(:Path) - @path = hash[:Path] - hash.delete(:Path) - else - if defined? Puppet - @path = Puppet[:bucketdir] - else - @path = File.expand_path("~/.filebucket") - end - end - - Puppet.settings.use(:filebucket) - + @path = hash[:Path] || Puppet[:bucketdir] @name = "Filebucket[#{@path}]" end # Accept a file from a client and store it by md5 sum, returning # the sum. def addfile(contents, path, client = nil, clientip = nil) if client contents = Base64.decode64(contents) end - md5 = Digest::MD5.hexdigest(contents) - - bpath, bfile, pathpath = FileBucket.paths(@path,md5) - - # If the file already exists, just return the md5 sum. - if FileTest.exists?(bfile) - # If verification is enabled, then make sure the text matches. - if conflict_check? - verify(contents, md5, bfile) - end - return md5 - end - - # Make the directories if necessary. - unless FileTest.directory?(bpath) - Puppet::Util.withumask(0007) do - FileUtils.mkdir_p(bpath) - end - end - - # Write the file to disk. - msg = "Adding %s(%s)" % [path, md5] - msg += " from #{client}" if client - self.info msg - - # ...then just create the file - Puppet::Util.withumask(0007) do - File.open(bfile, File::WRONLY|File::CREAT, 0440) { |of| - of.print contents - } - end - - # Write the path to the paths file. - add_path(path, pathpath) - - return md5 + bucket = Puppet::FileBucket::File.new(contents) + return bucket.save end # Return the contents associated with a given md5 sum. def getfile(md5, client = nil, clientip = nil) - bpath, bfile, bpaths = FileBucket.paths(@path,md5) - - unless FileTest.exists?(bfile) - # Try the old flat style. - bpath, bfile, bpaths = FileBucket.oldpaths(@path,md5) - unless FileTest.exists?(bfile) - return false - end - end - - contents = nil - File.open(bfile) { |of| - contents = of.read - } + bucket = Puppet::FileBucket::File.find("md5:#{md5}") + contents = bucket.contents if client return Base64.encode64(contents) else return contents end end - def paths(md5) - self.class(@path, md5) - end - def to_s self.name end - - private - - # Add our path to the paths file if necessary. - def add_path(path, file) - if FileTest.exists?(file) - File.open(file) { |of| - return if of.readlines.collect { |l| l.chomp }.include?(path) - } - end - - # if it's a new file, or if our path isn't in the file yet, add it - File.open(file, File::WRONLY|File::CREAT|File::APPEND) { |of| - of.puts path - } - end - - # If conflict_check is enabled, verify that the passed text is - # the same as the text in our file. - def verify(content, md5, bfile) - curfile = File.read(bfile) - - # 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) - else - msg = "Got duplicate %s(%s)" % [path, md5] - msg += " from #{client}" if client - self.info msg - end - end end end diff --git a/lib/puppet/network/http/handler.rb b/lib/puppet/network/http/handler.rb index 444fbf7e7..01ca65023 100644 --- a/lib/puppet/network/http/handler.rb +++ b/lib/puppet/network/http/handler.rb @@ -1,229 +1,229 @@ module Puppet::Network::HTTP end require 'puppet/network/http/api/v1' require 'puppet/network/rest_authorization' require 'puppet/network/rights' require 'resolv' module Puppet::Network::HTTP::Handler include Puppet::Network::HTTP::API::V1 include Puppet::Network::RestAuthorization attr_reader :server, :handler # Retrieve the accept header from the http request. def accept_header(request) raise NotImplementedError end # Retrieve the Content-Type header from the http request. def content_type_header(request) raise NotImplementedError end # Which format to use when serializing our response or interpreting the request. # IF the client provided a Content-Type use this, otherwise use the Accept header # and just pick the first value. def format_to_use(request) unless header = accept_header(request) raise ArgumentError, "An Accept header must be provided to pick the right format" end format = nil header.split(/,\s*/).each do |name| next unless format = Puppet::Network::FormatHandler.format(name) next unless format.suitable? return format end raise "No specified acceptable formats (%s) are functional on this machine" % header end def request_format(request) if header = content_type_header(request) header.gsub!(/\s*;.*$/,'') # strip any charset format = Puppet::Network::FormatHandler.mime(header) raise "Client sent a mime-type (%s) that doesn't correspond to a format we support" % header if format.nil? return format.name.to_s if format.suitable? end raise "No Content-Type header was received, it isn't possible to unserialize the request" end def format_to_mime(format) format.is_a?(Puppet::Network::Format) ? format.mime : format end def initialize_for_puppet(server) @server = server end # handle an HTTP request def process(request, response) indirection_request = uri2indirection(http_method(request), path(request), params(request)) check_authorization(indirection_request) send("do_%s" % indirection_request.method, indirection_request, request, response) rescue SystemExit,NoMemoryError raise rescue Exception => e return do_exception(response, e) end # Set the response up, with the body and status. def set_response(response, body, status = 200) raise NotImplementedError end # Set the specified format as the content type of the response. def set_content_type(response, format) raise NotImplementedError end def do_exception(response, exception, status=400) if exception.is_a?(Puppet::Network::AuthorizationError) # make sure we return the correct status code # for authorization issues status = 403 if status == 400 end if exception.is_a?(Exception) puts exception.backtrace if Puppet[:trace] Puppet.err(exception) end set_content_type(response, "text/plain") set_response(response, exception.to_s, status) end # Execute our find. def do_find(indirection_request, request, response) unless result = indirection_request.model.find(indirection_request.key, indirection_request.to_hash) Puppet.info("Could not find %s for '%s'" % [indirection_request.indirection_name, indirection_request.key]) return do_exception(response, "Could not find %s %s" % [indirection_request.indirection_name, indirection_request.key], 404) end # The encoding of the result must include the format to use, # and it needs to be used for both the rendering and as # the content type. format = format_to_use(request) set_content_type(response, format) set_response(response, result.render(format)) end # Execute our search. def do_search(indirection_request, request, response) result = indirection_request.model.search(indirection_request.key, indirection_request.to_hash) if result.nil? or (result.is_a?(Array) and result.empty?) return do_exception(response, "Could not find instances in %s with '%s'" % [indirection_request.indirection_name, indirection_request.to_hash.inspect], 404) end format = format_to_use(request) set_content_type(response, format) set_response(response, indirection_request.model.render_multiple(format, result)) end # Execute our destroy. def do_destroy(indirection_request, request, response) result = indirection_request.model.destroy(indirection_request.key, indirection_request.to_hash) return_yaml_response(response, result) end # Execute our save. def do_save(indirection_request, request, response) data = body(request).to_s raise ArgumentError, "No data to save" if !data or data.empty? format = request_format(request) obj = indirection_request.model.convert_from(format, data) result = save_object(indirection_request, obj) return_yaml_response(response, result) end # resolve node name from peer's ip address # this is used when the request is unauthenticated def resolve_node(result) begin return Resolv.getname(result[:ip]) rescue => detail Puppet.err "Could not resolve %s: %s" % [result[:ip], detail] end return result[:ip] end private def return_yaml_response(response, body) set_content_type(response, Puppet::Network::FormatHandler.format("yaml")) set_response(response, body.to_yaml) end # LAK:NOTE This has to be here for testing; it's a stub-point so # we keep infinite recursion from happening. def save_object(ind_request, object) - object.save(ind_request.to_hash) + object.save(ind_request) end def get?(request) http_method(request) == 'GET' end def put?(request) http_method(request) == 'PUT' end def delete?(request) http_method(request) == 'DELETE' end # methods to be overridden by the including web server class def http_method(request) raise NotImplementedError end def path(request) raise NotImplementedError end def request_key(request) raise NotImplementedError end def body(request) raise NotImplementedError end def params(request) raise NotImplementedError end def decode_params(params) params.inject({}) do |result, ary| param, value = ary next result if param.nil? || param.empty? param = param.to_sym # These shouldn't be allowed to be set by clients # in the query string, for security reasons. next result if param == :node next result if param == :ip value = CGI.unescape(value) if value =~ /^---/ value = YAML.load(value) else value = true if value == "true" value = false if value == "false" value = Integer(value) if value =~ /^\d+$/ value = value.to_f if value =~ /^\d+\.\d+$/ end result[param] = value result end end end diff --git a/lib/puppet/type/filebucket.rb b/lib/puppet/type/filebucket.rb index 468f92638..4754e9f3a 100755 --- a/lib/puppet/type/filebucket.rb +++ b/lib/puppet/type/filebucket.rb @@ -1,96 +1,96 @@ module Puppet - require 'puppet/network/client' + require 'puppet/file_bucket/dipper' newtype(:filebucket) do @doc = "A repository for backing up files. If no filebucket is defined, then files will be backed up in their current directory, but the filebucket can be either a host- or site-global repository for backing up. It stores files and returns the MD5 sum, which can later be used to retrieve the file if restoration becomes necessary. A filebucket does not do any work itself; instead, it can be specified as the value of *backup* in a **file** object. Currently, filebuckets are only useful for manual retrieval of accidentally removed files (e.g., you look in the log for the md5 sum and retrieve the file with that sum from the filebucket), but when transactions are fully supported filebuckets will be used to undo transactions. You will normally want to define a single filebucket for your whole network and then use that as the default backup location:: # Define the bucket filebucket { main: server => puppet } # Specify it as the default target File { backup => main } Puppetmaster servers create a filebucket by default, so this will work in a default configuration." newparam(:name) do desc "The name of the filebucket." isnamevar end newparam(:server) do desc "The server providing the remote filebucket. If this is not specified then *path* is checked. If it is set, then the bucket is local. Otherwise the puppetmaster server specified in the config or at the commandline is used." defaultto { Puppet[:server] } end newparam(:port) do desc "The port on which the remote server is listening. Defaults to the normal Puppet port, %s." % Puppet[:masterport] defaultto { Puppet[:masterport] } end newparam(:path) do desc "The path to the local filebucket. If this is unset, then the bucket is remote. The parameter *server* must can be specified to set the remote server." defaultto { Puppet[:clientbucketdir] } end # Create a default filebucket. def self.mkdefaultbucket new(:name => "puppet", :path => Puppet[:clientbucketdir]) end def bucket mkbucket() unless defined? @bucket return @bucket end private def mkbucket # Default is a local filebucket, if no server is given. # If the default path has been removed, too, then # the puppetmaster is used as default server type = "local" args = {} if self[:path] args[:Path] = self[:path] else args[:Server] = self[:server] args[:Port] = self[:port] end begin - @bucket = Puppet::Network::Client.client(:Dipper).new(args) + @bucket = Puppet::FileBucket::Dipper.new(args) rescue => detail puts detail.backtrace if Puppet[:trace] self.fail("Could not create %s filebucket: %s" % [type, detail]) end @bucket.name = self.name end end end diff --git a/lib/puppet/type/tidy.rb b/lib/puppet/type/tidy.rb index 3d7190c27..830f47640 100755 --- a/lib/puppet/type/tidy.rb +++ b/lib/puppet/type/tidy.rb @@ -1,335 +1,335 @@ Puppet::Type.newtype(:tidy) do require 'puppet/file_serving/fileset' @doc = "Remove unwanted files based on specific criteria. Multiple criteria are OR'd together, so a file that is too large but is not old enough will still get tidied. If you don't specify either 'age' or 'size', then all files will be removed. This resource type works by generating a file resource for every file that should be deleted and then letting that resource perform the actual deletion. " newparam(:path) do desc "The path to the file or directory to manage. Must be fully qualified." isnamevar end newparam(:matches) do desc "One or more (shell type) file glob patterns, which restrict the list of files to be tidied to those whose basenames match at least one of the patterns specified. Multiple patterns can be specified using an array. Example:: tidy { \"/tmp\": age => \"1w\", recurse => false, matches => [ \"[0-9]pub*.tmp\", \"*.temp\", \"tmpfile?\" ] } This removes files from \/tmp if they are one week old or older, are not in a subdirectory and match one of the shell globs given. Note that the patterns are matched against the basename of each file -- that is, your glob patterns should not have any '/' characters in them, since you are only specifying against the last bit of the file." # Make sure we convert to an array. munge do |value| value = [value] unless value.is_a?(Array) value end # Does a given path match our glob patterns, if any? Return true # if no patterns have been provided. def tidy?(path, stat) basename = File.basename(path) flags = File::FNM_DOTMATCH | File::FNM_PATHNAME return true if value.find {|pattern| File.fnmatch(pattern, basename, flags) } return false end end newparam(:backup) do desc "Whether tidied files should be backed up. Any values are passed directly to the file resources used for actual file deletion, so use its backup documentation to determine valid values." end newparam(:age) do desc "Tidy files whose age is equal to or greater than the specified time. You can choose seconds, minutes, hours, days, or weeks by specifying the first letter of any of those words (e.g., '1w'). Specifying 0 will remove all files." @@ageconvertors = { :s => 1, :m => 60 } @@ageconvertors[:h] = @@ageconvertors[:m] * 60 @@ageconvertors[:d] = @@ageconvertors[:h] * 24 @@ageconvertors[:w] = @@ageconvertors[:d] * 7 def convert(unit, multi) if num = @@ageconvertors[unit] return num * multi else self.fail "Invalid age unit '%s'" % unit end end def tidy?(path, stat) # If the file's older than we allow, we should get rid of it. if (Time.now.to_i - stat.send(resource[:type]).to_i) > value return true else return false end end munge do |age| unit = multi = nil case age when /^([0-9]+)(\w)\w*$/ multi = Integer($1) unit = $2.downcase.intern when /^([0-9]+)$/ multi = Integer($1) unit = :d else self.fail "Invalid tidy age %s" % age end convert(unit, multi) end end newparam(:size) do desc "Tidy files whose size is equal to or greater than the specified size. Unqualified values are in kilobytes, but *b*, *k*, and *m* can be appended to specify *bytes*, *kilobytes*, and *megabytes*, respectively. Only the first character is significant, so the full word can also be used." @@sizeconvertors = { :b => 0, :k => 1, :m => 2, :g => 3 } def convert(unit, multi) if num = @@sizeconvertors[unit] result = multi num.times do result *= 1024 end return result else self.fail "Invalid size unit '%s'" % unit end end def tidy?(path, stat) if stat.size >= value return true else return false end end munge do |size| case size when /^([0-9]+)(\w)\w*$/ multi = Integer($1) unit = $2.downcase.intern when /^([0-9]+)$/ multi = Integer($1) unit = :k else self.fail "Invalid tidy size %s" % age end convert(unit, multi) end end newparam(:type) do desc "Set the mechanism for determining age." newvalues(:atime, :mtime, :ctime) defaultto :atime end newparam(:recurse) do desc "If target is a directory, recursively descend into the directory looking for files to tidy." newvalues(:true, :false, :inf, /^[0-9]+$/) # Replace the validation so that we allow numbers in # addition to string representations of them. validate { |arg| } munge do |value| newval = super(value) case newval when :true, :inf; true when :false; false when Integer, Fixnum, Bignum; value when /^\d+$/; Integer(value) else raise ArgumentError, "Invalid recurse value %s" % value.inspect end end end newparam(:rmdirs, :boolean => true) do desc "Tidy directories in addition to files; that is, remove directories whose age is older than the specified criteria. This will only remove empty directories, so all contained files must also be tidied before a directory gets removed." newvalues :true, :false end # Erase PFile's validate method validate do end def self.instances [] end @depthfirst = true def initialize(hash) super # only allow backing up into filebuckets - unless self[:backup].is_a? Puppet::Network::Client.dipper + unless self[:backup].is_a? Puppet::FileBucket::Dipper self[:backup] = false end end # Make a file resource to remove a given file. def mkfile(path) # Force deletion, so directories actually get deleted. Puppet::Type.type(:file).new :path => path, :backup => self[:backup], :ensure => :absent, :force => true end def retrieve # Our ensure property knows how to retrieve everything for us. if obj = @parameters[:ensure] return obj.retrieve else return {} end end # Hack things a bit so we only ever check the ensure property. def properties [] end def eval_generate [] end def generate return [] unless stat(self[:path]) case self[:recurse] when Integer, Fixnum, Bignum, /^\d+$/ parameter = { :recurse => true, :recurselimit => self[:recurse] } when true, :true, :inf parameter = { :recurse => true } end if parameter files = Puppet::FileServing::Fileset.new(self[:path], parameter).files.collect do |f| f == "." ? self[:path] : File.join(self[:path], f) end else files = [self[:path]] end result = files.find_all { |path| tidy?(path) }.collect { |path| mkfile(path) }.each { |file| notice "Tidying %s" % file.ref }.sort { |a,b| b[:path] <=> a[:path] } # No need to worry about relationships if we don't have rmdirs; there won't be # any directories. return result unless rmdirs? # Now make sure that all directories require the files they contain, if all are available, # so that a directory is emptied before we try to remove it. files_by_name = result.inject({}) { |hash, file| hash[file[:path]] = file; hash } files_by_name.keys.sort { |a,b| b <=> b }.each do |path| dir = File.dirname(path) next unless resource = files_by_name[dir] if resource[:require] resource[:require] << Puppet::Resource::Reference.new(:file, path) else resource[:require] = [Puppet::Resource::Reference.new(:file, path)] end end return result end # Does a given path match our glob patterns, if any? Return true # if no patterns have been provided. def matches?(path) return true unless self[:matches] basename = File.basename(path) flags = File::FNM_DOTMATCH | File::FNM_PATHNAME if self[:matches].find {|pattern| File.fnmatch(pattern, basename, flags) } return true else debug "No specified patterns match %s, not tidying" % path return false end end # Should we remove the specified file? def tidy?(path) return false unless stat = self.stat(path) return false if stat.ftype == "directory" and ! rmdirs? # The 'matches' parameter isn't OR'ed with the other tests -- # it's just used to reduce the list of files we can match. return false if param = parameter(:matches) and ! param.tidy?(path, stat) tested = false [:age, :size].each do |name| next unless param = parameter(name) tested = true return true if param.tidy?(path, stat) end # If they don't specify either, then the file should always be removed. return true unless tested return false end def stat(path) begin File.lstat(path) rescue Errno::ENOENT => error info "File does not exist" return nil rescue Errno::EACCES => error warning "Could not stat; permission denied" return nil end end end diff --git a/spec/integration/checksum.rb b/spec/integration/checksum.rb deleted file mode 100755 index 49ae8d2b7..000000000 --- a/spec/integration/checksum.rb +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env ruby -# -# Created by Luke Kanies on 2007-9-22. -# Copyright (c) 2007. All rights reserved. - -require File.dirname(__FILE__) + '/../spec_helper' - -require 'puppet/checksum' - -describe Puppet::Checksum, " when using the file terminus" do - before do - Puppet.settings.stubs(:use) - Puppet::Checksum.terminus_class = :file - @content = "this is some content" - @sum = Puppet::Checksum.new(@content) - - @file = Puppet::Checksum.indirection.terminus.path(@sum.checksum) - end - - it "should store content at a path determined by its checksum" do - File.stubs(:directory?).returns(true) - filehandle = mock 'filehandle' - filehandle.expects(:print).with(@content) - File.expects(:open).with(@file, "w").yields(filehandle) - - @sum.save - end - - it "should retrieve stored content when the checksum is provided as the key" do - File.stubs(:exist?).returns(true) - File.expects(:read).with(@file).returns(@content) - - newsum = Puppet::Checksum.find(@sum.checksum) - - newsum.content.should == @content - end - - it "should remove specified files when asked" do - File.stubs(:exist?).returns(true) - File.expects(:unlink).with(@file) - - Puppet::Checksum.destroy(@sum.name) - end - - after do - Puppet.settings.clear - end -end diff --git a/spec/integration/indirector/bucket_file/rest.rb b/spec/integration/indirector/bucket_file/rest.rb new file mode 100644 index 000000000..296b03eb6 --- /dev/null +++ b/spec/integration/indirector/bucket_file/rest.rb @@ -0,0 +1,68 @@ +#!/usr/bin/env ruby + +Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } + +require 'puppet/file_bucket/file' +require 'puppet/network/server' +require 'puppet/network/http/webrick/rest' + +describe "Filebucket REST Terminus" do + before do + Puppet[:masterport] = 34343 + Puppet[:server] = "localhost" + + # Get a safe temporary file + @tmpfile = Tempfile.new("webrick_integration_testing") + @dir = @tmpfile.path + "_dir" + + Puppet.settings[:confdir] = @dir + Puppet.settings[:vardir] = @dir + Puppet.settings[:server] = "127.0.0.1" + Puppet.settings[:masterport] = "34343" + + Puppet::Util::Cacher.expire + + Puppet[:servertype] = 'webrick' + Puppet[:server] = '127.0.0.1' + Puppet[:certname] = '127.0.0.1' + + # Generate the certificate with a local CA + Puppet::SSL::Host.ca_location = :local + ca = Puppet::SSL::CertificateAuthority.new + ca.generate(Puppet[:certname]) unless Puppet::SSL::Certificate.find(Puppet[:certname]) + ca.generate("foo.madstop.com") unless Puppet::SSL::Certificate.find(Puppet[:certname]) + + @host = Puppet::SSL::Host.new(Puppet[:certname]) + + @params = { :port => 34343, :handlers => [ :file_bucket_file ] } + @server = Puppet::Network::Server.new(@params) + @server.listen + + @old_terminus = Puppet::FileBucket::File.indirection.terminus_class + Puppet::FileBucket::File.terminus_class = :rest + + # LAK:NOTE We need to have a fake model here so that our indirected methods get + # passed through REST; otherwise we'd be stubbing 'find', which would cause an immediate + # return. + @file_bucket_file = stub_everything 'file_bucket_file' + @mock_model = stub('faked model', :name => "file_bucket_file", :convert_from => @file_bucket_file) + Puppet::Indirector::Request.any_instance.stubs(:model).returns(@mock_model) + + Puppet::Network::HTTP::WEBrickREST.any_instance.stubs(:check_authorization) + end + + after do + Puppet::Network::HttpPool.expire + Puppet::SSL::Host.ca_location = :none + Puppet.settings.clear + @server.unlisten + Puppet::FileBucket::File.terminus_class = @old_terminus + end + + it "should be able save a file to the remote filebucket" do + @file_bucket_file.expects(:save) + + file_bucket_file = Puppet::FileBucket::File.new("pouet") + file_bucket_file.save() + end +end diff --git a/spec/integration/network/client.rb b/spec/integration/network/client.rb index 970763712..fe1524e60 100755 --- a/spec/integration/network/client.rb +++ b/spec/integration/network/client.rb @@ -1,19 +1,19 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/network/client' describe Puppet::Network::Client do - %w{ca dipper file report resource runner status}.each do |name| + %w{ca file report resource runner status}.each do |name| it "should have a #{name} client" do Puppet::Network::Client.client(name).should be_instance_of(Class) end [:name, :handler, :drivername].each do |data| it "should have a #{data} value for the #{name} client" do Puppet::Network::Client.client(name).send(data).should_not be_nil end end end end diff --git a/spec/unit/application/filebucket.rb b/spec/unit/application/filebucket.rb index e87bab402..f78c0b7be 100644 --- a/spec/unit/application/filebucket.rb +++ b/spec/unit/application/filebucket.rb @@ -1,220 +1,220 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/application/filebucket' describe "Filebucket" do before :each do @filebucket = Puppet::Application[:filebucket] end it "should ask Puppet::Application to not parse Puppet configuration file" do @filebucket.should_parse_config?.should be_false end it "should declare a get command" do @filebucket.should respond_to(:get) end it "should declare a backup command" do @filebucket.should respond_to(:backup) end it "should declare a restore command" do @filebucket.should respond_to(:restore) end [:bucket, :debug, :local, :remote, :verbose].each do |option| it "should declare handle_#{option} method" do @filebucket.should respond_to("handle_#{option}".to_sym) end it "should store argument value when calling handle_#{option}" do @filebucket.options.expects(:[]=).with("#{option}".to_sym, 'arg') @filebucket.send("handle_#{option}".to_sym, 'arg') end end describe "during setup" do before :each do Puppet::Log.stubs(:newdestination) Puppet.stubs(:settraps) Puppet::Log.stubs(:level=) Puppet.stubs(:parse_config) - Puppet::Network::Client.dipper.stubs(:new) + Puppet::FileBucket::Dipper.stubs(:new) @filebucket.options.stubs(:[]).with(any_parameters) end it "should set console as the log destination" do Puppet::Log.expects(:newdestination).with(:console) @filebucket.run_setup end it "should trap INT" do @filebucket.expects(:trap).with(:INT) @filebucket.run_setup end it "should set log level to debug if --debug was passed" do @filebucket.options.stubs(:[]).with(:debug).returns(true) Puppet::Log.expects(:level=).with(:debug) @filebucket.run_setup end it "should set log level to info if --verbose was passed" do @filebucket.options.stubs(:[]).with(:verbose).returns(true) Puppet::Log.expects(:level=).with(:info) @filebucket.run_setup end it "should Parse puppet config" do Puppet.expects(:parse_config) @filebucket.run_setup end it "should print puppet config if asked to in Puppet config" do @filebucket.stubs(:exit) Puppet.settings.stubs(:print_configs?).returns(true) Puppet.settings.expects(:print_configs) @filebucket.run_setup end it "should exit after printing puppet config if asked to in Puppet config" do Puppet.settings.stubs(:print_configs?).returns(true) lambda { @filebucket.run_setup }.should raise_error(SystemExit) end describe "with local bucket" do before :each do @filebucket.options.stubs(:[]).with(:local).returns(true) end it "should create a client with the default bucket if none passed" do Puppet.stubs(:[]).with(:bucketdir).returns("path") - Puppet::Network::Client::Dipper.expects(:new).with { |h| h[:Path] == "path" } + Puppet::FileBucket::Dipper.expects(:new).with { |h| h[:Path] == "path" } @filebucket.run_setup end - it "should create a local Client dipper with the given bucket" do + it "should create a local Dipper with the given bucket" do @filebucket.options.stubs(:[]).with(:bucket).returns("path") - Puppet::Network::Client::Dipper.expects(:new).with { |h| h[:Path] == "path" } + Puppet::FileBucket::Dipper.expects(:new).with { |h| h[:Path] == "path" } @filebucket.run_setup end end describe "with remote bucket" do it "should create a remote Client to the configured server" do Puppet.stubs(:[]).with(:server).returns("puppet.reductivelabs.com") - Puppet::Network::Client::Dipper.expects(:new).with { |h| h[:Server] == "puppet.reductivelabs.com" } + Puppet::FileBucket::Dipper.expects(:new).with { |h| h[:Server] == "puppet.reductivelabs.com" } @filebucket.run_setup end end end describe "when running" do before :each do Puppet::Log.stubs(:newdestination) Puppet.stubs(:settraps) Puppet::Log.stubs(:level=) Puppet.stubs(:parse_config) - Puppet::Network::Client.dipper.stubs(:new) + Puppet::FileBucket::Dipper.stubs(:new) @filebucket.options.stubs(:[]).with(any_parameters) @client = stub 'client' - Puppet::Network::Client::Dipper.stubs(:new).returns(@client) + Puppet::FileBucket::Dipper.stubs(:new).returns(@client) @filebucket.run_setup end it "should use the first non-option parameter as the dispatch" do ARGV.stubs(:shift).returns(:get) @filebucket.get_command.should == :get end describe "the command get" do before :each do @filebucket.stubs(:print) end it "should call the client getfile method" do @client.expects(:getfile) @filebucket.get end it "should call the client getfile method with the given md5" do md5="DEADBEEF" ARGV.stubs(:shift).returns(md5) @client.expects(:getfile).with(md5) @filebucket.get end it "should print the file content" do @client.stubs(:getfile).returns("content") @filebucket.expects(:print).returns("content") @filebucket.get end end describe "the command backup" do it "should call the client backup method for each given parameter" do @filebucket.stubs(:puts) FileTest.stubs(:exists?).returns(true) FileTest.stubs(:readable?).returns(true) ARGV.stubs(:each).multiple_yields("file1","file2") @client.expects(:backup).with("file1") @client.expects(:backup).with("file2") @filebucket.backup end end describe "the command restore" do it "should call the client getfile method with the given md5" do md5="DEADBEEF" file="testfile" ARGV.stubs(:shift).returns(file,md5) @client.expects(:restore).with(file,md5) @filebucket.restore end end end end diff --git a/spec/unit/application/main.rb b/spec/unit/application/main.rb index 74c40c14a..ea8c43fe6 100755 --- a/spec/unit/application/main.rb +++ b/spec/unit/application/main.rb @@ -1,384 +1,384 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/application/main' describe "Puppet" do before :each do @main = Puppet::Application[:main] Puppet::Util::Log.stubs(:newdestination) Puppet::Util::Log.stubs(:level=) end [:debug,:loadclasses,:verbose,:use_nodes,:detailed_exitcodes].each do |option| it "should declare handle_#{option} method" do @main.should respond_to("handle_#{option}".to_sym) end it "should store argument value when calling handle_#{option}" do @main.options.expects(:[]=).with(option, 'arg') @main.send("handle_#{option}".to_sym, 'arg') end end it "should set the code to the provided code when :execute is used" do @main.options.expects(:[]=).with(:code, 'arg') @main.send("handle_execute".to_sym, 'arg') end it "should ask Puppet::Application to parse Puppet configuration file" do @main.should_parse_config?.should be_true end describe "when applying options" do it "should set the log destination with --logdest" do Puppet::Log.expects(:newdestination).with("console") @main.handle_logdest("console") end it "should put the logset options to true" do @main.options.expects(:[]=).with(:logset,true) @main.handle_logdest("console") end end describe "during setup" do before :each do Puppet::Log.stubs(:newdestination) Puppet.stubs(:trap) Puppet::Log.stubs(:level=) Puppet.stubs(:parse_config) - Puppet::Network::Client.dipper.stubs(:new) + Puppet::FileBucket::Dipper.stubs(:new) STDIN.stubs(:read) @main.options.stubs(:[]).with(any_parameters) end it "should set show_diff on --noop" do Puppet.stubs(:[]=) Puppet.stubs(:[]).with(:config) Puppet.stubs(:[]).with(:noop).returns(true) Puppet.expects(:[]=).with(:show_diff, true) @main.run_setup end it "should set console as the log destination if logdest option wasn't provided" do Puppet::Log.expects(:newdestination).with(:console) @main.run_setup end it "should set INT trap" do @main.expects(:trap).with(:INT) @main.run_setup end it "should set log level to debug if --debug was passed" do @main.options.stubs(:[]).with(:debug).returns(true) Puppet::Log.expects(:level=).with(:debug) @main.run_setup end it "should set log level to info if --verbose was passed" do @main.options.stubs(:[]).with(:verbose).returns(true) Puppet::Log.expects(:level=).with(:info) @main.run_setup end it "should print puppet config if asked to in Puppet config" do @main.stubs(:exit) Puppet.settings.stubs(:print_configs?).returns(true) Puppet.settings.expects(:print_configs) @main.run_setup end it "should exit after printing puppet config if asked to in Puppet config" do Puppet.settings.stubs(:print_configs?).returns(true) lambda { @main.run_setup }.should raise_error(SystemExit) end end describe "when executing" do it "should dispatch to parseonly if parseonly is set" do @main.stubs(:options).returns({}) Puppet.stubs(:[]).with(:parseonly).returns(true) @main.get_command.should == :parseonly end it "should dispatch to 'apply' if it was called with 'apply'" do @main.options[:catalog] = "foo" @main.get_command.should == :apply end it "should dispatch to main if parseonly is not set" do @main.stubs(:options).returns({}) Puppet.stubs(:[]).with(:parseonly).returns(false) @main.get_command.should == :main end describe "the parseonly command" do before :each do Puppet.stubs(:[]).with(:environment) Puppet.stubs(:[]).with(:manifest).returns("site.pp") Puppet.stubs(:err) @main.stubs(:exit) @main.options.stubs(:[]).with(:code).returns "some code" @collection = stub_everything Puppet::Resource::TypeCollection.stubs(:new).returns(@collection) end it "should use a Puppet Resource Type Collection to parse the file" do @collection.expects(:perform_initial_import) @main.parseonly end it "should exit with exit code 0 if no error" do @main.expects(:exit).with(0) @main.parseonly end it "should exit with exit code 1 if error" do @collection.stubs(:perform_initial_import).raises(Puppet::ParseError) @main.expects(:exit).with(1) @main.parseonly end end describe "the main command" do before :each do Puppet.stubs(:[]) Puppet.settings.stubs(:use) Puppet.stubs(:[]).with(:prerun_command).returns "" Puppet.stubs(:[]).with(:postrun_command).returns "" Puppet.stubs(:[]).with(:trace).returns(true) @main.options.stubs(:[]) @facts = stub_everything 'facts' Puppet::Node::Facts.stubs(:find).returns(@facts) @node = stub_everything 'node' Puppet::Node.stubs(:find).returns(@node) @catalog = stub_everything 'catalog' @catalog.stubs(:to_ral).returns(@catalog) Puppet::Resource::Catalog.stubs(:find).returns(@catalog) STDIN.stubs(:read) @transaction = stub_everything 'transaction' @catalog.stubs(:apply).returns(@transaction) @main.stubs(:exit) end it "should set the code to run from --code" do @main.options.stubs(:[]).with(:code).returns("code to run") Puppet.expects(:[]=).with(:code,"code to run") @main.main end it "should set the code to run from STDIN if no arguments" do ARGV.stubs(:length).returns(0) STDIN.stubs(:read).returns("code to run") Puppet.expects(:[]=).with(:code,"code to run") @main.main end it "should set the manifest if some files are passed on command line" do ARGV.stubs(:length).returns(1) ARGV.stubs(:shift).returns("site.pp") Puppet.expects(:[]=).with(:manifest,"site.pp") @main.main end it "should collect the node facts" do Puppet::Node::Facts.expects(:find).returns(@facts) @main.main end it "should raise an error if we can't find the node" do Puppet::Node::Facts.expects(:find).returns(nil) lambda { @main.main }.should raise_error end it "should find the node" do Puppet::Node.expects(:find).returns(@node) @main.main end it "should raise an error if we can't find the node" do Puppet::Node.expects(:find).returns(nil) lambda { @main.main }.should raise_error end it "should merge in our node the loaded facts" do @facts.stubs(:values).returns("values") @node.expects(:merge).with("values") @main.main end it "should load custom classes if loadclasses" do @main.options.stubs(:[]).with(:loadclasses).returns(true) Puppet.stubs(:[]).with(:classfile).returns("/etc/puppet/classes.txt") FileTest.stubs(:exists?).with("/etc/puppet/classes.txt").returns(true) FileTest.stubs(:readable?).with("/etc/puppet/classes.txt").returns(true) File.stubs(:read).with("/etc/puppet/classes.txt").returns("class") @node.expects(:classes=) @main.main end it "should compile the catalog" do Puppet::Resource::Catalog.expects(:find).returns(@catalog) @main.main end it "should transform the catalog to ral" do @catalog.expects(:to_ral).returns(@catalog) @main.main end it "should finalize the catalog" do @catalog.expects(:finalize) @main.main end it "should call the prerun and postrun commands on a Configurer instance" do configurer = stub 'configurer' Puppet::Configurer.expects(:new).returns configurer configurer.expects(:execute_prerun_command) configurer.expects(:execute_postrun_command) @main.main end it "should apply the catalog" do @catalog.expects(:apply) @main.main end describe "with detailed_exitcodes" do it "should exit with report's computed exit status" do Puppet.stubs(:[]).with(:noop).returns(false) @main.options.stubs(:[]).with(:detailed_exitcodes).returns(true) report = stub 'report', :exit_status => 666 @transaction.stubs(:report).returns(report) @main.expects(:exit).with(666) @main.main end it "should always exit with 0 if option is disabled" do Puppet.stubs(:[]).with(:noop).returns(false) @main.options.stubs(:[]).with(:detailed_exitcodes).returns(false) report = stub 'report', :exit_status => 666 @transaction.stubs(:report).returns(report) @main.expects(:exit).with(0) @main.main end it "should always exit with 0 if --noop" do Puppet.stubs(:[]).with(:noop).returns(true) @main.options.stubs(:[]).with(:detailed_exitcodes).returns(true) report = stub 'report', :exit_status => 666 @transaction.stubs(:report).returns(report) @main.expects(:exit).with(0) @main.main end end end describe "the 'apply' command" do it "should read the catalog in from disk if a file name is provided" do @main.options[:catalog] = "/my/catalog.pson" File.expects(:read).with("/my/catalog.pson").returns "something" Puppet::Resource::Catalog.stubs(:convert_from).with(:pson,'something').returns Puppet::Resource::Catalog.new @main.apply end it "should read the catalog in from stdin if '-' is provided" do @main.options[:catalog] = "-" $stdin.expects(:read).returns "something" Puppet::Resource::Catalog.stubs(:convert_from).with(:pson,'something').returns Puppet::Resource::Catalog.new @main.apply end it "should deserialize the catalog from the default format" do @main.options[:catalog] = "/my/catalog.pson" File.stubs(:read).with("/my/catalog.pson").returns "something" Puppet::Resource::Catalog.stubs(:default_format).returns :rot13_piglatin Puppet::Resource::Catalog.stubs(:convert_from).with(:rot13_piglatin,'something').returns Puppet::Resource::Catalog.new @main.apply end it "should fail helpfully if deserializing fails" do @main.options[:catalog] = "/my/catalog.pson" File.stubs(:read).with("/my/catalog.pson").returns "something syntacically invalid" lambda { @main.apply }.should raise_error(Puppet::Error) end it "should convert plain data structures into a catalog if deserialization does not do so" do @main.options[:catalog] = "/my/catalog.pson" File.stubs(:read).with("/my/catalog.pson").returns "something" Puppet::Resource::Catalog.stubs(:convert_from).with(:pson,"something").returns({:foo => "bar"}) Puppet::Resource::Catalog.expects(:pson_create).with({:foo => "bar"}).returns(Puppet::Resource::Catalog.new) @main.apply end it "should convert the catalog to a RAL catalog and use a Configurer instance to apply it" do @main.options[:catalog] = "/my/catalog.pson" File.stubs(:read).with("/my/catalog.pson").returns "something" catalog = Puppet::Resource::Catalog.new Puppet::Resource::Catalog.stubs(:convert_from).with(:pson,'something').returns catalog catalog.expects(:to_ral).returns "mycatalog" configurer = stub 'configurer' Puppet::Configurer.expects(:new).returns configurer configurer.expects(:run).with(:catalog => "mycatalog") @main.apply end end end end diff --git a/spec/unit/file_bucket/file.rb b/spec/unit/file_bucket/file.rb new file mode 100644 index 000000000..76f8e2599 --- /dev/null +++ b/spec/unit/file_bucket/file.rb @@ -0,0 +1,239 @@ +#!/usr/bin/env ruby + +require ::File.dirname(__FILE__) + '/../../spec_helper' + +require 'puppet/file_bucket/file' +require 'digest/md5' +require 'digest/sha1' + +describe Puppet::FileBucket::File do + before do + # this is the default from spec_helper, but it keeps getting reset at odd times + Puppet[:bucketdir] = "/dev/null/bucket" + + @digest = "4a8ec4fa5f01b4ab1a0ab8cbccb709f0" + @checksum = "md5:4a8ec4fa5f01b4ab1a0ab8cbccb709f0" + @dir = '/dev/null/bucket/4/a/8/e/c/4/f/a/4a8ec4fa5f01b4ab1a0ab8cbccb709f0' + + @contents = "file contents" + end + + it "should save a file" do + ::File.expects(:exist?).with("#{@dir}/contents").returns false + ::File.expects(:directory?).with(@dir).returns false + ::FileUtils.expects(:mkdir_p).with(@dir) + ::File.expects(:open).with("#{@dir}/contents", ::File::WRONLY|::File::CREAT, 0440) + + bucketfile = Puppet::FileBucket::File.new(@contents) + bucketfile.save + + end + + describe "using the indirector's find method" do + it "should return nil if a file doesn't exist" do + ::File.expects(:exist?).with("#{@dir}/contents").returns false + + bucketfile = Puppet::FileBucket::File.find("md5:#{@digest}") + bucketfile.should == nil + end + + it "should find a filebucket if the file exists" do + ::File.expects(:exist?).with("#{@dir}/contents").returns true + ::File.expects(:exist?).with("#{@dir}/paths").returns false + ::File.expects(:read).with("#{@dir}/contents").returns @contents + + bucketfile = Puppet::FileBucket::File.find("md5:#{@digest}") + bucketfile.should_not == nil + end + + describe "using RESTish digest notation" do + it "should return nil if a file doesn't exist" do + ::File.expects(:exist?).with("#{@dir}/contents").returns false + + bucketfile = Puppet::FileBucket::File.find("md5/#{@digest}") + bucketfile.should == nil + end + + it "should find a filebucket if the file exists" do + ::File.expects(:exist?).with("#{@dir}/contents").returns true + ::File.expects(:exist?).with("#{@dir}/paths").returns false + ::File.expects(:read).with("#{@dir}/contents").returns @contents + + bucketfile = Puppet::FileBucket::File.find("md5/#{@digest}") + bucketfile.should_not == nil + end + + end + end + + it "should have a to_s method to return the contents" do + Puppet::FileBucket::File.new(@contents).to_s.should == @contents + end + + it "should have a method that returns the digest algorithm" do + Puppet::FileBucket::File.new(@contents, :checksum => @checksum).checksum_type.should == :md5 + end + + it "should allow contents to be specified in a block" do + bucket = Puppet::FileBucket::File.new(nil) do |fb| + fb.contents = "content" + end + bucket.contents.should == "content" + end + + it "should raise an error if changing content" do + x = Puppet::FileBucket::File.new("first") + proc { x.contents = "new" }.should raise_error + end + + it "should require contents to be a string" do + proc { Puppet::FileBucket::File.new(5) }.should raise_error(ArgumentError) + end + + it "should raise an error if setting contents to a non-string" do + proc do + Puppet::FileBucket::File.new(nil) do |x| + x.contents = 5 + end + end.should raise_error(ArgumentError) + end + + it "should set the contents appropriately" do + Puppet::FileBucket::File.new(@contents).contents.should == @contents + end + + it "should calculate the checksum" do + Digest::MD5.expects(:hexdigest).with(@contents).returns('mychecksum') + Puppet::FileBucket::File.new(@contents).checksum.should == 'md5:mychecksum' + end + + it "should remove the old checksum value if the algorithm is changed" do + Digest::MD5.expects(:hexdigest).with(@contents).returns('oldsum') + sum = Puppet::FileBucket::File.new(@contents) + oldsum = sum.checksum + + sum.checksum_type = :sha1 + Digest::SHA1.expects(:hexdigest).with(@contents).returns('newsum') + sum.checksum.should == 'sha1:newsum' + end + + it "should default to 'md5' as the checksum algorithm if the algorithm is not in the name" do + Puppet::FileBucket::File.new(@contents).checksum_type.should == :md5 + end + + it "should support specifying the checksum_type during initialization" do + sum = Puppet::FileBucket::File.new(@contents, :checksum_type => :sha1) + sum.checksum_type.should == :sha1 + end + + it "should fail when an unsupported checksum_type is used" do + proc { Puppet::FileBucket::File.new(@contents, :checksum_type => :nope) }.should raise_error(ArgumentError) + end + + it "should fail if given an invalid checksum at initialization" do + proc { Puppet::FileBucket::File.new(@contents, :checksum => "md5:00000000000000000000000000000000") }.should raise_error(RuntimeError) + end + + it "should fail if assigned an invalid checksum " do + bucket = Puppet::FileBucket::File.new(@contents) + proc { bucket.checksum = "md5:00000000000000000000000000000000" }.should raise_error(RuntimeError) + end + + it "should accept checksum_data without a prefix" do + bucket = Puppet::FileBucket::File.new(@contents) + bucket.checksum_data = @digest + end + + + describe "when using back-ends" do + it "should redirect using Puppet::Indirector" do + Puppet::Indirector::Indirection.instance(:file_bucket_file).model.should equal(Puppet::FileBucket::File) + end + + it "should have a :save instance method" do + Puppet::FileBucket::File.new("mysum").should respond_to(:save) + end + + it "should respond to :find" do + Puppet::FileBucket::File.should respond_to(:find) + end + + it "should respond to :destroy" do + Puppet::FileBucket::File.should respond_to(:destroy) + end + end + + describe "when saving files" do + it "should save the contents to the calculated path" do + ::File.stubs(:directory?).with(@dir).returns(true) + ::File.expects(:exist?).with("#{@dir}/contents").returns false + + mockfile = mock "file" + mockfile.expects(:print).with(@contents) + ::File.expects(:open).with("#{@dir}/contents", ::File::WRONLY|::File::CREAT, 0440).yields(mockfile) + + Puppet::FileBucket::File.new(@contents).save + end + + it "should make any directories necessary for storage" do + FileUtils.expects(:mkdir_p).with do |arg| + ::File.umask == 0007 and arg == @dir + end + ::File.expects(:directory?).with(@dir).returns(false) + ::File.expects(:open).with("#{@dir}/contents", ::File::WRONLY|::File::CREAT, 0440) + ::File.expects(:exist?).with("#{@dir}/contents").returns false + + Puppet::FileBucket::File.new(@contents).save + end + end + + it "should accept a path" do + remote_path = '/path/on/the/remote/box' + Puppet::FileBucket::File.new(@contents, :path => remote_path).path.should == remote_path + end + + it "should append the path to the paths file" do + remote_path = '/path/on/the/remote/box' + + ::File.expects(:directory?).with(@dir).returns(true) + ::File.expects(:open).with("#{@dir}/contents", ::File::WRONLY|::File::CREAT, 0440) + ::File.expects(:exist?).with("#{@dir}/contents").returns false + + mockfile = mock "file" + mockfile.expects(:puts).with('/path/on/the/remote/box') + ::File.expects(:exist?).with("#{@dir}/paths").returns false + ::File.expects(:open).with("#{@dir}/paths", ::File::WRONLY|::File::CREAT|::File::APPEND).yields mockfile + Puppet::FileBucket::File.new(@contents, :path => remote_path).save + + end + + it "should return a url-ish name" do + Puppet::FileBucket::File.new(@contents).name.should == "md5/4a8ec4fa5f01b4ab1a0ab8cbccb709f0" + end + + it "should reject a url-ish name with an invalid checksum" do + bucket = Puppet::FileBucket::File.new(@contents) + lambda { bucket.name = "sha1/4a8ec4fa5f01b4ab1a0ab8cbccb709f0/new/path" }.should raise_error + end + + it "should accept a url-ish name" do + bucket = Puppet::FileBucket::File.new(@contents) + lambda { bucket.name = "sha1/034fa2ed8e211e4d20f20e792d777f4a30af1a93/new/path" }.should_not raise_error + bucket.checksum_type.should == :sha1 + bucket.checksum_data.should == '034fa2ed8e211e4d20f20e792d777f4a30af1a93' + bucket.path.should == "new/path" + end + + it "should return a url-ish name with a path" do + Puppet::FileBucket::File.new(@contents, :path => 'my/path').name.should == "md5/4a8ec4fa5f01b4ab1a0ab8cbccb709f0/my/path" + end + + it "should convert the contents to PSON" do + Puppet::FileBucket::File.new(@contents).to_pson.should == '{"contents":"file contents"}' + end + + it "should load from PSON" do + Puppet::FileBucket::File.from_pson({"contents"=>"file contents"}).contents.should == "file contents" + end + +end diff --git a/spec/unit/indirector/file_bucket_file/file.rb b/spec/unit/indirector/file_bucket_file/file.rb new file mode 100755 index 000000000..0df530d74 --- /dev/null +++ b/spec/unit/indirector/file_bucket_file/file.rb @@ -0,0 +1,288 @@ +#!/usr/bin/env ruby + +require ::File.dirname(__FILE__) + '/../../../spec_helper' + +require 'puppet/indirector/file_bucket_file/file' + +describe Puppet::FileBucketFile::File do + it "should be a subclass of the Code terminus class" do + Puppet::FileBucketFile::File.superclass.should equal(Puppet::Indirector::Code) + end + + it "should have documentation" do + Puppet::FileBucketFile::File.doc.should be_instance_of(String) + end + + describe "when initializing" do + it "should use the filebucket settings section" do + Puppet.settings.expects(:use).with(:filebucket) + Puppet::FileBucketFile::File.new + end + end + + + describe "the find_by_checksum method" do + before do + # this is the default from spec_helper, but it keeps getting reset at odd times + Puppet[:bucketdir] = "/dev/null/bucket" + + @digest = "4a8ec4fa5f01b4ab1a0ab8cbccb709f0" + @checksum = "md5:4a8ec4fa5f01b4ab1a0ab8cbccb709f0" + @dir = '/dev/null/bucket/4/a/8/e/c/4/f/a/4a8ec4fa5f01b4ab1a0ab8cbccb709f0' + + @contents = "file contents" + end + + it "should return nil if a file doesn't exist" do + ::File.expects(:exist?).with("#{@dir}/contents").returns false + + bucketfile = Puppet::FileBucketFile::File.new.send(:find_by_checksum, "md5:#{@digest}") + bucketfile.should == nil + end + + it "should find a filebucket if the file exists" do + ::File.expects(:exist?).with("#{@dir}/contents").returns true + ::File.expects(:exist?).with("#{@dir}/paths").returns false + ::File.expects(:read).with("#{@dir}/contents").returns @contents + + bucketfile = Puppet::FileBucketFile::File.new.send(:find_by_checksum, "md5:#{@digest}") + bucketfile.should_not == nil + end + + it "should load the paths" do + paths = ["path1", "path2"] + ::File.expects(:exist?).with("#{@dir}/contents").returns true + ::File.expects(:exist?).with("#{@dir}/paths").returns true + ::File.expects(:read).with("#{@dir}/contents").returns @contents + + mockfile = mock "file" + mockfile.expects(:readlines).returns( paths ) + ::File.expects(:open).with("#{@dir}/paths").yields mockfile + + Puppet::FileBucketFile::File.new.send(:find_by_checksum, "md5:#{@digest}").paths.should == paths + end + + end + + describe "when retrieving files" do + before :each do + Puppet.settings.stubs(:use) + @store = Puppet::FileBucketFile::File.new + + @digest = "70924d6fa4b2d745185fa4660703a5c0" + @sum = stub 'sum', :name => @digest + + @dir = "/what/ever" + + Puppet.stubs(:[]).with(:bucketdir).returns(@dir) + + @contents_path = '/what/ever/7/0/9/2/4/d/6/f/70924d6fa4b2d745185fa4660703a5c0/contents' + @paths_path = '/what/ever/7/0/9/2/4/d/6/f/70924d6fa4b2d745185fa4660703a5c0/paths' + + @request = stub 'request', :key => "md5/#{@digest}/remote/path" + end + + it "should call find_by_checksum" do + @store.expects(:find_by_checksum).with("md5:#{@digest}").returns(false) + @store.find(@request) + end + + it "should look for the calculated path" do + ::File.expects(:exist?).with(@contents_path).returns(false) + @store.find(@request) + end + + it "should return an instance of Puppet::FileBucket::File created with the content if the file exists" do + content = "my content" + bucketfile = stub 'bucketfile' + bucketfile.stubs(:bucket_path) + bucketfile.stubs(:checksum_data).returns(@digest) + bucketfile.stubs(:checksum).returns(@checksum) + + bucketfile.expects(:contents=).with(content) + Puppet::FileBucket::File.expects(:new).with(nil, {:checksum => "md5:#{@digest}"}).yields(bucketfile).returns(bucketfile) + + ::File.expects(:exist?).with(@contents_path).returns(true) + ::File.expects(:exist?).with(@paths_path).returns(false) + ::File.expects(:read).with(@contents_path).returns(content) + + @store.find(@request).should equal(bucketfile) + end + + it "should return nil if no file is found" do + ::File.expects(:exist?).with(@contents_path).returns(false) + @store.find(@request).should be_nil + end + + it "should fail intelligently if a found file cannot be read" do + ::File.expects(:exist?).with(@contents_path).returns(true) + ::File.expects(:read).with(@contents_path).raises(RuntimeError) + proc { @store.find(@request) }.should raise_error(Puppet::Error) + end + + end + + describe "when determining file paths" do + before do + Puppet[:bucketdir] = '/dev/null/bucketdir' + @digest = 'DEADBEEFC0FFEE' + @bucket = stub_everything "bucket" + @bucket.expects(:checksum_data).returns(@digest) + end + + it "should use the value of the :bucketdir setting as the root directory" do + path = Puppet::FileBucketFile::File.new.send(:contents_path_for, @bucket) + path.should =~ %r{^/dev/null/bucketdir} + end + + it "should choose a path 8 directories deep with each directory name being the respective character in the filebucket" do + path = Puppet::FileBucketFile::File.new.send(:contents_path_for, @bucket) + dirs = @digest[0..7].split("").join(File::SEPARATOR) + path.should be_include(dirs) + end + + it "should use the full filebucket as the final directory name" do + path = Puppet::FileBucketFile::File.new.send(:contents_path_for, @bucket) + ::File.basename(::File.dirname(path)).should == @digest + end + + it "should use 'contents' as the actual file name" do + path = Puppet::FileBucketFile::File.new.send(:contents_path_for, @bucket) + ::File.basename(path).should == "contents" + end + + it "should use the bucketdir, the 8 sum character directories, the full filebucket, and 'contents' as the full file name" do + path = Puppet::FileBucketFile::File.new.send(:contents_path_for, @bucket) + path.should == ['/dev/null/bucketdir', @digest[0..7].split(""), @digest, "contents"].flatten.join(::File::SEPARATOR) + end + end + + describe "when saving files" do + before do + # this is the default from spec_helper, but it keeps getting reset at odd times + Puppet[:bucketdir] = "/dev/null/bucket" + + @digest = "4a8ec4fa5f01b4ab1a0ab8cbccb709f0" + @checksum = "md5:4a8ec4fa5f01b4ab1a0ab8cbccb709f0" + @dir = '/dev/null/bucket/4/a/8/e/c/4/f/a/4a8ec4fa5f01b4ab1a0ab8cbccb709f0' + + @contents = "file contents" + + @bucket = stub "bucket file" + @bucket.stubs(:bucket_path) + @bucket.stubs(:checksum_data).returns(@digest) + @bucket.stubs(:path).returns(nil) + @bucket.stubs(:contents).returns("file contents") + end + + it "should save the contents to the calculated path" do + ::File.stubs(:directory?).with(@dir).returns(true) + ::File.expects(:exist?).with("#{@dir}/contents").returns false + + mockfile = mock "file" + mockfile.expects(:print).with(@contents) + ::File.expects(:open).with("#{@dir}/contents", ::File::WRONLY|::File::CREAT, 0440).yields(mockfile) + + Puppet::FileBucketFile::File.new.send(:save_to_disk, @bucket) + end + + it "should make any directories necessary for storage" do + FileUtils.expects(:mkdir_p).with do |arg| + ::File.umask == 0007 and arg == @dir + end + ::File.expects(:directory?).with(@dir).returns(false) + ::File.expects(:open).with("#{@dir}/contents", ::File::WRONLY|::File::CREAT, 0440) + ::File.expects(:exist?).with("#{@dir}/contents").returns false + + Puppet::FileBucketFile::File.new.send(:save_to_disk, @bucket) + end + end + + + describe "when verifying identical files" do + before do + # this is the default from spec_helper, but it keeps getting reset at odd times + Puppet[:bucketdir] = "/dev/null/bucket" + + @digest = "4a8ec4fa5f01b4ab1a0ab8cbccb709f0" + @checksum = "md5:4a8ec4fa5f01b4ab1a0ab8cbccb709f0" + @dir = '/dev/null/bucket/4/a/8/e/c/4/f/a/4a8ec4fa5f01b4ab1a0ab8cbccb709f0' + + @contents = "file contents" + + @bucket = stub "bucket file" + @bucket.stubs(:bucket_path) + @bucket.stubs(:checksum).returns(@checksum) + @bucket.stubs(:checksum_data).returns(@digest) + @bucket.stubs(:path).returns(nil) + @bucket.stubs(:contents).returns("file contents") + end + + it "should raise an error if the files don't match" do + File.expects(:read).with("#{@dir}/contents").returns("corrupt contents") + lambda{ Puppet::FileBucketFile::File.new.send(:verify_identical_file!, @bucket) }.should raise_error(Puppet::FileBucket::BucketError) + end + + it "should do nothing if the files match" do + File.expects(:read).with("#{@dir}/contents").returns("file contents") + Puppet::FileBucketFile::File.new.send(:verify_identical_file!, @bucket) + end + + end + + + describe "when writing to the paths file" do + before do + Puppet[:bucketdir] = '/dev/null/bucketdir' + @digest = '70924d6fa4b2d745185fa4660703a5c0' + @bucket = stub_everything "bucket" + + @paths_path = '/dev/null/bucketdir/7/0/9/2/4/d/6/f/70924d6fa4b2d745185fa4660703a5c0/paths' + + @paths = [] + @bucket.stubs(:paths).returns(@paths) + @bucket.stubs(:checksum_data).returns(@digest) + end + + it "should create a file if it doesn't exist" do + @bucket.expects(:path).returns('path/to/save').at_least_once + File.expects(:exist?).with(@paths_path).returns(false) + file = stub "file" + file.expects(:puts).with('path/to/save') + File.expects(:open).with(@paths_path, ::File::WRONLY|::File::CREAT|::File::APPEND).yields(file) + + Puppet::FileBucketFile::File.new.send(:save_path_to_paths_file, @bucket) + end + + it "should append to a file if it exists" do + @bucket.expects(:path).returns('path/to/save').at_least_once + File.expects(:exist?).with(@paths_path).returns(true) + old_file = stub "file" + old_file.stubs(:readlines).returns [] + File.expects(:open).with(@paths_path).yields(old_file) + + file = stub "file" + file.expects(:puts).with('path/to/save') + File.expects(:open).with(@paths_path, ::File::WRONLY|::File::CREAT|::File::APPEND).yields(file) + + Puppet::FileBucketFile::File.new.send(:save_path_to_paths_file, @bucket) + end + + it "should not alter a file if it already contains the path" do + @bucket.expects(:path).returns('path/to/save').at_least_once + File.expects(:exist?).with(@paths_path).returns(true) + old_file = stub "file" + old_file.stubs(:readlines).returns ["path/to/save\n"] + File.expects(:open).with(@paths_path).yields(old_file) + + Puppet::FileBucketFile::File.new.send(:save_path_to_paths_file, @bucket) + end + + it "should do nothing if there is no path" do + @bucket.expects(:path).returns(nil).at_least_once + + Puppet::FileBucketFile::File.new.send(:save_path_to_paths_file, @bucket) + end + end + +end diff --git a/spec/unit/indirector/file_bucket_file/rest.rb b/spec/unit/indirector/file_bucket_file/rest.rb new file mode 100755 index 000000000..3aacd3ca4 --- /dev/null +++ b/spec/unit/indirector/file_bucket_file/rest.rb @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby + +Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } + +require 'puppet/indirector/file_bucket_file/rest' + +describe Puppet::FileBucketFile::Rest do + it "should be a sublcass of Puppet::Indirector::REST" do + Puppet::FileBucketFile::Rest.superclass.should equal(Puppet::Indirector::REST) + end +end diff --git a/spec/unit/indirector/indirection.rb b/spec/unit/indirector/indirection.rb index ca2a412e3..02d04a3f7 100755 --- a/spec/unit/indirector/indirection.rb +++ b/spec/unit/indirector/indirection.rb @@ -1,806 +1,806 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/indirector/indirection' describe "Indirection Delegator", :shared => true do it "should create a request object with the appropriate method name and all of the passed arguments" do request = Puppet::Indirector::Request.new(:indirection, :find, "me") @indirection.expects(:request).with(@method, "mystuff", :one => :two).returns request @terminus.stubs(@method) @indirection.send(@method, "mystuff", :one => :two) end it "should let the :select_terminus method choose the terminus using the created request if the :select_terminus method is available" do # Define the method, so our respond_to? hook matches. class << @indirection def select_terminus(request) end end request = Puppet::Indirector::Request.new(:indirection, :find, "me") @indirection.stubs(:request).returns request @indirection.expects(:select_terminus).with(request).returns :test_terminus @indirection.stubs(:check_authorization) @terminus.expects(@method) @indirection.send(@method, "me") end it "should fail if the :select_terminus hook does not return a terminus name" do # Define the method, so our respond_to? hook matches. class << @indirection def select_terminus(request) end end request = stub 'request', :key => "me", :options => {} @indirection.stubs(:request).returns request @indirection.expects(:select_terminus).with(request).returns nil lambda { @indirection.send(@method, "me") }.should raise_error(ArgumentError) end it "should choose the terminus returned by the :terminus_class method if no :select_terminus method is available" do @indirection.expects(:terminus_class).returns :test_terminus @terminus.expects(@method) @indirection.send(@method, "me") end it "should let the appropriate terminus perform the lookup" do @terminus.expects(@method).with { |r| r.is_a?(Puppet::Indirector::Request) } @indirection.send(@method, "me") end end describe "Delegation Authorizer", :shared => true do before do # So the :respond_to? turns out correctly. class << @terminus def authorized? end end end it "should not check authorization if a node name is not provided" do @terminus.expects(:authorized?).never @terminus.stubs(@method) # The quotes are necessary here, else it looks like a block. @request.stubs(:options).returns({}) @indirection.send(@method, "/my/key") end it "should pass the request to the terminus's authorization method" do @terminus.expects(:authorized?).with { |r| r.is_a?(Puppet::Indirector::Request) }.returns(true) @terminus.stubs(@method) @indirection.send(@method, "/my/key", :node => "mynode") end it "should fail if authorization returns false" do @terminus.expects(:authorized?).returns(false) @terminus.stubs(@method) proc { @indirection.send(@method, "/my/key", :node => "mynode") }.should raise_error(ArgumentError) end it "should continue if authorization returns true" do @terminus.expects(:authorized?).returns(true) @terminus.stubs(@method) @indirection.send(@method, "/my/key", :node => "mynode") end end describe Puppet::Indirector::Indirection do after do Puppet::Util::Cacher.expire end describe "when initializing" do # (LAK) I've no idea how to test this, really. it "should store a reference to itself before it consumes its options" do proc { @indirection = Puppet::Indirector::Indirection.new(Object.new, :testingness, :not_valid_option) }.should raise_error Puppet::Indirector::Indirection.instance(:testingness).should be_instance_of(Puppet::Indirector::Indirection) Puppet::Indirector::Indirection.instance(:testingness).delete end it "should keep a reference to the indirecting model" do model = mock 'model' @indirection = Puppet::Indirector::Indirection.new(model, :myind) @indirection.model.should equal(model) end it "should set the name" do @indirection = Puppet::Indirector::Indirection.new(mock('model'), :myind) @indirection.name.should == :myind end it "should require indirections to have unique names" do @indirection = Puppet::Indirector::Indirection.new(mock('model'), :test) proc { Puppet::Indirector::Indirection.new(:test) }.should raise_error(ArgumentError) end it "should extend itself with any specified module" do mod = Module.new @indirection = Puppet::Indirector::Indirection.new(mock('model'), :test, :extend => mod) @indirection.metaclass.included_modules.should include(mod) end after do @indirection.delete if defined? @indirection end end describe "when an instance" do before :each do @terminus_class = mock 'terminus_class' @terminus = mock 'terminus' @terminus_class.stubs(:new).returns(@terminus) @cache = stub 'cache', :name => "mycache" @cache_class = mock 'cache_class' Puppet::Indirector::Terminus.stubs(:terminus_class).with(:test, :cache_terminus).returns(@cache_class) Puppet::Indirector::Terminus.stubs(:terminus_class).with(:test, :test_terminus).returns(@terminus_class) @indirection = Puppet::Indirector::Indirection.new(mock('model'), :test) @indirection.terminus_class = :test_terminus @instance = stub 'instance', :expiration => nil, :expiration= => nil, :name => "whatever" @name = :mything #@request = stub 'instance', :key => "/my/key", :instance => @instance, :options => {} @request = mock 'instance' end it "should allow setting the ttl" do @indirection.ttl = 300 @indirection.ttl.should == 300 end it "should default to the :runinterval setting, converted to an integer, for its ttl" do Puppet.settings.expects(:value).returns "1800" @indirection.ttl.should == 1800 end it "should calculate the current expiration by adding the TTL to the current time" do @indirection.stubs(:ttl).returns(100) now = Time.now Time.stubs(:now).returns now @indirection.expiration.should == (Time.now + 100) end it "should have a method for creating an indirection request instance" do @indirection.should respond_to(:request) end describe "creates a request" do it "should create it with its name as the request's indirection name" do Puppet::Indirector::Request.expects(:new).with { |name, *other| @indirection.name == name } @indirection.request(:funtest, "yayness") end it "should require a method and key" do Puppet::Indirector::Request.expects(:new).with { |name, method, key, *other| method == :funtest and key == "yayness" } @indirection.request(:funtest, "yayness") end it "should support optional arguments" do Puppet::Indirector::Request.expects(:new).with { |name, method, key, other| other == {:one => :two} } @indirection.request(:funtest, "yayness", :one => :two) end - it "should default to the arguments being nil" do - Puppet::Indirector::Request.expects(:new).with { |name, method, key, args| args.nil? } + it "should default to the arguments being empty" do + Puppet::Indirector::Request.expects(:new).with { |name, method, key, args| args == {} } @indirection.request(:funtest, "yayness") end it "should return the request" do request = mock 'request' Puppet::Indirector::Request.expects(:new).returns request @indirection.request(:funtest, "yayness").should equal(request) end end describe "and looking for a model instance" do before { @method = :find } it_should_behave_like "Indirection Delegator" it_should_behave_like "Delegation Authorizer" it "should return the results of the delegation" do @terminus.expects(:find).returns(@instance) @indirection.find("me").should equal(@instance) end it "should set the expiration date on any instances without one set" do @terminus.stubs(:find).returns(@instance) @indirection.expects(:expiration).returns :yay @instance.expects(:expiration).returns(nil) @instance.expects(:expiration=).with(:yay) @indirection.find("/my/key") end it "should not override an already-set expiration date on returned instances" do @terminus.stubs(:find).returns(@instance) @indirection.expects(:expiration).never @instance.expects(:expiration).returns(:yay) @instance.expects(:expiration=).never @indirection.find("/my/key") end it "should filter the result instance if the terminus supports it" do @terminus.stubs(:find).returns(@instance) @terminus.stubs(:respond_to?).with(:filter).returns(true) @terminus.expects(:filter).with(@instance) @indirection.find("/my/key") end describe "when caching is enabled" do before do @indirection.cache_class = :cache_terminus @cache_class.stubs(:new).returns(@cache) @instance.stubs(:expired?).returns false end it "should first look in the cache for an instance" do @terminus.stubs(:find).never @cache.expects(:find).returns @instance @indirection.find("/my/key") end it "should not look in the cache if the request specifies not to use the cache" do @terminus.expects(:find).returns @instance @cache.expects(:find).never @cache.stubs(:save) @indirection.find("/my/key", :ignore_cache => true) end it "should still save to the cache even if the cache is being ignored during readin" do @terminus.expects(:find).returns @instance @cache.expects(:save) @indirection.find("/my/key", :ignore_cache => true) end it "should only look in the cache if the request specifies not to use the terminus" do @terminus.expects(:find).never @cache.expects(:find) @indirection.find("/my/key", :ignore_terminus => true) end it "should use a request to look in the cache for cached objects" do @cache.expects(:find).with { |r| r.method == :find and r.key == "/my/key" }.returns @instance @cache.stubs(:save) @indirection.find("/my/key") end it "should return the cached object if it is not expired" do @instance.stubs(:expired?).returns false @cache.stubs(:find).returns @instance @indirection.find("/my/key").should equal(@instance) end it "should not fail if the cache fails" do @terminus.stubs(:find).returns @instance @cache.expects(:find).raises ArgumentError @cache.stubs(:save) lambda { @indirection.find("/my/key") }.should_not raise_error end it "should look in the main terminus if the cache fails" do @terminus.expects(:find).returns @instance @cache.expects(:find).raises ArgumentError @cache.stubs(:save) @indirection.find("/my/key").should equal(@instance) end it "should send a debug log if it is using the cached object" do Puppet.expects(:debug) @cache.stubs(:find).returns @instance @indirection.find("/my/key") end it "should not return the cached object if it is expired" do @instance.stubs(:expired?).returns true @cache.stubs(:find).returns @instance @terminus.stubs(:find).returns nil @indirection.find("/my/key").should be_nil end it "should send an info log if it is using the cached object" do Puppet.expects(:info) @instance.stubs(:expired?).returns true @cache.stubs(:find).returns @instance @terminus.stubs(:find).returns nil @indirection.find("/my/key") end it "should cache any objects not retrieved from the cache" do @cache.expects(:find).returns nil @terminus.expects(:find).returns(@instance) @cache.expects(:save) @indirection.find("/my/key") end it "should use a request to look in the cache for cached objects" do @cache.expects(:find).with { |r| r.method == :find and r.key == "/my/key" }.returns nil @terminus.stubs(:find).returns(@instance) @cache.stubs(:save) @indirection.find("/my/key") end it "should cache the instance using a request with the instance set to the cached object" do @cache.stubs(:find).returns nil @terminus.stubs(:find).returns(@instance) @cache.expects(:save).with { |r| r.method == :save and r.instance == @instance } @indirection.find("/my/key") end it "should send an info log that the object is being cached" do @cache.stubs(:find).returns nil @terminus.stubs(:find).returns(@instance) @cache.stubs(:save) Puppet.expects(:info) @indirection.find("/my/key") end end end describe "and storing a model instance" do before { @method = :save } it_should_behave_like "Indirection Delegator" it_should_behave_like "Delegation Authorizer" it "should return the result of the save" do @terminus.stubs(:save).returns "foo" @indirection.save(@instance).should == "foo" end describe "when caching is enabled" do before do @indirection.cache_class = :cache_terminus @cache_class.stubs(:new).returns(@cache) @instance.stubs(:expired?).returns false end it "should return the result of saving to the terminus" do request = stub 'request', :instance => @instance, :node => nil @indirection.expects(:request).returns request @cache.stubs(:save) @terminus.stubs(:save).returns @instance @indirection.save(@instance).should equal(@instance) end it "should use a request to save the object to the cache" do request = stub 'request', :instance => @instance, :node => nil @indirection.expects(:request).returns request @cache.expects(:save).with(request) @terminus.stubs(:save) @indirection.save(@instance) end it "should not save to the cache if the normal save fails" do request = stub 'request', :instance => @instance, :node => nil @indirection.expects(:request).returns request @cache.expects(:save).never @terminus.expects(:save).raises "eh" lambda { @indirection.save(@instance) }.should raise_error end end end describe "and removing a model instance" do before { @method = :destroy } it_should_behave_like "Indirection Delegator" it_should_behave_like "Delegation Authorizer" it "should return the result of removing the instance" do @terminus.stubs(:destroy).returns "yayness" @indirection.destroy("/my/key").should == "yayness" end describe "when caching is enabled" do before do @indirection.cache_class = :cache_terminus @cache_class.expects(:new).returns(@cache) @instance.stubs(:expired?).returns false end it "should use a request instance to search in and remove objects from the cache" do destroy = stub 'destroy_request', :key => "/my/key", :node => nil find = stub 'destroy_request', :key => "/my/key", :node => nil @indirection.expects(:request).with(:destroy, "/my/key").returns destroy @indirection.expects(:request).with(:find, "/my/key").returns find cached = mock 'cache' @cache.expects(:find).with(find).returns cached @cache.expects(:destroy).with(destroy) @terminus.stubs(:destroy) @indirection.destroy("/my/key") end end end describe "and searching for multiple model instances" do before { @method = :search } it_should_behave_like "Indirection Delegator" it_should_behave_like "Delegation Authorizer" it "should set the expiration date on any instances without one set" do @terminus.stubs(:search).returns([@instance]) @indirection.expects(:expiration).returns :yay @instance.expects(:expiration).returns(nil) @instance.expects(:expiration=).with(:yay) @indirection.search("/my/key") end it "should not override an already-set expiration date on returned instances" do @terminus.stubs(:search).returns([@instance]) @indirection.expects(:expiration).never @instance.expects(:expiration).returns(:yay) @instance.expects(:expiration=).never @indirection.search("/my/key") end it "should return the results of searching in the terminus" do @terminus.expects(:search).returns([@instance]) @indirection.search("/my/key").should == [@instance] end end describe "and expiring a model instance" do describe "when caching is not enabled" do it "should do nothing" do @cache_class.expects(:new).never @indirection.expire("/my/key") end end describe "when caching is enabled" do before do @indirection.cache_class = :cache_terminus @cache_class.expects(:new).returns(@cache) @instance.stubs(:expired?).returns false @cached = stub 'cached', :expiration= => nil, :name => "/my/key" end it "should use a request to find within the cache" do @cache.expects(:find).with { |r| r.is_a?(Puppet::Indirector::Request) and r.method == :find } @indirection.expire("/my/key") end it "should do nothing if no such instance is cached" do @cache.expects(:find).returns nil @indirection.expire("/my/key") end it "should log when expiring a found instance" do @cache.expects(:find).returns @cached @cache.stubs(:save) Puppet.expects(:info) @indirection.expire("/my/key") end describe "and the terminus supports removal of cache items with destroy" do it "should destroy the cached instance" do @cache.expects(:find).returns @cached @cache.expects(:destroy).with { |r| r.method == :destroy and r.key == "/my/key" } @cache.expects(:save).never @indirection.expire("/my/key") end end describe "and the terminus does not support removal of cache items with destroy" do it "should set the cached instance's expiration to a time in the past" do @cache.expects(:find).returns @cached @cache.stubs(:save) @cached.expects(:expiration=).with { |t| t < Time.now } @indirection.expire("/my/key") end it "should save the now expired instance back into the cache" do @cache.expects(:find).returns @cached @cached.expects(:expiration=).with { |t| t < Time.now } @cache.expects(:save) @indirection.expire("/my/key") end it "should use a request to save the expired resource to the cache" do @cache.expects(:find).returns @cached @cached.expects(:expiration=).with { |t| t < Time.now } @cache.expects(:save).with { |r| r.is_a?(Puppet::Indirector::Request) and r.instance == @cached and r.method == :save }.returns(@cached) @indirection.expire("/my/key") end end end end after :each do @indirection.delete Puppet::Util::Cacher.expire end end describe "when managing indirection instances" do it "should allow an indirection to be retrieved by name" do @indirection = Puppet::Indirector::Indirection.new(mock('model'), :test) Puppet::Indirector::Indirection.instance(:test).should equal(@indirection) end it "should return nil when the named indirection has not been created" do Puppet::Indirector::Indirection.instance(:test).should be_nil end it "should allow an indirection's model to be retrieved by name" do mock_model = mock('model') @indirection = Puppet::Indirector::Indirection.new(mock_model, :test) Puppet::Indirector::Indirection.model(:test).should equal(mock_model) end it "should return nil when no model matches the requested name" do Puppet::Indirector::Indirection.model(:test).should be_nil end after do @indirection.delete if defined? @indirection end end describe "when routing to the correct the terminus class" do before do @indirection = Puppet::Indirector::Indirection.new(mock('model'), :test) @terminus = mock 'terminus' @terminus_class = stub 'terminus class', :new => @terminus Puppet::Indirector::Terminus.stubs(:terminus_class).with(:test, :default).returns(@terminus_class) end it "should fail if no terminus class can be picked" do proc { @indirection.terminus_class }.should raise_error(Puppet::DevError) end it "should choose the default terminus class if one is specified" do @indirection.terminus_class = :default @indirection.terminus_class.should equal(:default) end it "should use the provided Puppet setting if told to do so" do Puppet::Indirector::Terminus.stubs(:terminus_class).with(:test, :my_terminus).returns(mock("terminus_class2")) Puppet.settings.expects(:value).with(:my_setting).returns("my_terminus") @indirection.terminus_setting = :my_setting @indirection.terminus_class.should equal(:my_terminus) end it "should fail if the provided terminus class is not valid" do Puppet::Indirector::Terminus.stubs(:terminus_class).with(:test, :nosuchclass).returns(nil) proc { @indirection.terminus_class = :nosuchclass }.should raise_error(ArgumentError) end after do @indirection.delete if defined? @indirection end end describe "when specifying the terminus class to use" do before do @indirection = Puppet::Indirector::Indirection.new(mock('model'), :test) @terminus = mock 'terminus' @terminus_class = stub 'terminus class', :new => @terminus end it "should allow specification of a terminus type" do @indirection.should respond_to(:terminus_class=) end it "should fail to redirect if no terminus type has been specified" do proc { @indirection.find("blah") }.should raise_error(Puppet::DevError) end it "should fail when the terminus class name is an empty string" do proc { @indirection.terminus_class = "" }.should raise_error(ArgumentError) end it "should fail when the terminus class name is nil" do proc { @indirection.terminus_class = nil }.should raise_error(ArgumentError) end it "should fail when the specified terminus class cannot be found" do Puppet::Indirector::Terminus.expects(:terminus_class).with(:test, :foo).returns(nil) proc { @indirection.terminus_class = :foo }.should raise_error(ArgumentError) end it "should select the specified terminus class if a terminus class name is provided" do Puppet::Indirector::Terminus.expects(:terminus_class).with(:test, :foo).returns(@terminus_class) @indirection.terminus(:foo).should equal(@terminus) end it "should use the configured terminus class if no terminus name is specified" do Puppet::Indirector::Terminus.stubs(:terminus_class).with(:test, :foo).returns(@terminus_class) @indirection.terminus_class = :foo @indirection.terminus().should equal(@terminus) end after do @indirection.delete if defined? @indirection end end describe "when managing terminus instances" do before do @indirection = Puppet::Indirector::Indirection.new(mock('model'), :test) @terminus = mock 'terminus' @terminus_class = mock 'terminus class' Puppet::Indirector::Terminus.stubs(:terminus_class).with(:test, :foo).returns(@terminus_class) end it "should create an instance of the chosen terminus class" do @terminus_class.stubs(:new).returns(@terminus) @indirection.terminus(:foo).should equal(@terminus) end # Make sure it caches the terminus. it "should return the same terminus instance each time for a given name" do @terminus_class.stubs(:new).returns(@terminus) @indirection.terminus(:foo).should equal(@terminus) @indirection.terminus(:foo).should equal(@terminus) end it "should not create a terminus instance until one is actually needed" do Puppet::Indirector.expects(:terminus).never indirection = Puppet::Indirector::Indirection.new(mock('model'), :lazytest) end after do @indirection.delete end end describe "when deciding whether to cache" do before do @indirection = Puppet::Indirector::Indirection.new(mock('model'), :test) @terminus = mock 'terminus' @terminus_class = mock 'terminus class' @terminus_class.stubs(:new).returns(@terminus) Puppet::Indirector::Terminus.stubs(:terminus_class).with(:test, :foo).returns(@terminus_class) @indirection.terminus_class = :foo end it "should provide a method for setting the cache terminus class" do @indirection.should respond_to(:cache_class=) end it "should fail to cache if no cache type has been specified" do proc { @indirection.cache }.should raise_error(Puppet::DevError) end it "should fail to set the cache class when the cache class name is an empty string" do proc { @indirection.cache_class = "" }.should raise_error(ArgumentError) end it "should allow resetting the cache_class to nil" do @indirection.cache_class = nil @indirection.cache_class.should be_nil end it "should fail to set the cache class when the specified cache class cannot be found" do Puppet::Indirector::Terminus.expects(:terminus_class).with(:test, :foo).returns(nil) proc { @indirection.cache_class = :foo }.should raise_error(ArgumentError) end after do @indirection.delete end end describe "when using a cache" do before :each do Puppet.settings.stubs(:value).with("test_terminus").returns("test_terminus") @terminus_class = mock 'terminus_class' @terminus = mock 'terminus' @terminus_class.stubs(:new).returns(@terminus) @cache = mock 'cache' @cache_class = mock 'cache_class' Puppet::Indirector::Terminus.stubs(:terminus_class).with(:test, :cache_terminus).returns(@cache_class) Puppet::Indirector::Terminus.stubs(:terminus_class).with(:test, :test_terminus).returns(@terminus_class) @indirection = Puppet::Indirector::Indirection.new(mock('model'), :test) @indirection.terminus_class = :test_terminus end describe "and managing the cache terminus" do it "should not create a cache terminus at initialization" do # This is weird, because all of the code is in the setup. If we got # new() called on the cache class, we'd get an exception here. end it "should reuse the cache terminus" do @cache_class.expects(:new).returns(@cache) Puppet.settings.stubs(:value).with("test_cache").returns("cache_terminus") @indirection.cache_class = :cache_terminus @indirection.cache.should equal(@cache) @indirection.cache.should equal(@cache) end end describe "and saving" do end describe "and finding" do end after :each do @indirection.delete end end end diff --git a/spec/unit/indirector/request.rb b/spec/unit/indirector/request.rb index 848a608a4..b885779ed 100755 --- a/spec/unit/indirector/request.rb +++ b/spec/unit/indirector/request.rb @@ -1,308 +1,308 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/indirector/request' describe Puppet::Indirector::Request do describe "when initializing" do it "should require an indirection name, a key, and a method" do lambda { Puppet::Indirector::Request.new }.should raise_error(ArgumentError) end it "should always convert the indirection name to a symbol" do Puppet::Indirector::Request.new("ind", :method, "mykey").indirection_name.should == :ind end it "should use provided value as the key if it is a string" do Puppet::Indirector::Request.new(:ind, :method, "mykey").key.should == "mykey" end it "should use provided value as the key if it is a symbol" do Puppet::Indirector::Request.new(:ind, :method, :mykey).key.should == :mykey end it "should use the name of the provided instance as its key if an instance is provided as the key instead of a string" do instance = mock 'instance', :name => "mykey" request = Puppet::Indirector::Request.new(:ind, :method, instance) request.key.should == "mykey" request.instance.should equal(instance) end it "should support options specified as a hash" do lambda { Puppet::Indirector::Request.new(:ind, :method, :key, :one => :two) }.should_not raise_error(ArgumentError) end it "should support nil options" do lambda { Puppet::Indirector::Request.new(:ind, :method, :key, nil) }.should_not raise_error(ArgumentError) end it "should support unspecified options" do lambda { Puppet::Indirector::Request.new(:ind, :method, :key) }.should_not raise_error(ArgumentError) end it "should fail if options are specified as anything other than nil or a hash" do lambda { Puppet::Indirector::Request.new(:ind, :method, :key, [:one, :two]) }.should raise_error(ArgumentError) end it "should use an empty options hash if nil was provided" do Puppet::Indirector::Request.new(:ind, :method, :key, nil).options.should == {} end it "should default to a nil node" do Puppet::Indirector::Request.new(:ind, :method, :key, nil).node.should be_nil end it "should set its node attribute if provided in the options" do Puppet::Indirector::Request.new(:ind, :method, :key, :node => "foo.com").node.should == "foo.com" end it "should default to a nil ip" do Puppet::Indirector::Request.new(:ind, :method, :key, nil).ip.should be_nil end it "should set its ip attribute if provided in the options" do Puppet::Indirector::Request.new(:ind, :method, :key, :ip => "192.168.0.1").ip.should == "192.168.0.1" end it "should default to being unauthenticated" do Puppet::Indirector::Request.new(:ind, :method, :key, nil).should_not be_authenticated end it "should set be marked authenticated if configured in the options" do Puppet::Indirector::Request.new(:ind, :method, :key, :authenticated => "eh").should be_authenticated end it "should keep its options as a hash even if a node is specified" do Puppet::Indirector::Request.new(:ind, :method, :key, :node => "eh").options.should be_instance_of(Hash) end it "should keep its options as a hash even if another option is specified" do Puppet::Indirector::Request.new(:ind, :method, :key, :foo => "bar").options.should be_instance_of(Hash) end it "should treat options other than :ip, :node, and :authenticated as options rather than attributes" do Puppet::Indirector::Request.new(:ind, :method, :key, :server => "bar").options[:server].should == "bar" end it "should normalize options to use symbols as keys" do Puppet::Indirector::Request.new(:ind, :method, :key, "foo" => "bar").options[:foo].should == "bar" end describe "and the request key is a URI" do describe "and the URI is a 'file' URI" do before do @request = Puppet::Indirector::Request.new(:ind, :method, "file:///my/file with spaces") end it "should set the request key to the unescaped full file path" do @request.key.should == "/my/file with spaces" end it "should not set the protocol" do @request.protocol.should be_nil end it "should not set the port" do @request.port.should be_nil end it "should not set the server" do @request.server.should be_nil end end it "should set the protocol to the URI scheme" do Puppet::Indirector::Request.new(:ind, :method, "http://host/stuff").protocol.should == "http" end it "should set the server if a server is provided" do Puppet::Indirector::Request.new(:ind, :method, "http://host/stuff").server.should == "host" end it "should set the server and port if both are provided" do Puppet::Indirector::Request.new(:ind, :method, "http://host:543/stuff").port.should == 543 end it "should default to the masterport if the URI scheme is 'puppet'" do Puppet.settings.expects(:value).with(:masterport).returns "321" Puppet::Indirector::Request.new(:ind, :method, "puppet://host/stuff").port.should == 321 end it "should use the provided port if the URI scheme is not 'puppet'" do Puppet::Indirector::Request.new(:ind, :method, "http://host/stuff").port.should == 80 end - it "should set the request key to the unescaped unqualified path from the URI" do - Puppet::Indirector::Request.new(:ind, :method, "http:///stuff with spaces").key.should == "stuff with spaces" + it "should set the request key to the unescaped key part path from the URI" do + Puppet::Indirector::Request.new(:ind, :method, "http://host/environment/terminus/stuff with spaces").key.should == "stuff with spaces" end it "should set the :uri attribute to the full URI" do Puppet::Indirector::Request.new(:ind, :method, "http:///stuff").uri.should == "http:///stuff" end end it "should allow indication that it should not read a cached instance" do Puppet::Indirector::Request.new(:ind, :method, :key, :ignore_cache => true).should be_ignore_cache end it "should default to not ignoring the cache" do Puppet::Indirector::Request.new(:ind, :method, :key).should_not be_ignore_cache end it "should allow indication that it should not not read an instance from the terminus" do Puppet::Indirector::Request.new(:ind, :method, :key, :ignore_terminus => true).should be_ignore_terminus end it "should default to not ignoring the terminus" do Puppet::Indirector::Request.new(:ind, :method, :key).should_not be_ignore_terminus end end it "should look use the Indirection class to return the appropriate indirection" do ind = mock 'indirection' Puppet::Indirector::Indirection.expects(:instance).with(:myind).returns ind request = Puppet::Indirector::Request.new(:myind, :method, :key) request.indirection.should equal(ind) end it "should use its indirection to look up the appropriate model" do ind = mock 'indirection' Puppet::Indirector::Indirection.expects(:instance).with(:myind).returns ind request = Puppet::Indirector::Request.new(:myind, :method, :key) ind.expects(:model).returns "mymodel" request.model.should == "mymodel" end it "should fail intelligently when asked to find a model but the indirection cannot be found" do Puppet::Indirector::Indirection.expects(:instance).with(:myind).returns nil request = Puppet::Indirector::Request.new(:myind, :method, :key) lambda { request.model }.should raise_error(ArgumentError) end it "should have a method for determining if the request is plural or singular" do Puppet::Indirector::Request.new(:myind, :method, :key).should respond_to(:plural?) end it "should be considered plural if the method is 'search'" do Puppet::Indirector::Request.new(:myind, :search, :key).should be_plural end it "should not be considered plural if the method is not 'search'" do Puppet::Indirector::Request.new(:myind, :find, :key).should_not be_plural end it "should use its uri, if it has one, as its string representation" do Puppet::Indirector::Request.new(:myind, :find, "foo://bar/baz").to_s.should == "foo://bar/baz" end it "should use its indirection name and key, if it has no uri, as its string representation" do Puppet::Indirector::Request.new(:myind, :find, "key") == "/myind/key" end it "should be able to return the URI-escaped key" do Puppet::Indirector::Request.new(:myind, :find, "my key").escaped_key.should == URI.escape("my key") end it "should have an environment accessor" do Puppet::Indirector::Request.new(:myind, :find, "my key", :environment => "foo").should respond_to(:environment) end it "should set its environment to an environment instance when a string is specified as its environment" do Puppet::Indirector::Request.new(:myind, :find, "my key", :environment => "foo").environment.should == Puppet::Node::Environment.new("foo") end it "should use any passed in environment instances as its environment" do env = Puppet::Node::Environment.new("foo") Puppet::Indirector::Request.new(:myind, :find, "my key", :environment => env).environment.should equal(env) end it "should use the default environment when none is provided" do Puppet::Indirector::Request.new(:myind, :find, "my key" ).environment.should equal(Puppet::Node::Environment.new) end it "should support converting its options to a hash" do Puppet::Indirector::Request.new(:myind, :find, "my key" ).should respond_to(:to_hash) end it "should include all of its attributes when its options are converted to a hash" do Puppet::Indirector::Request.new(:myind, :find, "my key", :node => 'foo').to_hash[:node].should == 'foo' end describe "when building a query string from its options" do before do @request = Puppet::Indirector::Request.new(:myind, :find, "my key") end it "should return an empty query string if there are no options" do @request.stubs(:options).returns nil @request.query_string.should == "" end it "should return an empty query string if the options are empty" do @request.stubs(:options).returns({}) @request.query_string.should == "" end it "should prefix the query string with '?'" do @request.stubs(:options).returns(:one => "two") @request.query_string.should =~ /^\?/ end it "should include all options in the query string, separated by '&'" do @request.stubs(:options).returns(:one => "two", :three => "four") @request.query_string.sub(/^\?/, '').split("&").sort.should == %w{one=two three=four}.sort end it "should ignore nil options" do @request.stubs(:options).returns(:one => "two", :three => nil) @request.query_string.should_not be_include("three") end it "should convert 'true' option values into strings" do @request.stubs(:options).returns(:one => true) @request.query_string.should == "?one=true" end it "should convert 'false' option values into strings" do @request.stubs(:options).returns(:one => false) @request.query_string.should == "?one=false" end it "should convert to a string all option values that are integers" do @request.stubs(:options).returns(:one => 50) @request.query_string.should == "?one=50" end it "should convert to a string all option values that are floating point numbers" do @request.stubs(:options).returns(:one => 1.2) @request.query_string.should == "?one=1.2" end it "should CGI-escape all option values that are strings" do escaping = CGI.escape("one two") @request.stubs(:options).returns(:one => "one two") @request.query_string.should == "?one=#{escaping}" end it "should YAML-dump and CGI-escape arrays" do escaping = CGI.escape(YAML.dump(%w{one two})) @request.stubs(:options).returns(:one => %w{one two}) @request.query_string.should == "?one=#{escaping}" end it "should convert to a string and CGI-escape all option values that are symbols" do escaping = CGI.escape("sym bol") @request.stubs(:options).returns(:one => :"sym bol") @request.query_string.should == "?one=#{escaping}" end it "should fail if options other than booleans or strings are provided" do @request.stubs(:options).returns(:one => {:one => :two}) lambda { @request.query_string }.should raise_error(ArgumentError) end end end diff --git a/spec/unit/network/client/dipper.rb b/spec/unit/network/client/dipper.rb index d1631fbb5..7d8b3da08 100755 --- a/spec/unit/network/client/dipper.rb +++ b/spec/unit/network/client/dipper.rb @@ -1,16 +1,110 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' -describe Puppet::Network::Client.dipper do +require 'puppet/file_bucket/dipper' +describe Puppet::FileBucket::Dipper do it "should fail in an informative way when there are failures backing up to the server" do - FileTest.stubs(:exists?).returns true + File.stubs(:exists?).returns true File.stubs(:read).returns "content" - @dipper = Puppet::Network::Client::Dipper.new(:Path => "/my/bucket") + @dipper = Puppet::FileBucket::Dipper.new(:Path => "/my/bucket") - @dipper.driver.expects(:addfile).raises ArgumentError + filemock = stub "bucketfile" + Puppet::FileBucket::File.stubs(:new).returns(filemock) + filemock.expects(:name).returns "name" + filemock.expects(:save).raises ArgumentError lambda { @dipper.backup("/my/file") }.should raise_error(Puppet::Error) end + + it "should backup files to a local bucket" do + @dipper = Puppet::FileBucket::Dipper.new( + :Path => "/my/bucket" + ) + + File.stubs(:exists?).returns true + File.stubs(:read).with("/my/file").returns "my contents" + + req = stub "req" + bucketfile = stub "bucketfile" + bucketfile.stubs(:name).returns('md5/DIGEST123') + bucketfile.stubs(:checksum_data).returns("DIGEST123") + bucketfile.expects(:save).with(req) + + Puppet::FileBucket::File.stubs(:new).with( + "my contents", + :bucket_path => '/my/bucket', + :path => '/my/file' + ).returns(bucketfile) + + Puppet::Indirector::Request.stubs(:new).with(:file_bucket_file, :save, 'md5/DIGEST123').returns(req) + + @dipper.backup("/my/file").should == "DIGEST123" + end + + it "should retrieve files from a local bucket" do + @dipper = Puppet::FileBucket::Dipper.new( + :Path => "/my/bucket" + ) + + File.stubs(:exists?).returns true + File.stubs(:read).with("/my/file").returns "my contents" + + bucketfile = stub "bucketfile" + bucketfile.stubs(:to_s).returns "Content" + + Puppet::FileBucket::File.expects(:find).with( + 'md5/DIGEST123' + ).returns(bucketfile) + + @dipper.getfile("DIGEST123").should == "Content" + end + + it "should backup files to a remote server" do + @dipper = Puppet::FileBucket::Dipper.new( + :Server => "puppetmaster", + :Port => "31337" + ) + + File.stubs(:exists?).returns true + File.stubs(:read).with("/my/file").returns "my contents" + + req = stub "req" + bucketfile = stub "bucketfile" + bucketfile.stubs(:name).returns('md5/DIGEST123') + bucketfile.stubs(:checksum_data).returns("DIGEST123") + bucketfile.expects(:save).with(req) + + Puppet::FileBucket::File.stubs(:new).with( + "my contents", + :bucket_path => nil, + :path => '/my/file' + ).returns(bucketfile) + + Puppet::Indirector::Request.stubs(:new).with(:file_bucket_file, :save, 'https://puppetmaster:31337/production/file_bucket_file/md5/DIGEST123').returns(req) + + @dipper.backup("/my/file").should == "DIGEST123" + end + + it "should retrieve files from a remote server" do + @dipper = Puppet::FileBucket::Dipper.new( + :Server => "puppetmaster", + :Port => "31337" + ) + + File.stubs(:exists?).returns true + File.stubs(:read).with("/my/file").returns "my contents" + + bucketfile = stub "bucketfile" + bucketfile.stubs(:to_s).returns "Content" + + Puppet::FileBucket::File.expects(:find).with( + 'https://puppetmaster:31337/production/file_bucket_file/md5/DIGEST123' + ).returns(bucketfile) + + @dipper.getfile("DIGEST123").should == "Content" + end + + end diff --git a/spec/unit/network/http/handler.rb b/spec/unit/network/http/handler.rb index 559812159..2218a0a78 100755 --- a/spec/unit/network/http/handler.rb +++ b/spec/unit/network/http/handler.rb @@ -1,458 +1,455 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' require 'puppet/network/http/handler' require 'puppet/network/rest_authorization' class HttpHandled include Puppet::Network::HTTP::Handler end describe Puppet::Network::HTTP::Handler do before do @handler = HttpHandled.new end it "should include the v1 REST API" do Puppet::Network::HTTP::Handler.ancestors.should be_include(Puppet::Network::HTTP::API::V1) end it "should include the Rest Authorization system" do Puppet::Network::HTTP::Handler.ancestors.should be_include(Puppet::Network::RestAuthorization) end it "should have a method for initializing" do @handler.should respond_to(:initialize_for_puppet) end describe "when initializing" do it "should fail when no server type has been provided" do lambda { @handler.initialize_for_puppet }.should raise_error(ArgumentError) end it "should set server type" do @handler.initialize_for_puppet("foo") @handler.server.should == "foo" end end it "should be able to process requests" do @handler.should respond_to(:process) end describe "when processing a request" do before do @request = stub('http request') @request.stubs(:[]).returns "foo" @response = stub('http response') @model_class = stub('indirected model class') @result = stub 'result', :render => "mytext" @handler.stubs(:check_authorization) stub_server_interface end # Stub out the interface we require our including classes to # implement. def stub_server_interface @handler.stubs(:accept_header ).returns "format_one,format_two" @handler.stubs(:content_type_header).returns "text/yaml" @handler.stubs(:set_content_type ).returns "my_result" @handler.stubs(:set_response ).returns "my_result" @handler.stubs(:path ).returns "/my_handler/my_result" @handler.stubs(:http_method ).returns("GET") @handler.stubs(:params ).returns({}) @handler.stubs(:content_type ).returns("text/plain") end it "should create an indirection request from the path, parameters, and http method" do @handler.expects(:path).with(@request).returns "mypath" @handler.expects(:http_method).with(@request).returns "mymethod" @handler.expects(:params).with(@request).returns "myparams" @handler.expects(:uri2indirection).with("mymethod", "mypath", "myparams").returns stub("request", :method => :find) @handler.stubs(:do_find) @handler.process(@request, @response) end it "should call the 'do' method associated with the indirection method" do request = stub 'request' @handler.expects(:uri2indirection).returns request request.expects(:method).returns "mymethod" @handler.expects(:do_mymethod).with(request, @request, @response) @handler.process(@request, @response) end it "should delegate authorization to the RestAuthorization layer" do request = stub 'request' @handler.expects(:uri2indirection).returns request request.expects(:method).returns "mymethod" @handler.expects(:do_mymethod).with(request, @request, @response) @handler.expects(:check_authorization).with(request) @handler.process(@request, @response) end it "should return 403 if the request is not authorized" do request = stub 'request' @handler.expects(:uri2indirection).returns request @handler.expects(:do_mymethod).never @handler.expects(:check_authorization).with(request).raises(Puppet::Network::AuthorizationError.new("forbindden")) @handler.expects(:set_response).with { |response, body, status| status == 403 } @handler.process(@request, @response) end it "should serialize a controller exception when an exception is thrown while finding the model instance" do @handler.expects(:uri2indirection).returns stub("request", :method => :find) @handler.expects(:do_find).raises(ArgumentError, "The exception") @handler.expects(:set_response).with { |response, body, status| body == "The exception" and status == 400 } @handler.process(@request, @response) end it "should set the format to text/plain when serializing an exception" do @handler.expects(:set_content_type).with(@response, "text/plain") @handler.do_exception(@response, "A test", 404) end it "should raise an error if the request is formatted in an unknown format" do @handler.stubs(:content_type_header).returns "unknown format" lambda { @handler.request_format(@request) }.should raise_error end it "should still find the correct format if content type contains charset information" do @handler.stubs(:content_type_header).returns "text/plain; charset=UTF-8" @handler.request_format(@request).should == "s" end describe "when finding a model instance" do before do @irequest = stub 'indirection_request', :method => :find, :indirection_name => "my_handler", :to_hash => {}, :key => "my_result", :model => @model_class @model_class.stubs(:find).returns @result @format = stub 'format', :suitable? => true, :mime => "text/format", :name => "format" Puppet::Network::FormatHandler.stubs(:format).returns @format @oneformat = stub 'one', :suitable? => true, :mime => "text/one", :name => "one" Puppet::Network::FormatHandler.stubs(:format).with("one").returns @oneformat end it "should use the indirection request to find the model class" do @irequest.expects(:model).returns @model_class @handler.do_find(@irequest, @request, @response) end it "should use the escaped request key" do @model_class.expects(:find).with do |key, args| key == "my_result" end.returns @result @handler.do_find(@irequest, @request, @response) end it "should use a common method for determining the request parameters" do @irequest.stubs(:to_hash).returns(:foo => :baz, :bar => :xyzzy) @model_class.expects(:find).with do |key, args| args[:foo] == :baz and args[:bar] == :xyzzy end.returns @result @handler.do_find(@irequest, @request, @response) end it "should set the content type to the first format specified in the accept header" do @handler.expects(:accept_header).with(@request).returns "one,two" @handler.expects(:set_content_type).with(@response, @oneformat) @handler.do_find(@irequest, @request, @response) end it "should fail if no accept header is provided" do @handler.expects(:accept_header).with(@request).returns nil lambda { @handler.do_find(@irequest, @request, @response) }.should raise_error(ArgumentError) end it "should fail if the accept header does not contain a valid format" do @handler.expects(:accept_header).with(@request).returns "" lambda { @handler.do_find(@irequest, @request, @response) }.should raise_error(RuntimeError) end it "should not use an unsuitable format" do @handler.expects(:accept_header).with(@request).returns "foo,bar" foo = mock 'foo', :suitable? => false bar = mock 'bar', :suitable? => true Puppet::Network::FormatHandler.expects(:format).with("foo").returns foo Puppet::Network::FormatHandler.expects(:format).with("bar").returns bar @handler.expects(:set_content_type).with(@response, bar) # the suitable one @handler.do_find(@irequest, @request, @response) end it "should render the result using the first format specified in the accept header" do @handler.expects(:accept_header).with(@request).returns "one,two" @result.expects(:render).with(@oneformat) @handler.do_find(@irequest, @request, @response) end it "should use the default status when a model find call succeeds" do @handler.expects(:set_response).with { |response, body, status| status.nil? } @handler.do_find(@irequest, @request, @response) end it "should return a serialized object when a model find call succeeds" do @model_instance = stub('model instance') @model_instance.expects(:render).returns "my_rendered_object" @handler.expects(:set_response).with { |response, body, status| body == "my_rendered_object" } @model_class.stubs(:find).returns(@model_instance) @handler.do_find(@irequest, @request, @response) end it "should return a 404 when no model instance can be found" do @model_class.stubs(:name).returns "my name" @handler.expects(:set_response).with { |response, body, status| status == 404 } @model_class.stubs(:find).returns(nil) @handler.do_find(@irequest, @request, @response) end it "should write a log message when no model instance can be found" do @model_class.stubs(:name).returns "my name" @model_class.stubs(:find).returns(nil) Puppet.expects(:info).with("Could not find my_handler for 'my_result'") @handler.do_find(@irequest, @request, @response) end it "should serialize the result in with the appropriate format" do @model_instance = stub('model instance') @handler.expects(:format_to_use).returns(@oneformat) @model_instance.expects(:render).with(@oneformat).returns "my_rendered_object" @model_class.stubs(:find).returns(@model_instance) @handler.do_find(@irequest, @request, @response) end end describe "when searching for model instances" do before do @irequest = stub 'indirection_request', :method => :find, :indirection_name => "my_handler", :to_hash => {}, :key => "key", :model => @model_class @result1 = mock 'result1' @result2 = mock 'results' @result = [@result1, @result2] @model_class.stubs(:render_multiple).returns "my rendered instances" @model_class.stubs(:search).returns(@result) @format = stub 'format', :suitable? => true, :mime => "text/format", :name => "format" Puppet::Network::FormatHandler.stubs(:format).returns @format @oneformat = stub 'one', :suitable? => true, :mime => "text/one", :name => "one" Puppet::Network::FormatHandler.stubs(:format).with("one").returns @oneformat end it "should use the indirection request to find the model" do @irequest.expects(:model).returns @model_class @handler.do_search(@irequest, @request, @response) end it "should use a common method for determining the request parameters" do @irequest.stubs(:to_hash).returns(:foo => :baz, :bar => :xyzzy) @model_class.expects(:search).with do |key, args| args[:foo] == :baz and args[:bar] == :xyzzy end.returns @result @handler.do_search(@irequest, @request, @response) end it "should use the default status when a model search call succeeds" do @model_class.stubs(:search).returns(@result) @handler.do_search(@irequest, @request, @response) end it "should set the content type to the first format returned by the accept header" do @handler.expects(:accept_header).with(@request).returns "one,two" @handler.expects(:set_content_type).with(@response, @oneformat) @handler.do_search(@irequest, @request, @response) end it "should return a list of serialized objects when a model search call succeeds" do @handler.expects(:accept_header).with(@request).returns "one,two" @model_class.stubs(:search).returns(@result) @model_class.expects(:render_multiple).with(@oneformat, @result).returns "my rendered instances" @handler.expects(:set_response).with { |response, data| data == "my rendered instances" } @handler.do_search(@irequest, @request, @response) end it "should return a 404 when searching returns an empty array" do @model_class.stubs(:name).returns "my name" @handler.expects(:set_response).with { |response, body, status| status == 404 } @model_class.stubs(:search).returns([]) @handler.do_search(@irequest, @request, @response) end it "should return a 404 when searching returns nil" do @model_class.stubs(:name).returns "my name" @handler.expects(:set_response).with { |response, body, status| status == 404 } @model_class.stubs(:search).returns([]) @handler.do_search(@irequest, @request, @response) end end describe "when destroying a model instance" do before do @irequest = stub 'indirection_request', :method => :destroy, :indirection_name => "my_handler", :to_hash => {}, :key => "key", :model => @model_class @result = stub 'result', :render => "the result" @model_class.stubs(:destroy).returns @result end it "should use the indirection request to find the model" do @irequest.expects(:model).returns @model_class @handler.do_destroy(@irequest, @request, @response) end it "should use the escaped request key to destroy the instance in the model" do @irequest.expects(:key).returns "foo bar" @model_class.expects(:destroy).with do |key, args| key == "foo bar" end @handler.do_destroy(@irequest, @request, @response) end it "should use a common method for determining the request parameters" do @irequest.stubs(:to_hash).returns(:foo => :baz, :bar => :xyzzy) @model_class.expects(:destroy).with do |key, args| args[:foo] == :baz and args[:bar] == :xyzzy end @handler.do_destroy(@irequest, @request, @response) end it "should use the default status code a model destroy call succeeds" do @handler.expects(:set_response).with { |response, body, status| status.nil? } @handler.do_destroy(@irequest, @request, @response) end it "should return a yaml-encoded result when a model destroy call succeeds" do @result = stub 'result', :to_yaml => "the result" @model_class.expects(:destroy).returns(@result) @handler.expects(:set_response).with { |response, body, status| body == "the result" } @handler.do_destroy(@irequest, @request, @response) end end describe "when saving a model instance" do before do @irequest = stub 'indirection_request', :method => :save, :indirection_name => "my_handler", :to_hash => {}, :key => "key", :model => @model_class @handler.stubs(:body).returns('my stuff') @handler.stubs(:content_type_header).returns("text/yaml") @result = stub 'result', :render => "the result" @model_instance = stub('indirected model instance', :save => true) @model_class.stubs(:convert_from).returns(@model_instance) @format = stub 'format', :suitable? => true, :name => "format", :mime => "text/format" Puppet::Network::FormatHandler.stubs(:format).returns @format @yamlformat = stub 'yaml', :suitable? => true, :name => "yaml", :mime => "text/yaml" Puppet::Network::FormatHandler.stubs(:format).with("yaml").returns @yamlformat end it "should use the indirection request to find the model" do @irequest.expects(:model).returns @model_class @handler.do_save(@irequest, @request, @response) end it "should use the 'body' hook to retrieve the body of the request" do @handler.expects(:body).returns "my body" @model_class.expects(:convert_from).with { |format, body| body == "my body" }.returns @model_instance @handler.do_save(@irequest, @request, @response) end it "should fail to save model if data is not specified" do @handler.stubs(:body).returns('') lambda { @handler.do_save(@irequest, @request, @response) }.should raise_error(ArgumentError) end it "should use a common method for determining the request parameters" do - @irequest.stubs(:to_hash).returns(:foo => :baz, :bar => :xyzzy) - @model_instance.expects(:save).with do |args| - args[:foo] == :baz and args[:bar] == :xyzzy - end + @model_instance.expects(:save).with(@irequest) @handler.do_save(@irequest, @request, @response) end it "should use the default status when a model save call succeeds" do @handler.expects(:set_response).with { |response, body, status| status.nil? } @handler.do_save(@irequest, @request, @response) end it "should return the yaml-serialized result when a model save call succeeds" do @model_instance.stubs(:save).returns(@model_instance) @model_instance.expects(:to_yaml).returns('foo') @handler.do_save(@irequest, @request, @response) end it "should set the content to yaml" do @handler.expects(:set_content_type).with(@response, @yamlformat) @handler.do_save(@irequest, @request, @response) end it "should use the content-type header to know the body format" do @handler.expects(:content_type_header).returns("text/format") Puppet::Network::FormatHandler.stubs(:mime).with("text/format").returns @format @model_class.expects(:convert_from).with { |format, body| format == "format" }.returns @model_instance @handler.do_save(@irequest, @request, @response) end end end describe "when resolving node" do it "should use a look-up from the ip address" do Resolv.expects(:getname).with("1.2.3.4").returns("host.domain.com") @handler.resolve_node(:ip => "1.2.3.4") end it "should return the look-up result" do Resolv.stubs(:getname).with("1.2.3.4").returns("host.domain.com") @handler.resolve_node(:ip => "1.2.3.4").should == "host.domain.com" end it "should return the ip address if resolving fails" do Resolv.stubs(:getname).with("1.2.3.4").raises(RuntimeError, "no such host") @handler.resolve_node(:ip => "1.2.3.4").should == "1.2.3.4" end end end diff --git a/spec/unit/other/checksum.rb b/spec/unit/other/checksum.rb deleted file mode 100755 index 6a63e833d..000000000 --- a/spec/unit/other/checksum.rb +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env ruby -# -# Created by Luke Kanies on 2007-9-22. -# Copyright (c) 2007. All rights reserved. - -require File.dirname(__FILE__) + '/../../spec_helper' - -require 'puppet/checksum' - -describe Puppet::Checksum do - it "should have 'Checksum' and the checksum algorithm when converted to a string" do - inst = Puppet::Checksum.new("whatever", "md5") - inst.to_s.should == "Checksum<{md5}#{inst.checksum}>" - end - - it "should convert algorithm names to symbols when they are set after checksum creation" do - sum = Puppet::Checksum.new("whatever") - sum.algorithm = "md5" - sum.algorithm.should == :md5 - end - - it "should return the checksum as the name" do - sum = Puppet::Checksum.new("whatever") - sum.checksum.should == sum.name - end -end - -describe Puppet::Checksum, " when initializing" do - before do - @content = "this is some content" - @sum = Puppet::Checksum.new(@content) - end - - it "should require content" do - proc { Puppet::Checksum.new(nil) }.should raise_error(ArgumentError) - end - - it "should set the content appropriately" do - @sum.content.should == @content - end - - it "should calculate the checksum" do - require 'digest/md5' - Digest::MD5.expects(:hexdigest).with(@content).returns(:mychecksum) - @sum.checksum.should == :mychecksum - end - - it "should not calculate the checksum until it is asked for" do - require 'digest/md5' - Digest::MD5.expects(:hexdigest).never - sum = Puppet::Checksum.new(@content, :md5) - end - - it "should remove the old checksum value if the algorithm is changed" do - Digest::MD5.expects(:hexdigest).with(@content).returns(:oldsum) - oldsum = @sum.checksum - @sum.algorithm = :sha1 - Digest::SHA1.expects(:hexdigest).with(@content).returns(:newsum) - @sum.checksum.should == :newsum - end - - it "should default to 'md5' as the checksum algorithm if the algorithm is not in the name" do - @sum.algorithm.should == :md5 - end - - it "should support specifying the algorithm during initialization" do - sum = Puppet::Checksum.new(@content, :sha1) - sum.algorithm.should == :sha1 - end - - it "should fail when an unsupported algorithm is used" do - proc { Puppet::Checksum.new(@content, :nope) }.should raise_error(ArgumentError) - end -end - -describe Puppet::Checksum, " when using back-ends" do - it "should redirect using Puppet::Indirector" do - Puppet::Indirector::Indirection.instance(:checksum).model.should equal(Puppet::Checksum) - end - - it "should have a :save instance method" do - Puppet::Checksum.new("mysum").should respond_to(:save) - end - - it "should respond to :find" do - Puppet::Checksum.should respond_to(:find) - end - - it "should respond to :destroy" do - Puppet::Checksum.should respond_to(:destroy) - end -end diff --git a/spec/unit/type/file.rb b/spec/unit/type/file.rb index afb050a1f..cedb1701d 100755 --- a/spec/unit/type/file.rb +++ b/spec/unit/type/file.rb @@ -1,842 +1,842 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::Type.type(:file) do before do Puppet.settings.stubs(:use) @path = Tempfile.new("puppetspec") pathname = @path.path @path.close!() @path = pathname @file = Puppet::Type::File.new(:name => @path) @catalog = Puppet::Resource::Catalog.new @file.catalog = @catalog end describe "#write" do it "should propagate failures encountered when renaming the temporary file" do File.stubs(:open) File.expects(:rename).raises ArgumentError file = Puppet::Type::File.new(:name => "/my/file", :backup => "puppet") lambda { file.write("something", :content) }.should raise_error(Puppet::Error) end describe "when validating the checksum" do before { @file.stubs(:validate_checksum?).returns(true) } it "should fail if the checksum property and content checksums do not match" do property = stub('checksum_property', :checktype => :md5, :md5 => 'checksum_a', :getsum => 'checksum_b') @file.stubs(:property).with(:checksum).returns(property) @file.stubs(:validate_checksum?).returns(true) lambda { @file.write "something", :NOTUSED }.should raise_error(Puppet::Error) end end describe "when not validating the checksum" do before { @file.stubs(:validate_checksum?).returns(false) } it "should not fail if the checksum property and content checksums do not match" do property = stub('checksum_property', :checktype => :md5, :md5 => 'checksum_a', :getsum => 'checksum_b') @file.stubs(:property).with(:checksum).returns(property) lambda { @file.write "something", :NOTUSED }.should_not raise_error(Puppet::Error) end end end it "should have a method for determining if the file is present" do @file.must respond_to(:exist?) end it "should be considered existent if it can be stat'ed" do @file.expects(:stat).returns mock('stat') @file.must be_exist end it "should be considered nonexistent if it can not be stat'ed" do @file.expects(:stat).returns nil @file.must_not be_exist end it "should have a method for determining if the file should be a normal file" do @file.must respond_to(:should_be_file?) end it "should be a file if :ensure is set to :file" do @file[:ensure] = :file @file.must be_should_be_file end it "should be a file if :ensure is set to :present and the file exists as a normal file" do @file.stubs(:stat).returns(mock('stat', :ftype => "file")) @file[:ensure] = :present @file.must be_should_be_file end it "should not be a file if :ensure is set to something other than :file" do @file[:ensure] = :directory @file.must_not be_should_be_file end it "should not be a file if :ensure is set to :present and the file exists but is not a normal file" do @file.stubs(:stat).returns(mock('stat', :ftype => "directory")) @file[:ensure] = :present @file.must_not be_should_be_file end it "should be a file if :ensure is not set and :content is" do @file[:content] = "foo" @file.must be_should_be_file end it "should be a file if neither :ensure nor :content is set but the file exists as a normal file" do @file.stubs(:stat).returns(mock("stat", :ftype => "file")) @file.must be_should_be_file end it "should not be a file if neither :ensure nor :content is set but the file exists but not as a normal file" do @file.stubs(:stat).returns(mock("stat", :ftype => "directory")) @file.must_not be_should_be_file end it "should autorequire its parent directory" do catalog = Puppet::Resource::Catalog.new file = Puppet::Type::File.new(:name => "/foo/bar") dir = Puppet::Type::File.new(:name => "/foo") catalog.add_resource file catalog.add_resource dir reqs = file.autorequire reqs[0].source.must == dir reqs[0].target.must == file end it "should not autorequire its parent dir if its parent dir is itself" do catalog = Puppet::Resource::Catalog.new file = Puppet::Type::File.new(:name => "/") catalog.add_resource file file.autorequire.should be_empty end describe "when initializing" do it "should set a desired 'ensure' value if none is set and 'content' is set" do file = Puppet::Type::File.new(:name => "/my/file", :content => "/foo/bar") file[:ensure].should == :file end it "should set a desired 'ensure' value if none is set and 'target' is set" do file = Puppet::Type::File.new(:name => "/my/file", :target => "/foo/bar") file[:ensure].should == :symlink end end describe "when validating attributes" do %w{path backup recurse recurselimit source replace force ignore links purge sourceselect}.each do |attr| it "should have a '#{attr}' parameter" do Puppet::Type.type(:file).attrtype(attr.intern).should == :param end end %w{checksum content target ensure owner group mode type}.each do |attr| it "should have a '#{attr}' property" do Puppet::Type.type(:file).attrtype(attr.intern).should == :property end end it "should have its 'path' attribute set as its namevar" do Puppet::Type.type(:file).namevar.should == :path end end describe "when managing links" do require 'puppettest/support/assertions' include PuppetTest require 'tempfile' before do @basedir = tempfile Dir.mkdir(@basedir) @file = File.join(@basedir, "file") @link = File.join(@basedir, "link") File.open(@file, "w", 0644) { |f| f.puts "yayness"; f.flush } File.symlink(@file, @link) @resource = Puppet::Type.type(:file).new( :path => @link, :mode => "755" ) @catalog.add_resource @resource end after do remove_tmp_files end it "should default to managing the link" do @catalog.apply # I convert them to strings so they display correctly if there's an error. ("%o" % (File.stat(@file).mode & 007777)).should == "%o" % 0644 end it "should be able to follow links" do @resource[:links] = :follow @catalog.apply ("%o" % (File.stat(@file).mode & 007777)).should == "%o" % 0755 end end it "should be able to retrieve a stat instance for the file it is managing" do Puppet::Type.type(:file).new(:path => "/foo/bar", :source => "/bar/foo").should respond_to(:stat) end describe "when stat'ing its file" do before do @resource = Puppet::Type.type(:file).new(:path => "/foo/bar") @resource[:links] = :manage # so we always use :lstat end it "should use :stat if it is following links" do @resource[:links] = :follow File.expects(:stat) @resource.stat end it "should use :lstat if is it not following links" do @resource[:links] = :manage File.expects(:lstat) @resource.stat end it "should stat the path of the file" do File.expects(:lstat).with("/foo/bar") @resource.stat end # This only happens in testing. it "should return nil if the stat does not exist" do File.expects(:lstat).returns nil @resource.stat.should be_nil end it "should return nil if the file does not exist" do File.expects(:lstat).raises(Errno::ENOENT) @resource.stat.should be_nil end it "should return nil if the file cannot be stat'ed" do File.expects(:lstat).raises(Errno::EACCES) @resource.stat.should be_nil end it "should return the stat instance" do File.expects(:lstat).returns "mystat" @resource.stat.should == "mystat" end it "should cache the stat instance if it has a catalog and is applying" do stat = mock 'stat' File.expects(:lstat).returns stat catalog = Puppet::Resource::Catalog.new @resource.catalog = catalog catalog.stubs(:applying?).returns true @resource.stat.should equal(@resource.stat) end end describe "when flushing" do it "should flush all properties that respond to :flush" do @resource = Puppet::Type.type(:file).new(:path => "/foo/bar", :source => "/bar/foo") @resource.parameter(:source).expects(:flush) @resource.flush end it "should reset its stat reference" do @resource = Puppet::Type.type(:file).new(:path => "/foo/bar") File.expects(:lstat).times(2).returns("stat1").then.returns("stat2") @resource.stat.should == "stat1" @resource.flush @resource.stat.should == "stat2" end end it "should have a method for performing recursion" do @file.must respond_to(:perform_recursion) end describe "when executing a recursive search" do it "should use Metadata to do its recursion" do Puppet::FileServing::Metadata.expects(:search) @file.perform_recursion(@file[:path]) end it "should use the provided path as the key to the search" do Puppet::FileServing::Metadata.expects(:search).with { |key, options| key == "/foo" } @file.perform_recursion("/foo") end it "should return the results of the metadata search" do Puppet::FileServing::Metadata.expects(:search).returns "foobar" @file.perform_recursion(@file[:path]).should == "foobar" end it "should pass its recursion value to the search" do @file[:recurse] = true Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:recurse] == true } @file.perform_recursion(@file[:path]) end it "should pass true if recursion is remote" do @file[:recurse] = :remote Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:recurse] == true } @file.perform_recursion(@file[:path]) end it "should pass its recursion limit value to the search" do @file[:recurselimit] = 10 Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:recurselimit] == 10 } @file.perform_recursion(@file[:path]) end it "should configure the search to ignore or manage links" do @file[:links] = :manage Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:links] == :manage } @file.perform_recursion(@file[:path]) end it "should pass its 'ignore' setting to the search if it has one" do @file[:ignore] = %w{.svn CVS} Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:ignore] == %w{.svn CVS} } @file.perform_recursion(@file[:path]) end end it "should have a method for performing local recursion" do @file.must respond_to(:recurse_local) end describe "when doing local recursion" do before do @metadata = stub 'metadata', :relative_path => "my/file" end it "should pass its path to the :perform_recursion method" do @file.expects(:perform_recursion).with(@file[:path]).returns [@metadata] @file.stubs(:newchild) @file.recurse_local end it "should return an empty hash if the recursion returns nothing" do @file.expects(:perform_recursion).returns nil @file.recurse_local.should == {} end it "should create a new child resource with each generated metadata instance's relative path" do @file.expects(:perform_recursion).returns [@metadata] @file.expects(:newchild).with(@metadata.relative_path).returns "fiebar" @file.recurse_local end it "should not create a new child resource for the '.' directory" do @metadata.stubs(:relative_path).returns "." @file.expects(:perform_recursion).returns [@metadata] @file.expects(:newchild).never @file.recurse_local end it "should return a hash of the created resources with the relative paths as the hash keys" do @file.expects(:perform_recursion).returns [@metadata] @file.expects(:newchild).with("my/file").returns "fiebar" @file.recurse_local.should == {"my/file" => "fiebar"} end it "should set checksum_type to none if this file checksum is none" do @file[:checksum] = :none Puppet::FileServing::Metadata.expects(:search).with { |path,params| params[:checksum_type] == :none }.returns [@metadata] @file.expects(:newchild).with("my/file").returns "fiebar" @file.recurse_local end end it "should have a method for performing link recursion" do @file.must respond_to(:recurse_link) end describe "when doing link recursion" do before do @first = stub 'first', :relative_path => "first", :full_path => "/my/first", :ftype => "directory" @second = stub 'second', :relative_path => "second", :full_path => "/my/second", :ftype => "file" @resource = stub 'file', :[]= => nil end it "should pass its target to the :perform_recursion method" do @file[:target] = "mylinks" @file.expects(:perform_recursion).with("mylinks").returns [@first] @file.stubs(:newchild).returns @resource @file.recurse_link({}) end it "should ignore the recursively-found '.' file and configure the top-level file to create a directory" do @first.stubs(:relative_path).returns "." @file[:target] = "mylinks" @file.expects(:perform_recursion).with("mylinks").returns [@first] @file.stubs(:newchild).never @file.expects(:[]=).with(:ensure, :directory) @file.recurse_link({}) end it "should create a new child resource for each generated metadata instance's relative path that doesn't already exist in the children hash" do @file.expects(:perform_recursion).returns [@first, @second] @file.expects(:newchild).with(@first.relative_path).returns @resource @file.recurse_link("second" => @resource) end it "should not create a new child resource for paths that already exist in the children hash" do @file.expects(:perform_recursion).returns [@first] @file.expects(:newchild).never @file.recurse_link("first" => @resource) end it "should set the target to the full path of discovered file and set :ensure to :link if the file is not a directory" do file = stub 'file' file.expects(:[]=).with(:target, "/my/second") file.expects(:[]=).with(:ensure, :link) @file.stubs(:perform_recursion).returns [@first, @second] @file.recurse_link("first" => @resource, "second" => file) end it "should :ensure to :directory if the file is a directory" do file = stub 'file' file.expects(:[]=).with(:ensure, :directory) @file.stubs(:perform_recursion).returns [@first, @second] @file.recurse_link("first" => file, "second" => @resource) end it "should return a hash with both created and existing resources with the relative paths as the hash keys" do file = stub 'file', :[]= => nil @file.expects(:perform_recursion).returns [@first, @second] @file.stubs(:newchild).returns file @file.recurse_link("second" => @resource).should == {"second" => @resource, "first" => file} end end it "should have a method for performing remote recursion" do @file.must respond_to(:recurse_remote) end describe "when doing remote recursion" do before do @file[:source] = "puppet://foo/bar" @first = Puppet::FileServing::Metadata.new("/my", :relative_path => "first") @second = Puppet::FileServing::Metadata.new("/my", :relative_path => "second") @first.stubs(:ftype).returns "directory" @second.stubs(:ftype).returns "directory" @parameter = stub 'property', :metadata= => nil @resource = stub 'file', :[]= => nil, :parameter => @parameter end it "should pass its source to the :perform_recursion method" do data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => "foobar") @file.expects(:perform_recursion).with("puppet://foo/bar").returns [data] @file.stubs(:newchild).returns @resource @file.recurse_remote({}) end it "should not recurse when the remote file is not a directory" do data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => ".") data.stubs(:ftype).returns "file" @file.expects(:perform_recursion).with("puppet://foo/bar").returns [data] @file.expects(:newchild).never @file.recurse_remote({}) end it "should set the source of each returned file to the searched-for URI plus the found relative path" do @first.expects(:source=).with File.join("puppet://foo/bar", @first.relative_path) @file.expects(:perform_recursion).returns [@first] @file.stubs(:newchild).returns @resource @file.recurse_remote({}) end it "should create a new resource for any relative file paths that do not already have a resource" do @file.stubs(:perform_recursion).returns [@first] @file.expects(:newchild).with("first").returns @resource @file.recurse_remote({}).should == {"first" => @resource} end it "should not create a new resource for any relative file paths that do already have a resource" do @file.stubs(:perform_recursion).returns [@first] @file.expects(:newchild).never @file.recurse_remote("first" => @resource) end it "should set the source of each resource to the source of the metadata" do @file.stubs(:perform_recursion).returns [@first] @resource.stubs(:[]=) @resource.expects(:[]=).with(:source, File.join("puppet://foo/bar", @first.relative_path)) @file.recurse_remote("first" => @resource) end # LAK:FIXME This is a bug, but I can't think of a fix for it. Fortunately it's already # filed, and when it's fixed, we'll just fix the whole flow. it "should set the checksum type to :md5 if the remote file is a file" do @first.stubs(:ftype).returns "file" @file.stubs(:perform_recursion).returns [@first] @resource.stubs(:[]=) @resource.expects(:[]=).with(:checksum, :md5) @file.recurse_remote("first" => @resource) end it "should store the metadata in the source property for each resource so the source does not have to requery the metadata" do @file.stubs(:perform_recursion).returns [@first] @resource.expects(:parameter).with(:source).returns @parameter @parameter.expects(:metadata=).with(@first) @file.recurse_remote("first" => @resource) end it "should not create a new resource for the '.' file" do @first.stubs(:relative_path).returns "." @file.stubs(:perform_recursion).returns [@first] @file.expects(:newchild).never @file.recurse_remote({}) end it "should store the metadata in the main file's source property if the relative path is '.'" do @first.stubs(:relative_path).returns "." @file.stubs(:perform_recursion).returns [@first] @file.parameter(:source).expects(:metadata=).with @first @file.recurse_remote("first" => @resource) end describe "and multiple sources are provided" do describe "and :sourceselect is set to :first" do it "should create file instances for the results for the first source to return any values" do data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => "foobar") @file[:source] = %w{/one /two /three /four} @file.expects(:perform_recursion).with("/one").returns nil @file.expects(:perform_recursion).with("/two").returns [] @file.expects(:perform_recursion).with("/three").returns [data] @file.expects(:perform_recursion).with("/four").never @file.expects(:newchild).with("foobar").returns @resource @file.recurse_remote({}) end end describe "and :sourceselect is set to :all" do before do @file[:sourceselect] = :all end it "should return every found file that is not in a previous source" do klass = Puppet::FileServing::Metadata @file[:source] = %w{/one /two /three /four} @file.stubs(:newchild).returns @resource one = [klass.new("/one", :relative_path => "a")] @file.expects(:perform_recursion).with("/one").returns one @file.expects(:newchild).with("a").returns @resource two = [klass.new("/two", :relative_path => "a"), klass.new("/two", :relative_path => "b")] @file.expects(:perform_recursion).with("/two").returns two @file.expects(:newchild).with("b").returns @resource three = [klass.new("/three", :relative_path => "a"), klass.new("/three", :relative_path => "c")] @file.expects(:perform_recursion).with("/three").returns three @file.expects(:newchild).with("c").returns @resource @file.expects(:perform_recursion).with("/four").returns [] @file.recurse_remote({}) end end end end describe "when returning resources with :eval_generate" do before do @graph = stub 'graph', :add_edge => nil @catalog.stubs(:relationship_graph).returns @graph @file.catalog = @catalog @file[:recurse] = true end it "should recurse if recursion is enabled" do resource = stub('resource', :[] => "resource") @file.expects(:recurse?).returns true @file.expects(:recurse).returns [resource] @file.eval_generate.should == [resource] end it "should not recurse if recursion is disabled" do @file.expects(:recurse?).returns false @file.expects(:recurse).never @file.eval_generate.should == [] end it "should return each resource found through recursion" do foo = stub 'foo', :[] => "/foo" bar = stub 'bar', :[] => "/bar" bar2 = stub 'bar2', :[] => "/bar" @file.expects(:recurse).returns [foo, bar] @file.eval_generate.should == [foo, bar] end end describe "when recursing" do before do @file[:recurse] = true @metadata = Puppet::FileServing::Metadata end describe "and a source is set" do before { @file[:source] = "/my/source" } it "should pass the already-discovered resources to recurse_remote" do @file.stubs(:recurse_local).returns(:foo => "bar") @file.expects(:recurse_remote).with(:foo => "bar").returns [] @file.recurse end end describe "and a target is set" do before { @file[:target] = "/link/target" } it "should use recurse_link" do @file.stubs(:recurse_local).returns(:foo => "bar") @file.expects(:recurse_link).with(:foo => "bar").returns [] @file.recurse end end it "should use recurse_local if recurse is not remote" do @file.expects(:recurse_local).returns({}) @file.recurse end it "should not use recurse_local if recurse remote" do @file[:recurse] = :remote @file.expects(:recurse_local).never @file.recurse end it "should return the generated resources as an array sorted by file path" do one = stub 'one', :[] => "/one" two = stub 'two', :[] => "/one/two" three = stub 'three', :[] => "/three" @file.expects(:recurse_local).returns(:one => one, :two => two, :three => three) @file.recurse.should == [one, two, three] end describe "and purging is enabled" do before do @file[:purge] = true end it "should configure each file to be removed" do local = stub 'local' local.stubs(:[]).with(:source).returns nil # Thus, a local file local.stubs(:[]).with(:path).returns "foo" @file.expects(:recurse_local).returns("local" => local) local.expects(:[]=).with(:ensure, :absent) @file.recurse end it "should not remove files that exist in the remote repository" do @file["source"] = "/my/file" @file.expects(:recurse_local).returns({}) remote = stub 'remote' remote.stubs(:[]).with(:source).returns "/whatever" # Thus, a remote file remote.stubs(:[]).with(:path).returns "foo" @file.expects(:recurse_remote).with { |hash| hash["remote"] = remote } remote.expects(:[]=).with(:ensure, :absent).never @file.recurse end end describe "and making a new child resource" do it "should not copy the parent resource's parent" do Puppet::Type.type(:file).expects(:new).with { |options| ! options.include?(:parent) } @file.newchild("my/path") end {:recurse => true, :target => "/foo/bar", :ensure => :present, :alias => "yay", :source => "/foo/bar"}.each do |param, value| it "should not pass on #{param} to the sub resource" do @file = Puppet::Type::File.new(:name => @path, param => value, :catalog => @catalog) @file.class.expects(:new).with { |params| params[param].nil? } @file.newchild("sub/file") end end it "should copy all of the parent resource's 'should' values that were set at initialization" do file = @file.class.new(:path => "/foo/bar", :owner => "root", :group => "wheel") @catalog.add_resource(file) file.class.expects(:new).with { |options| options[:owner] == "root" and options[:group] == "wheel" } file.newchild("my/path") end it "should not copy default values to the new child" do @file.class.expects(:new).with { |params| params[:backup].nil? } @file.newchild("my/path") end it "should not copy values to the child which were set by the source" do @file[:source] = "/foo/bar" metadata = stub 'metadata', :owner => "root", :group => "root", :mode => 0755, :ftype => "file", :checksum => "{md5}whatever" @file.parameter(:source).stubs(:metadata).returns metadata @file.parameter(:source).copy_source_values @file.class.expects(:new).with { |params| params[:group].nil? } @file.newchild("my/path") end end end describe "when setting the backup" do it "should default to 'puppet'" do Puppet::Type::File.new(:name => "/my/file")[:backup].should == "puppet" end it "should allow setting backup to 'false'" do Puppet::Type::File.new(:name => "/my/file", :backup => false)[:backup].should be_false end it "should set the backup to '.puppet-bak' if it is set to true" do Puppet::Type::File.new(:name => "/my/file", :backup => true)[:backup].should == ".puppet-bak" end it "should support any other backup extension" do Puppet::Type::File.new(:name => "/my/file", :backup => ".bak")[:backup].should == ".bak" end it "should set the filebucket when backup is set to a string matching the name of a filebucket in the catalog" do catalog = Puppet::Resource::Catalog.new bucket_resource = Puppet::Type.type(:filebucket).new :name => "foo", :path => "/my/file/bucket" catalog.add_resource bucket_resource file = Puppet::Type::File.new(:name => "/my/file") catalog.add_resource file file[:backup] = "foo" file.bucket.should == bucket_resource.bucket end it "should find filebuckets added to the catalog after the file resource was created" do catalog = Puppet::Resource::Catalog.new file = Puppet::Type::File.new(:name => "/my/file", :backup => "foo") catalog.add_resource file bucket_resource = Puppet::Type.type(:filebucket).new :name => "foo", :path => "/my/file/bucket" catalog.add_resource bucket_resource file.bucket.should == bucket_resource.bucket end it "should have a nil filebucket if backup is false" do catalog = Puppet::Resource::Catalog.new bucket_resource = Puppet::Type.type(:filebucket).new :name => "foo", :path => "/my/file/bucket" catalog.add_resource bucket_resource file = Puppet::Type::File.new(:name => "/my/file", :backup => false) catalog.add_resource file file.bucket.should be_nil end it "should have a nil filebucket if backup is set to a string starting with '.'" do catalog = Puppet::Resource::Catalog.new bucket_resource = Puppet::Type.type(:filebucket).new :name => "foo", :path => "/my/file/bucket" catalog.add_resource bucket_resource file = Puppet::Type::File.new(:name => "/my/file", :backup => ".foo") catalog.add_resource file file.bucket.should be_nil end it "should fail if there's no catalog and backup is not false" do file = Puppet::Type::File.new(:name => "/my/file", :backup => "foo") lambda { file.bucket }.should raise_error(Puppet::Error) end it "should fail if a non-existent catalog is specified" do file = Puppet::Type::File.new(:name => "/my/file", :backup => "foo") catalog = Puppet::Resource::Catalog.new catalog.add_resource file lambda { file.bucket }.should raise_error(Puppet::Error) end it "should be able to use the default filebucket without a catalog" do file = Puppet::Type::File.new(:name => "/my/file", :backup => "puppet") - file.bucket.should be_instance_of(Puppet::Network::Client::Dipper) + file.bucket.should be_instance_of(Puppet::FileBucket::Dipper) end it "should look up the filebucket during finish()" do file = Puppet::Type::File.new(:name => "/my/file", :backup => ".foo") file.expects(:bucket) file.finish end end describe "when writing the file" do it "should propagate failures encountered when renaming the temporary file" do File.stubs(:open) File.expects(:rename).raises ArgumentError file = Puppet::Type::File.new(:name => "/my/file", :backup => "puppet") lambda { file.write("something", :content) }.should raise_error(Puppet::Error) end end describe "when retrieving the current file state" do it "should copy the source values if the 'source' parameter is set" do file = Puppet::Type::File.new(:name => "/my/file", :source => "/foo/bar") file.parameter(:source).expects(:copy_source_values) file.retrieve end end end diff --git a/spec/unit/type/filebucket.rb b/spec/unit/type/filebucket.rb index 78bfa3655..a0ff45ab6 100644 --- a/spec/unit/type/filebucket.rb +++ b/spec/unit/type/filebucket.rb @@ -1,74 +1,74 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::Type.type(:filebucket) do describe "when validating attributes" do %w{name server port path}.each do |attr| it "should have a '#{attr}' parameter" do Puppet::Type.type(:filebucket).attrtype(attr.intern).should == :param end end it "should have its 'name' attribute set as its namevar" do Puppet::Type.type(:filebucket).namevar.should == :name end end it "should use the clientbucketdir as the path by default path" do Puppet.settings[:clientbucketdir] = "/my/bucket" Puppet::Type.type(:filebucket).new(:name => "main")[:path].should == Puppet[:clientbucketdir] end it "should use the masterport as the path by default port" do Puppet.settings[:masterport] = 50 Puppet::Type.type(:filebucket).new(:name => "main")[:port].should == Puppet[:masterport] end it "should use the server as the path by default server" do Puppet.settings[:server] = "myserver" Puppet::Type.type(:filebucket).new(:name => "main")[:server].should == Puppet[:server] end it "be local by default" do bucket = Puppet::Type.type(:filebucket).new :name => "main" bucket.bucket.should be_local end it "not be local if path is false" do bucket = Puppet::Type.type(:filebucket).new :name => "main", :path => false bucket.bucket.should_not be_local end it "be local if both a path and a server are specified" do bucket = Puppet::Type.type(:filebucket).new :name => "main", :server => "puppet", :path => "/my/path" bucket.bucket.should be_local end describe "when creating the filebucket" do before do @bucket = stub 'bucket', :name= => nil end it "should use any provided path" do bucket = Puppet::Type.type(:filebucket).new :name => "main", :path => "/foo/bar" - Puppet::Network::Client.client(:Dipper).expects(:new).with(:Path => "/foo/bar").returns @bucket + Puppet::FileBucket::Dipper.expects(:new).with(:Path => "/foo/bar").returns @bucket bucket.bucket end it "should use any provided server and port" do bucket = Puppet::Type.type(:filebucket).new :name => "main", :server => "myserv", :port => "myport", :path => false - Puppet::Network::Client.client(:Dipper).expects(:new).with(:Server => "myserv", :Port => "myport").returns @bucket + Puppet::FileBucket::Dipper.expects(:new).with(:Server => "myserv", :Port => "myport").returns @bucket bucket.bucket end it "should use the default server if the path is unset and no server is provided" do Puppet.settings[:server] = "myserv" bucket = Puppet::Type.type(:filebucket).new :name => "main", :path => false - Puppet::Network::Client.client(:Dipper).expects(:new).with { |args| args[:Server] == "myserv" }.returns @bucket + Puppet::FileBucket::Dipper.expects(:new).with { |args| args[:Server] == "myserv" }.returns @bucket bucket.bucket end end end diff --git a/test/network/client/dipper.rb b/test/network/client/dipper.rb index 51494be9d..611ee7391 100755 --- a/test/network/client/dipper.rb +++ b/test/network/client/dipper.rb @@ -1,34 +1,34 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../lib/puppettest' require 'puppettest' require 'puppet/network/client/resource' class TestDipperClient < Test::Unit::TestCase include PuppetTest::ServerTest def setup super - @dipper = Puppet::Network::Client.dipper.new(:Path => tempfile) + @dipper = Puppet::FileBucket::Dipper.new(:Path => tempfile) end # Make sure we can create a new file with 'restore'. def test_restore_to_new_file file = tempfile text = "asdf;lkajseofiqwekj" File.open(file, "w") { |f| f.puts text } md5 = nil assert_nothing_raised("Could not send file") do md5 = @dipper.backup(file) end newfile = tempfile assert_nothing_raised("could not restore to new path") do @dipper.restore(newfile, md5) end assert_equal(File.read(file), File.read(newfile), "did not restore correctly") end end diff --git a/test/network/handler/bucket.rb b/test/network/handler/bucket.rb deleted file mode 100755 index 24e70cf01..000000000 --- a/test/network/handler/bucket.rb +++ /dev/null @@ -1,313 +0,0 @@ -#!/usr/bin/env ruby - -require File.dirname(__FILE__) + '/../../lib/puppettest' - -require 'puppettest' -require 'puppet/network/handler/filebucket' -require 'base64' -require 'mocha' - -class TestBucket < Test::Unit::TestCase - include PuppetTest::ServerTest - - def out - if defined? @num - @num += 1 - else - @num = 1 - end - - #Puppet.err "#{Process.pid}: %s: %s" % [@num, memory()] - #gcdebug(String) - end - - # run through all of the files and exercise the filebucket methods - def checkfiles(client) - files = filelist() - - # iterate across all of the files - files.each { |file| - Puppet.warning file - out - tempdir = tempfile() - Dir.mkdir(tempdir) - name = File.basename(file) - tmppath = File.join(tempdir,name) - @@tmpfiles << tmppath - - out - # copy the files to our tmp directory so we can modify them... - FileUtils.cp(file, tmppath) - - # make sure the copy worked - assert(FileTest.exists?(tmppath)) - - # backup both the orig file and the tmp file - osum = nil - tsum = nil - nsum = nil - out - assert_nothing_raised("Could not back up file") { - osum = client.backup(file) - } - out - assert_nothing_raised("Could not back up second file") { - tsum = client.backup(tmppath) - } - out - - # verify you got the same sum back for both - assert(tsum == osum) - - # modify our tmp file - unless FileTest.writable?(tmppath) - File.chmod(0644, tmppath) - end - File.open(tmppath,File::WRONLY|File::TRUNC) { |wf| - wf.print "This is some test text\n" - } - out - - # back it up - assert_nothing_raised { - #STDERR.puts("backing up %s" % tmppath) if $debug - nsum = client.backup(tmppath) - } - out - - # and verify the sum changed - assert(tsum != nsum) - - # restore the orig - assert_nothing_raised { - nsum = client.restore(tmppath,tsum) - } - out - - # and verify it actually got restored - contents = File.open(tmppath) { |rf| - #STDERR.puts("reading %s" % tmppath) if $debug - rf.read - } - out - csum = Digest::MD5.hexdigest(contents) - out - assert(tsum == csum) - } - end - - # a list of files that should be on the system - # just something to test moving files around - def filelist - if defined? @files - return @files - else - @files = [] - end - - %w{ - who bash sh uname /etc/passwd /etc/syslog.conf /etc/hosts - }.each { |file| - # if it's fully qualified, just add it - if file =~ /^\// - if FileTest.exists?(file) - @files.push file - end - else - # else if it's unqualified, look for it in our path - begin - path = %x{which #{file}} - rescue => detail - #STDERR.puts "Could not search for binaries: %s" % detail - next - end - - if path != "" - @files.push path.chomp - end - end - } - - return @files - end - - def setup - super - @bucket = tempfile() - end - - #def teardown - # system("lsof -p %s" % Process.pid) - # super - #end - - # test operating against the local filebucket object - # this calls the direct server methods, which are different than the - # Dipper methods - def test_localserver - files = filelist() - server = nil - assert_nothing_raised { - server = Puppet::Network::Handler.filebucket.new( - :Path => @bucket - ) - } - - # iterate across them... - files.each { |file| - contents = File.open(file) { |of| of.read } - - md5 = nil - - # add a file to the repository - assert_nothing_raised { - #STDERR.puts("adding %s" % file) if $debug - md5 = server.addfile(Base64.encode64(contents),file) - } - - # and get it back again - newcontents = nil - assert_nothing_raised { - #STDERR.puts("getting %s" % file) if $debug - newcontents = Base64.decode64(server.getfile(md5)) - } - - # and then make sure they're still the same - assert( - contents == newcontents - ) - } - end - - # test with a server and a Dipper - def test_localboth - files = filelist() - - bucket = nil - client = nil - threads = [] - assert_nothing_raised { - bucket = Puppet::Network::Handler.filebucket.new( - :Path => @bucket - ) - } - - #sleep(30) - assert_nothing_raised { - client = Puppet::Network::Client.dipper.new( - :Bucket => bucket - ) - } - - #4.times { checkfiles(client) } - checkfiles(client) - end - - def test_no_path_duplicates - bucket = nil - assert_nothing_raised { - bucket = Puppet::Network::Handler.filebucket.new( - :Path => @bucket - ) - } - - sum = nil - assert_nothing_raised { - sum = bucket.addfile("yayness", "/my/file") - } - assert_nothing_raised { - bucket.addfile("yayness", "/my/file") - } - - a, b, pathfile = bucket.class.paths(bucket.path, sum) - - assert(FileTest.exists?(pathfile), "No path file at %s" % pathfile) - - assert_equal("/my/file\n", File.read(pathfile)) - end - - # #447 -- a flat file structure just won't suffice. - def test_deeper_filestructure - bucket = Puppet::Network::Handler.filebucket.new(:Path => @bucket) - - text = "this is some text" - md5 = Digest::MD5.hexdigest(text) - - olddir = File.join(@bucket, md5) - FileUtils.mkdir_p(olddir) - oldcontent = File.join(olddir, "contents") - File.open(oldcontent, "w") { |f| f.print text } - - result = nil - assert_nothing_raised("Could not retrieve content from old structure") do - result = bucket.getfile(md5) - end - assert_equal(text, result, "old-style content is wrong") - - text = "and this is some new text" - md5 = Digest::MD5.hexdigest(text) - - dirs = File.join(md5[0..7].split("")) - dir = File.join(@bucket, dirs, md5) - filedir, contents, paths = bucket.class.paths(@bucket, md5) - - assert_equal(dir, filedir, "did not use a deeper file structure") - assert_equal(File.join(dir, "contents"), contents, - "content path is not the deeper version") - assert_equal(File.join(dir, "paths"), paths, - "paths file path is not the deeper version") - - # Store our new text and make sure it gets stored in the new location - path = "/some/fake/path" - assert_nothing_raised("Could not store text") do - bucket.addfile(text, path) - end - assert(FileTest.exists?(contents), "did not create content file") - assert_equal(text, File.read(contents), "content is not right") - assert(FileTest.exists?(paths), "did not create paths file") - assert(File.read(paths).include?(path), "paths file does not contain path") - - # And make sure we get it back out again - assert_nothing_raised("Could not retrieve new-style content") do - result = bucket.getfile(md5) - end - assert_equal(text, result, "did not retrieve new content correctly") - end - - def test_add_path - bucket = Puppet::Network::Handler.filebucket.new(:Path => @bucket) - - file = tempfile() - - assert(! FileTest.exists?(file), "file already exists") - - path = "/some/thing" - assert_nothing_raised("Could not add path") do - bucket.send(:add_path, path, file) - end - assert_equal(path + "\n", File.read(file), "path was not added") - - assert_nothing_raised("Could not add path second time") do - bucket.send(:add_path, path, file) - end - assert_equal(path + "\n", File.read(file), "path was duplicated") - - # Now try a new path - newpath = "/another/path" - assert_nothing_raised("Could not add path second time") do - bucket.send(:add_path, newpath, file) - end - text = [path, newpath].join("\n") + "\n" - assert_equal(text, File.read(file), "path was duplicated") - - assert_nothing_raised("Could not add path third time") do - bucket.send(:add_path, path, file) - end - assert_equal(text, File.read(file), "path was duplicated") - assert_nothing_raised("Could not add second path second time") do - bucket.send(:add_path, newpath, file) - end - assert_equal(text, File.read(file), "path was duplicated") - end -end -