diff --git a/lib/puppet/daemon.rb b/lib/puppet/daemon.rb index b97f2c361..fd6269eb7 100755 --- a/lib/puppet/daemon.rb +++ b/lib/puppet/daemon.rb @@ -1,195 +1,213 @@ require 'puppet' require 'puppet/util/pidlock' require 'puppet/application' # A module that handles operations common to all daemons. This is included # into the Server and Client base classes. class Puppet::Daemon attr_accessor :agent, :server, :argv def daemonname Puppet[:name] end # Put the daemon into the background. def daemonize if pid = fork Process.detach(pid) exit(0) end create_pidfile # Get rid of console logging Puppet::Util::Log.close(:console) Process.setsid Dir.chdir("/") + end + + # Close stdin/stdout/stderr so that we can finish our transition into 'daemon' mode. + # @return nil + def self.close_streams() + Puppet.debug("Closing streams for daemon mode") begin $stdin.reopen "/dev/null" $stdout.reopen "/dev/null", "a" $stderr.reopen $stdout Puppet::Util::Log.reopen + Puppet.debug("Finished closing streams for daemon mode") rescue => detail Puppet.err "Could not start #{Puppet[:name]}: #{detail}" Puppet::Util::replace_file("/tmp/daemonout", 0644) do |f| f.puts "Could not start #{Puppet[:name]}: #{detail}" end exit(12) end end + # Convenience signature for calling Puppet::Daemon.close_streams + def close_streams() + Puppet::Daemon.close_streams + end + # Create a pidfile for our daemon, so we can be stopped and others # don't try to start. def create_pidfile Puppet::Util.synchronize_on(Puppet[:name],Sync::EX) do raise "Could not create PID file: #{pidfile}" unless Puppet::Util::Pidlock.new(pidfile).lock end end # Provide the path to our pidfile. def pidfile Puppet[:pidfile] end def reexec raise Puppet::DevError, "Cannot reexec unless ARGV arguments are set" unless argv command = $0 + " " + argv.join(" ") Puppet.notice "Restarting with '#{command}'" stop(:exit => false) exec(command) end def reload return unless agent if agent.running? Puppet.notice "Not triggering already-running agent" return end agent.run end # Remove the pid file for our daemon. def remove_pidfile Puppet::Util.synchronize_on(Puppet[:name],Sync::EX) do Puppet::Util::Pidlock.new(pidfile).unlock end end def restart Puppet::Application.restart! reexec unless agent and agent.running? end def reopen_logs Puppet::Util::Log.reopen end # Trap a couple of the main signals. This should probably be handled # in a way that anyone else can register callbacks for traps, but, eh. def set_signal_traps signals = {:INT => :stop, :TERM => :stop } # extended signals not supported under windows signals.update({:HUP => :restart, :USR1 => :reload, :USR2 => :reopen_logs }) unless Puppet.features.microsoft_windows? signals.each do |signal, method| Signal.trap(signal) do Puppet.notice "Caught #{signal}; calling #{method}" send(method) end end end # Stop everything def stop(args = {:exit => true}) Puppet::Application.stop! server.stop if server remove_pidfile Puppet::Util::Log.close_all exit if args[:exit] end def start set_signal_traps create_pidfile raise Puppet::DevError, "Daemons must have an agent, server, or both" unless agent or server # Start the listening server, if required. server.start if server + # now that the server has started, we've waited just about as long as possible to close + # our streams and become a "real" daemon process. This is in hopes of allowing + # errors to have the console available as a fallback for logging for as long as + # possible. + close_streams + # Finally, loop forever running events - or, at least, until we exit. run_event_loop end def run_event_loop # Now, we loop waiting for either the configuration file to change, or the # next agent run to be due. Fun times. # # We want to trigger the reparse if 15 seconds passed since the previous # wakeup, and the agent run if Puppet[:runinterval] seconds have passed # since the previous wakeup. # # We always want to run the agent on startup, so it was always before now. # Because 0 means "continuously run", `to_i` does the right thing when the # input is strange or badly formed by returning 0. Integer will raise, # which we don't want, and we want to protect against -1 or below. next_agent_run = 0 agent_run_interval = [Puppet[:runinterval].to_i, 0].max # We may not want to reparse; that can be disable. Fun times. next_reparse = 0 reparse_interval = Puppet[:filetimeout].to_i loop do now = Time.now.to_i # Handle reparsing of configuration files, if desired and required. # `reparse` will just check if the action is required, and would be # better named `reparse_if_changed` instead. if reparse_interval > 0 and now >= next_reparse Puppet.settings.reparse # The time to the next reparse might have changed, so recalculate # now. That way we react dynamically to reconfiguration. reparse_interval = Puppet[:filetimeout].to_i next_reparse = now + reparse_interval # We should also recalculate the agent run interval, and adjust the # next time it is scheduled to run, just in case. In the event that # we made no change the result will be a zero second adjustment. new_run_interval = [Puppet[:runinterval].to_i, 0].max next_agent_run += agent_run_interval - new_run_interval agent_run_interval = new_run_interval end # Handle triggering another agent run. This will block the next check # for configuration reparsing, which is a desired and deliberate # behaviour. You should not change that. --daniel 2012-02-21 if agent and now >= next_agent_run agent.run next_agent_run = now + agent_run_interval end # Finally, an interruptable able sleep until the next scheduled event. # We also set a default wakeup of "one hour from now", which will # recheck everything at a minimum every hour. Just in case something in # the math messes up or something; it should be inexpensive enough to # wake once an hour, then go back to sleep after doing nothing, if # someone only wants listen mode. next_event = now + 60 * 60 next_event > next_reparse and next_event = next_reparse next_event > next_agent_run and next_event = next_agent_run how_long = next_event - now how_long > 0 and select([], [], [], how_long) end end end diff --git a/lib/puppet/network/server.rb b/lib/puppet/network/server.rb index f92d600a5..a328a1ba8 100644 --- a/lib/puppet/network/server.rb +++ b/lib/puppet/network/server.rb @@ -1,139 +1,137 @@ require 'puppet/network/http' require 'puppet/util/pidlock' class Puppet::Network::Server attr_reader :server_type, :address, :port + + # TODO: does anything actually call this? It seems like it's a duplicate of the code in Puppet::Daemon, but that + # it's not actually called anywhere. + # Put the daemon into the background. def daemonize if pid = fork Process.detach(pid) exit(0) end # Get rid of console logging Puppet::Util::Log.close(:console) Process.setsid Dir.chdir("/") - begin - $stdin.reopen "/dev/null" - $stdout.reopen "/dev/null", "a" - $stderr.reopen $stdout - Puppet::Util::Log.reopen - rescue => detail - Puppet::Util.replace_file("/tmp/daemonout", 0644) { |f| - f.puts "Could not start #{Puppet[:name]}: #{detail}" - } - raise "Could not start #{Puppet[:name]}: #{detail}" - end + end + + def close_streams() + Puppet::Daemon.close_streams() end # Create a pidfile for our daemon, so we can be stopped and others # don't try to start. def create_pidfile Puppet::Util.synchronize_on(Puppet[:name],Sync::EX) do raise "Could not create PID file: #{pidfile}" unless Puppet::Util::Pidlock.new(pidfile).lock end end # Remove the pid file for our daemon. def remove_pidfile Puppet::Util.synchronize_on(Puppet[:name],Sync::EX) do Puppet::Util::Pidlock.new(pidfile).unlock end end # Provide the path to our pidfile. def pidfile Puppet[:pidfile] end def initialize(args = {}) valid_args = [:handlers, :port] bad_args = args.keys.find_all { |p| ! valid_args.include?(p) }.collect { |p| p.to_s }.join(",") raise ArgumentError, "Invalid argument(s) #{bad_args}" unless bad_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}]") @port = args[:port] || Puppet[:masterport] || raise(ArgumentError, "Must specify :port or configure Puppet :masterport") @address = determine_bind_address @listening = false @routes = {} self.register(args[:handlers]) if args[:handlers] # Make sure we have all of the directories we need to function. #Puppet.settings.use(:main, :ssl, Puppet[:name]) Puppet.settings.use(:main, :ssl, :application) end # Register handlers for REST networking, based on the Indirector. def register(*indirections) raise ArgumentError, "Indirection names are required." if indirections.empty? indirections.flatten.each do |name| Puppet::Indirector::Indirection.model(name) || raise(ArgumentError, "Cannot locate indirection '#{name}'.") @routes[name.to_sym] = true end end # Unregister Indirector handlers. 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 [#{i}] is unknown.") unless @routes[i.to_sym] end indirections.flatten.each do |i| @routes.delete(i.to_sym) end end def listening? @listening end def listen raise "Cannot listen -- already listening." if listening? @listening = true http_server.listen(:address => address, :port => port, :handlers => @routes.keys) 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 def start create_pidfile + close_streams listen end def stop unlisten remove_pidfile end private 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 def determine_bind_address tmp = Puppet[:bindaddress] return tmp if tmp != "" server_type.to_s == "webrick" ? "0.0.0.0" : "127.0.0.1" end end diff --git a/lib/puppet/util.rb b/lib/puppet/util.rb index 9d1e56367..76bfbe6fd 100644 --- a/lib/puppet/util.rb +++ b/lib/puppet/util.rb @@ -1,520 +1,529 @@ # A module to collect utility functions. require 'English' require 'puppet/external/lock' require 'puppet/error' require 'puppet/util/execution_stub' require 'uri' require 'sync' require 'monitor' require 'tempfile' require 'pathname' module Puppet module Util require 'puppet/util/monkey_patches' require 'benchmark' # These are all for backward compatibility -- these are methods that used # to be in Puppet::Util but have been moved into external modules. require 'puppet/util/posix' extend Puppet::Util::POSIX @@sync_objects = {}.extend MonitorMixin def self.activerecord_version if (defined?(::ActiveRecord) and defined?(::ActiveRecord::VERSION) and defined?(::ActiveRecord::VERSION::MAJOR) and defined?(::ActiveRecord::VERSION::MINOR)) ([::ActiveRecord::VERSION::MAJOR, ::ActiveRecord::VERSION::MINOR].join('.').to_f) else 0 end end # Run some code with a specific environment. Resets the environment back to # what it was at the end of the code. def self.withenv(hash) saved = ENV.to_hash hash.each do |name, val| ENV[name.to_s] = val end yield ensure ENV.clear saved.each do |name, val| ENV[name] = val end end # Execute a given chunk of code with a new umask. def self.withumask(mask) cur = File.umask(mask) begin yield ensure File.umask(cur) end end def self.synchronize_on(x,type) sync_object,users = 0,1 begin @@sync_objects.synchronize { (@@sync_objects[x] ||= [Sync.new,0])[users] += 1 } @@sync_objects[x][sync_object].synchronize(type) { yield } ensure @@sync_objects.synchronize { @@sync_objects.delete(x) unless (@@sync_objects[x][users] -= 1) > 0 } end end # Change the process to a different user def self.chuser if group = Puppet[:group] begin Puppet::Util::SUIDManager.change_group(group, true) rescue => detail Puppet.warning "could not change to group #{group.inspect}: #{detail}" $stderr.puts "could not change to group #{group.inspect}" # Don't exit on failed group changes, since it's # not fatal #exit(74) end end if user = Puppet[:user] begin Puppet::Util::SUIDManager.change_user(user, true) rescue => detail $stderr.puts "Could not change to user #{user}: #{detail}" exit(74) end end end # Create instance methods for each of the log levels. This allows # the messages to be a little richer. Most classes will be calling this # method. def self.logmethods(klass, useself = true) Puppet::Util::Log.eachlevel { |level| klass.send(:define_method, level, proc { |args| args = args.join(" ") if args.is_a?(Array) if useself Puppet::Util::Log.create( :level => level, :source => self, :message => args ) else Puppet::Util::Log.create( :level => level, :message => args ) end }) } end # Proxy a bunch of methods to another object. def self.classproxy(klass, objmethod, *methods) classobj = class << klass; self; end methods.each do |method| classobj.send(:define_method, method) do |*args| obj = self.send(objmethod) obj.send(method, *args) end end end # Proxy a bunch of methods to another object. def self.proxy(klass, objmethod, *methods) methods.each do |method| klass.send(:define_method, method) do |*args| obj = self.send(objmethod) obj.send(method, *args) end end end def benchmark(*args) msg = args.pop level = args.pop object = nil if args.empty? if respond_to?(level) object = self else object = Puppet end else object = args.pop end raise Puppet::DevError, "Failed to provide level to :benchmark" unless level unless level == :none or object.respond_to? level raise Puppet::DevError, "Benchmarked object does not respond to #{level}" end # Only benchmark if our log level is high enough if level != :none and Puppet::Util::Log.sendlevel?(level) result = nil seconds = Benchmark.realtime { yield } object.send(level, msg + (" in %0.2f seconds" % seconds)) return seconds else yield end end def which(bin) if absolute_path?(bin) return bin if FileTest.file? bin and FileTest.executable? bin else ENV['PATH'].split(File::PATH_SEPARATOR).each do |dir| begin dest = File.expand_path(File.join(dir, bin)) rescue ArgumentError => e # if the user's PATH contains a literal tilde (~) character and HOME is not set, we may get # an ArgumentError here. Let's check to see if that is the case; if not, re-raise whatever error # was thrown. raise e unless ((dir =~ /~/) && ((ENV['HOME'].nil? || ENV['HOME'] == ""))) # if we get here they have a tilde in their PATH. We'll issue a single warning about this and then # ignore this path element and carry on with our lives. Puppet::Util::Warnings.warnonce("PATH contains a ~ character, and HOME is not set; ignoring PATH element '#{dir}'.") next end if Puppet.features.microsoft_windows? && File.extname(dest).empty? exts = ENV['PATHEXT'] exts = exts ? exts.split(File::PATH_SEPARATOR) : %w[.COM .EXE .BAT .CMD] exts.each do |ext| destext = File.expand_path(dest + ext) return destext if FileTest.file? destext and FileTest.executable? destext end end return dest if FileTest.file? dest and FileTest.executable? dest end end nil end module_function :which # Determine in a platform-specific way whether a path is absolute. This # defaults to the local platform if none is specified. def absolute_path?(path, platform=nil) # Escape once for the string literal, and once for the regex. slash = '[\\\\/]' name = '[^\\\\/]+' regexes = { :windows => %r!^(([A-Z]:#{slash})|(#{slash}#{slash}#{name}#{slash}#{name})|(#{slash}#{slash}\?#{slash}#{name}))!i, :posix => %r!^/!, } require 'puppet' platform ||= Puppet.features.microsoft_windows? ? :windows : :posix !! (path =~ regexes[platform]) end module_function :absolute_path? # Convert a path to a file URI def path_to_uri(path) return unless path params = { :scheme => 'file' } if Puppet.features.microsoft_windows? path = path.gsub(/\\/, '/') if unc = /^\/\/([^\/]+)(\/[^\/]+)/.match(path) params[:host] = unc[1] path = unc[2] elsif path =~ /^[a-z]:\//i path = '/' + path end end params[:path] = URI.escape(path) begin URI::Generic.build(params) rescue => detail raise Puppet::Error, "Failed to convert '#{path}' to URI: #{detail}" end end module_function :path_to_uri # Get the path component of a URI def uri_to_path(uri) return unless uri.is_a?(URI) path = URI.unescape(uri.path) if Puppet.features.microsoft_windows? and uri.scheme == 'file' if uri.host path = "//#{uri.host}" + path # UNC else path.sub!(/^\//, '') end end path end module_function :uri_to_path # Create an exclusive lock. def threadlock(resource, type = Sync::EX) Puppet::Util.synchronize_on(resource,type) { yield } end module_function :benchmark def memory unless defined?(@pmap) @pmap = which('pmap') end if @pmap %x{#{@pmap} #{Process.pid}| grep total}.chomp.sub(/^\s*total\s+/, '').sub(/K$/, '').to_i else 0 end end def symbolize(value) if value.respond_to? :intern value.intern else value end end def symbolizehash(hash) newhash = {} hash.each do |name, val| if name.is_a? String newhash[name.intern] = val else newhash[name] = val end end end def symbolizehash!(hash) hash.each do |name, val| if name.is_a? String hash[name.intern] = val hash.delete(name) end end hash end module_function :symbolize, :symbolizehash, :symbolizehash! # Just benchmark, with no logging. def thinmark seconds = Benchmark.realtime { yield } seconds end module_function :memory, :thinmark # Because IO#binread is only available in 1.9 def binread(file) File.open(file, 'rb') { |f| f.read } end module_function :binread # utility method to get the current call stack and format it to a human-readable string (which some IDEs/editors # will recognize as links to the line numbers in the trace) def self.pretty_backtrace(backtrace = caller(1)) backtrace.collect do |line| file_path, line_num = line.split(":") file_path = expand_symlinks(File.expand_path(file_path)) file_path + ":" + line_num end .join("\n") end # utility method that takes a path as input, checks each component of the path to see if it is a symlink, and expands # it if it is. returns the expanded path. def self.expand_symlinks(file_path) file_path.split("/").inject do |full_path, next_dir| next_path = full_path + "/" + next_dir if File.symlink?(next_path) then link = File.readlink(next_path) next_path = case link when /^\// then link else File.expand_path(full_path + "/" + link) end end next_path end end # Replace a file, securely. This takes a block, and passes it the file # handle of a file open for writing. Write the replacement content inside # the block and it will safely replace the target file. # # This method will make no changes to the target file until the content is # successfully written and the block returns without raising an error. # # As far as possible the state of the existing file, such as mode, is # preserved. This works hard to avoid loss of any metadata, but will result # in an inode change for the file. # # Arguments: `filename`, `default_mode` # # The filename is the file we are going to replace. # # The default_mode is the mode to use when the target file doesn't already # exist; if the file is present we copy the existing mode/owner/group values # across. def replace_file(file, default_mode, &block) raise Puppet::DevError, "replace_file requires a block" unless block_given? file = Pathname(file) tempfile = Tempfile.new(file.basename.to_s, file.dirname.to_s) file_exists = file.exist? # If the file exists, use its current mode/owner/group. If it doesn't, use # the supplied mode, and default to current user/group. if file_exists if Puppet.features.microsoft_windows? mode = Puppet::Util::Windows::Security.get_mode(file.to_s) uid = Puppet::Util::Windows::Security.get_owner(file.to_s) gid = Puppet::Util::Windows::Security.get_owner(file.to_s) else stat = file.lstat mode = stat.mode uid = stat.uid gid = stat.gid end # We only care about the four lowest-order octets. Higher octets are # filesystem-specific. mode &= 07777 else mode = default_mode uid = Process.euid gid = Process.egid end # Set properties of the temporary file before we write the content, because # Tempfile doesn't promise to be safe from reading by other people, just # that it avoids races around creating the file. if Puppet.features.microsoft_windows? Puppet::Util::Windows::Security.set_mode(mode, tempfile.path) Puppet::Util::Windows::Security.set_owner(uid, tempfile.path) Puppet::Util::Windows::Security.set_group(gid, tempfile.path) else tempfile.chmod(mode) tempfile.chown(uid, gid) end # OK, now allow the caller to write the content of the file. yield tempfile # Now, make sure the data (which includes the mode) is safe on disk. tempfile.flush begin tempfile.fsync rescue NotImplementedError # fsync may not be implemented by Ruby on all platforms, but # there is absolutely no recovery path if we detect that. So, we just # ignore the return code. # # However, don't be fooled: that is accepting that we are running in # an unsafe fashion. If you are porting to a new platform don't stub # that out. end tempfile.close File.rename(tempfile.path, file) # Ideally, we would now fsync the directory as well, but Ruby doesn't # have support for that, and it doesn't matter /that/ much... # Return something true, and possibly useful. file end module_function :replace_file # Executes a block of code, wrapped with some special exception handling. Causes the ruby interpreter to # exit if the block throws an exception. # # @param [String] message a message to log if the block fails # @param [Integer] code the exit code that the ruby interpreter should return if the block fails # @yield def exit_on_fail(message, code = 1) yield - rescue ArgumentError, RuntimeError, NotImplementedError => detail - Puppet.log_exception(detail, "Could not #{message}: #{detail}") + # First, we need to check and see if we are catching a SystemExit error. These will be raised + # when we daemonize/fork, and they do not necessarily indicate a failure case. + rescue SystemExit => err + raise err + + # Now we need to catch *any* other kind of exception, because we may be calling third-party + # code (e.g. webrick), and we have no idea what they might throw. + rescue Exception => err + Puppet.log_exception(err, "Could not #{message}: #{err}") exit(code) end module_function :exit_on_fail + + ####################################################################################################### # Deprecated methods relating to process execution; these have been moved to Puppet::Util::Execution ####################################################################################################### def execpipe(command, failonfail = true, &block) Puppet.deprecation_warning("Puppet::Util.execpipe is deprecated; please use Puppet::Util::Execution.execpipe") Puppet::Util::Execution.execpipe(command, failonfail, &block) end module_function :execpipe def execfail(command, exception) Puppet.deprecation_warning("Puppet::Util.execfail is deprecated; please use Puppet::Util::Execution.execfail") Puppet::Util::Execution.execfail(command, exception) end module_function :execfail def execute(command, arguments = {}) Puppet.deprecation_warning("Puppet::Util.execute is deprecated; please use Puppet::Util::Execution.execute") Puppet::Util::Execution.execute(command, arguments) end module_function :execute end end require 'puppet/util/errors' require 'puppet/util/methodhelper' require 'puppet/util/metaid' require 'puppet/util/classgen' require 'puppet/util/docs' require 'puppet/util/execution' require 'puppet/util/logging' require 'puppet/util/package' require 'puppet/util/warnings' diff --git a/spec/unit/daemon_spec.rb b/spec/unit/daemon_spec.rb index 03824bf60..e6b700fb5 100755 --- a/spec/unit/daemon_spec.rb +++ b/spec/unit/daemon_spec.rb @@ -1,267 +1,268 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/daemon' require 'puppet/agent' def without_warnings flag = $VERBOSE $VERBOSE = nil yield $VERBOSE = flag end class TestClient def lockfile_path "/dev/null" end end describe Puppet::Daemon, :unless => Puppet.features.microsoft_windows? do include PuppetSpec::Files before do @agent = Puppet::Agent.new(TestClient.new) @daemon = Puppet::Daemon.new + @daemon.stubs(:close_streams).returns nil end it "should be able to manage an agent" do @daemon.should respond_to(:agent) end it "should be able to manage a network server" do @daemon.should respond_to(:server) end it "should reopen the Log logs when told to reopen logs" do Puppet::Util::Log.expects(:reopen) @daemon.reopen_logs end describe "when setting signal traps" do signals = {:INT => :stop, :TERM => :stop } signals.update({:HUP => :restart, :USR1 => :reload, :USR2 => :reopen_logs}) unless Puppet.features.microsoft_windows? signals.each do |signal, method| it "should log and call #{method} when it receives #{signal}" do Signal.expects(:trap).with(signal).yields Puppet.expects(:notice) @daemon.expects(method) @daemon.set_signal_traps end end end describe "when starting" do before do @daemon.stubs(:create_pidfile) @daemon.stubs(:set_signal_traps) @daemon.stubs(:run_event_loop) end it "should fail if it has neither agent nor server" do lambda { @daemon.start }.should raise_error(Puppet::DevError) end it "should create its pidfile" do @daemon.agent = @agent @daemon.expects(:create_pidfile) @daemon.start end it "should start its server if one is configured" do server = mock 'server' server.expects(:start) @daemon.stubs(:server).returns server @daemon.start end end describe "when stopping" do before do @daemon.stubs(:remove_pidfile) Puppet::Util::Log.stubs(:close_all) # to make the global safe to mock, set it to a subclass of itself, # then restore it in an after pass without_warnings { Puppet::Application = Class.new(Puppet::Application) } end after do # restore from the superclass so we lose the stub garbage without_warnings { Puppet::Application = Puppet::Application.superclass } end it "should stop its server if one is configured" do server = mock 'server' server.expects(:stop) @daemon.stubs(:server).returns server expect { @daemon.stop }.to exit_with 0 end it 'should request a stop from Puppet::Application' do Puppet::Application.expects(:stop!) expect { @daemon.stop }.to exit_with 0 end it "should remove its pidfile" do @daemon.expects(:remove_pidfile) expect { @daemon.stop }.to exit_with 0 end it "should close all logs" do Puppet::Util::Log.expects(:close_all) expect { @daemon.stop }.to exit_with 0 end it "should exit unless called with ':exit => false'" do expect { @daemon.stop }.to exit_with 0 end it "should not exit if called with ':exit => false'" do @daemon.stop :exit => false end end describe "when creating its pidfile" do it "should use an exclusive mutex" do Puppet.settings.expects(:value).with(:name).returns "me" Puppet::Util.expects(:synchronize_on).with("me",Sync::EX) @daemon.create_pidfile end it "should lock the pidfile using the Pidlock class" do pidfile = mock 'pidfile' Puppet.settings.stubs(:value).with(:name).returns "eh" Puppet.settings.expects(:value).with(:pidfile).returns make_absolute("/my/file") Puppet::Util::Pidlock.expects(:new).with(make_absolute("/my/file")).returns pidfile pidfile.expects(:lock).returns true @daemon.create_pidfile end it "should fail if it cannot lock" do pidfile = mock 'pidfile' Puppet.settings.stubs(:value).with(:name).returns "eh" Puppet.settings.stubs(:value).with(:pidfile).returns make_absolute("/my/file") Puppet::Util::Pidlock.expects(:new).with(make_absolute("/my/file")).returns pidfile pidfile.expects(:lock).returns false lambda { @daemon.create_pidfile }.should raise_error end end describe "when removing its pidfile" do it "should use an exclusive mutex" do Puppet.settings.expects(:value).with(:name).returns "me" Puppet::Util.expects(:synchronize_on).with("me",Sync::EX) @daemon.remove_pidfile end it "should do nothing if the pidfile is not present" do pidfile = mock 'pidfile', :unlock => false Puppet[:pidfile] = make_absolute("/my/file") Puppet::Util::Pidlock.expects(:new).with(make_absolute("/my/file")).returns pidfile @daemon.remove_pidfile end it "should unlock the pidfile using the Pidlock class" do pidfile = mock 'pidfile', :unlock => true Puppet[:pidfile] = make_absolute("/my/file") Puppet::Util::Pidlock.expects(:new).with(make_absolute("/my/file")).returns pidfile @daemon.remove_pidfile end end describe "when reloading" do it "should do nothing if no agent is configured" do @daemon.reload end it "should do nothing if the agent is running" do @agent.expects(:running?).returns true @daemon.agent = @agent @daemon.reload end it "should run the agent if one is available and it is not running" do @agent.expects(:running?).returns false @agent.expects :run @daemon.agent = @agent @daemon.reload end end describe "when restarting" do before do without_warnings { Puppet::Application = Class.new(Puppet::Application) } end after do without_warnings { Puppet::Application = Puppet::Application.superclass } end it 'should set Puppet::Application.restart!' do Puppet::Application.expects(:restart!) @daemon.stubs(:reexec) @daemon.restart end it "should reexec itself if no agent is available" do @daemon.expects(:reexec) @daemon.restart end it "should reexec itself if the agent is not running" do @agent.expects(:running?).returns false @daemon.agent = @agent @daemon.expects(:reexec) @daemon.restart end end describe "when reexecing it self" do before do @daemon.stubs(:exec) @daemon.stubs(:stop) end it "should fail if no argv values are available" do @daemon.expects(:argv).returns nil lambda { @daemon.reexec }.should raise_error(Puppet::DevError) end it "should shut down without exiting" do @daemon.argv = %w{foo} @daemon.expects(:stop).with(:exit => false) @daemon.reexec end it "should call 'exec' with the original executable and arguments" do @daemon.argv = %w{foo} @daemon.expects(:exec).with($0 + " foo") @daemon.reexec end end end diff --git a/spec/unit/network/server_spec.rb b/spec/unit/network/server_spec.rb index 8da5cff60..0b9e73a56 100755 --- a/spec/unit/network/server_spec.rb +++ b/spec/unit/network/server_spec.rb @@ -1,409 +1,410 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/network/server' describe Puppet::Network::Server do before do @mock_http_server_class = mock('http server class') Puppet.settings.stubs(:use) Puppet.settings.stubs(:value).with(:name).returns("me") Puppet.settings.stubs(:value).with(:servertype).returns(:suparserver) Puppet.settings.stubs(:value).with(:bindaddress).returns("") Puppet.settings.stubs(:value).with(:masterport).returns(8140) Puppet::Network::HTTP.stubs(:server_class_by_type).returns(@mock_http_server_class) Puppet.settings.stubs(:value).with(:servertype).returns(:suparserver) @server = Puppet::Network::Server.new(:port => 31337) + @server.stubs(:close_streams).returns(nil) end describe "when initializing" do before do Puppet::Indirector::Indirection.stubs(:model).returns mock('indirection') Puppet.settings.stubs(:value).with(:bindaddress).returns("") Puppet.settings.stubs(:value).with(:masterport).returns('') end it 'should fail if an unknown option is provided' do lambda { Puppet::Network::Server.new(:foo => 31337) }.should raise_error(ArgumentError) end it "should allow specifying a listening port" do Puppet.settings.stubs(:value).with(:bindaddress).returns('') @server = Puppet::Network::Server.new(:port => 31337) @server.port.should == 31337 end it "should use the :bindaddress setting to determine the default listening address" do Puppet.settings.stubs(:value).with(:masterport).returns('') Puppet.settings.expects(:value).with(:bindaddress).returns("10.0.0.1") @server = Puppet::Network::Server.new @server.address.should == "10.0.0.1" end it "should set the bind address to '127.0.0.1' if the default address is an empty string and the server type is mongrel" do Puppet.settings.stubs(:value).with(:servertype).returns("mongrel") Puppet.settings.expects(:value).with(:bindaddress).returns("") @server = Puppet::Network::Server.new @server.address.should == '127.0.0.1' end it "should set the bind address to '0.0.0.0' if the default address is an empty string and the server type is webrick" do Puppet.settings.stubs(:value).with(:servertype).returns("webrick") Puppet.settings.expects(:value).with(:bindaddress).returns("") @server = Puppet::Network::Server.new @server.address.should == '0.0.0.0' end it "should use the Puppet configurator to find a default listening port" do Puppet.settings.stubs(:value).with(:bindaddress).returns('') Puppet.settings.expects(:value).with(:masterport).returns(6667) @server = Puppet::Network::Server.new @server.port.should == 6667 end it "should fail to initialize if no listening port can be found" do Puppet.settings.stubs(:value).with(:bindaddress).returns("127.0.0.1") Puppet.settings.stubs(:value).with(:masterport).returns(nil) lambda { 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.settings.expects(:value).with(:servertype).returns(:suparserver) @server = Puppet::Network::Server.new(: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.settings.expects(:value).with(:servertype).returns(nil) lambda { Puppet::Network::Server.new(: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(:port => 31337) end it "should fail if the HTTP server class is unknown" do Puppet::Network::HTTP.stubs(:server_class_by_type).returns(nil) lambda { Puppet::Network::Server.new(:port => 31337) }.should raise_error(ArgumentError) end it "should allow registering REST handlers" do @server = Puppet::Network::Server.new(:port => 31337, :handlers => [ :foo, :bar, :baz]) lambda { @server.unregister(:foo, :bar, :baz) }.should_not raise_error end it "should not be listening after initialization" do Puppet::Network::Server.new(:port => 31337).should_not be_listening end it "should use the :main setting section" do Puppet.settings.expects(:use).with { |*args| args.include?(:main) } @server = Puppet::Network::Server.new(:port => 31337) end it "should use the :application setting section" do #Puppet.settings.expects(:value).with(:name).returns "me" Puppet.settings.expects(:use).with { |*args| args.include?(:application) } @server = Puppet::Network::Server.new(:port => 31337) end end # We don't test the method, because it's too much of a Unix-y pain. it "should be able to daemonize" do @server.should respond_to(:daemonize) end describe "when being started" do before do @server.stubs(:listen) @server.stubs(:create_pidfile) end it "should listen" do @server.expects(:listen) @server.start end it "should create its PID file" do @server.expects(:create_pidfile) @server.start end end describe "when being stopped" do before do @server.stubs(:unlisten) @server.stubs(:remove_pidfile) end it "should unlisten" do @server.expects(:unlisten) @server.stop end it "should remove its PID file" do @server.expects(:remove_pidfile) @server.stop end end describe "when creating its pidfile" do it "should use an exclusive mutex" do Puppet.settings.expects(:value).with(:name).returns "me" Puppet::Util.expects(:synchronize_on).with("me",Sync::EX) @server.create_pidfile end it "should lock the pidfile using the Pidlock class" do pidfile = mock 'pidfile' Puppet.settings.stubs(:value).with(:name).returns "eh" Puppet.settings.expects(:value).with(:pidfile).returns "/my/file" Puppet::Util::Pidlock.expects(:new).with("/my/file").returns pidfile pidfile.expects(:lock).returns true @server.create_pidfile end it "should fail if it cannot lock" do pidfile = mock 'pidfile' Puppet.settings.stubs(:value).with(:name).returns "eh" Puppet.settings.stubs(:value).with(:pidfile).returns "/my/file" Puppet::Util::Pidlock.expects(:new).with("/my/file").returns pidfile pidfile.expects(:lock).returns false lambda { @server.create_pidfile }.should raise_error end end describe "when removing its pidfile" do it "should use an exclusive mutex" do Puppet.settings.expects(:value).with(:name).returns "me" Puppet::Util.expects(:synchronize_on).with("me",Sync::EX) @server.remove_pidfile end it "should do nothing if the pidfile is not present" do pidfile = mock 'pidfile', :unlock => false Puppet::Util::Pidlock.expects(:new).with("/my/file").returns pidfile Puppet.settings.stubs(:value).with(:pidfile).returns "/my/file" @server.remove_pidfile end it "should unlock the pidfile using the Pidlock class" do pidfile = mock 'pidfile', :unlock => true Puppet::Util::Pidlock.expects(:new).with("/my/file").returns pidfile Puppet.settings.stubs(:value).with(:pidfile).returns "/my/file" @server.remove_pidfile end end describe "when managing indirection registrations" do before do Puppet::Indirector::Indirection.stubs(:model).returns mock('indirection') end it "should allow registering an indirection for client access by specifying its indirection name" do lambda { @server.register(:foo) }.should_not raise_error end it "should require that the indirection be valid" do Puppet::Indirector::Indirection.expects(:model).with(:foo).returns nil lambda { @server.register(:foo) }.should raise_error(ArgumentError) end it "should require at least one indirection name when registering indirections for client access" do lambda { @server.register }.should raise_error(ArgumentError) end it "should allow for numerous indirections to be registered at once for client access" do lambda { @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) lambda { @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) lambda { @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) lambda { @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) lambda { @server.unregister(:foo, :bar, :baz) }.should raise_error(ArgumentError) lambda { @server.unregister(:foo, :bar) }.should_not raise_error end it "should not allow turning off unknown indirection names" do @server.register(:foo, :bar) lambda { @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) lambda { @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| lambda { @server.unregister(indirection) }.should raise_error(ArgumentError) end 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 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 it "should allow for multiple configurations, each handling different indirections" do Puppet::Indirector::Indirection.stubs(:model).returns mock('indirection') @server2 = Puppet::Network::Server.new(:port => 31337) @server.register(:foo, :bar) @server2.register(:foo, :xyzzy) @server.unregister(:foo, :bar) @server2.unregister(:foo, :xyzzy) lambda { @server.unregister(:xyzzy) }.should raise_error(ArgumentError) lambda { @server2.unregister(:bar) }.should raise_error(ArgumentError) end describe "when listening is off" do before do @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 off" do lambda { @server.unlisten }.should raise_error(RuntimeError) end it "should allow listening to be turned on" do lambda { @server.listen }.should_not raise_error end end describe "when listening is on" do before do @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 it is listening" do @server.should be_listening end it "should not allow listening to be turned on" do lambda { @server.listen }.should raise_error(RuntimeError) end it "should allow listening to be turned off" do lambda { @server.unlisten }.should_not raise_error end end describe "when listening is being turned on" do before do Puppet::Indirector::Indirection.stubs(:model).returns mock('indirection') @server = Puppet::Network::Server.new(:port => 31337, :handlers => [:node]) @mock_http_server = mock('http server') @mock_http_server.stubs(:listen) end it "should fetch an instance of an HTTP server" do @server.stubs(:http_server_class).returns(@mock_http_server_class) @mock_http_server_class.expects(:new).returns(@mock_http_server) @server.listen end it "should cause the HTTP server to listen" do @server.stubs(:http_server).returns(@mock_http_server) @mock_http_server.expects(:listen) @server.listen end it "should pass the listening address to the HTTP server" do @server.stubs(:http_server).returns(@mock_http_server) @mock_http_server.expects(:listen).with do |args| args[:address] == '127.0.0.1' end @server.listen end it "should pass the listening port to the HTTP server" do @server.stubs(:http_server).returns(@mock_http_server) @mock_http_server.expects(:listen).with do |args| args[:port] == 31337 end @server.listen end it "should pass a list of REST handlers to the HTTP server" do @server.stubs(:http_server).returns(@mock_http_server) @mock_http_server.expects(:listen).with do |args| args[:handlers] == [ :node ] end @server.listen end end describe "when listening is being turned off" do before do @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" do @mock_http_server.expects(:unlisten) @server.unlisten end it "should not allow for indirections to be turned off" do Puppet::Indirector::Indirection.stubs(:model).returns mock('indirection') @server.register(:foo) lambda { @server.unregister(:foo) }.should raise_error(RuntimeError) end end end