diff options
author | danielsdeleo <dan@opscode.com> | 2012-12-18 10:47:24 -0800 |
---|---|---|
committer | danielsdeleo <dan@opscode.com> | 2012-12-18 17:10:05 -0800 |
commit | a61c9865684466adcc9c933f05eebc1090624ea0 (patch) | |
tree | 224e1719aa7a49a2cd3e1e3c7265d204701121b5 | |
parent | 2de707d27251f38790a71043843749a570302aaf (diff) | |
download | chef-a61c9865684466adcc9c933f05eebc1090624ea0.tar.gz |
[CHEF-3689] Extract registration to a class
-rw-r--r-- | lib/chef/api_client.rb | 5 | ||||
-rw-r--r-- | lib/chef/api_client/registration.rb | 110 | ||||
-rw-r--r-- | spec/unit/api_client/registration_spec.rb | 171 | ||||
-rw-r--r-- | spec/unit/api_client_spec.rb | 12 |
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") |