/
chef_layer.rb
300 lines (275 loc) · 11.2 KB
/
chef_layer.rb
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
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
#
# OK so things get a little fishy here, and it's all Opscode's fault ;-)
#
# There's currently no API for setting ACLs. However, if the *client the
# node will run as* is the *client that creates the node*, it is granted the
# correct permissions.
#
# * client exists, node exists: don't need to do anything. We trust that permissions are correct.
# * client absent, node exists: client created, node is fine. We trust that permissions are correct.
# * client absent, node absent: client created, so have key; client creates node, so it has write permissions.
# * client exists, node absent: FAIL.
#
# The current implementation persists the client keys locally to your
# Chef::Config[:client_key_dir]. This is insecure and unmanageable; and the
# node will shortly re-register the key, making it invalide anyway.
#
# If the client's private_key is empty/wrong and the node is absent, it will
# cause an error. in that case, you can:
#
# * create the node yourself in the management console, and
# grant access to its eponymous client; OR
# * nuke the client key from orbit (it's the only way to be sure) and re-run,
# taking all responsibility for the catastrophic results of an errant nuke; OR
# * wait for opscode to open API access for ACLs.
#
#
module Ironfan
module DryRunnable
# Run given block unless in dry_run mode (Ironfan.chef_config[:dry_run]
# is true)
def unless_dry_run
if Ironfan.dry_run?
ui.info(" ... but not really (#{ui.color("dry run", :bold, :yellow)} for server #{name})")
return nil
else
yield
end
end
end
ComputeBuilder.class_eval do
def new_chef_role(role_name, cluster, facet=nil)
chef_role = Chef::Role.new
chef_role.name role_name
chef_role.description "Ironfan generated role for #{[cluster_name, facet_name].compact.join('-')}" unless chef_role.description
chef_role.instance_eval{ @cluster = cluster; @facet = facet; }
@chef_roles << chef_role
chef_role
end
end
ServerSlice.class_eval do
include DryRunnable
def sync_roles
step(" syncing cluster and facet roles")
unless_dry_run do
chef_roles.each(&:save)
end
end
end
#
# Ironfan::Server methods that handle chef actions
#
Server.class_eval do
include DryRunnable
# The chef client, if it already exists in the server.
# Use the 'ensure' method to create/update it.
def chef_client
return @chef_client unless @chef_client.nil?
@chef_client = cluster.find_client(fullname) || false
end
# The chef node, if it already exists in the server.
# Use the 'ensure' method to create/update it.
def chef_node
return @chef_node unless @chef_node.nil?
@chef_node = cluster.find_node(fullname) || false
end
# true if chef client is created and discovered
def chef_client?
chef_client.present?
end
# true if chef node is created and discovered
def chef_node?
chef_node.present?
end
def delete_chef
if chef_node then
step(" deleting chef node", :red)
unless_dry_run do
chef_node.destroy
end
@chef_node = nil
end
if chef_client
step(" deleting chef client", :red)
unless_dry_run do
chef_client.destroy
end
@chef_client = nil
end
end
# creates or updates the chef node.
#
# See notes at top of file for why all this jiggery-fuckery
#
# * client exists, node exists: assume client can update, weep later when
# the initial chef run fails. Not much we can do here -- holler at opscode.
# * client exists, node absent: see if client can create, fail otherwise
# * client absent, node absent: see if client can create both, fail otherwise
# * client absent, node exists: fail (we can't get permissions)
def sync_chef_node
step(" syncing chef node using the server's key")
# force-fetch the node so that we have its full attributes (the discovery
# does not pull all of it back)
@chef_node = handle_chef_response('404'){ Chef::Node.load( fullname ) }
# sets @chef_client if it exists
chef_client
#
case
when @chef_client && @chef_node then _update_chef_node # this will fail later if the chef client is in a bad state but whaddayagonnado
when @chef_client && (! @chef_node) then _create_chef_node
when (! @chef_client) && (! @chef_node) then # create both
ensure_chef_client
_create_chef_node
when (! @chef_client) && @chef_node
raise("The #{fullname} node exists, but its client does not.\nDue to limitations in the Opscode API, if we create a client, it will lack write permissions to the node. Small sadness now avoids much sadness later\nYou must either create a client manually, fix its permissions in the Chef console, and drop its client key where we can find it; or (if you are aware of the consequences) do \nknife node delete #{fullname}")
end
@chef_node
end
def client_key
@client_key ||= Ironfan::ChefClientKey.new("client-#{fullname}", chef_client) do |body|
chef_client.private_key(body) if chef_client.present? && body.present?
cloud.user_data(:client_key => body)
end
end
def chef_client_script_content
return @chef_client_script_content if @chef_client_script_content
return unless cloud.chef_client_script
script_filename = File.expand_path("../../config/#{cloud.chef_client_script}", File.dirname(File.realdirpath(__FILE__)))
@chef_client_script_content = safely{ File.read(script_filename) }
end
def announce_state state
@chef_node.set[:state] = state
end
protected
# Create the chef client on the server. Do not call this directly -- go
# through sync_chef_node.
#
# this is done as the eponymous client, ensuring that the client does in
# fact have permissions on the node
#
# preconditions: @chef_node is set
def _create_chef_node(&block)
step(" creating chef node", :green)
@chef_node = Chef::Node.new
@chef_node.name(fullname)
set_chef_node_attributes
set_chef_node_environment
sync_volume_attributes
unless_dry_run do
chef_api_server_as_client.post_rest('nodes', @chef_node)
end
end
# Update the chef client on the server. Do not call this directly -- go
# through create_or_update_chef_node.
#
# this is done as the eponymous client, ensuring that the client does in
# fact have permissions on the node.
#
# preconditions: @chef_node is set
def _update_chef_node
step(" updating chef node", :blue)
set_chef_node_attributes
set_chef_node_environment
sync_volume_attributes
unless_dry_run do
chef_api_server_as_admin.put_rest("nodes/#{@chef_node.name}", @chef_node)
end
end
def sync_volume_attributes
composite_volumes.each do |vol_name, vol|
chef_node.normal[:volumes] ||= Mash.new
chef_node.normal[:volumes][vol_name] = vol.to_mash.compact
end
end
def set_chef_node_attributes
step(" setting node runlist and essential attributes")
@chef_node.run_list = Chef::RunList.new(*@settings[:run_list])
@chef_node.normal[:organization] = organization if organization
@chef_node.normal[:permanent] = cloud.permanent if cloud.permanent
@chef_node.normal[:cluster_name] = cluster_name
@chef_node.normal[:facet_name] = facet_name
@chef_node.normal[:facet_index] = facet_index
end
def set_chef_node_environment
@chef_node.chef_environment(environment.to_s) if environment.present?
end
#
# Don't call this directly -- only through ensure_chef_node_and_client
#
def ensure_chef_client
step(" ensuring chef client exists")
return @chef_client if chef_client
step( " creating chef client", :green)
@chef_client = Chef::ApiClient.new
@chef_client.name(fullname)
@chef_client.admin(false)
#
# ApiClient#create sends extra params that fail -- we'll do it ourselves
# purposefully *not* catching the 'but it already exists' error: if it
# didn't show up in the discovery process, we're in an inconsistent state
unless_dry_run do
response = chef_api_server_as_admin.post_rest("clients", { 'name' => fullname, 'admin' => false, 'private_key' => true })
client_key.body = response['private_key']
end
client_key.save
@chef_client
end
def chef_api_server_as_client
return @chef_api_server_as_client if @chef_api_server_as_client
unless File.exists?(client_key.filename)
raise("Cannot create chef node #{fullname} -- client #{@chef_client} exists but no client key found in #{client_key.filename}.")
end
@chef_api_server_as_client = Chef::REST.new(Chef::Config[:chef_server_url], fullname, client_key.filename)
end
def chef_api_server_as_admin
@chef_api_server_as_admin ||= Chef::REST.new(Chef::Config[:chef_server_url])
end
# Execute the given chef call, but don't explode if the given http status
# code comes back
#
# @return chef object, or false if the server returned a recoverable response
def handle_chef_response(recoverable_responses, &block)
begin
block.call
rescue Net::HTTPServerException => e
raise unless Array(recoverable_responses).include?(e.response.code)
Chef::Log.debug("Swallowing a #{e.response.code} response in #{self.fullname}: #{e}")
return false
end
end
#
# The below *was* present but was pulled from the API by opscode for some reason (2011/10/20)
#
# # The client is required to have these permissions on its eponymous node
# REQUIRED_PERMISSIONS = %w[read create update]
#
# #
# # Verify that the client has required _acl's on the node.
# #
# # We don't raise an error, just a very noisy warning.
# #
# def check_node_permissions
# step(" ensuring chef node permissions are correct")
# chef_server_rest = Chef::REST.new(Chef::Config[:chef_server_url])
# handle_chef_response('404') do
# perms = chef_server_rest.get_rest("nodes/#{fullname}/_acl")
# perms_valid = {}
# REQUIRED_PERMISSIONS.each{|perm| perms_valid[perm] = perms[perm] && perms[perm]['actors'].include?(fullname) }
# Chef::Log.debug("Checking permissions: #{perms_valid.inspect} -- #{ perms_valid.values.all? ? 'correct' : 'BADNESS' }")
# unless perms_valid.values.all?
# ui.info(" ************************ ")
# ui.info(" ")
# ui.info(" INCONSISTENT PERMISSIONS for node #{fullname}:")
# ui.info(" The client[#{fullname}] should have permissions for #{REQUIRED_PERMISSIONS.join(', ')}")
# ui.info(" Instead, they are #{perms_valid.inspect}")
# ui.info(" You should create the node #{fullname} as client[#{fullname}], not as yourself.")
# ui.info(" ")
# ui.info(" Please adjust the permissions on the Opscode console, at")
# ui.info(" https://manage.opscode.com/nodes/#{fullname}/_acl")
# ui.info(" ")
# ui.info(" ************************ ")
# end
# end
# end
end
end