diff options
author | Luke Kanies <luke@madstop.com> | 2007-11-12 22:11:40 -0600 |
---|---|---|
committer | Luke Kanies <luke@madstop.com> | 2007-11-12 22:11:40 -0600 |
commit | fa1924eb04a2d6600349eddf13e1f3e62b45d6ce (patch) | |
tree | 978aa0e92812f5854978048162c6e2ab752dad72 | |
parent | a535cbbe148802c0afe62cd2d5b29d0768b3a0f0 (diff) | |
parent | 72510bfaa65e97f4eaaf246ef8f1c155716967b6 (diff) | |
download | puppet-fa1924eb04a2d6600349eddf13e1f3e62b45d6ce.tar.gz puppet-fa1924eb04a2d6600349eddf13e1f3e62b45d6ce.tar.xz puppet-fa1924eb04a2d6600349eddf13e1f3e62b45d6ce.zip |
Merge branch 'master' of ssh://reductivelabs.com/opt/rl/git/puppet-luke
61 files changed, 2599 insertions, 933 deletions
@@ -1,3 +1,19 @@ + Fixed #800 -- invalid configurations are no longer + cached. This was done partially by adding a relationship + validation step once the entire configuration is created, + but it also required the previously-mentioned changes + to how the configuration retrieval process works. + + Removed some functionality from the Master client, + since the local functionality has been replaced + with the Indirector already, and rearranging how configuration + retrieval is done to fix ordering and caching bugs. + + The node scope is now above all other scopes besides + the 'main' scope, which should help make its variables + visible to other classes, assuming those classes were + not included in the node's parent. + Replaced GRATR::Digraph with Puppet::SimpleGraph as the base class for Puppet's graphing. Functionality should be equivalent but with dramatically better diff --git a/lib/puppet/indirector/facts/memory.rb b/lib/puppet/indirector/facts/memory.rb new file mode 100644 index 000000000..3c10d5964 --- /dev/null +++ b/lib/puppet/indirector/facts/memory.rb @@ -0,0 +1,9 @@ +require 'puppet/node/facts' +require 'puppet/indirector/memory' + +class Puppet::Node::Facts::Memory < Puppet::Indirector::Memory + desc "Keep track of facts in memory but nowhere else. This is used for + one-time compiles, such as what the stand-alone ``puppet`` does. + To use this terminus, you must load it with the data you want it + to contain." +end diff --git a/lib/puppet/indirector/indirection.rb b/lib/puppet/indirector/indirection.rb index 9a9b1c0bf..816b4ffc5 100644 --- a/lib/puppet/indirector/indirection.rb +++ b/lib/puppet/indirector/indirection.rb @@ -13,6 +13,14 @@ class Puppet::Indirector::Indirection @@indirections.find { |i| i.name == 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) + match = @@indirections.find { |i| i.name == name } + return nil unless match + match.model + end + attr_accessor :name, :model # Create and return our cache terminus. diff --git a/lib/puppet/metatype/instances.rb b/lib/puppet/metatype/instances.rb index 2f9918067..8cc648e8f 100644 --- a/lib/puppet/metatype/instances.rb +++ b/lib/puppet/metatype/instances.rb @@ -99,6 +99,8 @@ class Puppet::Type end end + # If they've specified a type and called on the base, then + # delegate to the subclass. if type if typeklass = self.type(type) return typeklass.create(hash) @@ -233,19 +235,22 @@ class Puppet::Type hash.delete :name end - unless title - raise Puppet::Error, - "You must specify a title for objects of type %s" % self.to_s + if configuration = hash[:configuration] + hash.delete(:configuration) end + raise(Puppet::Error, "You must specify a title for objects of type %s" % self.to_s) unless title + if hash.include? :type unless self.validattr? :type hash.delete :type end end + # okay, now make a transobject out of hash begin trans = Puppet::TransObject.new(title, self.name.to_s) + trans.configuration = configuration if configuration hash.each { |param, value| trans[param] = value } diff --git a/lib/puppet/metatype/metaparams.rb b/lib/puppet/metatype/metaparams.rb index 1ab3f26c1..eb158a47d 100644 --- a/lib/puppet/metatype/metaparams.rb +++ b/lib/puppet/metatype/metaparams.rb @@ -94,7 +94,7 @@ class Puppet::Type # We've got four relationship metaparameters, so this method is used # to reduce code duplication between them. - def store_relationship(param, values) + def munge_relationship(param, values) # We need to support values passed in as an array or as a # resource reference. result = [] @@ -194,20 +194,24 @@ class Puppet::Type unless aliases.is_a?(Array) aliases = [aliases] end - @resource.info "Adding aliases %s" % aliases.collect { |a| - a.inspect - }.join(", ") + + raise(ArgumentError, "Cannot add aliases without a configuration") unless @resource.configuration + + @resource.info "Adding aliases %s" % aliases.collect { |a| a.inspect }.join(", ") + aliases.each do |other| - if obj = @resource.class[other] - unless obj == @resource - self.fail( - "%s can not create alias %s: object already exists" % - [@resource.title, other] - ) + if obj = @resource.configuration.resource(@resource.class.name, other) + unless obj.object_id == @resource.object_id + self.fail("%s can not create alias %s: object already exists" % [@resource.title, other]) end next end + + # LAK:FIXME Old-school, add the alias to the class. @resource.class.alias(other, @resource) + + # Newschool, add it to the configuration. + @resource.configuration.alias(@resource, other) end end end @@ -247,7 +251,16 @@ class Puppet::Type end def munge(rels) - @resource.store_relationship(self.class.name, rels) + @resource.munge_relationship(self.class.name, rels) + end + + def validate_relationship + @value.each do |value| + unless @resource.configuration.resource(*value) + description = self.class.direction == :in ? "dependency" : "dependent" + raise Puppet::Error, "Could not find #{description} %s[%s]" % [value[0].to_s.capitalize, value[1]] + end + end end # Create edges from each of our relationships. :in diff --git a/lib/puppet/network/client/master.rb b/lib/puppet/network/client/master.rb index 5408cabe4..ea351ddc3 100644 --- a/lib/puppet/network/client/master.rb +++ b/lib/puppet/network/client/master.rb @@ -139,63 +139,57 @@ class Puppet::Network::Client::Master < Puppet::Network::Client facts = self.class.facts end - if self.configuration or FileTest.exists?(self.cachefile) - if self.fresh?(facts) - Puppet.info "Config is up to date" - if self.configuration - return - end - if oldtext = self.retrievecache - begin - @configuration = YAML.load(oldtext).to_configuration - rescue => detail - Puppet.warning "Could not load cached configuration: %s" % detail - end - return - end - end - end - Puppet.debug("getting config") + raise Puppet::Network::ClientError.new("Could not retrieve any facts") unless facts.length > 0 # Retrieve the plugins. - if Puppet[:pluginsync] - getplugins() - end + getplugins() if Puppet[:pluginsync] - unless facts.length > 0 - raise Puppet::Network::ClientError.new( - "Could not retrieve any facts" - ) + if (self.configuration or FileTest.exist?(self.cachefile)) and self.fresh?(facts) + Puppet.info "Configuration is up to date" + return if use_cached_config end - unless objects = get_actual_config(facts) - @configuration = nil + Puppet.debug("Retrieving configuration") + + # If we can't retrieve the configuration, just return, which will either + # fail, or use the in-memory configuration. + unless yaml_objects = get_actual_config(facts) + use_cached_config(true) return end - unless objects.is_a?(Puppet::TransBucket) - raise NetworkClientError, - "Invalid returned objects of type %s" % objects.class + begin + objects = YAML.load(yaml_objects) + rescue => detail + msg = "Configuration could not be translated from yaml" + msg += "; using cached configuration" if use_cached_config(true) + Puppet.warning msg + return end self.setclasses(objects.classes) # Clear all existing objects, so we can recreate our stack. - if self.configuration - clear() - end + clear() if self.configuration # Now convert the objects to a puppet configuration graph. - @configuration = objects.to_configuration + begin + @configuration = objects.to_configuration + rescue => detail + clear() + puts detail.backtrace if Puppet[:trace] + msg = "Configuration could not be instantiated: %s" % detail + msg += "; using cached configuration" if use_cached_config(true) + Puppet.warning msg + return + end - if @configuration.nil? - raise Puppet::Error, "Configuration could not be processed" + if ! @configuration.from_cache + self.cache(yaml_objects) end # Keep the state database up to date. @configuration.host_config = true - - return @configuration end # A simple proxy method, so it's easy to test. @@ -270,11 +264,9 @@ class Puppet::Network::Client::Master < Puppet::Network::Client Puppet.err "Could not retrieve configuration: %s" % detail end - if defined? @configuration and @configuration + if self.configuration @configuration.retrieval_duration = duration - unless @local - Puppet.notice "Starting configuration run" - end + Puppet.notice "Starting configuration run" unless @local benchmark(:notice, "Finished configuration run") do @configuration.apply(options) end @@ -500,34 +492,16 @@ class Puppet::Network::Client::Master < Puppet::Network::Client # Actually retrieve the configuration, either from the server or from a # local master. def get_actual_config(facts) - if @local - return get_local_config(facts) - else - begin - Timeout::timeout(self.class.timeout) do - return get_remote_config(facts) - end - rescue Timeout::Error - Puppet.err "Configuration retrieval timed out" - return nil + begin + Timeout::timeout(self.class.timeout) do + return get_remote_config(facts) end + rescue Timeout::Error + Puppet.err "Configuration retrieval timed out" + return nil end end - # Retrieve a configuration from a local master. - def get_local_config(facts) - # If we're local, we don't have to do any of the conversion - # stuff. - objects = @driver.getconfig(facts, "yaml") - @compile_time = Time.now - - if objects == "" - raise Puppet::Error, "Could not retrieve configuration" - end - - return objects - end - # Retrieve a config from a remote master. def get_remote_config(facts) textobjects = "" @@ -545,45 +519,18 @@ class Puppet::Network::Client::Master < Puppet::Network::Client end rescue => detail - puts detail.backtrace Puppet.err "Could not retrieve configuration: %s" % detail - - unless Puppet[:usecacheonfailure] - @configuration = nil - Puppet.warning "Not using cache on failed configuration" - return - end + return nil end end - fromcache = false - if textobjects == "" - unless textobjects = self.retrievecache - raise Puppet::Error.new( - "Cannot connect to server and there is no cached configuration" - ) - end - Puppet.warning "Could not get config; using cached copy" - fromcache = true - else - @compile_time = Time.now - Puppet::Util::Storage.cache(:configuration)[:facts] = facts - Puppet::Util::Storage.cache(:configuration)[:compile_time] = @compile_time - end + return nil if textobjects == "" - begin - objects = YAML.load(textobjects) - rescue => detail - raise Puppet::Error, - "Could not understand configuration: %s" % - detail.to_s - end + @compile_time = Time.now + Puppet::Util::Storage.cache(:configuration)[:facts] = facts + Puppet::Util::Storage.cache(:configuration)[:compile_time] = @compile_time - if @cache and ! fromcache - self.cache(textobjects) - end - - return objects + return textobjects end def lockfile @@ -609,4 +556,32 @@ class Puppet::Network::Client::Master < Puppet::Network::Client Puppet.info "Sleeping for %s seconds (splay is enabled)" % time sleep(time) end + + private + + # Use our cached config, optionally specifying whether this is + # necessary because of a failure. + def use_cached_config(because_of_failure = false) + return true if self.configuration + + if because_of_failure and ! Puppet[:usecacheonfailure] + @configuration = nil + Puppet.warning "Not using cache on failed configuration" + return false + end + + return false unless oldtext = self.retrievecache + + begin + @configuration = YAML.load(oldtext).to_configuration + @configuration.from_cache = true + @configuration.host_config = true + rescue => detail + puts detail.backtrace if Puppet[:trace] + Puppet.warning "Could not load cached configuration: %s" % detail + clear + return false + end + return true + end end diff --git a/lib/puppet/network/handler/runner.rb b/lib/puppet/network/handler/runner.rb index a8d0da9ce..c97e4791a 100755 --- a/lib/puppet/network/handler/runner.rb +++ b/lib/puppet/network/handler/runner.rb @@ -43,7 +43,7 @@ class Puppet::Network::Handler end if ignoreschedules - msg += " without schedules" + msg += " ignoring schedules" end Puppet.notice msg diff --git a/lib/puppet/network/http.rb b/lib/puppet/network/http.rb new file mode 100644 index 000000000..062c67c71 --- /dev/null +++ b/lib/puppet/network/http.rb @@ -0,0 +1,13 @@ +class Puppet::Network::HTTP + def self.server_class_by_type(kind) + return Puppet::Network::HTTP::WEBrick if kind.to_sym == :webrick + if kind.to_sym == :mongrel + raise ArgumentError, "Mongrel is not installed on this platform" unless Puppet.features.mongrel? + return Puppet::Network::HTTP::Mongrel + end + raise ArgumentError, "Unknown HTTP server name [#{kind}]" + end +end + +require 'puppet/network/http/webrick' +require 'puppet/network/http/mongrel' diff --git a/lib/puppet/network/http/handler.rb b/lib/puppet/network/http/handler.rb new file mode 100644 index 000000000..773381c8d --- /dev/null +++ b/lib/puppet/network/http/handler.rb @@ -0,0 +1,109 @@ +class Puppet::Network::HTTP::Handler + def initialize(args = {}) + raise ArgumentError unless @server = args[:server] + raise ArgumentError unless @handler = args[:handler] + @model = find_model_for_handler(@handler) + register_handler + end + + # handle an HTTP request + def process(request, response) + return do_find(request, response) if get?(request) and singular?(request) + return do_search(request, response) if get?(request) and plural?(request) + return do_destroy(request, response) if delete?(request) and singular?(request) + return do_save(request, response) if put?(request) and singular?(request) + raise ArgumentError, "Did not understand HTTP #{http_method(request)} request for '#{path(request)}'" + rescue Exception => e + return do_exception(request, response, e) + end + + private + + def do_find(request, response) + key = request_key(request) || raise(ArgumentError, "Could not locate lookup key in request path [#{path}]") + args = params(request) + result = @model.find(key, args).to_yaml + encode_result(request, response, result) + end + + def do_search(request, response) + args = params(request) + result = @model.search(args).collect {|obj| obj.to_yaml } + encode_result(request, response, result) + end + + def do_destroy(request, response) + key = request_key(request) || raise(ArgumentError, "Could not locate lookup key in request path [#{path}]") + args = params(request) + result = @model.destroy(key, args) + encode_result(request, response, YAML.dump(result)) + end + + def do_save(request, response) + data = body(request) + raise ArgumentError, "No data to save" if !data or data.empty? + args = params(request) + obj = @model.new + result = obj.save(args.merge(:data => data)).to_yaml + encode_result(request, response, result) + end + + def do_exception(request, response, exception, status=404) + encode_result(request, response, exception.to_s, status) + end + + def find_model_for_handler(handler) + Puppet::Indirector::Indirection.model(handler) || + raise(ArgumentError, "Cannot locate indirection [#{handler}].") + 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 + + def singular?(request) + %r{/#{@handler.to_s}$}.match(path(request)) + end + + def plural?(request) + %r{/#{@handler.to_s}s$}.match(path(request)) + end + + # methods specific to a given web server + + def register_handler + raise NotImplementedError + end + + 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 encode_result(request, response, result, status = 200) + raise NotImplementedError + end +end diff --git a/lib/puppet/network/http/mongrel.rb b/lib/puppet/network/http/mongrel.rb new file mode 100644 index 000000000..8ea669531 --- /dev/null +++ b/lib/puppet/network/http/mongrel.rb @@ -0,0 +1,54 @@ +require 'mongrel' if Puppet.features.mongrel? + +require 'puppet/network/http/mongrel/rest' +require 'puppet/network/http/mongrel/xmlrpc' + +class Puppet::Network::HTTP::Mongrel + def initialize(args = {}) + @listening = false + end + + def listen(args = {}) + raise ArgumentError, ":handlers must be specified." if !args[:handlers] or args[:handlers].empty? + raise ArgumentError, ":protocols must be specified." if !args[:protocols] or args[:protocols].empty? + raise ArgumentError, ":address must be specified." unless args[:address] + raise ArgumentError, ":port must be specified." unless args[:port] + raise "Mongrel server is already listening" if listening? + + @protocols = args[:protocols] + @handlers = args[:handlers] + @server = Mongrel::HttpServer.new(args[:address], args[:port]) + + setup_handlers + + @server.run + @listening = true + end + + def unlisten + raise "Mongrel server is not listening" unless listening? + @server.graceful_shutdown + @listening = false + end + + def listening? + @listening + end + + private + + def setup_handlers + @protocols.each do |protocol| + @handlers.each do |handler| + class_for_protocol(protocol).new(:server => @server, :handler => handler) + end + end + end + + # TODO/FIXME: need a spec which forces delegation to the real class + def class_for_protocol(protocol) + return Puppet::Network::HTTP::MongrelREST if protocol.to_sym == :rest + return Puppet::Network::HTTP::MongrelXMLRPC if protocol.to_sym == :xmlrpc + raise ArgumentError, "Unknown protocol [#{protocol}]." + end +end diff --git a/lib/puppet/network/http/mongrel/rest.rb b/lib/puppet/network/http/mongrel/rest.rb new file mode 100644 index 000000000..db63613ab --- /dev/null +++ b/lib/puppet/network/http/mongrel/rest.rb @@ -0,0 +1,37 @@ +require 'puppet/network/http/handler' + +class Puppet::Network::HTTP::MongrelREST < Puppet::Network::HTTP::Handler + + private + + def register_handler + @server.register('/' + @handler.to_s, self) + @server.register('/' + @handler.to_s + 's', self) + end + + def http_method(request) + request.params[Mongrel::Const::REQUEST_METHOD] + end + + def path(request) + '/' + request.params[Mongrel::Const::REQUEST_PATH].split('/')[1] + end + + def request_key(request) + request.params[Mongrel::Const::REQUEST_PATH].split('/')[2] + end + + def body(request) + request.body + end + + def params(request) + Mongrel::HttpRequest.query_parse(request.params["QUERY_STRING"]) + end + + def encode_result(request, response, result, status = 200) + response.start(status) do |head, body| + body.write(result) + end + end +end diff --git a/lib/puppet/network/http/mongrel/xmlrpc.rb b/lib/puppet/network/http/mongrel/xmlrpc.rb new file mode 100644 index 000000000..92acd4f0e --- /dev/null +++ b/lib/puppet/network/http/mongrel/xmlrpc.rb @@ -0,0 +1,4 @@ +class Puppet::Network::HTTP::MongrelXMLRPC + def initialize(args = {}) + end +end diff --git a/lib/puppet/network/http/webrick.rb b/lib/puppet/network/http/webrick.rb new file mode 100644 index 000000000..c4b2ed3c6 --- /dev/null +++ b/lib/puppet/network/http/webrick.rb @@ -0,0 +1,51 @@ +require 'webrick' +require 'webrick/https' +require 'puppet/network/http/webrick/rest' +require 'puppet/network/http/webrick/xmlrpc' + +class Puppet::Network::HTTP::WEBrick + def initialize(args = {}) + @listening = false + end + + def listen(args = {}) + raise ArgumentError, ":handlers must be specified." if !args[:handlers] or args[:handlers].empty? + raise ArgumentError, ":protocols must be specified." if !args[:protocols] or args[:protocols].empty? + raise ArgumentError, ":address must be specified." unless args[:address] + raise ArgumentError, ":port must be specified." unless args[:port] + raise "WEBrick server is already listening" if listening? + + @protocols = args[:protocols] + @handlers = args[:handlers] + @server = WEBrick::HTTPServer.new(:BindAddress => args[:address], :Port => args[:port]) + setup_handlers + @server.start + @listening = true + end + + def unlisten + raise "WEBrick server is not listening" unless listening? + @server.shutdown + @listening = false + end + + def listening? + @listening + end + + private + + def setup_handlers + @protocols.each do |protocol| + @handlers.each do |handler| + class_for_protocol(protocol).new(:server => @server, :handler => handler) + end + end + end + + def class_for_protocol(protocol) + return Puppet::Network::HTTP::WEBrickREST if protocol.to_sym == :rest + return Puppet::Network::HTTP::WEBrickXMLRPC if protocol.to_sym == :xmlrpc + raise ArgumentError, "Unknown protocol [#{protocol}]." + end +end diff --git a/lib/puppet/network/http/webrick/rest.rb b/lib/puppet/network/http/webrick/rest.rb new file mode 100644 index 000000000..dd0c84d61 --- /dev/null +++ b/lib/puppet/network/http/webrick/rest.rb @@ -0,0 +1,41 @@ +require 'puppet/network/http/handler' + +class Puppet::Network::HTTP::WEBrickREST < Puppet::Network::HTTP::Handler + + # WEBrick uses a service() method to respond to requests. Simply delegate to the handler response() method. + def service(request, response) + process(request, response) + end + + private + + def register_handler + @server.mount('/' + @handler.to_s, self) + @server.mount('/' + @handler.to_s + 's', self) + end + + def http_method(request) + request.request_method + end + + def path(request) + '/' + request.path.split('/')[1] + end + + def request_key(request) + request.path.split('/')[2] + end + + def body(request) + request.body + end + + def params(request) + request.query + end + + def encode_result(request, response, result, status = 200) + response.status = status + response.body = result + end +end
\ No newline at end of file diff --git a/lib/puppet/network/http/webrick/xmlrpc.rb b/lib/puppet/network/http/webrick/xmlrpc.rb new file mode 100644 index 000000000..793708f8a --- /dev/null +++ b/lib/puppet/network/http/webrick/xmlrpc.rb @@ -0,0 +1,4 @@ +class Puppet::Network::HTTP::WEBrickXMLRPC + def initialize(args = {}) + end +end diff --git a/lib/puppet/network/server.rb b/lib/puppet/network/server.rb index 84a71a6b4..50e3bd686 100644 --- a/lib/puppet/network/server.rb +++ b/lib/puppet/network/server.rb @@ -1,66 +1,65 @@ class Puppet::Network::Server - attr_reader :server_type + attr_reader :server_type, :protocols, :address, :port - # which HTTP server subclass actually handles web requests of a certain type? (e.g., :rest => RESTServer) - def self.server_class_by_name(name) - klass = (name.to_s + 'Server').to_sym - const_get klass - end - - # we will actually return an instance of the Server subclass which handles the HTTP web server, instead of - # an instance of this generic Server class. A tiny bit of sleight-of-hand is necessary to make this happen. - def self.new(args = {}) - server_type = Puppet[:servertype] or raise "No servertype configuration found." - obj = self.server_class_by_name(server_type).allocate - obj.send :initialize, args.merge(:server_type => server_type) - obj - end - - def initialize(args = {}) - @routes = {} - @listening = false - @server_type = args[:server_type] - self.register(args[:handlers]) if args[:handlers] - end + def initialize(args = {}) + @server_type = Puppet[:servertype] or raise "No servertype configuration found." # e.g., WEBrick, Mongrel, etc. + http_server_class || raise(ArgumentError, "Could not determine HTTP Server class for server type [#{@server_type}]") + @address = args[:address] || Puppet[:bindaddress] || + raise(ArgumentError, "Must specify :address or configure Puppet :bindaddress.") + @port = args[:port] || Puppet[:masterport] || + raise(ArgumentError, "Must specify :port or configure Puppet :masterport") + @protocols = [] + @listening = false + @routes = {} + self.register(args[:handlers]) if args[:handlers] + end - def register(*indirections) - raise ArgumentError, "indirection names are required" if indirections.empty? - indirections.flatten.each { |i| @routes[i.to_sym] = true } - end + def register(*indirections) + raise ArgumentError, "Indirection names are required." if indirections.empty? + indirections.flatten.each { |i| @routes[i.to_sym] = true } + end - def unregister(*indirections) - indirections = @routes.keys if indirections.empty? - indirections.flatten.each do |i| - raise(ArgumentError, "indirection [%s] is not known" % i) unless @routes[i.to_sym] - @routes.delete(i.to_sym) + def unregister(*indirections) + raise "Cannot unregister indirections while server is listening." if listening? + indirections = @routes.keys if indirections.empty? + + indirections.flatten.each do |i| + raise(ArgumentError, "Indirection [%s] is unknown." % i) unless @routes[i.to_sym] + end + + indirections.flatten.each do |i| + @routes.delete(i.to_sym) + end end - end - def listening? - @listening - end + def listening? + @listening + end - def listen - raise "Cannot listen -- already listening" if listening? - start_web_server - @listening = true - end + def listen + raise "Cannot listen -- already listening." if listening? + http_server.listen(@routes.dup) + @listening = true + end - def unlisten - raise "Cannot unlisten -- not currently listening" unless listening? - stop_web_server - @listening = false - end + def unlisten + raise "Cannot unlisten -- not currently listening." unless listening? + http_server.unlisten + @listening = false + end + + def http_server_class + http_server_class_by_type(@server_type) + end private - def start_web_server - raise NotImplementedError, "this method needs to be implemented by the actual web server (sub)class" - end - - def stop_web_server - raise NotImplementedError, "this method needs to be implemented by the actual web server (sub)class" - end + def http_server + @http_server ||= http_server_class.new + end + + def http_server_class_by_type(kind) + Puppet::Network::HTTP.server_class_by_type(kind) + end end - diff --git a/lib/puppet/node/configuration.rb b/lib/puppet/node/configuration.rb index e49090d70..804f357d1 100644 --- a/lib/puppet/node/configuration.rb +++ b/lib/puppet/node/configuration.rb @@ -38,6 +38,10 @@ class Puppet::Node::Configuration < Puppet::PGraph # relationship graph. attr_accessor :is_relationship_graph + # Whether this configuration was retrieved from the cache, which affects + # whether it is written back out again. + attr_accessor :from_cache + # Add classes to our class list. def add_class(*classes) classes.each do |klass| @@ -66,6 +70,16 @@ class Puppet::Node::Configuration < Puppet::PGraph end end + # Create an alias for a resource. + def alias(resource, name) + resource.ref =~ /^(.+)\[/ + + newref = "%s[%s]" % [$1 || resource.class.name, name] + raise(ArgumentError, "Cannot alias %s to %s; resource %s already exists" % [resource.ref, name, newref]) if @resource_table[newref] + @resource_table[newref] = resource + @aliases[resource.ref] << newref + end + # Apply our configuration to the local host. Valid options # are: # :tags - set the tags that restrict what resources run @@ -274,6 +288,8 @@ class Puppet::Node::Configuration < Puppet::PGraph @applying = false @relationship_graph = nil + @aliases = Hash.new { |hash, key| hash[key] = [] } + if block_given? yield(self) finalize() @@ -331,7 +347,9 @@ class Puppet::Node::Configuration < Puppet::PGraph # references to the resource instances. def remove_resource(*resources) resources.each do |resource| - @resource_table.delete(resource.ref) if @resource_table.include?(resource.ref) + @resource_table.delete(resource.ref) + @aliases[resource.ref].each { |res_alias| @resource_table.delete(res_alias) } + @aliases[resource.ref].clear remove_vertex!(resource) if vertex?(resource) @relationship_graph.remove_vertex!(resource) if @relationship_graph and @relationship_graph.vertex?(resource) resource.remove diff --git a/lib/puppet/parser/compile.rb b/lib/puppet/parser/compile.rb index f23b42a35..7f568f1b3 100644 --- a/lib/puppet/parser/compile.rb +++ b/lib/puppet/parser/compile.rb @@ -14,7 +14,7 @@ require 'puppet/util/errors' class Puppet::Parser::Compile include Puppet::Util include Puppet::Util::Errors - attr_reader :topscope, :parser, :node, :facts, :collections, :configuration + attr_reader :parser, :node, :facts, :collections, :configuration, :node_scope # Add a collection to the global list. def add_collection(coll) @@ -107,7 +107,7 @@ class Puppet::Parser::Compile # Evaluate all of the classes specified by the node. def evaluate_node_classes - evaluate_classes(@node.classes, @topscope) + evaluate_classes(@node.classes, topscope) end # Evaluate each specified class in turn. If there are any classes we can't @@ -142,9 +142,7 @@ class Puppet::Parser::Compile # Return a resource by either its ref or its type and title. def findresource(string, name = nil) - if name - string = "%s[%s]" % [string.capitalize, name] - end + string = "%s[%s]" % [string.capitalize, name] if name @resource_table[string] end @@ -173,7 +171,7 @@ class Puppet::Parser::Compile # using the top scope. Adds an edge between the scope and # its parent to the graph. def newscope(parent, options = {}) - parent ||= @topscope + parent ||= topscope options[:compile] = self options[:parser] ||= self.parser scope = Puppet::Parser::Scope.new(options) @@ -229,6 +227,12 @@ class Puppet::Parser::Compile @configuration.add_edge!(scope.resource, resource) end + # The top scope is usually the top-level scope, but if we're using AST nodes, + # then it is instead the node's scope. + def topscope + node_scope || @topscope + end + private # If ast nodes are enabled, then see if we can find and evaluate one. @@ -241,10 +245,7 @@ class Puppet::Parser::Compile break if astnode = @parser.nodes[name.to_s.downcase] end - unless astnode - astnode = @parser.nodes["default"] - end - unless astnode + unless (astnode ||= @parser.nodes["default"]) raise Puppet::ParseError, "Could not find default node or by name with '%s'" % node.names.join(", ") end @@ -253,6 +254,12 @@ class Puppet::Parser::Compile resource = Puppet::Parser::Resource.new(:type => "node", :title => astnode.classname, :scope => topscope, :source => topscope.source) store_resource(topscope, resource) @configuration.tag(astnode.classname) + + resource.evaluate + + # Now set the node scope appropriately, so that :topscope can + # behave differently. + @node_scope = class_scope(astnode) end # Evaluate our collections and return true if anything returned an object. @@ -318,6 +325,8 @@ class Puppet::Parser::Compile @configuration.add_vertex!(@main_resource) @resource_table["Class[main]"] = @main_resource + + @main_resource.evaluate end # Make sure the entire configuration is evaluated. @@ -408,7 +417,6 @@ class Puppet::Parser::Compile # A graph for maintaining scope relationships. @scope_graph = GRATR::Digraph.new - @scope_graph.add_vertex!(@topscope) # For maintaining the relationship between scopes and their resources. @configuration = Puppet::Node::Configuration.new(@node.name) diff --git a/lib/puppet/transaction.rb b/lib/puppet/transaction.rb index c8fc2f199..ef53889cf 100644 --- a/lib/puppet/transaction.rb +++ b/lib/puppet/transaction.rb @@ -311,7 +311,7 @@ class Transaction ret = eval_resource(resource) end - if Puppet[:evaltrace] + if Puppet[:evaltrace] and @configuration.host_config? resource.info "Evaluated in %0.2f seconds" % seconds end ret diff --git a/lib/puppet/transportable.rb b/lib/puppet/transportable.rb index 281ad00d3..6a573489c 100644 --- a/lib/puppet/transportable.rb +++ b/lib/puppet/transportable.rb @@ -59,15 +59,11 @@ module Puppet tmpname = @type end trans = TransObject.new(tmpname, :component) - if defined? @parameters - @parameters.each { |param,value| - Puppet.debug "Defining %s on %s of type %s" % - [param,@name,@type] - trans[param] = value - } - else - #Puppet.debug "%s[%s] has no parameters" % [@type, @name] - end + @params.each { |param,value| + next unless Puppet::Type::Component.validattr?(param) + Puppet.debug "Defining %s on %s of type %s" % [param,@name,@type] + trans[param] = value + } Puppet::Type::Component.create(trans) end @@ -107,16 +103,7 @@ module Puppet def to_type retobj = nil if typeklass = Puppet::Type.type(self.type) - # FIXME This should really be done differently, but... - if retobj = typeklass[self.name] - self.each do |param, val| - retobj[param] = val - end - else - unless retobj = typeklass.create(self) - return nil - end - end + return typeklass.create(self) else return to_component end @@ -135,7 +122,7 @@ module Puppet class TransBucket include Enumerable - attr_accessor :name, :type, :file, :line, :classes, :keyword, :top + attr_accessor :name, :type, :file, :line, :classes, :keyword, :top, :configuration %w{delete shift include? length empty? << []}.each { |method| define_method(method) do |*args| @@ -218,11 +205,13 @@ module Puppet def to_configuration configuration = Puppet::Node::Configuration.new(Facter.value("hostname")) do |config| delver = proc do |obj| + obj.configuration = config unless container = config.resource(obj.to_ref) container = obj.to_type config.add_resource container end obj.each do |child| + child.configuration = config unless resource = config.resource(child.to_ref) next unless resource = child.to_type config.add_resource resource @@ -252,65 +241,25 @@ module Puppet end def to_type - # this container will contain the equivalent of all objects at - # this level - #container = Puppet::Component.new(:name => @name, :type => @type) - #unless defined? @name - # raise Puppet::DevError, "TransBuckets must have names" - #end unless defined? @type Puppet.debug "TransBucket '%s' has no type" % @name end - usetrans = true - if usetrans - tmpname = nil - - # Nodes have the same name and type - if self.name - tmpname = "%s[%s]" % [@type, self.name] - else - tmpname = @type - end - trans = TransObject.new(tmpname, :component) - if defined? @parameters - @parameters.each { |param,value| - Puppet.debug "Defining %s on %s of type %s" % - [param,@name,@type] - trans[param] = value - } - else - #Puppet.debug "%s[%s] has no parameters" % [@type, @name] - end - container = Puppet::Type::Component.create(trans) + # Nodes have the same name and type + if self.name + tmpname = "%s[%s]" % [@type, self.name] else - hash = { - :name => self.name, - :type => @type - } - if defined? @parameters - @parameters.each { |param,value| - Puppet.debug "Defining %s on %s of type %s" % - [param,@name,@type] - hash[param] = value - } - else - #Puppet.debug "%s[%s] has no parameters" % [@type, @name] - end - - container = Puppet::Type::Component.create(hash) + tmpname = @type end - #Puppet.info container.inspect - - # unless we successfully created the container, return an error - unless container - Puppet.warning "Got no container back" - return nil + trans = TransObject.new(tmpname, :component) + if defined? @parameters + @parameters.each { |param,value| + Puppet.debug "Defining %s on %s of type %s" % + [param,@name,@type] + trans[param] = value + } end - - # at this point, no objects at are level are still Transportable - # objects - return container + return Puppet::Type::Component.create(trans) end def param(param,value) diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb index b7ff1f664..f5dd0f8dd 100644 --- a/lib/puppet/type.rb +++ b/lib/puppet/type.rb @@ -182,7 +182,7 @@ class Type # directly from it. This is the main object instantiation mechanism. if hash.is_a?(Puppet::TransObject) #self[:name] = hash[:name] - [:file, :line, :tags].each { |getter| + [:file, :line, :tags, :configuration].each { |getter| if hash.respond_to?(getter) setter = getter.to_s + "=" if val = hash.send(getter) @@ -194,10 +194,11 @@ class Type # XXX This will need to change when transobjects change to titles. @title = hash.name hash = hash.to_hash - elsif hash[:title] - # XXX This should never happen - @title = hash[:title] - hash.delete(:title) + else + if hash[:title] + @title = hash[:title] + hash.delete(:title) + end end # Before anything else, set our parent if it was included @@ -221,7 +222,7 @@ class Type if attrs.include?(namevar) attrs.delete(namevar) else - self.devfail "My namevar isn\'t a valid attribute...?" + self.devfail "My namevar isn't a valid attribute...?" end else self.devfail "I was not passed a namevar" @@ -284,6 +285,14 @@ class Type # Scheduling has to be done when the whole config is instantiated, so # that file order doesn't matter in finding them. self.schedule + + # Make sure all of our relationships are valid. Again, must be done + # when the entire configuration is instantiated. + self.class.relationship_params.collect do |klass| + if param = @parameters[klass.name] + param.validate_relationship + end + end.flatten.reject { |r| r.nil? } end # Return a cached value diff --git a/lib/puppet/type/component.rb b/lib/puppet/type/component.rb index 4dc542a65..7aa24a302 100644 --- a/lib/puppet/type/component.rb +++ b/lib/puppet/type/component.rb @@ -93,9 +93,9 @@ Puppet::Type.newtype(:component) do end # Initialize a new component - def initialize(args) + def initialize(*args) @children = [] - super(args) + super # If the title isn't a full resource reference, assume # we're a class and make an alias for that. diff --git a/lib/puppet/type/pfile/group.rb b/lib/puppet/type/pfile/group.rb index 9625b6354..5f7caf342 100755 --- a/lib/puppet/type/pfile/group.rb +++ b/lib/puppet/type/pfile/group.rb @@ -6,6 +6,10 @@ module Puppet name or group ID." @event = :file_changed + validate do |group| + raise(Puppet::Error, "Invalid group name '%s'" % group.inspect) unless group and group != "" + end + def id2name(id) if id > 70000 return nil diff --git a/spec/lib/autotest/discover.rb b/spec/lib/autotest/discover.rb new file mode 100644 index 000000000..0ac563724 --- /dev/null +++ b/spec/lib/autotest/discover.rb @@ -0,0 +1,9 @@ +require 'autotest' + +Autotest.add_discovery do + "rspec" +end + +Autotest.add_discovery do + "puppet" +end diff --git a/spec/lib/autotest/puppet_rspec.rb b/spec/lib/autotest/puppet_rspec.rb new file mode 100644 index 000000000..8536f3912 --- /dev/null +++ b/spec/lib/autotest/puppet_rspec.rb @@ -0,0 +1,46 @@ +require 'autotest' +require 'autotest/rspec' + +class Autotest::PuppetRspec < Autotest::Rspec + def initialize # :nodoc: + super + @test_mappings = { + # the libraries under lib/puppet + %r%^lib/puppet/(.*)\.rb$% => proc { |filename, m| + files_matching %r!spec/(unit|integration)/#{m[1]}.rb! + }, + + # the actual spec files themselves + %r%^spec/(unit|integration)/.*\.rb$% => proc { |filename, _| + filename + }, + + # force a complete re-run for all of these: + + # main puppet lib + %r!^lib/puppet\.rb$! => proc { |filename, _| + files_matching %r!spec/(unit|integration)/.*\.rb! + }, + + # the spec_helper + %r!^spec/spec_helper\.rb$! => proc { |filename, _| + files_matching %r!spec/(unit|integration)/.*\.rb! + }, + + # the puppet test libraries + %r!^test/lib/puppettest/.*! => proc { |filename, _| + files_matching %r!spec/(unit|integration)/.*\.rb! + }, + + # the puppet spec libraries + %r!^spec/lib/spec.*! => proc { |filename, _| + files_matching %r!spec/(unit|integration)/.*\.rb! + }, + + # the monkey patches for rspec + %r!^spec/lib/monkey_patches/.*! => proc { |filename, _| + files_matching %r!spec/(unit|integration)/.*\.rb! + }, + } + end +end diff --git a/spec/lib/autotest/rspec.rb b/spec/lib/autotest/rspec.rb new file mode 100644 index 000000000..d4b77ea6b --- /dev/null +++ b/spec/lib/autotest/rspec.rb @@ -0,0 +1,95 @@ +require 'autotest' + +class RspecCommandError < StandardError; end + +class Autotest::Rspec < Autotest + + def initialize(kernel=Kernel, separator=File::SEPARATOR, alt_separator=File::ALT_SEPARATOR) # :nodoc: + super() + @kernel, @separator, @alt_separator = kernel, separator, alt_separator + @spec_command = spec_command + + # watch out: Ruby bug (1.8.6): + # %r(/) != /\// + # since Ruby compares the REGEXP source, not the resulting pattern + @test_mappings = { + %r%^spec/.*\.rb$% => kernel.proc { |filename, _| + filename + }, + %r%^lib/(.*)\.rb$% => kernel.proc { |_, m| + ["spec/#{m[1]}_spec.rb"] + }, + %r%^spec/(spec_helper|shared/.*)\.rb$% => kernel.proc { + files_matching %r%^spec/.*_spec\.rb$% + } + } + end + + def tests_for_file(filename) + super.select { |f| @files.has_key? f } + end + + alias :specs_for_file :tests_for_file + + def failed_results(results) + results.scan(/^\d+\)\n(?:\e\[\d*m)?(?:.*?Error in )?'([^\n]*)'(?: FAILED)?(?:\e\[\d*m)?\n(.*?)\n\n/m) + end + + def handle_results(results) + @files_to_test = consolidate_failures failed_results(results) + unless @files_to_test.empty? then + hook :red + else + hook :green + end unless $TESTING + @tainted = true unless @files_to_test.empty? + end + + def consolidate_failures(failed) + filters = Hash.new { |h,k| h[k] = [] } + failed.each do |spec, failed_trace| + @files.keys.select{|f| f =~ /spec\//}.each do |f| + if failed_trace =~ Regexp.new(f) + filters[f] << spec + break + end + end + end + return filters + end + + def make_test_cmd(files_to_test) + return "#{ruby} -S #{@spec_command} #{add_options_if_present} #{files_to_test.keys.flatten.join(' ')}" + end + + def add_options_if_present + File.exist?("spec/spec.opts") ? "-O spec/spec.opts " : "" + end + + # Finds the proper spec command to use. Precendence + # is set in the lazily-evaluated method spec_commands. Alias + Override + # that in ~/.autotest to provide a different spec command + # then the default paths provided. + def spec_command + spec_commands.each do |command| + if File.exists?(command) + return @alt_separator ? (command.gsub @separator, @alt_separator) : command + end + end + + raise RspecCommandError, "No spec command could be found!" + end + + # Autotest will look for spec commands in the following + # locations, in this order: + # + # * bin/spec + # * default spec bin/loader installed in Rubygems + def spec_commands + [ + File.join('bin', 'spec'), + File.join(Config::CONFIG['bindir'], 'spec') + ] + end + +end diff --git a/spec/lib/monkey_patches/add_confine_and_runnable_to_rspec_dsl.rb b/spec/lib/monkey_patches/add_confine_and_runnable_to_rspec_dsl.rb new file mode 100644 index 000000000..a2467eac8 --- /dev/null +++ b/spec/lib/monkey_patches/add_confine_and_runnable_to_rspec_dsl.rb @@ -0,0 +1,30 @@ +dir = File.expand_path(File.dirname(__FILE__)) +$LOAD_PATH.unshift("#{dir}/../../lib") +$LOAD_PATH.unshift("#{dir}/../../../lib") +$LOAD_PATH.unshift("#{dir}/../../../test/lib") # Add the old test dir, so that we can still find our local mocha and spec + +require 'spec' +require 'puppettest' +require 'puppettest/runnable_test' + +module Spec + module Runner + class BehaviourRunner + def run_behaviours + @behaviours.each do |behaviour| + # LAK:NOTE: this 'runnable' test is Puppet-specific. + next unless behaviour.runnable? + behaviour.run(@options.reporter, @options.dry_run, @options.reverse, @options.timeout) + end + end + end + end +end + +module Spec + module DSL + class EvalModule < Module + include PuppetTest::RunnableTest + end + end +end diff --git a/spec/lib/spec/dsl/behaviour.rb b/spec/lib/spec/dsl/behaviour.rb index 159a0ba7e..cc71ccffe 100644 --- a/spec/lib/spec/dsl/behaviour.rb +++ b/spec/lib/spec/dsl/behaviour.rb @@ -2,9 +2,6 @@ require(File.expand_path(File.dirname(__FILE__) + '../../../../../test/lib/puppe module Spec module DSL - class EvalModule < Module; - include PuppetTest::RunnableTest - end class Behaviour extend BehaviourCallbacks diff --git a/spec/lib/spec/runner/behaviour_runner.rb b/spec/lib/spec/runner/behaviour_runner.rb index 078490e92..1ac891f3c 100644 --- a/spec/lib/spec/runner/behaviour_runner.rb +++ b/spec/lib/spec/runner/behaviour_runner.rb @@ -55,8 +55,6 @@ module Spec def run_behaviours @behaviours.each do |behaviour| - # LAK:NOTE: this 'runnable' test is Puppet-specific. - next unless behaviour.runnable? behaviour.run(@options.reporter, @options.dry_run, @options.reverse, @options.timeout) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3017f272a..bfac9095f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,12 +1,11 @@ dir = File.expand_path(File.dirname(__FILE__)) -$:.unshift("#{dir}/lib") -$:.unshift("#{dir}/../lib") +$LOAD_PATH.unshift("#{dir}/lib") +$LOAD_PATH.unshift("#{dir}/../lib") +$LOAD_PATH.unshift("#{dir}/../test/lib") # Add the old test dir, so that we can still find our local mocha and spec -# Add the old test dir, so that we can still find mocha and spec -$:.unshift("#{dir}/../test/lib") - -require 'mocha' require 'puppettest' +require 'puppettest/runnable_test' +require 'mocha' require 'spec' Spec::Runner.configure do |config| @@ -19,3 +18,5 @@ Spec::Runner.configure do |config| teardown() if respond_to? :teardown end end + +require "#{dir}/lib/monkey_patches/add_confine_and_runnable_to_rspec_dsl" diff --git a/spec/unit/indirector/indirection.rb b/spec/unit/indirector/indirection.rb index 0ac2356d6..5d8453905 100755 --- a/spec/unit/indirector/indirection.rb +++ b/spec/unit/indirector/indirection.rb @@ -156,11 +156,21 @@ describe Puppet::Indirector::Indirection, " when managing indirection instances" @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 diff --git a/spec/unit/indirector/node/memory.rb b/spec/unit/indirector/node/memory.rb index f57cae818..a924c6209 100755 --- a/spec/unit/indirector/node/memory.rb +++ b/spec/unit/indirector/node/memory.rb @@ -4,9 +4,8 @@ require File.dirname(__FILE__) + '/../../../spec_helper' require 'puppet/indirector/node/memory' -# All of our behaviour is described here, so we always have to -# include it. -require 'unit/indirector/memory' +# All of our behaviour is described here, so we always have to include it. +require File.dirname(__FILE__) + '/../memory' describe Puppet::Node::Memory do before do diff --git a/spec/unit/network/client/master.rb b/spec/unit/network/client/master.rb new file mode 100755 index 000000000..dca923994 --- /dev/null +++ b/spec/unit/network/client/master.rb @@ -0,0 +1,365 @@ +#!/usr/bin/env ruby +# +# Created by Luke Kanies on 2007-11-12. +# Copyright (c) 2007. All rights reserved. + +require File.dirname(__FILE__) + '/../../../spec_helper' +require 'puppet/network/client/master' + +describe Puppet::Network::Client::Master, " when retrieving the configuration" do + before do + @master = mock 'master' + @client = Puppet::Network::Client.master.new( + :Master => @master + ) + @facts = {"one" => "two", "three" => "four"} + end + + it "should initialize the metadata store" do + @client.class.stubs(:facts).returns(@facts) + @client.expects(:dostorage) + @master.stubs(:getconfig).returns(nil) + @client.getconfig + end + + it "should collect facts to use for configuration retrieval" do + @client.stubs(:dostorage) + @client.class.expects(:facts).returns(@facts) + @master.stubs(:getconfig).returns(nil) + @client.getconfig + end + + it "should fail if no facts could be collected" do + @client.stubs(:dostorage) + @client.class.expects(:facts).returns({}) + @master.stubs(:getconfig).returns(nil) + proc { @client.getconfig }.should raise_error(Puppet::Network::ClientError) + end + + it "should use the cached configuration if it is up to date" do + file = "/path/to/cachefile" + @client.stubs(:cachefile).returns(file) + FileTest.expects(:exist?).with(file).returns(true) + @client.expects(:fresh?).with(@facts).returns true + @client.class.stubs(:facts).returns(@facts) + @client.expects(:use_cached_config).returns(true) + Puppet.stubs(:info) + + @client.getconfig + end + + it "should log that the configuration does not need a recompile" do + file = "/path/to/cachefile" + @client.stubs(:cachefile).returns(file) + FileTest.stubs(:exist?).with(file).returns(true) + @client.stubs(:fresh?).with(@facts).returns true + @client.stubs(:use_cached_config).returns(true) + @client.class.stubs(:facts).returns(@facts) + Puppet.expects(:info).with { |m| m.include?("up to date") } + + @client.getconfig + end + + it "should retrieve plugins if :pluginsync is enabled" do + file = "/path/to/cachefile" + @client.stubs(:cachefile).returns(file) + @client.stubs(:dostorage) + @client.class.stubs(:facts).returns(@facts) + Puppet.settings.expects(:value).with(:pluginsync).returns(true) + @client.expects(:getplugins) + @client.stubs(:get_actual_config).returns(nil) + FileTest.stubs(:exist?).with(file).returns(true) + @client.stubs(:fresh?).with(@facts).returns true + @client.stubs(:use_cached_config).returns(true) + @client.class.stubs(:facts).returns(@facts) + @client.getconfig + end + + it "should use the cached configuration if no configuration could be retrieved" do + @client.stubs(:dostorage) + @client.class.stubs(:facts).returns(@facts) + @master.stubs(:getconfig).raises(ArgumentError.new("whev")) + @client.expects(:use_cached_config).with(true) + @client.getconfig + end + + it "should load the retrieved configuration using YAML" do + @client.stubs(:dostorage) + @client.class.stubs(:facts).returns(@facts) + @master.stubs(:getconfig).returns("myconfig") + + config = mock 'config' + YAML.expects(:load).with("myconfig").returns(config) + + @client.stubs(:setclasses) + + config.stubs(:classes) + config.stubs(:to_configuration).returns(config) + config.stubs(:host_config=) + config.stubs(:from_cache).returns(true) + + @client.getconfig + end + + it "should use the cached configuration if the retrieved configuration cannot be converted from YAML" do + @client.stubs(:dostorage) + @client.class.stubs(:facts).returns(@facts) + @master.stubs(:getconfig).returns("myconfig") + + YAML.expects(:load).with("myconfig").raises(ArgumentError) + + @client.expects(:use_cached_config).with(true) + + @client.getconfig + end + + it "should set the classes.txt file with the classes listed in the retrieved configuration" do + @client.stubs(:dostorage) + @client.class.stubs(:facts).returns(@facts) + @master.stubs(:getconfig).returns("myconfig") + + config = mock 'config' + YAML.expects(:load).with("myconfig").returns(config) + + config.expects(:classes).returns(:myclasses) + @client.expects(:setclasses).with(:myclasses) + + config.stubs(:to_configuration).returns(config) + config.stubs(:host_config=) + config.stubs(:from_cache).returns(true) + + @client.getconfig + end + + it "should convert the retrieved configuration to a RAL configuration" do + @client.stubs(:dostorage) + @client.class.stubs(:facts).returns(@facts) + @master.stubs(:getconfig).returns("myconfig") + + yamlconfig = mock 'yaml config' + YAML.stubs(:load).returns(yamlconfig) + + @client.stubs(:setclasses) + + config = mock 'config' + + yamlconfig.stubs(:classes) + yamlconfig.expects(:to_configuration).returns(config) + + config.stubs(:host_config=) + config.stubs(:from_cache).returns(true) + + @client.getconfig + end + + it "should use the cached configuration if the retrieved configuration cannot be converted to a RAL configuration" do + @client.stubs(:dostorage) + @client.class.stubs(:facts).returns(@facts) + @master.stubs(:getconfig).returns("myconfig") + + yamlconfig = mock 'yaml config' + YAML.stubs(:load).returns(yamlconfig) + + @client.stubs(:setclasses) + + config = mock 'config' + + yamlconfig.stubs(:classes) + yamlconfig.expects(:to_configuration).raises(ArgumentError) + + @client.expects(:use_cached_config).with(true) + + @client.getconfig + end + + it "should clear the failed configuration if using the cached configuration after failing to instantiate the retrieved configuration" do + @client.stubs(:dostorage) + @client.class.stubs(:facts).returns(@facts) + @master.stubs(:getconfig).returns("myconfig") + + yamlconfig = mock 'yaml config' + YAML.stubs(:load).returns(yamlconfig) + + @client.stubs(:setclasses) + + config = mock 'config' + + yamlconfig.stubs(:classes) + yamlconfig.stubs(:to_configuration).raises(ArgumentError) + + @client.stubs(:use_cached_config).with(true) + + @client.expects(:clear) + + @client.getconfig + end + + it "should cache the retrieved yaml configuration if it is not from the cache and is valid" do + @client.stubs(:dostorage) + @client.class.stubs(:facts).returns(@facts) + @master.stubs(:getconfig).returns("myconfig") + + yamlconfig = mock 'yaml config' + YAML.stubs(:load).returns(yamlconfig) + + @client.stubs(:setclasses) + + config = mock 'config' + + yamlconfig.stubs(:classes) + yamlconfig.expects(:to_configuration).returns(config) + + config.stubs(:host_config=) + + config.expects(:from_cache).returns(false) + + @client.expects(:cache).with("myconfig") + + @client.getconfig + end + + it "should mark the configuration as a host configuration" do + @client.stubs(:dostorage) + @client.class.stubs(:facts).returns(@facts) + @master.stubs(:getconfig).returns("myconfig") + + yamlconfig = mock 'yaml config' + YAML.stubs(:load).returns(yamlconfig) + + @client.stubs(:setclasses) + + config = mock 'config' + + yamlconfig.stubs(:classes) + yamlconfig.expects(:to_configuration).returns(config) + + config.stubs(:from_cache).returns(true) + + config.expects(:host_config=).with(true) + + @client.getconfig + end +end + +describe Puppet::Network::Client::Master, " when using the cached configuration" do + before do + @master = mock 'master' + @client = Puppet::Network::Client.master.new( + :Master => @master + ) + @facts = {"one" => "two", "three" => "four"} + end + + it "should return do nothing and true if there is already an in-memory configuration" do + @client.configuration = :whatever + Puppet::Network::Client::Master.publicize_methods :use_cached_config do + @client.use_cached_config.should be_true + end + end + + it "should return do nothing and false if it has been told there is a failure and :nocacheonfailure is enabled" do + Puppet.settings.expects(:value).with(:usecacheonfailure).returns(false) + Puppet::Network::Client::Master.publicize_methods :use_cached_config do + @client.use_cached_config(true).should be_false + end + end + + it "should return false if no cached configuration can be found" do + @client.expects(:retrievecache).returns(nil) + Puppet::Network::Client::Master.publicize_methods :use_cached_config do + @client.use_cached_config().should be_false + end + end + + it "should return false if the cached configuration cannot be instantiated" do + YAML.expects(:load).raises(ArgumentError) + @client.expects(:retrievecache).returns("whatever") + Puppet::Network::Client::Master.publicize_methods :use_cached_config do + @client.use_cached_config().should be_false + end + end + + it "should warn if the cached configuration cannot be instantiated" do + YAML.stubs(:load).raises(ArgumentError) + @client.stubs(:retrievecache).returns("whatever") + Puppet.expects(:warning).with { |m| m.include?("Could not load cache") } + Puppet::Network::Client::Master.publicize_methods :use_cached_config do + @client.use_cached_config().should be_false + end + end + + it "should clear the client if the cached configuration cannot be instantiated" do + YAML.stubs(:load).raises(ArgumentError) + @client.stubs(:retrievecache).returns("whatever") + @client.expects(:clear) + Puppet::Network::Client::Master.publicize_methods :use_cached_config do + @client.use_cached_config().should be_false + end + end + + it "should return true if the cached configuration can be instantiated" do + config = mock 'config' + YAML.stubs(:load).returns(config) + + ral_config = mock 'ral config' + ral_config.stubs(:from_cache=) + ral_config.stubs(:host_config=) + config.expects(:to_configuration).returns(ral_config) + + @client.stubs(:retrievecache).returns("whatever") + Puppet::Network::Client::Master.publicize_methods :use_cached_config do + @client.use_cached_config().should be_true + end + end + + it "should set the configuration instance variable if the cached configuration can be instantiated" do + config = mock 'config' + YAML.stubs(:load).returns(config) + + ral_config = mock 'ral config' + ral_config.stubs(:from_cache=) + ral_config.stubs(:host_config=) + config.expects(:to_configuration).returns(ral_config) + + @client.stubs(:retrievecache).returns("whatever") + Puppet::Network::Client::Master.publicize_methods :use_cached_config do + @client.use_cached_config() + end + + @client.configuration.should equal(ral_config) + end + + it "should mark the configuration as a host_config if valid" do + config = mock 'config' + YAML.stubs(:load).returns(config) + + ral_config = mock 'ral config' + ral_config.stubs(:from_cache=) + ral_config.expects(:host_config=).with(true) + config.expects(:to_configuration).returns(ral_config) + + @client.stubs(:retrievecache).returns("whatever") + Puppet::Network::Client::Master.publicize_methods :use_cached_config do + @client.use_cached_config() + end + + @client.configuration.should equal(ral_config) + end + + it "should mark the configuration as from the cache if valid" do + config = mock 'config' + YAML.stubs(:load).returns(config) + + ral_config = mock 'ral config' + ral_config.expects(:from_cache=).with(true) + ral_config.stubs(:host_config=) + config.expects(:to_configuration).returns(ral_config) + + @client.stubs(:retrievecache).returns("whatever") + Puppet::Network::Client::Master.publicize_methods :use_cached_config do + @client.use_cached_config() + end + + @client.configuration.should equal(ral_config) + end +end diff --git a/spec/unit/network/http.rb b/spec/unit/network/http.rb new file mode 100644 index 000000000..79a0a88d4 --- /dev/null +++ b/spec/unit/network/http.rb @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +# +# Created by Rick Bradley on 2007-10-03. +# Copyright (c) 2007. All rights reserved. + +require File.dirname(__FILE__) + '/../../spec_helper' + +require 'puppet/network/http' + +describe Puppet::Network::HTTP do + it "should return the webrick HTTP server class when asked for a webrick server" do + Puppet::Network::HTTP.server_class_by_type(:webrick).should be(Puppet::Network::HTTP::WEBrick) + end + + if Puppet.features.mongrel? + it "should return the mongrel HTTP server class when asked for a mongrel server" do + Puppet::Network::HTTP.server_class_by_type(:mongrel).should be(Puppet::Network::HTTP::Mongrel) + end + end + + it "should fail to return the mongrel HTTP server class if mongrel is not available " do + Puppet.features.expects(:mongrel?).returns(false) + Proc.new { Puppet::Network::HTTP.server_class_by_type(:mongrel) }.should raise_error(ArgumentError) + end + + it "should return an error when asked for an unknown server" do + Proc.new { Puppet::Network::HTTP.server_class_by_type :foo }.should raise_error(ArgumentError) + end +end diff --git a/spec/unit/network/http/mongrel.rb b/spec/unit/network/http/mongrel.rb new file mode 100644 index 000000000..b6ad07567 --- /dev/null +++ b/spec/unit/network/http/mongrel.rb @@ -0,0 +1,124 @@ +#!/usr/bin/env ruby +# +# Created by Rick Bradley on 2007-10-15. +# Copyright (c) 2007. All rights reserved. + +require File.dirname(__FILE__) + '/../../../spec_helper' +require 'puppet/network/http' + +describe Puppet::Network::HTTP::Mongrel, "after initializing" do + confine "Mongrel is not available" => Puppet.features.mongrel? + + it "should not be listening" do + Puppet::Network::HTTP::Mongrel.new.should_not be_listening + end +end + +describe Puppet::Network::HTTP::Mongrel, "when turning on listening" do + confine "Mongrel is not available" => Puppet.features.mongrel? + + before do + @server = Puppet::Network::HTTP::Mongrel.new + @mock_mongrel = mock('mongrel') + @mock_mongrel.stubs(:run) + @mock_mongrel.stubs(:register) + Mongrel::HttpServer.stubs(:new).returns(@mock_mongrel) + @listen_params = { :address => "127.0.0.1", :port => 31337, :handlers => [ :node, :configuration ], :protocols => [ :rest, :xmlrpc ] } + end + + it "should fail if already listening" do + @server.listen(@listen_params) + Proc.new { @server.listen(@listen_params) }.should raise_error(RuntimeError) + end + + it "should require at least one handler" do + Proc.new { @server.listen(@listen_params.delete_if {|k,v| :handlers == k}) }.should raise_error(ArgumentError) + end + + it "should require at least one protocol" do + Proc.new { @server.listen(@listen_params.delete_if {|k,v| :protocols == k}) }.should raise_error(ArgumentError) + end + + it "should require a listening address to be specified" do + Proc.new { @server.listen(@listen_params.delete_if {|k,v| :address == k})}.should raise_error(ArgumentError) + end + + it "should require a listening port to be specified" do + Proc.new { @server.listen(@listen_params.delete_if {|k,v| :port == k})}.should raise_error(ArgumentError) + end + + it "should order a mongrel server to start" do + @mock_mongrel.expects(:run) + @server.listen(@listen_params) + end + + it "should tell mongrel to listen on the specified address and port" do + Mongrel::HttpServer.expects(:new).with("127.0.0.1", 31337).returns(@mock_mongrel) + @server.listen(@listen_params) + end + + it "should be listening" do + Mongrel::HttpServer.expects(:new).returns(@mock_mongrel) + @server.listen(@listen_params) + @server.should be_listening + end + + it "should instantiate a handler for each protocol+handler pair to configure web server routing" do + @listen_params[:protocols].each do |protocol| + mock_handler = mock("handler instance for [#{protocol}]") + mock_handler_class = mock("handler class for [#{protocol}]") + @listen_params[:handlers].each do |handler| + mock_handler_class.expects(:new).with {|args| + args[:server] == @mock_mongrel and args[:handler] == handler + }.returns(mock_handler) + end + @server.expects(:class_for_protocol).with(protocol).at_least_once.returns(mock_handler_class) + end + @server.listen(@listen_params) + end + + it "should use a Mongrel + REST class to configure Mongrel when REST services are requested" do + Puppet::Network::HTTP::MongrelREST.expects(:new).at_least_once + @server.listen(@listen_params.merge(:protocols => [:rest])) + end + + it "should use a Mongrel + XMLRPC class to configure Mongrel when XMLRPC services are requested" do + Puppet::Network::HTTP::MongrelXMLRPC.expects(:new).at_least_once + @server.listen(@listen_params.merge(:protocols => [:xmlrpc])) + end + + it "should fail if services from an unknown protocol are requested" do + Proc.new { @server.listen(@listen_params.merge(:protocols => [ :foo ]))}.should raise_error(ArgumentError) + end + +end + +describe Puppet::Network::HTTP::Mongrel, "when turning off listening" do + confine "Mongrel is not available" => Puppet.features.mongrel? + + before do + @mock_mongrel = mock('mongrel httpserver') + @mock_mongrel.stubs(:run) + @mock_mongrel.stubs(:register) + Mongrel::HttpServer.stubs(:new).returns(@mock_mongrel) + @server = Puppet::Network::HTTP::Mongrel.new + @listen_params = { :address => "127.0.0.1", :port => 31337, :handlers => [ :node, :configuration ], :protocols => [ :rest, :xmlrpc ] } + end + + it "should fail unless listening" do + Proc.new { @server.unlisten }.should raise_error(RuntimeError) + end + + it "should order mongrel server to stop" do + @server.listen(@listen_params) + @mock_mongrel.expects(:graceful_shutdown) + @server.unlisten + end + + it "should not be listening" do + @server.listen(@listen_params) + @mock_mongrel.stubs(:graceful_shutdown) + @server.unlisten + @server.should_not be_listening + end +end diff --git a/spec/unit/network/http/mongrel/rest.rb b/spec/unit/network/http/mongrel/rest.rb new file mode 100644 index 000000000..9b2feb6ee --- /dev/null +++ b/spec/unit/network/http/mongrel/rest.rb @@ -0,0 +1,287 @@ +#!/usr/bin/env ruby +# +# Created by Rick Bradley on 2007-10-16. +# Copyright (c) 2007. All rights reserved. + +require File.dirname(__FILE__) + '/../../../../spec_helper' +require 'puppet/network/http' + +describe Puppet::Network::HTTP::MongrelREST, "when initializing" do + confine "Mongrel is not available" => Puppet.features.mongrel? + + before do + @mock_mongrel = mock('Mongrel server') + @mock_mongrel.stubs(:register) + @mock_model = mock('indirected model') + Puppet::Indirector::Indirection.stubs(:model).with(:foo).returns(@mock_model) + @params = { :server => @mock_mongrel, :handler => :foo } + end + + it "should require access to a Mongrel server" do + Proc.new { Puppet::Network::HTTP::MongrelREST.new(@params.delete_if {|k,v| :server == k })}.should raise_error(ArgumentError) + end + + it "should require an indirection name" do + Proc.new { Puppet::Network::HTTP::MongrelREST.new(@params.delete_if {|k,v| :handler == k })}.should raise_error(ArgumentError) + end + + it "should look up the indirection model from the indirection name" do + Puppet::Indirector::Indirection.expects(:model).with(:foo).returns(@mock_model) + Puppet::Network::HTTP::MongrelREST.new(@params) + end + + it "should fail if the indirection is not known" do + Puppet::Indirector::Indirection.expects(:model).with(:foo).returns(nil) + Proc.new { Puppet::Network::HTTP::MongrelREST.new(@params) }.should raise_error(ArgumentError) + end + + it "should register itself with the mongrel server for the singular HTTP methods" do + @mock_mongrel.expects(:register).with do |*args| + args.first == '/foo' and args.last.is_a? Puppet::Network::HTTP::MongrelREST + end + Puppet::Network::HTTP::MongrelREST.new(@params) + end + + it "should register itself with the mongrel server for the plural GET method" do + @mock_mongrel.expects(:register).with do |*args| + args.first == '/foos' and args.last.is_a? Puppet::Network::HTTP::MongrelREST + end + Puppet::Network::HTTP::MongrelREST.new(@params) + end +end + +describe Puppet::Network::HTTP::MongrelREST, "when receiving a request" do + confine "Mongrel is not available" => Puppet.features.mongrel? + + before do + @mock_request = stub('mongrel http request') + @mock_head = stub('response head') + @mock_body = stub('response body', :write => true) + @mock_response = stub('mongrel http response') + @mock_response.stubs(:start).yields(@mock_head, @mock_body) + @mock_model_class = stub('indirected model class') + @mock_mongrel = stub('mongrel http server', :register => true) + Puppet::Indirector::Indirection.stubs(:model).with(:foo).returns(@mock_model_class) + @handler = Puppet::Network::HTTP::MongrelREST.new(:server => @mock_mongrel, :handler => :foo) + end + + def setup_find_request(params = {}) + @mock_request.stubs(:params).returns({ Mongrel::Const::REQUEST_METHOD => 'GET', + Mongrel::Const::REQUEST_PATH => '/foo/key', + 'QUERY_STRING' => ''}.merge(params)) + @mock_model_class.stubs(:find) + end + + def setup_search_request(params = {}) + @mock_request.stubs(:params).returns({ Mongrel::Const::REQUEST_METHOD => 'GET', + Mongrel::Const::REQUEST_PATH => '/foos', + 'QUERY_STRING' => '' }.merge(params)) + @mock_model_class.stubs(:search).returns([]) + end + + def setup_destroy_request(params = {}) + @mock_request.stubs(:params).returns({ Mongrel::Const::REQUEST_METHOD => 'DELETE', + Mongrel::Const::REQUEST_PATH => '/foo/key', + 'QUERY_STRING' => '' }.merge(params)) + @mock_model_class.stubs(:destroy) + end + + def setup_save_request(params = {}) + @mock_request.stubs(:params).returns({ Mongrel::Const::REQUEST_METHOD => 'PUT', + Mongrel::Const::REQUEST_PATH => '/foo', + 'QUERY_STRING' => '' }.merge(params)) + @mock_request.stubs(:body).returns('this is a fake request body') + @mock_model_instance = stub('indirected model instance', :save => true) + @mock_model_class.stubs(:new).returns(@mock_model_instance) + end + + def setup_bad_request + @mock_request.stubs(:params).returns({ Mongrel::Const::REQUEST_METHOD => 'POST', Mongrel::Const::REQUEST_PATH => '/foos'}) + end + + it "should call the model find method if the request represents a singular HTTP GET" do + setup_find_request + @mock_model_class.expects(:find).with('key', {}) + @handler.process(@mock_request, @mock_response) + end + + it "should call the model search method if the request represents a plural HTTP GET" do + setup_search_request + @mock_model_class.expects(:search).with({}).returns([]) + @handler.process(@mock_request, @mock_response) + end + + it "should call the model destroy method if the request represents an HTTP DELETE" do + setup_destroy_request + @mock_model_class.expects(:destroy).with('key', {}) + @handler.process(@mock_request, @mock_response) + end + + it "should call the model save method if the request represents an HTTP PUT" do + setup_save_request + @mock_model_instance.expects(:save).with(:data => 'this is a fake request body') + @handler.process(@mock_request, @mock_response) + end + + it "should fail if the HTTP method isn't supported" do + @mock_request.stubs(:params).returns({ Mongrel::Const::REQUEST_METHOD => 'POST', Mongrel::Const::REQUEST_PATH => '/foo'}) + @mock_response.expects(:start).with(404) + @handler.process(@mock_request, @mock_response) + end + + it "should fail if the request's pluralization is wrong" do + @mock_request.stubs(:params).returns({ Mongrel::Const::REQUEST_METHOD => 'DELETE', Mongrel::Const::REQUEST_PATH => '/foos/key'}) + @mock_response.expects(:start).with(404) + @handler.process(@mock_request, @mock_response) + + @mock_request.stubs(:params).returns({ Mongrel::Const::REQUEST_METHOD => 'PUT', Mongrel::Const::REQUEST_PATH => '/foos/key'}) + @mock_response.expects(:start).with(404) + @handler.process(@mock_request, @mock_response) + end + + it "should fail if the request is for an unknown path" do + @mock_request.stubs(:params).returns({ Mongrel::Const::REQUEST_METHOD => 'GET', + Mongrel::Const::REQUEST_PATH => '/bar/key', + 'QUERY_STRING' => '' }) + @mock_response.expects(:start).with(404) + @handler.process(@mock_request, @mock_response) + end + + it "should fail to find model if key is not specified" do + @mock_request.stubs(:params).returns({ Mongrel::Const::REQUEST_METHOD => 'GET', Mongrel::Const::REQUEST_PATH => '/foo'}) + @mock_response.expects(:start).with(404) + @handler.process(@mock_request, @mock_response) + end + + it "should fail to destroy model if key is not specified" do + @mock_request.stubs(:params).returns({ Mongrel::Const::REQUEST_METHOD => 'DELETE', Mongrel::Const::REQUEST_PATH => '/foo'}) + @mock_response.expects(:start).with(404) + @handler.process(@mock_request, @mock_response) + end + + it "should fail to save model if data is not specified" do + @mock_request.stubs(:params).returns({ Mongrel::Const::REQUEST_METHOD => 'PUT', Mongrel::Const::REQUEST_PATH => '/foo'}) + @mock_request.stubs(:body).returns('') + @mock_response.expects(:start).with(404) + @handler.process(@mock_request, @mock_response) + end + + it "should pass HTTP request parameters to model find" do + setup_find_request('QUERY_STRING' => 'foo=baz&bar=xyzzy') + @mock_model_class.expects(:find).with do |key, args| + key == 'key' and args['foo'] == 'baz' and args['bar'] == 'xyzzy' + end + @handler.process(@mock_request, @mock_response) + end + + it "should pass HTTP request parameters to model search" do + setup_search_request('QUERY_STRING' => 'foo=baz&bar=xyzzy') + @mock_model_class.expects(:search).with do |args| + args['foo'] == 'baz' and args['bar'] == 'xyzzy' + end.returns([]) + @handler.process(@mock_request, @mock_response) + end + + it "should pass HTTP request parameters to model delete" do + setup_destroy_request('QUERY_STRING' => 'foo=baz&bar=xyzzy') + @mock_model_class.expects(:destroy).with do |key, args| + key == 'key' and args['foo'] == 'baz' and args['bar'] == 'xyzzy' + end + @handler.process(@mock_request, @mock_response) + end + + it "should pass HTTP request parameters to model save" do + setup_save_request('QUERY_STRING' => 'foo=baz&bar=xyzzy') + @mock_model_instance.expects(:save).with do |args| + args[:data] == 'this is a fake request body' and args['foo'] == 'baz' and args['bar'] == 'xyzzy' + end + @handler.process(@mock_request, @mock_response) + end + + it "should generate a 200 response when a model find call succeeds" do + setup_find_request + @mock_response.expects(:start).with(200) + @handler.process(@mock_request, @mock_response) + end + + it "should generate a 200 response when a model search call succeeds" do + setup_search_request + @mock_response.expects(:start).with(200) + @handler.process(@mock_request, @mock_response) + end + + it "should generate a 200 response when a model destroy call succeeds" do + setup_destroy_request + @mock_response.expects(:start).with(200) + @handler.process(@mock_request, @mock_response) + end + + it "should generate a 200 response when a model save call succeeds" do + setup_save_request + @mock_response.expects(:start).with(200) + @handler.process(@mock_request, @mock_response) + end + + it "should return a serialized object when a model find call succeeds" do + setup_find_request + @mock_model_instance = stub('model instance') + @mock_model_instance.expects(:to_yaml) + @mock_model_class.stubs(:find).returns(@mock_model_instance) + @handler.process(@mock_request, @mock_response) + end + + it "should return a list of serialized objects when a model search call succeeds" do + setup_search_request + mock_matches = [1..5].collect {|i| mock("model instance #{i}", :to_yaml => "model instance #{i}") } + @mock_model_class.stubs(:search).returns(mock_matches) + @handler.process(@mock_request, @mock_response) + end + + it "should return a serialized success result when a model destroy call succeeds" do + setup_destroy_request + @mock_model_class.stubs(:destroy).returns(true) + @mock_body.expects(:write).with("--- true\n") + @handler.process(@mock_request, @mock_response) + end + + it "should return a serialized object when a model save call succeeds" do + setup_save_request + @mock_model_instance.stubs(:save).returns(@mock_model_instance) + @mock_model_instance.expects(:to_yaml).returns('foo') + @handler.process(@mock_request, @mock_response) + end + + it "should serialize a controller exception when an exception is thrown by find" do + setup_find_request + @mock_model_class.expects(:find).raises(ArgumentError) + @mock_response.expects(:start).with(404) + @handler.process(@mock_request, @mock_response) + end + + it "should serialize a controller exception when an exception is thrown by search" do + setup_search_request + @mock_model_class.expects(:search).raises(ArgumentError) + @mock_response.expects(:start).with(404) + @handler.process(@mock_request, @mock_response) + end + + it "should serialize a controller exception when an exception is thrown by destroy" do + setup_destroy_request + @mock_model_class.expects(:destroy).raises(ArgumentError) + @mock_response.expects(:start).with(404) + @handler.process(@mock_request, @mock_response) + end + + it "should serialize a controller exception when an exception is thrown by save" do + setup_save_request + @mock_model_instance.expects(:save).raises(ArgumentError) + @mock_response.expects(:start).with(404) + @handler.process(@mock_request, @mock_response) + end + + it "should serialize a controller exception if the request fails" do + setup_bad_request + @mock_response.expects(:start).with(404) + @handler.process(@mock_request, @mock_response) + end +end diff --git a/spec/unit/network/http/mongrel/xmlrpc.rb b/spec/unit/network/http/mongrel/xmlrpc.rb new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/spec/unit/network/http/mongrel/xmlrpc.rb diff --git a/spec/unit/network/http/webrick.rb b/spec/unit/network/http/webrick.rb new file mode 100644 index 000000000..3ed223e7e --- /dev/null +++ b/spec/unit/network/http/webrick.rb @@ -0,0 +1,115 @@ +#!/usr/bin/env ruby +# +# Created by Rick Bradley on 2007-10-15. +# Copyright (c) 2007. All rights reserved. + +require File.dirname(__FILE__) + '/../../../spec_helper' +require 'puppet/network/http' + +describe Puppet::Network::HTTP::WEBrick, "after initializing" do + it "should not be listening" do + Puppet::Network::HTTP::WEBrick.new.should_not be_listening + end +end + +describe Puppet::Network::HTTP::WEBrick, "when turning on listening" do + before do + @mock_webrick = mock('webrick') + [:mount, :start, :shutdown].each {|meth| @mock_webrick.stubs(meth)} + WEBrick::HTTPServer.stubs(:new).returns(@mock_webrick) + @server = Puppet::Network::HTTP::WEBrick.new + @listen_params = { :address => "127.0.0.1", :port => 31337, :handlers => [ :node, :configuration ], :protocols => [ :rest, :xmlrpc ] } + end + + it "should fail if already listening" do + @server.listen(@listen_params) + Proc.new { @server.listen(@listen_params) }.should raise_error(RuntimeError) + end + + it "should require at least one handler" do + Proc.new { @server.listen(@listen_params.delete_if {|k,v| :handlers == k}) }.should raise_error(ArgumentError) + end + + it "should require at least one protocol" do + Proc.new { @server.listen(@listen_params.delete_if {|k,v| :protocols == k}) }.should raise_error(ArgumentError) + end + + it "should require a listening address to be specified" do + Proc.new { @server.listen(@listen_params.delete_if {|k,v| :address == k})}.should raise_error(ArgumentError) + end + + it "should require a listening port to be specified" do + Proc.new { @server.listen(@listen_params.delete_if {|k,v| :port == k})}.should raise_error(ArgumentError) + end + + it "should order a webrick server to start" do + @mock_webrick.expects(:start) + @server.listen(@listen_params) + end + + it "should tell webrick to listen on the specified address and port" do + WEBrick::HTTPServer.expects(:new).with {|args| + args[:Port] == 31337 and args[:BindAddress] == "127.0.0.1" + }.returns(@mock_webrick) + @server.listen(@listen_params) + end + + it "should be listening" do + @server.listen(@listen_params) + @server.should be_listening + end + + it "should instantiate a handler for each protocol+handler pair to configure web server routing" do + @listen_params[:protocols].each do |protocol| + mock_handler = mock("handler instance for [#{protocol}]") + mock_handler_class = mock("handler class for [#{protocol}]") + @listen_params[:handlers].each do |handler| + mock_handler_class.expects(:new).with {|args| + args[:server] == @mock_webrick and args[:handler] == handler + }.returns(mock_handler) + end + @server.expects(:class_for_protocol).with(protocol).at_least_once.returns(mock_handler_class) + end + @server.listen(@listen_params) + end + + it "should use a WEBrick + REST class to configure WEBrick when REST services are requested" do + Puppet::Network::HTTP::WEBrickREST.expects(:new).at_least_once + @server.listen(@listen_params.merge(:protocols => [:rest])) + end + + it "should use a WEBrick + XMLRPC class to configure WEBrick when XMLRPC services are requested" do + Puppet::Network::HTTP::WEBrickXMLRPC.expects(:new).at_least_once + @server.listen(@listen_params.merge(:protocols => [:xmlrpc])) + end + + it "should fail if services from an unknown protocol are requested" do + Proc.new { @server.listen(@listen_params.merge(:protocols => [ :foo ]))}.should raise_error(ArgumentError) + end +end + +describe Puppet::Network::HTTP::WEBrick, "when turning off listening" do + before do + @mock_webrick = mock('webrick') + [:mount, :start, :shutdown].each {|meth| @mock_webrick.stubs(meth)} + WEBrick::HTTPServer.stubs(:new).returns(@mock_webrick) + @server = Puppet::Network::HTTP::WEBrick.new + @listen_params = { :address => "127.0.0.1", :port => 31337, :handlers => [ :node, :configuration ], :protocols => [ :rest, :xmlrpc ] } + end + + it "should fail unless listening" do + Proc.new { @server.unlisten }.should raise_error(RuntimeError) + end + + it "should order webrick server to stop" do + @mock_webrick.expects(:shutdown) + @server.listen(@listen_params) + @server.unlisten + end + + it "should no longer be listening" do + @server.listen(@listen_params) + @server.unlisten + @server.should_not be_listening + end +end diff --git a/spec/unit/network/http/webrick/rest.rb b/spec/unit/network/http/webrick/rest.rb new file mode 100644 index 000000000..aa7a3d53a --- /dev/null +++ b/spec/unit/network/http/webrick/rest.rb @@ -0,0 +1,286 @@ +#!/usr/bin/env ruby +# +# Created by Rick Bradley on 2007-10-16. +# Copyright (c) 2007. All rights reserved. + +require File.dirname(__FILE__) + '/../../../../spec_helper' +require 'puppet/network/http' + +describe Puppet::Network::HTTP::WEBrickREST, "when initializing" do + before do + @mock_webrick = stub('WEBrick server', :mount => true) + @mock_model = mock('indirected model') + Puppet::Indirector::Indirection.stubs(:model).returns(@mock_model) + @params = { :server => @mock_webrick, :handler => :foo } + end + + it "should require access to a WEBrick server" do + Proc.new { Puppet::Network::HTTP::WEBrickREST.new(@params.delete_if {|k,v| :server == k })}.should raise_error(ArgumentError) + end + + it "should require an indirection name" do + Proc.new { Puppet::Network::HTTP::WEBrickREST.new(@params.delete_if {|k,v| :handler == k })}.should raise_error(ArgumentError) + end + + it "should look up the indirection model from the indirection name" do + Puppet::Indirector::Indirection.expects(:model).returns(@mock_model) + Puppet::Network::HTTP::WEBrickREST.new(@params) + end + + it "should fail if the indirection is not known" do + Puppet::Indirector::Indirection.expects(:model).returns(nil) + Proc.new { Puppet::Network::HTTP::WEBrickREST.new(@params) }.should raise_error(ArgumentError) + end + + it "should register itself with the WEBrick server for the singular HTTP methods" do + @mock_webrick.expects(:mount).with do |*args| + args.first == '/foo' and args.last.is_a?(Puppet::Network::HTTP::WEBrickREST) + end + Puppet::Network::HTTP::WEBrickREST.new(@params) + end + + it "should register itself with the WEBrick server for the plural GET method" do + @mock_webrick.expects(:mount).with do |*args| + args.first == '/foos' and args.last.is_a?(Puppet::Network::HTTP::WEBrickREST) + end + Puppet::Network::HTTP::WEBrickREST.new(@params) + end +end + +describe Puppet::Network::HTTP::WEBrickREST, "when receiving a request" do + before do + @mock_request = stub('webrick http request', :query => {}) + @mock_response = stub('webrick http response', :status= => true, :body= => true) + @mock_model_class = stub('indirected model class') + @mock_webrick = stub('mongrel http server', :mount => true) + Puppet::Indirector::Indirection.stubs(:model).with(:foo).returns(@mock_model_class) + @handler = Puppet::Network::HTTP::WEBrickREST.new(:server => @mock_webrick, :handler => :foo) + end + + def setup_find_request + @mock_request.stubs(:request_method).returns('GET') + @mock_request.stubs(:path).returns('/foo/key') + @mock_model_class.stubs(:find) + end + + def setup_search_request + @mock_request.stubs(:request_method).returns('GET') + @mock_request.stubs(:path).returns('/foos') + @mock_model_class.stubs(:search).returns([]) + end + + def setup_destroy_request + @mock_request.stubs(:request_method).returns('DELETE') + @mock_request.stubs(:path).returns('/foo/key') + @mock_model_class.stubs(:destroy) + end + + def setup_save_request + @mock_request.stubs(:request_method).returns('PUT') + @mock_request.stubs(:path).returns('/foo') + @mock_request.stubs(:body).returns('This is a fake request body') + @mock_model_instance = stub('indirected model instance', :save => true) + @mock_model_class.stubs(:new).returns(@mock_model_instance) + end + + def setup_bad_request + @mock_request.stubs(:request_method).returns('POST') + @mock_request.stubs(:path).returns('/foos') + end + + it "should call the model find method if the request represents a singular HTTP GET" do + setup_find_request + @mock_model_class.expects(:find).with('key', {}) + @handler.service(@mock_request, @mock_response) + end + + it "should call the model search method if the request represents a plural HTTP GET" do + setup_search_request + @mock_model_class.expects(:search).returns([]) + @handler.service(@mock_request, @mock_response) + end + + it "should call the model destroy method if the request represents an HTTP DELETE" do + setup_destroy_request + @mock_model_class.expects(:destroy).with('key', {}) + @handler.service(@mock_request, @mock_response) + end + + it "should call the model save method if the request represents an HTTP PUT" do + setup_save_request + @mock_model_instance.expects(:save).with(:data => 'This is a fake request body') + @mock_model_class.expects(:new).returns(@mock_model_instance) + @handler.service(@mock_request, @mock_response) + end + + it "should fail if the HTTP method isn't supported" do + @mock_request.stubs(:request_method).returns('POST') + @mock_request.stubs(:path).returns('/foo') + @mock_response.expects(:status=).with(404) + @handler.process(@mock_request, @mock_response) + end + + it "should fail if the request's pluralization is wrong" do + @mock_request.stubs(:request_method).returns('DELETE') + @mock_request.stubs(:path).returns('/foos/key') + @mock_response.expects(:status=).with(404) + @handler.process(@mock_request, @mock_response) + + @mock_request.stubs(:request_method).returns('PUT') + @mock_request.stubs(:path).returns('/foos/key') + @mock_response.expects(:status=).with(404) + @handler.process(@mock_request, @mock_response) + end + + it "should fail if the request is for an unknown path" do + @mock_request.stubs(:request_method).returns('GET') + @mock_request.stubs(:path).returns('/bar/key') + @mock_response.expects(:status=).with(404) + @handler.process(@mock_request, @mock_response) + end + + it "should fail to find model if key is not specified" do + @mock_request.stubs(:request_method).returns('GET') + @mock_request.stubs(:path).returns('/foo') + @mock_response.expects(:status=).with(404) + @handler.process(@mock_request, @mock_response) + end + + it "should fail to destroy model if key is not specified" do + @mock_request.stubs(:request_method).returns('DELETE') + @mock_request.stubs(:path).returns('/foo') + @mock_response.expects(:status=).with(404) + @handler.process(@mock_request, @mock_response) + end + + it "should fail to save model if data is not specified" do + @mock_request.stubs(:request_method).returns('PUT') + @mock_request.stubs(:path).returns('/foo') + @mock_request.stubs(:body).returns('') + @mock_response.expects(:status=).with(404) + @handler.process(@mock_request, @mock_response) + end + + it "should pass HTTP request parameters to model find" do + setup_find_request + @mock_request.stubs(:query).returns(:foo => :baz, :bar => :xyzzy) + @mock_model_class.expects(:find).with do |key, args| + key == 'key' and args[:foo] == :baz and args[:bar] == :xyzzy + end + @handler.service(@mock_request, @mock_response) + end + + it "should pass HTTP request parameters to model search" do + setup_search_request + @mock_request.stubs(:query).returns(:foo => :baz, :bar => :xyzzy) + @mock_model_class.expects(:search).with do |args| + args[:foo] == :baz and args[:bar] == :xyzzy + end.returns([]) + @handler.service(@mock_request, @mock_response) + end + + it "should pass HTTP request parameters to model destroy" do + setup_destroy_request + @mock_request.stubs(:query).returns(:foo => :baz, :bar => :xyzzy) + @mock_model_class.expects(:destroy).with do |key, args| + key == 'key' and args[:foo] == :baz and args[:bar] == :xyzzy + end + @handler.service(@mock_request, @mock_response) + end + + it "should pass HTTP request parameters to model save" do + setup_save_request + @mock_request.stubs(:query).returns(:foo => :baz, :bar => :xyzzy) + @mock_model_instance.expects(:save).with do |args| + args[:data] == 'This is a fake request body' and args[:foo] == :baz and args[:bar] == :xyzzy + end + @handler.service(@mock_request, @mock_response) + end + + it "should generate a 200 response when a model find call succeeds" do + setup_find_request + @mock_response.expects(:status=).with(200) + @handler.process(@mock_request, @mock_response) + end + + it "should generate a 200 response when a model search call succeeds" do + setup_search_request + @mock_response.expects(:status=).with(200) + @handler.process(@mock_request, @mock_response) + end + + it "should generate a 200 response when a model destroy call succeeds" do + setup_destroy_request + @mock_response.expects(:status=).with(200) + @handler.process(@mock_request, @mock_response) + end + + it "should generate a 200 response when a model save call succeeds" do + setup_save_request + @mock_response.expects(:status=).with(200) + @handler.process(@mock_request, @mock_response) + end + + it "should return a serialized object when a model find call succeeds" do + setup_find_request + @mock_model_instance = stub('model instance') + @mock_model_instance.expects(:to_yaml) + @mock_model_class.stubs(:find).returns(@mock_model_instance) + @handler.process(@mock_request, @mock_response) + end + + it "should return a list of serialized objects when a model search call succeeds" do + setup_search_request + mock_matches = [1..5].collect {|i| mock("model instance #{i}", :to_yaml => "model instance #{i}") } + @mock_model_class.stubs(:search).returns(mock_matches) + @handler.process(@mock_request, @mock_response) + end + + it "should return a serialized success result when a model destroy call succeeds" do + setup_destroy_request + @mock_model_class.stubs(:destroy).returns(true) + @mock_response.expects(:body=).with("--- true\n") + @handler.process(@mock_request, @mock_response) + end + + it "should return a serialized object when a model save call succeeds" do + setup_save_request + @mock_model_instance.stubs(:save).returns(@mock_model_instance) + @mock_model_instance.expects(:to_yaml).returns('foo') + @handler.process(@mock_request, @mock_response) + end + + it "should serialize a controller exception when an exception is thrown by find" do + setup_find_request + @mock_model_class.expects(:find).raises(ArgumentError) + @mock_response.expects(:status=).with(404) + @handler.process(@mock_request, @mock_response) + end + + it "should serialize a controller exception when an exception is thrown by search" do + setup_search_request + @mock_model_class.expects(:search).raises(ArgumentError) + @mock_response.expects(:status=).with(404) + @handler.process(@mock_request, @mock_response) + end + + it "should serialize a controller exception when an exception is thrown by destroy" do + setup_destroy_request + @mock_model_class.expects(:destroy).raises(ArgumentError) + @mock_response.expects(:status=).with(404) + @handler.process(@mock_request, @mock_response) + end + + it "should serialize a controller exception when an exception is thrown by save" do + setup_save_request + @mock_model_instance.expects(:save).raises(ArgumentError) + @mock_response.expects(:status=).with(404) + @handler.process(@mock_request, @mock_response) + end + + it "should serialize a controller exception if the request fails" do + setup_bad_request + @mock_response.expects(:status=).with(404) + @handler.process(@mock_request, @mock_response) + end +end diff --git a/spec/unit/network/http/webrick/xmlrpc.rb b/spec/unit/network/http/webrick/xmlrpc.rb new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/spec/unit/network/http/webrick/xmlrpc.rb diff --git a/spec/unit/network/rest_controller.rb b/spec/unit/network/rest_controller.rb deleted file mode 100644 index 0bcc0abf2..000000000 --- a/spec/unit/network/rest_controller.rb +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env ruby -# -# Created by Rick Bradley on 2007-10-03. -# Copyright (c) 2007. All rights reserved. - -require File.dirname(__FILE__) + '/../../spec_helper' - -require 'puppet/network/rest_controller' - -describe Puppet::Network::RESTController, "in general" do - it "should route GET requests on indirector's name to indirector find for the model class" - it "should route GET requests on indirector's plural name to indirector search for the model class" - it "should route DELETE requests on indirector's name to indirector destroy for the model class" - it "should route POST requests on indirector's name to indirector save for the model class" - it "should serialize result data when methods are handled" - it "should serialize an error condition when indirection method call generates an exception" -end - -__END__ - -# possible implementation of the satisfying class - -class RESTController - def initialize(klass) - @klass = klass - end - - # TODO: is it possible to distinguish from the request object the path which we were called by? - - def do_GET(request, response) - return do_GETS(request, response) if asked_for_plural?(request) - args = request.something - result = @klass.find args - return serialize(result) - end - - def do_GETS(request, response) - args = request.something - result = @klass.search args - return serialize(result) - end - - def do_DELETE(request, response) - args = request.something - result = @klass.destroy args - return serialize(result) - end - - def do_PUT(request, response) - args = request.something - obj = @klass.new(args) - result = obj.save - return serialize(result) - end - - def do_POST(request, response) - do_PUT(request, response) - end - - private - - def asked_for_plural?(request) - # TODO: pick apart the request and see if this was trying to do a plural or singular GET - end -end diff --git a/spec/unit/network/server.rb b/spec/unit/network/server.rb index 17ed336de..3e29807ad 100644 --- a/spec/unit/network/server.rb +++ b/spec/unit/network/server.rb @@ -4,181 +4,286 @@ # Copyright (c) 2007. All rights reserved. require File.dirname(__FILE__) + '/../../spec_helper' - require 'puppet/network/server' -# a fake server class, so we don't have to implement full autoloading etc. (or at least just yet) just to do testing -class TestServer < Puppet::Network::Server - def start_web_server - end - - def stop_web_server - end -end - describe Puppet::Network::Server, "when initializing" do - before do - Puppet::Network::Server.stubs(:server_class_by_name).returns(TestServer) - end - - it "should use the Puppet configurator to determine which HTTP server will be used to provide access to clients" do - Puppet.expects(:[]).with(:servertype).returns(:suparserver) - @server = Puppet::Network::Server.new - @server.server_type.should == :suparserver - end - - it "should fail to initialize if there is no HTTP server known to the Puppet configurator" do - Puppet.expects(:[]).with(:servertype).returns(nil) - Proc.new { Puppet::Network::Server.new }.should raise_error - end - - it "should use the Puppet Configurator to determine what style of service we will offer to clients (e.g., REST, XMLRPC, ...)" - it "should fail to initialize if there is no style of service known to the Puppet configurator" - - it "should allow registering indirections" do - @server = Puppet::Network::Server.new(:handlers => [ :foo, :bar, :baz]) - Proc.new { @server.unregister(:foo, :bar, :baz) }.should_not raise_error - end - - it "should not be listening after initialization" do - Puppet::Network::Server.new.should_not be_listening - end + before do + @mock_http_server_class = mock('http server class') + Puppet.stubs(:[]).with(:servertype).returns(:suparserver) + Puppet::Network::HTTP.stubs(:server_class_by_type).returns(@mock_http_server_class) + end + + it "should allow specifying a listening address" do + Puppet.stubs(:[]).with(:masterport).returns('') + @server = Puppet::Network::Server.new(:address => "127.0.0.1") + @server.address.should == "127.0.0.1" + end + + it "should allow specifying a listening port" do + Puppet.stubs(:[]).with(:bindaddress).returns('') + @server = Puppet::Network::Server.new(:port => 31337) + @server.port.should == 31337 + end + + it "should use the Puppet configurator to find a default listening address" do + Puppet.stubs(:[]).with(:masterport).returns('') + Puppet.expects(:[]).with(:bindaddress).returns("10.0.0.1") + @server = Puppet::Network::Server.new + @server.address.should == "10.0.0.1" + end + + it "should use the Puppet configurator to find a default listening port" do + Puppet.stubs(:[]).with(:bindaddress).returns('') + Puppet.expects(:[]).with(:masterport).returns(6667) + @server = Puppet::Network::Server.new + @server.port.should == 6667 + end + + it "should fail to initialize if no listening address can be found" do + Puppet.stubs(:[]).with(:masterport).returns(6667) + Puppet.stubs(:[]).with(:bindaddress).returns(nil) + Proc.new { Puppet::Network::Server.new }.should raise_error(ArgumentError) + end + + it "should fail to initialize if no listening port can be found" do + Puppet.stubs(:[]).with(:bindaddress).returns("127.0.0.1") + Puppet.stubs(:[]).with(:masterport).returns(nil) + Proc.new { Puppet::Network::Server.new }.should raise_error(ArgumentError) + end + + it "should use the Puppet configurator to determine which HTTP server will be used to provide access to clients" do + Puppet.expects(:[]).with(:servertype).returns(:suparserver) + @server = Puppet::Network::Server.new(:address => "127.0.0.1", :port => 31337) + @server.server_type.should == :suparserver + end + + it "should fail to initialize if there is no HTTP server known to the Puppet configurator" do + Puppet.expects(:[]).with(:servertype).returns(nil) + Proc.new { Puppet::Network::Server.new(:address => "127.0.0.1", :port => 31337) }.should raise_error + end + + it "should ask the Puppet::Network::HTTP class to fetch the proper HTTP server class" do + Puppet::Network::HTTP.expects(:server_class_by_type).with(:suparserver).returns(@mock_http_server_class) + @server = Puppet::Network::Server.new(:address => "127.0.0.1", :port => 31337) + end + + it "should fail if the HTTP server class is unknown" do + Puppet::Network::HTTP.stubs(:server_class_by_type).returns(nil) + Proc.new { Puppet::Network::Server.new(:address => "127.0.0.1", :port => 31337) }.should raise_error(ArgumentError) + end + + it "should allow registering indirections" do + @server = Puppet::Network::Server.new(:address => "127.0.0.1", :port => 31337, :handlers => [ :foo, :bar, :baz]) + Proc.new { @server.unregister(:foo, :bar, :baz) }.should_not raise_error + end + + it "should not be listening after initialization" do + Puppet::Network::Server.new(:address => "127.0.0.1", :port => 31337).should_not be_listening + end end describe Puppet::Network::Server, "in general" do - before do - Puppet::Network::Server.stubs(:server_class_by_name).returns(TestServer) - Puppet.stubs(:[]).with(:servertype).returns(:suparserver) - @server = Puppet::Network::Server.new - end - - it "should allow registering an indirection for client access by specifying its indirection name" do - Proc.new { @server.register(:foo) }.should_not raise_error - end - - it "should require at least one indirection name when registering indirections for client access" do - Proc.new { @server.register }.should raise_error(ArgumentError) - end - - it "should allow for numerous indirections to be registered at once for client access" do - Proc.new { @server.register(:foo, :bar, :baz) }.should_not raise_error - end - - it "should allow the use of indirection names to specify which indirections are to be no longer accessible to clients" do - @server.register(:foo) - Proc.new { @server.unregister(:foo) }.should_not raise_error - end - - it "should leave other indirections accessible to clients when turning off indirections" do - @server.register(:foo, :bar) - @server.unregister(:foo) - Proc.new { @server.unregister(:bar)}.should_not raise_error - end - - it "should allow specifying numerous indirections which are to be no longer accessible to clients" do - @server.register(:foo, :bar) - Proc.new { @server.unregister(:foo, :bar) }.should_not raise_error - end - - it "should not allow turning off unknown indirection names" do - @server.register(:foo, :bar) - Proc.new { @server.unregister(:baz) }.should raise_error(ArgumentError) - end - - it "should disable client access immediately when turning off indirections" do - @server.register(:foo, :bar) - @server.unregister(:foo) - Proc.new { @server.unregister(:foo) }.should raise_error(ArgumentError) - end - - it "should allow turning off all indirections at once" do - @server.register(:foo, :bar) - @server.unregister - [ :foo, :bar, :baz].each do |indirection| - Proc.new { @server.unregister(indirection) }.should raise_error(ArgumentError) - end - end - - it "should provide a means of determining whether it is listening" do - @server.should respond_to(:listening?) - end - - it "should provide a means of determining which HTTP server will be used to provide access to clients" do - @server.server_type.should == :suparserver - end - - it "should provide a means of determining which style of service is being offered to clients" - - it "should allow for multiple configurations, each handling different indirections" do - @server2 = Puppet::Network::Server.new - @server.register(:foo, :bar) - @server2.register(:foo, :xyzzy) - @server.unregister(:foo, :bar) - @server2.unregister(:foo, :xyzzy) - Proc.new { @server.unregister(:xyzzy) }.should raise_error(ArgumentError) - Proc.new { @server2.unregister(:bar) }.should raise_error(ArgumentError) - end -end + before do + @mock_http_server_class = mock('http server class') + Puppet::Network::HTTP.stubs(:server_class_by_type).returns(@mock_http_server_class) + Puppet.stubs(:[]).with(:servertype).returns(:suparserver) + @server = Puppet::Network::Server.new(:address => "127.0.0.1", :port => 31337) + end + + it "should allow registering an indirection for client access by specifying its indirection name" do + Proc.new { @server.register(:foo) }.should_not raise_error + end + + it "should require at least one indirection name when registering indirections for client access" do + Proc.new { @server.register }.should raise_error(ArgumentError) + end + + it "should allow for numerous indirections to be registered at once for client access" do + Proc.new { @server.register(:foo, :bar, :baz) }.should_not raise_error + end -describe Puppet::Network::Server, "when listening is turned off" do - before do - Puppet::Network::Server.stubs(:server_class_by_name).returns(TestServer) - Puppet.stubs(:[]).with(:servertype).returns(:suparserver) - @server = Puppet::Network::Server.new - end + it "should allow the use of indirection names to specify which indirections are to be no longer accessible to clients" do + @server.register(:foo) + Proc.new { @server.unregister(:foo) }.should_not raise_error + end + + it "should leave other indirections accessible to clients when turning off indirections" do + @server.register(:foo, :bar) + @server.unregister(:foo) + Proc.new { @server.unregister(:bar)}.should_not raise_error + end - it "should allow listening to be turned on" do - Proc.new { @server.listen }.should_not raise_error - end + it "should allow specifying numerous indirections which are to be no longer accessible to clients" do + @server.register(:foo, :bar) + Proc.new { @server.unregister(:foo, :bar) }.should_not raise_error + end + + it "should not turn off any indirections if given unknown indirection names to turn off" do + @server.register(:foo, :bar) + Proc.new { @server.unregister(:foo, :bar, :baz) }.should raise_error(ArgumentError) + Proc.new { @server.unregister(:foo, :bar) }.should_not raise_error + end - it "should not allow listening to be turned off" do - Proc.new { @server.unlisten }.should raise_error(RuntimeError) - end + it "should not allow turning off unknown indirection names" do + @server.register(:foo, :bar) + Proc.new { @server.unregister(:baz) }.should raise_error(ArgumentError) + end - it "should indicate that it is not listening" do - @server.should_not be_listening - end + it "should disable client access immediately when turning off indirections" do + @server.register(:foo, :bar) + @server.unregister(:foo) + Proc.new { @server.unregister(:foo) }.should raise_error(ArgumentError) + end - it "should cause the HTTP server to listen when listening is turned on" do - @server.expects(:start_web_server) - @server.listen - end + it "should allow turning off all indirections at once" do + @server.register(:foo, :bar) + @server.unregister + [ :foo, :bar, :baz].each do |indirection| + Proc.new { @server.unregister(indirection) }.should raise_error(ArgumentError) + end + end + + it "should provide a means of determining whether it is listening" do + @server.should respond_to(:listening?) + end - it "should not route HTTP GET requests to a controller for the registered indirection" - it "should not route HTTP DELETE requests to a controller for the registered indirection" - it "should not route HTTP POST requests to a controller for the registered indirection" + it "should provide a means of determining which HTTP server will be used to provide access to clients" do + @server.server_type.should == :suparserver + end + + it "should allow for multiple configurations, each handling different indirections" do + @server2 = Puppet::Network::Server.new(:address => "127.0.0.1", :port => 31337) + @server.register(:foo, :bar) + @server2.register(:foo, :xyzzy) + @server.unregister(:foo, :bar) + @server2.unregister(:foo, :xyzzy) + Proc.new { @server.unregister(:xyzzy) }.should raise_error(ArgumentError) + Proc.new { @server2.unregister(:bar) }.should raise_error(ArgumentError) + end - # TODO: FIXME write integrations which fire up actual webrick / mongrel servers and are thus webrick / mongrel specific?] + it "should provide a means of determining which style of service is being offered to clients" do + @server.protocols.should == [] + end + + it "should provide a means of determining the listening address" do + @server.address.should == "127.0.0.1" + end + + it "should provide a means of determining the listening port" do + @server.port.should == 31337 + end end -describe Puppet::Network::Server, "when listening is turned on" do - before do - Puppet::Network::Server.stubs(:server_class_by_name).returns(TestServer) - Puppet.stubs(:[]).with(:servertype).returns(:suparserver) - @server = Puppet::Network::Server.new - @server.listen - end - - it "should allow listening to be turned off" do - Proc.new { @server.unlisten }.should_not raise_error - end +describe Puppet::Network::Server, "when listening is off" do + before do + @mock_http_server_class = mock('http server class') + Puppet::Network::HTTP.stubs(:server_class_by_type).returns(@mock_http_server_class) + Puppet.stubs(:[]).with(:servertype).returns(:suparserver) + @server = Puppet::Network::Server.new(:address => "127.0.0.1", :port => 31337) + @mock_http_server = mock('http server') + @mock_http_server.stubs(:listen) + @server.stubs(:http_server).returns(@mock_http_server) + end + + it "should indicate that it is not listening" do + @server.should_not be_listening + end - it "should not allow listening to be turned on" do - Proc.new { @server.listen }.should raise_error(RuntimeError) - end + it "should not allow listening to be turned off" do + Proc.new { @server.unlisten }.should raise_error(RuntimeError) + end - it "should indicate that listening is turned off" do - @server.should be_listening - end + it "should allow listening to be turned on" do + Proc.new { @server.listen }.should_not raise_error + end + +end - it "should cause the HTTP server to stop listening when listening is turned off" do - @server.expects(:stop_web_server) - @server.unlisten - end +describe Puppet::Network::Server, "when listening is on" do + before do + @mock_http_server_class = mock('http server class') + Puppet::Network::HTTP.stubs(:server_class_by_type).returns(@mock_http_server_class) + Puppet.stubs(:[]).with(:servertype).returns(:suparserver) + @server = Puppet::Network::Server.new(:address => "127.0.0.1", :port => 31337) + @mock_http_server = mock('http server') + @mock_http_server.stubs(:listen) + @mock_http_server.stubs(:unlisten) + @server.stubs(:http_server).returns(@mock_http_server) + @server.listen + end + + it "should indicate that listening is turned off" do + @server.should be_listening + end + + it "should not allow listening to be turned on" do + Proc.new { @server.listen }.should raise_error(RuntimeError) + end - it "should route HTTP GET requests to a controller for the registered indirection" - it "should route HTTP DELETE requests to a controller for the registered indirection" - it "should route HTTP POST requests to a controller for the registered indirection" + it "should allow listening to be turned off" do + Proc.new { @server.unlisten }.should_not raise_error + end +end + +describe Puppet::Network::Server, "when listening is being turned on" do + before do + @mock_http_server_class = mock('http server class') + Puppet::Network::HTTP.stubs(:server_class_by_type).returns(@mock_http_server_class) + Puppet.stubs(:[]).with(:servertype).returns(:suparserver) + @server = Puppet::Network::Server.new(:address => "127.0.0.1", :port => 31337) + @mock_http_server = mock('http server') + @mock_http_server.stubs(:listen) + end + + it "should fetch an instance of an HTTP server when listening is turned on" do + mock_http_server_class = mock('http server class') + mock_http_server_class.expects(:new).returns(@mock_http_server) + @server.expects(:http_server_class).returns(mock_http_server_class) + @server.listen + end + + it "should cause the HTTP server to listen when listening is turned on" do + @mock_http_server.expects(:listen) + @server.expects(:http_server).returns(@mock_http_server) + @server.listen + end +end + +describe Puppet::Network::Server, "when listening is being turned off" do + before do + @mock_http_server_class = mock('http server class') + Puppet::Network::HTTP.stubs(:server_class_by_type).returns(@mock_http_server_class) + Puppet.stubs(:[]).with(:servertype).returns(:suparserver) + @server = Puppet::Network::Server.new(:address => "127.0.0.1", :port => 31337) + @mock_http_server = mock('http server') + @mock_http_server.stubs(:listen) + @server.stubs(:http_server).returns(@mock_http_server) + @server.listen + end + + it "should cause the HTTP server to stop listening when listening is turned off" do + @mock_http_server.expects(:unlisten) + @server.unlisten + end + + it "should not allow for indirections to be turned off" do + @server.register(:foo) + Proc.new { @server.unregister(:foo) }.should raise_error(RuntimeError) + end +end + + +describe Class.new, "put these somewhere" do + it "should allow indirections to deny access to services based upon which client is connecting, or whether the client is authorized" + it "should deny access to clients based upon rules" + it "should have the ability to use a class-level from_ hook (from_yaml, from_text, etc.) that can be called, based on content-type header, to allow for different deserializations of an object" + it "should allow from_* on the inbound :data packet (look at its content_type) when doing a PUT/.new.save" + it "should prepend a rest version number on the path (w00t)" + it "should ... on server side, .save should from_yaml, then foo.save(args) instead of just Foo.new.save(args)" + it "should have a from_yaml class_method in the indirector (... default: yaml.load(data) => instance, but can be overridden)" +end - # TODO: FIXME [ write integrations which fire up actual webrick / mongrel servers and are thus webrick / mongrel specific?] +describe Puppet::Indirector, "stuff required by HTTP servers" do + it "should provide the model with the ability to serialize to XML" + it "should provide the model with the ability to deserialize from XML" end diff --git a/spec/unit/node/configuration.rb b/spec/unit/node/configuration.rb index e9dc6b85d..5780d4fbb 100755 --- a/spec/unit/node/configuration.rb +++ b/spec/unit/node/configuration.rb @@ -301,9 +301,9 @@ end describe Puppet::Node::Configuration, " when functioning as a resource container" do before do @config = Puppet::Node::Configuration.new("host") - @one = stub 'resource1', :ref => "Me[you]", :configuration= => nil - @two = stub 'resource2', :ref => "Me[him]", :configuration= => nil - @dupe = stub 'resource3', :ref => "Me[you]", :configuration= => nil + @one = stub 'resource1', :ref => "Me[one]", :configuration= => nil + @two = stub 'resource2', :ref => "Me[two]", :configuration= => nil + @dupe = stub 'resource3', :ref => "Me[one]", :configuration= => nil end it "should provide a method to add one or more resources" do @@ -376,7 +376,7 @@ describe Puppet::Node::Configuration, " when functioning as a resource container it "should be able to find resources by reference or by type/title tuple" do @config.add_resource @one - @config.resource("me", "you").should equal(@one) + @config.resource("me", "one").should equal(@one) end it "should have a mechanism for removing resources" do @@ -386,6 +386,32 @@ describe Puppet::Node::Configuration, " when functioning as a resource container @config.resource(@one.ref).should be_nil @config.vertex?(@one).should be_false end + + it "should have a method for creating aliases for resources" do + @config.add_resource @one + @config.alias(@one, "other") + @config.resource("me", "other").should equal(@one) + end + + # This test is the same as the previous, but the behaviour should be explicit. + it "should alias using the class name from the resource reference, not the resource class name" do + @config.add_resource @one + @config.alias(@one, "other") + @config.resource("me", "other").should equal(@one) + end + + it "should fail to add an alias if the aliased name already exists" do + @config.add_resource @one + proc { @config.alias @two, "one" }.should raise_error(ArgumentError) + end + + it "should remove resource aliases when the target resource is removed" do + @config.add_resource @one + @config.alias(@one, "other") + @one.expects :remove + @config.remove_resource(@one) + @config.resource("me", "other").should be_nil + end end module ApplyingConfigurations diff --git a/spec/unit/other/transbucket.rb b/spec/unit/other/transbucket.rb index 8cb9abaa4..0da808460 100755 --- a/spec/unit/other/transbucket.rb +++ b/spec/unit/other/transbucket.rb @@ -102,6 +102,18 @@ describe Puppet::TransBucket, " when generating a configuration" do @top.to_configuration end + it "should set each TransObject's configuration before converting to a RAL resource" do + @middleobj.expects(:configuration=).with { |c| c.is_a?(Puppet::Node::Configuration) } + @top.to_configuration + end + + it "should set each TransBucket's configuration before converting to a RAL resource" do + # each bucket is seen twice in the loop, so we have to handle the case where the config + # is set twice + @bottom.expects(:configuration=).with { |c| c.is_a?(Puppet::Node::Configuration) }.at_least_once + @top.to_configuration + end + after do Puppet::Type.allclear end diff --git a/spec/unit/other/transobject.rb b/spec/unit/other/transobject.rb index 144940b7e..eaca855db 100755 --- a/spec/unit/other/transobject.rb +++ b/spec/unit/other/transobject.rb @@ -2,114 +2,73 @@ require File.dirname(__FILE__) + '/../../spec_helper' -describe Puppet::TransObject, " when building its search path" do -end - -describe Puppet::TransObject, " when building its search path" do -end -#!/usr/bin/env ruby - -$:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ - -require 'puppet' require 'puppet/transportable' -require 'puppettest' -require 'puppettest/parsertesting' -require 'yaml' -class TestTransportable < Test::Unit::TestCase - include PuppetTest::ParserTesting - - def test_yamldumpobject - obj = mk_transobject - obj.to_yaml_properties - str = nil - assert_nothing_raised { - str = YAML.dump(obj) - } - - newobj = nil - assert_nothing_raised { - newobj = YAML.load(str) - } - - assert(newobj.name, "Object has no name") - assert(newobj.type, "Object has no type") +describe Puppet::TransObject, " when serializing" do + before do + @resource = Puppet::TransObject.new("/my/file", "file") + @resource["one"] = "test" + @resource["two"] = "other" end - def test_yamldumpbucket - objects = %w{/etc/passwd /etc /tmp /var /dev}.collect { |d| - mk_transobject(d) - } - bucket = mk_transbucket(*objects) - str = nil - assert_nothing_raised { - str = YAML.dump(bucket) - } - - newobj = nil - assert_nothing_raised { - newobj = YAML.load(str) - } - - assert(newobj.name, "Bucket has no name") - assert(newobj.type, "Bucket has no type") + it "should be able to be dumped to yaml" do + proc { YAML.dump(@resource) }.should_not raise_error end - # Verify that we correctly strip out collectable objects, since they should - # not be sent to the client. - def test_collectstrip - top = mk_transtree do |object, depth, width| - if width % 2 == 1 - object.collectable = true - end - end + it "should produce an equivalent yaml object" do + text = YAML.dump(@resource) - assert(top.flatten.find_all { |o| o.collectable }.length > 0, - "Could not find any collectable objects") + newresource = YAML.load(text) + newresource.name.should == "/my/file" + newresource.type.should == "file" + %w{one two}.each do |param| + newresource[param].should == @resource[param] + end + end +end - # Now strip out the collectable objects - top.collectstrip! +describe Puppet::TransObject, " when converting to a RAL resource" do + before do + @resource = Puppet::TransObject.new("/my/file", "file") + @resource["one"] = "test" + @resource["two"] = "other" + end - # And make sure they're actually gone - assert_equal(0, top.flatten.find_all { |o| o.collectable }.length, - "Still found collectable objects") + it "should use the resource type's :create method to create the resource" do + type = mock 'resource type' + type.expects(:create).with(@resource).returns(:myresource) + Puppet::Type.expects(:type).with("file").returns(type) + @resource.to_type.should == :myresource end - # Make sure our 'delve' command is working - def test_delve - top = mk_transtree do |object, depth, width| - if width % 2 == 1 - object.collectable = true - end - end + it "should convert to a component instance if the resource type cannot be found" do + Puppet::Type.expects(:type).with("file").returns(nil) + @resource.expects(:to_component).returns(:mycomponent) + @resource.to_type.should == :mycomponent + end +end - objects = [] - buckets = [] - collectable = [] +describe Puppet::TransObject, " when converting to a RAL component instance" do + before do + @resource = Puppet::TransObject.new("/my/file", "one::two") + @resource["one"] = "test" + @resource["noop"] = "other" + end - count = 0 - assert_nothing_raised { - top.delve do |object| - count += 1 - if object.is_a? Puppet::TransBucket - buckets << object - else - objects << object - if object.collectable - collectable << object - end - end - end - } + it "should use a new TransObject whose name is a resource reference of the type and title of the original TransObject" do + Puppet::Type::Component.expects(:create).with { |resource| resource.type == :component and resource.name == "One::Two[/my/file]" }.returns(:yay) + @resource.to_component.should == :yay + end - top.flatten.each do |obj| - assert(objects.include?(obj), "Missing obj %s[%s]" % [obj.type, obj.name]) - end + it "should pass the resource parameters on to the newly created TransObject" do + Puppet::Type::Component.expects(:create).with { |resource| resource["noop"] == "other" }.returns(:yay) + @resource.to_component.should == :yay + end - assert_equal(collectable.length, - top.flatten.find_all { |o| o.collectable }.length, - "Found incorrect number of collectable objects") + # LAK:FIXME This really isn't the design we want going forward, but it's + # good enough for now. + it "should not pass resource paramaters that are not metaparams" do + Puppet::Type::Component.expects(:create).with { |resource| resource["one"].nil? }.returns(:yay) + @resource.to_component.should == :yay end end - diff --git a/spec/unit/parser/compile.rb b/spec/unit/parser/compile.rb index 93c440417..5f239636b 100755 --- a/spec/unit/parser/compile.rb +++ b/spec/unit/parser/compile.rb @@ -141,3 +141,103 @@ describe Puppet::Parser::Compile, " when evaluating found classes" do @compile.evaluate_classes(%w{myclass notfound}, @scope).should == %w{myclass} end end + +describe Puppet::Parser::Compile, " when evaluating AST nodes with no AST nodes present" do + before do + @node = stub 'node', :name => "foo" + @parser = stub 'parser', :version => "1.0", :nodes => {} + @compile = Puppet::Parser::Compile.new(@node, @parser) + end + + it "should do nothing" do + @compile.expects(:ast_nodes?).returns(false) + @compile.parser.expects(:nodes).never + Puppet::Parser::Resource.expects(:new).never + + @compile.send(:evaluate_ast_node) + end +end + +describe Puppet::Parser::Compile, " when evaluating AST nodes with AST nodes present" do + before do + @node = stub 'node', :name => "foo" + @parser = stub 'parser', :version => "1.0", :nodes => {} + @compile = Puppet::Parser::Compile.new(@node, @parser) + + @nodes = mock 'node_hash' + @compile.stubs(:ast_nodes?).returns(true) + @compile.parser.stubs(:nodes).returns(@nodes) + + # Set some names for our test + @node.stubs(:names).returns(%w{a b c}) + @nodes.stubs(:[]).with("a").returns(nil) + @nodes.stubs(:[]).with("b").returns(nil) + @nodes.stubs(:[]).with("c").returns(nil) + + # It should check this last, of course. + @nodes.stubs(:[]).with("default").returns(nil) + end + + it "should fail if the named node cannot be found" do + proc { @compile.send(:evaluate_ast_node) }.should raise_error(Puppet::ParseError) + end + + it "should create a resource for the first node class matching the node name" do + node_class = stub 'node', :classname => "c" + @nodes.stubs(:[]).with("c").returns(node_class) + + node_resource = stub 'node resource', :ref => "Node[c]", :evaluate => nil + Puppet::Parser::Resource.expects(:new).with { |args| args[:title] == "c" and args[:type] == "node" }.returns(node_resource) + + @compile.send(:evaluate_ast_node) + end + + it "should match the default node if no matching node can be found" do + node_class = stub 'node', :classname => "default" + @nodes.stubs(:[]).with("default").returns(node_class) + + node_resource = stub 'node resource', :ref => "Node[default]", :evaluate => nil + Puppet::Parser::Resource.expects(:new).with { |args| args[:title] == "default" and args[:type] == "node" }.returns(node_resource) + + @compile.send(:evaluate_ast_node) + end + + it "should tag the configuration with the found node name" do + node_class = stub 'node', :classname => "c" + @nodes.stubs(:[]).with("c").returns(node_class) + + node_resource = stub 'node resource', :ref => "Node[c]", :evaluate => nil + Puppet::Parser::Resource.stubs(:new).returns(node_resource) + + @compile.configuration.expects(:tag).with("c") + @compile.send(:evaluate_ast_node) + end + + it "should evaluate the node resource immediately rather than using lazy evaluation" do + node_class = stub 'node', :classname => "c" + @nodes.stubs(:[]).with("c").returns(node_class) + + node_resource = stub 'node resource', :ref => "Node[c]" + Puppet::Parser::Resource.stubs(:new).returns(node_resource) + + node_resource.expects(:evaluate) + + @compile.send(:evaluate_ast_node) + end + + it "should set the node's scope as the top scope" do + node_class = stub 'node', :classname => "c" + @nodes.stubs(:[]).with("c").returns(node_class) + + node_resource = stub 'node resource', :ref => "Node[c]" + Puppet::Parser::Resource.stubs(:new).returns(node_resource) + + # The #evaluate method normally does this. + @compile.class_set(node_class.classname, :my_node_scope) + node_resource.stubs(:evaluate) + + @compile.send(:evaluate_ast_node) + + @compile.topscope.should == :my_node_scope + end +end diff --git a/test/language/compile.rb b/test/language/compile.rb index 380da3da5..50b16a24d 100755 --- a/test/language/compile.rb +++ b/test/language/compile.rb @@ -174,88 +174,18 @@ class TestCompile < Test::Unit::TestCase # "". def test_evaluate_main compile = mkcompile - main = mock 'main_class' - compile.topscope.expects(:source=).with(main) - @parser.expects(:findclass).with("", "").returns(main) + main_class = mock 'main_class' + compile.topscope.expects(:source=).with(main_class) + @parser.expects(:findclass).with("", "").returns(main_class) - assert_nothing_raised("Could not call evaluate_main") do - compile.send(:evaluate_main) - end - - assert(compile.resources.find { |r| r.to_s == "Class[main]" }, "Did not create a 'main' resource") - end - - # Make sure we either don't look for nodes, or that we find and evaluate the right object. - def test_evaluate_ast_node - # First try it with ast_nodes disabled - compile = mkcompile - name = compile.node.name - compile.expects(:ast_nodes?).returns(false) - compile.parser.expects(:nodes).never - - assert_nothing_raised("Could not call evaluate_ast_node when ast nodes are disabled") do - compile.send(:evaluate_ast_node) - end - - assert_nil(compile.resources.find { |r| r.to_s == "Node[#{name}]" }, "Created node object when ast_nodes was false") - - # Now try it with them enabled, but no node found. - nodes = mock 'node_hash' - compile = mkcompile - name = compile.node.name - compile.expects(:ast_nodes?).returns(true) - compile.parser.stubs(:nodes).returns(nodes) - - # Set some names for our test - @node.names = %w{a b c} - nodes.expects(:[]).with("a").returns(nil) - nodes.expects(:[]).with("b").returns(nil) - nodes.expects(:[]).with("c").returns(nil) - - # It should check this last, of course. - nodes.expects(:[]).with("default").returns(nil) + main_resource = mock 'main resource' + Puppet::Parser::Resource.expects(:new).with { |args| args[:title] == :main }.returns(main_resource) - # And make sure the lack of a node throws an exception - assert_raise(Puppet::ParseError, "Did not fail when we couldn't find an ast node") do - compile.send(:evaluate_ast_node) - end - - # Finally, make sure it works dandily when we have a node - compile = mkcompile - compile.expects(:ast_nodes?).returns(true) - - node = stub 'node', :classname => "c" - nodes = {"c" => node} - compile.parser.stubs(:nodes).returns(nodes) - - # Set some names for our test - @node.names = %w{a b c} - - # And make sure we throw no exceptions. - assert_nothing_raised("Failed when a node was found") do - compile.send(:evaluate_ast_node) - end + main_resource.expects(:evaluate) - assert_instance_of(Puppet::Parser::Resource, compile.resources.find { |r| r.to_s == "Node[c]" }, - "Did not create node resource") - - # Lastly, check when we actually find the default. - compile = mkcompile - compile.expects(:ast_nodes?).returns(true) - - node = stub 'node', :classname => "default" - nodes = {"default" => node} - compile.parser.stubs(:nodes).returns(nodes) - - # Set some names for our test - @node.names = %w{a b c} - - # And make sure the lack of a node throws an exception - assert_nothing_raised("Failed when a node was found") do - compile.send(:evaluate_ast_node) + assert_nothing_raised("Could not call evaluate_main") do + compile.send(:evaluate_main) end - assert_instance_of(Puppet::Parser::Resource, compile.resources.find { |r| r.to_s == "Node[default]" }, - "Did not create default node resource") end def test_evaluate_node_classes diff --git a/test/language/parser.rb b/test/language/parser.rb index 04cd3a095..bc89eff20 100755 --- a/test/language/parser.rb +++ b/test/language/parser.rb @@ -694,6 +694,7 @@ file { "/tmp/yayness": manifest = File.join(modpath, "manifest.pp") manifest_texts.each do |txt| + Puppet::Type.allclear File.open(manifest, "w") { |f| f.puts txt } assert_nothing_raised { diff --git a/test/lib/puppettest.rb b/test/lib/puppettest.rb index 440ba3ba2..6447b80fb 100755 --- a/test/lib/puppettest.rb +++ b/test/lib/puppettest.rb @@ -20,6 +20,17 @@ if ARGV.include?("-d") $console = true end +# Some monkey-patching to allow us to test private methods. +class Class + def publicize_methods(*methods) + saved_private_instance_methods = methods.empty? ? self.private_instance_methods : methods + + self.class_eval { public *saved_private_instance_methods } + yield + self.class_eval { private *saved_private_instance_methods } + end +end + module PuppetTest # Munge cli arguments, so we can enable debugging if we want # and so we can run just specific methods. diff --git a/test/lib/puppettest/support/assertions.rb b/test/lib/puppettest/support/assertions.rb index 7e3e5ca2b..906bb3c76 100644 --- a/test/lib/puppettest/support/assertions.rb +++ b/test/lib/puppettest/support/assertions.rb @@ -1,7 +1,9 @@ require 'puppettest' +require 'puppettest/support/utils' require 'fileutils' module PuppetTest + include PuppetTest::Support::Utils def assert_logged(level, regex, msg = nil) # Skip verifying logs that we're not supposed to send. return unless Puppet::Util::Log.sendlevel?(level) diff --git a/test/network/client/master.rb b/test/network/client/master.rb index 4f6956470..7216af936 100755 --- a/test/network/client/master.rb +++ b/test/network/client/master.rb @@ -7,38 +7,6 @@ require 'mocha' class TestMasterClient < Test::Unit::TestCase include PuppetTest::ServerTest - - class FakeTrans - def initialize - @counters = Hash.new { |h,k| h[k] = 0 } - end - [:evaluate, :report, :cleanup, :addtimes, :tags, :ignoreschedules].each do |m| - define_method(m.to_s + "=") do |*args| - @counters[m] += 1 - end - define_method(m) do |*args| - @counters[m] += 1 - end - define_method(m.to_s + "?") do - @counters[m] - end - end - end - class FakeComponent - attr_accessor :trans - def evaluate - @trans = FakeTrans.new - @trans - end - - def finalize - @finalized = true - end - - def finalized? - @finalized - end - end def setup super @@ -67,66 +35,6 @@ class TestMasterClient < Test::Unit::TestCase return client end - - def mk_fake_client - server = Puppet::Network::Handler.master.new :Code => "" - master = Puppet::Network::Client.master.new :Master => server, :Local => true - - # Now create some objects - objects = FakeComponent.new - - master.send(:instance_variable_set, "@objects", objects) - - class << master - def report(r) - @reported ||= 0 - @reported += 1 - end - def reported - @reported ||= 0 - @reported - end - end - return master, objects - end - - def test_getconfig - client = mkclient - - $methodsrun = [] - cleanup { $methodsrun = nil } - client.meta_def(:getplugins) do - $methodsrun << :getplugins - end - client.meta_def(:get_actual_config) do - $methodsrun << :get_actual_config - result = Puppet::TransBucket.new() - result.type = "testing" - result.name = "yayness" - result - end - - assert_nothing_raised do - client.getconfig - end - [:get_actual_config].each do |method| - assert($methodsrun.include?(method), "method %s was not run" % method) - end - assert(! $methodsrun.include?(:getplugins), "plugins were synced even tho disabled") - - # Now set pluginsync - Puppet[:pluginsync] = true - $methodsrun.clear - - assert_nothing_raised do - client.getconfig - end - [:getplugins, :get_actual_config].each do |method| - assert($methodsrun.include?(method), "method %s was not run" % method) - end - - assert_instance_of(Puppet::Node::Configuration, client.configuration, "Configuration was not created") - end def test_disable FileUtils.mkdir_p(Puppet[:statedir]) @@ -136,27 +44,22 @@ class TestMasterClient < Test::Unit::TestCase client = mkclient(master) - assert(! FileTest.exists?(@createdfile)) - - assert_nothing_raised { + assert_nothing_raised("Could not disable client") { client.disable } - assert_nothing_raised { - client.run - } + client.expects(:getconfig).never - assert(! FileTest.exists?(@createdfile), "Disabled client ran") + client.run - assert_nothing_raised { - client.enable - } + client = mkclient(master) - assert_nothing_raised { - client.run - } + client.expects(:getconfig) - assert(FileTest.exists?(@createdfile), "Enabled client did not run") + assert_nothing_raised("Could not enable client") { + client.enable + } + client.run end # Make sure we're getting the client version in our list of facts @@ -652,15 +555,16 @@ end client = mkclient ftype = Puppet::Type.type(:file) + file = ftype.create :title => "/what/ever", :ensure => :present + config = Puppet::Node::Configuration.new + config.add_resource(file) - assert_nil(ftype[@createdfile], "file object already exists") - assert(! FileTest.exists?(@createdfile), "File already exists on disk") + config.expects :apply - assert_nothing_raised("Could not apply config") do - client.run - end + client.configuration = config + client.expects(:getconfig) + client.run - assert(FileTest.exists?(@createdfile), "File does not exist on disk") assert_nil(ftype[@createdfile], "file object was not removed from memory") end @@ -675,20 +579,20 @@ end end end - # #800 -- we cannot fix this using the current design. - def disabled_test_invalid_relationships_do_not_get_cached - # Make a master with an invalid relationship + def test_invalid_configurations_do_not_get_cached master = mkmaster :Code => "notify { one: require => File[yaytest] }" master.local = false # so it gets cached client = mkclient(master) + client.stubs(:facts).returns({}) client.local = false - client.getconfig - # Doesn't throw an exception, but definitely fails. - client.apply + Puppet::Node::Facts.indirection.stubs(:terminus_class).returns(:memory) # Make sure the config is not cached. - config = Puppet.settings[:localconfig] + ".yaml" - assert(! File.exists?(config), "Cached an invalid configuration") + client.expects(:cache).never + + client.getconfig + # Doesn't throw an exception, but definitely fails. + client.run end end diff --git a/test/network/client/resource.rb b/test/network/client/resource.rb index 83c195035..eb8e829eb 100755 --- a/test/network/client/resource.rb +++ b/test/network/client/resource.rb @@ -3,10 +3,13 @@ require File.dirname(__FILE__) + '/../../lib/puppettest' require 'puppettest' +require 'puppettest/support/utils' +require 'puppettest/support/assertions' require 'puppet/network/client/resource' class TestResourceClient < Test::Unit::TestCase include PuppetTest::ServerTest + include PuppetTest::Support::Utils def mkresourceserver Puppet::Network::Handler.resource.new @@ -35,6 +38,7 @@ class TestResourceClient < Test::Unit::TestCase assert_instance_of(Puppet::TransObject, tobj) + Puppet::Type.allclear obj = nil assert_nothing_raised { obj = tobj.to_type @@ -45,6 +49,7 @@ class TestResourceClient < Test::Unit::TestCase File.unlink(file) # Now test applying + Puppet::Type.allclear result = nil assert_nothing_raised { result = client.apply(tobj) @@ -52,6 +57,7 @@ class TestResourceClient < Test::Unit::TestCase assert(FileTest.exists?(file), "File was not created on apply") # Lastly, test "list" + Puppet::Type.allclear list = nil assert_nothing_raised { list = client.list("user") @@ -64,12 +70,14 @@ class TestResourceClient < Test::Unit::TestCase break if count > 3 assert_instance_of(Puppet::TransObject, tobj) + Puppet::Type.allclear tobj2 = nil assert_nothing_raised { tobj2 = client.describe(tobj.type, tobj.name) } obj = nil + Puppet::Type.allclear assert_nothing_raised { obj = tobj2.to_type } diff --git a/test/network/handler/master.rb b/test/network/handler/master.rb index 694888f4d..25117030e 100755 --- a/test/network/handler/master.rb +++ b/test/network/handler/master.rb @@ -13,39 +13,11 @@ class TestMaster < Test::Unit::TestCase Puppet::Indirector::Indirection.clear_cache end - def test_defaultmanifest - textfiles { |file| - Puppet[:manifest] = file - client = nil - master = nil - assert_nothing_raised() { - # this is the default server setup - master = Puppet::Network::Handler.master.new( - :Manifest => file, - :UseNodes => false, - :Local => true - ) - } - assert_nothing_raised() { - client = Puppet::Network::Client.master.new( - :Master => master - ) - } - - # pull our configuration - assert_nothing_raised() { - client.getconfig - stopservices - Puppet::Type.allclear - } - - break - } - end - + # Make sure that files are reread when they change. def test_filereread # Start with a normal setting Puppet[:filetimeout] = 15 + manifest = mktestmanifest() facts = Puppet::Network::Client.master.facts @@ -63,35 +35,12 @@ class TestMaster < Test::Unit::TestCase :Local => true ) } - assert_nothing_raised() { - client = Puppet::Network::Client.master.new( - :Master => master - ) - } - assert(client, "did not create master client") - # The client doesn't have a config, so it can't be up to date - assert(! client.fresh?(facts), - "Client is incorrectly up to date") - - Puppet.settings.use(:main) - config = nil - assert_nothing_raised { - config = client.getconfig - config.apply - } - - # Now it should be up to date - assert(client.fresh?(facts), "Client is not up to date") + config = master.getconfig({"hostname" => "blah"}) # Cache this value for later parse1 = master.freshness("mynode") - # Verify the config got applied - assert(FileTest.exists?(@createdfile), - "Created file %s does not exist" % @createdfile) - Puppet::Type.allclear - sleep 1.5 # Create a new manifest File.open(manifest, "w") { |f| @@ -101,7 +50,6 @@ class TestMaster < Test::Unit::TestCase # Verify that the master doesn't immediately reparse the file; we # want to wait through the timeout assert_equal(parse1, master.freshness("mynode"), "Master did not wait through timeout") - assert(client.fresh?(facts), "Client is not up to date") # Then eliminate it Puppet[:filetimeout] = 0 @@ -109,16 +57,8 @@ class TestMaster < Test::Unit::TestCase # Now make sure the master does reparse #Puppet.notice "%s vs %s" % [parse1, master.freshness] assert(parse1 != master.freshness("mynode"), "Master did not reparse file") - assert(! client.fresh?(facts), "Client is incorrectly up to date") - - # Retrieve and apply the new config - assert_nothing_raised { - config = client.getconfig - config.apply - } - assert(client.fresh?(facts), "Client is not up to date") - assert(FileTest.exists?(file2), "Second file %s does not exist" % file2) + assert(master.getconfig({"hostname" => "blah"}) != config, "Did not use reloaded config") end # Make sure we're correctly doing clientname manipulations. diff --git a/test/network/handler/runner.rb b/test/network/handler/runner.rb index 402b27ffc..171458ffa 100755 --- a/test/network/handler/runner.rb +++ b/test/network/handler/runner.rb @@ -29,7 +29,8 @@ class TestHandlerRunner < Test::Unit::TestCase client end - def test_runner + def setup + super FileUtils.mkdir_p(Puppet[:statedir]) Puppet[:ignoreschedules] = false # Okay, make our manifest @@ -37,7 +38,7 @@ class TestHandlerRunner < Test::Unit::TestCase created = tempfile() # We specify the schedule here, because I was having problems with # using default schedules. - code = %{ + @code = %{ class yayness { schedule { "yayness": period => weekly } file { "#{created}": ensure => file, schedule => yayness } @@ -46,59 +47,24 @@ class TestHandlerRunner < Test::Unit::TestCase include yayness } - client = mkclient(code) + @client = mkclient(@code) - runner = nil - assert_nothing_raised { - runner = Puppet::Network::Handler.runner.new - } - # First: tags - # Second: ignore schedules true/false - # Third: background true/false - # Fourth: whether file should exist true/false - [ - ["with no backgrounding", - nil, true, true, true], - ["in the background", - nil, true, false, true], - ["with a bad tag", - ["coolness"], true, false, false], - ["with another bad tag", - "coolness", true, false, false], - ["with a good tag", - ["coolness", "yayness"], true, false, true], - ["with another good tag", - ["yayness"], true, false, true], - ["with a third good tag", - "yayness", true, false, true], - ["with no tags", - "", true, false, true], - ["not ignoring schedules", - nil, false, false, false], - ["ignoring schedules", - nil, true, false, true], - ].each do |msg, tags, ignore, fg, shouldexist| - if FileTest.exists?(created) - File.unlink(created) - end - assert_nothing_raised { - # Try it without backgrounding - runner.run(tags, ignore, fg) - } + @runner = Puppet::Network::Handler.runner.new + end + + def test_runner_when_in_foreground + @client.expects(:run).with(:tags => "mytags", :ignoreschedules => true) + + Process.expects(:newthread).never - unless fg - Puppet.join - end - - if shouldexist - assert(FileTest.exists?(created), "File did not get created " + - msg) - else - assert(!FileTest.exists?(created), "File got created incorrectly " + - msg) - end - end + @runner.run("mytags", true, true) end -end + def test_runner_when_in_background + @client.expects(:run).with(:tags => "mytags", :ignoreschedules => true) + + Puppet.expects(:newthread).yields + @runner.run("mytags", true, false) + end +end diff --git a/test/other/relationships.rb b/test/other/relationships.rb index bfe7e88d7..0f2a103fe 100755 --- a/test/other/relationships.rb +++ b/test/other/relationships.rb @@ -111,7 +111,7 @@ class TestRelationships < Test::Unit::TestCase end end - def test_store_relationship + def test_munge_relationship file = Puppet::Type.newfile :path => tempfile(), :mode => 0755 execs = [] 3.times do |i| @@ -122,7 +122,7 @@ class TestRelationships < Test::Unit::TestCase result = nil [execs[0], [:exec, "yay0"], ["exec", "yay0"]].each do |target| assert_nothing_raised do - result = file.send(:store_relationship, :require, target) + result = file.send(:munge_relationship, :require, target) end assert_equal([[:exec, "yay0"]], result) @@ -133,7 +133,7 @@ class TestRelationships < Test::Unit::TestCase strings = execs.collect { |e| [e.class.name.to_s, e.title] } [execs, symbols, strings].each do |target| assert_nothing_raised do - result = file.send(:store_relationship, :require, target) + result = file.send(:munge_relationship, :require, target) end assert_equal(symbols, result) @@ -141,7 +141,7 @@ class TestRelationships < Test::Unit::TestCase # Make sure we can mix it up, even though this shouldn't happen assert_nothing_raised do - result = file.send(:store_relationship, :require, [execs[0], [execs[1].class.name, execs[1].title]]) + result = file.send(:munge_relationship, :require, [execs[0], [execs[1].class.name, execs[1].title]]) end assert_equal([[:exec, "yay0"], [:exec, "yay1"]], result) @@ -151,7 +151,7 @@ class TestRelationships < Test::Unit::TestCase file[:require] = execs[0] assert_nothing_raised do - result = file.send(:store_relationship, :require, [execs[1], execs[2]]) + result = file.send(:munge_relationship, :require, [execs[1], execs[2]]) end assert_equal(symbols, result) diff --git a/test/ral/manager/instances.rb b/test/ral/manager/instances.rb index 6ac4322f5..88f766038 100755 --- a/test/ral/manager/instances.rb +++ b/test/ral/manager/instances.rb @@ -93,7 +93,8 @@ class TestTypeInstances < Test::Unit::TestCase # Make sure resources are entirely deleted. def test_delete aliases = %w{one} - obj = @type.create(:name => "testing", :alias => "two") + config = mk_configuration + obj = @type.create(:name => "testing", :alias => "two", :configuration => config) aliases << "two" @type.alias("two", obj) diff --git a/test/ral/manager/type.rb b/test/ral/manager/type.rb index 57248159b..350d3dd15 100755 --- a/test/ral/manager/type.rb +++ b/test/ral/manager/type.rb @@ -131,27 +131,60 @@ class TestType < Test::Unit::TestCase } end - # Verify that aliasing works - def test_aliasing - file = tempfile() + def test_aliases_to_self_are_not_failures + resource = Puppet.type(:file).create( + :name => "/path/to/some/missing/file", + :ensure => "file" + ) + resource.stubs(:path).returns("") - baseobj = nil - assert_nothing_raised { - baseobj = Puppet.type(:file).create( - :name => file, - :ensure => "file", - :alias => ["funtest"] - ) - } + configuration = stub 'configuration' + configuration.expects(:resource).with(:file, "/path/to/some/missing/file").returns(resource) + resource.configuration = configuration # Verify our adding ourselves as an alias isn't an error. - assert_nothing_raised { - baseobj[:alias] = file + assert_nothing_raised("Could not add alias") { + resource[:alias] = "/path/to/some/missing/file" + } + + assert_equal(resource.object_id, Puppet.type(:file)["/path/to/some/missing/file"].object_id, "Could not retrieve alias to self") + end + + def test_aliases_are_added_to_class_and_configuration + resource = Puppet.type(:file).create( + :name => "/path/to/some/missing/file", + :ensure => "file" + ) + resource.stubs(:path).returns("") + + configuration = stub 'configuration' + configuration.stubs(:resource).returns(nil) + configuration.expects(:alias).with(resource, "funtest") + resource.configuration = configuration + + assert_nothing_raised("Could not add alias") { + resource[:alias] = "funtest" } - assert_instance_of(Puppet.type(:file), Puppet.type(:file)["funtest"], - "Could not retrieve alias") + assert_equal(resource.object_id, Puppet.type(:file)["funtest"].object_id, "Could not retrieve alias") + end + + def test_aliasing_fails_without_a_configuration + resource = Puppet.type(:file).create( + :name => "/no/such/file", + :ensure => "file" + ) + + assert_raise(Puppet::Error, "Did not fail to alias when no configuration was available") { + resource[:alias] = "funtest" + } + end + def test_configurations_are_set_during_initialization_if_present_on_the_transobject + trans = Puppet::TransObject.new("/path/to/some/file", :file) + trans.configuration = :my_config + resource = trans.to_type + assert_equal(resource.configuration, trans.configuration, "Did not set configuration on initialization") end # Verify that requirements don't depend on file order diff --git a/test/ral/types/file.rb b/test/ral/types/file.rb index aca9d3b9c..73095a783 100755 --- a/test/ral/types/file.rb +++ b/test/ral/types/file.rb @@ -104,6 +104,12 @@ class TestFile < Test::Unit::TestCase } end + def test_groups_fails_when_invalid + assert_raise(Puppet::Error, "did not fail when the group was empty") do + Puppet::Type.type(:file).create :path => "/some/file", :group => "" + end + end + if Puppet::Util::SUIDManager.uid == 0 def test_createasuser dir = tmpdir() diff --git a/test/ral/types/host.rb b/test/ral/types/host.rb index 69e37da0f..1013651c5 100755 --- a/test/ral/types/host.rb +++ b/test/ral/types/host.rb @@ -38,12 +38,16 @@ class TestHost < Test::Unit::TestCase else @hcount = 1 end + + @configuration ||= mk_configuration + host = nil assert_nothing_raised { host = Puppet.type(:host).create( :name => "fakehost%s" % @hcount, :ip => "192.168.27.%s" % @hcount, - :alias => "alias%s" % @hcount + :alias => "alias%s" % @hcount, + :configuration => @configuration ) } diff --git a/test/ral/types/parameter.rb b/test/ral/types/parameter.rb index 89c8b944d..1d402cb85 100755 --- a/test/ral/types/parameter.rb +++ b/test/ral/types/parameter.rb @@ -113,6 +113,9 @@ class TestParameter < Test::Unit::TestCase inst = type.create(:name => "test") + config = mk_configuration + inst.configuration = config + assert_nothing_raised("Could not create shadowed param") { inst[:alias] = "foo" } @@ -133,7 +136,7 @@ class TestParameter < Test::Unit::TestCase # Now try it during initialization other = nil assert_nothing_raised("Could not create instance with shadow") do - other = type.create(:name => "rah", :alias => "one") + other = type.create(:name => "rah", :alias => "one", :configuration => config) end params = other.instance_variable_get("@parameters") obj = params[:alias] diff --git a/test/ral/types/sshkey.rb b/test/ral/types/sshkey.rb index c610eb9e9..c99f7562a 100755 --- a/test/ral/types/sshkey.rb +++ b/test/ral/types/sshkey.rb @@ -47,12 +47,15 @@ class TestSSHKey < Test::Unit::TestCase @kcount = 1 end + @config ||= mk_configuration + assert_nothing_raised { key = @sshkeytype.create( :name => "host%s.madstop.com" % @kcount, :key => "%sAAAAB3NzaC1kc3MAAACBAMnhSiku76y3EGkNCDsUlvpO8tRgS9wL4Eh54WZfQ2lkxqfd2uT/RTT9igJYDtm/+UHuBRdNGpJYW1Nw2i2JUQgQEEuitx4QKALJrBotejGOAWxxVk6xsh9xA0OW8Q3ZfuX2DDitfeC8ZTCl4xodUMD8feLtP+zEf8hxaNamLlt/AAAAFQDYJyf3vMCWRLjTWnlxLtOyj/bFpwAAAIEAmRxxXb4jjbbui9GYlZAHK00689DZuX0EabHNTl2yGO5KKxGC6Esm7AtjBd+onfu4Rduxut3jdI8GyQCIW8WypwpJofCIyDbTUY4ql0AQUr3JpyVytpnMijlEyr41FfIb4tnDqnRWEsh2H7N7peW+8DWZHDFnYopYZJ9Yu4/jHRYAAACAERG50e6aRRb43biDr7Ab9NUCgM9bC0SQscI/xdlFjac0B/kSWJYTGVARWBDWug705hTnlitY9cLC5Ey/t/OYOjylTavTEfd/bh/8FkAYO+pWdW3hx6p97TBffK0b6nrc6OORT2uKySbbKOn0681nNQh4a6ueR3JRppNkRPnTk5c=" % @kcount, :type => "ssh-dss", - :alias => ["192.168.0.%s" % @kcount] + :alias => ["192.168.0.%s" % @kcount], + :configuration => @config ) } |