summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Roberts <chrisroberts.code@gmail.com>2012-05-11 14:51:06 -0700
committerBryan McLellan <btm@opscode.com>2012-07-26 11:31:25 -0700
commiteab92691df5665994c888f3727eecc3577fb5f69 (patch)
tree9a5414ead047d6338fd669cf2e23f510b30f5557
parent43e2a3851bac0fa587ec71f594c3c9ea2cdfec1b (diff)
downloadchef-eab92691df5665994c888f3727eecc3577fb5f69.tar.gz
Converge via fork
-rw-r--r--chef/lib/chef/application/client.rb6
-rw-r--r--chef/lib/chef/application/solo.rb8
-rw-r--r--chef/lib/chef/client.rb121
-rw-r--r--chef/lib/chef/config.rb1
-rw-r--r--chef/spec/unit/client_spec.rb62
5 files changed, 135 insertions, 63 deletions
diff --git a/chef/lib/chef/application/client.rb b/chef/lib/chef/application/client.rb
index dffae827ac..e9408ce5fc 100644
--- a/chef/lib/chef/application/client.rb
+++ b/chef/lib/chef/application/client.rb
@@ -169,6 +169,12 @@ class Chef::Application::Client < Chef::Application
:long => '--why-run',
:description => 'Enable whyrun mode',
:boolean => true
+
+ option :client_fork,
+ :short => "-f",
+ :long => "--fork",
+ :description => "Fork client",
+ :boolean => true
attr_reader :chef_client_json
diff --git a/chef/lib/chef/application/solo.rb b/chef/lib/chef/application/solo.rb
index 93b7f0bb34..7ec6c36e76 100644
--- a/chef/lib/chef/application/solo.rb
+++ b/chef/lib/chef/application/solo.rb
@@ -132,7 +132,13 @@ class Chef::Application::Solo < Chef::Application
Chef::RunList::RunListItem.new(item)
}
}
-
+
+ option :client_fork,
+ :short => "-f",
+ :long => "--fork",
+ :description => "Fork client",
+ :boolean => true
+
option :why_run,
:short => '-W',
:long => '--why-run',
diff --git a/chef/lib/chef/client.rb b/chef/lib/chef/client.rb
index 2802163313..ed052c1fdd 100644
--- a/chef/lib/chef/client.rb
+++ b/chef/lib/chef/client.rb
@@ -153,62 +153,30 @@ class Chef
end
# Do a full run for this Chef::Client. Calls:
+ # * do_run
#
- # * run_ohai - Collect information about the system
- # * load_node - Get the last known stage from the server
- # * build_node - Merge state with local changes
- # * register - If not in solo mode, make sure the server knows about this client
- # * sync_cookbooks - If not in solo mode, populate the local cache with the node's cookbooks
- # * converge - Bring this system up to date
- #
+ # This provides a wrapper around #do_run allowing the
+ # run to be optionally forked.
# === Returns
- # true:: Always returns true.
+ # boolean:: Return value from #do_run. Should always returns true.
def run
- run_context = nil
-
- @events.run_start(Chef::VERSION)
- Chef::Log.info("*** Chef #{Chef::VERSION} ***")
- enforce_path_sanity
- run_ohai
- @events.ohai_completed(node)
- register unless Chef::Config[:solo]
-
- load_node
-
- begin
- build_node
-
- run_status.start_clock
- Chef::Log.info("Starting Chef Run for #{node.name}")
- run_started
-
- run_context = setup_run_context
-
- converge(run_context)
-
- save_updated_node
-
- run_status.stop_clock
- Chef::Log.info("Chef Run complete in #{run_status.elapsed_time} seconds")
- run_completed_successfully
- @events.run_completed(node)
+ if(Chef::Config[:client_fork] && Process.respond_to?(:fork))
+ Chef::Log.info "Forking chef instance to converge..."
+ pid = fork do
+ Chef::Log.info "Forked instance now converging"
+ do_run
+ exit
+ end
+ Chef::Log.info "Fork successful. Waiting for new chef pid: #{pid}"
+ result = Process.waitpid2(pid)
+ raise "Forked convergence run failed" unless result.last.success?
+ Chef::Log.info "Forked child successfully reaped (pid: #{pid})"
true
- rescue Exception => e
- run_status.stop_clock
- run_status.exception = e
- run_failed
- Chef::Log.debug("Re-raising exception: #{e.class} - #{e.message}\n#{e.backtrace.join("\n ")}")
- @events.run_failed(e)
- raise
- ensure
- run_status = nil
- run_context = nil
- GC.start
+ else
+ do_run
end
- true
end
-
# Configures the Chef::Cookbook::FileVendor class to fetch file from the
# server or disk as appropriate, creates the run context for this run, and
# sanity checks the cookbook collection.
@@ -412,6 +380,61 @@ class Chef
private
+ # Do a full run for this Chef::Client. Calls:
+ #
+ # * run_ohai - Collect information about the system
+ # * build_node - Get the last known state, merge with local changes
+ # * register - If not in solo mode, make sure the server knows about this client
+ # * sync_cookbooks - If not in solo mode, populate the local cache with the node's cookbooks
+ # * converge - Bring this system up to date
+ #
+ # === Returns
+ # true:: Always returns true.
+ def do_run
+ run_context = nil
+
+ @events.run_start(Chef::VERSION)
+ Chef::Log.info("*** Chef #{Chef::VERSION} ***")
+ enforce_path_sanity
+ run_ohai
+ @events.ohai_completed(node)
+ register unless Chef::Config[:solo]
+
+ load_node
+
+ begin
+ build_node
+
+ run_status.start_clock
+ Chef::Log.info("Starting Chef Run for #{node.name}")
+ run_started
+
+ run_context = setup_run_context
+
+ converge(run_context)
+
+ save_updated_node
+
+ run_status.stop_clock
+ Chef::Log.info("Chef Run complete in #{run_status.elapsed_time} seconds")
+ run_completed_successfully
+ @events.run_completed(node)
+ true
+ rescue Exception => e
+ run_status.stop_clock
+ run_status.exception = e
+ run_failed
+ Chef::Log.debug("Re-raising exception: #{e.class} - #{e.message}\n#{e.backtrace.join("\n ")}")
+ @events.run_failed(e)
+ raise
+ ensure
+ run_status = nil
+ run_context = nil
+ GC.start
+ end
+ true
+ end
+
# Ensures runlist override contains RunListItem instances
def runlist_override_sanity_check!
@override_runlist = @override_runlist.split(',') if @override_runlist.is_a?(String)
diff --git a/chef/lib/chef/config.rb b/chef/lib/chef/config.rb
index 65af681f9e..727314280f 100644
--- a/chef/lib/chef/config.rb
+++ b/chef/lib/chef/config.rb
@@ -196,6 +196,7 @@ class Chef
splay nil
why_run false
color false
+ client_fork false
# Set these to enable SSL authentication / mutual-authentication
# with the server
diff --git a/chef/spec/unit/client_spec.rb b/chef/spec/unit/client_spec.rb
index f7f74fa3b1..64bb8799de 100644
--- a/chef/spec/unit/client_spec.rb
+++ b/chef/spec/unit/client_spec.rb
@@ -24,7 +24,7 @@ require 'chef/run_context'
require 'chef/rest'
require 'rbconfig'
-describe Chef::Client do
+shared_examples_for Chef::Client do
before do
Chef::Log.logger = Logger.new(StringIO.new)
@@ -52,24 +52,26 @@ describe Chef::Client do
end
describe "run" do
+
it "should identify the node and run ohai, then register the client" do
mock_chef_rest_for_node = mock("Chef::REST (node)")
mock_chef_rest_for_client = mock("Chef::REST (client)")
mock_chef_rest_for_node_save = mock("Chef::REST (node save)")
mock_chef_runner = mock("Chef::Runner")
- # --Client#register
+ # --Client.register
# Make sure Client#register thinks the client key doesn't
# exist, so it tries to register and create one.
File.should_receive(:exists?).with(Chef::Config[:client_key]).exactly(1).times.and_return(false)
- # Client#register will register with the validation client name.
- Chef::REST.should_receive(:new).with(Chef::Config[:client_url], Chef::Config[:validation_client_name], Chef::Config[:validation_key]).and_return(mock_chef_rest_for_client)
- mock_chef_rest_for_client.should_receive(:register).with(@fqdn, Chef::Config[:client_key]).and_return(true)
- # Client#register will then turn around create another
+ # Client.register will register with the validation client name.
+ Chef::REST.should_receive(:new).with(Chef::Config[:client_url], Chef::Config[:validation_client_name], Chef::Config[:validation_key]).exactly(1).and_return(mock_chef_rest_for_client)
+ mock_chef_rest_for_client.should_receive(:register).with(@fqdn, Chef::Config[:client_key]).exactly(1).and_return(true)
+ # Client.register will then turn around create another
+
# Chef::REST object, this time with the client key it got from the
# previous step.
- Chef::REST.should_receive(:new).with(Chef::Config[:chef_server_url], @fqdn, Chef::Config[:client_key]).and_return(mock_chef_rest_for_node)
+ Chef::REST.should_receive(:new).with(Chef::Config[:chef_server_url], @fqdn, Chef::Config[:client_key]).exactly(1).and_return(mock_chef_rest_for_node)
# --Client#build_node
# looks up the node, which we will return, then later saves it.
@@ -98,19 +100,42 @@ describe Chef::Client do
Chef::REST.should_receive(:new).with(Chef::Config[:chef_server_url]).and_return(mock_chef_rest_for_node_save)
mock_chef_rest_for_node_save.should_receive(:put_rest).with("nodes/#{@fqdn}", @node).and_return(true)
+ # Post conditions: check that node has been filled in correctly
@client.should_receive(:run_started)
@client.should_receive(:run_completed_successfully)
-
+ if(Chef::Config[:client_fork])
+ require 'stringio'
+ if(Chef::Config[:pipe_node])
+ pipe_sim = StringIO.new
+ pipe_sim.should_receive(:close).exactly(4).and_return(nil)
+ res = ''
+ pipe_sim.should_receive(:puts) do |string|
+ res.replace(string)
+ end
+ pipe_sim.should_receive(:gets).and_return(res)
+ Chef::CouchDB.should_receive(:new).and_return(nil)
+ IO.should_receive(:pipe).and_return([pipe_sim, pipe_sim])
+ IO.should_receive(:select).and_return(true)
+ end
+ proc_ret = Class.new.new
+ proc_ret.should_receive(:success?).and_return(true)
+ Process.should_receive(:waitpid2).and_return([1, proc_ret])
+ @client.should_receive(:exit).and_return(nil)
+ @client.should_receive(:fork) do |&block|
+ block.call
+ end
+ end
+
# This is what we're testing.
@client.run
-
- # Post conditions: check that node has been filled in correctly
- @node.automatic_attrs[:platform].should == "example-platform"
- @node.automatic_attrs[:platform_version].should == "example-platform-1.0"
+ if(!Chef::Config[:client_fork] || Chef::Config[:pipe_node])
+ @node.automatic_attrs[:platform].should == "example-platform"
+ @node.automatic_attrs[:platform_version].should == "example-platform-1.0"
+ end
end
-
+
describe "when notifying other objects of the status of the chef run" do
before do
Chef::Client.clear_notifications
@@ -224,3 +249,14 @@ describe Chef::Client do
end
end
+
+describe Chef::Client do
+ it_behaves_like Chef::Client
+end
+
+describe "Chef::Client Forked" do
+ it_behaves_like Chef::Client
+ before do
+ Chef::Config[:client_fork] = true
+ end
+end