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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
|
#
# 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/server_api"
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 :destination
attr_reader :name
def initialize(name, destination, http_api: nil)
@name = name
@destination = destination
@http_api = http_api
@server_generated_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::ServerAPI. 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
client = nil
begin
client = api_client(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
client
end
def assert_destination_writable!
if (File.exists?(destination) && !File.writable?(destination)) or !File.writable?(File.dirname(destination))
abs_path = File.expand_path(destination)
raise Chef::Exceptions::CannotWritePrivateKey, "I can't write your private key to #{abs_path} - check permissions?"
end
end
def write_key
::File.open(destination, file_flags, 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", post_data)
@server_generated_private_key = response["private_key"]
response
end
def update
response = http_api.put("clients/#{name}", put_data)
if response.respond_to?(:private_key) # Chef 11
@server_generated_private_key = response.private_key
else # Chef 10
@server_generated_private_key = response["private_key"]
end
response
end
def api_client(response)
return response if response.is_a?(Chef::ApiClient)
client = Chef::ApiClient.new
client.name(name)
client.public_key(api_client_key(response, "public_key"))
client.private_key(api_client_key(response, "private_key"))
client
end
def api_client_key(response, key_name)
if response[key_name]
if response[key_name].respond_to?(:to_pem)
response[key_name].to_pem
else
response[key_name]
end
elsif response["chef_key"]
response["chef_key"][key_name]
end
end
def put_data
base_put_data = { :name => name, :admin => false }
if self_generate_keys?
base_put_data[:public_key] = generated_public_key
else
base_put_data[:private_key] = true
end
base_put_data
end
def post_data
post_data = { :name => name, :admin => false }
post_data[:public_key] = generated_public_key if self_generate_keys?
post_data
end
def http_api
@http_api ||= Chef::ServerAPI.new(Chef::Config[:chef_server_url],
{
:api_version => "0",
:client_name => Chef::Config[:validation_client_name],
:signing_key_filename => Chef::Config[:validation_key],
},
)
end
# Whether or not to generate keys locally and post the public key to the
# server. Delegates to `Chef::Config.local_key_generation`. Servers
# before 11.0 do not support this feature.
def self_generate_keys?
Chef::Config.local_key_generation
end
def private_key
if self_generate_keys?
generated_private_key.to_pem
else
@server_generated_private_key
end
end
def generated_private_key
@generated_key ||= OpenSSL::PKey::RSA.generate(2048)
end
def generated_public_key
generated_private_key.public_key.to_pem
end
def file_flags
base_flags = File::CREAT|File::TRUNC|File::RDWR
# Windows doesn't have symlinks, so it doesn't have NOFOLLOW
if defined?(File::NOFOLLOW) && !Chef::Config[:follow_client_key_symlink]
base_flags |= File::NOFOLLOW
end
base_flags
end
end
end
end
|