diff --git a/lib/puppet/application/run.rb b/lib/puppet/application/run.rb index f08fade8d..26ca362ff 100644 --- a/lib/puppet/application/run.rb +++ b/lib/puppet/application/run.rb @@ -1,210 +1,212 @@ 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 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 - client = Puppet::Network::Client.runner.new( - :Server => host, - :Port => Puppet[:puppetport] - ) + + 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 - result = client.run(@tags, options[:ignoreschedules] || false, options[:foreground] || false) + request = Puppet::Indirector::Request.new(:run, :save, url) # Yuck. + run_options = { + :tags => @tags, + :background => ! options[:foreground], + :ignoreschedules => options[:ignoreschedules] + } + run = Puppet::Run.new( run_options ).save( request ) + result = run.status 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 - if @tags.empty? - @tags = "" - else - @tags = @tags.join(",") - 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/lib/puppet/indirector/run/local.rb b/lib/puppet/indirector/run/local.rb new file mode 100644 index 000000000..5e8f349ee --- /dev/null +++ b/lib/puppet/indirector/run/local.rb @@ -0,0 +1,8 @@ +require 'puppet/run' +require 'puppet/indirector/code' + +class Puppet::Run::Local < Puppet::Indirector::Code + def save( request ) + request.instance.run + end +end diff --git a/lib/puppet/run.rb b/lib/puppet/run.rb index 1503f5d7f..20e21dc57 100644 --- a/lib/puppet/run.rb +++ b/lib/puppet/run.rb @@ -1,65 +1,80 @@ require 'puppet/agent' require 'puppet/configurer' require 'puppet/indirector' # A basic class for running the agent. Used by # puppetrun to kick off agents remotely. class Puppet::Run extend Puppet::Indirector - indirects :runner, :terminus_class => :rest + indirects :run, :terminus_class => :local attr_reader :status, :background, :options def agent Puppet::Agent.new(Puppet::Configurer) end def background? background end def initialize(options = {}) if options.include?(:background) @background = options[:background] options.delete(:background) end valid_options = [:tags, :ignoreschedules] options.each do |key, value| - raise ArgumentError, "Runner does not accept %s" % key unless valid_options.include?(key) + raise ArgumentError, "Run does not accept %s" % key unless valid_options.include?(key) end @options = options end def log_run msg = "" msg += "triggered run" % if options[:tags] - msg += " with tags %s" % options[:tags] + msg += " with tags #{options[:tags].inspect}" end if options[:ignoreschedules] msg += " ignoring schedules" end Puppet.notice msg end def run if agent.running? @status = "running" - return + return self end log_run() if background? Thread.new { agent.run(options) } else agent.run(options) end @status = "success" + + self + end + + def self.from_pson( pson ) + options = {} + pson.each do |key, value| + options[key.to_sym] = value + end + + new(options) + end + + def to_pson + @options.merge(:background => @background).to_pson end end diff --git a/spec/unit/application/puppetrun.rb b/spec/unit/application/puppetrun.rb index 271fd6ca4..ce3af5a5b 100755 --- a/spec/unit/application/puppetrun.rb +++ b/spec/unit/application/puppetrun.rb @@ -1,289 +1,292 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/util/ldap/connection' require 'puppet/application/run' describe "run" do before :each do Puppet::Util::Ldap::Connection.stubs(:new).returns(stub_everything) @run = Puppet::Application[:run] 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 end it "should declare a main command" do @run.should respond_to(:main) end it "should declare a test command" do @run.should respond_to(:test) end it "should declare a preinit block" do @run.should respond_to(:run_preinit) end describe "during preinit" do before :each do @run.stubs(:trap) end it "should catch INT and TERM" do @run.stubs(:trap).with { |arg,block| arg == :INT or arg == :TERM } @run.run_preinit end it "should set parallel option to 1" do @run.run_preinit @run.options[:parallel].should == 1 end it "should set verbose by default" do @run.run_preinit @run.options[:verbose].should be_true end it "should set fqdn by default" do @run.run_preinit @run.options[:fqdn].should be_true end it "should set ignoreschedules to 'false'" do @run.run_preinit @run.options[:ignoreschedules].should be_false end it "should set foreground to 'false'" do @run.run_preinit @run.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) end it "should store argument value when calling handle_#{option}" do @run.options.expects(:[]=).with(option, 'arg') @run.send("handle_#{option}".to_sym, 'arg') end end it "should add to the host list with the host option" do @run.handle_host('host') @run.hosts.should == ['host'] end it "should add to the tag list with the tag option" do @run.handle_tag('tag') @run.tags.should == ['tag'] end it "should add to the class list with the class option" do @run.handle_class('class') @run.classes.should == ['class'] end end describe "during setup" do before :each do @run.classes = [] @run.tags = [] @run.hosts = [] Puppet::Log.stubs(:level=) @run.stubs(:trap) @run.stubs(:puts) Puppet.stubs(:parse_config) @run.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) Puppet::Log.expects(:level=).with(:debug) @run.run_setup end it "should set log level to info if --verbose was passed" do @run.options.stubs(:[]).with(:verbose).returns(true) Puppet::Log.expects(:level=).with(:info) @run.run_setup end it "should Parse puppet config" do Puppet.expects(:parse_config) @run.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) Puppet::Node.expects(:search).with("whatever",:fqdn => :something).returns([]) @run.run_setup end it "should search for all nodes if --all" do @run.options.stubs(:[]).with(:all).returns(true) @run.stubs(:puts) Puppet::Node.expects(:search).with("whatever",:fqdn => nil).returns([]) @run.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'] Puppet::Node.expects(:search).with("whatever", :class => "class", :fqdn => nil).returns([]) @run.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'] @run.expects(:exit).with(24) @run.run_setup end end end describe "when running" do before :each do @run.stubs(:puts) end it "should dispatch to test if --test is used" do @run.options.stubs(:[]).with(:test).returns(true) @run.get_command.should == :test end it "should dispatch to main if --test is not used" do @run.options.stubs(:[]).with(:test).returns(false) @run.get_command.should == :main end describe "the test command" do it "should exit with exit code 0 " do @run.expects(:exit).with(0) @run.test end end describe "the main command" do before :each do - @client = stub_everything 'client' - @client.stubs(:run).returns("success") - Puppet::Network::Client.runner.stubs(:new).returns(@client) @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) $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) 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) lambda { @run.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 Process.stubs(:wait).returns(1).then.raises(Errno::ECHILD) @run.expects(:run_for_host).times(2) lambda { @run.main }.should raise_error end describe "during call of run_for_host" do - it "should create a Runner Client per given host" do - Puppet::Network::Client.runner.expects(:new).returns(@client) - - @run.run_for_host('host') + before do + require 'puppet/run' + options = { + :background => true, :ignoreschedules => false, :tags => [] + } + @run = Puppet::Run.new( options.dup ) + @run.stubs(:status).returns("success") + + Puppet::Run.indirection.expects(:terminus_class=).with( :rest ) + Puppet::Run.expects(:new).with( options ).returns(@run) end - it "should call Client.run for the given host" do - @client.expects(:run) + it "should call run on a Puppet::Run for the given host" do + @run.expects(:save).with{|req| req.uri == 'https://host:8139/production/run/host'}.returns(@run) @run.run_for_host('host') end it "should exit the child with 0 on success" do - @client.stubs(:run).returns("success") + @run.stubs(:status).returns("success") @run.expects(:exit).with(0) @run.run_for_host('host') end it "should exit the child with 3 on running" do - @client.stubs(:run).returns("running") + @run.stubs(:status).returns("running") @run.expects(:exit).with(3) @run.run_for_host('host') end it "should exit the child with 12 on unknown answer" do - @client.stubs(:run).returns("whatever") + @run.stubs(:status).returns("whatever") @run.expects(:exit).with(12) @run.run_for_host('host') end end end end end diff --git a/spec/unit/indirector/run/local.rb b/spec/unit/indirector/run/local.rb new file mode 100644 index 000000000..face61d5c --- /dev/null +++ b/spec/unit/indirector/run/local.rb @@ -0,0 +1,20 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../../spec_helper' + +require 'puppet/indirector/run/local' + +describe Puppet::Run::Local do + it "should be a sublcass of Puppet::Indirector::Code" do + Puppet::Run::Local.superclass.should equal(Puppet::Indirector::Code) + end + + it "should call runner.run on save and return the runner" do + runner = Puppet::Run.new + runner.stubs(:run).returns(runner) + + request = Puppet::Indirector::Request.new(:indirection, :save, "anything") + request.instance = runner = Puppet::Run.new + Puppet::Run::Local.new.save(request).should == runner + end +end diff --git a/spec/unit/indirector/run/rest.rb b/spec/unit/indirector/run/rest.rb index e07fe7fc2..ee976ed9f 100755 --- a/spec/unit/indirector/run/rest.rb +++ b/spec/unit/indirector/run/rest.rb @@ -1,11 +1,11 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' -require 'puppet/indirector/runner/rest' +require 'puppet/indirector/run/rest' describe Puppet::Run::Rest do it "should be a sublcass of Puppet::Indirector::REST" do Puppet::Run::Rest.superclass.should equal(Puppet::Indirector::REST) end end diff --git a/spec/unit/run.rb b/spec/unit/run.rb index 57eff0f98..4c5f6b1af 100755 --- a/spec/unit/run.rb +++ b/spec/unit/run.rb @@ -1,118 +1,134 @@ #!/usr/bin/env ruby -require File.dirname(__FILE__) + '/../../spec_helper' +require File.dirname(__FILE__) + '/../spec_helper' require 'puppet/agent' require 'puppet/run' describe Puppet::Run do before do @runner = Puppet::Run.new end - it "should indirect :runner" do - Puppet::Run.indirection.name.should == :runner + it "should indirect :run" do + Puppet::Run.indirection.name.should == :run end it "should use a configurer agent as its agent" do agent = mock 'agent' Puppet::Agent.expects(:new).with(Puppet::Configurer).returns agent @runner.agent.should equal(agent) end it "should accept options at initialization" do lambda { Puppet::Run.new :background => true }.should_not raise_error end it "should default to running in the foreground" do Puppet::Run.new.should_not be_background end it "should default to its options being an empty hash" do Puppet::Run.new.options.should == {} end it "should accept :tags for the agent" do Puppet::Run.new(:tags => "foo").options[:tags].should == "foo" end it "should accept :ignoreschedules for the agent" do Puppet::Run.new(:ignoreschedules => true).options[:ignoreschedules].should be_true end it "should accept an option to configure it to run in the background" do Puppet::Run.new(:background => true).should be_background end it "should retain the background option" do Puppet::Run.new(:background => true).options[:background].should be_nil end it "should not accept arbitrary options" do lambda { Puppet::Run.new(:foo => true) }.should raise_error(ArgumentError) end describe "when asked to run" do before do @agent = stub 'agent', :run => nil, :running? => false @runner.stubs(:agent).returns @agent end it "should run its agent" do agent = stub 'agent2', :running? => false @runner.stubs(:agent).returns agent agent.expects(:run) @runner.run end it "should pass any of its options on to the agent" do @runner.stubs(:options).returns(:foo => :bar) @agent.expects(:run).with(:foo => :bar) @runner.run end it "should log its run using the provided options" do @runner.expects(:log_run) @runner.run end it "should set its status to 'already_running' if the agent is already running" do @agent.expects(:running?).returns true @runner.run @runner.status.should == "running" end it "should set its status to 'success' if the agent is run" do @agent.expects(:running?).returns false @runner.run @runner.status.should == "success" end it "should run the agent in a thread if asked to run it in the background" do Thread.expects(:new) @runner.expects(:background?).returns true @agent.expects(:run).never # because our thread didn't yield @runner.run end it "should run the agent directly if asked to run it in the foreground" do Thread.expects(:new).never @runner.expects(:background?).returns false @agent.expects(:run) @runner.run end end + + describe ".from_pson" do + it "should accept a hash of options, and pass them with symbolified keys to new" do + options = { + "tags" => "whatever", + "background" => true, + } + + Puppet::Run.expects(:new).with({ + :tags => "whatever", + :background => true, + }) + + Puppet::Run.from_pson(options) + end + end end