diff --git a/lib/puppet/indirector/indirection.rb b/lib/puppet/indirector/indirection.rb index 5d8cfe9b5..7ac214cd0 100644 --- a/lib/puppet/indirector/indirection.rb +++ b/lib/puppet/indirector/indirection.rb @@ -1,304 +1,304 @@ require 'puppet/util/docs' require 'puppet/indirector/envelope' require 'puppet/indirector/request' require 'puppet/util/cacher' # The class that connects functional classes with their different collection # back-ends. Each indirection has a set of associated terminus classes, # each of which is a subclass of Puppet::Indirector::Terminus. class Puppet::Indirector::Indirection include Puppet::Util::Cacher include Puppet::Util::Docs @@indirections = [] # Find an indirection by name. This is provided so that Terminus classes # can specifically hook up with the indirections they are associated with. def self.instance(name) @@indirections.find { |i| i.name == name } end # Return a list of all known indirections. Used to generate the # reference. def self.instances @@indirections.collect { |i| i.name } end # Find an indirected model by name. This is provided so that Terminus classes # can specifically hook up with the indirections they are associated with. def self.model(name) return nil unless match = @@indirections.find { |i| i.name == name } match.model end attr_accessor :name, :model # Create and return our cache terminus. def cache raise(Puppet::DevError, "Tried to cache when no cache class was set") unless cache_class terminus(cache_class) end # Should we use a cache? def cache? cache_class ? true : false end attr_reader :cache_class # Define a terminus class to be used for caching. def cache_class=(class_name) validate_terminus_class(class_name) if class_name @cache_class = class_name end # This is only used for testing. def delete @@indirections.delete(self) if @@indirections.include?(self) end # Set the time-to-live for instances created through this indirection. def ttl=(value) raise ArgumentError, "Indirection TTL must be an integer" unless value.is_a?(Fixnum) @ttl = value end # Default to the runinterval for the ttl. def ttl unless defined?(@ttl) @ttl = Puppet[:runinterval].to_i end @ttl end # Calculate the expiration date for a returned instance. def expiration Time.now + ttl end # Generate the full doc string. def doc text = "" if defined? @doc and @doc text += scrub(@doc) + "\n\n" end if s = terminus_setting() text += "* **Terminus Setting**: %s" % terminus_setting end text end def initialize(model, name, options = {}) @model = model @name = name @cache_class = nil @terminus_class = nil raise(ArgumentError, "Indirection %s is already defined" % @name) if @@indirections.find { |i| i.name == @name } @@indirections << self if mod = options[:extend] extend(mod) options.delete(:extend) end # This is currently only used for cache_class and terminus_class. options.each do |name, value| begin send(name.to_s + "=", value) rescue NoMethodError raise ArgumentError, "%s is not a valid Indirection parameter" % name end end end # Set up our request object. def request(method, key, arguments = nil) Puppet::Indirector::Request.new(self.name, method, key, arguments) 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 don't actually # remove it, we expire it and write it back out to disk. This way people # can still use the expired object if they want. def expire(key, *args) request = request(:expire, key, *args) return nil unless cache? return nil unless instance = cache.find(request(:find, key, *args)) Puppet.info "Expiring the %s cache of %s" % [self.name, instance.name] # Set an expiration date in the past instance.expiration = Time.now - 60 cache.save(request(:save, instance, *args)) 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) # See if our instance is in the cache and up to date. if 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] else Puppet.debug "Using cached %s for %s" % [self.name, request.key] return cached end 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 result end return nil end # Remove something via the terminus. def destroy(key, *args) request = request(:destroy, key, *args) terminus = prepare(request) result = terminus.destroy(request) if cache? and cached = cache.find(request(:find, key, *args)) # Reuse the existing request, since it's equivalent. cache.destroy(request) end result end # Search for more than one instance. Should always return an array. def search(key, *args) request = request(:search, key, *args) terminus = prepare(request) if result = terminus.search(request) raise Puppet::DevError, "Search results from terminus %s are not an array" % terminus.name unless result.is_a?(Array) result.each do |instance| instance.expiration ||= self.expiration end return result end end # Save the instance in the appropriate terminus. This method is # normally an instance method on the indirected class. def save(instance, *args) request = request(:save, instance, *args) 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.key] + 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 8227db174..c6f268ab2 100644 --- a/lib/puppet/indirector/request.rb +++ b/lib/puppet/indirector/request.rb @@ -1,111 +1,116 @@ 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 :indirection_name, :key, :method, :options, :instance, :node, :ip, :authenticated, :ignore_cache, :ignore_terminus attr_accessor :server, :port, :uri, :protocol # Is this an authenticated request? def authenticated? # Double negative, so we just get true or false ! ! authenticated 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) @indirection_name, @method = indirection_name, method @options = options.inject({}) do |result, ary| param, value = ary if respond_to?(param.to_s + "=") send(param.to_s + "=", value) else result[param] = value end result end if key.is_a?(String) or key.is_a?(Symbol) # If the request key is a URI, then we need to treat it specially, # because it rewrites the key. We could otherwise strip server/port/etc # info out in the REST class, but it seemed bad design for the REST # class to rewrite the key. if key.to_s =~ /^\w+:\/\// # it's a URI set_uri_key(key) else @key = key end else @instance = key @key = @instance.name end end # Look up the indirection based on the name provided. def indirection Puppet::Indirector::Indirection.instance(indirection_name) end # 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 + def to_s + return uri if uri + return "/%s/%s" % [indirection_name, key] + end + private # 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" % [source, detail.to_s] end # Just short-circuit these to full paths if uri.scheme == "file" @key = uri.path return end @server = uri.host if uri.host # If the URI class can look up the scheme, it will provide a port, # otherwise it will default to '0'. if uri.port.to_i == 0 and uri.scheme == "puppet" @port = Puppet.settings[:masterport].to_i else @port = uri.port.to_i end @protocol = uri.scheme @key = uri.path.sub(/^\//, '') end end diff --git a/spec/unit/indirector/request.rb b/spec/unit/indirector/request.rb index 12e196f77..dc1e4e39d 100755 --- a/spec/unit/indirector/request.rb +++ b/spec/unit/indirector/request.rb @@ -1,166 +1,174 @@ #!/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 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 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") end it "should set the request key to the full file path" do @request.key.should == "/my/file" 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 unqualified path from the URI" do Puppet::Indirector::Request.new(:ind, :method, "http:///stuff").key.should == "stuff" 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 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 end