summaryrefslogtreecommitdiff
path: root/lib/chef/api_client/registration.rb
blob: 354267f733bb36f4e10d4649762f1fc3154b151a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#
# 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.
      #--
      # If client creation fails with a 5xx, it is retried up to 5 times. These
      # retries are on top of the retries with randomized exponential backoff
      # built in to Chef::REST. The retries here are a workaround for failures
      # caused by resource contention in Hosted Chef when creating a very large
      # number of clients simultaneously, (e.g., spinning up 100s of ec2 nodes
      # at once). Future improvements to the affected component should make
      # these retries unnecessary.
      def run
        assert_destination_writable!
        retries = Config[:client_registration_retries] || 5
        begin
          create_or_update
        rescue Net::HTTPFatalError => e
          # HTTPFatalError implies 5xx.
          raise if retries <= 0
          retries -= 1
          Chef::Log.warn("Failed to register new client, #{retries} tries remaining")
          Chef::Log.warn("Response: HTTP #{e.response.code} - #{e}")
          retry
        end
        write_key
      end

      def assert_destination_writable!
        if (File.exists?(destination) && !File.writable?(destination)) or !File.writable?(File.dirname(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