diff --git a/lib/puppet/application/resource.rb b/lib/puppet/application/resource.rb index 0046fc1d8..ae4349850 100644 --- a/lib/puppet/application/resource.rb +++ b/lib/puppet/application/resource.rb @@ -1,124 +1,123 @@ require 'puppet' require 'puppet/application' require 'facter' Puppet::Application.new(:resource) do should_not_parse_config attr_accessor :host, :extra_params preinit do @extra_params = [] @host = nil Facter.loadfacts end option("--debug","-d") option("--verbose","-v") option("--edit","-e") option("--host HOST","-H") do |arg| @host = arg end option("--types", "-t") do |arg| types = [] Puppet::Type.loadall Puppet::Type.eachtype do |t| next if t.name == :component types << t.name.to_s end puts types.sort exit end option("--param PARAM", "-p") do |arg| @extra_params << arg.to_sym end command(:main) do type = ARGV.shift or raise "You must specify the type to display" typeobj = Puppet::Type.type(type) or raise "Could not find type #{type}" name = ARGV.shift params = {} ARGV.each do |setting| if setting =~ /^(\w+)=(.+)$/ params[$1] = $2 else raise "Invalid parameter setting %s" % setting end end if options[:edit] and @host raise "You cannot edit a remote host" end properties = typeobj.properties.collect { |s| s.name } format = proc {|trans| trans.dup.collect do |param, value| if value.nil? or value.to_s.empty? trans.delete(param) elsif value.to_s == "absent" and param.to_s != "ensure" trans.delete(param) end unless properties.include?(param) or @extra_params.include?(param) trans.delete(param) end end trans.to_manifest } if @host Puppet::Resource.indirection.terminus_class = :rest port = Puppet[:puppetport] key = ["https://#{host}:#{port}", "production", "resources", type, name].join('/') else key = [type, name].join('/') end text = if name if params.empty? [ Puppet::Resource.find( key ) ] else - request = Puppet::Indirector::Request.new(:resource, :save, key) # Yuck. - [ Puppet::Resource.new( type, name, params ).save( request ) ] + [ Puppet::Resource.new( type, name, params ).save( key ) ] end else Puppet::Resource.search( key, {} ) end.map(&format).join("\n") if options[:edit] file = "/tmp/x2puppet-#{Process.pid}.pp" begin File.open(file, "w") do |f| f.puts text end ENV["EDITOR"] ||= "vi" system(ENV["EDITOR"], file) system("puppet -v " + file) ensure #if FileTest.exists? file # File.unlink(file) #end end else puts text end end setup do Puppet::Util::Log.newdestination(:console) # Now parse the config Puppet.parse_config if options[:debug] Puppet::Util::Log.level = :debug elsif options[:verbose] Puppet::Util::Log.level = :info end end end diff --git a/lib/puppet/application/run.rb b/lib/puppet/application/run.rb index 26ca362ff..46d1b191f 100644 --- a/lib/puppet/application/run.rb +++ b/lib/puppet/application/run.rb @@ -1,212 +1,211 @@ require 'puppet' require 'puppet/application' Puppet.warning "RubyGems not installed" unless Puppet.features.rubygems? Puppet.warning "Failed to load ruby LDAP library. LDAP functionality will not be available" unless Puppet.features.ldap? Puppet::Application.new(:run) do should_not_parse_config attr_accessor :hosts, :tags, :classes option("--all","-a") option("--foreground","-f") option("--debug","-d") option("--ping","-P") option("--test") option("--host HOST") do |arg| @hosts << arg end option("--tag TAG", "-t") do |arg| @tags << arg end option("--class CLASS", "-c") do |arg| @classes << arg end option("--no-fqdn", "-n") do |arg| options[:fqdn] = false end option("--parallel PARALLEL", "-p") do |arg| begin options[:parallel] = Integer(arg) rescue $stderr.puts "Could not convert %s to an integer" % arg.inspect exit(23) end end dispatch do options[:test] ? :test : :main end command(:test) do puts "Skipping execution in test mode" exit(0) end command(:main) do require 'puppet/network/client' require 'puppet/util/ldap/connection' todo = @hosts.dup failures = [] # Now do the actual work go = true while go # If we don't have enough children in process and we still have hosts left to # do, then do the next host. if @children.length < options[:parallel] and ! todo.empty? host = todo.shift pid = fork do run_for_host(host) end @children[pid] = host else # Else, see if we can reap a process. begin pid = Process.wait if host = @children[pid] # Remove our host from the list of children, so the parallelization # continues working. @children.delete(pid) if $?.exitstatus != 0 failures << host end print "%s finished with exit code %s\n" % [host, $?.exitstatus] else $stderr.puts "Could not find host for PID %s with status %s" % [pid, $?.exitstatus] end rescue Errno::ECHILD # There are no children left, so just exit unless there are still # children left to do. next unless todo.empty? if failures.empty? puts "Finished" exit(0) else puts "Failed: %s" % failures.join(", ") exit(3) end end end end end def run_for_host(host) if options[:ping] out = %x{ping -c 1 #{host}} unless $? == 0 $stderr.print "Could not contact %s\n" % host next end end require 'puppet/run' Puppet::Run.indirection.terminus_class = :rest port = Puppet[:puppetport] url = ["https://#{host}:#{port}", "production", "run", host].join('/') print "Triggering %s\n" % host begin - request = Puppet::Indirector::Request.new(:run, :save, url) # Yuck. run_options = { :tags => @tags, :background => ! options[:foreground], :ignoreschedules => options[:ignoreschedules] } - run = Puppet::Run.new( run_options ).save( request ) + run = Puppet::Run.new( run_options ).save( url ) result = run.status rescue => detail puts detail.backtrace if Puppet[:trace] $stderr.puts "Host %s failed: %s\n" % [host, detail] exit(2) end case result when "success"; exit(0) when "running" $stderr.puts "Host %s is already running" % host exit(3) else $stderr.puts "Host %s returned unknown answer '%s'" % [host, result] exit(12) end end preinit do [:INT, :TERM].each do |signal| trap(signal) do $stderr.puts "Cancelling" exit(1) end end options[:parallel] = 1 options[:verbose] = true options[:fqdn] = true options[:ignoreschedules] = false options[:foreground] = false @hosts = [] @classes = [] @tags = [] end setup do if options[:debug] Puppet::Util::Log.level = :debug else Puppet::Util::Log.level = :info end # Now parse the config Puppet.parse_config if Puppet[:node_terminus] == "ldap" and (options[:all] or @classes) if options[:all] @hosts = Puppet::Node.search("whatever", :fqdn => options[:fqdn]).collect { |node| node.name } puts "all: %s" % @hosts.join(", ") else @hosts = [] @classes.each do |klass| list = Puppet::Node.search("whatever", :fqdn => options[:fqdn], :class => klass).collect { |node| node.name } puts "%s: %s" % [klass, list.join(", ")] @hosts += list end end elsif ! @classes.empty? $stderr.puts "You must be using LDAP to specify host classes" exit(24) end @children = {} # If we get a signal, then kill all of our children and get out. [:INT, :TERM].each do |signal| trap(signal) do Puppet.notice "Caught #{signal}; shutting down" @children.each do |pid, host| Process.kill("INT", pid) end waitall exit(1) end end end end diff --git a/lib/puppet/file_bucket/dipper.rb b/lib/puppet/file_bucket/dipper.rb index c73d76345..b13e590a9 100644 --- a/lib/puppet/file_bucket/dipper.rb +++ b/lib/puppet/file_bucket/dipper.rb @@ -1,97 +1,95 @@ 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) @local_path = hash[:Path] @rest_path = nil else @local_path = nil @rest_path = "https://#{server}:#{port}/#{environment}/file_bucket_file/" end end def local? !! @local_path end # Back up a file to our bucket def backup(file) unless ::File.exist?(file) raise(ArgumentError, "File %s does not exist" % file) end contents = ::File.read(file) begin 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) + file_bucket_file.save(dest_path) 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) 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/indirector.rb b/lib/puppet/indirector.rb index 61ef2db29..91c759331 100644 --- a/lib/puppet/indirector.rb +++ b/lib/puppet/indirector.rb @@ -1,68 +1,68 @@ # Manage indirections to termini. They are organized in terms of indirections - # - e.g., configuration, node, file, certificate -- and each indirection has one # or more terminus types defined. The indirection is configured via the # +indirects+ method, which will be called by the class extending itself # with this module. module Puppet::Indirector # LAK:FIXME We need to figure out how to handle documentation for the # different indirection types. require 'puppet/indirector/indirection' require 'puppet/indirector/terminus' require 'puppet/indirector/envelope' require 'puppet/network/format_handler' # Declare that the including class indirects its methods to # this terminus. The terminus name must be the name of a Puppet # default, not the value -- if it's the value, then it gets # evaluated at parse time, which is before the user has had a chance # to override it. def indirects(indirection, options = {}) raise(ArgumentError, "Already handling indirection for %s; cannot also handle %s" % [@indirection.name, indirection]) if defined?(@indirection) and @indirection # populate this class with the various new methods extend ClassMethods include InstanceMethods include Puppet::Indirector::Envelope extend Puppet::Network::FormatHandler # instantiate the actual Terminus for that type and this name (:ldap, w/ args :node) # & hook the instantiated Terminus into this class (Node: @indirection = terminus) @indirection = Puppet::Indirector::Indirection.new(self, indirection, options) @indirection end module ClassMethods attr_reader :indirection def cache_class=(klass) indirection.cache_class = klass end def terminus_class=(klass) indirection.terminus_class = klass end # Expire any cached instance. def expire(*args) indirection.expire(*args) end def find(*args) indirection.find(*args) end def destroy(*args) indirection.destroy(*args) end def search(*args) indirection.search(*args) end end module InstanceMethods - def save(*args) - self.class.indirection.save self, *args + def save(key = nil) + self.class.indirection.save key, self end end end diff --git a/lib/puppet/indirector/indirection.rb b/lib/puppet/indirector/indirection.rb index 3c6414624..266758b84 100644 --- a/lib/puppet/indirector/indirection.rb +++ b/lib/puppet/indirector/indirection.rb @@ -1,318 +1,312 @@ 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, 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) + def request(*args) + Puppet::Indirector::Request.new(self.name, *args) 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, request_or_options = nil) - request = request(:save, instance, request_or_options) + def save(key, instance = nil) + request = request(:save, key, instance) 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 14608d0dc..cd354ac16 100644 --- a/lib/puppet/indirector/request.rb +++ b/lib/puppet/indirector/request.rb @@ -1,193 +1,203 @@ 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) + def initialize(indirection_name, method, key_or_instance, options_or_instance = {}) + if options_or_instance.is_a? Hash + options = options_or_instance + @instance = nil + else + options = {} + @instance = options_or_instance + end 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 key_or_instance.is_a?(String) || key_or_instance.is_a?(Symbol) + key = key_or_instance + else + @instance = key_or_instance if ! @instance + end + + if key # 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 + + @key = @instance.name if ! @key and @instance 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 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/http/handler.rb b/lib/puppet/network/http/handler.rb index 01ca65023..ab853665c 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) + object.save(ind_request.key) 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/node/facts.rb b/lib/puppet/node/facts.rb index dca435c7d..ed7fe1253 100755 --- a/lib/puppet/node/facts.rb +++ b/lib/puppet/node/facts.rb @@ -1,65 +1,65 @@ require 'puppet/node' require 'puppet/indirector' # Manage a given node's facts. This either accepts facts and stores them, or # returns facts for a given node. class Puppet::Node::Facts # Set up indirection, so that nodes can be looked for in # the node sources. extend Puppet::Indirector # We want to expire any cached nodes if the facts are saved. module NodeExpirer - def save(instance, *args) + def save(key, instance) Puppet::Node.expire(instance.name) super end end # Use the node source as the indirection terminus. indirects :facts, :terminus_class => :facter, :extend => NodeExpirer attr_accessor :name, :values def add_local_facts values["clientversion"] = Puppet.version.to_s values["environment"] ||= Puppet.settings[:environment] end def initialize(name, values = {}) @name = name @values = values add_internal end def downcase_if_necessary return unless Puppet.settings[:downcasefacts] Puppet.warning "DEPRECATION NOTICE: Fact downcasing is deprecated; please disable (20080122)" values.each do |fact, value| values[fact] = value.downcase if value.is_a?(String) end end # Convert all fact values into strings. def stringify values.each do |fact, value| values[fact] = value.to_s end end private # Add internal data to the facts for storage. def add_internal self.values[:_timestamp] = Time.now end # Strip out that internal data. def strip_internal newvals = values.dup newvals.find_all { |name, value| name.to_s =~ /^_/ }.each { |name, value| newvals.delete(name) } newvals end end diff --git a/spec/integration/configurer.rb b/spec/integration/configurer.rb index 7bd351aa1..fce24a1b2 100755 --- a/spec/integration/configurer.rb +++ b/spec/integration/configurer.rb @@ -1,35 +1,35 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../spec_helper' require 'puppet/configurer' describe Puppet::Configurer do describe "when downloading plugins" do it "should use the :pluginsignore setting, split on whitespace, for ignoring remote files" do resource = Puppet::Type.type(:notify).new :name => "yay" Puppet::Type.type(:file).expects(:new).with { |args| args[:ignore] == Puppet[:pluginsignore].split(/\s+/) }.returns resource configurer = Puppet::Configurer.new configurer.stubs(:download_plugins?).returns true configurer.download_plugins end end describe "when running" do it "should send a transaction report with valid data" do catalog = Puppet::Resource::Catalog.new catalog.add_resource(Puppet::Type.type(:notify).new(:title => "testing")) configurer = Puppet::Configurer.new - Puppet::Transaction::Report.indirection.expects(:save).with do |report| + Puppet::Transaction::Report.indirection.expects(:save).with do |x, report| report.time.class == Time and report.logs.length > 0 end Puppet[:report] = true configurer.run :catalog => catalog end end end diff --git a/spec/unit/application/puppetrun.rb b/spec/unit/application/puppetrun.rb index ce3af5a5b..f8ca48061 100755 --- a/spec/unit/application/puppetrun.rb +++ b/spec/unit/application/puppetrun.rb @@ -1,292 +1,292 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/util/ldap/connection' require 'puppet/application/run' describe "run" do before :each do Puppet::Util::Ldap::Connection.stubs(:new).returns(stub_everything) @run = Puppet::Application[:run] Puppet::Util::Log.stubs(:newdestination) Puppet::Util::Log.stubs(:level=) end it "should ask Puppet::Application to not parse Puppet configuration file" do @run.should_parse_config?.should be_false end it "should declare a main command" do @run.should respond_to(:main) end it "should declare a test command" do @run.should respond_to(:test) end it "should declare a preinit block" do @run.should respond_to(:run_preinit) end describe "during preinit" do before :each do @run.stubs(:trap) end it "should catch INT and TERM" do @run.stubs(:trap).with { |arg,block| arg == :INT or arg == :TERM } @run.run_preinit end it "should set parallel option to 1" do @run.run_preinit @run.options[:parallel].should == 1 end it "should set verbose by default" do @run.run_preinit @run.options[:verbose].should be_true end it "should set fqdn by default" do @run.run_preinit @run.options[:fqdn].should be_true end it "should set ignoreschedules to 'false'" do @run.run_preinit @run.options[:ignoreschedules].should be_false end it "should set foreground to 'false'" do @run.run_preinit @run.options[:foreground].should be_false end end describe "when applying options" do [:all, :foreground, :debug, :ping, :test].each do |option| it "should declare handle_#{option} method" do @run.should respond_to("handle_#{option}".to_sym) end it "should store argument value when calling handle_#{option}" do @run.options.expects(:[]=).with(option, 'arg') @run.send("handle_#{option}".to_sym, 'arg') end end it "should add to the host list with the host option" do @run.handle_host('host') @run.hosts.should == ['host'] end it "should add to the tag list with the tag option" do @run.handle_tag('tag') @run.tags.should == ['tag'] end it "should add to the class list with the class option" do @run.handle_class('class') @run.classes.should == ['class'] end end describe "during setup" do before :each do @run.classes = [] @run.tags = [] @run.hosts = [] Puppet::Log.stubs(:level=) @run.stubs(:trap) @run.stubs(:puts) Puppet.stubs(:parse_config) @run.options.stubs(:[]).with(any_parameters) end it "should set log level to debug if --debug was passed" do @run.options.stubs(:[]).with(:debug).returns(true) Puppet::Log.expects(:level=).with(:debug) @run.run_setup end it "should set log level to info if --verbose was passed" do @run.options.stubs(:[]).with(:verbose).returns(true) Puppet::Log.expects(:level=).with(:info) @run.run_setup end it "should Parse puppet config" do Puppet.expects(:parse_config) @run.run_setup end describe "when using the ldap node terminus" do before :each do Puppet.stubs(:[]).with(:node_terminus).returns("ldap") end it "should pass the fqdn option to search" do @run.options.stubs(:[]).with(:fqdn).returns(:something) @run.options.stubs(:[]).with(:all).returns(true) @run.stubs(:puts) Puppet::Node.expects(:search).with("whatever",:fqdn => :something).returns([]) @run.run_setup end it "should search for all nodes if --all" do @run.options.stubs(:[]).with(:all).returns(true) @run.stubs(:puts) Puppet::Node.expects(:search).with("whatever",:fqdn => nil).returns([]) @run.run_setup end it "should search for nodes including given classes" do @run.options.stubs(:[]).with(:all).returns(false) @run.stubs(:puts) @run.classes = ['class'] Puppet::Node.expects(:search).with("whatever", :class => "class", :fqdn => nil).returns([]) @run.run_setup end end describe "when using regular nodes" do it "should fail if some classes have been specified" do $stderr.stubs(:puts) @run.classes = ['class'] @run.expects(:exit).with(24) @run.run_setup end end end describe "when running" do before :each do @run.stubs(:puts) end it "should dispatch to test if --test is used" do @run.options.stubs(:[]).with(:test).returns(true) @run.get_command.should == :test end it "should dispatch to main if --test is not used" do @run.options.stubs(:[]).with(:test).returns(false) @run.get_command.should == :main end describe "the test command" do it "should exit with exit code 0 " do @run.expects(:exit).with(0) @run.test end end describe "the main command" do before :each do @run.options.stubs(:[]).with(:parallel).returns(1) @run.options.stubs(:[]).with(:ping).returns(false) @run.options.stubs(:[]).with(:ignoreschedules).returns(false) @run.options.stubs(:[]).with(:foreground).returns(false) @run.stubs(:print) @run.stubs(:exit) $stderr.stubs(:puts) end it "should create as much childs as --parallel" do @run.options.stubs(:[]).with(:parallel).returns(3) @run.hosts = ['host1', 'host2', 'host3'] @run.stubs(:exit).raises(SystemExit) Process.stubs(:wait).returns(1).then.returns(2).then.returns(3).then.raises(Errno::ECHILD) @run.expects(:fork).times(3).returns(1).then.returns(2).then.returns(3) lambda { @run.main }.should raise_error end it "should delegate to run_for_host per host" do @run.hosts = ['host1', 'host2'] @run.stubs(:exit).raises(SystemExit) @run.stubs(:fork).returns(1).yields Process.stubs(:wait).returns(1).then.raises(Errno::ECHILD) @run.expects(:run_for_host).times(2) lambda { @run.main }.should raise_error end describe "during call of run_for_host" do before do require 'puppet/run' options = { :background => true, :ignoreschedules => false, :tags => [] } @run = Puppet::Run.new( options.dup ) @run.stubs(:status).returns("success") Puppet::Run.indirection.expects(:terminus_class=).with( :rest ) Puppet::Run.expects(:new).with( options ).returns(@run) end it "should call run on a Puppet::Run for the given host" do - @run.expects(:save).with{|req| req.uri == 'https://host:8139/production/run/host'}.returns(@run) + @run.expects(:save).with('https://host:8139/production/run/host').returns(@run) @run.run_for_host('host') end it "should exit the child with 0 on success" do @run.stubs(:status).returns("success") @run.expects(:exit).with(0) @run.run_for_host('host') end it "should exit the child with 3 on running" do @run.stubs(:status).returns("running") @run.expects(:exit).with(3) @run.run_for_host('host') end it "should exit the child with 12 on unknown answer" do @run.stubs(:status).returns("whatever") @run.expects(:exit).with(12) @run.run_for_host('host') end end end end end diff --git a/spec/unit/application/resource.rb b/spec/unit/application/resource.rb index 9d47ba56e..ebb8c4f3e 100755 --- a/spec/unit/application/resource.rb +++ b/spec/unit/application/resource.rb @@ -1,254 +1,254 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/application/resource' describe "resource" do before :each do @resource = Puppet::Application[:resource] Puppet::Util::Log.stubs(:newdestination) Puppet::Util::Log.stubs(:level=) end it "should ask Puppet::Application to not parse Puppet configuration file" do @resource.should_parse_config?.should be_false end it "should declare a main command" do @resource.should respond_to(:main) end it "should declare a host option" do @resource.should respond_to(:handle_host) end it "should declare a types option" do @resource.should respond_to(:handle_types) end it "should declare a param option" do @resource.should respond_to(:handle_param) end it "should declare a preinit block" do @resource.should respond_to(:run_preinit) end describe "in preinit" do it "should set hosts to nil" do @resource.run_preinit @resource.host.should be_nil end it "should init extra_params to empty array" do @resource.run_preinit @resource.extra_params.should == [] end it "should load Facter facts" do Facter.expects(:loadfacts).once @resource.run_preinit end end describe "when handling options" do [:debug, :verbose, :edit].each do |option| it "should declare handle_#{option} method" do @resource.should respond_to("handle_#{option}".to_sym) end it "should store argument value when calling handle_#{option}" do @resource.options.expects(:[]=).with(option, 'arg') @resource.send("handle_#{option}".to_sym, 'arg') end end it "should set options[:host] to given host" do @resource.handle_host(:whatever) @resource.host.should == :whatever end it "should load an display all types with types option" do type1 = stub_everything 'type1', :name => :type1 type2 = stub_everything 'type2', :name => :type2 Puppet::Type.stubs(:loadall) Puppet::Type.stubs(:eachtype).multiple_yields(type1,type2) @resource.stubs(:exit) @resource.expects(:puts).with(['type1','type2']) @resource.handle_types(nil) end it "should add param to extra_params list" do @resource.extra_params = [ :param1 ] @resource.handle_param("whatever") @resource.extra_params.should == [ :param1, :whatever ] end end describe "during setup" do before :each do Puppet::Log.stubs(:newdestination) Puppet::Log.stubs(:level=) Puppet.stubs(:parse_config) end it "should set console as the log destination" do Puppet::Log.expects(:newdestination).with(:console) @resource.run_setup end it "should set log level to debug if --debug was passed" do @resource.options.stubs(:[]).with(:debug).returns(true) Puppet::Log.expects(:level=).with(:debug) @resource.run_setup end it "should set log level to info if --verbose was passed" do @resource.options.stubs(:[]).with(:debug).returns(false) @resource.options.stubs(:[]).with(:verbose).returns(true) Puppet::Log.expects(:level=).with(:info) @resource.run_setup end it "should Parse puppet config" do Puppet.expects(:parse_config) @resource.run_setup end end describe "when running" do def set_args(args) (ARGV.clear << args).flatten! end def push_args(*args) @args_stack ||= [] @args_stack << ARGV.dup set_args(args) end def pop_args set_args(@args_stack.pop) end before :each do @type = stub_everything 'type', :properties => [] push_args('type') Puppet::Type.stubs(:type).returns(@type) end after :each do pop_args end it "should raise an error if no type is given" do push_args lambda { @resource.main }.should raise_error pop_args end it "should raise an error when editing a remote host" do @resource.options.stubs(:[]).with(:edit).returns(true) @resource.host = 'host' lambda { @resource.main }.should raise_error end it "should raise an error if the type is not found" do Puppet::Type.stubs(:type).returns(nil) lambda { @resource.main }.should raise_error end describe "with a host" do before :each do @resource.stubs(:puts) @resource.host = 'host' Puppet::Resource.stubs(:find ).never Puppet::Resource.stubs(:search).never Puppet::Resource.stubs(:save ).never end it "should search for resources" do Puppet::Resource.expects(:search).with('https://host:8139/production/resources/type/', {}).returns([]) @resource.main end it "should describe the given resource" do push_args('type','name') x = stub_everything 'resource' Puppet::Resource.expects(:find).with('https://host:8139/production/resources/type/name').returns(x) @resource.main pop_args end it "should add given parameters to the object" do push_args('type','name','param=temp') res = stub "resource" - res.expects(:save).with{|x| x.uri == 'https://host:8139/production/resources/type/name'}.returns(res) + res.expects(:save).with('https://host:8139/production/resources/type/name').returns(res) res.expects(:collect) res.expects(:to_manifest) Puppet::Resource.expects(:new).with('type', 'name', {'param' => 'temp'}).returns(res) @resource.main pop_args end end describe "without a host" do before :each do @resource.stubs(:puts) @resource.host = nil Puppet::Resource.stubs(:find ).never Puppet::Resource.stubs(:search).never Puppet::Resource.stubs(:save ).never end it "should search for resources" do Puppet::Resource.expects(:search).with('type/', {}).returns([]) @resource.main end it "should describe the given resource" do push_args('type','name') x = stub_everything 'resource' Puppet::Resource.expects(:find).with('type/name').returns(x) @resource.main pop_args end it "should add given parameters to the object" do push_args('type','name','param=temp') res = stub "resource" - res.expects(:save).with{|x| x.uri == nil}.returns(res) + res.expects(:save).with('type/name').returns(res) res.expects(:collect) res.expects(:to_manifest) Puppet::Resource.expects(:new).with('type', 'name', {'param' => 'temp'}).returns(res) @resource.main pop_args end end end end diff --git a/spec/unit/file_bucket/dipper.rb b/spec/unit/file_bucket/dipper.rb index ffa2d4576..5d6625103 100755 --- a/spec/unit/file_bucket/dipper.rb +++ b/spec/unit/file_bucket/dipper.rb @@ -1,110 +1,104 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' 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 File.stubs(:exist?).returns true File.stubs(:read).returns "content" @dipper = Puppet::FileBucket::Dipper.new(:Path => "/my/bucket") 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(:exist?).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) + bucketfile.expects(:save).with('md5/DIGEST123') 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(:exist?).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(:exist?).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) + bucketfile.expects(:save).with('https://puppetmaster:31337/production/file_bucket_file/md5/DIGEST123') 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(:exist?).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/indirector.rb b/spec/unit/indirector.rb index 0b4c61695..cb1b04932 100755 --- a/spec/unit/indirector.rb +++ b/spec/unit/indirector.rb @@ -1,157 +1,157 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../spec_helper' require 'puppet/defaults' require 'puppet/indirector' describe Puppet::Indirector, " when available to a model" do before do @thingie = Class.new do extend Puppet::Indirector end end it "should provide a way for the model to register an indirection under a name" do @thingie.should respond_to(:indirects) end end describe Puppet::Indirector, "when registering an indirection" do before do @thingie = Class.new do extend Puppet::Indirector attr_reader :name def initialize(name) @name = name end end end it "should require a name when registering a model" do Proc.new {@thingie.send(:indirects) }.should raise_error(ArgumentError) end it "should create an indirection instance to manage each indirecting model" do @indirection = @thingie.indirects(:test) @indirection.should be_instance_of(Puppet::Indirector::Indirection) end it "should not allow a model to register under multiple names" do # Keep track of the indirection instance so we can delete it on cleanup @indirection = @thingie.indirects :first Proc.new { @thingie.indirects :second }.should raise_error(ArgumentError) end it "should make the indirection available via an accessor" do @indirection = @thingie.indirects :first @thingie.indirection.should equal(@indirection) end it "should pass any provided options to the indirection during initialization" do klass = mock 'terminus class' Puppet::Indirector::Indirection.expects(:new).with(@thingie, :first, {:some => :options}) @indirection = @thingie.indirects :first, :some => :options end it "should extend the class with the Format Handler" do @indirection = @thingie.indirects :first @thingie.metaclass.ancestors.should be_include(Puppet::Network::FormatHandler) end after do @indirection.delete if @indirection end end describe "Delegated Indirection Method", :shared => true do it "should delegate to the indirection" do @indirection.expects(@method) @thingie.send(@method, "me") end it "should pass all of the passed arguments directly to the indirection instance" do @indirection.expects(@method).with("me", :one => :two) @thingie.send(@method, "me", :one => :two) end it "should return the results of the delegation as its result" do request = mock 'request' @indirection.expects(@method).returns "yay" @thingie.send(@method, "me").should == "yay" end end describe Puppet::Indirector, "when redirecting a model" do before do @thingie = Class.new do extend Puppet::Indirector attr_reader :name def initialize(name) @name = name end end @indirection = @thingie.send(:indirects, :test) end it "should include the Envelope module in the model" do @thingie.ancestors.should be_include(Puppet::Indirector::Envelope) end describe "when finding instances via the model" do before { @method = :find } it_should_behave_like "Delegated Indirection Method" end describe "when destroying instances via the model" do before { @method = :destroy } it_should_behave_like "Delegated Indirection Method" end describe "when searching for instances via the model" do before { @method = :search } it_should_behave_like "Delegated Indirection Method" end describe "when expiring instances via the model" do before { @method = :expire } it_should_behave_like "Delegated Indirection Method" end # This is an instance method, so it behaves a bit differently. describe "when saving instances via the model" do before do @instance = @thingie.new("me") end it "should delegate to the indirection" do @indirection.expects(:save) @instance.save end - it "should pass the instance and all arguments to the indirection's :save method" do - @indirection.expects(:save).with(@instance, :one => :two) - @instance.save :one => :two + it "should pass the instance and an optional key to the indirection's :save method" do + @indirection.expects(:save).with("key", @instance) + @instance.save "key" end it "should return the results of the delegation as its result" do request = mock 'request' @indirection.expects(:save).returns "yay" @instance.save.should == "yay" end end it "should give the model the ability to set the indirection terminus class" do @indirection.expects(:terminus_class=).with(:myterm) @thingie.terminus_class = :myterm end it "should give the model the ability to set the indirection cache class" do @indirection.expects(:cache_class=).with(:mycache) @thingie.cache_class = :mycache end after do @indirection.delete end end diff --git a/spec/unit/indirector/indirection.rb b/spec/unit/indirector/indirection.rb index 02d04a3f7..0663fe55e 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 empty" do - Puppet::Indirector::Request.expects(:new).with { |name, method, key, args| args == {} } + it "should not pass options if none are supplied" do + Puppet::Indirector::Request.expects(:new).with { |*args| args.length < 4 } @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 b885779ed..58226397e 100755 --- a/spec/unit/indirector/request.rb +++ b/spec/unit/indirector/request.rb @@ -1,308 +1,304 @@ #!/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 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/http/handler.rb b/spec/unit/network/http/handler.rb index 2218a0a78..e6dd88130 100755 --- a/spec/unit/network/http/handler.rb +++ b/spec/unit/network/http/handler.rb @@ -1,455 +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 - @model_instance.expects(:save).with(@irequest) + @model_instance.expects(:save).with('key').once @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/ssl/certificate_request.rb b/spec/unit/ssl/certificate_request.rb index a4eee92d6..63bc9f110 100755 --- a/spec/unit/ssl/certificate_request.rb +++ b/spec/unit/ssl/certificate_request.rb @@ -1,220 +1,220 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/ssl/certificate_request' require 'puppet/ssl/key' describe Puppet::SSL::CertificateRequest do before do @class = Puppet::SSL::CertificateRequest end it "should be extended with the Indirector module" do @class.metaclass.should be_include(Puppet::Indirector) end it "should indirect certificate_request" do @class.indirection.name.should == :certificate_request end it "should use any provided name as its name" do @class.new("myname").name.should == "myname" end it "should only support the text format" do @class.supported_formats.should == [:s] end describe "when converting from a string" do it "should create a CSR instance with its name set to the CSR subject and its content set to the extracted CSR" do csr = stub 'csr', :subject => "/CN=Foo.madstop.com" OpenSSL::X509::Request.expects(:new).with("my csr").returns(csr) mycsr = stub 'sslcsr' mycsr.expects(:content=).with(csr) @class.expects(:new).with("foo.madstop.com").returns mycsr @class.from_s("my csr") end end describe "when managing instances" do before do @request = @class.new("myname") end it "should have a name attribute" do @request.name.should == "myname" end it "should downcase its name" do @class.new("MyName").name.should == "myname" end it "should have a content attribute" do @request.should respond_to(:content) end it "should be able to read requests from disk" do path = "/my/path" File.expects(:read).with(path).returns("my request") request = mock 'request' OpenSSL::X509::Request.expects(:new).with("my request").returns(request) @request.read(path).should equal(request) @request.content.should equal(request) end it "should return an empty string when converted to a string with no request" do @request.to_s.should == "" end it "should convert the request to pem format when converted to a string" do request = mock 'request', :to_pem => "pem" @request.content = request @request.to_s.should == "pem" end it "should have a :to_text method that it delegates to the actual key" do real_request = mock 'request' real_request.expects(:to_text).returns "requesttext" @request.content = real_request @request.to_text.should == "requesttext" end end describe "when generating" do before do @instance = @class.new("myname") key = Puppet::SSL::Key.new("myname") @key = key.generate @request = OpenSSL::X509::Request.new OpenSSL::X509::Request.expects(:new).returns(@request) @request.stubs(:verify).returns(true) end it "should use the content of the provided key if the key is a Puppet::SSL::Key instance" do key = Puppet::SSL::Key.new("test") key.expects(:content).returns @key @request.expects(:sign).with{ |key, digest| key == @key } @instance.generate(key) end it "should log that it is creating a new certificate request" do Puppet.expects(:info).twice @instance.generate(@key) end it "should set the subject to [CN, name]" do subject = mock 'subject' OpenSSL::X509::Name.expects(:new).with([["CN", @instance.name]]).returns(subject) @request.expects(:subject=).with(subject) @instance.generate(@key) end it "should set the CN to the CSR name when the CSR is not for a CA" do subject = mock 'subject' OpenSSL::X509::Name.expects(:new).with { |subject| subject[0][1] == @instance.name }.returns(subject) @request.expects(:subject=).with(subject) @instance.generate(@key) end it "should set the CN to the :ca_name setting when the CSR is for a CA" do subject = mock 'subject' Puppet.settings.expects(:value).with(:ca_name).returns "mycertname" OpenSSL::X509::Name.expects(:new).with { |subject| subject[0][1] == "mycertname" }.returns(subject) @request.expects(:subject=).with(subject) Puppet::SSL::CertificateRequest.new(Puppet::SSL::CA_NAME).generate(@key) end it "should set the version to 0" do @request.expects(:version=).with(0) @instance.generate(@key) end it "should set the public key to the provided key's public key" do # Yay, the private key extracts a new key each time. pubkey = @key.public_key @key.stubs(:public_key).returns pubkey @request.expects(:public_key=).with(@key.public_key) @instance.generate(@key) end it "should sign the csr with the provided key and a digest" do digest = mock 'digest' OpenSSL::Digest::MD5.expects(:new).returns(digest) @request.expects(:sign).with(@key, digest) @instance.generate(@key) end it "should verify the generated request using the public key" do # Stupid keys don't have a competent == method. @request.expects(:verify).with { |public_key| public_key.to_s == @key.public_key.to_s }.returns true @instance.generate(@key) end it "should fail if verification fails" do @request.expects(:verify).returns false lambda { @instance.generate(@key) }.should raise_error(Puppet::Error) end it "should fingerprint the request" do @instance.expects(:fingerprint) @instance.generate(@key) end it "should display the fingerprint" do Puppet.stubs(:info) @instance.stubs(:fingerprint).returns("FINGERPRINT") Puppet.expects(:info).with { |s| s =~ /FINGERPRINT/ } @instance.generate(@key) end it "should return the generated request" do @instance.generate(@key).should equal(@request) end it "should set its content to the generated request" do @instance.generate(@key) @instance.content.should equal(@request) end end describe "when a CSR is saved" do it "should allow arguments" do csr = Puppet::SSL::CertificateRequest.new("me") csr.class.indirection.stubs(:save) lambda { csr.save :ipaddress => "foo" }.should_not raise_error end describe "and a CA is available" do it "should save the CSR and trigger autosigning" do ca = mock 'ca', :autosign Puppet::SSL::CertificateAuthority.expects(:instance).returns ca csr = Puppet::SSL::CertificateRequest.new("me") - Puppet::SSL::CertificateRequest.indirection.expects(:save).with(csr) + Puppet::SSL::CertificateRequest.indirection.expects(:save).with(nil, csr) csr.save end end describe "and a CA is not available" do it "should save the CSR" do Puppet::SSL::CertificateAuthority.expects(:instance).returns nil csr = Puppet::SSL::CertificateRequest.new("me") - Puppet::SSL::CertificateRequest.indirection.expects(:save).with(csr) + Puppet::SSL::CertificateRequest.indirection.expects(:save).with(nil, csr) csr.save end end end end