diff --git a/lib/puppet/application/run.rb b/lib/puppet/application/kick.rb similarity index 99% rename from lib/puppet/application/run.rb rename to lib/puppet/application/kick.rb index 395ba987b..7b4993bb4 100644 --- a/lib/puppet/application/run.rb +++ b/lib/puppet/application/kick.rb @@ -1,214 +1,214 @@ require 'puppet' require 'puppet/application' Puppet.warning "RubyGems not installed" unless Puppet.features.rubygems? Puppet.warning "Failed to load ruby LDAP library. LDAP functionality will not be available" unless Puppet.features.ldap? -Puppet::Application.new(:run) do +Puppet::Application.new(:kick) do should_not_parse_config attr_accessor :hosts, :tags, :classes option("--all","-a") option("--foreground","-f") option("--debug","-d") option("--ping","-P") option("--test") option("--host HOST") do |arg| @hosts << arg end option("--tag TAG", "-t") do |arg| @tags << arg end option("--class CLASS", "-c") do |arg| @classes << arg end option("--no-fqdn", "-n") do |arg| options[:fqdn] = false end option("--parallel PARALLEL", "-p") do |arg| begin options[:parallel] = Integer(arg) rescue $stderr.puts "Could not convert %s to an integer" % arg.inspect exit(23) end end dispatch do options[:test] ? :test : :main end command(:test) do puts "Skipping execution in test mode" exit(0) end command(:main) do require 'puppet/network/client' require 'puppet/util/ldap/connection' todo = @hosts.dup failures = [] # Now do the actual work go = true while go # If we don't have enough children in process and we still have hosts left to # do, then do the next host. if @children.length < options[:parallel] and ! todo.empty? host = todo.shift pid = fork do run_for_host(host) end @children[pid] = host else # Else, see if we can reap a process. begin pid = Process.wait if host = @children[pid] # Remove our host from the list of children, so the parallelization # continues working. @children.delete(pid) if $?.exitstatus != 0 failures << host end print "%s finished with exit code %s\n" % [host, $?.exitstatus] else $stderr.puts "Could not find host for PID %s with status %s" % [pid, $?.exitstatus] end rescue Errno::ECHILD # There are no children left, so just exit unless there are still # children left to do. next unless todo.empty? if failures.empty? puts "Finished" exit(0) else puts "Failed: %s" % failures.join(", ") exit(3) end end end end end def run_for_host(host) if options[:ping] out = %x{ping -c 1 #{host}} unless $? == 0 $stderr.print "Could not contact %s\n" % host next end end require 'puppet/run' Puppet::Run.indirection.terminus_class = :rest port = Puppet[:puppetport] url = ["https://#{host}:#{port}", "production", "run", host].join('/') print "Triggering %s\n" % host begin run_options = { :tags => @tags, :background => ! options[:foreground], :ignoreschedules => options[:ignoreschedules] } run = Puppet::Run.new( run_options ).save( url ) puts "Getting status" result = run.status puts "status is #{result}" rescue => detail puts detail.backtrace if Puppet[:trace] $stderr.puts "Host %s failed: %s\n" % [host, detail] exit(2) end case result when "success"; exit(0) when "running" $stderr.puts "Host %s is already running" % host exit(3) else $stderr.puts "Host %s returned unknown answer '%s'" % [host, result] exit(12) end end preinit do [:INT, :TERM].each do |signal| trap(signal) do $stderr.puts "Cancelling" exit(1) end end options[:parallel] = 1 options[:verbose] = true options[:fqdn] = true options[:ignoreschedules] = false options[:foreground] = false @hosts = [] @classes = [] @tags = [] end setup do if options[:debug] Puppet::Util::Log.level = :debug else Puppet::Util::Log.level = :info end # Now parse the config Puppet.parse_config if Puppet[:node_terminus] == "ldap" and (options[:all] or @classes) if options[:all] @hosts = Puppet::Node.search("whatever", :fqdn => options[:fqdn]).collect { |node| node.name } puts "all: %s" % @hosts.join(", ") else @hosts = [] @classes.each do |klass| list = Puppet::Node.search("whatever", :fqdn => options[:fqdn], :class => klass).collect { |node| node.name } puts "%s: %s" % [klass, list.join(", ")] @hosts += list end end elsif ! @classes.empty? $stderr.puts "You must be using LDAP to specify host classes" exit(24) end @children = {} # If we get a signal, then kill all of our children and get out. [:INT, :TERM].each do |signal| trap(signal) do Puppet.notice "Caught #{signal}; shutting down" @children.each do |pid, host| Process.kill("INT", pid) end waitall exit(1) end end end end diff --git a/sbin/puppetrun b/sbin/puppetrun index 6dc8fd25a..641c1bb78 100755 --- a/sbin/puppetrun +++ b/sbin/puppetrun @@ -1,130 +1,130 @@ #!/usr/bin/env ruby # # = Synopsis # # Trigger a puppetd run on a set of hosts. # # = Usage # # puppetrun [-a|--all] [-c|--class ] [-d|--debug] [-f|--foreground] # [-h|--help] [--host ] [--no-fqdn] [--ignoreschedules] # [-t|--tag ] [--test] [-p|--ping] # # = Description # # This script can be used to connect to a set of machines running +puppetd+ # and trigger them to run their configurations. The most common usage would # be to specify a class of hosts and a set of tags, and +puppetrun+ would # look up in LDAP all of the hosts matching that class, then connect to # each host and trigger a run of all of the objects with the specified tags. # # If you are not storing your host configurations in LDAP, you can specify # hosts manually. # # You will most likely have to run +puppetrun+ as root to get access to # the SSL certificates. # # +puppetrun+ reads +puppetmaster+'s configuration file, so that it can copy # things like LDAP settings. # # = Usage Notes # # +puppetrun+ is useless unless +puppetd+ is listening. See its documentation # for more information, but the gist is that you must enable +listen+ on the # +puppetd+ daemon, either using +--listen+ on the command line or adding # 'listen: true' in its config file. In addition, you need to set the daemons # up to specifically allow connections by creating the +namespaceauth+ file, # normally at '/etc/puppet/namespaceauth.conf'. This file specifies who has # access to each namespace; if you create the file you must add every namespace # you want any Puppet daemon to allow -- it is currently global to all Puppet # daemons. # # An example file looks like this:: # # [fileserver] # allow *.madstop.com # # [puppetmaster] # allow *.madstop.com # # [puppetrunner] # allow culain.madstop.com # # This is what you would install on your Puppet master; non-master hosts could # leave off the 'fileserver' and 'puppetmaster' namespaces. # # Expect more documentation on this eventually. # # = Options # # Note that any configuration parameter that's valid in the configuration file # is also a valid long argument. For example, 'ssldir' is a valid configuration # parameter, so you can specify '--ssldir ' as an argument. # # See the configuration file documentation at # http://reductivelabs.com/projects/puppet/reference/configref.html for # the full list of acceptable parameters. A commented list of all # configuration options can also be generated by running puppetmasterdd with # '--genconfig'. # # # all:: # Connect to all available hosts. Requires LDAP support at this point. # # class:: # Specify a class of machines to which to connect. This only works if you # have LDAP configured, at the moment. # # debug:: # Enable full debugging. # # foreground:: # Run each configuration in the foreground; that is, when connecting to a host, # do not return until the host has finished its run. The default is false. # # help:: # Print this help message # # host:: # A specific host to which to connect. This flag can be specified more # than once. # # ignoreschedules:: # Whether the client should ignore schedules when running its configuration. # This can be used to force the client to perform work it would not normally # perform so soon. The default is false. # # parallel:: # How parallel to make the connections. Parallelization is provided by forking # for each client to which to connect. The default is 1, meaning serial execution. # # tag:: # Specify a tag for selecting the objects to apply. Does not work with the # --test option. # # # test:: # Print the hosts you would connect to but do not actually connect. This # option requires LDAP support at this point. # # ping:: # # Do a ICMP echo against the target host. Skip hosts that don't respond to ping. # # = Example # # sudo puppetrun -p 10 --host host1 --host host2 -t remotefile -t webserver # # = Author # # Luke Kanies # # = Copyright # # Copyright (c) 2005 Reductive Labs, LLC # Licensed under the GNU Public License -require 'puppet/application/run' -Puppet::Application[:run].run +require 'puppet/application/kick' +Puppet::Application[:kick].run diff --git a/spec/unit/application/run.rb b/spec/unit/application/kick.rb similarity index 56% rename from spec/unit/application/run.rb rename to spec/unit/application/kick.rb index bb76c5b59..b617537d4 100755 --- a/spec/unit/application/run.rb +++ b/spec/unit/application/kick.rb @@ -1,292 +1,293 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/util/ldap/connection' -require 'puppet/application/run' +require 'puppet/application/kick' -describe "run" do +describe Puppet::Application[:kick] do before :each do Puppet::Util::Ldap::Connection.stubs(:new).returns(stub_everything) - @run = Puppet::Application[:run] + @kick = Puppet::Application[:kick] Puppet::Util::Log.stubs(:newdestination) Puppet::Util::Log.stubs(:level=) end it "should ask Puppet::Application to not parse Puppet configuration file" do - @run.should_parse_config?.should be_false + p @kick.object_id + @kick.should_parse_config?.should be_false end it "should declare a main command" do - @run.should respond_to(:main) + @kick.should respond_to(:main) end it "should declare a test command" do - @run.should respond_to(:test) + @kick.should respond_to(:test) end it "should declare a preinit block" do - @run.should respond_to(:run_preinit) + @kick.should respond_to(:run_preinit) end describe "during preinit" do before :each do - @run.stubs(:trap) + @kick.stubs(:trap) end it "should catch INT and TERM" do - @run.stubs(:trap).with { |arg,block| arg == :INT or arg == :TERM } + @kick.stubs(:trap).with { |arg,block| arg == :INT or arg == :TERM } - @run.run_preinit + @kick.run_preinit end it "should set parallel option to 1" do - @run.run_preinit + @kick.run_preinit - @run.options[:parallel].should == 1 + @kick.options[:parallel].should == 1 end it "should set verbose by default" do - @run.run_preinit + @kick.run_preinit - @run.options[:verbose].should be_true + @kick.options[:verbose].should be_true end it "should set fqdn by default" do - @run.run_preinit + @kick.run_preinit - @run.options[:fqdn].should be_true + @kick.options[:fqdn].should be_true end it "should set ignoreschedules to 'false'" do - @run.run_preinit + @kick.run_preinit - @run.options[:ignoreschedules].should be_false + @kick.options[:ignoreschedules].should be_false end it "should set foreground to 'false'" do - @run.run_preinit + @kick.run_preinit - @run.options[:foreground].should be_false + @kick.options[:foreground].should be_false end end describe "when applying options" do [:all, :foreground, :debug, :ping, :test].each do |option| it "should declare handle_#{option} method" do - @run.should respond_to("handle_#{option}".to_sym) + @kick.should respond_to("handle_#{option}".to_sym) end it "should store argument value when calling handle_#{option}" do - @run.options.expects(:[]=).with(option, 'arg') - @run.send("handle_#{option}".to_sym, 'arg') + @kick.options.expects(:[]=).with(option, 'arg') + @kick.send("handle_#{option}".to_sym, 'arg') end end it "should add to the host list with the host option" do - @run.handle_host('host') + @kick.handle_host('host') - @run.hosts.should == ['host'] + @kick.hosts.should == ['host'] end it "should add to the tag list with the tag option" do - @run.handle_tag('tag') + @kick.handle_tag('tag') - @run.tags.should == ['tag'] + @kick.tags.should == ['tag'] end it "should add to the class list with the class option" do - @run.handle_class('class') + @kick.handle_class('class') - @run.classes.should == ['class'] + @kick.classes.should == ['class'] end end describe "during setup" do before :each do - @run.classes = [] - @run.tags = [] - @run.hosts = [] + @kick.classes = [] + @kick.tags = [] + @kick.hosts = [] Puppet::Log.stubs(:level=) - @run.stubs(:trap) - @run.stubs(:puts) + @kick.stubs(:trap) + @kick.stubs(:puts) Puppet.stubs(:parse_config) - @run.options.stubs(:[]).with(any_parameters) + @kick.options.stubs(:[]).with(any_parameters) end it "should set log level to debug if --debug was passed" do - @run.options.stubs(:[]).with(:debug).returns(true) + @kick.options.stubs(:[]).with(:debug).returns(true) Puppet::Log.expects(:level=).with(:debug) - @run.run_setup + @kick.run_setup end it "should set log level to info if --verbose was passed" do - @run.options.stubs(:[]).with(:verbose).returns(true) + @kick.options.stubs(:[]).with(:verbose).returns(true) Puppet::Log.expects(:level=).with(:info) - @run.run_setup + @kick.run_setup end it "should Parse puppet config" do Puppet.expects(:parse_config) - @run.run_setup + @kick.run_setup end describe "when using the ldap node terminus" do before :each do Puppet.stubs(:[]).with(:node_terminus).returns("ldap") end it "should pass the fqdn option to search" do - @run.options.stubs(:[]).with(:fqdn).returns(:something) - @run.options.stubs(:[]).with(:all).returns(true) - @run.stubs(:puts) + @kick.options.stubs(:[]).with(:fqdn).returns(:something) + @kick.options.stubs(:[]).with(:all).returns(true) + @kick.stubs(:puts) Puppet::Node.expects(:search).with("whatever",:fqdn => :something).returns([]) - @run.run_setup + @kick.run_setup end it "should search for all nodes if --all" do - @run.options.stubs(:[]).with(:all).returns(true) - @run.stubs(:puts) + @kick.options.stubs(:[]).with(:all).returns(true) + @kick.stubs(:puts) Puppet::Node.expects(:search).with("whatever",:fqdn => nil).returns([]) - @run.run_setup + @kick.run_setup end it "should search for nodes including given classes" do - @run.options.stubs(:[]).with(:all).returns(false) - @run.stubs(:puts) - @run.classes = ['class'] + @kick.options.stubs(:[]).with(:all).returns(false) + @kick.stubs(:puts) + @kick.classes = ['class'] Puppet::Node.expects(:search).with("whatever", :class => "class", :fqdn => nil).returns([]) - @run.run_setup + @kick.run_setup end end describe "when using regular nodes" do it "should fail if some classes have been specified" do $stderr.stubs(:puts) - @run.classes = ['class'] + @kick.classes = ['class'] - @run.expects(:exit).with(24) + @kick.expects(:exit).with(24) - @run.run_setup + @kick.run_setup end end end describe "when running" do before :each do - @run.stubs(:puts) + @kick.stubs(:puts) end it "should dispatch to test if --test is used" do - @run.options.stubs(:[]).with(:test).returns(true) + @kick.options.stubs(:[]).with(:test).returns(true) - @run.get_command.should == :test + @kick.get_command.should == :test end it "should dispatch to main if --test is not used" do - @run.options.stubs(:[]).with(:test).returns(false) + @kick.options.stubs(:[]).with(:test).returns(false) - @run.get_command.should == :main + @kick.get_command.should == :main end describe "the test command" do it "should exit with exit code 0 " do - @run.expects(:exit).with(0) + @kick.expects(:exit).with(0) - @run.test + @kick.test end end describe "the main command" do before :each do - @run.options.stubs(:[]).with(:parallel).returns(1) - @run.options.stubs(:[]).with(:ping).returns(false) - @run.options.stubs(:[]).with(:ignoreschedules).returns(false) - @run.options.stubs(:[]).with(:foreground).returns(false) - @run.stubs(:print) - @run.stubs(:exit) + @kick.options.stubs(:[]).with(:parallel).returns(1) + @kick.options.stubs(:[]).with(:ping).returns(false) + @kick.options.stubs(:[]).with(:ignoreschedules).returns(false) + @kick.options.stubs(:[]).with(:foreground).returns(false) + @kick.stubs(:print) + @kick.stubs(:exit) $stderr.stubs(:puts) end it "should create as much childs as --parallel" do - @run.options.stubs(:[]).with(:parallel).returns(3) - @run.hosts = ['host1', 'host2', 'host3'] - @run.stubs(:exit).raises(SystemExit) + @kick.options.stubs(:[]).with(:parallel).returns(3) + @kick.hosts = ['host1', 'host2', 'host3'] + @kick.stubs(:exit).raises(SystemExit) Process.stubs(:wait).returns(1).then.returns(2).then.returns(3).then.raises(Errno::ECHILD) - @run.expects(:fork).times(3).returns(1).then.returns(2).then.returns(3) + @kick.expects(:fork).times(3).returns(1).then.returns(2).then.returns(3) - lambda { @run.main }.should raise_error + lambda { @kick.main }.should raise_error end it "should delegate to run_for_host per host" do - @run.hosts = ['host1', 'host2'] - @run.stubs(:exit).raises(SystemExit) - @run.stubs(:fork).returns(1).yields + @kick.hosts = ['host1', 'host2'] + @kick.stubs(:exit).raises(SystemExit) + @kick.stubs(:fork).returns(1).yields Process.stubs(:wait).returns(1).then.raises(Errno::ECHILD) - @run.expects(:run_for_host).times(2) + @kick.expects(:run_for_host).times(2) - lambda { @run.main }.should raise_error + lambda { @kick.main }.should raise_error end describe "during call of run_for_host" do before do require 'puppet/run' options = { :background => true, :ignoreschedules => false, :tags => [] } @agent_run = Puppet::Run.new( options.dup ) @agent_run.stubs(:status).returns("success") Puppet::Run.indirection.expects(:terminus_class=).with( :rest ) Puppet::Run.expects(:new).with( options ).returns(@agent_run) end it "should call run on a Puppet::Run for the given host" do @agent_run.expects(:save).with('https://host:8139/production/run/host').returns(@agent_run) - @run.run_for_host('host') + @kick.run_for_host('host') end it "should exit the child with 0 on success" do @agent_run.stubs(:status).returns("success") - @run.expects(:exit).with(0) + @kick.expects(:exit).with(0) - @run.run_for_host('host') + @kick.run_for_host('host') end it "should exit the child with 3 on running" do @agent_run.stubs(:status).returns("running") - @run.expects(:exit).with(3) + @kick.expects(:exit).with(3) - @run.run_for_host('host') + @kick.run_for_host('host') end it "should exit the child with 12 on unknown answer" do @agent_run.stubs(:status).returns("whatever") - @run.expects(:exit).with(12) + @kick.expects(:exit).with(12) - @run.run_for_host('host') + @kick.run_for_host('host') end end end end end