summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordanielsdeleo <dan@opscode.com>2012-12-18 10:47:24 -0800
committerdanielsdeleo <dan@opscode.com>2012-12-18 17:10:05 -0800
commita61c9865684466adcc9c933f05eebc1090624ea0 (patch)
tree224e1719aa7a49a2cd3e1e3c7265d204701121b5
parent2de707d27251f38790a71043843749a570302aaf (diff)
downloadchef-a61c9865684466adcc9c933f05eebc1090624ea0.tar.gz
[CHEF-3689] Extract registration to a class
-rw-r--r--lib/chef/api_client.rb5
-rw-r--r--lib/chef/api_client/registration.rb110
-rw-r--r--spec/unit/api_client/registration_spec.rb171
-rw-r--r--spec/unit/api_client_spec.rb12
4 files changed, 282 insertions, 16 deletions
diff --git a/lib/chef/api_client.rb b/lib/chef/api_client.rb
index 30ea698e17..6c7132d28f 100644
--- a/lib/chef/api_client.rb
+++ b/lib/chef/api_client.rb
@@ -206,11 +206,6 @@ class Chef
@http_api ||= Chef::REST.new(Chef::Config[:chef_server_url])
end
- def http_api_as_validator
- @http_api_as_validator ||= Chef::REST.new(Chef::Config[:chef_server_url],
- Chef::Config[:validation_client_name],
- Chef::Config[:validation_key])
- end
end
end
diff --git a/lib/chef/api_client/registration.rb b/lib/chef/api_client/registration.rb
new file mode 100644
index 0000000000..d961e23ac2
--- /dev/null
+++ b/lib/chef/api_client/registration.rb
@@ -0,0 +1,110 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/config'
+require 'chef/rest'
+require 'chef/exceptions'
+
+class Chef
+ class ApiClient
+
+ # ==Chef::ApiClient::Registration
+ # Manages the process of creating or updating a Chef::ApiClient on the
+ # server and writing the resulting private key to disk. Registration uses
+ # the validator credentials for its API calls. This allows it to bootstrap
+ # a new client/node identity by borrowing the validator client identity
+ # when creating a new client.
+ class Registration
+ attr_reader :private_key
+ attr_reader :destination
+ attr_reader :name
+
+ def initialize(name, destination)
+ @name = name
+ @destination = destination
+ @private_key = nil
+ end
+
+ # Runs the client registration process, including creating the client on
+ # the chef-server and writing its private key to disk.
+ def run
+ assert_destination_writable!
+ retries = Config[:client_registration_retries] || 5
+ begin
+ create_or_update
+ rescue Net::HTTPFatalError => e
+ # only retry 500s
+ raise if retries <= 0 or e.response.code != "500"
+ retries -= 1
+ Chef::Log.warn("Failed to register new client, #{retries} tries remaining")
+ retry
+ end
+ write_key
+ end
+
+ def assert_destination_writable!
+ if (File.exists?(destination) && !File.writable?(destination))
+ raise Chef::Exceptions::CannotWritePrivateKey, "I cannot write your private key to #{destination} - check permissions?"
+ end
+ end
+
+ def write_key
+ ::File.open(destination, File::CREAT|File::TRUNC|File::RDWR|File::NOFOLLOW, 0600) do |f|
+ f.print(private_key)
+ end
+ rescue IOError => e
+ raise Chef::Exceptions::CannotWritePrivateKey, "Error writing private key to #{destination}: #{e}"
+ end
+
+ def create_or_update
+ create
+ rescue Net::HTTPServerException => e
+ # If create fails because the client exists, attempt to update. This
+ # requires admin privileges.
+ raise unless e.response.code == "409"
+ update
+ end
+
+ def create
+ response = http_api.post("clients", :name => name, :admin => false)
+ @private_key = response["private_key"]
+ response
+ end
+
+ def update
+ response = http_api.put("clients/#{name}", :name => name,
+ :admin => false,
+ :private_key => true)
+ if response.respond_to?(:private_key) # Chef 11
+ @private_key = response.private_key
+ else # Chef 10
+ @private_key = response["private_key"]
+ end
+ response
+ end
+
+ def http_api
+ @http_api_as_validator ||= Chef::REST.new(Chef::Config[:chef_server_url],
+ Chef::Config[:validation_client_name],
+ Chef::Config[:validation_key])
+ end
+ end
+ end
+end
+
+
diff --git a/spec/unit/api_client/registration_spec.rb b/spec/unit/api_client/registration_spec.rb
new file mode 100644
index 0000000000..513fa43a98
--- /dev/null
+++ b/spec/unit/api_client/registration_spec.rb
@@ -0,0 +1,171 @@
+#
+# Author:: Daniel DeLeo (<dan@opscode.com>)
+# Copyright:: Copyright (c) 2012 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'spec_helper'
+require 'tempfile'
+
+require 'chef/api_client/registration'
+
+describe Chef::ApiClient::Registration do
+ let(:key_location) do
+ path = Tempfile.open("client-registration-key") {|f| f.path }
+ File.unlink(path)
+ path
+ end
+
+ let(:registration) { Chef::ApiClient::Registration.new("silent-bob", key_location) }
+
+ let :private_key_data do
+ File.open(Chef::Config[:validation_key], "rb") {|f| f.read.chomp }
+ end
+
+ before do
+ Chef::Config[:validation_client_name] = "test-validator"
+ Chef::Config[:validation_key] = File.expand_path('ssl/private_key.pem', CHEF_SPEC_DATA)
+ end
+
+ after do
+ File.unlink(key_location) if File.exist?(key_location)
+ Chef::Config[:validation_client_name] = nil
+ Chef::Config[:validation_key] = nil
+ end
+
+ it "has an HTTP client configured with validator credentials" do
+ registration.http_api.should be_a_kind_of(Chef::REST)
+ registration.http_api.client_name.should == "test-validator"
+ registration.http_api.signing_key.should == private_key_data
+ end
+
+ describe "when creating/updating the client on the server" do
+ let(:http_mock) { mock("Chef::REST mock") }
+
+ before do
+ registration.stub!(:http_api).and_return(http_mock)
+ end
+
+ it "creates a new ApiClient on the server using the validator identity" do
+ response = {"uri" => "https://chef.local/clients/silent-bob",
+ "private_key" => "--begin rsa key etc--"}
+ http_mock.should_receive(:post).
+ with("clients", :name => 'silent-bob', :admin => false).
+ and_return(response)
+ registration.create_or_update.should == response
+ registration.private_key.should == "--begin rsa key etc--"
+ end
+
+ context "and the client already exists on a Chef 10 server" do
+ it "requests a new key from the server and saves it" do
+ response = {"name" => "silent-bob", "private_key" => "--begin rsa key etc--" }
+
+ response_409 = Net::HTTPConflict.new("1.1", "409", "Conflict")
+ exception_409 = Net::HTTPServerException.new("409 conflict", response_409)
+
+ http_mock.should_receive(:post).and_raise(exception_409)
+ http_mock.should_receive(:put).
+ with("clients/silent-bob", :name => 'silent-bob', :admin => false, :private_key => true).
+ and_return(response)
+ registration.create_or_update.should == response
+ registration.private_key.should == "--begin rsa key etc--"
+ end
+ end
+
+ context "and the client already exists on a Chef 11 server" do
+ it "requests a new key from the server and saves it" do
+ response = Chef::ApiClient.new
+ response.name("silent-bob")
+ response.private_key("--begin rsa key etc--")
+
+ response_409 = Net::HTTPConflict.new("1.1", "409", "Conflict")
+ exception_409 = Net::HTTPServerException.new("409 conflict", response_409)
+
+ http_mock.should_receive(:post).and_raise(exception_409)
+ http_mock.should_receive(:put).
+ with("clients/silent-bob", :name => 'silent-bob', :admin => false, :private_key => true).
+ and_return(response)
+ registration.create_or_update.should == response
+ registration.private_key.should == "--begin rsa key etc--"
+ end
+ end
+ end
+
+ describe "when writing the private key to disk" do
+ before do
+ registration.stub!(:private_key).and_return('--begin rsa key etc--')
+ end
+
+ it "creates the file with 0600 permissions" do
+ File.should_not exist(key_location)
+ registration.write_key
+ File.should exist(key_location)
+ stat = File.stat(key_location)
+ (stat.mode & 07777).should == 0600
+ end
+
+ it "writes the private key content to the file" do
+ registration.write_key
+ IO.read(key_location).should == "--begin rsa key etc--"
+ end
+ end
+
+ describe "when registering a client" do
+
+ let(:http_mock) { mock("Chef::REST mock") }
+
+ before do
+ registration.stub!(:http_api).and_return(http_mock)
+ end
+
+ it "creates the client on the server and writes the key" do
+ response = {"uri" => "http://chef.local/clients/silent-bob",
+ "private_key" => "--begin rsa key etc--" }
+ http_mock.should_receive(:post).ordered.and_return(response)
+ registration.run
+ IO.read(key_location).should == "--begin rsa key etc--"
+ end
+
+ it "retries up to 5 times" do
+ response_500 = Net::HTTPInternalServerError.new("1.1", "500", "Internal Server Error")
+ exception_500 = Net::HTTPFatalError.new("500 Internal Server Error", response_500)
+
+ http_mock.should_receive(:post).ordered.and_raise(exception_500) # 1
+ http_mock.should_receive(:post).ordered.and_raise(exception_500) # 2
+ http_mock.should_receive(:post).ordered.and_raise(exception_500) # 3
+ http_mock.should_receive(:post).ordered.and_raise(exception_500) # 4
+ http_mock.should_receive(:post).ordered.and_raise(exception_500) # 5
+
+ response = {"uri" => "http://chef.local/clients/silent-bob",
+ "private_key" => "--begin rsa key etc--" }
+ http_mock.should_receive(:post).ordered.and_return(response)
+ registration.run
+ IO.read(key_location).should == "--begin rsa key etc--"
+ end
+
+ it "gives up retrying after the max attempts" do
+ response_500 = Net::HTTPInternalServerError.new("1.1", "500", "Internal Server Error")
+ exception_500 = Net::HTTPFatalError.new("500 Internal Server Error", response_500)
+
+ http_mock.should_receive(:post).exactly(6).times.and_raise(exception_500)
+
+ lambda {registration.run}.should raise_error(Net::HTTPFatalError)
+ end
+
+ end
+
+end
+
+
diff --git a/spec/unit/api_client_spec.rb b/spec/unit/api_client_spec.rb
index b47df6ad5c..6368082815 100644
--- a/spec/unit/api_client_spec.rb
+++ b/spec/unit/api_client_spec.rb
@@ -146,8 +146,6 @@ describe Chef::ApiClient do
before do
Chef::Config[:node_name] = "silent-bob"
Chef::Config[:client_key] = File.expand_path('ssl/private_key.pem', CHEF_SPEC_DATA)
- Chef::Config[:validation_client_name] = "test-validator"
- Chef::Config[:validation_key] = File.expand_path('ssl/private_key.pem', CHEF_SPEC_DATA)
end
after do
@@ -155,23 +153,15 @@ describe Chef::ApiClient do
Chef::Config[:client_key] = nil
end
- let :private_key_data do
- File.open(Chef::Config[:client_key], "rb") {|f| f.read.chomp }
- end
-
it "has an HTTP client configured with default credentials" do
@client.http_api.should be_a_kind_of(Chef::REST)
@client.http_api.client_name.should == "silent-bob"
@client.http_api.signing_key.to_s.should == private_key_data
end
- it "has an HTTP client configured with validator credentials" do
- @client.http_api_as_validator.should be_a_kind_of(Chef::REST)
- @client.http_api_as_validator.client_name.should == "test-validator"
- @client.http_api_as_validator.signing_key.should == private_key_data
- end
end
+
describe "when requesting a new key" do
before do
@http_client = mock("Chef::REST mock")