diff --git a/lib/puppet/agent.rb b/lib/puppet/agent.rb index b684470e3..d51729397 100644 --- a/lib/puppet/agent.rb +++ b/lib/puppet/agent.rb @@ -1,114 +1,140 @@ require 'sync' require 'puppet/external/event-loop' require 'puppet/application' # A general class for triggering a run of another # class. class Puppet::Agent require 'puppet/agent/locker' include Puppet::Agent::Locker require 'puppet/agent/disabler' include Puppet::Agent::Disabler attr_reader :client_class, :client, :splayed + attr_accessor :should_fork # Just so we can specify that we are "the" instance. def initialize(client_class) @splayed = false @client_class = client_class end def lockfile_path client_class.lockfile_path end def needing_restart? Puppet::Application.restart_requested? end # Perform a run with our client. def run(*args) if running? Puppet.notice "Run of #{client_class} already in progress; skipping" return end if disabled? Puppet.notice "Skipping run of #{client_class}; administratively disabled: #{disable_message}" return end result = nil block_run = Puppet::Application.controlled_run do splay - with_client do |client| - begin - sync.synchronize { lock { result = client.run(*args) } } - rescue SystemExit,NoMemoryError - raise - rescue Exception => detail - puts detail.backtrace if Puppet[:trace] - Puppet.err "Could not run #{client_class}: #{detail}" + result = run_in_fork(should_fork) do + with_client do |client| + begin + sync.synchronize { lock { client.run(*args) } } + rescue SystemExit,NoMemoryError + raise + rescue Exception => detail + puts detail.backtrace if Puppet[:trace] + Puppet.err "Could not run #{client_class}: #{detail}" + end end end true end Puppet.notice "Shutdown/restart in progress; skipping run" unless block_run result end def stopping? Puppet::Application.stop_requested? end # Have we splayed already? def splayed? splayed end # Sleep when splay is enabled; else just return. def splay return unless Puppet[:splay] return if splayed? time = rand(Integer(Puppet[:splaylimit]) + 1) Puppet.info "Sleeping for #{time} seconds (splay is enabled)" sleep(time) @splayed = true end # Start listening for events. We're pretty much just listening for # timer events here. def start # Create our timer. Puppet will handle observing it and such. timer = EventLoop::Timer.new(:interval => Puppet[:runinterval], :tolerance => 1, :start? => true) do run end # Run once before we start following the timer timer.sound_alarm end def sync @sync ||= Sync.new end + def run_in_fork(forking = true) + return yield unless forking or Puppet.features.windows? + + child_pid = Kernel.fork do + $0 = "puppet agent: applying configuration" + begin + exit(yield) + rescue SystemExit + exit(-1) + rescue NoMemoryError + exit(-2) + end + end + exit_code = Process.waitpid2(child_pid) + case exit_code[1].exitstatus + when -1 + raise SystemExit + when -2 + raise NoMemoryError + end + exit_code[1].exitstatus + end + private # Create and yield a client instance, keeping a reference # to it during the yield. def with_client begin @client = client_class.new rescue SystemExit,NoMemoryError raise rescue Exception => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Could not create instance of #{client_class}: #{detail}" return end yield @client ensure @client = nil end end diff --git a/lib/puppet/agent/locker.rb b/lib/puppet/agent/locker.rb index d41b12547..42eb6fab5 100644 --- a/lib/puppet/agent/locker.rb +++ b/lib/puppet/agent/locker.rb @@ -1,30 +1,27 @@ require 'puppet/util/pidlock' # Break out the code related to locking the agent. This module is just # included into the agent, but having it here makes it easier to test. module Puppet::Agent::Locker # Yield if we get a lock, else do nothing. Return # true/false depending on whether we get the lock. def lock if lockfile.lock begin yield ensure lockfile.unlock end - return true - else - return false end end def lockfile @lockfile ||= Puppet::Util::Pidlock.new(lockfile_path) @lockfile end def running? lockfile.locked? end end diff --git a/lib/puppet/application/agent.rb b/lib/puppet/application/agent.rb index 74eb22256..465b26863 100644 --- a/lib/puppet/application/agent.rb +++ b/lib/puppet/application/agent.rb @@ -1,483 +1,484 @@ require 'puppet/application' class Puppet::Application::Agent < Puppet::Application should_parse_config run_mode :agent attr_accessor :args, :agent, :daemon, :host def preinit # Do an initial trap, so that cancels don't get a stack trace. Signal.trap(:INT) do $stderr.puts "Cancelling startup" exit(0) end { :waitforcert => nil, :detailed_exitcodes => false, :verbose => false, :debug => false, :centrallogs => false, :setdest => false, :enable => false, :disable => false, :client => true, :fqdn => nil, :serve => [], :digest => :MD5, :fingerprint => false, }.each do |opt,val| options[opt] = val end @args = {} require 'puppet/daemon' @daemon = Puppet::Daemon.new @daemon.argv = ARGV.dup end option("--centrallogging") option("--disable [MESSAGE]") do |message| options[:disable] = true options[:disable_message] = message end option("--enable") option("--debug","-d") option("--fqdn FQDN","-f") option("--test","-t") option("--verbose","-v") option("--fingerprint") option("--digest DIGEST") option("--no-client") do |arg| options[:client] = false end option("--detailed-exitcodes") do |arg| options[:detailed_exitcodes] = true end option("--logdest DEST", "-l DEST") do |arg| begin Puppet::Util::Log.newdestination(arg) options[:setdest] = true rescue => detail puts detail.backtrace if Puppet[:debug] $stderr.puts detail.to_s end end option("--waitforcert WAITFORCERT", "-w") do |arg| options[:waitforcert] = arg.to_i end option("--port PORT","-p") do |arg| @args[:Port] = arg end def help <<-HELP puppet-agent(8) -- The puppet agent daemon ======== SYNOPSIS -------- Retrieves the client configuration from the puppet master and applies it to the local host. This service may be run as a daemon, run periodically using cron (or something similar), or run interactively for testing purposes. USAGE ----- puppet agent [--certname ] [-D|--daemonize|--no-daemonize] [-d|--debug] [--detailed-exitcodes] [--digest ] [--disable [message]] [--enable] [--fingerprint] [-h|--help] [-l|--logdest syslog||console] [--no-client] [--noop] [-o|--onetime] [-t|--test] [-v|--verbose] [-V|--version] [-w|--waitforcert ] DESCRIPTION ----------- This is the main puppet client. Its job is to retrieve the local machine's configuration from a remote server and apply it. In order to successfully communicate with the remote server, the client must have a certificate signed by a certificate authority that the server trusts; the recommended method for this, at the moment, is to run a certificate authority as part of the puppet server (which is the default). The client will connect and request a signed certificate, and will continue connecting until it receives one. Once the client has a signed certificate, it will retrieve its configuration and apply it. USAGE NOTES ----------- 'puppet agent' does its best to find a compromise between interactive use and daemon use. Run with no arguments and no configuration, it will go into the background, attempt to get a signed certificate, and retrieve and apply its configuration every 30 minutes. Some flags are meant specifically for interactive use -- in particular, 'test', 'tags' or 'fingerprint' are useful. 'test' enables verbose logging, causes the daemon to stay in the foreground, exits if the server's configuration is invalid (this happens if, for instance, you've left a syntax error on the server), and exits after running the configuration once (rather than hanging around as a long-running process). 'tags' allows you to specify what portions of a configuration you want to apply. Puppet elements are tagged with all of the class or definition names that contain them, and you can use the 'tags' flag to specify one of these names, causing only configuration elements contained within that class or definition to be applied. This is very useful when you are testing new configurations -- for instance, if you are just starting to manage 'ntpd', you would put all of the new elements into an 'ntpd' class, and call puppet with '--tags ntpd', which would only apply that small portion of the configuration during your testing, rather than applying the whole thing. 'fingerprint' is a one-time flag. In this mode 'puppet agent' will run once and display on the console (and in the log) the current certificate (or certificate request) fingerprint. Providing the '--digest' option allows to use a different digest algorithm to generate the fingerprint. The main use is to verify that before signing a certificate request on the master, the certificate request the master received is the same as the one the client sent (to prevent against man-in-the-middle attacks when signing certificates). OPTIONS ------- Note that any configuration parameter that's valid in the configuration file is also a valid long argument. For example, 'server' is a valid configuration parameter, so you can specify '--server ' as an argument. See the configuration file documentation at http://docs.puppetlabs.com/references/stable/configuration.html for the full list of acceptable parameters. A commented list of all configuration options can also be generated by running puppet agent with '--genconfig'. * --certname: Set the certname (unique ID) of the client. The master reads this unique identifying string, which is usually set to the node's fully-qualified domain name, to determine which configurations the node will receive. Use this option to debug setup problems or implement unusual node identification schemes. * --daemonize: Send the process into the background. This is the default. * --no-daemonize: Do not send the process into the background. * --debug: Enable full debugging. * --detailed-exitcodes: Provide transaction information via exit codes. If this is enabled, an exit code of '2' means there were changes, an exit code of '4' means there were failures during the transaction, and an exit code of '6' means there were both changes and failures. * --digest: Change the certificate fingerprinting digest algorithm. The default is MD5. Valid values depends on the version of OpenSSL installed, but should always at least contain MD5, MD2, SHA1 and SHA256. * --disable: Disable working on the local system. This puts a lock file in place, causing 'puppet agent' not to work on the system until the lock file is removed. This is useful if you are testing a configuration and do not want the central configuration to override the local state until everything is tested and committed. Disable can also take an optional message that will be reported by the 'puppet agent' at the next disabled run. 'puppet agent' uses the same lock file while it is running, so no more than one 'puppet agent' process is working at a time. 'puppet agent' exits after executing this. * --enable: Enable working on the local system. This removes any lock file, causing 'puppet agent' to start managing the local system again (although it will continue to use its normal scheduling, so it might not start for another half hour). 'puppet agent' exits after executing this. * --fingerprint: Display the current certificate or certificate signing request fingerprint and then exit. Use the '--digest' option to change the digest algorithm used. * --help: Print this help message * --logdest: Where to send messages. Choose between syslog, the console, and a log file. Defaults to sending messages to syslog, or the console if debugging or verbosity is enabled. * --no-client: Do not create a config client. This will cause the daemon to run without ever checking for its configuration automatically, and only makes sense when puppet agent is being run with listen = true in puppet.conf or was started with the `--listen` option. * --noop: Use 'noop' mode where the daemon runs in a no-op or dry-run mode. This is useful for seeing what changes Puppet will make without actually executing the changes. * --onetime: Run the configuration once. Runs a single (normally daemonized) Puppet run. Useful for interactively running puppet agent when used in conjunction with the --no-daemonize option. * --test: Enable the most common options used for testing. These are 'onetime', 'verbose', 'ignorecache', 'no-daemonize', 'no-usecacheonfailure', 'detailed-exit-codes', 'no-splay', and 'show_diff'. * --verbose: Turn on verbose reporting. * --version: Print the puppet version number and exit. * --waitforcert: This option only matters for daemons that do not yet have certificates and it is enabled by default, with a value of 120 (seconds). This causes 'puppet agent' to connect to the server every 2 minutes and ask it to sign a certificate request. This is useful for the initial setup of a puppet client. You can turn off waiting for certificates by specifying a time of 0. EXAMPLE ------- $ puppet agent --server puppet.domain.com DIAGNOSTICS ----------- Puppet agent accepts the following signals: * SIGHUP: Restart the puppet agent daemon. * SIGINT and SIGTERM: Shut down the puppet agent daemon. * SIGUSR1: Immediately retrieve and apply configurations from the puppet master. AUTHOR ------ Luke Kanies COPYRIGHT --------- Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License HELP end def run_command return fingerprint if options[:fingerprint] return onetime if Puppet[:onetime] main end def fingerprint unless cert = host.certificate || host.certificate_request $stderr.puts "Fingerprint asked but no certificate nor certificate request have yet been issued" exit(1) return end unless fingerprint = cert.fingerprint(options[:digest]) raise ArgumentError, "Could not get fingerprint for digest '#{options[:digest]}'" end puts fingerprint end def onetime unless options[:client] $stderr.puts "onetime is specified but there is no client" exit(43) return end @daemon.set_signal_traps begin - report = @agent.run + @agent.should_fork = false + exitstatus = @agent.run rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err detail.to_s end @daemon.stop(:exit => false) - if not report + if not exitstatus exit(1) elsif options[:detailed_exitcodes] then - exit(report.exit_status) + exit(exitstatus) else exit(0) end end def main Puppet.notice "Starting Puppet client version #{Puppet.version}" @daemon.start end # Enable all of the most common test options. def setup_test Puppet.settings.handlearg("--ignorecache") Puppet.settings.handlearg("--no-usecacheonfailure") Puppet.settings.handlearg("--no-splay") Puppet.settings.handlearg("--show_diff") Puppet.settings.handlearg("--no-daemonize") options[:verbose] = true Puppet[:onetime] = true options[:detailed_exitcodes] = true end # Handle the logging settings. def setup_logs if options[:debug] or options[:verbose] Puppet::Util::Log.newdestination(:console) if options[:debug] Puppet::Util::Log.level = :debug else Puppet::Util::Log.level = :info end end Puppet::Util::Log.newdestination(:syslog) unless options[:setdest] end def enable_disable_client(agent) if options[:enable] agent.enable elsif options[:disable] agent.disable(options[:disable_message] || 'reason not specified') end exit(0) end def setup_listen unless FileTest.exists?(Puppet[:rest_authconfig]) Puppet.err "Will not start without authorization file #{Puppet[:rest_authconfig]}" exit(14) end require 'puppet/network/server' # No REST handlers yet. server = Puppet::Network::Server.new(:port => Puppet[:puppetport]) @daemon.server = server end def setup_host @host = Puppet::SSL::Host.new waitforcert = options[:waitforcert] || (Puppet[:onetime] ? 0 : 120) cert = @host.wait_for_cert(waitforcert) unless options[:fingerprint] end def setup_agent # We need tomake the client either way, we just don't start it # if --no-client is set. require 'puppet/agent' require 'puppet/configurer' @agent = Puppet::Agent.new(Puppet::Configurer) enable_disable_client(@agent) if options[:enable] or options[:disable] @daemon.agent = agent if options[:client] # It'd be nice to daemonize later, but we have to daemonize before the # waitforcert happens. @daemon.daemonize if Puppet[:daemonize] setup_host @objects = [] # This has to go after the certs are dealt with. if Puppet[:listen] unless Puppet[:onetime] setup_listen else Puppet.notice "Ignoring --listen on onetime run" end end end def setup setup_test if options[:test] setup_logs exit(Puppet.settings.print_configs ? 0 : 1) if Puppet.settings.print_configs? args[:Server] = Puppet[:server] if options[:fqdn] args[:FQDN] = options[:fqdn] Puppet[:certname] = options[:fqdn] end if options[:centrallogs] logdest = args[:Server] logdest += ":" + args[:Port] if args.include?(:Port) Puppet::Util::Log.newdestination(logdest) end Puppet.settings.use :main, :agent, :ssl # Always ignoreimport for agent. It really shouldn't even try to import, # but this is just a temporary band-aid. Puppet[:ignoreimport] = true # We need to specify a ca location for all of the SSL-related i # indirected classes to work; in fingerprint mode we just need # access to the local files and we don't need a ca. Puppet::SSL::Host.ca_location = options[:fingerprint] ? :none : :remote Puppet::Transaction::Report.indirection.terminus_class = :rest # we want the last report to be persisted locally Puppet::Transaction::Report.indirection.cache_class = :yaml # Override the default; puppetd needs this, usually. # You can still override this on the command-line with, e.g., :compiler. Puppet[:catalog_terminus] = :rest # Override the default. Puppet[:facts_terminus] = :facter Puppet::Resource::Catalog.indirection.cache_class = :yaml unless options[:fingerprint] setup_agent else setup_host end end end diff --git a/lib/puppet/application/apply.rb b/lib/puppet/application/apply.rb index eb57ce506..588248ccf 100644 --- a/lib/puppet/application/apply.rb +++ b/lib/puppet/application/apply.rb @@ -1,266 +1,266 @@ require 'puppet/application' class Puppet::Application::Apply < Puppet::Application should_parse_config option("--debug","-d") option("--execute EXECUTE","-e") do |arg| options[:code] = arg end option("--loadclasses","-L") option("--verbose","-v") option("--use-nodes") option("--detailed-exitcodes") option("--apply catalog", "-a catalog") do |arg| Puppet.warning <'. EOM options[:catalog] = arg end option("--catalog catalog", "-c catalog") do |arg| options[:catalog] = arg end option("--logdest LOGDEST", "-l") do |arg| begin Puppet::Util::Log.newdestination(arg) options[:logset] = true rescue => detail $stderr.puts detail.to_s end end option("--parseonly") do puts "--parseonly has been removed. Please use 'puppet parser validate '" exit 1 end def help <<-HELP puppet-apply(8) -- Apply Puppet manifests locally ======== SYNOPSIS -------- Applies a standalone Puppet manifest to the local system. USAGE ----- puppet apply [-h|--help] [-V|--version] [-d|--debug] [-v|--verbose] [-e|--execute] [--detailed-exitcodes] [-l|--logdest ] [--apply ] [--catalog ] DESCRIPTION ----------- This is the standalone puppet execution tool; use it to apply individual manifests. When provided with a modulepath, via command line or config file, puppet apply can effectively mimic the catalog that would be served by puppet master with access to the same modules, although there are some subtle differences. When combined with scheduling and an automated system for pushing manifests, this can be used to implement a serverless Puppet site. Most users should use 'puppet agent' and 'puppet master' for site-wide manifests. OPTIONS ------- Note that any configuration parameter that's valid in the configuration file is also a valid long argument. For example, 'tags' is a valid configuration parameter, so you can specify '--tags ,' as an argument. See the configuration file documentation at http://docs.puppetlabs.com/references/stable/configuration.html for the full list of acceptable parameters. A commented list of all configuration options can also be generated by running puppet with '--genconfig'. * --debug: Enable full debugging. * --detailed-exitcodes: Provide transaction information via exit codes. If this is enabled, an exit code of '2' means there were changes, an exit code of '4' means there were failures during the transaction, and an exit code of '6' means there were both changes and failures. * --help: Print this help message * --loadclasses: Load any stored classes. 'puppet agent' caches configured classes (usually at /etc/puppet/classes.txt), and setting this option causes all of those classes to be set in your puppet manifest. * --logdest: Where to send messages. Choose between syslog, the console, and a log file. Defaults to sending messages to the console. * --execute: Execute a specific piece of Puppet code * --verbose: Print extra information. * --apply: Apply a JSON catalog (such as one generated with 'puppet master --compile'). You can either specify a JSON file or pipe in JSON from standard input. Deprecated, please use --catalog instead. * --catalog: Apply a JSON catalog (such as one generated with 'puppet master --compile'). You can either specify a JSON file or pipe in JSON from standard input. EXAMPLE ------- $ puppet apply -l /tmp/manifest.log manifest.pp $ puppet apply --modulepath=/root/dev/modules -e "include ntpd::server" $ puppet apply --catalog catalog.json AUTHOR ------ Luke Kanies COPYRIGHT --------- Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License HELP end def run_command if options[:catalog] apply else main end end def apply if options[:catalog] == "-" text = $stdin.read else text = File.read(options[:catalog]) end begin catalog = Puppet::Resource::Catalog.convert_from(Puppet::Resource::Catalog.default_format,text) catalog = Puppet::Resource::Catalog.pson_create(catalog) unless catalog.is_a?(Puppet::Resource::Catalog) rescue => detail raise Puppet::Error, "Could not deserialize catalog from pson: #{detail}" end catalog = catalog.to_ral require 'puppet/configurer' configurer = Puppet::Configurer.new configurer.run :catalog => catalog end def main # Set our code or file to use. if options[:code] or command_line.args.length == 0 Puppet[:code] = options[:code] || STDIN.read else manifest = command_line.args.shift raise "Could not find file #{manifest}" unless File.exist?(manifest) Puppet.warning("Only one file can be applied per run. Skipping #{command_line.args.join(', ')}") if command_line.args.size > 0 Puppet[:manifest] = manifest end # Collect our facts. unless facts = Puppet::Node::Facts.indirection.find(Puppet[:node_name_value]) raise "Could not find facts for #{Puppet[:node_name_value]}" end unless Puppet[:node_name_fact].empty? Puppet[:node_name_value] = facts.values[Puppet[:node_name_fact]] facts.name = Puppet[:node_name_value] end # Find our Node unless node = Puppet::Node.indirection.find(Puppet[:node_name_value]) raise "Could not find node #{Puppet[:node_name_value]}" end # Merge in the facts. node.merge(facts.values) # Allow users to load the classes that puppet agent creates. if options[:loadclasses] file = Puppet[:classfile] if FileTest.exists?(file) unless FileTest.readable?(file) $stderr.puts "#{file} is not readable" exit(63) end node.classes = File.read(file).split(/[\s\n]+/) end end begin # Compile our catalog starttime = Time.now catalog = Puppet::Resource::Catalog.indirection.find(node.name, :use_node => node) # Translate it to a RAL catalog catalog = catalog.to_ral catalog.finalize catalog.retrieval_duration = Time.now - starttime require 'puppet/configurer' configurer = Puppet::Configurer.new - report = configurer.run(:skip_plugin_download => true, :catalog => catalog) + exit_status = configurer.run(:skip_plugin_download => true, :catalog => catalog) - if not report + if not exit_status exit(1) elsif options[:detailed_exitcodes] then - exit(report.exit_status) + exit(exit_status) else exit(0) end rescue => detail puts detail.backtrace if Puppet[:trace] $stderr.puts detail.message exit(1) end end def setup exit(Puppet.settings.print_configs ? 0 : 1) if Puppet.settings.print_configs? Puppet::Util::Log.newdestination(:console) unless options[:logset] client = nil server = nil Signal.trap(:INT) do $stderr.puts "Exiting" exit(1) end # we want the last report to be persisted locally Puppet::Transaction::Report.indirection.cache_class = :yaml if options[:debug] Puppet::Util::Log.level = :debug elsif options[:verbose] Puppet::Util::Log.level = :info end end end diff --git a/lib/puppet/configurer.rb b/lib/puppet/configurer.rb index 1157eda6f..f314fa378 100644 --- a/lib/puppet/configurer.rb +++ b/lib/puppet/configurer.rb @@ -1,253 +1,257 @@ # The client for interacting with the puppetmaster config server. require 'sync' require 'timeout' require 'puppet/network/http_pool' require 'puppet/util' class Puppet::Configurer require 'puppet/configurer/fact_handler' require 'puppet/configurer/plugin_handler' include Puppet::Configurer::FactHandler include Puppet::Configurer::PluginHandler # For benchmarking include Puppet::Util attr_reader :compile_time # Provide more helpful strings to the logging that the Agent does def self.to_s "Puppet configuration client" end class << self # Puppetd should only have one instance running, and we need a way # to retrieve it. attr_accessor :instance include Puppet::Util end # How to lock instances of this class. def self.lockfile_path Puppet[:puppetdlockfile] end def clear @catalog.clear(true) if @catalog @catalog = nil end def execute_postrun_command execute_from_setting(:postrun_command) end def execute_prerun_command execute_from_setting(:prerun_command) end # Initialize and load storage def dostorage Puppet::Util::Storage.load @compile_time ||= Puppet::Util::Storage.cache(:configuration)[:compile_time] rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Corrupt state file #{Puppet[:statefile]}: #{detail}" begin ::File.unlink(Puppet[:statefile]) retry rescue => detail raise Puppet::Error.new("Cannot remove #{Puppet[:statefile]}: #{detail}") end end # Just so we can specify that we are "the" instance. def initialize Puppet.settings.use(:main, :ssl, :agent) self.class.instance = self @running = false @splayed = false end # Prepare for catalog retrieval. Downloads everything necessary, etc. def prepare(options) dostorage download_plugins unless options[:skip_plugin_download] end # Get the remote catalog, yo. Returns nil if no catalog can be found. def retrieve_catalog(fact_options) fact_options ||= {} # First try it with no cache, then with the cache. unless (Puppet[:use_cached_catalog] and result = retrieve_catalog_from_cache(fact_options)) or result = retrieve_new_catalog(fact_options) if ! Puppet[:usecacheonfailure] Puppet.warning "Not using cache on failed catalog" return nil end result = retrieve_catalog_from_cache(fact_options) end return nil unless result convert_catalog(result, @duration) end # Convert a plain resource catalog into our full host catalog. def convert_catalog(result, duration) catalog = result.to_ral catalog.finalize catalog.retrieval_duration = duration catalog.write_class_file catalog.write_resource_file catalog end # Retrieve (optionally) and apply a catalog. If a catalog is passed in # the options, then apply that one, otherwise retrieve it. def retrieve_and_apply_catalog(options, fact_options) unless catalog = (options.delete(:catalog) || retrieve_catalog(fact_options)) Puppet.err "Could not retrieve catalog; skipping run" return end report = options[:report] report.configuration_version = catalog.version report.environment = Puppet[:environment] benchmark(:notice, "Finished catalog run") do catalog.apply(options) end report.finalize_report report end # The code that actually runs the catalog. # This just passes any options on to the catalog, # which accepts :tags and :ignoreschedules. def run(options = {}) options[:report] ||= Puppet::Transaction::Report.new("apply") report = options[:report] Puppet::Util::Log.newdestination(report) begin prepare(options) if Puppet::Resource::Catalog.indirection.terminus_class == :rest # This is a bit complicated. We need the serialized and escaped facts, # and we need to know which format they're encoded in. Thus, we # get a hash with both of these pieces of information. fact_options = facts_for_uploading end # set report host name now that we have the fact report.host = Puppet[:node_name_value] begin execute_prerun_command or return nil - retrieve_and_apply_catalog(options, fact_options) + if retrieve_and_apply_catalog(options, fact_options) + report.exit_status + else + nil + end rescue SystemExit,NoMemoryError raise rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Failed to apply catalog: #{detail}" return nil ensure execute_postrun_command or return nil end ensure # Make sure we forget the retained module_directories of any autoload # we might have used. Thread.current[:env_module_directories] = nil end ensure Puppet::Util::Log.close(report) send_report(report) end def send_report(report) puts report.summary if Puppet[:summarize] save_last_run_summary(report) Puppet::Transaction::Report.indirection.save(report) if Puppet[:report] rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Could not send report: #{detail}" end def save_last_run_summary(report) last_run = Puppet.settings.setting(:lastrunfile) last_run.create = true # force file creation resource = last_run.to_resource resource[:content] = YAML.dump(report.raw_summary) catalog = Puppet::Resource::Catalog.new("last_run_file") catalog.add_resource(resource) ral = catalog.to_ral ral.host_config = false ral.apply rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Could not save last run local report: #{detail}" end private def self.timeout timeout = Puppet[:configtimeout] case timeout when String if timeout =~ /^\d+$/ timeout = Integer(timeout) else raise ArgumentError, "Configuration timeout must be an integer" end when Integer # nothing else raise ArgumentError, "Configuration timeout must be an integer" end timeout end def execute_from_setting(setting) return true if (command = Puppet[setting]) == "" begin Puppet::Util.execute([command]) true rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Could not run command from #{setting}: #{detail}" false end end def retrieve_catalog_from_cache(fact_options) result = nil @duration = thinmark do result = Puppet::Resource::Catalog.indirection.find(Puppet[:node_name_value], fact_options.merge(:ignore_terminus => true)) end Puppet.notice "Using cached catalog" result rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Could not retrieve catalog from cache: #{detail}" return nil end def retrieve_new_catalog(fact_options) result = nil @duration = thinmark do result = Puppet::Resource::Catalog.indirection.find(Puppet[:node_name_value], fact_options.merge(:ignore_cache => true)) end result rescue SystemExit,NoMemoryError raise rescue Exception => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Could not retrieve catalog from remote server: #{detail}" return nil end end diff --git a/lib/puppet/daemon.rb b/lib/puppet/daemon.rb index 22630ffb8..03c21a282 100755 --- a/lib/puppet/daemon.rb +++ b/lib/puppet/daemon.rb @@ -1,130 +1,133 @@ require 'puppet' require 'puppet/util/pidlock' require 'puppet/external/event-loop' 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("/") begin $stdin.reopen "/dev/null" $stdout.reopen "/dev/null", "a" $stderr.reopen $stdout Puppet::Util::Log.reopen rescue => detail Puppet.err "Could not start #{Puppet[:name]}: #{detail}" Puppet::Util::secure_open("/tmp/daemonout", "w") { |f| f.puts "Could not start #{Puppet[:name]}: #{detail}" } exit(12) end 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 locker = Puppet::Util::Pidlock.new(pidfile) locker.unlock or Puppet.err "Could not remove PID file #{pidfile}" if locker.locked? 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 server.start if server - agent.start if agent + if agent + agent.should_fork = true + agent.start + end EventLoop.current.run end end diff --git a/spec/unit/agent/locker_spec.rb b/spec/unit/agent/locker_spec.rb index 9b530c0d8..d649be75e 100755 --- a/spec/unit/agent/locker_spec.rb +++ b/spec/unit/agent/locker_spec.rb @@ -1,87 +1,87 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/agent' require 'puppet/agent/locker' class LockerTester include Puppet::Agent::Locker end describe Puppet::Agent::Locker do before do @locker = LockerTester.new @locker.stubs(:lockfile_path).returns "/my/lock" end it "should use a Pidlock instance as its lockfile" do @locker.lockfile.should be_instance_of(Puppet::Util::Pidlock) end it "should use 'lockfile_path' to determine its lockfile path" do @locker.expects(:lockfile_path).returns "/my/lock" lock = Puppet::Util::Pidlock.new("/my/lock") Puppet::Util::Pidlock.expects(:new).with("/my/lock").returns lock @locker.lockfile end it "should reuse the same lock file each time" do @locker.lockfile.should equal(@locker.lockfile) end it "should have a method that yields when a lock is attained" do @locker.lockfile.expects(:lock).returns true yielded = false @locker.lock do yielded = true end yielded.should be_true end - it "should return true when the lock method successfully locked" do + it "should return the block result when the lock method successfully locked" do @locker.lockfile.expects(:lock).returns true - @locker.lock {}.should be_true + @locker.lock { :result }.should == :result end - it "should return true when the lock method does not receive the lock" do + it "should return nil when the lock method does not receive the lock" do @locker.lockfile.expects(:lock).returns false - @locker.lock {}.should be_false + @locker.lock {}.should be_nil end it "should not yield when the lock method does not receive the lock" do @locker.lockfile.expects(:lock).returns false yielded = false @locker.lock { yielded = true } yielded.should be_false end it "should not unlock when a lock was not received" do @locker.lockfile.expects(:lock).returns false @locker.lockfile.expects(:unlock).never @locker.lock {} end it "should unlock after yielding upon obtaining a lock" do @locker.lockfile.stubs(:lock).returns true @locker.lockfile.expects(:unlock) @locker.lock {} end it "should unlock after yielding upon obtaining a lock, even if the block throws an exception" do @locker.lockfile.stubs(:lock).returns true @locker.lockfile.expects(:unlock) lambda { @locker.lock { raise "foo" } }.should raise_error(RuntimeError) end it "should be considered running if the lockfile is locked" do @locker.lockfile.expects(:locked?).returns true @locker.should be_running end end diff --git a/spec/unit/agent_spec.rb b/spec/unit/agent_spec.rb index 9e2840c58..48cc76006 100755 --- a/spec/unit/agent_spec.rb +++ b/spec/unit/agent_spec.rb @@ -1,285 +1,346 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/agent' class AgentTestClient def run # no-op end def stop # no-op end end def without_warnings flag = $VERBOSE $VERBOSE = nil yield $VERBOSE = flag end describe Puppet::Agent do before do @agent = Puppet::Agent.new(AgentTestClient) # So we don't actually try to hit the filesystem. @agent.stubs(:lock).yields @agent.stubs(:disabled?).returns(false) # make Puppet::Application safe for stubbing; restore in an :after block; silence warnings for this. without_warnings { Puppet::Application = Class.new(Puppet::Application) } Puppet::Application.stubs(:clear?).returns(true) Puppet::Application.class_eval do class << self def controlled_run(&block) block.call end end end end after do # restore Puppet::Application from stub-safe subclass, and silence warnings without_warnings { Puppet::Application = Puppet::Application.superclass } end it "should set its client class at initialization" do Puppet::Agent.new("foo").client_class.should == "foo" end it "should include the Locker module" do Puppet::Agent.ancestors.should be_include(Puppet::Agent::Locker) end it "should create an instance of its client class and run it when asked to run" do client = mock 'client' AgentTestClient.expects(:new).returns client client.expects(:run) @agent.stubs(:running?).returns false @agent.run end it "should determine its lock file path by asking the client class" do AgentTestClient.expects(:lockfile_path).returns "/my/lock" @agent.lockfile_path.should == "/my/lock" end it "should be considered running if the lock file is locked" do lockfile = mock 'lockfile' @agent.expects(:lockfile).returns lockfile lockfile.expects(:locked?).returns true @agent.should be_running end describe "when being run" do before do AgentTestClient.stubs(:lockfile_path).returns "/my/lock" @agent.stubs(:running?).returns false end it "should splay" do @agent.expects(:splay) @agent.stubs(:running?).returns false @agent.run end it "should do nothing if already running" do @agent.expects(:running?).returns true AgentTestClient.expects(:new).never @agent.run end it "should do nothing if disabled" do @agent.expects(:disabled?).returns(true) AgentTestClient.expects(:new).never @agent.run end it "should use Puppet::Application.controlled_run to manage process state behavior" do calls = sequence('calls') Puppet::Application.expects(:controlled_run).yields.in_sequence(calls) AgentTestClient.expects(:new).once.in_sequence(calls) @agent.run end it "should not fail if a client class instance cannot be created" do AgentTestClient.expects(:new).raises "eh" Puppet.expects(:err) @agent.run end it "should not fail if there is an exception while running its client" do client = AgentTestClient.new AgentTestClient.expects(:new).returns client client.expects(:run).raises "eh" Puppet.expects(:err) @agent.run end it "should use a mutex to restrict multi-threading" do client = AgentTestClient.new AgentTestClient.expects(:new).returns client mutex = mock 'mutex' @agent.expects(:sync).returns mutex mutex.expects(:synchronize) client.expects(:run).never # if it doesn't run, then we know our yield is what triggers it @agent.run end it "should use a filesystem lock to restrict multiple processes running the agent" do client = AgentTestClient.new AgentTestClient.expects(:new).returns client @agent.expects(:lock) client.expects(:run).never # if it doesn't run, then we know our yield is what triggers it @agent.run end it "should make its client instance available while running" do client = AgentTestClient.new AgentTestClient.expects(:new).returns client client.expects(:run).with { @agent.client.should equal(client); true } @agent.run end it "should run the client instance with any arguments passed to it" do client = AgentTestClient.new AgentTestClient.expects(:new).returns client client.expects(:run).with("testargs") @agent.run("testargs") end + + it "should return the agent result" do + client = AgentTestClient.new + AgentTestClient.expects(:new).returns client + + @agent.expects(:lock).returns(:result) + @agent.run.should == :result + end + + describe "when should_fork is true" do + before do + @agent.should_fork = true + Kernel.stubs(:fork) + Process.stubs(:waitpid2).returns [123, (stub 'process::status', :exitstatus => 0)] + @agent.stubs(:exit) + end + + it "should run the agent in a forked process" do + client = AgentTestClient.new + AgentTestClient.expects(:new).returns client + + client.expects(:run) + + Kernel.expects(:fork).yields + @agent.run + end + + it "should exit child process if child exit" do + client = AgentTestClient.new + AgentTestClient.expects(:new).returns client + + client.expects(:run).raises(SystemExit) + + Kernel.expects(:fork).yields + @agent.expects(:exit).with(-1) + @agent.run + end + + it "should re-raise exit happening in the child" do + Process.stubs(:waitpid2).returns [123, (stub 'process::status', :exitstatus => -1)] + lambda { @agent.run }.should raise_error(SystemExit) + end + + it "should re-raise NoMoreMemory happening in the child" do + Process.stubs(:waitpid2).returns [123, (stub 'process::status', :exitstatus => -2)] + lambda { @agent.run }.should raise_error(NoMemoryError) + end + + it "should return the child exit code" do + Process.stubs(:waitpid2).returns [123, (stub 'process::status', :exitstatus => 777)] + @agent.run.should == 777 + end + + it "should return the block exit code as the child exit code" do + Kernel.expects(:fork).yields + @agent.expects(:exit).with(777) + @agent.run_in_fork { + 777 + } + end + end end describe "when splaying" do before do Puppet.settings.stubs(:value).with(:splay).returns true Puppet.settings.stubs(:value).with(:splaylimit).returns "10" end it "should do nothing if splay is disabled" do Puppet.settings.expects(:value).returns false @agent.expects(:sleep).never @agent.splay end it "should do nothing if it has already splayed" do @agent.expects(:splayed?).returns true @agent.expects(:sleep).never @agent.splay end it "should log that it is splaying" do @agent.stubs :sleep Puppet.expects :info @agent.splay end it "should sleep for a random portion of the splaylimit plus 1" do Puppet.settings.expects(:value).with(:splaylimit).returns "50" @agent.expects(:rand).with(51).returns 10 @agent.expects(:sleep).with(10) @agent.splay end it "should mark that it has splayed" do @agent.stubs(:sleep) @agent.splay @agent.should be_splayed end end describe "when checking execution state" do describe 'with regular run status' do before :each do Puppet::Application.stubs(:restart_requested?).returns(false) Puppet::Application.stubs(:stop_requested?).returns(false) Puppet::Application.stubs(:interrupted?).returns(false) Puppet::Application.stubs(:clear?).returns(true) end it 'should be false for :stopping?' do @agent.stopping?.should be_false end it 'should be false for :needing_restart?' do @agent.needing_restart?.should be_false end end describe 'with a stop requested' do before :each do Puppet::Application.stubs(:clear?).returns(false) Puppet::Application.stubs(:restart_requested?).returns(false) Puppet::Application.stubs(:stop_requested?).returns(true) Puppet::Application.stubs(:interrupted?).returns(true) end it 'should be true for :stopping?' do @agent.stopping?.should be_true end it 'should be false for :needing_restart?' do @agent.needing_restart?.should be_false end end describe 'with a restart requested' do before :each do Puppet::Application.stubs(:clear?).returns(false) Puppet::Application.stubs(:restart_requested?).returns(true) Puppet::Application.stubs(:stop_requested?).returns(false) Puppet::Application.stubs(:interrupted?).returns(true) end it 'should be false for :stopping?' do @agent.stopping?.should be_false end it 'should be true for :needing_restart?' do @agent.needing_restart?.should be_true end end end describe "when starting" do before do @agent.stubs(:observe_signal) end it "should create a timer with the runinterval, a tolerance of 1, and :start? set to true" do Puppet.settings.expects(:value).with(:runinterval).returns 5 timer = stub 'timer', :sound_alarm => nil EventLoop::Timer.expects(:new).with(:interval => 5, :start? => true, :tolerance => 1).returns timer @agent.stubs(:run) @agent.start end it "should run once immediately" do timer = mock 'timer' EventLoop::Timer.expects(:new).returns timer timer.expects(:sound_alarm) @agent.start end it "should run within the block passed to the timer" do timer = stub 'timer', :sound_alarm => nil EventLoop::Timer.expects(:new).returns(timer).yields @agent.expects(:run) @agent.start end end end diff --git a/spec/unit/application/agent_spec.rb b/spec/unit/application/agent_spec.rb index f512ba4da..e10f10a95 100755 --- a/spec/unit/application/agent_spec.rb +++ b/spec/unit/application/agent_spec.rb @@ -1,592 +1,595 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/agent' require 'puppet/application/agent' require 'puppet/network/server' require 'puppet/daemon' describe Puppet::Application::Agent do before :each do @puppetd = Puppet::Application[:agent] @puppetd.stubs(:puts) @daemon = stub_everything 'daemon' Puppet::Daemon.stubs(:new).returns(@daemon) Puppet[:daemonize] = false @agent = stub_everything 'agent' Puppet::Agent.stubs(:new).returns(@agent) @puppetd.preinit Puppet::Util::Log.stubs(:newdestination) Puppet::Node.indirection.stubs(:terminus_class=) Puppet::Node.indirection.stubs(:cache_class=) Puppet::Node::Facts.indirection.stubs(:terminus_class=) end it "should operate in agent run_mode" do @puppetd.class.run_mode.name.should == :agent end it "should ask Puppet::Application to parse Puppet configuration file" do @puppetd.should_parse_config?.should be_true end it "should declare a main command" do @puppetd.should respond_to(:main) end it "should declare a onetime command" do @puppetd.should respond_to(:onetime) end it "should declare a fingerprint command" do @puppetd.should respond_to(:fingerprint) end it "should declare a preinit block" do @puppetd.should respond_to(:preinit) end describe "in preinit" do it "should catch INT" do Signal.expects(:trap).with { |arg,block| arg == :INT } @puppetd.preinit end it "should init client to true" do @puppetd.preinit @puppetd.options[:client].should be_true end it "should init fqdn to nil" do @puppetd.preinit @puppetd.options[:fqdn].should be_nil end it "should init serve to []" do @puppetd.preinit @puppetd.options[:serve].should == [] end it "should use MD5 as default digest algorithm" do @puppetd.preinit @puppetd.options[:digest].should == :MD5 end it "should not fingerprint by default" do @puppetd.preinit @puppetd.options[:fingerprint].should be_false end end describe "when handling options" do before do @puppetd.command_line.stubs(:args).returns([]) end [:centrallogging, :enable, :debug, :fqdn, :test, :verbose, :digest].each do |option| it "should declare handle_#{option} method" do @puppetd.should respond_to("handle_#{option}".to_sym) end it "should store argument value when calling handle_#{option}" do @puppetd.options.expects(:[]=).with(option, 'arg') @puppetd.send("handle_#{option}".to_sym, 'arg') end end it "should set client to false with --no-client" do @puppetd.handle_no_client(nil) @puppetd.options[:client].should be_false end it "should set waitforcert to 0 with --onetime and if --waitforcert wasn't given" do Puppet[:onetime] = true Puppet::SSL::Host.any_instance.expects(:wait_for_cert).with(0) @puppetd.setup_host end it "should use supplied waitforcert when --onetime is specified" do Puppet[:onetime] = true @puppetd.handle_waitforcert(60) Puppet::SSL::Host.any_instance.expects(:wait_for_cert).with(60) @puppetd.setup_host end it "should use a default value for waitforcert when --onetime and --waitforcert are not specified" do Puppet::SSL::Host.any_instance.expects(:wait_for_cert).with(120) @puppetd.setup_host end it "should set the log destination with --logdest" do @puppetd.options.stubs(:[]=).with { |opt,val| opt == :setdest } Puppet::Log.expects(:newdestination).with("console") @puppetd.handle_logdest("console") end it "should put the setdest options to true" do @puppetd.options.expects(:[]=).with(:setdest,true) @puppetd.handle_logdest("console") end it "should parse the log destination from the command line" do @puppetd.command_line.stubs(:args).returns(%w{--logdest /my/file}) Puppet::Util::Log.expects(:newdestination).with("/my/file") @puppetd.parse_options end it "should store the waitforcert options with --waitforcert" do @puppetd.options.expects(:[]=).with(:waitforcert,42) @puppetd.handle_waitforcert("42") end it "should set args[:Port] with --port" do @puppetd.handle_port("42") @puppetd.args[:Port].should == "42" end end describe "during setup" do before :each do @puppetd.options.stubs(:[]) Puppet.stubs(:info) FileTest.stubs(:exists?).returns(true) Puppet[:libdir] = "/dev/null/lib" Puppet::SSL::Host.stubs(:ca_location=) Puppet::Transaction::Report.indirection.stubs(:terminus_class=) Puppet::Transaction::Report.indirection.stubs(:cache_class=) Puppet::Resource::Catalog.indirection.stubs(:terminus_class=) Puppet::Resource::Catalog.indirection.stubs(:cache_class=) Puppet::Node::Facts.indirection.stubs(:terminus_class=) @host = stub_everything 'host' Puppet::SSL::Host.stubs(:new).returns(@host) Puppet.stubs(:settraps) end describe "with --test" do before :each do #Puppet.settings.stubs(:handlearg) @puppetd.options.stubs(:[]=) end it "should call setup_test" do @puppetd.options.stubs(:[]).with(:test).returns(true) @puppetd.expects(:setup_test) @puppetd.setup end it "should set options[:verbose] to true" do @puppetd.options.expects(:[]=).with(:verbose,true) @puppetd.setup_test end it "should set options[:onetime] to true" do Puppet[:onetime] = false @puppetd.setup_test Puppet[:onetime].should == true end it "should set options[:detailed_exitcodes] to true" do @puppetd.options.expects(:[]=).with(:detailed_exitcodes,true) @puppetd.setup_test end end it "should call setup_logs" do @puppetd.expects(:setup_logs) @puppetd.setup end describe "when setting up logs" do before :each do Puppet::Util::Log.stubs(:newdestination) end it "should set log level to debug if --debug was passed" do @puppetd.options.stubs(:[]).with(:debug).returns(true) @puppetd.setup_logs Puppet::Util::Log.level.should == :debug end it "should set log level to info if --verbose was passed" do @puppetd.options.stubs(:[]).with(:verbose).returns(true) @puppetd.setup_logs Puppet::Util::Log.level.should == :info end [:verbose, :debug].each do |level| it "should set console as the log destination with level #{level}" do @puppetd.options.stubs(:[]).with(level).returns(true) Puppet::Util::Log.expects(:newdestination).with(:console) @puppetd.setup_logs end end it "should set syslog as the log destination if no --logdest" do @puppetd.options.stubs(:[]).with(:setdest).returns(false) Puppet::Util::Log.expects(:newdestination).with(:syslog) @puppetd.setup_logs end end it "should print puppet config if asked to in Puppet config" do Puppet[:configprint] = "pluginsync" Puppet.settings.expects(:print_configs).returns true expect { @puppetd.setup }.to exit_with 0 end it "should exit after printing puppet config if asked to in Puppet config" do Puppet[:modulepath] = '/my/path' Puppet[:configprint] = "modulepath" Puppet::Util::Settings.any_instance.expects(:puts).with('/my/path') expect { @puppetd.setup }.to exit_with 0 end it "should set a central log destination with --centrallogs" do @puppetd.options.stubs(:[]).with(:centrallogs).returns(true) Puppet[:server] = "puppet.reductivelabs.com" Puppet::Util::Log.stubs(:newdestination).with(:syslog) Puppet::Util::Log.expects(:newdestination).with("puppet.reductivelabs.com") @puppetd.setup end it "should use :main, :puppetd, and :ssl" do Puppet.settings.expects(:use).with(:main, :agent, :ssl) @puppetd.setup end it "should install a remote ca location" do Puppet::SSL::Host.expects(:ca_location=).with(:remote) @puppetd.setup end it "should install a none ca location in fingerprint mode" do @puppetd.options.stubs(:[]).with(:fingerprint).returns(true) Puppet::SSL::Host.expects(:ca_location=).with(:none) @puppetd.setup end it "should tell the report handler to use REST" do Puppet::Transaction::Report.indirection.expects(:terminus_class=).with(:rest) @puppetd.setup end it "should tell the report handler to cache locally as yaml" do Puppet::Transaction::Report.indirection.expects(:cache_class=).with(:yaml) @puppetd.setup end it "should change the catalog_terminus setting to 'rest'" do Puppet[:catalog_terminus] = :foo @puppetd.setup Puppet[:catalog_terminus].should == :rest end it "should tell the catalog handler to use cache" do Puppet::Resource::Catalog.indirection.expects(:cache_class=).with(:yaml) @puppetd.setup end it "should change the facts_terminus setting to 'facter'" do Puppet[:facts_terminus] = :foo @puppetd.setup Puppet[:facts_terminus].should == :facter end it "should create an agent" do Puppet::Agent.stubs(:new).with(Puppet::Configurer) @puppetd.setup end [:enable, :disable].each do |action| it "should delegate to enable_disable_client if we #{action} the agent" do @puppetd.options.stubs(:[]).with(action).returns(true) @puppetd.expects(:enable_disable_client).with(@agent) @puppetd.setup end end describe "when enabling or disabling agent" do [:enable, :disable].each do |action| it "should call client.#{action}" do @puppetd.options.stubs(:[]).with(action).returns(true) @agent.expects(action) expect { @puppetd.enable_disable_client(@agent) }.to exit_with 0 end end it "should pass the disable message when disabling" do @puppetd.options.stubs(:[]).with(:disable).returns(true) @puppetd.options.stubs(:[]).with(:disable_message).returns("message") @agent.expects(:disable).with("message") expect { @puppetd.enable_disable_client(@agent) }.to exit_with 0 end it "should pass the default disable message when disabling without a message" do @puppetd.options.stubs(:[]).with(:disable).returns(true) @puppetd.options.stubs(:[]).with(:disable_message).returns(nil) @agent.expects(:disable).with("reason not specified") expect { @puppetd.enable_disable_client(@agent) }.to exit_with 0 end it "should finally exit" do expect { @puppetd.enable_disable_client(@agent) }.to exit_with 0 end end it "should inform the daemon about our agent if :client is set to 'true'" do @puppetd.options.expects(:[]).with(:client).returns true @daemon.expects(:agent=).with(@agent) @puppetd.setup end it "should not inform the daemon about our agent if :client is set to 'false'" do @puppetd.options[:client] = false @daemon.expects(:agent=).never @puppetd.setup end it "should daemonize if needed" do Puppet.features.stubs(:microsoft_windows?).returns false Puppet[:daemonize] = true @daemon.expects(:daemonize) @puppetd.setup end it "should wait for a certificate" do @puppetd.options.stubs(:[]).with(:waitforcert).returns(123) @host.expects(:wait_for_cert).with(123) @puppetd.setup end it "should not wait for a certificate in fingerprint mode" do @puppetd.options.stubs(:[]).with(:fingerprint).returns(true) @puppetd.options.stubs(:[]).with(:waitforcert).returns(123) @host.expects(:wait_for_cert).never @puppetd.setup end it "should setup listen if told to and not onetime" do Puppet[:listen] = true @puppetd.options.stubs(:[]).with(:onetime).returns(false) @puppetd.expects(:setup_listen) @puppetd.setup end describe "when setting up listen" do before :each do Puppet[:authconfig] = 'auth' FileTest.stubs(:exists?).with('auth').returns(true) File.stubs(:exist?).returns(true) @puppetd.options.stubs(:[]).with(:serve).returns([]) @server = stub_everything 'server' Puppet::Network::Server.stubs(:new).returns(@server) end it "should exit if no authorization file" do Puppet.stubs(:err) FileTest.stubs(:exists?).with(Puppet[:rest_authconfig]).returns(false) expect { @puppetd.setup_listen }.to exit_with 14 end it "should use puppet default port" do Puppet[:puppetport] = 32768 Puppet::Network::Server.expects(:new).with { |args| args[:port] == 32768 } @puppetd.setup_listen end end describe "when setting up for fingerprint" do before(:each) do @puppetd.options.stubs(:[]).with(:fingerprint).returns(true) end it "should not setup as an agent" do @puppetd.expects(:setup_agent).never @puppetd.setup end it "should not create an agent" do Puppet::Agent.stubs(:new).with(Puppet::Configurer).never @puppetd.setup end it "should not daemonize" do @daemon.expects(:daemonize).never @puppetd.setup end it "should setup our certificate host" do @puppetd.expects(:setup_host) @puppetd.setup end end end describe "when running" do before :each do @puppetd.agent = @agent @puppetd.daemon = @daemon @puppetd.options.stubs(:[]).with(:fingerprint).returns(false) end it "should dispatch to fingerprint if --fingerprint is used" do @puppetd.options.stubs(:[]).with(:fingerprint).returns(true) @puppetd.stubs(:fingerprint) @puppetd.run_command end it "should dispatch to onetime if --onetime is used" do @puppetd.options.stubs(:[]).with(:onetime).returns(true) @puppetd.stubs(:onetime) @puppetd.run_command end it "should dispatch to main if --onetime and --fingerprint are not used" do @puppetd.options.stubs(:[]).with(:onetime).returns(false) @puppetd.stubs(:main) @puppetd.run_command end describe "with --onetime" do before :each do @agent.stubs(:run).returns(:report) @puppetd.options.stubs(:[]).with(:client).returns(:client) @puppetd.options.stubs(:[]).with(:detailed_exitcodes).returns(false) Puppet.stubs(:newservice) end it "should exit if no defined --client" do $stderr.stubs(:puts) @puppetd.options.stubs(:[]).with(:client).returns(nil) expect { @puppetd.onetime }.to exit_with 43 end it "should setup traps" do @daemon.expects(:set_signal_traps) expect { @puppetd.onetime }.to exit_with 0 end + it "should not let the agent fork" do + @agent.expects(:should_fork=).with(false) + expect { @puppetd.onetime }.to exit_with 0 + end + it "should let the agent run" do @agent.expects(:run).returns(:report) expect { @puppetd.onetime }.to exit_with 0 end it "should finish by exiting with 0 error code" do expect { @puppetd.onetime }.to exit_with 0 end it "should stop the daemon" do @daemon.expects(:stop).with(:exit => false) expect { @puppetd.onetime }.to exit_with 0 end describe "and --detailed-exitcodes" do before :each do @puppetd.options.stubs(:[]).with(:detailed_exitcodes).returns(true) end - it "should exit with report's computed exit status" do + it "should exit with agent computed exit status" do Puppet[:noop] = false - report = stub 'report', :exit_status => 666 - @agent.stubs(:run).returns(report) + @agent.stubs(:run).returns(666) expect { @puppetd.onetime }.to exit_with 666 end - it "should exit with the report's computer exit status, even if --noop is set." do + it "should exit with the agent's exit status, even if --noop is set." do Puppet[:noop] = true - report = stub 'report', :exit_status => 666 - @agent.stubs(:run).returns(report) + @agent.stubs(:run).returns(666) expect { @puppetd.onetime }.to exit_with 666 end end end describe "with --fingerprint" do before :each do @cert = stub_everything 'cert' @puppetd.options.stubs(:[]).with(:fingerprint).returns(true) @puppetd.options.stubs(:[]).with(:digest).returns(:MD5) @host = stub_everything 'host' @puppetd.stubs(:host).returns(@host) end it "should fingerprint the certificate if it exists" do @host.expects(:certificate).returns(@cert) @cert.expects(:fingerprint).with(:MD5).returns "fingerprint" @puppetd.fingerprint end it "should fingerprint the certificate request if no certificate have been signed" do @host.expects(:certificate).returns(nil) @host.expects(:certificate_request).returns(@cert) @cert.expects(:fingerprint).with(:MD5).returns "fingerprint" @puppetd.fingerprint end it "should display the fingerprint" do @host.stubs(:certificate).returns(@cert) @cert.stubs(:fingerprint).with(:MD5).returns("DIGEST") @puppetd.expects(:puts).with "DIGEST" @puppetd.fingerprint end end describe "without --onetime and --fingerprint" do before :each do Puppet.stubs(:notice) @puppetd.options.stubs(:[]).with(:client) end it "should start our daemon" do @daemon.expects(:start) @puppetd.main end end end end diff --git a/spec/unit/configurer_spec.rb b/spec/unit/configurer_spec.rb index 5ea2e4fb1..694b52c34 100755 --- a/spec/unit/configurer_spec.rb +++ b/spec/unit/configurer_spec.rb @@ -1,603 +1,607 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/configurer' describe Puppet::Configurer do before do Puppet.settings.stubs(:use).returns(true) @agent = Puppet::Configurer.new @agent.stubs(:dostorage) Puppet::Util::Storage.stubs(:store) Puppet[:server] = "puppetmaster" Puppet[:report] = true end it "should include the Plugin Handler module" do Puppet::Configurer.ancestors.should be_include(Puppet::Configurer::PluginHandler) end it "should include the Fact Handler module" do Puppet::Configurer.ancestors.should be_include(Puppet::Configurer::FactHandler) end it "should use the puppetdlockfile as its lockfile path" do Puppet.settings.expects(:value).with(:puppetdlockfile).returns("/my/lock") Puppet::Configurer.lockfile_path.should == "/my/lock" end describe "when executing a pre-run hook" do it "should do nothing if the hook is set to an empty string" do Puppet.settings[:prerun_command] = "" Puppet::Util.expects(:exec).never @agent.execute_prerun_command end it "should execute any pre-run command provided via the 'prerun_command' setting" do Puppet.settings[:prerun_command] = "/my/command" Puppet::Util.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed") @agent.execute_prerun_command end it "should fail if the command fails" do Puppet.settings[:prerun_command] = "/my/command" Puppet::Util.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed") @agent.execute_prerun_command.should be_false end end describe "when executing a post-run hook" do it "should do nothing if the hook is set to an empty string" do Puppet.settings[:postrun_command] = "" Puppet::Util.expects(:exec).never @agent.execute_postrun_command end it "should execute any post-run command provided via the 'postrun_command' setting" do Puppet.settings[:postrun_command] = "/my/command" Puppet::Util.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed") @agent.execute_postrun_command end it "should fail if the command fails" do Puppet.settings[:postrun_command] = "/my/command" Puppet::Util.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed") @agent.execute_postrun_command.should be_false end end describe "when executing a catalog run" do before do Puppet.settings.stubs(:use).returns(true) @agent.stubs(:prepare) Puppet::Node::Facts.indirection.terminus_class = :memory @facts = Puppet::Node::Facts.new(Puppet[:node_name_value]) Puppet::Node::Facts.indirection.save(@facts) @catalog = Puppet::Resource::Catalog.new @catalog.stubs(:to_ral).returns(@catalog) Puppet::Resource::Catalog.indirection.terminus_class = :rest Puppet::Resource::Catalog.indirection.stubs(:find).returns(@catalog) @agent.stubs(:send_report) @agent.stubs(:save_last_run_summary) Puppet::Util::Log.stubs(:close_all) end after :all do Puppet::Node::Facts.indirection.reset_terminus_class Puppet::Resource::Catalog.indirection.reset_terminus_class end it "should prepare for the run" do @agent.expects(:prepare) @agent.run end it "should initialize a transaction report if one is not provided" do report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.expects(:new).returns report @agent.run end it "should respect node_name_fact when setting the host on a report" do Puppet[:node_name_fact] = 'my_name_fact' @facts.values = {'my_name_fact' => 'node_name_from_fact'} - @agent.run.host.should == 'node_name_from_fact' + report = Puppet::Transaction::Report.new("apply") + + @agent.run(:report => report) + report.host.should == 'node_name_from_fact' end it "should pass the new report to the catalog" do report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.stubs(:new).returns report @catalog.expects(:apply).with{|options| options[:report] == report} @agent.run end it "should use the provided report if it was passed one" do report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.expects(:new).never @catalog.expects(:apply).with{|options| options[:report] == report} @agent.run(:report => report) end it "should set the report as a log destination" do report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.expects(:new).returns report Puppet::Util::Log.expects(:newdestination).with(report) Puppet::Util::Log.expects(:close).with(report) @agent.run end it "should retrieve the catalog" do @agent.expects(:retrieve_catalog) @agent.run end it "should log a failure and do nothing if no catalog can be retrieved" do @agent.expects(:retrieve_catalog).returns nil Puppet.expects(:err).with "Could not retrieve catalog; skipping run" @agent.run end it "should apply the catalog with all options to :run" do @agent.expects(:retrieve_catalog).returns @catalog @catalog.expects(:apply).with { |args| args[:one] == true } @agent.run :one => true end it "should accept a catalog and use it instead of retrieving a different one" do @agent.expects(:retrieve_catalog).never @catalog.expects(:apply) @agent.run :one => true, :catalog => @catalog end it "should benchmark how long it takes to apply the catalog" do @agent.expects(:benchmark).with(:notice, "Finished catalog run") @agent.expects(:retrieve_catalog).returns @catalog @catalog.expects(:apply).never # because we're not yielding @agent.run end it "should execute post-run hooks after the run" do @agent.expects(:execute_postrun_command) @agent.run end it "should send the report" do report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.expects(:new).returns(report) @agent.expects(:send_report).with(report) @agent.run end it "should send the transaction report even if the catalog could not be retrieved" do @agent.expects(:retrieve_catalog).returns nil report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.expects(:new).returns(report) @agent.expects(:send_report) @agent.run end it "should send the transaction report even if there is a failure" do @agent.expects(:retrieve_catalog).raises "whatever" report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.expects(:new).returns(report) @agent.expects(:send_report) @agent.run.should be_nil end it "should remove the report as a log destination when the run is finished" do report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.expects(:new).returns(report) @agent.run Puppet::Util::Log.destinations.should_not include(report) end - it "should return the report as the result of the run" do + it "should return the report exit_status as the result of the run" do report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.expects(:new).returns(report) + report.expects(:exit_status).returns(1234) - @agent.run.should equal(report) + @agent.run.should == 1234 end it "should send the transaction report even if the pre-run command fails" do report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.expects(:new).returns(report) Puppet.settings[:prerun_command] = "/my/command" Puppet::Util.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed") @agent.expects(:send_report) @agent.run.should be_nil end it "should include the pre-run command failure in the report" do report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.expects(:new).returns(report) Puppet.settings[:prerun_command] = "/my/command" Puppet::Util.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed") report.expects(:<<).with { |log| log.message.include?("Could not run command from prerun_command") } @agent.run.should be_nil end it "should send the transaction report even if the post-run command fails" do report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.expects(:new).returns(report) Puppet.settings[:postrun_command] = "/my/command" Puppet::Util.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed") @agent.expects(:send_report) @agent.run.should be_nil end it "should include the post-run command failure in the report" do report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.expects(:new).returns(report) Puppet.settings[:postrun_command] = "/my/command" Puppet::Util.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed") report.expects(:<<).with { |log| log.message.include?("Could not run command from postrun_command") } @agent.run.should be_nil end it "should execute post-run command even if the pre-run command fails" do Puppet.settings[:prerun_command] = "/my/precommand" Puppet.settings[:postrun_command] = "/my/postcommand" Puppet::Util.expects(:execute).with(["/my/precommand"]).raises(Puppet::ExecutionFailure, "Failed") Puppet::Util.expects(:execute).with(["/my/postcommand"]) @agent.run.should be_nil end it "should finalize the report" do report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.expects(:new).returns(report) report.expects(:finalize_report) @agent.run end it "should not apply the catalog if the pre-run command fails" do report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.expects(:new).returns(report) Puppet.settings[:prerun_command] = "/my/command" Puppet::Util.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed") @catalog.expects(:apply).never() @agent.expects(:send_report) @agent.run.should be_nil end it "should apply the catalog, send the report, and return nil if the post-run command fails" do report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.expects(:new).returns(report) Puppet.settings[:postrun_command] = "/my/command" Puppet::Util.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed") @catalog.expects(:apply) @agent.expects(:send_report) @agent.run.should be_nil end describe "when not using a REST terminus for catalogs" do it "should not pass any facts when retrieving the catalog" do Puppet::Resource::Catalog.indirection.terminus_class = :compiler @agent.expects(:facts_for_uploading).never Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:facts].nil? }.returns @catalog @agent.run end end describe "when using a REST terminus for catalogs" do it "should pass the prepared facts and the facts format as arguments when retrieving the catalog" do Puppet::Resource::Catalog.indirection.terminus_class = :rest @agent.expects(:facts_for_uploading).returns(:facts => "myfacts", :facts_format => :foo) Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:facts] == "myfacts" and options[:facts_format] == :foo }.returns @catalog @agent.run end end end describe "when sending a report" do include PuppetSpec::Files before do Puppet.settings.stubs(:use).returns(true) @configurer = Puppet::Configurer.new Puppet[:lastrunfile] = tmpfile('last_run_file') @report = Puppet::Transaction::Report.new("apply") end it "should print a report summary if configured to do so" do Puppet.settings[:summarize] = true @report.expects(:summary).returns "stuff" @configurer.expects(:puts).with("stuff") @configurer.send_report(@report) end it "should not print a report summary if not configured to do so" do Puppet.settings[:summarize] = false @configurer.expects(:puts).never @configurer.send_report(@report) end it "should save the report if reporting is enabled" do Puppet.settings[:report] = true Puppet::Transaction::Report.indirection.expects(:save).with(@report) @configurer.send_report(@report) end it "should not save the report if reporting is disabled" do Puppet.settings[:report] = false Puppet::Transaction::Report.indirection.expects(:save).with(@report).never @configurer.send_report(@report) end it "should save the last run summary if reporting is enabled" do Puppet.settings[:report] = true @configurer.expects(:save_last_run_summary).with(@report) @configurer.send_report(@report) end it "should save the last run summary if reporting is disabled" do Puppet.settings[:report] = false @configurer.expects(:save_last_run_summary).with(@report) @configurer.send_report(@report) end it "should log but not fail if saving the report fails" do Puppet.settings[:report] = true Puppet::Transaction::Report.indirection.expects(:save).raises("whatever") Puppet.expects(:err) lambda { @configurer.send_report(@report) }.should_not raise_error end end describe "when saving the summary report file" do include PuppetSpec::Files before do Puppet.settings.stubs(:use).returns(true) @configurer = Puppet::Configurer.new @report = stub 'report', :raw_summary => {} Puppet[:lastrunfile] = tmpfile('last_run_file') end it "should write the last run file" do @configurer.save_last_run_summary(@report) FileTest.exists?(Puppet[:lastrunfile]).should be_true end it "should write the raw summary as yaml" do @report.expects(:raw_summary).returns("summary") @configurer.save_last_run_summary(@report) File.read(Puppet[:lastrunfile]).should == YAML.dump("summary") end it "should log but not fail if saving the last run summary fails" do Puppet[:lastrunfile] = "/dev/null/inexistant" Puppet.expects(:err) lambda { @configurer.save_last_run_summary(@report) }.should_not raise_error end end describe "when retrieving a catalog" do before do Puppet.settings.stubs(:use).returns(true) @agent.stubs(:facts_for_uploading).returns({}) @catalog = Puppet::Resource::Catalog.new # this is the default when using a Configurer instance Puppet::Resource::Catalog.indirection.stubs(:terminus_class).returns :rest @agent.stubs(:convert_catalog).returns @catalog end describe "and configured to only retrieve a catalog from the cache" do before do Puppet.settings[:use_cached_catalog] = true end it "should first look in the cache for a catalog" do Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_terminus] == true }.returns @catalog Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.never @agent.retrieve_catalog({}).should == @catalog end it "should compile a new catalog if none is found in the cache" do Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_terminus] == true }.returns nil Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns @catalog @agent.retrieve_catalog({}).should == @catalog end end it "should use the Catalog class to get its catalog" do Puppet::Resource::Catalog.indirection.expects(:find).returns @catalog @agent.retrieve_catalog({}) end it "should use its node_name_value to retrieve the catalog" do Facter.stubs(:value).returns "eh" Puppet.settings[:node_name_value] = "myhost.domain.com" Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| name == "myhost.domain.com" }.returns @catalog @agent.retrieve_catalog({}) end it "should default to returning a catalog retrieved directly from the server, skipping the cache" do Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns @catalog @agent.retrieve_catalog({}).should == @catalog end it "should log and return the cached catalog when no catalog can be retrieved from the server" do Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns nil Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_terminus] == true }.returns @catalog Puppet.expects(:notice) @agent.retrieve_catalog({}).should == @catalog end it "should not look in the cache for a catalog if one is returned from the server" do Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns @catalog Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_terminus] == true }.never @agent.retrieve_catalog({}).should == @catalog end it "should return the cached catalog when retrieving the remote catalog throws an exception" do Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.raises "eh" Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_terminus] == true }.returns @catalog @agent.retrieve_catalog({}).should == @catalog end it "should log and return nil if no catalog can be retrieved from the server and :usecacheonfailure is disabled" do Puppet.stubs(:[]) Puppet.expects(:[]).with(:usecacheonfailure).returns false Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns nil Puppet.expects(:warning) @agent.retrieve_catalog({}).should be_nil end it "should return nil if no cached catalog is available and no catalog can be retrieved from the server" do Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns nil Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_terminus] == true }.returns nil @agent.retrieve_catalog({}).should be_nil end it "should convert the catalog before returning" do Puppet::Resource::Catalog.indirection.stubs(:find).returns @catalog @agent.expects(:convert_catalog).with { |cat, dur| cat == @catalog }.returns "converted catalog" @agent.retrieve_catalog({}).should == "converted catalog" end it "should return nil if there is an error while retrieving the catalog" do Puppet::Resource::Catalog.indirection.expects(:find).at_least_once.raises "eh" @agent.retrieve_catalog({}).should be_nil end end describe "when converting the catalog" do before do Puppet.settings.stubs(:use).returns(true) @catalog = Puppet::Resource::Catalog.new @oldcatalog = stub 'old_catalog', :to_ral => @catalog end it "should convert the catalog to a RAL-formed catalog" do @oldcatalog.expects(:to_ral).returns @catalog @agent.convert_catalog(@oldcatalog, 10).should equal(@catalog) end it "should finalize the catalog" do @catalog.expects(:finalize) @agent.convert_catalog(@oldcatalog, 10) end it "should record the passed retrieval time with the RAL catalog" do @catalog.expects(:retrieval_duration=).with 10 @agent.convert_catalog(@oldcatalog, 10) end it "should write the RAL catalog's class file" do @catalog.expects(:write_class_file) @agent.convert_catalog(@oldcatalog, 10) end it "should write the RAL catalog's resource file" do @catalog.expects(:write_resource_file) @agent.convert_catalog(@oldcatalog, 10) end end describe "when preparing for a run" do before do Puppet.settings.stubs(:use).returns(true) @facts = {"one" => "two", "three" => "four"} end it "should initialize the metadata store" do @agent.class.stubs(:facts).returns(@facts) @agent.expects(:dostorage) @agent.prepare({}) end it "should download plugins" do @agent.expects(:download_plugins) @agent.prepare({}) end end end diff --git a/spec/unit/daemon_spec.rb b/spec/unit/daemon_spec.rb index fc43d93ad..da7953738 100755 --- a/spec/unit/daemon_spec.rb +++ b/spec/unit/daemon_spec.rb @@ -1,291 +1,302 @@ #!/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 do before do + @agent = Puppet::Agent.new(TestClient.new) @daemon = Puppet::Daemon.new 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) EventLoop.current.stubs(:run) 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.stubs(:agent).returns stub('agent', :start => nil) + @daemon.agent = @agent @daemon.expects(:create_pidfile) @daemon.start end it "should start the agent if the agent is configured" do - agent = mock 'agent' - agent.expects(:start) - @daemon.stubs(:agent).returns agent + @agent.expects(:start) + @daemon.agent = @agent + + @daemon.start + end + + it "should start the agent with should_fork at true" do + @agent.expects(:should_fork=).with(true) + @daemon.agent = @agent @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 it "should let the current EventLoop run" do - @daemon.stubs(:agent).returns stub('agent', :start => nil) + @daemon.agent = @agent EventLoop.current.expects(:run) @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 "/my/file" Puppet::Util::Pidlock.expects(:new).with("/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 "/my/file" Puppet::Util::Pidlock.expects(:new).with("/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', :locked? => false Puppet::Util::Pidlock.expects(:new).with("/my/file").returns pidfile Puppet.settings.stubs(:value).with(:name).returns "eh" Puppet.settings.stubs(:value).with(:pidfile).returns "/my/file" pidfile.expects(:unlock).never @daemon.remove_pidfile end it "should unlock the pidfile using the Pidlock class" do pidfile = mock 'pidfile', :locked? => true Puppet::Util::Pidlock.expects(:new).with("/my/file").returns pidfile pidfile.expects(:unlock).returns true Puppet.settings.stubs(:value).with(:name).returns "eh" Puppet.settings.stubs(:value).with(:pidfile).returns "/my/file" @daemon.remove_pidfile end it "should warn if it cannot remove the pidfile" do pidfile = mock 'pidfile', :locked? => true Puppet::Util::Pidlock.expects(:new).with("/my/file").returns pidfile pidfile.expects(:unlock).returns false Puppet.settings.stubs(:value).with(:name).returns "eh" Puppet.settings.stubs(:value).with(:pidfile).returns "/my/file" Puppet.expects :err @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 = mock 'agent' - agent.expects(:running?).returns true + @agent.expects(:running?).returns true - @daemon.stubs(:agent).returns agent + @daemon.agent = @agent @daemon.reload end it "should run the agent if one is available and it is not running" do - agent = mock 'agent' - agent.expects(:running?).returns false - agent.expects :run + @agent.expects(:running?).returns false + @agent.expects :run - @daemon.stubs(:agent).returns agent + @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 = mock 'agent' - agent.expects(:running?).returns false - @daemon.stubs(:agent).returns agent + @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